Jump to content

  • Log In with Google      Sign In   
  • Create Account

True, False, Maybe



State Machines in Games – Part 5

Posted by , 16 March 2013 - - - - - - · 1,393 views

Okay, quick review.

Part 1 - basics of a state machine; Part 2 - simple state machine interface; Part 3 - turning the simple state machine into a dungeon exploration game; Part 4 - making it data driven.

That brings us to Part 5, an artificial intelligence system.

First, I know this pattern is used in major games. How do I know? First, because I wrote almost exactly this same system in Littlest PetShop. According to VGChartz the game series sold over 4.15 million copies globally. Second, I used a very similar pattern when working on The Sims 2 and The Sims 3.

Behavioral Trees

Okay, so this gets in to a concept called a behavioral tree. There are actors and objects, and the actors do something. Here is the structure I created for this demo:

Attached Image


The game’s container is a playing field. It basically manages the state machine. It gets regular updates at about 30 frames per second, and updates all the game objects.

So far I have two classes of game objects: Pets and Toys. These work together with activities.

The Base GameObject Class

A game object fits in with the state machines. They serve as both state machines AND as state nodes.

It has an Update(), which means to run the current state, and also to advance the current state if necessary. We’ll expand on this a little later.

The GameObject represents any object we can place on our game board. They have an owner (in this case, the playing field). They have a location. They have an image.

For convenience they have a ToString() override that makes things look nicer when I view them in a property grid.

Also over development, they have evolved to have a PushToward() and a MaxSpeed() method. These would probably be integrated into a physics system or a collision system, but for now this is their best natural fit.


    public abstract class GameObject
    {
        public PlayingField Owner { get; set; }

        /// <summary>
        /// Initializes a new instance of the GameObject class.
        /// </summary>
        public GameObject(PlayingField owner)
        {
            Owner = owner;
        }

        /// <summary>
        /// Update the object
        /// </summary>
        /// <param name="seconds">seconds since last update.</param>
        /// <remarks>seconds is the easiest scale for the individual settings</remarks>
        public abstract void Update(float seconds);

        /// <summary>
        /// Location on the playing field to draw the actor
        /// </summary>
        public PointF Location { get; set; }

        /// <summary>
        /// What to draw on the playing field
        /// </summary>
        public abstract Image Image { get; }

        /// <summary>
        /// Push the game object toward a location.  Default behavior is to not move.
        /// </summary>
        /// <param name="destination">Location to push toward</param>
        /// <param name="seconds">Seconds that have passed in this movement</param>
        public virtual void PushToward(PointF destination, float seconds) { return; }

        /// <summary>
        /// Get the maximim speed of this game object. Default behavior is not to move.
        /// </summary>
        /// <returns></returns>
        public virtual float MaxSpeed() { return 0; }

        /// <summary>
        /// Simplified name for the object to display in the property browser
        /// </summary>
        /// <returns>Shorter name</returns>
        public override string ToString()
        {
            string classname = base.ToString();

            int index = classname.LastIndexOf('.');
            string shortname = classname.Substring(index+1);
            return shortname;
        }

    }




Pets and Motives

The basic pet class is pretty simple.

A pet is a game object (so it gets everything above), plus it also gets an activity and a collection of motives.

The motives are nothing more than a wrapper for the pet’s status. In this case we are only tracking fun and energy. (Note for comparison in The Sims3 there are 8 visible motives – hunger, social, bladder, hygiene, energy, and fun.)

When a pet is created we default them to the Idle activity, and initialize their Motives.

We have a default update behavior to run whatever activity we are currently doing, or if we aren’t doing anything to create a new idle activity and do that instead.

We’ll also implement what it means to push a pet.


    public class MotiveBase
    {
        public float Fun { get; set; }
        public float Energy { get; set; }
    }





    public abstract class Pet : GameObject
    {
        public MotiveBase Motives { get; set; }

        public Activities.Activity Activity { get; set; }
        /// <summary>
        /// Initializes a new instance of the Pet class.
        /// </summary>
        public Pet(PlayingField owner)
            : base(owner)
        {
            Activity = new Activities.Idle(this, null);
            Motives = new MotiveBase();
        }

        /// <summary>
        /// Allow a pet to do something custom on their update
        /// </summary>
        /// <param name="seconds"></param>
        protected virtual void OnUpdate(float seconds) { return; }

        public override void Update(float seconds)
        {
            if (Activity == null)
            {
                Activity = new Activities.Idle(this, null);
            }
            Activity.Update(seconds);
        }

        public override void PushToward(System.Drawing.PointF destination, float seconds)
        {
            base.PushToward(destination, seconds);

            // TODO: Someday accumulate force and make a physics system.  Just bump it the correct direction.
            // TODO: Create a vector class someday
            float xDiff = destination.X - Location.X;
            float yDiff = destination.Y - Location.Y;
            float magnitude = (float)Math.Sqrt(xDiff * xDiff) + (float)Math.Sqrt(yDiff * yDiff);
            if (magnitude > (MaxSpeed() * seconds))
            {
                float scale = (MaxSpeed() * seconds) / magnitude;
                xDiff *= scale;
                yDiff *= scale;
            }
            Location = new PointF(xDiff + Location.X, yDiff + Location.Y);
        }
    }




Puppies!

So finally we’ll implement a type of pet.

A puppy has an image, and a puppy has a maximum speed. Note that we just pull these from the saved resources so a designer can adjust them later.
    class Puppy : Pet
    {
        /// <summary>
        /// Initializes a new instance of the GameObject class.
        /// </summary>
        public Puppy(PlayingField owner)
            : base(owner)
        {
            
        }

        public override System.Drawing.Image Image
        {
            get { return FSM_Puppies.Properties.Resources.Puppy; }
        }

        public override float MaxSpeed()
        {
            return FSM_Puppies.Properties.Settings.Default.Pet_Puppy_MaxSpeed;   
        }
    }


Toys

A toy is also a game object, so it can behave as a state machine and as a state node, as appropriate.

A toy has a default activity associated with it. When a pet attempts to use a toy they will get this default activity (aka behavior tree) and start running it.

A toy is also responsible for computing the interest level in the object. For now these will just be hard-coded formulas inside each toy object. Later on these could be a more complex series of interactions but for this system it is adequate.

Here’s the Toy interface
    public abstract class Toy : GameObject
    {
        /// <summary>
        /// Initializes a new instance of the Toy class.
        /// </summary>
        public Toy(PlayingField owner)
            : base(owner)
        {
            
        }

        public abstract Activities.Activity DefaultActivity(Pets.Pet actor, GameObject target);
        public abstract float Interest(Pets.Pet pet);

        public override void Update(float seconds)
        {
            // Note that most toys do nothing of themselves.  They are driven by their activities.
            return;
        }

    }


Two Toys

Now we’ll create two concrete classes for toys.

First, a sleeping mat. The interest of the sleeping mat is only based on energy. It has an image to draw. The default activity is to sleep on the mat.
    class SleepingMat : Toy
    {
        /// <summary>
        /// Initializes a new instance of the SleepingMat class.
        /// </summary>
        public SleepingMat(PlayingField owner)
            : base(owner)
        {
            
        }
        public override FSM_Puppies.Game.Activities.Activity DefaultActivity(Pets.Pet actor, GameObject target)
        {
            return new Activities.SleepOnMat(actor, this);
        }

        public override System.Drawing.Image Image
        {
            get { return FSM_Puppies.Properties.Resources.SleepingMat; }
        }

        public override float Interest(FSM_Puppies.Game.Pets.Pet pet)
        {
            return 100 - pet.Motives.Energy;
        }
    }



Second, a ball to kick around. The interest is only based on fun, although it probably should include an energy component. It has an image to draw, and the default activity is to chase the ball.

    class Ball : Toy
    {
        /// <summary>
        /// Initializes a new instance of the Ball class.
        /// </summary>
        public Ball(PlayingField owner)
            : base(owner)
        {
        }

        public override Image Image
        {
            get { return FSM_Puppies.Properties.Resources.Ball; }
        }

        public override Activities.Activity DefaultActivity(Pets.Pet actor, GameObject target)
        {
            return new Activities.ChaseBall(actor, target);
        }

        public override float Interest(FSM_Puppies.Game.Pets.Pet pet)
        {
            return 100 - pet.Motives.Fun;
        }
    }


Now we move on to the activities that drive the system.

Activities Are Glue and Oil

Activities serve as glue to the system. They are the interactions between actors and objects. Without them there wouldn’t be much of a connection between the two.

Activities also serve as the oil to the system. They are constantly moving. They change themselves, and they change the actors they work with, and they can change the objects they work with. A more complex example of a food bowl could change the actor by modifying hunger, and also change the target by reducing the amount of food in the bowl.

So here is our activity base class.

An activity has an Actor and a Target. I intentionally limited Actors to be pets. I could have allowed any object to interact with any object, but that doesn’t quite make sense in practice. We don’t really want a food bowl to interact with a chew toy, or a ball to interact with a sleeping mat. We DO want to allow a pet to be a target allowing default activities to play social events. For example, pets could dance together or sniff each other or do whatever groups of pets do together.

We allow an Update event on the activity base. This update is run by the pet earlier. We pass that on through the OnUpdate callback in each activity. If the activity returns true then we know it is complete and the pet needs to find something new to do.

