Mythruna
March 28, 2024, 07:53:01 AM *
Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length
News: Welcome to the new forums. See "Announcements" for a note for new users.
 
   Home   Help Search Login Register  
Pages: [1]
  Print  
Author Topic: Scripted Objects: WIP status...  (Read 14668 times)
pspeed
Administrator
Hero Member
*****
Posts: 5612



View Profile
« on: February 27, 2012, 11:07:40 AM »

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. Smiley  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 Blueprints

Some 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:
Code:
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.

Models

This 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:
Code:
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 Templates

This 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:
Code:
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"...

Code:
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:
Code:
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 Use

Objects 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:
Code:
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:
Code:
    // 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.
Logged
pspeed
Administrator
Hero Member
*****
Posts: 5612



View Profile
« Reply #1 on: March 05, 2012, 03:43:04 AM »

Posting this here basically unedited because I said I would in the video I just put up. Smiley

Video here:
http://www.youtube.com/watch?v=0R82bKkHtjY

Code:

/**
 *  Testing and demonstrating how action-able
 *  scripted objects can be added to the game.
 */
 
import mythruna.MaterialType;
import mythruna.es.*;
import mythruna.item.*;
 
loadBlueprint( "firewood.bp" );
 
createModel( "logs", 0, "firewood.bp" )
createModel( "campfire", 0, "campfire.bp" )
createModel( "match", 2, "match.bp", Scale.Item );
createModel( "bucket", 0, "bucket.bp", Scale.Item ); 
createModel( "bucket-sand", 0, "bucket-sand.bp", Scale.Item ); 
createModel( "bucket-water", 0, "bucket-water.bp", Scale.Item ); 


// Now create the object templates (classes) for some items

def firewood = objectTemplate( "Firewood" ) {

    setModel( "logs" );
    addParents( "BaseItem" );
   
    action( ":light" ) { self, tool ->
        def obj = entityObject(self);
        if( obj.locals.getInt("lit") == 0 ) {
            obj.setModel("campfire");
            obj.locals.setInt("lit", 1);
            echo( "A fire crackles to life." );
        } else {
            echo( "These logs are already lit." );
        }
    }
   
    action( ":douse" ) { self, tool ->
        def obj = entityObject(self);
        if( obj.locals.getInt("lit") == 0 ) {
            echo( "The fire is not lit." );
        } else {
            obj.setModel("logs");
            obj.locals.setInt("lit", 0);
            echo( "The fire goes out." );
        }           
    }
}

def match = objectTemplate( "Match" ) {
 
    setModel( "match" );
    addParents( "BaseTool" );
   
    defaultAction( "Light" ) { self, hit ->   
        if( hit == null || hit.object == null )
            return;
         
        entityObject(hit.object).execute( ":light", self );
       
    }.onlyIf() { self, hit ->   
 
        // Right now only if it has a ":light" action
        return entityObject(hit.object).hasAction( ":light" );
    }
}

