Data Driven Programming II

Recall from the last lecture on Data Driven Programming, we looked at the simple case of reading constants and simple properties of world elements from data files, so that as little as possible of the definition of our worlds was done in code. That way, we could make major changes to our game by changing only data files. The example we used was a simple level for a platforming game -- each platform was described completely in a data file (location of platform, image, if the platform was moving, path for platform to move along, etc). This lecture will concern functional aspects -- how do we define interactive elements of our game in a data driven way?

Working Example: Door

The working example we will be using is a simple door, that might be used as part of a puzzle in a in a cooperative platform game, or just as a visual nicety. When a player enters a trigger region, the door opens, and when the player leaves the trigger region, the door closes:

Door Example

If we make the trigger area cover the entire door, then the door is more of a visual nicety, like a star-trek door:

Door Example

So, how could we program this? Remember, we want it to be as versatile as possible! Let's start with a generic triggered door, and extend it to be more general.

Triggered Door

We can create a Triggered Door object that has two pieces: a trigger area (that is not drawn on the screen), and a physical door, which is both drawn on the screen, and prevents player / monsters from passing. Our door can have two states, open and closed. When closed, the collision region of the door fills the entire section of the corridor, and when open the collision region is moved (or the door object is flagged as an object that does not collide with anything.) With some C#-y pseudocode (Note: This is not the only way to code this kind of a door -- it is not even the best way to code this kind of door! You wouldn't want to cut and paste this coode, this is just to give you a concrete example to think about! This is also pseudocode -- you would need to add more to it for it to work properlly!):
   class WorldObject
   {
       public Vector2 Position { get; set; }
       public Rectangle CollisionArea { get; set; }
       public bool CallbackIfNotColliding { get; set; }
    
       // Called when the world detects a collision, with a pointer
       //  to the object we collided with
       public virtual void Collide(WorldObject otherObject) { }
    
       // Called when we don't collide with anything, if our
       // CallbackIfNotColliding flag is set
       public virtual void NoCollision() { }
   }
   class Door : WorldObject
   {
       public void Draw(Spritebatch sb)
       {
            if (mIsOpen)
            {
            	// Draw open door
            }
            else
            {
            	// Draw closed door
            }
       
       		// Could also keep track if door is opening / closing, draw animation
       }
       
       public void Update(GameTime gameTime)
       {
            // Updating for any dynamic behavior (like animating opening / closing,
            //    or a revolving door, or something else)       
       }
       
       
        public bool IsOpen
        {
            get
            {
                return mIsOpen;
            }
            set
            {
            	if (value != mIsOpen)
                {
                  // Either modify collision volume that world uses, or set a flag
                  //  for the world to ignore / allow collisions with this door 
                  mIsOpen = value;
                }
            }
       }
       		
       private bool mIsOpen;
   }
Our triggered door would contain a pointer to this door, and a collision area. If the world detects a collision with this area, then open the door. Note that we want to know if we are (or are not!) colliding with anything, so we may need to add some special case code to our world -- at the very least, add a flag that tells the world to give us a callback if we are or are not colliding with anything.
class TriggeredDoor : WorldObject
{
     private Door mDoor;

     public override void Collide(WorldObject otherObject)
     {
     	Door.IsOpen = true;
     }
     
     public override void NoCollide()
     {
     	Door.IsOpen = false;
     }
}

Data For Triggered Door

We'd like to make these doors as generic as possible. We could have the data file describe the position of the triggering rectangle, the collision area for an open (and closed!) door, position of the door texture, and the texture to use for an open or closed door:
<TriggeredDoor>
    <TriggerArea type = "Rectangle">
        <x>
            300
        </x>
        <y> 
            200
        </y>
        <w>
            30
        </w>
        <h>
            20
        </h>
    </TriggerArea>
    <ClosedCollisionArea type = "Rectangle">
        <x>
            320
        </x>
        <y> 
            200
        </y>
        <w>
            5
        </w>
        <h>
            20
        </h>
    </ClosedCollisionArea>
    <OpenCollisionArea type = "Rectangle">
        <x>
            320
        </x>
        <y> 
          200
        </y>
        <w>
            5
        </w>
        <h>
            2
        </h>
    </OpenCollisionArea>
    <OpenTexture type = "String">OpenDoor1</OpenTexture>
    <ClosedTexture type = "String">ClosedDoor1</ClosedTexture>
    <TexturePosition type = "Vector2">
        <x>