Finally we have a magical function, FindBestActivity() that needs to live somewhere in the world.

This FindBestActivity is the magic that makes the AI do fun things. In this example it is only 35 lines. We loop over all the toys in the game world and see how interesting they are. Then we take the best interaction and return a new instance of it. If we fail we just return the idle activity.

For a game like The Sims there are potentially tens of thousands of objects to choose from, and each object can have many activities associated with it. Finding the best activity among them all is a complex job. The theory behind it is no different: Find the best activity, and create an instance of it.
        public abstract class Activity
    {
        public Pets.Pet Actor { get; set; }
        public GameObject Target { get; set; }

        /// <summary>
        /// Initializes a new instance of the Activity class.
        /// </summary>
        public Activity(Pets.Pet actor, GameObject target)
        {
            Actor = actor;
            Target = target;
        }

        /// <summary>
        /// Update this activity state
        /// </summary>
        /// <param name="seconds">elapsed time</param>
        /// <returns>true if the activity is complete</returns>
        public abstract bool OnUpdate( float seconds );
        
        /// <summary>
        /// Update this activity state
        /// </summary>
        /// <param name="seconds">elapsed time</param>
        public void Update(float seconds)
        {
            if(OnUpdate(seconds))
            {
                Actor.Activity = new Idle(Actor, null);
            }
        }

        /// <summary>
        /// Utility function to locate the best next activity for the actor.
        /// </summary>
        /// <returns></returns>
        public static Activity FindBestActivity(Pets.Pet actor)
        {
            // Look for a toy to play with...
            if (actor.Owner != null
                && actor.Owner.GameObjects != null)
            {
                List<Toys.Toy> candidates = new List<Toys.Toy>();
                foreach (GameObject obj in actor.Owner.GameObjects)
                {
                    Toys.Toy t = obj as Toys.Toy;
                    if (t != null)
                    {
                        candidates.Add(t);
                    }
                }
                if (candidates.Count > 0)
                {
                    float bestScore = float.MinValue;
                    Toys.Toy bestToy = null;

                    foreach (Toys.Toy t in candidates)
                    {
                        float myscore = t.Interest(actor);
                        if(myscore>bestScore)
                        {
                            bestScore = myscore;
                            bestToy = t;
                        }
                    }
                    return bestToy.DefaultActivity(actor, bestToy);
                }
            }

            return new Idle(actor, null);
        }

        public override string ToString()
        {
            string classname = base.ToString();

            int index = classname.LastIndexOf('.');
            string shortname = classname.Substring(index + 1);
            return shortname;
        }
    }



Idle Activity

We’ll start with the idle activity.

It has an idle time. After enough time has passed we look for something new to do. This new activity will replace our current idle activity.
If we don’t find anything interesting to do we can just sit there, slowly dropping our fun and our energy.

Since this is C# we don’t need to schedule cleanup and deletion of our own idle activity which simplifies our code quite a lot.
    class Idle : Activity
    {
        float mTimeInIdle = 0;

        public Idle(Pets.Pet actor, GameObject target)
            : base(actor, target)
        {
        }
        
        public override bool OnUpdate(float seconds)
        {
            mTimeInIdle += seconds;
            if (mTimeInIdle >= FSM_Puppies.Properties.Settings.Default.Activity_Idle_WaitingTime)
            {
                Actor.Activity = FindBestActivity(Actor);
            }

            // Sitting there idle isn't much fun and slowly decays energy.  This encourages us to pick up other activiites.
            Actor.Motives.Fun += FSM_Puppies.Properties.Settings.Default.Activity_Idle_Fun * seconds;
            Actor.Motives.Energy += FSM_Puppies.Properties.Settings.Default.Activity_Idle_Energy * seconds;

            // Always return false because idle is never finished.  It auto-replaces if it can find something.
            return false;
        }
    }


ChaseBall Activity

This is actually TWO activities. Chasing a ball has one component “RunToObject”, and then a second component where they actually kick the ball.

So every update we attempt to run to the ball object. If we succeeded to running to the object, wet kick the ball a random distance. We also bump fun a little bit whenever they kick the ball.