def bucket = objectTemplate( "Bucket" ) {
 
    setModel( "bucket" );
    addParents( "BaseTool" );

    action( ":empty" ) { self ->       
        def obj = entityObject(self);
       
        // Empty the bucket           
        obj.locals.setInt("filled", 0);
        obj.setModel("bucket");       
    }

    defaultAction( "Empty" ) { self, hit ->   
       
        entityObject(self).execute( ":empty" );
        echo "The bucket is now empty.";
               
    }.onlyIf() { self, hit ->   

        if( hit == null || hit.object != null )
            return false;

        def material = hit.material;
        if( material != MaterialType.WATER && material != MaterialType.SAND )
            return false;
           
        // Else make sure we are trying to dump the material
        // back into its own type           
        def obj = entityObject(self);
        int holds = obj.locals.getInt("filled");
        return material.id == holds;
    }
   
    defaultAction( "Fill" ) { self, hit ->   
       
        def material = hit.material;
        def obj = entityObject(self);
        int holds = obj.locals.getInt("filled");
         
        if( holds != 0 ) {
            echo "The bucket is already full.";
            return;
        }
       
        // Else the bucket is empty so we can fill it
        if( material == MaterialType.WATER ) {
            obj.locals.setInt("filled", material.id);
            obj.setModel( "bucket-water" );
            echo "You filled the bucket with water."
        } else if( material == MaterialType.SAND ) {
            obj.locals.setInt("filled", material.id);
            obj.setModel( "bucket-sand" );
            echo "You fill the bucket with sand."
        }
               
    }.onlyIf() { self, hit ->   
        if( hit == null || hit.object != null )
            return false;
           
        def material = hit.material;
        return material == MaterialType.WATER || material == MaterialType.SAND;       
    }
   
    defaultAction( "Douse" ) { self, hit ->
               
        entityObject(self).execute( ":empty" );
        entityObject(hit.object).execute( ":douse", self );       
       
    }.onlyIf() { self, hit ->
        if( hit == null )
            return false;

        // If the target can't be doused, then we won't
        // worry about it
        if( !entityObject(hit.object).hasAction( ":douse" ) )
            return false;
 
        // If the target isn't firewood then we don't
        // care (also filters nulls)
        //if( !firewood.isInstance(hit.object) )
        //    return false;
           
        // See if we even contain anything
        return entityObject(self).locals.getInt("filled") != 0;
    }
}


on( [playerJoined] ) {
    type, event ->
 
    println "Making sure the player" + player + " has some standard test tools...";

    def items = getContainedItems( player );
    def classes = items.collect {
        it[ClassInstance.class].classEntity;
    }
 
    if( !classes.contains(firewood.classEntity) ) {
        println "Need to create firewood in the player's inventory...";
       
        firewood.createInstanceOn( player );
    }
       
    if( !classes.contains(match.classEntity) ) {
        println "Need to create a match in the player's inventory...";
       
        match.createInstanceOn( player );
    }

    if( !classes.contains(bucket.classEntity) ) {
        println "Need to create a bucket in the player's inventory...";
        bucket.createInstanceOn( player );
    }       
}   

I'll swing back and add more explanation another time.
Logged
BenKenobiWan
Friendly Moderator
Donators
Hero Member
***
Posts: 674


Jesus loves you!


View Profile
« Reply #2 on: March 05, 2012, 11:19:57 AM »

Where are you supposed to put all this code? And how do you define actions? (like, burn = put fire on top)
Logged
FutureB
Donators
Hero Member
***
Posts: 512


RAWR


View Profile
« Reply #3 on: March 05, 2012, 11:28:08 AM »

Hehe cool video when you see mythruna like that it seems like its coming along very nicly
Logged


Say the opposite of these words:
1)Always.
2)Coming.
3)From.
4)Take.
5)Me.
6)Down.
pspeed
Administrator
Hero Member
*****
Posts: 5612



View Profile
« Reply #4 on: March 05, 2012, 12:39:33 PM »

Where are you supposed to put all this code? And how do you define actions? (like, burn = put fire on top)

For single player, this would go in mods/scripts or mod/scripts/foo along with the .bp files it references.  I'll post about that when I release since it doesn't actually work until then.

The actions are defined in the code I posted.

Code:
def match = objectTemplate( "Match" ) {
 
    setModel( "match" );
    addParents( "BaseTool" );
   
    defaultAction( "Light" ) { self, hit ->   
        if( hit == null || hit.object == null )
            return;
         
        entityObject(hit.object).execute( ":light", self );
       
    }.onlyIf() { self, hit ->   
 
        // Right now only if it has a ":light" action
        return entityObject(hit.object).hasAction( ":light" );
    }
}

That snippet is what defines the "match" object.  It has a "Light" default action that can run on anything with a ":light" action.

Firewood has a ":light" action and so the match will work on it.

This is not the way real fire will work but was an example I used because it's relatively easy to understand.

Actions that are preceded with a ":" are not exposed to the user.  defaultActions() are the actions that are tried in order (until one has an onlyIf() that returns true) when the user clicks on an object.  In the release, right clicking on an object would bring up a list of all available actions, not just the default ones, that _don't_ have a ":" at the beginning.  The ":" actions are meant to be used internally by the scripts.

