wickedworx

February 7, 2011

A simple scripting system (as used by EGV)

Filed under: Technical — wickedworx dev clone 1 @ 6:40 pm

Summary

Excruciating Guitar Voyage was a game driven by content, as a lot of adventure games are. A key component in the arsenal of any content/story driven game developer is the ability to script game and world events.

A lot of developers, when talking about scripting, will think of LUA, JavaScript and AngelScript. But it is worth me saying – these may be “scripting languages”, but that doesn’t mean they are necessarily the right tool for the task of scripting events in your game.

For EGV, we wanted a scripting system which was higher level than what a pre-existing scripting language would provide.. a system which gave us direct control of the game. Another key element was that we wanted to be able to switch between a script ‘blocking’ the operation of the main game, and a script running alongside the main game per command… but I’ll get on to that later.

WELCOME TO: “EGV’S MEGA SIMPLE, EXTENDABLE, FLEXIBLE SCRIPTING SYSTEM PART 1”


This example is all in C#. I have implemented this exact same system in C++ on previous occasions…so, uh, write it in whatever language you want – the concept is the same.

Our requirements for this system:

  • High level – to the point it is almost ‘game specific’, and that game specific commands can be added
  • Mixed blocking/passing (similar to ‘mixed asynchronous’) behaviour
  • Simple and quick to use, reasonable to debug
  • Fast to parse (we pre-cache all potential scripts on level load)
  • Not exclusively for cutscenes – player doesn’t have to stop playing while scripts are running
  • Conditions, and branching (“if” support) would be ideal*

OK – so a few assumptions of the game:

  • All important game objects have a “tag” name.
  • There is a “GameResource” which contains a reference to pretty much any game system we may need
  • Game Objects in EGV use a component system. This shifts behaviours in to separate components, making it ideal for this kind of scripting.

* Branching/conditionals to be covered in next post.. we’ve already got it implemented, but I’ve taken it out from this code to avoid too much early bloat in the tutorial. Next post will go about adding it back in!

Implementation

Our scripting system consists of 3 main classes:

  • ScriptManager – holds a list of all currently running scripts, updates as necessary
  • Script – a script. holds a list of all the commands it must run, updates as necessary
  • IScriptItem – class off which script commands can inherit

class ScriptManager    
{        
    GameResource m_gameResource;        
    List<Script> m_runningScripts = new List<Script>();        
    List<Script> m_toAdd = new List<Script>();

Notice I’ve got two lists: “Running Scripts” and “To Add”. The second list is there in case one of our scripts launches another scripts while running – it means we can get around modifying the “Running Scripts” list while it is being iterated through. Each frame, scripts from the “To Add” list are copied to the “Running Scripts” list, and the “To Add” list is subsequently cleared.

See ScriptManager here http://pastebin.com/hg3nBMR0

So, the idea is:

  1. Event happens in game, causes a script to be loaded
  2. ScriptManager creates a new script, using the data from the .xml file
  3. Script reads data from xml file and creates an IScriptItem for each command* within it
  4. The Script now contains a list of IScriptItems and has a “current index” variable which stores which script command it is currently running
  5. Next frame, ScriptManager calls update on the new script
  6. Script calls ‘run’ on the first IScriptItem in the list. If/when this command is finished, it moves on to the next command.

Behaviour

So, there are two different situations which we have to take in to account:

  • Situation A) We want to run a number of script commands all in one frame. e.g. we want to remove the banana object, add an explosion there, play an explosion sound, spawn some particles. This all has to be done in one frame – if this were spread out over four frames (e.g. one frame per command) – it would look ridiculous.
  • Situation B) We want one script command to keep running for a number of frames. e.g. we want to wait 2 seconds between blowing up the bin and a character commenting on the fact the banana has exploded.

This is the interface which I came up with to solve these two situations:

public virtual void run(float dt) {}        
public virtual bool shouldUpdateGame() { return false; }        
public virtual bool isComplete() { return true; }