An activity’s return result indicates when it is complete. We’ll return true only when our fun is maxed out. We might want to have a second exit condition when energy runs low, but that is for later.
    class ChaseBall : Activity
    {
        RunToObject mRto;

        /// <summary>
        /// Initializes a new instance of the ChaseBall class.
        /// </summary>
        public ChaseBall(Pets.Pet actor, GameObject target)
            : base(actor, target)
        {
            mRto = new RunToObject(actor, target);
        }

        public override bool OnUpdate(float seconds)
        {
            // When they kick the ball, move it to a new location and continue our activity.
            if( mRto.OnUpdate(seconds))
            {
                float kickDistance = FSM_Puppies.Properties.Settings.Default.Activity_ChaseBall_KickDistance;
                // Get a random number with +/- kick distance
                float newX = Target.Location.X + (((float)Target.Owner.Rng.NextDouble()*(2*kickDistance))-kickDistance);
                float newY = Target.Location.Y + (((float)Target.Owner.Rng.NextDouble()*(2*kickDistance))-kickDistance);
                PointF randomLocation = new PointF(newX,newY);
                Target.Location = randomLocation;
                Actor.Motives.Fun += FSM_Puppies.Properties.Settings.Default.Toy_Ball_Fun;
                if(Actor.Motives.Fun > 100)
                    return true;
            }
            return false;
        }
    }


RunToObject Activity

Next we’ll look at how they run to an object.

It is pretty simple. If we are close enough (another designer-adjustable value) then they have made it to the object and we return true. If they are not there yet we push them toward the object, drop their energy, and return false (we aren’t done with the activity yet).
    class RunToObject : Activity
    {
        /// <summary>
        /// Initializes a new instance of the RunToObject class.
        /// </summary>
        public RunToObject(Pets.Pet actor, GameObject target)
            : base(actor, target)
        {
            
        }
        public override bool OnUpdate(float seconds)
        {
            // Are we there yet?
            // And why didn't PointF implement operator- ?
            PointF offset = new PointF( Target.Location.X - Actor.Location.X, Target.Location.Y - Actor.Location.Y);
            float distanceSquared = offset.X * offset.X + offset.Y * offset.Y;
            float closeEnough = FSM_Puppies.Properties.Settings.Default.Activity_RunToObject_CloseEnough;
            float closeEnoughSquared = closeEnough * closeEnough;
            if (distanceSquared < closeEnoughSquared)
                return true;

            Actor.PushToward(Target.Location, seconds);
            Actor.Motives.Energy += FSM_Puppies.Properties.Settings.Default.Activity_RunToObject_Energy * seconds;
            return false;
        }
    }


Sleeping On the Mat

Just like chasing a ball, we start out by running to the object. So first we call RunToObject.

If it succeeds (meaning we finally got there), then we start resting. We bump the motives, return true or false based on our energy status.
    class SleepOnMat : Activity
    {
        RunToObject mRto;

        /// <summary>
        /// Initializes a new instance of the SleepOnMat class.
        /// </summary>
        public SleepOnMat(Pets.Pet actor, GameObject target)
            : base(actor, target)
        {
            mRto = new RunToObject(actor, target);
        }
        public override bool OnUpdate(float seconds)
        {
            // Route to the sleeping mat
            if(mRto.OnUpdate(seconds))
            {
                // Now that we are on the mat, just sit here and increase our motives.
                Actor.Motives.Energy += FSM_Puppies.Properties.Settings.Default.Toy_SleepingMat_Energy;
                Actor.Motives.Fun += FSM_Puppies.Properties.Settings.Default.Toy_SleepingMat_Fun;

                if (Actor.Motives.Energy > 100)
                    return true;
            }
            return false;
        }
    }


All Done

Go ahead and play around with this. Drop multiple play mats and multiple balls and multiple puppies.

It is not much, but it again enough to see a game starting to grow.

Repeat this thirty times with new objects, add a few motives, and you can have your own self-running simulated pet world.

But I Wanted a First Person Shooter AI

That is solved very easily:

Rename “Pets” to “Monsters”, and “Puppy” to “Grunt”.

Rename “Toys” to “WayPoint”, “Ball” to “Flamethrower”, and “SleepingMat” to “SpawnPoint”.

Finally, rename activities as appropriate.

Always Leave Them Wanting More

Okay, I’m not going to add much of a preview in the source code. In this case it is difficult to reveal extensions without directly including them.

In this case my children are driving what we add in Part 6: The Conclusion. They have a laundry-list of toys and activities with the toys. The biggest modification will be allowing attributes of each pet to modify their interest levels. I can tell it has turned into a game because my three children are all begging to add game designs to it.

Also, I have been asked to turn this series of journal entries into a full GameDev.net Article. Or series of articles, I’m not sure quite yet. So I need to make some minor adjustments to accommodate that format.

Finally, the complete source code below.


Attached File  StateMachineTutorialsV5.zip (64.24KB)
downloads: 56


State Machines in Games – Part 4

Posted by , 13 March 2013 - - - - - - · 1,485 views

Back to the castle exploration game. We're still working with an single state machine in the AdaptableStateMachine engine. This will be our third concrete example of a state machine.

Recap:

In Part 1 we covered the definition of a state machine and implemented a simple machine with a case statement.