The behavior for what happens when you light (":light") the firewood is here:
Code:
action( ":light" ) { self, tool ->
        def obj = entityObject(self);
        if( obj.locals.getInt("lit") == 0 ) {
            obj.setModel("campfire");
            obj.locals.setInt("lit", 1);
            echo( "A fire crackles to life." );
        } else {
            echo( "These logs are already lit." );
        }
    }

It checks it it's already lit.  If not then it sets it to the "campfire" graphic (logs that have fire on them) and sets the "lit" attribute... and then sends a message to the user.

Real fire will be more fluid and based on materials.  You won't have to specifically script it... it will just jump from object to object, burn down forests, collapse houses, etc..  This is just an example of how a modder can make their own custom objects and have them interact.
Logged
BenKenobiWan
Friendly Moderator
Donators
Hero Member
***
Posts: 674


Jesus loves you!


View Profile
« Reply #5 on: March 05, 2012, 01:09:18 PM »

Okay, I kinda skimmed the code, and missed that. It looks interesting.
Logged
pspeed
Administrator
Hero Member
*****
Posts: 5612



View Profile
« Reply #6 on: March 06, 2012, 12:31:01 AM »

Just keeping this thread up-to-date... with some groovy magic I was able to collapse things a bit to no longer require the "entityObject()" wrapping all over the place:

Code:
/**
 *  Testing and demonstrating how action-able
 *  scripted objects can be added to the game.
 */
 
import mythruna.MaterialType;
import mythruna.es.*;
import mythruna.item.*;
 
loadBlueprint( "firewood.bp" );
 
createModel( "logs", 0, "firewood.bp" )
createModel( "campfire", 0, "campfire.bp" )
createModel( "match", 2, "match.bp", Scale.Item );
createModel( "bucket", 0, "bucket.bp", Scale.Item ); 
createModel( "bucket-sand", 0, "bucket-sand.bp", Scale.Item ); 
createModel( "bucket-water", 0, "bucket-water.bp", Scale.Item ); 


// Now create the object templates (classes) for some items

def firewood = objectTemplate( "Firewood" ) {

    setModel( "logs" );
    addParents( "BaseItem" );
   
    action( ":light" ) { self, tool ->
        if( self.locals.getInt("lit") == 0 ) {
            self.setModel("campfire");
            self.locals.setInt("lit", 1);
            echo( "A fire crackles to life." );
        } else {
            echo( "These logs are already lit." );
        }
    }
   
    action( ":douse" ) { self, tool ->
        if( self.locals.getInt("lit") == 0 ) {
            echo( "The fire is not lit." );
        } else {
            self.setModel("logs");
            self.locals.setInt("lit", 0);
            echo( "The fire goes out." );
        }           
    }
}

def match = objectTemplate( "Match" ) {
 
    setModel( "match" );
    addParents( "BaseTool" );
   
    defaultAction( "Light" ) { self, hit ->   
        hit.object.execute( ":light", self );
       
    }.onlyIf() { self, hit ->   
        if( hit == null || hit.object == null )
            return false;
 
        // Right now only if it has a ":light" action
        return hit.object.hasAction( ":light" );
    }
}

