Chapter 4. Indirect Control in a 2D Sprite-Based Game

In This Chapter

  • The bionic GUI

  • Controlling the controlees with controllers

  • Major Tom to finite state control

The Bionic GUI

What we’ve got so far is an interesting start for a 2D sprite-based game framework. However, nothing actually happens yet, and we can’t really do anything. As I said before, my idea for this game is that control of the characters will be indirect. That is to say, we won’t be able to take control of any character in the game and manually move it to one place or another. We also can’t take control of a character and manually force it to perform some action, as much fun as that might be.

What we can do is try to cajole a character into doing what we want, or we can issue orders that must be followed. As such, we need some method of showing what actions are available to the character. Since we will need a method of communicating stats to the player, we should use some kind of menu panel that slides out when a character is selected. We will need some kind of panel like that anyway, to give the player “assignments” in the game, so we might as well make one now.

Note

As with the previous chapter, Valued Reader, this chapter is not focused directly on the goal of creating VGAI and yummy emotional expression. Rather, this chapter continues to lay the groundwork and to build a bunch of base functionality into the game engine and gameplay code in an effort to make the inclusion of the VGAI and advanced AI easier. The material here may be a review for some, but not for others, so I want to go through the code in a detailed manner so that I can take it all for granted in Chapters 5, “Gidgets, Gadgets, and Gizbots,” and 6, “Pretending to Be an Individual: Synthetic Personality.”

To make things easy on ourselves, we will create a new solution in XNA called IndirectControlGame and import the 2DSpriteGame package. By doing this, we avoid all that yuckiness about changing namespaces, drinking milk, and crying.

To start, we just need to create a flat, bordered panel. It can be any color we want, so we might as well make black with a gray frame. The size is important, and we should make one piece of art that can serve multiple uses. For that reason, the art should be 850 pixels wide and 375 pixels tall. (Just go with it Valued Reader, and all shall be made apparent later—or if it helps to think of it this way, I’ll just say the dimension came to me in a dream.) With the artwork (it’s a beautiful piece of art, I might add) safely tucked away inside Content/GUI/GUIPanel.png, we need to add a public GuiPanel class inside the GUI namespace. This class will merely be a container for text and menu items, so it will be fairly simple. We will use multiple instances of this panel in the game, and some of them will need a reference to a character (much like the MouseHandler class), and then we need some magic in the CharacterManager to let the GuiPanel use it.

Basically, the design is this: There will be a left panel that contains the attributes and other information about the character that is selected with a left click. There will be a top panel in which we will deliver information about the game, instructions, hints, etc. There will also be a right panel, in which we will eventually display information about a character that is right clicked and the available actions that the left-click target can perform on the right-click target.

Let’s get started by adding that GuiPanel class. Right off the bat, we’re going to need another one of the enum identifier thingamabobs. In this case, the identifiers we know we will need are left, right, and top. We will include a member variable that is of the enum type to identify which type of panel the object will represent. We are doing this primarily to enable one class file to create multiple panels that are all essentially the same with a few minor behavioral differences. We will obviously need a 2D texture, a bounding box, and a ScreenText object (for the text we smash into these panels). Later we will add a few other GUI-type objects as well.

We will need a slew of constants to represent various screen positions, movement rates, the animation interval (for when the panel is moving), and about 48 bajillion properties. In other words, we want this:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

namespace _2DSpriteGame.GUI
{
    /// <summary>
    /// An implementation of a movable GUI panel that has mouse interaction.
    /// </summary>
    public class GuiPanel
    {
        #region members
        public enum PanelType
        {
            LEFT = 0, RIGHT, TOP,
        }

        private Texture2D tex;
        private Vector2 pos;
        private Rectangle bBox;
        private ScreenText textItems;
        private GuiPanel.PanelType pType;
        private int charId; //this will only be used in the left and right panels,
            and will represent the index of the target character for the panel
        private float deltaT;
        private Boolean isMovingOut; //this panel is moving toward the "active"
            position
        private Boolean isMovingAway; //this panel is moving toward the
            "inactive" position

        //bounding constants
        private const int w = 850; //the overall width of the panel
        private const int h = 375; //the overall height of the panel
        private const int clickx = 833; //the x offest for the clickable bounding
            box (left panel)
        private const int clickw = 17; //the width for the clickable bounding box
            (left and right panel)
        private const int clicky = 358; //the y offset for the clickable bounding
            box (top panel)
        private const int clickh = 17; //the height for the clickable bounding
            box (top panel)

        //other constants
        private const float INTERVAL = 1000f / 12f; //frame rate for moving the
            panels out and away

        //panel position constants
        private Vector2 UDMove = new Vector2(0,5); //the move step size for the
            top panel
        private Vector2 LRMove = new Vector2(5,0); //the move step size for the
            left and right panels
        private Vector2 LAway = new Vector2(-828, 25); //the "inactive" position
            of the left panel
        private Vector2 LOut = new Vector2(-578,25); //the "active" position of
            the left panel
        private Vector2 RAway = new Vector2(1007, 25); //the "inactive" position
            of the right panel
        private Vector2 ROut = new Vector2(757,25); //the "active" poisition of
            the right panel
        private Vector2 TAway = new Vector2(87,-355); //the "inactive" position
            of the top panel
        private Vector2 TOut = new Vector2(87,0); //the "active" position of the
            top panel

        //text position constants
        private Vector2 LeftSuma = new Vector2(600, 30); //the left panel text
            area
        private Vector2 RightSuma = new Vector2(35, 30); //the right panel text
            area
        private Vector2 Title = new Vector2(325, 40); //a centered position for
            the title in the top panel
        private Vector2 TopText = new Vector2(35, 80); //the top panel text area
            #endregion

        #region properties
        public Vector2 Position
        {
            get { return pos; }
            set { pos = value; }
         }
         public Rectangle Panel
         {
            get { return bBox; }
         }
         public int TargetCharacter
         {
            get { return charId; }
            set { charId = value; }
         }
         public GuiPanel.PanelType PType
         {
            get { return pType; }
         }
         public Boolean MovingOut
         {
            get { return isMovingOut; }
            set { isMovingOut = value; }
         }
         public Boolean MovingAway
         {
            get { return isMovingAway; }
            set { isMovingAway = value; }
         }
         public ScreenText Text
         {
            get { return textItems; }
         }
         public int LEFT
         {
            get { return bBox.Left; }
         }
         public int RIGHT
         {
            get { return bBox.Right; }
         }
         public int TOP
         {
            get { return bBox.Top; }
         }
         public int BOTTOM
         {
            get { return bBox.Bottom; }
         }
         public Vector2 LeftSide
         {
            get { return LeftSuma; }
         }
         public Vector2 RightSide
         {
            get { return RightSuma; }
         }
         public Vector2 TopSide
         {
            get { return TopText; }
         }
         #endregion

As usual, the next bits to cover are the constructors and initialization methods, as drab and dreary as they are. (Maybe we should paint them yellow?) Nothing here is rocket science, but we do take some more complicated steps inside the constructor. If you recall from the previous discussion, we are using the GuiPanel.PanelType member to control certain behaviors in the instantiated object. We will start this in the constructor, as each panel will be set up slightly differently depending on its use. Here’s the code:

#region constructors / init
public GuiPanel(GuiPanel.PanelType type)
{
    pType = type;

    textItems = new ScreenText();
    textItems.AbsolutePositioning = false;

    if (pType == GuiPanel.PanelType.LEFT)
    {
        pos = LAway;
        textItems.Print("Select a Suma", LeftSuma, Color.White,
            (int)ScreenText.FontName.SUBHEAD_BOLD);
        bBox = new Rectangle((int)pos.X + clickx, (int)pos.Y, clickw, h);
    }
    else if (pType == GuiPanel.PanelType.RIGHT)
    {
        pos = RAway;
        textItems.Print("Select a Suma", RightSuma, Color.White,
        (int)ScreenText.FontName.SUBHEAD_BOLD);
        bBox = new Rectangle((int)pos.X, (int)pos.Y, clickw, h);
    }
    else if (pType == GuiPanel.PanelType.TOP)
    {
        pos = TAway;
        textItems.Print("Suma Sumi", Title, Color.White,
            (int)ScreenText.FontName.TITLE);
        textItems.Print("Greetings, Earthling! Behold the great Suma Sumi - -
            the long boring journey of the", TopText, Color.White,
            (int)ScreenText.FontName.DEFAULT);
        textItems.Print("Suma that ends in your destruction! Make yourself
            useful. Keep us entertained!", new Vector2(TopText.X,
            TopText.Y + 20), Color.White, (int)ScreenText.FontName.DEFAULT);
        textItems.Print("Or we will have to destroy you (or something).", new
            Vector2(TopText.X, TopText.Y + 60), Color.White,
            (int)ScreenText.FontName.DEFAULT_BOLD);
        textItems.Print("This game will challenge you to perform certain feats
            of social communication - - raw", new Vector2(TopText.X,
            TopText.Y + 100), Color.White, (int)ScreenText.FontName.DEFAULT);
        textItems.Print("animal magnetism, chatty-Kathyism, get-down-on-itism,
            and maybe even exorcism.", new Vector2(TopText.X, TopText.Y + 120),
            Color.White, (int)ScreenText.FontName.DEFAULT);
        bBox = new Rectangle((int)pos.X, (int)pos.Y + clicky, w, clickh);
    }
    else
    {
        //oops.
        pos = new Vector2(-5000, -5000);
        bBox = new Rectangle((int)pos.X, (int)pos.Y, w, h);
    }

    textItems.Position = pos;
    charId = -1;
    deltaT = 0f;
    isMovingOut = false;
    isMovingAway = false;
}
/// <summary>
/// Loads the content for all components contained in the panel and for the panel
/// itself
/// </summary>
/// <param name="Content"></param>
public void LoadContent(ContentManager Content)
{
    textItems.LoadContent(Content);

    tex = Content.Load<Texture2D>("GUI/GuiPanel");
}
#endregion

Wait … I can hear you muttering under your breath, Valued Reader. I know why, and I can sympathize. I’ve pulled another fast one by adding to the functionality of the ScreenText.cs class. Please ignore that for now; I’ll come back to it in a few pages. In a nutshell, I changed that class so that it can either print the messages on the screen in coordinates that are absolute screen points or print to the screen using coordinates relative to some other screen object.

In the constructor, you can see how each object will be slightly different based on the GuiPanel.PanelType member. First, the starting position of each object will vary a bit. We are starting all panels in the “away” or “inactive” position for now. The second difference is the initial message and relative position we place in the ScreenText object. Finally, we instantiate the bounding box object differently for all three types, making the “leading edge” the thing you have to click on to hide and unhide the panel. The LoadContent() method is a simple pass-through for the ScreenText object and loads the content for the GuiPanel. Easy peasy.

We need utility methods next, and again, one is fairly complicated, one is stone simple, and the third is fairly basic. The Draw() method is the easy one—it’s responsible for calling the spriteBatch.Draw() method on its texture and then calling the ScreenText object’s Draw() method. Here’s the code:

/// <summary>
/// Draws all Gui components contained in the panel
/// </summary>
/// <param name="sb"></param>
public void Draw(SpriteBatch sb)
{
    sb.Draw(tex, pos, Color.White);
    textItems.Draw(sb);
}

The next easiest method is simply called Move() and is responsible for letting a chunk of outside code mandate that the panel “move” in or out, by setting the status of the two Boolean members (isMovingAway and isMovingOut). This is pretty simple; the code looks like this:

/// <summary>
/// Sets internal attributes that will cause the panel to move "out" or "away"
/// </summary>
public void Move()
{
    if (pType == GuiPanel.PanelType.LEFT)
    {
        if (pos.X <= LAway.X)
        {
            isMovingOut = true;
        }
        else if (pos.X >= LOut.X)
        {
            isMovingAway = true;
        }
    }
    else if (pType == GuiPanel.PanelType.RIGHT)
    {
        if (pos.X >= RAway.X)
        {
            isMovingOut = true;
        }
        else if (pos.X <= ROut.X)
        {
            isMovingAway = true;
        }
    }
    else if (pType == GuiPanel.PanelType.TOP)
    {
        if (pos.Y <= TAway.Y)
        {
            isMovingOut = true;
        }
        else if (pos.Y >= TOut.Y)
        {
            isMovingAway = true;
        }
    }
}

Finally, we are left with the Update() method, which isn’t really hard, but has a lot to structure and keep track of. The functioning starts out by determining if the panel is moving—either out or away. Inside both code blocks, the next step is to check to see if we’ve exhausted the animation interval and, if so, figure out how to move the panel. In either case, following the updating of the panel’s position, we pass the new position into the ScreenText object to keep the relative placement scheme updated. It should be easy to see that when the panel is moving out, it is moving in one direction, and when the panel is moving away, it is moving in exactly the opposite direction. Of course, this makes the two code blocks very similar, as you can see here:

/// <summary>
/// Update processing for the panel's movement and for all subcomponents
/// </summary>
/// <param name="gameTime"></param>
public void Update(GameTime gameTime)
{
    if (isMovingOut)
    {
        deltaT += (float)gameTime.ElapsedGameTime.TotalMilliseconds;
        if (deltaT > INTERVAL)
        {
            if (pType == GuiPanel.PanelType.LEFT)
            {
                pos += LRMove;
                if (pos.X >= LOut.X)
                {
                    pos = LOut;
                    isMovingOut = false;
                    deltaT = 0;
                }
                bBox.X = (int)pos.X + clickx;
                bBox.Y = (int)pos.Y;
            }
            else if (pType == GuiPanel.PanelType.RIGHT)
            {
                pos -= LRMove;
                if (pos.X <= ROut.X)
                {
                    pos = ROut;
                    isMovingOut = false;
                    deltaT = 0;
                }
                bBox.X = (int)pos.X;
                bBox.Y = (int)pos.Y;
            }
            else if (pType == GuiPanel.PanelType.TOP)
            {
                pos += UDMove;
                if (pos.Y >= TOut.Y)
                {
                    pos = TOut;
                    isMovingOut = false;
                    deltaT = 0;
                }
                bBox.X = (int)pos.X;
                bBox.Y = (int)pos.Y + clicky;
            }
        }
    }
    else if (isMovingAway)
    {
        deltaT += (float)gameTime.ElapsedGameTime.TotalMilliseconds;
        if (deltaT > INTERVAL)
        {
            if (pType == GuiPanel.PanelType.LEFT)
            {
                pos -= LRMove;
                if (pos.X <= LAway.X)
                {
                    pos = LAway;
                    isMovingAway = false;
                    deltaT = 0;
                }
                bBox.X = (int)pos.X + clickx;
                bBox.Y = (int)pos.Y;
            }
            else if (pType == GuiPanel.PanelType.RIGHT)
            {
                pos += LRMove;
                if (pos.X >= RAway.X)
                {
                    pos = RAway;
                    isMovingAway = false;
                    deltaT = 0;
                }
                bBox.X = (int)pos.X;
                bBox.Y = (int)pos.Y;
           }
           else if (pType == GuiPanel.PanelType.TOP)
           {
               pos -= UDMove;
               if (pos.Y <= TAway.Y)
               {
                   pos = TAway;
                   isMovingAway = false;
                   deltaT = 0;
               }
               bBox.X = (int)pos.X;
               bBox.Y = (int)pos.Y + clicky;
           }
       }
    }

    textItems.Position = pos;
}

That’s it for the entire class. As promised, here are the changes (in bold) that I made to the ScreenText class to allow the absolute versus relative positioning tricks:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