In Part 2 we created a common state machine interface, created a state machine runner for the interface, and a boring state machine concrete class.

In Part 3 we left the state machine runner unchanged, created new state machine concrete classes for a castle map. Finally it looks like a game. A single state machine game.

Now in Part 4 we will make the state machine game data driven.

What Does Data Driven Mean?

So far every state machine and every state has been written in C#. If I wanted to make changes I needed to modify the source code, recompile, and test the app. For a small map and a single developer this is not a bad thing.

But what happens when the app grows?

Let's imagine I hire a person to design my levels. That person is not a programmer. I don't want them mucking about in my C# files, and I don't want to teach them how to read and write C#. So what do I do?

Simple: I create a save file that contains all the information describing the level.

By loading the level at runtime I can allow non-programmers to develop the game. It means level designers can modify rooms, put different things in different locations, and otherwise improve the game without touching code. It means another programmer can implement game objects like ropes and bottles and such (should be in Part 7, I think) and the level designer can manipulate those in the world without modifying code. If I had artwork, artists could create new art and designers could put it in the world without touching code.

If this were a major game we might have ten or twenty people working on the game. Only a quarter of those people would be programmers. Everyone else would be modifying the game data rather than modifying the game code.

Data Driven means only the programmers need to touch the code, and everyone else gets to use data files to modify the game.

If we wanted to be fancy we could add a mechanism to reload data files while the game is running. That would let designers and artists and animators and modelers iterate on their work much faster, saving many hours of development time.

If we wanted to get REALLY fancy would could use operating system events to notify us when a change is made and automatically reload the resources at runtime.

On to the Code!

Once again I am using IState and IStateMachine interfaces for my machine.

And just like before, the entire game is managed by these same two files.

The States

States are only slightly modified from last time.

Last time I stored the state's name, description, and neighbors.

This time I add a unique name key and a set of flags.

The flags are: None, Enter, and Exit. This allows me to code in multiple exit nodes. I am still required to have only one entry node because that is just how state machines work; you need to start somewhere.

Next I added the functions ReadXml and WriteXml. These two functions save and load my five elements (unique name, flags, visible name, description, and neighbors) into an xml file. Because it is basically free I chose to implement them using the IXmlSerializable interface.

Since the state machine will need to create these objects from XML, I create a second constructor that takes an XmlReader and simply pass that on to the ReadXml function.

Finally I added some accessors and mutators (get and set functions) to help out the state machine.

The code:
    public class SavedMachineState : IState, IXmlSerializable
    {
        #region Members
        [Flags]
        public enum StateFlags
        {
            None = 0,
            Enter = 1,
            Exit = 2,
        }

        public string mKey;
        StateFlags mFlags;
        string mName;
        string mDescription;
        List<string> mNeighbors = new List<string>();
        public List<string> Neighbors { get { return mNeighbors; } }
        #endregion
        #region Constructors
        /// <summary>
        /// Manual constructor for default maze
        /// </summary>
        /// <param name="uniqueKey">unique name for the stateFlags</param>
        /// <param name="flags">flags to indicate enter nodes and exit nodes</param>
        /// <param name="name">name to show to the user</param>
        /// <param name="description">text to show for the description</param>
        /// <param name="neighbors">unique keys for neighboring rooms, seperated by commas and not spaces</param>
        public SavedMachineState(string uniqueKey, StateFlags flags, string name, string description, string neighbors)
        {
            mKey = uniqueKey;
            mFlags = flags;
            mName = name;
            mDescription = description;
            mNeighbors.AddRange(neighbors.Split(','));
        }
        /// <summary>
        /// Constructor to create an object from a save file
        /// </summary>
        /// <param name="reader">xml stream to read from</param>
        public SavedMachineState(XmlReader reader)
        {
            ReadXml(reader);
        }
        #endregion
        #region Helper Functions
        public bool IsStartState { get { return (mFlags & StateFlags.Enter) != StateFlags.None; } }
        public bool IsExitState { get { return (mFlags & StateFlags.Exit) != StateFlags.None; } }
        public string Key { get { return mKey; } }
        public bool IsMyName(string nameToTest)
        {
            //TODO: Add shortcuts to names.  For example, allow "Great Hall", "Hall", etc.
            if (nameToTest.ToLower() == mName.ToLower())
                return true;
            if (nameToTest.ToLower() == mKey.ToLower())
                return true;
            return false;
        }
        #endregion
        #region IState Overrides
        public override string GetName()
        {
            return mName;
        }

        public override void Run()
        {
            // We don't do any fancy stuff, just print out where we are
            Console.WriteLine();
            Console.WriteLine(mDescription);
        }
        #endregion
        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            reader.ReadStartElement();
            mKey = reader.ReadElementContentAsString("UniqueName","");
            string flagString = reader.ReadElementContentAsString("Flags","");
            mFlags = (StateFlags)Enum.Parse(typeof(StateFlags), flagString);
            mName = reader.ReadElementContentAsString("VisibleName", "");
            mDescription = reader.ReadElementContentAsString("Description", "");
            string neighborsString = reader.ReadElementContentAsString("Neighbors", "");
            mNeighbors.AddRange(neighborsString.Split(','));
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            writer.WriteElementString("UniqueName", mKey);
            writer.WriteElementString("Flags", mFlags.ToString());
            writer.WriteElementString("VisibleName", mName);
            writer.WriteElementString("Description", mDescription);
            string neighbors = String.Join(",",Neighbors.ToArray());
            writer.WriteElementString("Neighbors",neighbors);
        }

        #endregion
    }