100
</x>
<y>
100
</TexturePosition> </TriggeredDoor>
We could now easily create a star-trek door (opens when you get close to it, closes when your go away), a triggered-door for a two-person cooperative game, or a triggered door that lets monsters (or something else) out of an area -- and if you're really feeling mean, you could create a door that closes when you get close to it, by swapping the collision area and texture for "open" and "closed". Want to add a door with behavior that doesn't easily fit this model? You could special-case it, or you could see what new generic attributes you could add to be able to implement the new behavior.

Scripting

We've made some progress -- we can modify our functionality somewhat by changing data files -- but our changes are just doors opening and closing. If we wanted a different behavior triggered by entering an area (or a door triggered by something other than entering an area) we would need to write new code. It would be nice if we could create a triggering area, that could call arbirary code when it is enterred or exited -- and allow our door-opening code to be called by a variety of triggers. Let's say that for a particular piece of gameplay, you wanted the lights to turn off when you enterred a particular volume. If there was a global function for turning off the lights, we could create an entry in our world file that looked like the following:

  <TriggeringArea>
     <Area type = "Rectangle">
        <x>
           100
        </x>
        <y>
           50
        </y> 
        <w>
           30
        </w> 
        <h>
            10
        </h>
     </Area>
     <code>
         TurnOffLights()
     </code>
  </triggeringarea>
  

Whenever a player entered the volumne, the code "TurnOffLights()" will be called. This leaves us with a bit of a problem -- when we read in the description of the TriggeringArea, the <code> segment will be a string -- we need to be able to convert that string to a piece of code that we can actually execute.

Calling functions from Strings

One solution to executing code from data is to include an interpreter in your game engine. The interpreter can interpret the string, just like a python interpreter can interpret strings as you type them in. (In fact, some games do use Python as a scripting language, so they include a pared-down python interpreter as part of the game code.) Alas, there are currently not really any stable interpreters for scripting languages that play nice with the Xbox, and writing one from scratch is a little beyond the scope of this class. However, we can hack out some of the functionality of a scripting language using C# type reflection -- much as we did to write a (somewhat) generic XML reader. We will create a singleton Scripting class, which contains a method ExecuteScript(string scriptsString), which will interpret the string scriptString as a C# command. To make our lives a little easier, the scriptString will need to invoke a method in the Scripting class (so that we won't be able to call absolutely any method in our code, only methods defined in the Scripting class). Given any object, we can use the GetType() method to get an object representing the type of the object. From that, we get an object representing a method call. For instance, if the scripting class had a method TurnOnLights that took no paramerters, we could call it as follows:

  Object obj = Scripting.GetInstance();                              // Get a pointer ot the object
  MethodType method = obj.GetType().GetMethod("TurnOnLights");       // Get a pointer to the method object
  Obj [] params = new Obj[0];                                        // Array of parameters (none, in this example)
  method.Invoke(obj, params);                                        // Call the method
  

If we wanted to call a method that took parameters, we would just need to create the appropriate parameter list before invoking the method. For instance, we could call the method SetLightValue that took a boolen parameter as follows:

  Object obj = Scripting.GetInstance();                              // Get a pointer ot the object
  string methodName = "SetLightValue";
  MethodType method = obj.GetType().GetMethod(methodName);           // Get a pointer to the method object
  Obj [] params = new Obj[1];                                        // Create array of parameters
  params[0] = true;                                                  // c# boxing creates an object out of this primative type
  method.Invoke(obj, params);                                        // Call the method
  

