Tuesday, August 13, 2013

Bunny Hunters Online: Mode Management

Note: The mode management stuff described here requires an update to TwoDEngine now available.

A computer game typically progresses through a number of "screens" or "display modes".  Each of these has a specific function.  The title screen, help screen, options menu and play field are all examples of a mode.

Bunny hunters will have seven modes the player progresses though.  These modes are typical of a minimal online game and are described with the "bubble diagram" below:

Another word for a mode is a "state".  A machine that can be in one and only one state at a time is called a Finite State Automaton (FSA).   A game is typically an FSA.

TwoDEngine is made up of a set of completely independent modules.  You can use some or all of them in your game. So far, the game has used the Scenegraph module.  Another module in TwoDEngine is the ModeManager.  This is a simple FSA that makes writing and organizing your modes easier.

Each mode is defined in a class that implements the GameMode interface.   This interface looks like a simplified version of the Monogame Game class, defining only an Update and Draw method.  It also has two new methods "EnterMode()" and "ExitMode()".
public interface GameMode
    {
        void EnterMode();
        void Update(GameTime gameTime);
        void Draw(GameTime gameTime);
        void ExitMode();
    }
As explained above, the ModeManager can be in only one mode at a time.  This mode is either a programmer defined instance of the GameMode interface, or null meaning "no mode."

When a GameMode instance first becomes active, the ModeManager calls its "EnterMode()" Method.  When a different mode is set active, the previously active mode has its "ExitMode()" method called.  In between the Update() and Draw() methods of only the active mode get called by the ModeManager on every game Update and Draw.

In addition to implementing the four GameMode methods (an implementation can be empty but it must still be there), an instance of a GameMode must declare a special initializer.  This initializer takes two parameters.  The first is a reference to an instance of the Monogame Game class, which in BHO is the instance of the Game1.cs class.  The second is a string, which is the state name assigned to this mode on its creation.

The ModeManager can be used on its own, without the Scenegraph module.  In this case drawing would be handled by the implementation code of the mode's Draw method.  However, a utility class is provided to make it easy to use the ModeManager with the Scenegraph.  This is an abstract class called AbstractScenegraphMode.

Every instance of an AbstractScenegraphMode has a field called Root.  Rather then parenting that mode's scene graph to the Scenegraph object itself, the constructor of an AbstractScenegraphMode sub-class should parent its objects to the Root.  The EnterMode() and ExitMode() code inside of AbstractScenegraphMode switches the Root "on" and "off" so that the Scenegraph Objects under the Root only display and update when it is the current mode.

The first mode is very simple as it just displays the title screen until it sees a button click.  Create a new C# file in the shared source folder called TitleMode.  Note: VS2012 will helpfully think its namespace is BHO.shared_source because it is in the shared source folder.  Fix this by just deleting the .shared_source from the namespace line near the top of the file.  It should look something like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BHO
{
    class TitleMode
    {
    }
}

The first thing to add is using statements for the parts of TwoDEngine TilteMode will use. These are using TwoDEngine, using TwoDEngine.Scenegraph.SceneObjects and using TwoDEngine.ModeManager.  It also needs some Monongame namespaces: Microsoft.Xna.Framework,  Microsoft.Xna.Framework.Graphics and  Microsoft.Xna.Framework.Input.

The using block should look like this afterward:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using TwoDEngine;
using TwoDEngine.Scenegraph.SceneObjects;
using TwoDEngine.ModeManager;
The next thing to do is to make TitleMode a sub-class of AbstractScenegraphMode.  (I assume you know enough C# to do this so I won't go into any deeper explanation.)  Then you should right click on AbstractScenegraphMode and select "Implement Abstract Class".  (Note:  You may have to do a quick build for it to show you this options. If so then don't worry about the errors it spits out, its not done yet.)

That should leave you with a class skeleton that looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using TwoDEngine;
using TwoDEngine.Scenegraph.SceneObjects;
using TwoDEngine.ModeManager;

namespace BHO
{
    class TitleMode : AbstractScenegraphMode
    {
        public override void Draw(GameTime gameTime)
        {
            throw new NotImplementedException();
        }

        public override void Update(GameTime gameTime)
        {
            throw new NotImplementedException();
        }
    }
}

As generated, Draw and Update will throw an exception when called.  TitleMode won't do any drawing, thats up to the Scenegraph, but it shouldn't throw an error either, so remove that line and leave Draw() empty.  (I generally put a comment of //NOP in such methods just to make it clear I intended them to do nothing.)  Update will respond to the mouse button, but first TitleMode needs a constructor.

All GameMode instances must implement a constructor that takes two arguments.  The first is a Game reference to the main game object.  The second is a string that contains the name the ModeManager knows this mode by.   This constructor must in turn pass control to the no-argument constructor of AbstractScenegraphMode.  (Failure to pass control will cause the Root field to be null when you try to use it.)

Add a constructor like this:
public TitleMode(Game game,string name): base()
        {
            Texture2D startScreen = game.Content.Load<Texture2D>("intro");
            new BasicSprite(Root, startScreen);
        }
This constructor will load the title screen and attach it to Root so it can be switched on and off.

In order to detect a de-bounced mouse-button press, it is necessary to detect first a button down, then a button up.  This means it must store the fact that it has seen a button down.  This is easily accomplished by adding a class field to hold that information.  In general I add most class fields at the top of the class, but where a field is only relevant to a small section of the class, I will add it in that section.  In this case I added a boolean right above Update called mousePressed and set it to start false like this:

private bool mousePressed = false;
public override void Update(GameTime gameTime)
        {
            throw new NotImplementedException();
        }
Now the button press logic must be added to Update.  When update sees a left button down, it sets mousePressed true.  When it sees a left button up and mousePressed is true, it sets mousePressed back to false and change to the null mode (meaning no mode at all.)  That code looks like this:
public override void Update(GameTime gameTime)
        {
            MouseState mouseState = Mouse.GetState();
            if (!mousePressed && (mouseState.LeftButton == ButtonState.Pressed))
            {
                mousePressed = true;
            }
            if (mousePressed && (mouseState.LeftButton == ButtonState.Released))
            {
                mousePressed = false;
                Registry.Lookup<ModeManager>().ChangeMode((GameMode)null);
            }
        }
The completed TitleMode.cs file should now look like this:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using TwoDEngine;
using TwoDEngine.Scenegraph.SceneObjects;
using TwoDEngine.ModeManager;

namespace BHO
{
    class TitleMode : AbstractScenegraphMode
    {
        public TitleMode(Game game,string name): base()
        {
            Texture2D startScreen = game.Content.Load<Texture2D>("intro");
            new BasicSprite(Root, startScreen);
        }

        public override void Draw(GameTime gameTime)
        {
            //NOP
        }

        private bool mousePressed = false;
        public override void Update(GameTime gameTime)
        {
            MouseState mouseState = Mouse.GetState();
            if (!mousePressed && (mouseState.LeftButton == ButtonState.Pressed))
            {
                mousePressed = true;
            }
            if (mousePressed && (mouseState.LeftButton == ButtonState.Released))
            {
                mousePressed = false;
                Registry.Lookup<ModeManager>().ChangeMode((GameMode)null);
            }
        }
    }
}



The last thing needed to make this mode display is to change the code in Game1.cs for the constructor and the LoadContent method.  The constructor needs to create the ModeManager the same way it created the Scenegraph, with a new.  Add it to the bottom of the Game1() constructor so it looks like this:
public Game1()
        {
            Content.RootDirectory = "Content";
            Scenegraph sg = new Scenegraph(this);
            sg.SetBackgroundColor(Color.Aquamarine);
            //sg.SetBackgroundEnabled(false);
            GraphicsDeviceManager _graphics = Registry.Lookup<GraphicsDeviceManager>();
            _graphics.PreferredBackBufferHeight = 600;
            _graphics.PreferredBackBufferWidth = 800;
            new ModeManager(this);
        }

Currently the LoadContent method loads the title screen and attaches it to the Scenegraph.   In the new version of the game all it will do is create the mode and set the ModeManager.  Chagge it to look like this:

protected override void LoadContent()
        {
            
            // TODO: use this.Content to load your game content here
            ModeManager mgr = Registry.Lookup<ModeManager>();
            mgr.NewMode<TitleMode>("Title Mode");
            mgr.ChangeMode("Title Mode");

        }
Now build and run the game.  You should see the title screen. When you click your mouse on the title screen, it should change to black.

This line deserves some added explanation:


 mgr.NewMode<TitleMode>("Title Mode");


This is type of method is called a generic factory method, and is a particularly unique and useful C# trick.  A factory method is a method that creates and returns an instance of a class.  Traditionally, the actual class of the object is contained within the factory method and what it returns as a type is an interface.

In C# however, we can access generic types at run-time.  This makes possible a factory method that actually creates and returns an instance of a specific class we specify at run-time.  In this method, that class can be any sub-class of GameMode.   The passed in parameter is the name of this instance.

You might wonder why it doesn't just say "new TitleMode(this,"Title Mode").  This syntax would also create the same instance however the ModeManager would not be aware of it.  By using a factory method on the ModeManager, the ModeManager can internally track the names of all GameMode instances created.


In the next blog a new mode will be created that handles the network login.


No comments:

Post a Comment