namespace _2DSpriteGame.GUI
{
    /// <summary>
    /// This class keeps track of messages to be drawn to the screen and the fonts
    /// with which to draw them
    /// </summary>
    public class ScreenText
    {
        #region members
        //font controller helper
        public enum FontName
        {
        DEFAULT=0, DEFAULT_BOLD, SUBHEAD, SUBHEAD_BOLD, TITLE
    }

    //message data
    internal class Message
    {
        public String message;
        public double deltaT;
        public Vector2 position;
        public Color color;
        public int font;

        public Message(String m, double t)
        {
            message = m;
            deltaT = t;
            position = Vector2.Zero;
            color = Color.White;
            font = 0;
        }

        public Message(String m, double t, Vector2 p, Color c, int f)
        {
            message = m;
            deltaT = t;
            position = p;
            color = c;
            font = f;
        }
    }

    private SpriteFont[] fonts;
    private Message[] messages;
    private int currentMsg;
    private Boolean absPosition;
    private Vector2 pos;

    //constants
    private const int MAX_FONTS = 5; //update the FontName enum if this
        number is increased!
    private const int MAX_MSGS = 32;
    private const double DEF_TIMER = 999999;
    #endregion
    #region properties
    public Boolean AbsolutePositioning
    {
         get { return absPosition; }
         set { absPosition = value; }
    }
    public Vector2 Position
    {
         get { return pos; }
         set { pos = value; }
    }
    #endregion

    #region constructors / init
    public ScreenText()
    {
        fonts = new SpriteFont[MAX_FONTS];
        messages = new Message[MAX_MSGS];
        currentMsg = -1;
        absPosition = true;
    }
    public void LoadContent(ContentManager Content)
    {
        //update as more fonts are added
        fonts[(int)ScreenText.FontName.DEFAULT] = Content.Load
            <SpriteFont>("Fonts/Consolas12");
        fonts[(int)ScreenText.FontName.DEFAULT_BOLD] = Content.Load
            <SpriteFont>("Fonts/Consolas12Bold");
        fonts[(int)ScreenText.FontName.SUBHEAD] = Content.Load
            <SpriteFont>("Fonts/Consolas14");
        fonts[(int)ScreenText.FontName.SUBHEAD_BOLD] = Content.Load
            <SpriteFont>("Fonts/Consolas14Bold");
        fonts[(int)ScreenText.FontName.TITLE] = Content.Load
            <SpriteFont>("Fonts/Consolas18");
    }
    #endregion

    #region utilties
    /// <summary>
    /// print a message with all the defaults and with the default timer value
    /// </summary>
    /// <param name="message">The string to be printed</param>
    public void Print(String message)
    {
        Message m = new Message(message, DEF_TIMER);
        Add(m);
    }
    /// <summary>
    /// print a message with all the defaults and with custom timer value
    /// </summary>
    /// <param name="message">The string to be printed</param>
    /// <param name="t">time in millieseconds</param>
    public void Print(String message, double t)
    {
        Message m = new Message(message, t);
        Add(m);
    }
    /// <summary>
    /// print a message at a custom position, with a custom color and font, and
    /// with the default timer value
    /// </summary>
    /// <param name="message">The string to be printed</param>
    /// <param name="pos">The position of the message</param>
    /// <param name="c">The color of the text to draw</param>
    /// <param name="f">The index of the font to use</param>
    public void Print(String message, Vector2 pos, Color c, int f)
    {
        Message m = new Message(message, DEF_TIMER, pos, c, f);
        Add(m);
    }
    /// <summary>
    /// print a message at a custom position, with a custom color, font and
    /// timer value
    /// </summary>
    /// <param name="message">The string to be printed</param>
    /// <param name="t">time in millieseconds</param>
    /// <param name="pos">The position of the message</param>
    /// <param name="c">The color of the text to draw</param>
    /// <param name="f">The index of the font to use</param>
    public void Print(String message, double t, Vector2 pos, Color c, int f)
    {
        Message m = new Message(message, t, pos, c, f);
        Add(m);
    }
    /// <summary>
    /// dump all the messages and reset the list
    /// </summary>
    public void Clear()
    {
        messages = new Message[MAX_MSGS];
        currentMsg = -1;
    }
    /// <summary>
    /// add a new message in the proper place in the array (last!)
    /// </summary>
    /// <param name="m">The message to add</param>
    private void Add(Message m)
    {
        currentMsg++;
        if (currentMsg >= MAX_MSGS)
        {
            for (int i = 1; i < MAX_MSGS; i++)
            {
                messages[i - 1] = messages[i];
            }

            currentMsg = MAX_MSGS - 1;
        }
        messages[currentMsg] = m;
    }
    /// <summary>
    /// remove the message at index i
    /// </summary>
    /// <param name="i">The index of the message to remove</param>
    public void RemoveAt(int i)
    {
            if (currentMsg == -1)
            {
                return;
            }
            if (messages[i] != null)
            {
                messages[i] = null;
            }
            for (int j = i; j < MAX_MSGS; j++)
            {
                messages[j - 1] = messages[j];
            }
            currentMsg--;
        }
        /// <summary>
        /// remove the newest message in the list
        /// </summary>
        public void RemoveNewest()
        {
            if (currentMsg == -1)
            {
                return;
            }
            if (messages[currentMsg] != null)
            {
                messages[currentMsg] = null;
            }
            currentMsg--;
        }
        /// <summary>
        /// remove the oldest message in the list
        /// </summary>
        public void RemoveOldest()
        {
            if (currentMsg == -1)
            {
                return;
            }
            for (int i = 1; i < MAX_MSGS; i++)
            {
                 messages[i - 1] = messages[i];
            }
            currentMsg--;
        }
        /// <summary>
        /// update the message list
        /// </summary>
        /// <param name="gameTime"></param>
        public void Update(GameTime gameTime)
        {
            double tElapsed = gameTime.ElapsedGameTime.TotalMilliseconds;

            //step through the messages
            for (int i = 0; i < MAX_MSGS; i++)
            {
                //if a message is set to expire
                if (messages[i].deltaT != 999999)
                {
                   //update its time left in this world
                   messages[i].deltaT -= tElapsed;
                   //test to see if it is still viable
                   if (messages[i].deltaT < 0)
                   {
                      //and kill it if necessary
                      RemoveAt(i);
                      i--;
                   }
                }
            }
        }
        /// <summary>
        /// draw the messages to the screen
        /// </summary>
        /// <param name="sb"></param>
        public void Draw(SpriteBatch sb)
        {
            //step through the messages list
            if (currentMsg != -1)
            {
                Vector2 drawPos;
                for (int i = 0; i <= currentMsg; i++)
                {
                     //and draw them all
                     if ( absPosition )
                     {
                          drawPos = messages[i].position;
                     }
                     else
                     {
                          drawPos = messages[i].position + pos;
                     }

                     sb.DrawString(fonts[messages[i].font], messages[i].
                          message, drawPos, messages[i].color);
                }
            }
        }
        #endregion
    }
}

You can see this wasn’t a big change and that it basically boils down to maintaining a few members: a Vector2 for position and a Boolean to determine whether to use it or not; and a test against that Boolean that determines whether or not to add the stored position to the message’s position. Bam! Relative-screen-positioning-fu for cheap.

Next, it would be easiest (and it would be in line our existing design choices) to add a manager for the GUI. We’ll call it the GuiManager class, and for now it will contain and police our three panels. Peruse this:

using System;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace _2DSpriteGame.GUI
{
    /// <summary>
    /// A manager implementation for Gui objects
    /// </summary>
    public class GuiManager
    {
        #region members
        private GuiPanel LeftPanel;
        private GuiPanel RightPanel;
        private GuiPanel TopPanel;
        #endregion

        #region properties
        public ScreenText LeftText
        {
            get { return LeftPanel.Text; }
        }
        public ScreenText RightText
        {
            get { return RightPanel.Text; }
        }
        public ScreenText TopText
        {
            get { return TopPanel.Text; }
        }
        public Vector2 LeftTextPosition
        {
            get { return LeftPanel.LeftSide; }
        }
        public Vector2 RightTextPosition
        {
            get { return RightPanel.RightSide; }
        }
        public Vector2 TopTextPosition
        {
            get { return TopPanel.TopSide; }
        }
        #endregion

        #region constructors / init
        public GuiManager()
        {
            LeftPanel = new GuiPanel(GuiPanel.PanelType.LEFT);
            RightPanel = new GuiPanel(GuiPanel.PanelType.RIGHT);
            TopPanel = new GuiPanel(GuiPanel.PanelType.TOP);
        }
        /// <summary>
        /// Loads the content for all managed objects
        /// </summary>
        /// <param name="Content"></param>
        public void LoadContent(ContentManager Content)
        {
            LeftPanel.LoadContent(Content);
            RightPanel.LoadContent(Content);
            TopPanel.LoadContent(Content);
        }
        #endregion

        #region utilities
        /// <summary>
        /// to be used only for programmatic opening of the panels (not when user
        /// clicks on the panel)
        /// </summary>
        /// <param name="pt"></param>
        public void OpenPanel(GuiPanel.PanelType pt)
        {
            if (pt == GuiPanel.PanelType.LEFT)
            {
                LeftPanel.MovingAway = false;
                LeftPanel.MovingOut = true;
            }
            else if (pt == GuiPanel.PanelType.RIGHT)
            {
                RightPanel.MovingAway = false;
                RightPanel.MovingOut = true;
            }
            else if (pt == GuiPanel.PanelType.TOP)
            {
                TopPanel.MovingAway = false;
                TopPanel.MovingOut = true;
            }
        }
        /// <summary>
        /// Causes the left panel to move
        /// </summary>
        public void LeftMove()
        {
            LeftPanel.Move();
        }
        /// <summary>
        /// Causes the right panel to move
        /// </summary>
        public void RightMove()
        {
            RightPanel.Move();
        }
        /// <summary>
        /// Causes the top panel to move
        /// </summary>
        public void TopMove()
        {
            TopPanel.Move();
        }
        /// <summary>
        /// Basic collision detection--is the Rectangle parameter intersecting
        /// this panel?
        /// </summary>
        /// <param name="pt">Which panel to test against</param>
        /// <param name="hitter">The bounding box to collide with</param>
        /// <returns>true iff the Rectangle parameter is intersecting the local
        /// bounding box</returns>
        public Boolean Hit(GuiPanel.PanelType pt, Rectangle hitter)
        {
            if (pt == GuiPanel.PanelType.LEFT)
            {
                if (LeftPanel.Panel.Intersects(hitter))
                {
                    return true;
                }
            }
            else if (pt == GuiPanel.PanelType.RIGHT)
            {
                if (RightPanel.Panel.Intersects(hitter))
                {
                    return true;
                }
            }
            else if (pt == GuiPanel.PanelType.TOP)
            {
                if (TopPanel.Panel.Intersects(hitter))
                {
                    return true;
                }
            }

            return false;
        }
        /// <summary>
        /// Update processing for all managed objects
        /// </summary>
        /// <param name="gameTime"></param>
        public void Update(GameTime gameTime)
        {
            LeftPanel.Update(gameTime);
            RightPanel.Update(gameTime);
            TopPanel.Update(gameTime);
        }
        /// <summary>
        /// Draws all managed objects
        /// </summary>
        /// <param name="sb"></param>
        public void Draw(SpriteBatch sb)
        {
            LeftPanel.Draw(sb);
            RightPanel.Draw(sb);
            TopPanel.Draw(sb);
        }
        #endregion
    }
}

Almost all of that code is dead-bang obvious, but there are a few utility methods that bear discussing. The first is the OpenPanel() method, which exists solely to give programmatic control to opening the panels. No water-walking involved at all—in fact, it’s fairly boring code—you just set some internal attributes that cause a change in state. Next up are three pass-through methods that call the Move() method in the appropriate panel object. We will use these in collision detection and mouse handling. Finally, there’s a Hit() method that computes collision detection between all three panels and the mouse. That’s it—everything else is so standard it might make kittens cry.

Hand those kittens a tissue, however, because before we can use any of this, we need to enable Mr. MouseHandler with a few super powers. Namely, good ol’ MH needs to tell the world about a few clicks …. Here is the updated MouseHandler class with changes in bold:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;


namespace _2DSpriteGame.GameState.Utilities
{
    /// <summary>
    /// This class takes care of a mouse input device, maintaining the state of
    /// the mouse and its art assets (complete with a bounding box for box-on-box
    /// collision detection)
    /// </summary>
    public class MouseHandler
    {
        #region members
        public enum MouseTypes
        {
            IDLE = 0, LEFTDOWN, LEFTCLICK1, LEFTCLICK2, LEFTCLICK3, HUDMOUSE,
        }
        private MouseSprite mouse;
        private MouseSprite[] mice;
        private Rectangle bBox;
        private MouseState mouseState;
        private MouseState oldState;
        private Boolean drawMouse;
        private Boolean guiLeftClick;
        private float deltaT;
        private int mouseOver;

        //bounding constants
        private const int w = 1;
        private const int h = 1;

        //other constants
        private const int NUM_MICE = 5; //if more mice are added, remember to name
            them in the MouseTypes enum!
        private const float INTERVAL = 1000f / 16f;
        #endregion

        #region properties
        public Rectangle Mouse
        {
            get { return bBox; }
        }
        public Boolean DrawMouse
        {
            get { return drawMouse; }
            set { drawMouse = value; }
        }
        public Boolean GuiLClick
        {
            get { return guiLeftClick; }
            set { guiLeftClick = value; }
        }
        public int MouseOver
        {
            get { return mouseOver; }
            set { mouseOver = value; }
        }
        public int LEFT
        {
            get { return bBox.Left; }
        }
        public int RIGHT
        {
            get { return bBox.Right; }
        }
        public int TOP
        {
            get { return bBox.Top; }
        }
        public int BOTTOM
        {
            get { return bBox.Bottom; }
        }
        #endregion

        #region constructor / init
        public MouseHandler()
        {
            bBox = new Rectangle(0, 0, w, h);
            mice = new MouseSprite[NUM_MICE];
            drawMouse = false;
            guiLeftClick = false;
            mouseOver = -1;
        }

        public void LoadContent(ContentManager content)
        {
            mice[(int)MouseTypes.IDLE] = new MouseSprite((int)MouseTypes.
                IDLE, content.Load<Texture2D>("Mouse/Mouse"),
                new Vector2(0, 0), 1.0f);
            mice[(int)MouseTypes.LEFTDOWN] = new MouseSprite((int)
                MouseTypes.LEFTDOWN, content.Load<Texture2D>
                ("Mouse/LeftMouse"), new Vector2(0, 0), 1.0f);
            mice[(int)MouseTypes.LEFTCLICK1] = new MouseSprite((int)
                MouseTypes.LEFTCLICK1, content.Load<Texture2D>
                ("Mouse/LeftMouseUp1"), new Vector2(0, 0), 1.0f);
            mice[(int)MouseTypes.LEFTCLICK2] = new MouseSprite((int)
                MouseTypes.LEFTCLICK2, content.Load<Texture2D>
                ("Mouse/LeftMouseUp2"), new Vector2(0, 0), 1.0f);
            mice[(int)MouseTypes.LEFTCLICK3] = new MouseSprite((int)
                MouseTypes.LEFTCLICK3, content.Load<Texture2D>
                ("Mouse/LeftMouseUp3"), new Vector2(0, 0), 1.0f);
                mouse = mice[(int)MouseTypes.IDLE];
            }
            #endregion

            #region utilities
            public void Update(GameTime gameTime)
            {
                Character.CharacterManager cm = Game1.GameStateMan.CharManager;
                GUI.GuiManager gm = Game1.GameStateMan.GuiManager;

                //capture the last state of the mouse and the current state of the
                //mouse
                oldState = mouseState;
                mouseState = Microsoft.Xna.Framework.Input.Mouse.GetState();

                //we need an easy way to tell if the player is clicking on the Gui or on
                //a character…
                //we've already set mouseDraw based on the mouse bounds checking.
                if (drawMouse)
                {
                    //mouse is in the character area
                    //check the state of the left button for graphics changes
                    if (mouseState.LeftButton == ButtonState.Pressed &&
                        oldState.LeftButton == ButtonState.Released)
                    {
                        //new left-button-down state
                        mouse = mice[(int)MouseTypes.LEFTDOWN];
                    }

                    //handle left-click animation
                    if (mouse.Identity != (int)MouseTypes.IDLE &&
                        mouseState.LeftButton == ButtonState.Released)
                    {
                        deltaT += (float)gameTime.ElapsedGameTime.
                            TotalMilliseconds;
                        if (deltaT > INTERVAL)
                        {
                            if (mouse.Identity == (int)MouseTypes.LEFTCLICK1)
                            {
                                mouse = mice[(int)MouseTypes.LEFTCLICK2];
                            }
                            else if (mouse.Identity == (int)MouseTypes.LEFTCLICK2)
                            }
                                mouse = mice[(int)MouseTypes.LEFTCLICK3];
                            }
                            else if (mouse.Identity == (int)MouseTypes.LEFTCLICK3)
                            {
                                mouse = mice[(int)MouseTypes.IDLE];
                            }
                            deltaT = 0f;
                       }
                   }


                   //handle left click
                   if (oldState.LeftButton == ButtonState.Pressed &&
                       mouseState.LeftButton == ButtonState.Released)
                   {
                       //a mouse click occurred
                       mouse = mice[(int)MouseTypes.LEFTCLICK1];
                       deltaT = 0f;

                       if (mouseOver != -1)
                       {
                           gm.OpenPanel(GUI.GuiPanel.PanelType.RIGHT);
                       }
                   }
              }
              else
              {
                  if ((oldState.LeftButton = = ButtonState.Pressed &&
                      mouseState.LeftButton = = ButtonState.Released ) && midClick)
                  {
                      //mouse is in the Gui area
                      //handle left click
                      guiLeftClick = true;
                  }
              }

              //update the position of the mouse with the new screen coordinates
              //(we'll handle collision detection later)
              bBox.X = mouseState.X;
              bBox.Y = mouseState.Y;
              mouse.Position = new Vector2(mouseState.X, mouseState.Y);


        }
        public void Draw(SpriteBatch sb)
        {
            if (drawMouse)
            {
                mouse.Draw(sb);
            }
        }
        #endregion
    }
}

We’re not done yet, however. We need to add this marvel of object management to the GameStateManager class and then empower the CollisionHandler to do good stuff for us. In GameStateManager.cs, we’ll take all the same steps as we’ve taken for all the other managers. I’m not going to rehash it here, with the exception of talking about the order-of-execution changes in the Update() method:

public void Update(GameTime gameTime)
{
    bgm.Update(gameTime);
    mh.Update(gameTime);

    //bounds checking before the characters move
    ch.CheckBounds();
    ch.DoCollision();
    ch.CheckMouseBounds();
    ch.CheckMouseCollision();
    ch.DoGuiPanelCollision();

    gm.Update(gameTime);
    cm.Update(gameTime);
}

We also need to make an additional change in the Game1 class Update() method:

protected override void Update(GameTime gameTime)
{
    // Allows the game to exit
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        this.Exit();

    gsm.Update(gameTime);

    if (gsm.MHandler.DrawMouse)
    {
        IsMouseVisible = false;
    }
    else
    {
        IsMouseVisible = true;
    }

    base.Update(gameTime);
}

What is changed is this: We detect whether the mouse is in bounds by checking the state of the MouseHandler.DrawMouse property, and then set the default system mouse to visible or invisible appropriately. This allows us to keep the funky magic-wand mouse for interacting with characters and use a default mouse for GUI clickiness.

In the CollisionHandler, we just need to add one method to check for mouse-GUI collisions. It looks like this:

/// <summary>
/// Performs collision detection for all GUI objects
/// </summary>
public void DoGuiPanelCollision()
{
    GUI.GuiManager gm = Game1.GameStateMan.GuiManager;
    MouseHandler mh = Game1.GameStateMan.MHandler;

    if (mh.GuiLClick)
    {

        if (gm.Hit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse))
        {
            gm.LeftMove();
        }
        if (gm.Hit(GUI.GuiPanel.PanelType.RIGHT, mh.Mouse))
        {
            gm.RightMove();
        }
        if (gm.Hit(GUI.GuiPanel.PanelType.TOP, mh.Mouse))
        {
            gm.TopMove();
        }
        mh.GuiLClick = false;
    }

}

What we have here is a failure to fail to move the GUI! Run it and check out all the slidey-panel-goodness-fu.

Now that we have enough GUI horsepower to support indirect control, let’s move on to bossing our characters around a bit. We’ll need to add a GUI component or two, but it’ll be easy now that we have all the rest.

We need a button. I want to press a button, and there are no buttons to press. How can you build a game without buttons?! You can’t, that’s how. The GuiButton class just needs to correctly display the right artwork in the right place in our system (which is another way of saying that the GuiButton class will always be contained in a panel, the state of which will be checked to cause behavior changes elsewhere rather than directly causing those changes from within the class itself). Here’s the code:

using System;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;

namespace _2DSpriteGame.GUI
{
    /// <summary>
    /// A clickable GUI button implementation from scratch
    /// </summary>
    public class GuiButton
    {
        #region members
        public enum PressedState
        {
            RELEASED_OFF = 0, PRESSED_OFF, RELEASED_ON, PRESSED_ON,
        }
        public enum ButtonType
        {
            MOMENTARY = 0, TOGGLE,
        }

        private Texture2D[] bTextures;   //a list of textures to represent the
            "pressed states" of the button
        private GuiButton.PressedState state; //the current state of the button
        private GuiButton.ButtonType type; //the type of the button
        private Rectangle bBox; //bounding rectangle for collision detection
        private ScreenText label; //the screen text the button will display
        private String name; //the button's name
        private Boolean draw; //a flag used to determine whether the button
            should be drawn or not

        //bounding constants
        private const int w = 125;
        private const int h = 38;

        //positioning members
        private Vector2 pos; //the relative position of the button
        private Vector2 drawPos; //the calculated absolute position of the button
        private Vector2 labelPosition; //the position of the button text

        //other constants
        private const int NUM_TEXTURES = 4;   //be sure to update based on size of
            PressedState enum

        private Color rOffColor = Color.DarkGray;
        private Color pOffColor = Color.Black;
        private Color rOnColor = Color.Black;
        private Color pOnColor = Color.Black;
        #endregion

        #region properties
        public Vector2 Position
        {
            get { return pos; }
            set { pos = value; }
        }
        public GuiButton.PressedState State
        {
            get { return state; }
            set
            {
                state = value;
                if (state == GuiButton.PressedState.RELEASED_OFF)
                {
                    label.Clear();
                    label.Print(name, labelPosition, rOffColor, 0);
                }
                else if (state == GuiButton.PressedState.PRESSED_OFF)
                {
                    label.Clear();
                    label.Print(name, labelPosition, pOffColor, 0);
                }
                else if (state == GuiButton.PressedState.RELEASED_ON)
                {
                    label.Clear();
                    label.Print(name, labelPosition, rOnColor, 0);
                }
                else if (state == GuiButton.PressedState.PRESSED_ON)
                {
                    label.Clear();
                    label.Print(name, labelPosition, pOnColor, 0);
                }
                else
                {
                    label.Clear();
                    label.Print(name, labelPosition, Color.Black, 0);
                }
            }
       }
       public GuiButton.ButtonType Type
       {
           get { return type; }
       }
       public Rectangle Button
       {
           get { return bBox; }
       }
       public Boolean Enabled
       {
           get { return draw; }
           set { draw = value; }
       }
       #endregion

       #region constructors / init
       public GuiButton(GuiButton.PressedState s, GuiButton.ButtonType t,
           Vector2 bPos, String n, Vector2 nPos)
       {
           state = s;
           type = t;
           name = n;
           pos = bPos;
           drawPos = pos;
           labelPosition = nPos;

           label = new ScreenText();
           label.AbsolutePositioning = false;
           label.Print(name, labelPosition, rOffColor, 0);

           bBox = new Rectangle((int)pos.X, (int)pos.Y, w, h);
           bTextures = new Texture2D[NUM_TEXTURES];
        }
        /// <summary>
        /// Content loader for all the textures and text components of the button
        /// </summary>
        /// <param name="Content"></param>
        public void LoadContent(ContentManager Content)
        {
             bTextures[(int)GuiButton.PressedState.RELEASED_OFF] =
                 Content.Load<Texture2D>("GUI/ButtonOffReleased");
             bTextures[(int)GuiButton.PressedState.PRESSED_OFF] =
                 Content.Load<Texture2D>("GUI/ButtonOffPressed");
             bTextures[(int)GuiButton.PressedState.RELEASED_ON] =
                 Content.Load<Texture2D>("GUI/ButtonOnReleased");
             bTextures[(int)GuiButton.PressedState.PRESSED_ON] =
                 Content.Load<Texture2D>("GUI/ButtonOnPressed");

             label.LoadContent(Content);
        }
        #endregion

        #region utility methods
        /// <summary>
        /// Update processing for this button. Position is relative to the
        /// parent panel
        /// </summary>
        /// <param name="parentPos">The position of the parent panel</param>
        public void Update(Vector2 parentPos)
        {
            //use relative positioning to the button's parent container
            drawPos = pos + parentPos;
            bBox.X = (int)drawPos.X;
            bBox.Y = (int)drawPos.Y;
            label.Position = drawPos;
        }
        /// <summary>
        /// Draws the button if the button is enabled
        /// </summary>
        /// <param name="sb"></param>
        public void Draw(SpriteBatch sb)
        {
            if (draw)
            {
                sb.Draw(bTextures[(int)state], drawPos, Color.White);
                    label.Draw(sb);
            }
        }
        #endregion
    }
}

This code is very straightforward. The only potential kink is in the Update() method, where we add the pos member to the passed in position parameter (parentPos) to update drawPos. This is the calculation that converts the relative position into the absolute screen position (like we did in ScreenText.cs).

To add buttons to the GuiPanel class, we just add a few members like so:

private GuiButton[] buttons;
private String[] lButtonNames = new String[NUM_BUTTONS] { "Move To", "Stop",
"Forget", "Dance", "Sleep", "Free" };
private String[] rButtonNames = { "Go to", "Converse", "Chase", "Dance",
"Evade", "Fight" };

and

private const int NUM_BUTTONS = 6;

plus

//button position constants
private Vector2 LeftButtons = new Vector2(580, 245);
private Vector2 RightButtons = new Vector2(18, 245);

followed by instantiation code in the constructor and the LoadContent() methods:

#region constructors / init
public GuiPanel(GuiPanel.PanelType type)
{
    pType = type;

    textItems = new ScreenText();
    textItems.AbsolutePositioning = false;

    if (pType == GuiPanel.PanelType.LEFT)
    {
        pos = LAway;
        textItems.Print("Select a Suma", LeftSuma, Color.White,
            (int)ScreenText.FontName.SUBHEAD_BOLD);
        bBox = new Rectangle((int)pos.X + clickx, (int)pos.Y, clickw, h);

        Vector2 bPos = LeftButtons;
        Vector2 nPos = new Vector2(20,5);
        buttons = new GuiButton[NUM_BUTTONS];
        for (int i = 0; i < NUM_BUTTONS; i++)
        {
            buttons[i] = new GuiButton(GuiButton.PressedState.RELEASED_OFF,
                GuiButton.ButtonType.TOGGLE, bPos, lButtonNames[i], nPos);
            buttons[i].Enabled = false;
            if ((i % 2) = = 0)
            {
                bPos.X += 125;
            }
            else
            {
                bPos.X = LeftButtons.X;
            }

            if (i > 0 && ((i % 2) != 0))
            {
                bPos.Y += 40;
            }
        }

    }
    else if (pType == GuiPanel.PanelType.RIGHT)
    {
        pos = RAway;
        textItems.Print("Select a Suma", RightSuma, Color.White,
            (int)ScreenText.FontName.SUBHEAD_BOLD);
        bBox = new Rectangle((int)pos.X, (int)pos.Y, clickw, h);
        Vector2 bPos = RightButtons;
        Vector2 nPos = new Vector2(20,5);
        buttons = new GuiButton[NUM_BUTTONS];
        for (int i = 0; i < NUM_BUTTONS; i++)
        {
            buttons[i] = new GuiButton(GuiButton.PressedState.RELEASED_OFF,
                GuiButton.ButtonType.TOGGLE, bPos, rButtonNames[i], nPos);
            buttons[i].Enabled = false;
            if ((i % 2) == 0)
            {
                bPos.X += 125;
            }
            else
            {
               bPos.X = RightButtons.X;
            }

            if (i > 0 && ((i % 2) != 0))
            {
                bPos.Y += 40;
            }
        }

    }
    else if (pType == GuiPanel.PanelType.TOP)
    {
        pos = TAway;
        textItems.Print("Suma Sumi", Title, Color.White,
            (int)ScreenText.FontName.TITLE);
        textItems.Print("Greetings, Earthling! Behold the great Suma Sumi - -
            the long boring journey of the", TopText, Color.White,
            (int)ScreenText.FontName.DEFAULT);
        textItems.Print("Suma that ends in your destruction! Make yourself
            useful. Keep us entertained!", new Vector2(TopText.X,
            TopText.Y + 20), Color.White, (int)ScreenText.FontName.DEFAULT);
        textItems.Print("Or we will have to destroy you (or something).",
            new Vector2(TopText.X, TopText.Y + 60), Color.White,
            (int)ScreenText.FontName.DEFAULT_BOLD);
        textItems.Print("This game will challenge you to perform certain feats
            of social communication - - raw", new Vector2(TopText.X,
            TopText.Y + 100), Color.White, (int)ScreenText.FontName.DEFAULT);
        textItems.Print("animal magnetism, chatty-Kathyism, get-down-on-itism,
            and maybe even exorcism.", new Vector2(TopText.X, TopText.Y + 120),
            Color.White, (int)ScreenText.FontName.DEFAULT);
        bBox = new Rectangle((int)pos.X, (int)pos.Y + clicky, w, clickh);
    }
    else
    {
        //oops.
        pos = new Vector2(-5000, -5000);
        bBox = new Rectangle((int)pos.X, (int)pos.Y, w, h);
    }

    textItems.Position = pos;
    charId = -1;
    deltaT = 0f;
    isMovingOut = false;
    isMovingAway = false;
}
/// <summary>
/// Loads the content for all components contained in the panel and for the panel
itself
/// </summary>
/// <param name="Content"></param>
public void LoadContent(ContentManager Content)
{
    textItems.LoadContent(Content);

    if ((pType == GuiPanel.PanelType.LEFT) || (pType == GuiPanel.PanelType.
        RIGHT))
    {
        for (int i = 0; i < NUM_BUTTONS; i++)
        {
             buttons[i].LoadContent(Content);
        }
    }

    tex = Content.Load<Texture2D>("GUI/GuiPanel");
}
#endregion

Further, we will need a few extra utility methods like so:

       #region utilities
/// <summary>
/// Changes the Enabled property of the button at index i
/// </summary>
/// <param name="i">Index of the button to be changed</param>
public void ToggleButtonEnable(int i)
{
    if ((pType == GuiPanel.PanelType.LEFT) || (pType == GuiPanel.PanelType.
         RIGHT))
    {
         buttons[i].Enabled = !buttons[i].Enabled;
    }
}
/// <summary>
/// Systematically enables all buttons contained in the panel
/// </summary>
public void EnableAllButtons()
{
    if ((pType == GuiPanel.PanelType.LEFT) || (pType == GuiPanel.PanelType.
        RIGHT))
    {
        for (int i = 0; i < NUM_BUTTONS; i++)
        {
             buttons[i].Enabled = true;
        }
    }
}
/// <summary>
/// Public accessor for the button array
/// </summary>
/// <returns>The array of the panel's GuiButton objects</returns>
public GuiButton[] GetButtons()
{
    return buttons;
}
/// <summary>
/// Changes the state of the button texture for the button at index i
/// </summary>
/// <param name="i">The index of the button to update</param>
public void ChangeButtonState(int i)
{
    GuiButton.PressedState oldState = buttons[i].State;

    if (oldState == GuiButton.PressedState.RELEASED_OFF)
    {
        buttons[i].State = GuiButton.PressedState.PRESSED_OFF;
    }
    else if (oldState == GuiButton.PressedState.RELEASED_ON)
    {
        buttons[i].State = GuiButton.PressedState.PRESSED_ON;
    }
    else if (oldState == GuiButton.PressedState.PRESSED_OFF)
    {
        buttons[i].State = GuiButton.PressedState.RELEASED_ON;
    }
    else if (oldState == GuiButton.PressedState.PRESSED_ON)
    {
        buttons[i].State = GuiButton.PressedState.RELEASED_OFF;
    }

}

The first three of these methods are self-explanatory, and the final method is just a mapping of the current button state to the next state for both left-mouse-down and left-mouse-click events. By choosing to implement the asset changes based on these two events, we avoid the necessity of animating the buttons. Plus, we allow the user to start a mouse click (by pressing down on the left button) and then change his mind (by moving the mouse away before releasing the left button).

Since we programmed the buttons to convert relative positions to absolute positions, we also need to update each button during every Update() call. We do this by adding this code to the end of the existing GuiPanel.Update() method (just before we update the ScreenText object):

    if ((pType == GuiPanel.PanelType.LEFT) || (pType == GuiPanel.PanelType.RIGHT))
    {
        for (int i = 0; i < NUM_BUTTONS; i++)
        {
             buttons[i].Update(pos);
        }
    }

Similarly, we need to draw the button from the GuiPanel:

public void Draw(SpriteBatch sb)
{
    sb.Draw(tex, pos, Color.White);

    if ((pType == GuiPanel.PanelType.LEFT) || (pType == GuiPanel.PanelType.RIGHT))
    {
         for (int i = 0; i < NUM_BUTTONS; i++)
         {
              buttons[i].Draw(sb);
         }
    }

    textItems.Draw(sb);
}

Since these panels are all contained inside the GuiManager class, we also will need to expose whatever functionality we want other parts of the system to be able to use. In this case, we just need to add a single method:

/// <summary>
/// A pass-through function that allows the state change for a button of a
/// particular panel
/// </summary>
/// <param name="pt">The panel to change</param>
/// <param name="b">The index of the button to change</param>
/// <returns>true if successful, false otherwise</returns>
public void ButtonStateChange(GuiPanel.PanelType pt, int b)
{
    if (pt == GuiPanel.PanelType.LEFT)
    {
        LeftPanel.ChangeButtonState(b);
    }
    else if (pt == GuiPanel.PanelType.RIGHT)
    {
        RightPanel.ChangeButtonState(b);
    }
    else if (pt == GuiPanel.PanelType.TOP)
    {
        //empty until buttons added to top panel
    }
}

This will allow programmatic control of the state of the buttons in the two side panels. We also need to create a method for collision detection, and we will do that using the following method in the GuiManager class:

/// <summary>
/// Bounding box collision detection for buttons in a particular panel
/// </summary>
/// <param name="pt">The panel in which the buttons to be tested are found</param>
/// <param name="hitter">The bounding box to collide with</param>
/// <returns>the button hit if the local bounding box intersects with the
Rectangle parameter, -1 otherwise</returns>
public int ButtonHit(GuiPanel.PanelType pt, Rectangle hitter)
{
    GuiButton[] b;

    if (pt == GuiPanel.PanelType.TOP)
    {
        return -1;
    }
    else if (pt == GuiPanel.PanelType.LEFT)
    {
        b = LeftPanel.GetButtons();

        for (int i = 0; i < b.Length; i++)
        {
             if (b[i].Button.Intersects(hitter))
             {
                 return i;
             }
        }
    }
    else if (pt == GuiPanel.PanelType.RIGHT)
    {
        b = RightPanel.GetButtons();

        for (int i = 0; i < b.Length; i++)
        {
             if (b[i].Button.Intersects(hitter))
             {
                 return i;
             }
        }
    }

    return -1;
}

You will note that this method returns an integer rather than a Boolean, as have all the other collision-detection methods we’ve written so far. This is because we need to know which button was hit so we can create the appropriate behavior change down the road.

Since we want to allow the player to change his mind mid-click, we need to make a few small changes to the MouseHandler class. We just need to add a Boolean member to track if we’re in the left-mouse-down state:

private Boolean guiLeftDown;

provide external access:

public Boolean GuiLDown
{
    get { return guiLeftDown; }
    set { guiLeftDown = value; }
}

and update the code that tracks the left-mouse-button state in the Update() method:

//mouse is in the Gui area
//handle left down
if (oldState.LeftButton == ButtonState.Released && mouseState.LeftButton ==
    ButtonState.Pressed)
{
    guiLeftDown = true;
}


//handle left click
if (oldState.LeftButton == ButtonState.Pressed && mouseState.LeftButton ==
    ButtonState.Released)
{
    guiLeftClick = true;
}

Finally, we need to update the CollisionHandler. DoGuiPanelCollision() method to test for collision with all these new buttons. Here’s the code:

public void DoGuiPanelCollision()
{
    GUI.GuiManager gm = Game1.GameStateMan.GuiManager;
    MouseHandler mh = Game1.GameStateMan.MHandler;

    if (mh.GuiLClick)
    {

        if (gm.Hit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse))
        {
            gm.LeftMove();
        }
        if (gm.Hit(GUI.GuiPanel.PanelType.RIGHT, mh.Mouse))
        {
            gm.RightMove();
        }
        if (gm.Hit(GUI.GuiPanel.PanelType.TOP, mh.Mouse))
        {
            gm.TopMove();
        }
        int hit = gm.ButtonHit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse);
        if (hit != -1)
        {
            gm.ButtonStateChange(GUI.GuiPanel.PanelType.LEFT, hit);
        }
        else
        {
            hit = gm.ButtonHit(GUI.GuiPanel.PanelType.RIGHT, mh.Mouse);
            if (hit != -1)
            {
                gm.ButtonStateChange(GUI.GuiPanel.PanelType.RIGHT, hit);
            }
        }
        mh.GuiLClick = false;
    }
    else if (mh.GuiLDown)
    {

        int hit = gm.ButtonHit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse);
        if (hit != -1)
        {
            gm.ButtonStateChange(GUI.GuiPanel.PanelType.LEFT, hit);
        }
        else
        {
            hit = gm.ButtonHit(GUI.GuiPanel.PanelType.RIGHT, mh.Mouse);
            if (hit != -1)
            {
                gm.ButtonStateChange(GUI.GuiPanel.PanelType.RIGHT, hit);
            }
        }
        mh.GuiLDown = false;
    }
}

Now that we’ve finally added stuff to the game that might be clickable outside the main game window, we can click stuff when it’s hidden off screen. That’s not really a good thing, but there’s good news. It’s really, really easy to fix, given the system we’ve already got in place.

We just need to add a top-level bounding box that is the size of the screen and test to see if the mouse is inside the box before we do any collision detection with the GUI or characters. We need to add a Rectangle object:

//game bounding box
private Rectangle bBox;

a property for that object:

public Rectangle GameBounds
{
    get { return bBox; }
}

and initialize the object in the constructor like this:

#region constructor / init
public GameStateManager()
{
    bgm = new Environment.BackgroundManager();
    cm = new Character.CharacterManager();
    ch = new Utilities.CollisionHandler();
    mh = new Utilities.MouseHandler();
    gm = new GUI.GuiManager();

    bBox = new Rectangle(0, 0, 1024, 728); //check for changes in Game1 constructor
}

Now that we have that high-powered goodness, we need to use it in the MouseHandler class to stop setting mouse-click states. We can do this by simply testing to see if the GameBounds property contains the mouse before setting the state, like so:

if (Game1.GameStateMan.GameBounds.Contains(Mouse))
{
    //mouse is in the Gui area
    //handle left down
    if (oldState.LeftButton == ButtonState.Released && mouseState.LeftButton
        == ButtonState.Pressed)
    {
        guiLeftDown = true;
    }


    //handle left click
    if (oldState.LeftButton == ButtonState.Pressed && mouseState.LeftButton
        == ButtonState.Released)
    {
        guiLeftClick = true;
    }
}
else
{
    guiLeftDown = false;
    guiLeftClick = false;
}

Now if we try to click on stuff that’s not in the game window, nothing will happen. This does expose one small issue, however. If you change your mind mid-click, the state of the art assets in the button class gets corrupted. In other words, if you press down on the button, and then slide the button away before releasing it, the button looks like it’s in the pressed, but not yet on position (see Figure 4.1).

GuiButton in the oops state.

Figure 4.1. GuiButton in the oops state.

I wish I had good news, like “This can be fixed by thinking good thoughts,” but I don’t. It’s a fairly involved fix. Basically, we have to track which button we hit on the down click, whether we are in the middle of a click, and then, when the mouse click is finished, whether we are still in the GUI area—and if so, whether we are still over the same button we started with. Whew. Oh yeah, we also need a method to reset the button states.

So, in GuiPanel, we need to add a method that will walk through all buttons in the panel and reset any mid-click states. Like this:

/// <summary>
/// Iterative walks through all the buttons in this panel and corrects mid-
/// animation button states to the correct state
/// </summary>
public void FixButtonState()
{
    for (int i = 0; i < NUM_BUTTONS; i++)
    {
        if (buttons[i].State == GuiButton.PressedState.PRESSED_OFF)
        {
            buttons[i].State = GuiButton.PressedState.RELEASED_OFF;
        }
        else if (buttons[i].State == GuiButton.PressedState.PRESSED_ON)
        {
            buttons[i].State = GuiButton.PressedState.RELEASED_ON;
        }
    }
}

In MouseHandler, we need to add another state-tracking member called midClick, an integer to keep track of the last button we hit called lastButton, and properties for both. The rule for midClick is that it will only be true if we’ve set guiLeftDown to true, and set to false when we’ve set guiLeftClick to true (or if the mouse goes outside the game window or the GUI area). The lastButton value will always be set to –1 unless we are in the middle of a valid click. Other than initializing the values appropriately in the MouseHandler constructor, the only changes are in the Update() method:

if (Game1.GameStateMan.GameBounds.Contains(Mouse))
{
    //mouse is in the Gui area
    //handle left down
    if (oldState.LeftButton == ButtonState.Released && mouseState.LeftButton
        == ButtonState.Pressed)
    {
        guiLeftDown = true;
        midClick = true;
    }


    //handle left click
    if ((oldState.LeftButton == ButtonState.Pressed && mouseState.LeftButton
        == ButtonState.Released ) && midClick)
    {
        guiLeftClick = true;
        midClick = false;
    }
}
else
{
        guiLeftDown = false;
        guiLeftClick = false;
        midClick = false;
}

In the DoGuiPanelCollision() method of the CollisionHandler, we will make some fairly extensive changes. First, we need to set MouseHandler’s LastButtonHit property on any valid left mouse down over a button. To do that, we will make the following changes:

else if (mh.GuiLDown && mh.MidClick)
{
    int hit = gm.ButtonHit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse);
    if (hit != -1)
    {
        gm.ButtonStateChange(GUI.GuiPanel.PanelType.LEFT, hit);
        mh.LastButtonHit = hit;
    }
    else
    {
        hit = gm.ButtonHit(GUI.GuiPanel.PanelType.RIGHT, mh.Mouse);
        if (hit != -1)
        {
            gm.ButtonStateChange(GUI.GuiPanel.PanelType.RIGHT, hit);
            mh.LastButtonHit = hit;
        }
    }
    mh.GuiLDown = false;
}

To handle a successful left click with these changes, we need to first test to make sure we are releasing the left mouse button over the button we started with. We do this thusly:

int hit = gm.ButtonHit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse);
if (hit != -1)
{
    if (hit == mh.LastButtonHit)
    {
        gm.ButtonStateChange(GUI.GuiPanel.PanelType.LEFT, hit);
    }
}

To maintain the correct button states, and to update the LastButtonHit property in the MouseHandler, we just add this code:

if (mh.GuiLClick)
{

    if (gm.Hit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse))
    {
        gm.LeftMove();
    }
    if (gm.Hit(GUI.GuiPanel.PanelType.RIGHT, mh.Mouse))
    {
        gm.RightMove();
    }
    if (gm.Hit(GUI.GuiPanel.PanelType.TOP, mh.Mouse))
    {
        gm.TopMove();
    }
    int hit = gm.ButtonHit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse);
    if (hit != -1)
    {
        if (hit == mh.LastButtonHit)
        {
            gm.ButtonStateChange(GUI.GuiPanel.PanelType.LEFT, hit);
        }
    }
    else
    {
        hit = gm.ButtonHit(GUI.GuiPanel.PanelType.RIGHT, mh.Mouse);
        if (hit != -1)
        {
            if (hit == mh.LastButtonHit)
            {
                gm.ButtonStateChange(GUI.GuiPanel.PanelType.RIGHT, hit);
            }
        }
    }
    mh.GuiLClick = false;
    mh.LastButtonHit = -1;
    gm.FixButtonStates();
}

Almost there, Valued Reader, I promise. Last, we need to handle when the player presses the left button down and then moves the mouse outside the game window or back down into the character area. Luckily, that’s easy:

public void DoGuiPanelCollision()
{
    GUI.GuiManager gm = Game1.GameStateMan.GuiManager;
    MouseHandler mh = Game1.GameStateMan.MHandler;

    if (mh.DrawMouse || !Game1.GameStateMan.GameBounds.Contains(mh.Mouse))
    {
        gm.FixButtonStates();
        mh.LastButtonHit = -1;
        return;
    }

Bam! Take that stupid mouse code that wanted to leave buttons half selected! We pwn face.

Controlling the Controlees with Controllers

Now that we finally have the GUI sorted out, we can move on to adding some behaviors to all these buttons. In many games, there are several different applications for finite state machines (FSMs, explained in a second), including VGAI, animation controls, and basic character behavior. Because we can tame several tigers with one flank steak, let’s implement a finite state machine controller, a finite state machine, and states to create the behaviors for our buttons.

As tempting as it is to break out a bunch of Greek letters and formal math here, let’s just agree that FSMs are black boxes filled with magical smoke, and that the magical smoke can transform an input into an output. Great! Sounds like a program—and it should, as there’s no theoretical difference between a program and an FSM. What we are really talking about here is designing and implementing a way to program our game characters in response to a button press.

Every FSM contains a collection of states and a special function called a transition function. This transition function is responsible for ordering the FSM around in response to getting poked from the outside (i.e., inputs). This crazy function has to know which inputs are acceptable for each state and which state the machine should be in after being in any given state and receiving any given appropriate input.

In any implementation of an FSM, it must be decided whether to house the transitions in the state representation or the machine representation. In my mind, it’s easier architecturally (and easier on the ol’ noggin to keep everything straight) if the transition for each state is maintained locally, so unless you object, Valued Reader, we’ll do that.

The FSM code, then, just needs to be the container and data-flow controller for the states. It needs to keep track of which state is current and to call the transition function of that state with the correct input at the correct time. It will also house the available states and the acceptable inputs for the machine and will need a bit of easy processing to allow access to both of those chunks of data.

In video games, when we talk about FSMs, someone (usually the drunk guy to the left and down the hall) asks about FSM controllers. Given the theory behind FSMs, these controllers are not really necessary. They’re simply an extension of the machine itself, although there still must be some programmatic connection between the magical black box of the FSM and the running code of the game. For our purposes, our “FSM controller” will be the manager class that ultimately houses the FSM, but more on that later.

Since we may have multiple instances of FSMs in this system, we’re going to implement abstract classes for both the FSM and the states. An abstract class is a class that can’t be implemented directly, but may be inherited from. These kinds of classes allow us to indicate basic behaviors that can be later specified for polymorphic solutions. (If that sounded like gobbledygook, just remember that I’m a doctor, and that I’m in charge here. Oh, and keep in mind that polymorphic means “many forms” and is a concept that defines inheritance in object-oriented programming.) In our case, we’ll start with a public abstract class called FiniteStateMachine.cs.

The code is listed here:

using System;
using Microsoft.Xna.Framework;
using System.Collections;
namespace _2DSpriteGame.GameState.Utilities
{
    /// <summary>
    /// An abstract representation of a Finite State Machine. To use the machine,
    /// set the Token property with the desired input and call ExecuteTransition().
    /// </summary>
    public abstract class FiniteStateMachine
    {
        #region members
        protected Hashtable stateId; //a hash table that contains the string
            name of every state paired with its index into the states array
        protected Hashtable inputAlphabet; //a hash table that contains the
            string name of every button input paired with a numeric key
        protected State[] states; //the collection of implemented State objects
            that makes up a specific machine
        protected State currentState; //the State object the machine is
            currently in
        protected String inputToken; //the input that should be processed next
        #endregion

        #region properties
        public State CurrentState
        {
            get { return currentState; }
        }
        public String Token
        {
            get { return inputToken; }
            set { inputToken = value; }
        }
        #endregion

        #region constructors / init
        public FiniteStateMachine()
        {
        }
        /// <summary>
        /// An alternate constructor that sets a custom size to the stateId and
        /// inputAlphabet hash tables.
        /// </summary>
        /// <param name="sidCap">State ID hash table size</param>
        /// <param name="iaCap">Input alphabet hash table size</param>
        public FiniteStateMachine(int sidCap, int iaCap)
            : this()
        {
            stateId = new Hashtable(sidCap);
            inputAlphabet = new Hashtable(iaCap);
        }
        #endregion
        
        #region utilties
        /// <summary>
        /// Given an index into the array of states, returns the state at the
        /// index.
        /// </summary>
        /// <param name="key">Index of the state desired</param>
        /// <returns>State object at index key</returns>
        public State GetState(int key)
        {
            return states[key];
        }
        /// <summary>
        /// Returns the state associated with the name passed in
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public State GetState(String name)
        {
            if (name.Equals(""))
            {
                return null;
            }
                return states[GetStateIdKey(name)];
        }
        /// <summary>
        /// Passes the value stored in the Token property to the current state's
        /// transition function and changes state accordingly
        /// </summary>
        public void ExecuteTransition()
        {
            currentState = states[GetStateIdKey((currentState.Transition
                (GetInputKey(inputToken))))];
            currentState.TurnOnButton();
        }
        /// <summary>
        /// Passes the value stored in the Token property to the current state's
        /// transition function and changes state accordingly
        /// </summary>
        /// <param name="set">A flag for setting the Gui buttons (true to set,
        /// false to skip)</param>
        public void ExecuteTransition(Boolean set)
        {
            currentState = states[GetStateIdKey((currentState.Transition
                (GetInputKey(inputToken), set)))];
            if (set)
            {
                 currentState.TurnOnButton();
            }
        
        }
        /// <summary>
        /// Executes the current state's Action() method
        /// </summary>
        /// <param name="gameTime"></param>
        public void Update(GameTime gameTime)
        {
            currentState.Action(gameTime);
        }
        /// <summary>
        /// Abstract method that must return the key of the given inputAlphabet
        /// value
        /// </summary>
        /// <param name="s">inputAlphabet value</param>
        /// <returns>The key of the input value passed in</returns>
        public abstract int GetInputKey(String s);
        /// <summary>
        /// Abstract method that must return the key of the given stateId value
        /// </summary>
        /// <param name="s">stateId value</param>
        /// <returns>The key of the input value passed in</returns>
        public abstract int GetStateIdKey(String s);
        /// <summary>
        /// Abstract method that returns the inputAlphabet value for a given key
        /// </summary>
        /// <param name="k">Key in the inputAlphabet data structure</param>
        /// <returns>The value associated with the key</returns>
        public abstract String GetInputToken(int k);
        /// <summary>
        /// Abstract method that returns the stateId value for a given key
        /// </summary>
        /// <param name="k">Key in the stateId data structure</param>
        /// <returns>The value associated with the key</returns>
        public abstract String GetStateId(int k);
        #endregion

    }
}

Most of this is straightforward coding. If you aren’t familiar with abstract classes, however, the last four method descriptions may be confusing. Adding the abstract keyword to a method header tells the compiler that these methods must be implemented in any child of this class. Because of that, no implementation is required in the parent class.

Similarly, we’ll create a public abstract class called State, like so:

using System;
using Microsoft.Xna.Framework;
using System.Collections;

namespace _2DSpriteGame.GameState.Utilities
{
    /// <summary>
    /// An abstract representation of a Finite State. Instantiate objects of a
    /// child type within a FiniteStateMachine.cs child.
    /// </summary>
    public abstract class State
    {
       #region members
       protected Hashtable transitions; //a hash table that maps an input token
           to a state name string
       protected String id; //the state name of this instance
       protected Character.Character self; //the Character object that is in
           this state
       protected Character.Character target; //any partner or target Character
           object for this state
       protected FiniteStateMachine fsmRef; //a reference to the FSM object
           containing this state
       protected String lastState; //the last state the FSM was in prior to this
           state
       protected int[] buttons; //an on/off toggle used to control the drawing
           of GUI buttons
       protected int lButtonIndex; //the index of the left panel button
           controlling this state, or -1 if the button resides in the right panel
       protected int rButtonIndex; //the index of the right panel button
           controlling this state, or -1 if the button resides in the left panel
       #endregion

       #region properties
       public String Identifier
       {
           get { return id; }
       }
       public Character.Character Target
       {
           get { return target; }
           set { target = value; }
       }
       public String LastState
       {
           get { return lastState; }
           set { lastState = value; }
       }
       public int[] EnabledButtons
       {
           get { return buttons; }
       }
       public int LeftButtonId
       {
           get { return lButtonIndex; }
       }
       public int RightButtonId
       {
           get { return rButtonIndex; }
       }
       #endregion

       #region constructor / init
       public State()
       {
           lastState = "";
       }
       /// <summary>
       /// An optional constructor that sets the indices for left or right panel
       /// buttons (for Gui state maintenance)
       /// </summary>
       /// <param name="l">The index of the button in the left panel (-1 if the
       /// controlling button is in the right panel)</param>
       /// <param name="r">The index of the button in the right panel (-1 if the
       /// controlling button is in the left panel)</param>
       public State(int l, int r)
           : this()
       {
           lButtonIndex = l;
           rButtonIndex = r;
       }
      /// <summary>
      /// An optional constructor that sets the value of the Character this
      /// state belongs to, the finite state machine it is found in, and the state id
      /// </summary>
      /// <param name="s">the Character this state belongs to</param>
      /// <param name="f">the finite state machine the state is found in</param>
      /// <param name="i">the state id</param>
      public State(Character.Character s, FiniteStateMachine f, String i) :
          this()
      {
          self = s;
          fsmRef = f;
          id = i;
          transitions = new Hashtable();
      }
      /// <summary>
      /// An optional constructor that sets the value of the Character this
      /// state belongs to, the finite state machine it is found in, the size of
      /// the transition function data structure, and the state id
      /// </summary>
      /// <param name="s">the Character this state belongs to</param>
      /// <param name="f">the finite state machine the state is found in</param>
      /// <param name="cap">the size of the transition function data
      /// structure</param>
      /// <param name="i">the state id</param>
      public State(Character.Character s, FiniteStateMachine f, int cap, String i)
          : this()
      {
          self = s;
          fsmRef = f;
          id = i;
          transitions = new Hashtable(cap);
      }
      #endregion
      
      #region utilities
      /// <summary>
      /// The transition function for this state. Maps the inputKey to a new
      /// state id.
      /// </summary>
      /// <param name="inputKey">The numeric value for the desire input token
      /// </param>
      /// <returns>The state id for the new state</returns>
      public virtual String Transition(int inputKey)
      {
          String retVal = this.Identifier;

          if (transitions.ContainsKey(inputKey))
          {
              retVal = (String)transitions[inputKey];
          }

          if (retVal != this.Identifier)
          {
              fsmRef.GetState(retVal).LastState = this.Identifier;
              SetButtons(retVal);
          }
          else
          {
              SetButtons();
          }


          return retVal;
      }
      /// <summary>
      /// The transition function for this state. Maps the inputKey to a new
      /// state id.
      /// </summary>
      /// <param name="inputKey">The numeric value for the desired input
      /// token</param>
      /// <param name="set">A flag for setting the Gui buttons (true to set,
      /// false to skip)</param>
      /// <returns>The state id for the new state</returns>
      public virtual String Transition(int inputKey, Boolean set)
      {
          String retVal = this.Identifier;
      
          if (transitions.ContainsKey(inputKey))
          {
              retVal = (String)transitions[inputKey];
          }
      
          if (retVal != this.Identifier)
          {
              fsmRef.GetState(retVal).LastState = this.Identifier;
              if (set)
              {
                  SetButtons(retVal);
              }
          }
          else
          {
              if (set)
              {
                  SetButtons();
              }
          }
      
      
          return retVal;
      }
      /// <summary>
      /// This is a Gui helper function that controls the state of the buttons in
      /// both Gui panels for the identified state
      /// </summary>
      /// <param name="id">The id of the state to set up the buttons for</param>
      public void SetButtons(String id)
      {
          int[] onButtons = fsmRef.GetState(id).EnabledButtons;
          Game1.GameStateMan.GuiManager.EnableAllLeftPanelButtons();
          Game1.GameStateMan.GuiManager.EnableAllRightPanelButtons();
          for (int i = 0; i < onButtons.Length; i++)
          {
              if (onButtons[i] == 0)
              {
                  if (i < 6)
                  {
                      Game1.GameStateMan.GuiManager.
                          ToggleLeftPanelButtonEnable(i);
                  }
                  else
                  {
                      Game1.GameStateMan.GuiManager.
                          ToggleRightPanelButtonEnable(i - 6);
                  }
              }
          }
      }
      /// <summary>
      /// Calls the SetButtons(String id) method for this state
      /// </summary>
      public void SetButtons()
      {
          SetButtons(id);
      }
      /// <summary>
      /// Turns the button controlling this state on, while turning all other
      /// buttons off
      /// </summary>
      public void TurnOnButton()
      {
          if (LeftButtonId > -1)
          {
                Game1.GameStateMan.GuiManager.SetLeftPanelButton
                     (LeftButtonId);
                Game1.GameStateMan.GuiManager.ResetAllRightPanelButtons();
          }
          else if (RightButtonId > -1)
          {
              Game1.GameStateMan.GuiManager.ResetAllLeftPanelButtons();
              Game1.GameStateMan.GuiManager.SetRightPanelButton
                  (RightButtonId);
          }
        }
        /// <summary>
        /// An abstract method that allows a child state to implement a behavior
        /// </summary>
        /// <param name="gameTime"></param>
        public abstract void Action(GameTime gameTime);
        #endregion
    }
}

This class is much like the FiniteStateMachine class, but includes various controls for turning buttons on and off, etc. It also uses a new keyword: virtual. A virtual member of a class is one whose definition can be overridden in a child class. What the keyword does in this case is tell the compiler that if a child overrides this method, it should always use the child class’s method rather than this one, but if the child does not override the method, this one should be used. This will become more meaningful as we take a look at the classes we will be implementing and see that some will require a transition to a single state, regardless of the input.

The other thing you might notice is the abstract method I hid there at the end—the one called Action(). A pure FSM just maps inputs to states, true enough. But for that to be meaningful, the state has to affect the rest of the system somehow. In this implementation, that effect will be implemented in this abstract Action() method in the child class.

In combination, these two classes give us a handy way of controlling our characters indirectly. Our buttons can be tied to states in an FSM for characters by giving the button an associated input token for the FSM and causing the current state of the selected character to transition to a new state based on these tokens from the button presses. In Chapter 5 we will also see how we can create an independent AI using a similar method (without the buttons!). In the next section, we will take control of these little buggers who are constantly bumping around on our screen!

Major Tom to Finite State Control

The next step in adding behaviors is to implement children for these abstract classes. Let’s start with a public class called CharacterFSM. This class will be easy to implement because we’ve done a pretty good job with the abstract class. Essentially, we just have to populate the data structures defined in the parent and implement those four abstract methods. It looks like this:

using System;
using System.Collections;
using Utilities = _2DSpriteGame.GameState.Utilities;

namespace _2DSpriteGame.Character.Logic
{
    /// <summary>
    /// An FSM implementation that controls atomic character behaviors
    /// </summary>
    public class CharacterFSM : Utilities.FiniteStateMachine
    {
        #region members
        private int startState;
        //constants
        private const int NUM_STATES = 12;
        #endregion

        #region properties
        //none needed here
        #endregion

        #region constructors / init
        public CharacterFSM(Character s) : base()
        {
            stateId = new Hashtable();
            stateId.Add(0,"CHASE");
            stateId.Add(1,"EVADE");
            stateId.Add(2,"FORGET");
            stateId.Add(3,"MOVE");
            stateId.Add(4,"FREE_ROAM");
            stateId.Add(5,"SLEEP");
            stateId.Add(6,"STOP");
            stateId.Add(7, "GOTO");
            stateId.Add(8, "DANCE");
            stateId.Add(9, "DANCE_WITH");
            stateId.Add(10, "FIGHT");
            stateId.Add(11, "CHAT");

            inputAlphabet = new Hashtable();
            inputAlphabet.Add(0,"LAMBDA");
            inputAlphabet.Add(1,"FREE_ROAM");
            inputAlphabet.Add(2,"STOP");
            inputAlphabet.Add(3,"FORGET");
            inputAlphabet.Add(4,"MOVE_TO");
            inputAlphabet.Add(5,"DANCE");
            inputAlphabet.Add(6,"SLEEP");
            inputAlphabet.Add(7,"CHAT");
            inputAlphabet.Add(8,"CHASE");
            inputAlphabet.Add(9,"EVADE");
            inputAlphabet.Add(10,"FIGHT");
            inputAlphabet.Add(11, "DANCE_WITH");
            inputAlphabet.Add(12, "GOTO");

            states = new Utilities.State[NUM_STATES];
            states[0] = new CharacterFSMStates.ChaseState(s, this);
            states[1] = new CharacterFSMStates.EvadeState(s, this);
            states[2] = new CharacterFSMStates.ForgetState(s, this);
            states[3] = new CharacterFSMStates.MoveState(s, this);
            states[4] = new CharacterFSMStates.RandomWalkState(s, this);
            states[5] = new CharacterFSMStates.SleepState(s, this);
            states[6] = new CharacterFSMStates.StopState(s, this);
            states[7] = new CharacterFSMStates.GoToState(s, this);
            states[8] = new CharacterFSMStates.DanceState(s, this);
            states[9] = new CharacterFSMStates.DanceWithState(s, this);
            states[10] = new CharacterFSMStates.FightState(s, this);
            states[11] = new CharacterFSMStates.ChatState(s, this);

            currentState = states[6];
     }
     #endregion

     #region utilities
     /// <summary>
     /// Implementation of parent abstract method
     /// </summary>
     /// <param name="s">Value in the inputAlphabet data structure</param>
     /// <returns>The key associated with the value</returns>
     public override int GetInputKey(String s)
     {
         if (inputAlphabet.ContainsValue(s))
         {
             foreach (DictionaryEntry de in inputAlphabet)
             {
                 if ((de.Value as String).Equals(s))
                 {
                      return (int)de.Key;
                 }
             }
         }
     
         return -1;
     }
     /// <summary>
     /// Implementation of parent abstract method
     /// </summary>
     /// <param name="s">Value in the stateId data structure</param>
     /// <returns>The key associated with the value</returns>
     public override int GetStateIdKey(String s)
     {
         if (stateId.ContainsValue(s))
         {
             foreach (DictionaryEntry de in stateId)
             {
                 if ((de.Value as String).Equals(s))
                 {
                      return (int)de.Key;
                 }
             }
         }
     
             return -1;
     }
     /// <summary>
     /// Implementation of parent abstract method
     /// </summary>
     /// <param name="k">The key in the inputAlphabet data structure</param>
     /// <returns>The value associated with the key</returns>
     public override String GetInputToken(int k)
     {
         return (String)inputAlphabet[k];
     }
     /// <summary>
     /// Implementation of parent abstract method
     /// </summary>
     /// <param name="k">The key in the stateId data structure</param>
     /// <returns>The value associated with the key</returns>
     public override String GetStateId(int k)
     {
         return (String)stateId[k];
     }
     #endregion
   }
}

Pretty easy so far! For my next trick, I will saw the class in half, and then magically recombine it! The real sleight of hand here is in the constructor, where you will see I’ve consulted my Magic Eight Ball to learn the names of all the State children this machine will use.

The process of implementing the children of the State object will be more complicated. In the interest of saving a forest or two of paper, I’m going to show only representative class files here, and leave a review of the others as an exercise for … well, you, Valued Reader. What? Did you think there wouldn’t be homework?!

As an example of a very easy implementation, let’s take a look at the “STOP” state. This state makes the character stop moving, stand there until you give it another command, and otherwise makes the character look really, really lazy.

What we need to do to implement a basic State child class is to set the transitions and the buttons data, and then implement the Action() method. Here’s the code:

using Microsoft.Xna.Framework;
using System.Collections;
using Utilities = _2DSpriteGame.GameState.Utilities;

namespace _2DSpriteGame.Character.Logic.CharacterFSMStates
{
    /// <summary>
    /// A Finite State implementation that causes a character to stop moving until
    /// told otherwise. (Basic state implementation)
    /// </summary>
    public class StopState : Utilities.State
    {
        #region members
        #endregion

        #region properties
        #endregion

        #region constructor
        public StopState(Character s, CharacterFSM f) :base(1,-1)
        {
            self = s;
            id = "STOP";
            fsmRef = f as Utilities.FiniteStateMachine;
            transitions = new Hashtable();
            transitions.Add(f.GetInputKey("LAMBDA"), "STOP");
            transitions.Add(f.GetInputKey("STOP"), "STOP");
            transitions.Add(f.GetInputKey("MOVE_TO"), "MOVE");
            transitions.Add(f.GetInputKey("GOTO"), "GOTO");
            transitions.Add(f.GetInputKey("FREE_ROAM"), "FREE_ROAM");
            transitions.Add(f.GetInputKey("DANCE"), "DANCE");
            transitions.Add(f.GetInputKey("DANCE_WITH"), "DANCE_WITH");
            transitions.Add(f.GetInputKey("SLEEP"), "SLEEP");
            transitions.Add(f.GetInputKey("CHAT"), "CHAT");
            transitions.Add(f.GetInputKey("CHASE"), "CHASE");
            transitions.Add(f.GetInputKey("EVADE"), "EVADE");
            transitions.Add(f.GetInputKey("FIGHT"), "FIGHT");

            //flags for which buttons to enable in the Gui when this is the
            //current state
            buttons = new int[12];
            buttons[0] = 1; //move
            buttons[1] = 1; //stop
            buttons[2] = 1; //forget
            buttons[3] = 1; //dance
            buttons[4] = 1; //sleep
            buttons[5] = 1; //free roam
            buttons[6] = 1; //go to
            buttons[7] = 1; //chat
            buttons[8] = 1; //dance with
            buttons[9] = 1; //chase
            buttons[10] = 1; //evade
            buttons[11] = 1; //fight
        }
        #endregion

        #region utilities
        /// <summary>
        /// Implementation of abstract parent method. This method implements the
        /// behavior associated with this state.
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Action(GameTime gameTime)
        {
            self.ClearWaypoints();
        }
        #endregion
    }
}

You will see that when we implemented the Action() method, we replaced the abstract keyword from the original header with the override keyword. This is just the second side of the inheritance coin. This keyword tells the compiler that this method should be used in place of the definition found at the parent class. override is also used when replacing virtual methods.

If you are unfamiliar with OOP and inheritance, you might be wondering what happens to all those other methods that were implemented in the State class. When they are called, the compiler looks at the current object and fails to find the methods. In response to that failure, the compiler then checks up the inheritance tree and uses the first implementation it comes to (the one closest to the implemented child class). In this case, there are only the parent and child classes, so the method has to be in one or the other.

In a nutshell, all this class does is clear all the waypoints for the character in this state. Bang. Dead easy. The ForgetState is also very easy, but in this case it aborts a single waypoint rather than all of them. It also overrides the Transition() method to force the state to revert to the last state prior to entering the ForgetState. That code looks like this:

/// <summary>
/// Overrides the parent class Transition method. This method ignores the input
/// and automatically transitions back to the last state
/// </summary>
/// <param name="inputKey"></param>
/// <returns></returns>
public override String Transition(int inputKey)
{
    SetButtons(LastState);

    //this state should always return control to the preceding state
    return LastState;
}

For our next example, let’s examine the MoveState class. This state will be responsible for allowing the play to feed waypoints into the character’s path. As such, it will require a local member (a Vector2) for processing. Here’s the code:

using Microsoft.Xna.Framework;
using System.Collections;
using Utilities = _2DSpriteGame.GameState.Utilities;

namespace _2DSpriteGame.Character.Logic.CharacterFSMStates
{
    /// <summary>
    /// A Finite State implementation that causes a character to move to the
    /// passed-in waypoint. (Low complexity state implementation)
    /// </summary>
    public class MoveState : Utilities.State
    {
        #region members
        private Vector2 targetLoc;
        #endregion

        #region properties
        public Vector2 TargetLocation
        {
            get { return targetLoc; }
            set { targetLoc = value; }
        }
        #endregion

        #region constructor
        public MoveState(Character s, CharacterFSM f)
            : base(0,-1)
        {
            self = s;
            id = "MOVE";
            fsmRef = f as Utilities.FiniteStateMachine;
            transitions = new Hashtable();
            transitions.Add(f.GetInputKey("LAMBDA"), "MOVE");
            transitions.Add(f.GetInputKey("MOVE_TO"), "STOP");
            transitions.Add(f.GetInputKey("GOTO"), "GOTO");
            transitions.Add(f.GetInputKey("STOP"), "STOP");
            transitions.Add(f.GetInputKey("FORGET"), "FORGET");
            transitions.Add(f.GetInputKey("FREE_ROAM"), "FREE_ROAM");
            transitions.Add(f.GetInputKey("DANCE"), "DANCE");
            transitions.Add(f.GetInputKey("DANCE_WITH"), "DANCE_WITH");
            transitions.Add(f.GetInputKey("SLEEP"), "SLEEP");
            transitions.Add(f.GetInputKey("CHAT"), "CHAT");
            transitions.Add(f.GetInputKey("CHASE"), "CHASE");
            transitions.Add(f.GetInputKey("EVADE"), "EVADE");
            transitions.Add(f.GetInputKey("FIGHT"), "FIGHT");

            //flags for which buttons to enable in the Gui when this is the
            //current state
            buttons = new int[12];
            buttons[0] = 1; //move
            buttons[1] = 1; //stop
            buttons[2] = 1; //forget
            buttons[3] = 1; //dance
            buttons[4] = 1; //sleep
            buttons[5] = 1; //free roam
            buttons[6] = 1; //go to
            buttons[7] = 1; //chat
            buttons[8] = 1; //dance with
            buttons[9] = 1; //chase
            buttons[10] = 1; //evade
            buttons[11] = 1; //fight

            targetLoc = Vector2.Zero;
        }
        #endregion

        #region utility
        /// <summary>
        /// Implementation of abstract parent method. This method implements the
        /// behavior associated with this state.
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Action(GameTime gameTime)
        {
            if (targetLoc != Vector2.Zero)
            {
                self.Waypoint = targetLoc;
                targetLoc = Vector2.Zero;
            }

        }
        #endregion
     }
}

You can see that this class requires that targetLoc be updated externally for the state to have any effect. We need to set the value of targetLoc in response to a mouse click, which we will discuss later in the section. The GoToState functions in a very similar manner, but it requires a target character and obtains the waypoint from that character’s position. The DanceState also functions similarly, but it generates the next waypoint randomly.

For now, let’s get into something a little more complicated. I want the characters to be able to chase one another down (like the scurvy-ridden dogs they no doubt are). To enable this behavior, we need a ChaseState. We will implement this state’s Action() method by using an algorithm very close to that used by the infamous Terminator Chase Bot. Here’s the class:

using Microsoft.Xna.Framework;
using System.Collections;
using Utilities = _2DSpriteGame.GameState.Utilities;

namespace _2DSpriteGame.Character.Logic.CharacterFSMStates
{
    /// <summary>
    /// A Finite State implementation that causes a character to relentlessly
    /// follow the target character. (medium complexity state implementation)
    /// </summary>
    public class ChaseState : Utilities.State
    {
        #region members
        #endregion

        #region properties
        #endregion
        #region constructor
        public ChaseState(Character s, CharacterFSM f) :base(-1,3)
        {
            self = s;
            id = "CHASE";
            fsmRef = f as Utilities.FiniteStateMachine;
            transitions = new Hashtable();
            transitions.Add(f.GetInputKey("LAMBDA"), "CHASE");
            transitions.Add(f.GetInputKey("CHASE"), "STOP");
            transitions.Add(f.GetInputKey("STOP"), "STOP");
            transitions.Add(f.GetInputKey("MOVE_TO"), "MOVE");
            transitions.Add(f.GetInputKey("GOTO"), "GOTO");
            transitions.Add(f.GetInputKey("FREE_ROAM"), "FREE_ROAM");
            transitions.Add(f.GetInputKey("DANCE"), "DANCE");
            transitions.Add(f.GetInputKey("DANCE_WITH"), "DANCE_WITH");
            transitions.Add(f.GetInputKey("SLEEP"), "SLEEP");
            transitions.Add(f.GetInputKey("FIGHT"), "FIGHT");

            //flags for which buttons to enable in the Gui when this is the
            //current state
            buttons = new int[12];
            buttons[0] = 1; //move
            buttons[1] = 1; //stop
            buttons[2] = 0; //forget
            buttons[3] = 1; //dance
            buttons[4] = 1; //sleep
            buttons[5] = 1; //free roam
            buttons[6] = 1; //go to
            buttons[7] = 0; //chat
            buttons[8] = 0; //dance with
            buttons[9] = 1; //chase
            buttons[10] = 0; //evade
            buttons[11] = 1; //fight

        }
        #endregion

        #region utilities
        /// <summary>
        /// Implementation of abstract parent method. This method implements the
        /// behavior associated with this state.
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Action(GameTime gameTime)
        {
            if (target != null)
            {
                //drop everything and start chasing the character
                self.ClearWaypoints();

                Vector2 worker = target.Position;
                //get up close and personal with the target character if you can,
                //on the appropriate side
                if (self.Position.X > target.Position.X + 5)
                {
                    worker.X += 60;
                }
                else if (self.Position.X < target.Position.X - 5)
                {
                   worker.X -= 60;
                }
                else
                {
                   if (self.Position.Y < target.Position.Y - 5)
                   {
                       worker.Y += 85;
                   }
                   else if (self.Position.Y > target.Position.Y + 5)
                   {
                      worker.Y -= 85;
                   }
                }


                self.Waypoint = worker;
             }
          }
          #endregion
     }
}

You can see that the class always moves in the target’s direction on the x axis, and then does the same thing on the y axis. EvadeState is very similar, with the exception that we move away from the target (which is as easy as swapping a —= to a += operation) and impose an additional boundary of not running “too far” away.

As an example of the most complicated group of states, let’s take a look at the DanceWithState class. This class requires the cooperation of the target class (and for now, this cooperation is mandated), which means when we stop the first character from dancing with the second, the second needs to also change states. That’s a fairly complicated problem if both characters are using the same state code. The first character should transition to whatever state matches the player’s button press, but the second character should go back to whatever it was doing before. Because of those two mutually exclusive behaviors, we need a way to determine which character entered the state first. We’ll do that by keeping track with another Character reference object.

This class will also need to override the Transition() function, and will have a fairly complicated Action() method. The code looks like this:

using System;
using Microsoft.Xna.Framework;
using System.Collections;
using Utilities = _2DSpriteGame.GameState.Utilities;

namespace _2DSpriteGame.Character.Logic.CharacterFSMStates
{
    /// <summary>
    /// A Finite State implementation that causes a character to dance with the
    /// target character and vice versa. (high complexity state implementation)
    /// </summary>
    public class DanceWithState : Utilities.State
    {
        #region members
        private Random rand;
        private Character init; //a member that allows us to determine who
            started this interaction
        #endregion

        #region properties
        public String Initiatior
        {
            get { return init.Name; }
        }
        public Character InitCharacter
        {
            set { init = value; }
        }
       #endregion

       #region constructor
       public DanceWithState(Character s, CharacterFSM f)
           : base(-1,2)
       {
           rand = new Random();
           self = s;
           id = "DANCE_WITH";
           fsmRef = f as Utilities.FiniteStateMachine;
           transitions = new Hashtable();
           transitions.Add(f.GetInputKey("LAMBDA"), "DANCE_WITH");
           transitions.Add(f.GetInputKey("STOP"), "STOP");
           transitions.Add(f.GetInputKey("DANCE_WITH"), "STOP");
           transitions.Add(f.GetInputKey("CHAT"), "CHAT");
           transitions.Add(f.GetInputKey("MOVE_TO"), "MOVE");
           transitions.Add(f.GetInputKey("GOTO"), "GOTO");
           transitions.Add(f.GetInputKey("FORGET"), "FORGET");
           transitions.Add(f.GetInputKey("FREE_ROAM"), "FREE_ROAM");
           transitions.Add(f.GetInputKey("DANCE"), "DANCE");
           transitions.Add(f.GetInputKey("SLEEP"), "SLEEP");
           transitions.Add(f.GetInputKey("CHASE"), "CHASE");
           transitions.Add(f.GetInputKey("EVADE"), "EVADE");
           transitions.Add(f.GetInputKey("FIGHT"), "FIGHT");

           //flags for which buttons to enable in the Gui when this is the
           //current state
           buttons = new int[12];
           buttons[0] = 0; //move
           buttons[1] = 1; //stop
           buttons[2] = 0; //forget
           buttons[3] = 0; //dance
           buttons[4] = 0; //sleep
           buttons[5] = 0; //free roam
           buttons[6] = 0; //go to
           buttons[7] = 1; //chat
           buttons[8] = 1; //dance with
           buttons[9] = 0; //chase
           buttons[10] = 0; //evade
           buttons[11] = 0; //fight
       }
       #endregion
       #region utility
       /// <summary>
       /// Overrides the parent class Transition method. This method causes the
       /// correct transition in the target character (since we force the target
       /// into the same state at the outset of this state's behavior).
       /// </summary>
       /// <param name="inputKey"></param>
       /// <returns></returns>
       public override string Transition(int inputKey)
       {
           String temp = fsmRef.GetInputToken(inputKey);
           if (init.Name.Equals(self.Name))
           {
               if (temp.Equals("CHAT"))
               {
                   target.FSM.Token = temp;
               }
               else
               {
                   target.FSM.Token = target.FSM.CurrentState.LastState;
               }
               target.FSM.ExecuteTransition();
               if (target.FSM.CurrentState.GetType().Name.
                   Equals("ChatState"))
               {
                   (target.FSM.CurrentState as ChatState).InitCharacter =
                       self;
                   target.FSM.CurrentState.Target = self;
               }
           }
           return base.Transition(inputKey);
       }
       /// <summary>
       /// Implementation of abstract parent method. This method implements the
       /// behavior associated with this state.
       /// </summary>
       /// <param name="gameTime"></param>
       public override void Action(GameTime gameTime)
       {
           if (target.FSM.CurrentState.Identifier != "DANCE_WITH" &&
               init.Name.Equals(self.Name))
           {
               target.FSM.Token = "DANCE_WITH";
               target.FSM.ExecuteTransition();
               target.FSM.CurrentState.Target = self;
               (target.FSM.CurrentState as CharacterFSMStates.
                   DanceWithState).InitCharacter = self;
           }


           if (Vector2.DistanceSquared(self.Position, target.Position)
              <= 6400f)
           {
              int y = rand.Next(75);
              int x = rand.Next(75);

              int chance = rand.Next(100);

              if (chance < 25)
              {
                  x *= -1;
                  y *= -1;
              }
              else if (chance < 50)
              {
                  x *= -1;
              }
              else if (chance < 75)
              {
                  y *= -1;
              }


              self.Waypoint = new Vector2(self.Position.X + x,
                  self.Position.Y + y);
           }
           else
           {
              float deltaX = (float)Math.Abs(self.Position.X - target.
                  Position.X) / 2;
              float deltaY = (float)Math.Abs(self.Position.Y - target.
                  Position.Y) / 2;

              self.ClearWaypoints();

              Vector2 selfWP = self.Position;
              if (self.Position.X < target.Position.X)
              {
                  selfWP.X += deltaX;
              }
              else if (self.Position.X > target.Position.X)
              {
                  selfWP.X -= deltaX;
              }

              if (self.Position.Y < target.Position.Y)
              {
                  selfWP.Y += deltaY;
              }
              else if (self.Position.Y > target.Position.Y)
              {
                  selfWP.Y -= deltaY;
              }

              self.Waypoint = selfWP;
          }
       }
       #endregion
    }
}

Basically, when a character enters this state, the Action() method first checks to see if the target’s state is also the DanceWithState. If it isn’t, the character forces the change in state on the target. Once that is resolved, we simply check to see if the two characters are close enough together to dance and, if not, to cause them to move together.

Caution

Distance is very expensive to compute on a computer—mainly because it requires a square-root operation, which means kittens must be sacrificed, spinach eaten, and heavy things lifted. Because it is so expensive to get the real distance between two points, we approximate the distance by doing everything but the square root and then squaring the threshold value we are comparing against.

The final goody to look at here is the Transition() method. What we are doing there is first converting the input token back into a string, and then testing to see if this is the initiating state or not. If it is, we allow it to force a transition in the target character state, either another mutual interaction state or whatever the target’s last state was.

The other “high complexity” states include ChatState and FightState. All three of these states require special processing in the collision-detection method, as discussed below.

To enable the FSM, we are going to plug it into the Character class and then alter the CharacterManager class. The changes to Character.cs are trivial—simply adding the CharacterFSM object and a couple of properties—so I’ll leave the review of that class as an exercise. The CharacterManager class will get a few new methods, a few new members to facilitate target tracking for the GUI and states, and a pretty major overhaul of its Update() method. The new methods will be responsible for keeping the left and right panels updated with some debugging information (for now) and for allowing an easy way to get a character ID number for a Character object from outside the class. The new methods are next, followed by the revamped Update() method.

/// <summary>
/// Updates the information and buttons found in the left gui panel
/// </summary>
/// <param name="left">Character object that will be used to populate the left
/// gui panel</param>
public void UpdateLeftGui(Character left)
{
    if (left != null)
    {
        GUI.GuiManager gm = Game1.GameStateMan.GuiManager;

        gm.LeftText.Clear();
        gm.LeftText.Print("Suma " + left.Name, gm.LeftTextPosition, Color.
            White, (int)GUI.ScreenText.FontName.SUBHEAD);

        Vector2 tWalk = gm.LeftTextPosition;
        tWalk.Y += 50;
        String txt = "Current state = " + left.FSM.CurrentState.Identifier;
        gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
            FontName.DEFAULT);
        tWalk.Y += 20;
        txt = "Last state = " + left.FSM.CurrentState.LastState;
        gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
            FontName.DEFAULT);
        txt = "isSleeping = " + left.IsSleeping;
        tWalk.Y += 20;
        gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
            FontName.DEFAULT);
        txt = "Position = (" + (int)left.Position.X + "," + (int)left.Position.
            Y + ")";
        tWalk.Y += 20;
        gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
            FontName.DEFAULT);
        txt = "Waypoint = (" + (int)left.Waypoint.X + "," + (int)left.Waypoint.
            Y + ")";
        tWalk.Y += 20;
        gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
            FontName.DEFAULT);
    }
}
/// <summary>
/// Updates the information and buttons found in the right gui panel
/// </summary>
/// <param name="right">Character object that will be used to populate the right
/// gui panel</param>
public void UpdateRightGui(Character right)
{
    if (right != null)
    {
        GUI.GuiManager gm = Game1.GameStateMan.GuiManager;

        gm.RightText.Clear();
        gm.RightText.Print("Suma " + right.Name, gm.RightTextPosition,
            Color.White, (int)GUI.ScreenText.FontName.SUBHEAD);

        Vector2 tWalk = gm.RightTextPosition;
        tWalk.Y += 50;
        String txt = "Current state = " + right.FSM.CurrentState.Identifier;
        gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
            FontName.DEFAULT);
        tWalk.Y += 20;
        txt = "Last state = " + right.FSM.CurrentState.LastState;
        gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
            FontName.DEFAULT);
        txt = "isSleeping = " + right.IsSleeping;
        tWalk.Y += 20;
        gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
            FontName.DEFAULT);
        txt = "Position = (" + (int)right.Position.X + "," + (int)right.
            Position.Y + ")";
        tWalk.Y += 20;
        gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
            FontName.DEFAULT);
        txt = "Waypoint = (" + (int)right.Waypoint.X + "," + (int)right.
            Waypoint.Y + ")";
        tWalk.Y += 20;
        gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
            FontName.DEFAULT);
    }
}
/// <summary>
/// Updates the information and buttons found in the left gui panel
/// </summary>
/// <param name="i">Character object index that will be used to populate the left
/// gui panel</param>
public void UpdateLeftGui(int i)
{
    GUI.GuiManager gm = Game1.GameStateMan.GuiManager;

    gm.OpenPanel(GUI.GuiPanel.PanelType.LEFT);


    gm.LeftText.Clear();
    gm.LeftText.Print("Suma " + toons[i].Name, gm.LeftTextPosition, Color.
        White, (int)GUI.ScreenText.FontName.SUBHEAD);
    lTarget = i;
    gm.LeftTargetId = i;

    Vector2 tWalk = gm.LeftTextPosition;
    tWalk.Y += 50;
    String txt = "Current state = " + toons[i].FSM.CurrentState.Identifier;
    gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
    tWalk.Y += 20;
    txt = "Last state = " + toons[i].FSM.CurrentState.LastState;
    gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
    txt = "Current panel target = " + i;
    tWalk.Y += 20;
    gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
    txt = "isSleeping = " + toons[i].IsSleeping;
    tWalk.Y += 20;
    gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
    txt = "Position = (" + (int)toons[i].Position.X + "," + (int)toons[i].
        Position.Y + ")";
    tWalk.Y += 20;
    gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
    txt = "Waypoint = (" + (int)toons[i].Waypoint.X + "," + (int)toons[i].
        Waypoint.Y + ")";
    tWalk.Y += 20;
    gm.LeftText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
}
/// <summary>
/// Updates the information and buttons found in the right gui panel
/// </summary>
/// <param name="i">Character object index that will be used to populate the
/// right gui panel</param>
public void UpdateRightGui(int i)
{
    GUI.GuiManager gm = Game1.GameStateMan.GuiManager;

    gm.OpenPanel(GUI.GuiPanel.PanelType.RIGHT);
    gm.RightTargetId = i;

    gm.RightText.Clear();
    gm.RightText.Print("Suma " + toons[i].Name, gm.RightTextPosition, Color.
        White, (int)GUI.ScreenText.FontName.SUBHEAD);
    rTarget = i;

    Vector2 tWalk = gm.RightTextPosition;
    tWalk.Y += 50;
    String txt = "Current state = " + toons[i].FSM.CurrentState.Identifier;
    gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
    tWalk.Y += 20;
    txt = "Last state = " + toons[i].FSM.CurrentState.LastState;
    gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
    txt = "Current panel target = " + i;
    tWalk.Y += 20;
    gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
    txt = "isSleeping = " + toons[i].IsSleeping;
    tWalk.Y += 20;
    gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
    txt = "Position = (" + (int)toons[i].Position.X + "," + (int)toons[i].
        Position.Y + ")";
    tWalk.Y += 20;
    gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
    txt = "Waypoint = (" + (int)toons[i].Waypoint.X + "," + (int)toons[i].
        Waypoint.Y + ")";
    tWalk.Y += 20;
    gm.RightText.Print(txt, tWalk, Color.White, (int)GUI.ScreenText.
        FontName.DEFAULT);
}
/// <summary>
/// Returns the index of the Character object passed in
/// </summary>
/// <param name="c">The desired Character object</param>
/// <returns>Index of the Character object c</returns>
public int GetToonId(Character c)
{
    for (int i = 0; i < NUM_TOONS; i++)
    {
         if (toons[i].Name.Equals(c.Name))
         {
             return i;
         }
    }
    return -1;
}
/// <summary>
/// Performs update processing on all characters managed, updates the left and
/// right gui panels
/// </summary>
/// <param name="gameTime"></param>
public void Update(GameTime gameTime)
{
    deltaTime += (float)gameTime.ElapsedGameTime.TotalMilliseconds;
    if (deltaTime > INTERVAL)
    {
        for (int i = 0; i < NUM_TOONS; i++)
        {
             toons[i].FSM.Update(gameTime);
         }
 
        deltaTime = 0f;
     }

     UpdateLeftGui(Game1.GameStateMan.GuiManager.LeftTarget);
     UpdateRightGui(Game1.GameStateMan.GuiManager.RightTarget);

     for (int i = 0; i < NUM_TOONS; i++)
     {
          toons[i].Update(gameTime);
     }

}

Other major changes include some additions to the MouseHandler.Update() method (in bold):

/// <summary>
/// Update processing for the mouse, and Gui / Character maintenence based on the
/// update
/// </summary>
/// <param name="gameTime"></param>
public void Update(GameTime gameTime)
{
    Character.CharacterManager cm = Game1.GameStateMan.CharManager;
    GUI.GuiManager gm = Game1.GameStateMan.GuiManager;

    //capture the last state of the mouse and the current state of the mouse
    oldState = mouseState;
    mouseState = Microsoft.Xna.Framework.Input.Mouse.GetState();

    //we need an easy way to tell if the player is clicking on the Gui or on a
    //character…
    //we've already set mouseDraw based on the mouse bounds checking.
    if (drawMouse)
    {
        //mouse is in the character area
        //check the state of the left button for graphics changes
        if (mouseState.LeftButton == ButtonState.Pressed && oldState.
            LeftButton == ButtonState.Released)
        {
            //new left-button-down state
            mouse = mice[(int)MouseTypes.LEFTDOWN];
        }

       //handle left-click animation
       if (mouse.Identity != (int)MouseTypes.IDLE && mouseState.LeftButton ==
           ButtonState.Released)
       {
           deltaT += (float)gameTime.ElapsedGameTime.TotalMilliseconds;
           if (deltaT > INTERVAL)
           {
               if (mouse.Identity == (int)MouseTypes.LEFTCLICK1)
               {
                   mouse = mice[(int)MouseTypes.LEFTCLICK2];
               }
               else if (mouse.Identity == (int)MouseTypes.LEFTCLICK2)
               {
                   mouse = mice[(int)MouseTypes.LEFTCLICK3];
               }
               else if (mouse.Identity == (int)MouseTypes.LEFTCLICK3)
               {
                   mouse = mice[(int)MouseTypes.IDLE];
               }
               deltaT = 0f;
          }
      }


     //handle left click
     if (oldState.LeftButton == ButtonState.Pressed && mouseState.
         LeftButton == ButtonState.Released)
     {
         //a mouse click occurred
         mouse = mice[(int)MouseTypes.LEFTCLICK1];
         deltaT = 0f;
     
         if (mouseOver != -1)
         {
             gm.ClosePanel(GUI.GuiPanel.PanelType.RIGHT);
             cm.SetDrawMask(mouseOver);
             gm.EnableAllLeftPanelButtons();
             gm.ResetAllLeftPanelButtons();
     
             if (cm.LeftMouseTarget.FSM.CurrentState.Target != null)
             {
                 gm.RightTargetId = cm.GetToonId(cm.LeftMouse
                      Target.FSM.CurrentState.Target);
                 gm.OpenPanel(GUI.GuiPanel.PanelType.RIGHT);
                 cm.UpdateRightGui(gm.RightTargetId);
                 gm.EnableAllRightPanelButtons();
                 gm.ResetAllRightPanelButtons();
             }
     
             cm.GetToon(mouseOver).FSM.CurrentState.SetButtons();
             cm.GetToon(mouseOver).FSM.CurrentState.TurnOnButton();
             cm.UpdateLeftGui(mouseOver);
           }
           else if (gm.LeftTarget != null)
           {
              if (gm.LeftTarget.FSM.CurrentState.Identifier == "MOVE")
              {
                 //calculate the new waypoint based on the position of the
                 //mouse, offsetting so the center of the character ends up
                 //in the mouse location
                 (gm.LeftTarget.FSM.CurrentState as Character.Logic.
                      CharacterFSMStates.MoveState).TargetLocation =
                      new Vector2(mouseState.X - 50, mouseState.Y - 50);
             }
           }
     }
     
     //handle right click
     if (oldState.RightButton == ButtonState.Pressed && mouseState.
         RightButton == ButtonState.Released)
     {
         //a mouse click occured
         mouse = mice[(int)MouseTypes.LEFTCLICK1];
         deltaT = 0;
     
         if (mouseOver != -1)
         {
             gm.EnableAllRightPanelButtons();
             gm.ResetAllRightPanelButtons();
             cm.GetToon(cm.LMTarget).FSM.CurrentState.SetButtons();
             cm.GetToon(cm.LMTarget).FSM.CurrentState.TurnOnButton();
             cm.UpdateRightGui(mouseOver);
         }
      }
    }
    else
    {
        if (Game1.GameStateMan.GameBounds.Contains(Mouse))
        {
            //mouse is in the Gui area
            //handle left down
            if (oldState.LeftButton == ButtonState.Released && mouseState.
                LeftButton == ButtonState.Pressed)
            {
                guiLeftDown = true;
                midClick = true;
            }
 
 
            //handle left click
            if ((oldState.LeftButton == ButtonState.Pressed && mouseState.
                LeftButton == ButtonState.Released ) && midClick)
            {
                guiLeftClick = true;
                midClick = false;
            }
     }
     else
     {
         guiLeftDown = false;
         guiLeftClick = false;
         midClick = false;
     }
 
  }
 
  //update the position of the mouse with the new screen coordinates (we'll
  //handle collision detection later)
  bBox.X = mouseState.X;
  bBox.Y = mouseState.Y;
  mouse.Position = new Vector2(mouseState.X, mouseState.Y);
}

Next, we will move on to the CollisionHandler.DoPanelCollision() method, which needs pretty significant treatment. The changes that follow will cause transitions to occur inside the FSM for the character(s) involved. This will be accomplished by first determining which state change is appropriate, updating the character’s FSM with the appropriate input token, executing the transition function, and finally, performing whatever special processing steps are required.

The method should look like this:

/// <summary>
/// Performs collision detection for all GUI objects
/// </summary>
public void DoGuiPanelCollision()
{
     GUI.GuiManager gm = Game1.GameStateMan.GuiManager;
     MouseHandler mh = Game1.GameStateMan.MHandler;
     Character.CharacterManager cm = Game1.GameStateMan.CharManager;

     //if the mouse is no longer in the Gui area…
     if (mh.DrawMouse || !Game1.GameStateMan.GameBounds.Contains(mh.Mouse))
     {
        //fix the Gui
        gm.FixButtonStates();
        mh.LastButtonHit = -1;
        return;
     }

     if (mh.GuiLClick)
     {
        //left click has occurred – test for where

        if (gm.Hit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse))
        {
            gm.LeftMove();
        }
        if (gm.Hit(GUI.GuiPanel.PanelType.RIGHT, mh.Mouse))
        {
            gm.RightMove();
        }
        if (gm.Hit(GUI.GuiPanel.PanelType.TOP, mh.Mouse))
        {
            gm.TopMove();
        }
        //test to see if a button was left clicked
        //first the left panel buttons
        int hit = gm.ButtonHit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse);
   if (hit != -1)
   {
       //if a button was clicked, is it the same button the click started on?
       if (hit == mh.LastButtonHit)
       {
          //if so, see if the button's state change is valid
          if (gm.ButtonStateChange(GUI.GuiPanel.PanelType.LEFT, hit))
          {
             //if so, cause an FSM transition as appropriate and update
             //the new state
             gm.LeftTarget.FSM.Token = gm.GetButtonToInputToken
                (GUI.GuiPanel.PanelType.LEFT, hit);
             gm.LeftTarget.FSM.ExecuteTransition();
             cm.UpdateLeftGui(cm.LMTarget); }
          }
       }
    }
    else
    {
       //test the right panel buttons
       hit = gm.ButtonHit(GUI.GuiPanel.PanelType.RIGHT, mh.Mouse);
       if (hit != -1)
       {
          //if a button was clicked, is it the same button the click
          //started on?
          if (hit == mh.LastButtonHit)
          {
             //if so, see if the button's state change is valid
             if (gm.ButtonStateChange(GUI.GuiPanel.PanelType.RIGHT,
                 hit))
             {
                 //if so, cause an FSM transition as appropriate and
                 //update the new state
                 //in this case, it may be complicated by "special needs"
                 //states…
                 String temp = gm.GetButtonToInputToken(GUI.
                     GuiPanel.PanelType.RIGHT, hit);
                 switch (temp)
                 {
                          //handle the mutual interaction states first
                          //(DANCE_WITH,FIGHT,CHAT)
                     case "DANCE_WITH":
                          gm.LeftTarget.FSM.Token = temp;
                          gm.LeftTarget.FSM.ExecuteTransition();
                          if (gm.LeftTarget.FSM.CurrentState.
                               GetType().Name.Equals("DanceWithState"))
                          {
                               (gm.LeftTarget.FSM.CurrentState as
                                    Character.Logic.CharacterFSMStates.
                                    DanceWithState).InitCharacter =
                                    gm.LeftTarget;
                               gm.LeftTarget.FSM.CurrentState.Target =
                                    gm.RightTarget;
                          }
                          cm.UpdateLeftGui(cm.LMTarget);
                          cm.UpdateRightGui(cm.RMTarget);
                          break;
                     case "FIGHT":
                          gm.LeftTarget.FSM.Token = temp;
                          gm.LeftTarget.FSM.ExecuteTransition();
                          if (gm.LeftTarget.FSM.CurrentState.
                               GetType().Name.Equals("FightState"))
                          {
                               (gm.LeftTarget.FSM.CurrentState as
                                    Character.Logic.CharacterFSMStates.
                                    FightState).InitCharacter =
                                    gm.LeftTarget;
                               gm.LeftTarget.FSM.CurrentState.Target =
                                    gm.RightTarget;
                          }
                          cm.UpdateLeftGui(cm.LMTarget);
                          cm.UpdateRightGui(cm.RMTarget);
                          break;
                     case "CHAT":
                           gm.LeftTarget.FSM.Token = temp;
                           gm.LeftTarget.FSM.ExecuteTransition();
                           if (gm.LeftTarget.FSM.CurrentState.
                                GetType().Name.Equals("ChatState"))
                           {
                               (gm.LeftTarget.FSM.CurrentState as
                                     Character.Logic.CharacterFSMStates.
                                     ChatState).InitCharacter =
                                     gm.LeftTarget;
                               gm.LeftTarget.FSM.CurrentState.Target =
                                    gm.RightTarget;
                          }
                          cm.UpdateLeftGui(cm.LMTarget);
                          cm.UpdateRightGui(cm.RMTarget);
                          break;
                          //then states with special processing (GO_TO)
                     case "GOTO":
                           gm.LeftTarget.FSM.Token = temp;
                           gm.LeftTarget.FSM.ExecuteTransition();
                           if (gm.LeftTarget.FSM.CurrentState.
                                 GetType().Name.Equals("GoToState"))
                           {
                               gm.LeftTarget.FSM.CurrentState.Target =
                                    gm.RightTarget;
                               if (!(gm.LeftTarget.FSM.CurrentState as
                                   Character.Logic.CharacterFSMStates.
                                   GoToState).FirstRun)
                               {
                                   (gm.LeftTarget.FSM.CurrentState
                                        as Character.Logic.
                                        CharacterFSMStates.GoToState).
                                        FirstRun = true;
                               }
                           }
                           cm.UpdateLeftGui(cm.LMTarget);
                           cm.UpdateRightGui(cm.RMTarget);
                           break;
                           //then the rest…
                     default:
                          gm.LeftTarget.FSM.Token = temp;
                          gm.LeftTarget.FSM.ExecuteTransition();
                          gm.LeftTarget.FSM.CurrentState.Target =
                              gm.RightTarget;
                           cm.UpdateLeftGui(cm.LMTarget);
                           cm.UpdateRightGui(cm.RMTarget);
                           break;
                 }
 
             }
 
        }

    }
}
//reset the mouse-handler state and the Gui state
mh.GuiLClick = false;
  mh.LastButtonHit = -1;
  gm.FixButtonStates();
}
else if (mh.GuiLDown && mh.MidClick)
{
     //a left down has occured
     //test to see if a left panel button is the target
     int hit = gm.ButtonHit(GUI.GuiPanel.PanelType.LEFT, mh.Mouse);
     if (hit != -1)
     {
        //if so, start the button animation and track the button hit
        gm.ButtonStateChange(GUI.GuiPanel.PanelType.LEFT, hit, 1);
        mh.LastButtonHit = hit;
     }
     else
     {
        //test to see if a right panel button is the target
        hit = gm.ButtonHit(GUI.GuiPanel.PanelType.RIGHT, mh.Mouse);
        if (hit != -1)
        {
           //if so, start the button animation and track the button hit
           gm.ButtonStateChange(GUI.GuiPanel.PanelType.RIGHT, hit, 1);
           mh.LastButtonHit = hit;
        }
     }
     //advance the mouse-handler state
     mh.GuiLDown = false;
   }
}

We also need some updates to the GUI components, but as they are not directly relevant to the book’s topic and since they are pretty straightforward, I will list them, but not discuss them here. Here are the extra methods added to the GuiPanel class:

/// <summary>
/// Resets the panels to the default text when unselecting a character
/// </summary>
public void ResetPanelText()
{
    if (pType == GuiPanel.PanelType.LEFT)
    {
        textItems.Clear();
        textItems.Print("Select a Suma", LeftSuma, Color.White,
            (int)ScreenText.FontName.SUBHEAD_BOLD);
    }
    else if (pType == GuiPanel.PanelType.RIGHT)
    {
       textItems.Clear();
       textItems.Print("Select a Suma", RightSuma, Color.White,
           (int)ScreenText.FontName.SUBHEAD_BOLD);
    }
}
/// <summary>
/// Systematically disables all buttons contained in the panel
/// </summary>
public void DisableAllButtons()
{
    if ((pType == GuiPanel.PanelType.LEFT) || (pType == GuiPanel.PanelType.
         RIGHT))
    {
         for (int i = 0; i < NUM_BUTTONS; i++)
         {
              buttons[i].Enabled = false;
         }
    }
}
/// <summary>
/// Systematically resets all buttons contained in the panel
/// </summary>
public void ResetAllButtons()
{
    if ((pType == GuiPanel.PanelType.LEFT) || (pType == GuiPanel.PanelType.
         RIGHT))
    {
         for (int i = 0; i < NUM_BUTTONS; i++)
         {
              buttons[i].Reset();
         }
    }
}
/// <summary>
/// Returns the button at index i
/// </summary>
/// <param name="i">the index for the desired button</param>
/// <returns>The GetButton object stored at index i</returns>
public GuiButton GetButton(int i)
{
    return buttons[i];
}

Next up, new GuiButton methods:

/// <summary>
/// Resets the animation state for the button
/// </summary>
public void Reset()
{
    State = GuiButton.PressedState.RELEASED_OFF;
}

And finally, the new methods for the GuiManager class:

/// <summary>
/// to be used only for programmatic closing of the panels (not when user clicks on
/// the panel)
/// </summary>
/// <param name="pt"></param>
public void ClosePanel(GuiPanel.PanelType pt)
{
    if (pt == GuiPanel.PanelType.LEFT)
    {
        LeftPanel.MovingAway = true;
        LeftPanel.MovingOut = false;
        LeftPanel.ResetPanelText();
        LeftPanel.DisableAllButtons();
    }
    else if (pt == GuiPanel.PanelType.RIGHT)
    {
        RightPanel.MovingAway = true;
        RightPanel.MovingOut = false;
        RightPanel.ResetPanelText();
        RightPanel.DisableAllButtons();
    }
    else if (pt == GuiPanel.PanelType.TOP)
    {
        TopPanel.MovingAway = true;
        TopPanel.MovingOut = false;
    }
}
/// <summary>
/// Sets the state for the left panel button at index i to on if it is off
/// </summary>
/// <param name="i">Index of the button to set</param>
public void SetLeftPanelButton(int i)
{
    if (LeftPanel.GetButton(i).State == GuiButton.PressedState.RELEASED_OFF)
    {
        LeftPanel.GetButton(i).State = GuiButton.PressedState.RELEASED_ON;
    }
}
/// <summary>
/// Sets the state for the right panel button at index i to on if it is off
/// </summary>
/// <param name="i">Index of the button to set</param>
public void SetRightPanelButton(int i)
{
    if (RightPanel.GetButton(i).State == GuiButton.PressedState.RELEASED_OFF)
    {
        RightPanel.GetButton(i).State = GuiButton.PressedState.RELEASED_ON;
    }
}
/// <summary>
/// Toggles the state for the left panel button at index i
/// </summary>
/// <param name="i">Index of the button to toggle</param>
public void ToggleLeftPanelButtonEnable(int i)
{
    LeftPanel.ToggleButtonEnable(i);
}
/// <summary>
/// Toggles the state for the right panel button at index i
/// </summary>
/// <param name="i">Index of the button to toggle</param>
public void ToggleRightPanelButtonEnable(int i)
{
    RightPanel.ToggleButtonEnable(i);
}
/// <summary>
/// Pass-through method that enables all left panel buttons
/// </summary>
public void EnableAllLeftPanelButtons()
{
    LeftPanel.EnableAllButtons();
}
/// <summary>
/// Pass-through method that enables all right panel buttons
/// </summary>
public void EnableAllRightPanelButtons()
{
    RightPanel.EnableAllButtons();
}
/// <summary>
/// Pass-through method that resets all left panel buttons
/// </summary>
public void ResetAllLeftPanelButtons()
{
    LeftPanel.ResetAllButtons();
}
/// <summary>
/// Pass-through method that resets all right panel buttons
/// </summary>
public void ResetAllRightPanelButtons()
{
    RightPanel.ResetAllButtons();
}
/// <summary>
/// A pass-through function that allows the state change for a button of a
/// particular panel
/// </summary>
/// <param name="pt">The panel to change</param>
/// <param name="b">The index of the button to change</param>
/// <param name="x">- -ignored- -</param>
public void ButtonStateChange(GuiPanel.PanelType pt, int b, int x)
{
    if (pt == GuiPanel.PanelType.LEFT)
    {
         LeftPanel.ChangeButtonState(b);
    }
    else if (pt == GuiPanel.PanelType.RIGHT)
    {
         RightPanel.ChangeButtonState(b);
    }
    else if (pt == GuiPanel.PanelType.TOP)
    {
         //empty until buttons added to top panel
    }
}
/// <summary>
/// Fixes the button states of both panels
/// </summary>
public void FixButtonStates()
{
    LeftPanel.FixButtonState();
    RightPanel.FixButtonState();
}
/// <summary>
/// Converts panel type and button index into an input token
/// </summary>
/// <param name="pt">The panel holding the button</param>
/// <param name="hit">the button hit</param>
/// <returns>the appropriate input token</returns>
public String GetButtonToInputToken(GuiPanel.PanelType pt, int hit)
{
    if (pt == GuiPanel.PanelType.LEFT)
    {
         return LeftPanel.GetButton(hit).Token;
    }
    else
    {
        return RightPanel.GetButton(hit).Token;
    }
}

With these implementations of an interface that allows for indirect control of the characters and atomic behaviors for the characters, we are finally ready to start focusing on the VGAI and letting these insane but vaguely frog-like beings loose on the universe! Time to celebrate (or something).

This method of creating the base behaviors in advance of trying to tackle the VGAI enables us to focus all of our energies on understanding a complex and critical-to-success issue: creating behavior that appears natural to the player. In the next chapter, we will begin to explore what it takes to create a believable NPC.

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.92.116