It might be a little intimidating if this were the first exposure you had to the code, but if you have been following along and since it has been gradually expanding it should seem to be just an incremental change.

The State Machine

The changes to the state machine were a little more dramatic.

As promised last time I was able to remove the saved mExit state. Since this is now part of the data (and because there can now be more than one) we don't want to keep this around in code.

I moved all the map construction code from the constructor to its own function: GenerateDefaultMap(). No point in throwing away all that work, and it allows us to generate and save a map when bootstrapping the tool chain. Now the constructor calls ImportFromXml(). If that fails we generate the default map (which saves a copy with ExportToXML() ) and then reload our newly created map. It then searches for an entry node. If it cannot fine one, we abort.

The ExportToXML() function creates an XML writer, loops through the list of states, and writes each state out using a helper function. The ImportFromXML() function creates an XML reader, and reads it in using a helper function. Those helper functions are the same IXmlSerializable interface we used on the state. This allows us to someday take advantage of the C# serialization system. Perhaps around Part 8 or so.

The WriteXML() function creates an XML element <room> </room> and then calls the state's WriteXML to fill in the details. The ReadXML() function verifies that we have a room node, calls the state's constructor with the XML reader, and adds the newly created state to the list.

The Code:
    public class SavedMachine : IStateMachine, IXmlSerializable
    {
        #region Members
        List<SavedMachineState> mStates = new List<SavedMachineState>();
        SavedMachineState mCurrent;
        #endregion
        #region Constructor
        /// <summary>
        /// Initializes a new instance of the FunnerMachine class.
        /// </summary>
        public SavedMachine()
        {
            try
            {
                ImportFromXML();
            }
            catch (Exception ex)
            {
                mStates.Clear();
            }

            if (mStates.Count == 0)
            {
                GenerateDefaultMap();
                ImportFromXML();
            }

            // Find the entry state
            for (int i = 0; i < mStates.Count; i++)
            {
                if (mStates[i].IsStartState)
                {
                    mCurrent = mStates[i];
                    break;
                }
            }
            if (mCurrent == null)
            {
                Console.WriteLine("\n\nERROR! NO ENTRY STATE DEFINED.");
                throw new Exception("No entry state defined in this state machine.  Cannot continue.");
            }
        }
        #endregion
        #region Helper Functions
        private void GenerateDefaultMap()
        {
            mStates.Clear();

            // Create all the fun states in our mini-world
            mStates.Add(new SavedMachineState("entryHall", SavedMachineState.StateFlags.Enter, "Grand Entrance", "You are standing in a grand enterance of a castle.\nThere are tables and chairs, but nothing you can interact with.", "staircase,outside"));
            mStates.Add(new SavedMachineState("staircase", SavedMachineState.StateFlags.None, "Grand Staircase", "The staircase is made from beautiful granite.", "eastWing,westWing,entryHall"));
            mStates.Add(new SavedMachineState("eastWing", SavedMachineState.StateFlags.None, "East Wing", "This wing is devoted to bedrooms.", "bedroomA,bedroomB,bedroomC,staircase"));
            mStates.Add(new SavedMachineState("westWing", SavedMachineState.StateFlags.None, "West Wing", "This wing is devoted to business.", "workroomA,workroomB,workroomC"));
            mStates.Add(new SavedMachineState("bedroomA", SavedMachineState.StateFlags.None, "Master Suite", "This is the master suite.  What a fancy room.", "eastWing"));
            mStates.Add(new SavedMachineState("bedroomB", SavedMachineState.StateFlags.None, "Prince Bob's Room", "The prince has an extensive library on his wall.\nHe also has more clothes than most males know what to do with.", "eastWing"));
            mStates.Add(new SavedMachineState("bedroomC", SavedMachineState.StateFlags.None, "Princess Alice's Room", "The princess has filled her room with a small compur lab.\nShe spends her days playing games and writing code.", "eastWing"));
            mStates.Add(new SavedMachineState("workroomA", SavedMachineState.StateFlags.None, "Study", "This is the study.  It has many books.", "westWing"));
            mStates.Add(new SavedMachineState("workroomB", SavedMachineState.StateFlags.None, "Bathroom", "Every home needs one", "westWing"));
            mStates.Add(new SavedMachineState("workroomC", SavedMachineState.StateFlags.None, "Do Not Enter", "I warned you not to enter.\nYou are in a maze of twisty little passages, all alike.", "passage"));
            mStates.Add(new SavedMachineState("passage", SavedMachineState.StateFlags.None, "Twisty Passage", "You are in a maze of twisty little passages, all alike", "passage"));
            mStates.Add(new SavedMachineState("outside", SavedMachineState.StateFlags.Exit, "Outside", "You have successfully exited the castle.", ""));

            ExportToXML();
        }
        public void ExportToXML()
        {
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.OmitXmlDeclaration = true;
            settings.NewLineHandling = NewLineHandling.Entitize;

            using (XmlWriter writer = XmlWriter.Create("GameRooms.xml",settings))
            {
                writer.WriteStartDocument();
                writer.WriteStartElement("SavedMachine");
                WriteXml(writer);
                writer.WriteEndElement();
                writer.WriteEndDocument();
            }           
        }
        public void ImportFromXML()
        {
            XmlReaderSettings settings = new XmlReaderSettings();
            settings.IgnoreWhitespace = true;
            XmlReader reader = XmlReader.Create("GameRooms.xml", settings);
            ReadXml(reader);
        }
        #endregion
        #region IStateMachine Overrides
        public override IState CurrentState
        {
            get { return mCurrent; }
        }
        public override string[] PossibleTransitions()
        {
            List<string> result = new List<string>();
            foreach (string state in mCurrent.Neighbors)
            {
                result.Add(state);
            }
            return result.ToArray();
        }
        public override bool Advance(string nextState)
        {
            foreach (SavedMachineState state in mStates)
            {
                if(state.IsMyName(nextState)
                    && mCurrent.Neighbors.Contains(state.Key))
                {
                    mCurrent = state;
                    return true;
                }
            }
            System.Console.WriteLine("Cannot do that.");
            return false;
        }
        public override bool IsComplete()
        {
            return mCurrent.IsExitState;
        }
        #endregion
        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }


        public void ReadXml(XmlReader reader)
        {
            bool isEmpty = reader.IsEmptyElement;
            reader.ReadStartElement();
            if (isEmpty) return;
            while (reader.NodeType == XmlNodeType.Element)
            {
                if (reader.Name == "Room")
                {
                    mStates.Add(new SavedMachineState(reader));
                }
                else
                    throw new XmlException("Unexpected node: " + reader.Name);
            }
            reader.ReadEndElement();
        }

        public void WriteXml(XmlWriter writer)
        {
            foreach (SavedMachineState state in mStates)
            {
                writer.WriteStartElement("Room");
                state.WriteXml(writer);
                writer.WriteEndElement();
            }
        }

        #endregion
    }
Run the Game

Now when I run the game, I select option 3 for this new state machine. Note that I had already included that option in Part 2's source code.

When it runs it attempts to load the save file, cannot find one, and generates the new room xml file. Then it plays the state machine as normal.

I don't care to play right now, I just want to read the XML.

Looking at the GameRooms.xml

Let's jump strait to the generated file:
<SavedMachine>
  <Room>
    <UniqueName>entryHall</UniqueName>
    <Flags>Enter</Flags>
    <VisibleName>Grand Entrance</VisibleName>
    <Description>You are standing in a grand enterance of a castle.
There are tables and chairs, but nothing you can interact with.</Description>
    <Neighbors>staircase,outside</Neighbors>
  </Room>
  <Room>
    <UniqueName>staircase</UniqueName>
    <Flags>None</Flags>
    <VisibleName>Grand Staircase</VisibleName>
    <Description>The staircase is made from beautiful granite.</Description>
    <Neighbors>eastWing,westWing,entryHall</Neighbors>
  </Room>
  <Room>
    <UniqueName>eastWing</UniqueName>
    <Flags>None</Flags>
    <VisibleName>East Wing</VisibleName>
    <Description>This wing is devoted to bedrooms.</Description>
    <Neighbors>bedroomA,bedroomB,bedroomC,staircase</Neighbors>
  </Room>
  <Room>
    <UniqueName>westWing</UniqueName>
    <Flags>None</Flags>
    <VisibleName>West Wing</VisibleName>
    <Description>This wing is devoted to business.</Description>
    <Neighbors>workroomA,workroomB,workroomC</Neighbors>
  </Room>
  <Room>
    <UniqueName>bedroomA</UniqueName>
    <Flags>None</Flags>
    <VisibleName>Master Suite</VisibleName>
    <Description>This is the master suite.  What a fancy room.</Description>
    <Neighbors>eastWing</Neighbors>
  </Room>
  <Room>
    <UniqueName>bedroomB</UniqueName>
    <Flags>None</Flags>
    <VisibleName>Prince Bob's Room</VisibleName>
    <Description>The prince has an extensive library on his wall.
He also has more clothes than most males know what to do with.</Description>
    <Neighbors>eastWing</Neighbors>
  </Room>
  <Room>
    <UniqueName>bedroomC</UniqueName>
    <Flags>None</Flags>
    <VisibleName>Princess Alice's Room</VisibleName>
    <Description>The princess has filled her room with a small compur lab.
She spends her days playing games and writing code.</Description>
    <Neighbors>eastWing</Neighbors>
  </Room>
  <Room>
    <UniqueName>workroomA</UniqueName>
    <Flags>None</Flags>
    <VisibleName>Study</VisibleName>
    <Description>This is the study.  It has many books.</Description>
    <Neighbors>westWing</Neighbors>
  </Room>
  <Room>
    <UniqueName>workroomB</UniqueName>
    <Flags>None</Flags>
    <VisibleName>Bathroom</VisibleName>
    <Description>Every home needs one</Description>
    <Neighbors>westWing</Neighbors>
  </Room>
  <Room>
    <UniqueName>workroomC</UniqueName>
    <Flags>None</Flags>
    <VisibleName>Do Not Enter</VisibleName>
    <Description>I warned you not to enter.
You are in a maze of twisty little passages, all alike.</Description>
    <Neighbors>passage</Neighbors>
  </Room>
  <Room>
    <UniqueName>passage</UniqueName>
    <Flags>None</Flags>
    <VisibleName>Twisty Passage</VisibleName>
    <Description>You are in a maze of twisty little passages, all alike</Description>
    <Neighbors>passage</Neighbors>
  </Room>
  <Room>
    <UniqueName>outside</UniqueName>
    <Flags>Exit</Flags>
    <VisibleName>Outside</VisibleName>
    <Description>You have successfully exited the castle.</Description>
    <Neighbors />
  </Room>
</SavedMachine>
This looks suspiciously like our original map. In fact, it is our original map, just saved out to xml.


To prove that the save and load system actually works, let's make some minor modifications to the map.

Attached Image

This will require a tiny modification to the entrance hall (poininting the neighbor to 'courtyard') and making three new rooms.

Easy enough, a few seconds in a text editor, copy/paste, a bit of wordsmithing, and I get this addition:
  <Room>
    <UniqueName>courtyard</UniqueName>
    <Flags>None</Flags>
    <VisibleName>Courtyard</VisibleName>
    <Description>The courtyard is decorated with many large trees and several marble benches.</Description>
    <Neighbors>entryHall,townGate</Neighbors>
  </Room>
  <Room>
    <UniqueName>townGate</UniqueName>
    <Flags>None</Flags>
    <VisibleName>Town Gate</VisibleName>
    <Description>You arrive at the gate of the town.  Ahh, to be home again.\n\nNOTICE: The guards will not let you return to the castle if you leave.</Description>
    <Neighbors>courtyard,village</Neighbors>
  </Room>
  <Room>
    <UniqueName>village</UniqueName>
    <Flags>Exit</Flags>
    <VisibleName>Quaint Village</VisibleName>
    <Description>You return to your village.  You won't soon forget your experiences in the castle.</Description>
    <Neighbors />
  </Room>
Modify the save file, reload the app, and automatically I have three new rooms.

Since I haven't done this yet, I'll just say that a map editor and more complex maps are left as an exercise to the reader. You can do that if you want to. I'm busy writing code for parts 5, 6 and 7.

And speaking of Part 5...

Always Leave Them Wanting More

So now we have a game world that is data driven. Any programmer or level designer can modify the GameRooms.xml file to make the map as complex as you want. I recommend creating a simple tool if you plan on making more than a few dozen rooms, but that is really up to you.

Where are we going next?

I promised we'd get in to AI.

AI in a text adventure is nice, but it isn't very interactive.

So Part 5 will have an actual VIDEO game. With graphics. And an update loop. And individual actors that have an AI. But not so much for gameplay since all we are testing is AI. We'll also have a collection of state machines that interact with each other.

Naturally there is a preview of it in this part's source code, as an incentive. Like always.

Attached File  StateMachineTutorialsV4.zip (55.92KB)
downloads: 79





August 2016 »

S M T W T F S
 123456
78910111213
14151617181920
21222324252627
2829 30 31   

Recent Comments



PARTNERS