Note, I decided against a “pure” interface and have opted to add ‘defaults’ to the functions “shouldUpdateGame” and “isComplete”, as the majority of script commands will end immediately and not need a game update.

So here’s the idea in bullet-point go-to logic:

  • point A) Game is updating. Game updates  full frame, then updates ScriptManager.
  • point B) Script Manager runs, updates script, RUN is called on the current ScriptItem (using our index!)
  • shouldUpdateGame() is called and the result stored
  • isComplete() is called. If this returns true then the current ScriptItem index is increased
  • if the result stored from shouldUpdateGame is ‘true’, we’re back to “point A”.. if it is false, don’t update the game – essentially carry on from “point B” without letting another frame pass — UNLESS this script has entirely finished, in which case, we’re done anyway!

Here’s the logic for that:

public bool run(float dt)     //   Script.run(float dt)
{            
    bool shouldBreak = false;            
    while ( true )	        
    {		        
        if ( m_itemIndex >= (int)m_items.Count )		        
        {			        
            return false;	// return false - this script is finished	        
        }		        
        if ( shouldBreak )		        
        {			        
            return true;        // return true - let game carry on, but this script has more to do next frame
        }
                               // Continue on - run the next item        
        m_items[m_itemIndex].run(dt);		        
        shouldBreak = m_items[m_itemIndex].shouldUpdateGame();		        
        if ( m_items[m_itemIndex].isComplete() )		        
        {			        
            ++m_itemIndex;		        
        }
    }        
}

Loading

So, now we have the logic to run a linear script – we just need a few implementations of commands and a way to build a script!

The format of one of our scripts is as follows:

<script>
<command_name parameter1=”example parameteranother_parameter=”10” />
</script>

In our framework, across all our systems we have a unified XML load system. This converts XML (like above) to a tree of DataNodes. DataNodes have a name, an optional value and an optional list of child nodes.
In the above example, we’d end up with a DataNode with a name “script”, with a child DataNode (named “command_name”), with two child DataNodes (named “parameter1” and “another_parameter”), which have values of “example parameter” and “10” respectively.

Let’s add a couple more functions to our IScriptItem:

class IScriptItem    
{        
    GameResource m_gameResource;        
    public virtual void run(float dt) {}        
    public virtual bool shouldUpdateGame() { return false; }        
    public virtual bool isComplete() { return true; }        
    protected GameResource getGameResource()        // protected, access to GameResource
    {            
        return m_gameResource;        
    }        
    public void baseSetup(GameResource gr)        // set up base (don't rely on ScriptItem implementations to remember this)
    {            
        m_gameResource = gr;        
    }        
    public virtual void setup(DataNode node)        // set up function for overriding - passed a DataNode
    {
    }    
}

I decided to keep the name “IScriptItem” despite it not being a pure interface.

A Script is constructed from an xml file, which is converted to a DataNode. It then constructs all its script commands using the list of children in it’s DataNode. Each command in the script is build from a DataNode (and its children)

class Script    
{        
    List<IScriptItem> m_items = new List<IScriptItem>();        
    GameResource m_gameResource;        
    int m_itemIndex;        
    String m_filename;        
    bool m_shouldDelete = false;

