I made some progress on object-scripting this weekend, despite intermittent zombie attacks from the "day job" release I was supposed to cut on Friday (zombie attacks: kept coming back from the dead to steal my time).
I felt like posting some progress on what I've done so far... partially to keep people who care in the loop and partially to help resolidify where I am before continuing.
All of this is subject to change, of course.
So far, I have added:
- a way to easily important and export blueprints from scripts including a modders tool that can be used to export blueprints from in-game
- a way to logically associate a blueprint with a name during script init time... in a way that the blueprint can also be updated later.
- a way to create object "classes" to include their default blueprint, user-invokable actions, script-only invokable actions, etc.
- a way to instantiate objects into a players inventory
- and a way to execute actions on an object
(This is not including all of the "game mode UI" work that was done up to this point to help support the control interface.)
Next task is to actually hook the player-side tools up to the scripted actions. But I will explain a little about what I've done so far in the other areas...
Export BlueprintsSome internal static methods were created but the most useful one in this context is the ability to export a blueprint to a file based on its ID. It's probably easier just to show the modder script I made to let me right click on objects to export them:
import mythruna.db.*;
import mythruna.es.*;
import mythruna.script.*;
action( group:"ObjectModding", name:"Export\nBlueprint", type:ActionType.NameComponent ) {
source, component ->
def info = source[ModelInfo.class];
long id = info.blueprintId;
def data = world.getBlueprint(id);
def outFile = new File( component.component.name + ".bp" );
DefaultBlueprintDatabase.exportBlueprint( data, outFile );
echo "Wrote blueprint:" + id + " to:" + outFile;
}
entityAction( name:"Alternate Action" ) {
// Get the regular object radial menu by invoking the original Alternate Action
// from before our override
original(it);
type = it[ModelInfo.class];
if( type != null ) {
// It's a placeable objects, so we'll add an export option
name = it.name;
def existing = player[RadialActions.class];
refs = [];
// Add some modder actions
refs += actions.getRefs( "ObjectModding" );
player << new RadialActions( refs, existing );
}
}
With that, right-clicking on an object will show an additional "Export Blueprint" option. When clicked it will prompt you for a name for the file.
ModelsThis one is easy. If you have blueprints relative to your init script then you can easily create logically named models for them. Here I create three different models... the last one custom-configures the model if it has to load it again:
createModel( "logs", 0, "firewood.bp" )
createModel( "campfire", 0, "campfire.bp" )
createModel( "match", 2 ) {
// Run if the model and version doesn't exist yet.
// Load a specific blueprint
def bp = loadBlueprint("match.bp");
// Set it's scale to something custom
bp.scale = 0.05f;
// Add it to the world
// return the ID for the caller
return createBlueprint( bp );
}
The second number is the version. Internally, the game assigns an entity ID to the named blueprint so that the next time you run the game it doesn't import the blueprint again. Unless you increase the version. Increasing the version is a way to get the file to re-import the next time the game is run and is useful if you have to reset things about the blueprint or otherwise update the model. Note: any existing objects will still have the old blueprint in any case.
In the last example, I've intercepted the creation of the blueprint to do custom processing. In this case, I've changed the scale to what "item scale" will likely be. I could have also manually generated a blueprint, combined multiple ones together, etc... all in a way that only executes if the model wasn't already defined at that version level.
Object TemplatesThis is the most critical part of everything. It's how you tie scripts, models, default data, etc. together into an object that can be created in game to place in the world or player's inventory.
These examples will be a bit contrived, so bear with me. The simplest form, a name and some actions:
objectTemplate( "Wood" ) {
action( "Burn" ) {
echo( "Fire! Fire! Fire" );
}
}
Objects of type "Wood" held by a player would present a "Burn" action that could be performed on the wood... that in this case simply echoes to the scripting console.
That template has no model and so really can't be created on its own. I've created it as a contrived example of a "parent type"...
objectTemplate( "Book" ) {
addParents( "Wood" )
setModel( "book" )
action( "Read" ) {
echo( "Four score and seven years ago... " + it );
}
action( "Burn" ) {
echo( "That wouldn't be very nice." );
}
}
Here we've created a "Book" type that has a model and inherits the behaviors and properties from the "Wood" type. The "Burn" action will override the "Burn" action in the "Wood" type but there will be a way to call the parent type's version also (right now the scripting system actually runs all of the "Burn" actions that it finds without stopping... this may end up being the default behavior for other reasons in which case overriding will have to be specifically set for an action).
The other thing is that any init script can continue to add things to existing types... so later on or in some other init script, I could have the following:
objectTemplate( "Book" ) {
action( "Hide" ) {
echo( "It doesn't fit down your pants." );
}
}
...and that would add an additional "Hide" behavior.
Right now, actions that are preceded with a colon (":") are not exposed directly to the player. These are actions that scripts can call on objects. For example, it's probably not appropriate to expose "Burn" to the player directly in this case. It's more likely it would be ":burn" and that some other tool (a match maybe) would let you burn it.
Important Note: in the final game, fire will not be done this way at all as there will be an actual fire system that propagates, is aware of materials, etc... that being said, if objects need specific burning behavior, the fire system will probably periodically call their ":burn" actions. (maybe they explode at a certain temperature, or are fire resistant, or something else that script will want to do in the presence of fire... "cook and taste yummy?")
Object UseObjects can be created on a player (or any container) but calling createInstanceOn(player) on the template itself. The template can be retrieved (right now) just be reinvoking objectTemplate("myClass") but there will be a specific look-up for it before release. (The down side of using objectTemplate() is that it won't throw an error if you get the name wrong it would just create the class on the spot.)
You can also just snag a reference when the object is created. This script shows creating a class and making sure that every player has at least one of them in their inventory:
def match = objectTemplate( "Match" ) {
setModel( "match" );
action( "Light" ) {
echo( "Trying to light:" + it );
}
}
on( [playerJoined] ) {
type, event ->
def items = getContainedItems( player );
def classes = items.collect {
it[ClassInstance.class].classEntity;
}
if( !classes.contains(match.classEntity) ) {
println "Need to create a match in the player's inventory...";
match.createInstanceOn( player );
}
}
Once you have an object, it is easy to call actions on it from scripts:
// Grab an object to call things on for a 'tool' entity
def obj = entityObject( tool )
// Call an action
obj.execute( "Burn" );
// Call it with a parameter
obj.execute( "Burn", hit );
// Could have also just done this:
entityObject(tool).execute( "Burn" );
Anyway... that's where I am. Next steps are hooking up the player invoked actions which will consist of determining defaults actions for a tool and target, popping up radials for all actions, etc..
Hopefully a release will be ready soon for some eager scripters to play with.