def bucket = objectTemplate( "Bucket" ) {
 
    setModel( "bucket" );
    addParents( "BaseTool" );

    action( ":empty" ) { self ->       
        // Empty the bucket           
        self.locals.setInt("filled", 0);
        self.setModel("bucket");       
    }

    defaultAction( "Empty" ) { self, hit ->   
       
        self.execute( ":empty" );
        echo "The bucket is now empty.";
               
    }.onlyIf() { self, hit ->   

        if( hit == null || hit.object != null )
            return false;

        def material = hit.material;
        if( material != MaterialType.WATER && material != MaterialType.SAND )
            return false;
           
        // Else make sure we are trying to dump the material
        // back into its own type           
        int holds = self.locals.getInt("filled");
        return material.id == holds;
    }
   
    defaultAction( "Fill" ) { self, hit ->   
       
        def material = hit.material;
        int holds = self.locals.getInt("filled");
         
        if( holds != 0 ) {
            echo "The bucket is already full.";
            return;
        }
       
        // Else the bucket is empty so we can fill it
        if( material == MaterialType.WATER ) {
            self.locals.setInt("filled", material.id);
            self.setModel( "bucket-water" );
            echo "You filled the bucket with water."
        } else if( material == MaterialType.SAND ) {
            self.locals.setInt("filled", material.id);
            self.setModel( "bucket-sand" );
            echo "You fill the bucket with sand."
        }
               
    }.onlyIf() { self, hit ->   
        if( hit == null || hit.object != null )
            return false;
           
        def material = hit.material;
        return material == MaterialType.WATER || material == MaterialType.SAND;       
    }
   
    defaultAction( "Douse" ) { self, hit ->
 
        self.execute( ":empty" );
        hit.object.execute( ":douse", self );       
       
    }.onlyIf() { self, hit ->
   
        if( hit == null || hit.object == null )
            return false;

        // If the target can't be doused, then we won't
        // worry about it
        if( !hit.object.hasAction( ":douse" ) )
            return false;
 
        // See if we even contain anything
        return self.locals.getInt("filled") != 0;
    }
}


on( [playerJoined] ) {
    type, event ->
 
    println "Making sure the player" + player + " has some standard test tools...";

    def items = getContainedItems( player );
    def classes = items.collect {
        it[ClassInstance.class].classEntity;
    }
 
    if( !classes.contains(firewood.classEntity) ) {
        println "Need to create firewood in the player's inventory...";
       
        firewood.createInstanceOn( player );
    }
       
    if( !classes.contains(match.classEntity) ) {
        println "Need to create a match in the player's inventory...";
       
        match.createInstanceOn( player );
    }
 
    if( !classes.contains(bucket.classEntity) ) {
        println "Need to create a bucket in the player's inventory...";
        bucket.createInstanceOn( player );
    }       
}   

Still have a few more things I need to do before I can release it but hopefully tomorrow.  Some life-related things have gotten in the way this week.
Logged
randomprofile
Global Moderator
Sr. Member
*****
Posts: 265


View Profile WWW
« Reply #7 on: March 06, 2012, 02:58:53 AM »

Life?!?!?! As in mythruna players will have health!?!?!? Cheesy na jkz I understand what you meant Smiley
Logged
BenKenobiWan
Friendly Moderator
Donators
Hero Member
***
Posts: 674


Jesus loves you!


View Profile
« Reply #8 on: March 06, 2012, 09:47:46 AM »

What do the numbers mean? ("logs", 0...)
Logged
pspeed
Administrator
Hero Member
*****
Posts: 5612



View Profile
« Reply #9 on: March 06, 2012, 12:39:57 PM »

What do the numbers mean? ("logs", 0...)

This was covered in the first post, I think:
Quote
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.

It's a way for a mod writer to upgrade their models if the game already has their mod installed (ie: They updated the mod and want to make sure new objects get the new blueprint).

I only add the blueprint to the database the first time I see the name+version.  Next time you run the game, if the name+version is the same then I reuse the blueprint that was imported previously.
Logged
BenKenobiWan
Friendly Moderator
Donators
Hero Member
***
Posts: 674


Jesus loves you!


View Profile
« Reply #10 on: March 06, 2012, 03:05:28 PM »

Okay, thanks. Sorry for not reading carefully Sad
Logged
pspeed
Administrator
Hero Member
*****
Posts: 5612



View Profile
« Reply #11 on: March 06, 2012, 03:20:09 PM »

Okay, thanks. Sorry for not reading carefully Sad

No, it's cool.  It's a lot of information and stuff is easy to miss.  I didn't mean to sound snarky... just pointing to the original information in context in case it was useful.
Logged
Pages: [1]
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.20 | SMF © 2013, Simple Machines Valid XHTML 1.0! Valid CSS!