So, we could create a quick and dirty scripting system by writing a class that takes as input a string, and invokes the method with this string's name using reflection. A somewhat quick and dirty scripting solution like this can be found here:

This scripting code is a little more sophisticated -- the first call needs to be to a method within the Scripting class, but if that call returns an object, we can call any method we like on that returned object. For instance, one of the example scripting functions is

 public WorldElement GetWorldElement(string id)
 

This method returns an object, which we can also manipulate from within our script string as follows:

string script = "GetWorldElement(\"pf2\").SetIsMoving(true)"
Scrpting.GetInstance.ExecuteScript(script);

(Note that if we had read script in from a data file, we would not need to escape the quotation marks ("), we only need to do that when we want to include qutation marks in a string literal)

Referring to other objects in data files: Adding identifiers

If we want to have our triggering area affect any arbitrary object in our world, then we need a way for our "scripts" to refer to objects in the world. One solution is to give each object in the world an identifier string, and have our world keep a hash table of identifier strings to world objects. That way, we could easily refer to any object in our world by using the identifier string. Given a platform defintion:

   class WorldElement
    {
        public WorldElement() { }

        public Vector2 Position { get; set; }
        public Texture2D Texture { get; set; }
        public string TextureName { get; set; }
        public string Id { get; set; }
        public virtual void Update (GameTime time) { }
    }

    class Platform : WorldElement
    {
        public bool IsMoving {get; set;}
        public List Path { get; set; }
        public bool ReverseOnPathEnd { get; set; }
        public float Speed { get; set; }       
        
        // Any private / projectected data members
         
        public override void Update(GameTime time)
        {
        	// Code to move the platform
        }
     }
We could define a platform within our XML file as:
     <Platform>
      <Position type ="Vector2">
        <x>
          200
        </x>
        <y>
          100
        </y>
      </Position>
      <TextureName type ="String">platform1</TextureName>
      <Id type ="String">pf1</Id>
    </Platform>
or, for a moving platform:
    <Platform>
      <Id type ="String">pf2</Id>
      <Position type ="Vector2">
        <x>
          100
        </x>
        <y> 
          100
        </y>
      </Position>
      <TextureName type="string">platform1</TextureName>
      <IsMoving type ="bool">
        false
      </IsMoving>
      <ReverseOnPathEnd type ="bool">
        true
      </ReverseOnPathEnd>
      <Path type ="Vector2List">
        <point>
          <x>
            100
          </x>
          <y>
            100
          </y>
        </point>
        <point>
          <x>
            300
          </x>
          <y>
            100
          </y>
        </point>
        <point>
          <x>
            50
          </x>
          <y>
            200
          </y>
        </point>

      </Path>
      <Speed type ="float">
        60
      </Speed>
    </Platform>

Scripts can now refer to objects in the world from their identifiers. Of course, you could have a problem if two objects had the same identifier -- it would probably be a good idea to have your level load system throw an exception if two world objects had been given the same identifier.

Door Redux: Triggering Areas

OK, so now we can create a triggering area, that can call a "script" when it is enterer or exited:
    class TriggerArea
    {
        public TriggerArea() { }
        public Rectangle Area { get; set; }
        public String OnEnter { get; set; }
        public String OnExit { get; set; }
        public Texture2D DebugTexture { get; set; }
     }
Our world can keep a list of all TriggerAreas. Each frame, if the player was outside the triggered area before the frame, and inside the triggered area after the frame, then call the OnEnter script:
   // Inside update loop of world
   Point playerLocation =  new Point((int)mPlayer.Position.X, (int) mPlayer.Position.Y);
   foreach (TriggerArea trigger in mTriggerAreas)
   {
       if (trigger.Area.Contains(playerLocation) && player was not previously in this area)
       {
           player is now in this area
           Scripting.GetInstance().ExecuteScript(trigger.OnEnter); 
       }
       if (!(trigger.Area.Contains(playerLocation) && player was previously in this area)
       {
           player is no longer in this area
           Scripting.GetInstance().ExecuteScript(trigger.OnExit)
       }
   }
Our XML would look something like the following:
    <TriggerArea>
       <Area type ="Rectangle">
         <x>
           300
         </x>
         <y>
           300
         </y>
         <w>
           100

         </w>
         <h>
           250
         </h>
       </Area>
       <OnEnter type ="string">
         GetWorldElement("pf2").SetIsMoving(true)
       </OnEnter>
       <OnExit type ="string">
         GetWorldElement("pf2").SetIsMoving(false)
       </OnExit>
     </TriggerArea>
   

Adding Timers

What if we wanted a door to close a certain amount of time after we left a trigger area? We could add timer code to our door -- add a "close in 5 seconds" method to the door -- or we could do it in a more generic way, by adding simple timers that all scripts can use, as follows:
   //  Within Scripting class:
   
        class ScriptTimingPair
        {
            public ScriptTimingPair(string scriptName, float time)
            {
                Script = scriptName;
                TimeRemaining = time;
            }
            public string Script { get; set; }
            public float TimeRemaining { get; set; }
        }
        
        private List mScriptTimers;

        public void TimedScript(string script, float time)
        {
            mScriptTimers.Add(new ScriptTimingPair(script, time));
        }
        
        public void Update(GameTime time)
        {
            List elemsToRemove = null;

            foreach (ScriptTimingPair stp in mScriptTimers)
            {
                stp.TimeRemaining -= (float) time.ElapsedGameTime.TotalSeconds;
                if (stp.TimeRemaining < 0)
                {
                    if (elemsToRemove == null)
                        elemsToRemove = new List();
                    elemsToRemove.Add(stp);
                    ExecuteScript(stp.Script);
                }
            }
            if (elemsToRemove != null)
            {
                foreach (ScriptTimingPair stp in elemsToRemove)
                {
                    mScriptTimers.Remove(stp);
                }
            }

        }   
   
Now, we can have any script executed after a delay.
   <OnExit>
        TimedScript("GetWorldElement('door1').Close()", 5.0)
   </OnExit>
   

Reusing Components in New Ways

If we have created our data driven game objects using (relatively) generic elements, then we can mix and match them in several new ways. What if we wanted a collapsing platform? We can add a "Remove World Element" to our world class, then expose this method to our scripting class:
   // In scripting class:
   public void RemoveWorldElement(string id)
   {
        mWorld.RemoveWorldElement(id)
   }
   
   // In world class:
   public void RemoveWorldElement(string id)
   {
        // Look up id in world hash table
        // remove id from world list if found
   }   
   
Then we cold place a trigger area right on top of the platform we wanted to remove:
   <!-- in world definition XML -->
     <TriggerArea>
       <Area type ="Rectangle">
         <x>
           100
         </x>
         <y>
           50
         </y>
         <w>
           50
         </w>
         <h>
           50
         </h>
       </Area>
         <OnEnter type ="string">
           RemoveWorldElement("CrumblePlatform1")
         </OnEnter>
     </TriggerArea>
     
     <Platform>
      <Id type ="String">CrumblePlatform1</Id>
      <Position type ="Vector2">
        <x>
          100
        </x>
        <y>
          100
        </y>
      </Position>
      <TextureName type ="string">platform1</TextureName>
    </Platform>
   

We now have all sorts of options for script-driven behavior. Want a rolling boulder that comes down when a player enters an area? Want enemies to spawn when the player gets close, or triggers a switch? Want a spike trap to kill the player when s/he enters a particular area (or enters an area without holding a protetive totem?) All can be script driven. Even without scripts, you can have good data--driven behavior. Want to add jumppads, fans, inverted-gravity sections, enemy spawn points, rope swings, or a whole host of other gameplay features? Paramteratize them as much as possible, so that various versions can be added to your level with only a data change

Want to really impress me? Create a level-editing tool that allows users to create levels for your game using a simple GUI editor, that outputs the appropriate XML (or whatever data fromat your code uses)

Data files: