Jump to content

  • Log In with Google      Sign In   
  • Create Account






State Machines in Games – Part 5

Posted by frob, 16 March 2013 · 886 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: 37




September 2014 »

S M T W T F S
  1 23456
78910111213
14151617181920
21222324252627
282930    

Recent Comments

PARTNERS