    public Script(String filename, GameResource resource)
    {            
        m_filename = filename;            
        m_gameResource = resource;            
        DataNode rootNode = DataNodeResource.getDataNode(@"Content\scripts\" + filename);            
        m_itemIndex = 0;            
        addFromNode(rootNode);            
    }
    void addFromNode(DataNode node)        
    {            
        int nodeCount = node.getNodeCount();            
        for (int nodeIndex = 0; nodeIndex < nodeCount; ++nodeIndex)            
        {                
            DataNode thisNode = node.getNode(nodeIndex);                
            String itemType = thisNode.getName();
...

So, here we are iterating through command nodes, and getting a string from each – “itemType”. Using this itemType we can determine which class (inheriting from IScriptItem) to create an instance of. In our current code, we use Generics and a Registered Builder pattern to avoid a huge series of “if-else”.. but for this example, it’s probably easier to go down the “if-else” route! (There’s enough examples of using Generics to build objects from Strings in C# on the internet already!)

...
            IScriptItem itemToAdd = null;
            if ( itemType == "wait" ) // replace these else-ifs with your favourite String -> Object building logic
            {
                itemToAdd = new ScriptItems.Wait();
            }
            else if ( itemType == "play_cue" )
            {
                itemToAdd = new ScriptItems.PlayCue();
            }

            if (itemToAdd != null)
            {
                itemToAdd.baseSetup(m_gameResource);   // set up base
                itemToAdd.setup(thisNode); // pass data to item set up
                m_items.Add(itemToAdd);
            }
            else 
            {
// throw an exception or do some error thing here - you've tried to use a command in your script which doesn't exist!
            }

Full Script class here http://pastebin.com/DbX9SGUz

Command Logic

The great thing is – every time we want to add a new command, we just add a new class inheriting from
IScriptItem. EGV has about 30 commands, some of them very specific (“walk_to” and “speak”), and some of them more generic (“spawn_object”, “remove_object”, “set_animation”).
Adding new commands doesn’t seem to happen very often, and when it does – it’s usually a fairly quick addition…most of our commands involve finding a GameObject in the world, and calling a function on them.**

So, that should deal with adding two types of item – a “wait” and a “play_cue”. How do these look?

Wait

Class

class Wait        :        
IScriptItem    
{        
    float m_delay;        
    public override void setup(DataNode node)        
    {  
        // read in the delay required         
        m_delay = node.getNode("time").getValueF();        
    }        
    public override bool isComplete()        
    {            
        // this command isn't finished until the delay is over
        if (m_delay < 0)            
        {                
            return true;            
        }            
        return false;        
    }        
    public override bool shouldUpdateGame()        
    {            
        // we only want the *script* to wait, the rest of the game should carry on in the mean time
        return true;        
    }        
    public override void run(float dt)        
    {            
        m_delay -= dt;        
    }    
}

In Script

<wait time="5" />

This will cause a pause of 5 seconds in the script, while the game continues. Very useful in a script!

Play cue

Class

class PlayCue        
:        
IScriptItem    
{        
String m_cue;        
    public override void setup(DataNode n)        
    {            
        m_cue = n.getNode("cue").getValue();        
    }        
    public override void run(float dt)        
    {            
        getGameResource().getAudioManager().playAndDispose(m_cue);        
    }    
}

In Script

<play_cue cue="fx_explode" />

End of side 1

Well, there you have it – a simple linear scripting system – great for cutscenes. It’s a 5 minute job to add a new IScriptItem – so if you want a command to kick off an animation, set the camera’s target position, tell a character to walk to a new location, wait for a character to finish walking to their target location.

The majority of ‘acting’ logic still must be in your game objects (e.g. your script may command your character to walk to “location X”, but it is the character’s update which is responsible for actually walking him there). Using our component system, we have a component “ai_walk” which is responsible for this behaviour. If you command an object to walk, which doesn’t contain an “ai_walk” component – it simply won’t go anywhere…

Here are a few example script commands we use:

  • Spawn Object
  • Remove Object
  • Walk To (character walking)
  • Add Focal Point (camera)
  • Remove Focal Point (camera)
  • Add Objective (for player’s objective list)
  • Remove Objective
  • Speak (character talking)
  • Wait for talk (wait for a character to stop talking)

So, even this small set of commands, it is possible to see how a cutscene with a couple of characters talking / walking to a new location can be easily created.. (and object being added or removed, the player’s objectives being updated etc..)

In the next blog on this subject, I’ll write about how we implemented branching (IFs, etc..), variables and conditions.. transform this linear system can be transformed in to a full script language that can be used for more than just cutscenes.

**While on the train today, I had some thoughts about how this system could be made more “OO”, and more cohesive with our existing GameComponent/Message system… so maybe I’ll write a post about that (and why I don’t think we’ll be switching to it immediately) some time!


Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: