Honeycomb Rush Cross-Platform Game Development Tutorial: MonoGame – Part 1 (from MonkeySpace Conference)

Overview

This tutorial will introduce you to game development on Windows 8 using game state management basics, Visual Studio, and the MonoGame open source library. In Part 1 of this tutorial, you will start building the Honeycomb Rush 2D game by completing the following tasks:

Step 1 – Create MonoGame Game Project

Step 2 – Adding Assets (Content Pipeline)

Step 3 – Add Game State Management Base Classes; ScreenManager

This tutorial is an adaptation of the Honeycomb Rush Hand-On-Lab originally published on the XNA Creators Club (located at http://xbox.create.msdn.com/en-US/education/catalog/lab/honeycomb_rush), and is being delivered in two parts with the initial game creation steps being on this blog and the completion of the game with adding the Buddy API (http://buddy.com) to implement the high scores screen and additionally adding the Windows Phone platform to the implementation. This series will conclude with advanced features shown on Michael Cummings’s blog (http://michaelcummings.net) to include cross-platform options using MonoGame as a native cross-platform game framework.

This tutorial was originally presented in two parts at MonkeySpace 2013 in Chicago, IL USA. You can find both sets of presentations here.

Step 1: Create a MonoGame Project

In order to leverage MonoGame to create a C#/XNA project on Windows 8 and allow for the game to be a cross-platform solution, the first step is to install the build of MonoGame from www.monogame.net/downloads. My suggestion would be to install the stable build 3.01 from March 6th, however, getting the latest features and installing the latest build from the build server at this link will also suffice.

Once MonoGame is installed, go into Visual Studio 2012 and start a new MonoGame Windows Store Project. Name the project HoneycombRush and ensure that the option to Create a new solution is selected, as well as, the Create a new directory for solution option checked. See figure below:


Once the project is successfully created, your project should have a Program.cs file and a Game1.cs file and have a solution and project structure similar as noted below:


Run project to ensure all MonoGame was installed corrected and solution has all dependencies and files needed. You should get the typical Cornflower blue screen that is customary with the XNA Game Studio projects.


Step 2: Create Content Pipeline and Add Content files (graphics, sounds, etc.)

Given the MonoGame’s content pipeline is currently in beta, let’s quickly leverage the Visual Studio XNA Game Studio option to create the necessary .xnb binary files of the graphics, sound, and media needed to run the game. If you need to understand this process in detail, please see my previous blog post: Updating Graphics using Content Pipeline with MonoGame.

Temporarily, we are going to create and add Visual Studio XNA Game Studio Windows Phone Game project to the solution to use that project’s content pipeline to create the binary .xnb files needed to add the media files associated with the project. Right-click solution and add new project. When New project dialog comes up, select Windows Phone Game and name project GameContentResource and click ok. See graphics below.



In the GameContentResource project, under the GameContentResourceContent (Content) subproject, add the following folders:

  • Fonts
  • Sounds
  • Textures
  • Backgroundsput underneath Textures folder

Now to add the media files go to link http://sdrv.ms/16YapMn and download all the files into the folders noted above respective to their respective folders on the SkyDrive. Under the Sounds folder, find the files InGameSong_Loop.wav and MenuMusic_Loop.wav, right-click and select Properties window. Change the content processor to Song – XNA Framework. Right-click on the GameContentResource project and select Build. Once the project has built successfully, go to …\HoneycombRush\GameContentResource\GameContentResource\bin\Windows Phone\Debug\Content folder in Explorer. Select the folders and drag them into the HoneycombRush project under the Content folder.


After you have successfully dragged the three folders into the project under Content and verified the .xnb binary files are located underneath the folders as appropriate. Add a folder called Configuration under the Content folder, add the file Configuration.xml from http://sdrv.ms/149kUSk into the newly created folder. Finally, add the AnimationsDefinition.xml file from http://sdrv.ms/14hh1FV to the project in the Textures folder. The final folder structure should look similar to the graphic shown below:


Step 3: Add Game State Management Base Classes; ScreenManager

In order to build our Game Screens and take advantage of a Game State Management system for our game, we are going to leverage a great example of Game State Management from the Xbox Game State Management sample, however, we are going to tweak the sample for the needs of this game. Then we will build our screens to inherit from the base screens in the Game State Management base class and have all screens loaded managed by a single screen manager. Let’s get started!

First, let’s add a folder to the solution named ScreenManager and a folder called Screens into the HoneycombRush project. On the XNA Game Developer Center, there is a tutorial for Game State Management that is known for the base classes needed to effectively add Game State Management into XNA games. The sample is located at: http://xbox.create.msdn.com/en-US/education/catalog/sample/game_state_management

Let’s add the ScreenManager.cs, InputState.cs and the GameScreen.cs classes from this sample at the aforementioned link and alter these base classes for our needs. Open up the ScreenManager.cs class. Let’s look at what the ScreenManager class in designed to accomplish and let’s alter for our game as appropriate. The first thing you will notice is that the ScreenManager class contains two Lists of GameScreen objects, an InputState object, a SpriteBatch and Font, a blankTexture, and two Booleans values indicating whether it’s initialized and if trace is enabled. You will also notice see that the SpriteBatch and Font are exposed as read-only properties, we understand that they are read-only given they only have a “get” property assessor. The ScreenManager constructor does nothing new; Initialize() only sets the “isInitialized” boolean to true; and LoadContent() sets up the SpriteBatch, Font, and blankTexture members. We then proceed to iterate through our list of screens and call the LoadContent() method for each of the screens in the list. In UnloadContent(), we follow the same process. The heart of this class and its management of the screens presented is primarily in the Update and Draw methods.

In our Update() method, the first thing that is accomplished is the update of the InputState. The next step is that we will make a copy of the List containing the screens for the game, so as we are making changes we have a reference.

In order to update the ScreenManager class to reflect the game we are building, we will make some changes from the base functionality in provided class. The first change we will make is here by changing the variables to something more meaningful.

Go to the following code in ScreenManager.cs:

private const string StateFilename = “ScreenManagerState.xml”;

List<GameScreen> screens = new List<GameScreen>();
List<GameScreen> tempScreensList = new List<GameScreen>();
InputState input = new InputState();

Replace the code above with the following code:

//private const string StateFilename = “ScreenManagerState.xml”;

List<GameScreen> screens = new List<GameScreen>();
//List<GameScreen> tempScreensList = new List<GameScreen>();
List<GameScreen> screensToUpdate = new List<GameScreen>();
InputState input = new InputState();

The screensToUpdate List object of type GameScreen is now our reference of which screens to update so as any screen is requested to be added and/or removed, the ScreenManager will be able to update the appropriate screen in the list of screens of our game (screensToUpdate) so that the newly added or removed screen will be updated to be inserted or deleted in the next Update() call.

Next let’s update the variables by adding a private variable of type Texture2D that will represent the background of the buttons in our game and add a public property assessor for this Texture2D to our class.

Go to the following code in ScreenManager.cs:

SpriteBatch spriteBatch;
SpriteFont font;
Texture2D blankTexture;

Add another Texture2D for the Button Background; the code should look as follows:

SpriteBatch spriteBatch;
Vector2 drawingScale;
SpriteFont font;
Texture2D blankTexture;
Texture2D buttonBackground;

If you wish to trace the events, errors, and data associated with the various screens within your ScreenManager class, you may wish to enable a trace element. In order to track whether a screen should record a traceEnabled variable and property assessor is included in ScreenManager add a flag to determine whether the ScreenManager desires to trace activities.

Now that we have update the properties associated with this class, let’s update the class constructor to scale the screens as appropriate to the device screen capabilities and size of the user that is running out game. Currently the ScreenManager constructor looks as follows:

public ScreenManager(Game game): base(game)
{
// we must set EnabledGestures before we can query for them, but
// we don’t assume the game wants to read them.
   TouchPanel.EnabledGestures = GestureType.None;
}

Change the ScreenManager constructor to reflect the code changes noted below:

public ScreenManager(Game game, Vector2 drawingScale) : base(game)
{
  // we must set EnabledGestures before we can query for them, but
  // we don’t assume the game wants to read them.
    TouchPanel.EnabledGestures = GestureType.None;
    this.drawingScale = drawingScale;
}

Now let’s update the LoadContent() method to load the media content related to our game which are global resources to the game despite the screen that loaded; i.e. Items like font for the Menu or the graphic for the buttons in the game. We will load the graphics as related to the defined graphic properties of our ScreenManager class.

Go to the existing LoadContent() method which should look as follows:

protected override void LoadContent()
{
   // Load content belonging to the screen manager.
    ContentManager content = Game.Content;
    spriteBatch = new SpriteBatch(GraphicsDevice);
    font = content.Load<SpriteFont>(“menufont”);
    blankTexture = content.Load<Texture2D>(“blank”);
    // Tell each of the screens to load their content.
    foreach (GameScreen screen in screens)
    {
       screen.Activate(false);
   }
}

Update the code of the LoadContent() method so that the code mirrors the code below:

protected override void LoadContent()
{
    // Load content belonging to the screen manager.
     ContentManager content = Game.Content;
    //spriteBatch = new SpriteBatch(Game.GraphicsDevice, drawingScale);
      spriteBatch = new SpriteBatch(Game.GraphicsDevice);
Game.Services.AddService(typeof(SpriteBatch), spriteBatch);
      font = content.Load<SpriteFont>(“Fonts/MenuFont”);
      blankTexture = content.Load<Texture2D>(“Textures/Backgrounds/blank”);
      buttonBackground = content.Load<Texture2D>(“Textures/Backgrounds/buttonBackground”);
    // Tell each of the screens to load their content.
      foreach (GameScreen screen in screens)
      {
          screen.LoadContent();
      }
}

You will initially notice that the line of code screen.LoadContent() shows an error. This error is caused because the current GameScreen class does not have a definition for a LoadContent() method as yet. When we update the GameScreen class for our game, we will create a LoadContent() method so that each screen can load content that is specific to what is need for that screen’s display and/or functionality. Since we haven’t updated that code as of yet, it will currently show an error. Also note that in the LoadContent() method SpriteBatch for the entire game is added as a game service that will be leveraged across the various screens in our games.

Now let’s look at the existing UnloadContent() method that the ScreenManager:

protected override void UnloadContent()
{
// Tell each of the screens to unload their content.
   foreach (GameScreen screen in screens)
{
screen.Unload();
    }
}

Let’s change the UnloadContent() method to unload the content for each of the GameScreen noted in the list of GameScreens in the ScreenManager. Go the UnloadContent() method, we will unload content contained on each screen with an UnloadContent() method that we will add to the GameScreen class later in this tutorial. Right now since the method does not exist, similarly to the LoadContent method, we will get a compilation error noted for this method.

protected override void UnloadContent()
{
    // Tell each of the screens to unload their content.
    foreach (GameScreen screen in screens)
    {
//screen.Unload();
screen.UnloadContent();
}
}

Now let’s get into the meat of the Game State Management class by looking at the Update() and Draw() methods in the ScreenManager class. The first thing to do in updating our Update() is to change all of the references to tempScreensList
to screensToUpdate.

We will change the Update function to read input from any input device being used within the game before entering the foreach loop. We will also change the Update() method to make a copy of the all the game screens in the screens object which is defined as a List<GameScreen>into the screensToUpdate object. We will use the screensToUpdate object to loop through each of the screens starting with the topmost screen checking first whether the game is active and setting a boolean, otherScreenHasFocus, to the result of this check. We set this boolean assuming the screen is not covered by another screen based upon game active state. We then begin taking screens from the end of the list – the highest index is processed first – and looping over them. In each iteration of the loop, a screen is selected and removed from the list of screens to update then Update() method of the GameScreen is called on each screen selected. First, we verify that the screen might be ready to handle input – that it is either transitioned on, or active. If either of these is the case, we then verify that no other screen has handled the input, and if no other screen has, we pass the InputState into the HandleInput() method. We additionally check for active popup screens that may be covering a screen and update the Boolean flag as appropriate for this condition. Once this is done, we assume all input has been handled, and set the flag that prevents us from handling the same input on multiple screens.

Change your Update() class to reflect the changes discussed above and ensure the code is your Update() method looks like the code below:

public override void Update(GameTime gameTime)
{
 // Read the keyboard and gamepad.
input.Update();

    // Make a copy of the master screen list, to avoid confusion if
// the process of updating one screen adds or removes others.
screensToUpdate.Clear();

foreach (GameScreen screen in screens)
screensToUpdate.Add(screen);

bool otherScreenHasFocus = !Game.IsActive;
    bool coveredByOtherScreen = false;

// Loop as long as there are screens waiting to be updated.
    while (screensToUpdate.Count > 0)
    {
        // Pop the topmost screen off the waiting list.
        GameScreen screen = screensToUpdate[screensToUpdate.Count – 1];
screensToUpdate.RemoveAt(screensToUpdate.Count – 1);

// Update the screen.
        screen.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);

if (screen.ScreenState == ScreenState.TransitionOn || 
screen.ScreenState == ScreenState.Active)
        {
           // If this is the first active screen we came across,
           // give it a chance to handle input.
           if (!otherScreenHasFocus)
           {
              screen.HandleInput(gameTime, input);
              otherScreenHasFocus = true;
           }

// If this is an active non-popup, inform any subsequent
           // screens that they are covered by it.
           if (!screen.IsPopup)
               coveredByOtherScreen = true;
       }
}

// Print debug trace?
    if (traceEnabled)
       TraceScreens();
}

Since most of the heavy lifting of the ScreenManager class is done in the Update() method, the Draw() method then becomes a simple rendering of the media of the game screens that are not currently being hidden by the game. Therefore, the code in the Draw() method will not need to be updated and should reflect the code shown below:

public override void Draw(GameTime gameTime)
{
   foreach (GameScreen screen in screens)
   {
      if (screen.ScreenState == ScreenState.Hidden)
      continue;
      screen.Draw(gameTime);
   }
}

To complete the ScreenManager class all we have now to do is ensure that the methods to add, select, and remove screens to the ScreenManager object: AddScreen() and RemoveScreen() respectively are updated in our ScreenManager. These methods are simple as AddScreen() sets properties of the new screen being added and calls the LoadContent() method as appropriate to the screen. The RemoveScreen() method then removes desired screen by unloading any content and then removes the game screen from the permanent and temporary List<GameScreen> objects; screens and screensToUpdate. The GetScreens() method returns an array of GameScreen objects from the base List<GameScreen> object; screens.

Review the code below and ensure your ScreenManager class has methods for AddScreen(), GetScreen(), and RemoveScreen() that reflects code shown below. Make sure you update your RemoveScreen() method to use screensToUpdate variable instead of the tempScreensList variable.

public void AddScreen(GameScreen screen, PlayerIndex? controllingPlayer)
{
   screen.ControllingPlayer = controllingPlayer;
   screen.ScreenManager = this;
   screen.IsExiting = false;
   // If we have a graphics device, tell the screen to load content.

   if (isInitialized)
{
      screen.Activate(false);
   }
   screens.Add(screen);
   // update the TouchPanel to respond to gestures this screen is interested in
TouchPanel.EnabledGestures = screen.EnabledGestures;
}

public void RemoveScreen(GameScreen screen)
{
   // If we have a graphics device, tell the screen to unload content.
   if (isInitialized)
   {
     screen.Unload();
   }
   screens.Remove(screen);
screensToUpdate.Remove(screen);
// if there is a screen still in the manager, update TouchPanel
   // to respond to gestures that screen is interested in.
if (screens.Count > 0)
   {
      TouchPanel.EnabledGestures = screens[screens.Count – 1].EnabledGestures;
   }
}

public GameScreen[] GetScreens()
{
return screens.ToArray();
}

Finally, there’s a FadeBackBufferToBlack() method that can be used for fading colors. In this code a blankTexture displays a 4x4px sprite across the game screen/window. You can use any color you desire by designation of a color in the SpriteBatch.Draw() method.

Let do some finishing touches to this game. In the using statements of our ScreenManager class you will find a reference to System.IO.IsolatedStorage which is a library that is unique to Windows Phone only. Since we will are currently targeting Windows 8, but we will want our game to run on Windows Phone as well. Change the using statement for System.IO.IsolatedStorage to look as follows:

#if WINDOWS_PHONE

using System.IO.IsolatedStorage;

#endif

We have now implemented the first step in creating a game state management solution for our game by adding a ScreenManager to our game. In Part 2, we will update and implement the GameScreen class for our game and start building the first screen to our HoneycombRush game, as well as, include key game objects.