Building Your First Game with MonoGame: Finishing the App

Dean Ellis

Learning to build games has a reputation for being difficult. MonoGame, a cross-platform gaming framework based on Microsoft’s XNA framework, makes it easy. Yesterday, we walked through the process of building a user interface, complete with sounds, textures, and other assets, for MonkeyTap, a Whack-a-Mole style game where gamers must tap monkeys before they disappear or risk having the monkey steal all of their bananas. Today, we’re going to take a look at how to add simple game logic to MonkeyTap to make the game playable.

Adding Game Logic

Once you have a user interface built for your game, the next step is to add the necessary logic so that it can be played. To get started, either follow the steps for creating a user interface for MonkeyTap, or download a starter version to serve as a base to which to add game logic. If you run MonkeyTap, you should get a grid of monkeys as seen below:

The UI for MonkeyTap built with MonoGame and the Pipeline Editor.

We need to update the monkey grid to allow users to interact with it to play the game. First, define an enumeration GameState in the MonkeyTap Shared Project with the following values:

enum GameState 
{
    Start,
    Playing,
    GameOver
}

Copy and paste the following class-level fields into the Game1 class:

// Set initial game state
GameState currentState = GameState.Start;
Random rnd = new Random ();

// Text to display to user
string gameOverText = "Game Over";
string tapToStartText = "Tap to Start";
string scoreText = "Score : {0}";
    
// Timers: Calculate when events should occur in our game
TimeSpan gameTimer = TimeSpan.FromMilliseconds (0);
// Define how often the level difficulty increases
TimeSpan increaseLevelTimer = TimeSpan.FromMilliseconds(0);
// Define the delay between game ending and new game beginning
TimeSpan tapToRestartTimer = TimeSpan.FromSeconds(2);

// How many cells should be altered in a level
int cellsToChange = 0;
int maxCells = 1;
int maxCellsToChange = 14;
int score = 0;

All of these fields are used to track the various states the game can be in. As you continue to play MonkeyTap, more and more cells will be changed during a given level, making the game harder. Now that the bulk of the required configuration to keep track of game state is out of the way, it’s time to start implementing our game logic!

Process User Touch

Handling user input is a major part of the game mechanics, so it’s important to select a game engine that can handle all types of input. MonoGame has a great set of input controls, one of which is the TouchPanel. TouchPanel‘s GetState method returns a collection of touch locations, as well as their state, which can be Pressed, Moved, and Released. This allows you to track when and where a user touches the screen. In this case, if the user has tapped the screen, we will loop through all of the cells in the grid and check to see if that location intersects with the cell display rectangle. If it does, and the monkey was showing at the time, we play a sound, reset the cell, and increment the user’s score (they stopped that monkey from stealing their banana!). Add the ProcessTouches method below to your Game1 class:

void ProcessTouches(TouchCollection touchState)
{
    foreach (var touch in touchState) {
        if (touch.State != TouchLocationState.Released)
            continue;
        for (int i=0; i < grid.Count; i++) {
            if (grid [i].DisplayRectangle.Contains (touch.Position) && grid[i].Color == Color.White) {
                hit.Play ();
                grid [i].Reset ();
                score += 1;
            }
        }
    }
}

Check If The Game Is Over

In MonkeyTap, monkeys display for five seconds at a time. If the monkey is not tapped during that time, the game will end. To check for game over, we can loop through all of the items in the monkey grid and call the Update method, which will return true if a monkey has been showing for five seconds. In that case, the user has failed to tap a monkey within the given timeframe, so we need to change the GameState to GameOver and start the tapToRestartTimer, which prevents the game from immediately restarting (and the gamer not seeing their score) with an errant tap after the game has already ended. Add the CheckForGameOver method to the Game1 class:

void CheckForGameOver (GameTime gameTime)
{
    for (int i = 0; i < grid.Count; i++) {
        if (grid [i].Update (gameTime)) {
            currentState = GameState.GameOver;
            tapToRestartTimer = TimeSpan.FromSeconds (2);
            break;
        }
    }
}

Calculate Monkeys to Show for Level

Most games increase in difficulty as play continues, and MonkeyTap is no exception! Each level will show more and more monkeys, so we need to calculate exactly how many cells we need to display. The gameTimer we created earlier is used to keep track of how much time has elapsed since this level began. We can increment the timer by accessing gameTime.ElapsedGameTime, which holds the amount of time since the last Update was called. Once this timer is over two seconds, we reset it to zero and then calculate the number of cells to change up to a maximum number. Add the CalculateCellsToChange method to your Game1 class:

void CalculateCellsToChange (GameTime gameTime)
{
    gameTimer += gameTime.ElapsedGameTime;
    if (gameTimer.TotalSeconds > 2) {
        gameTimer = TimeSpan.FromMilliseconds (0);
        cellsToChange = Math.Min (maxCells, maxCellsToChange);
    }
}

Calculate Level Difficulty

As we saw in CalculateCellsToChange, maxCells defines the maximum number of monkeys to show in a particular level; by default, this value is set to 1. As the game progresses, we want the value to increase over time. To do this, we’ll use a timer to keep track of how long the level has elapsed, and after 10 seconds, move on to another level and increment the maximum number of monkeys displayed. As a result, MonkeyTap will show one extra monkey every 10 seconds. Add the IncreaseLevel method to your Game1 class:

void IncreaseLevel (GameTime gameTime)
{
    increaseLevelTimer += gameTime.ElapsedGameTime;
    if (increaseLevelTimer.TotalSeconds > 10) {
        increaseLevelTimer = TimeSpan.FromMilliseconds (0);
        maxCells++;
    }
}

Show Monkeys

Finally, we need to make the monkeys, which are invisible by default, visible. We don’t want MonkeyTap to be predictable, so we can use the Random class to select a random monkey in the grid. If the monkey isn’t already showing, we can display it and decrement the number of cells required to change for that level.

void MakeMonkeysVisible()
{
    if (cellsToChange > 0) {
        var idx = rnd.Next (grid.Count);
        if (grid [idx].Color == Color.TransparentBlack) {
            grid [idx].Show ();
            cellsToChange--;
        }
    }
}

Putting the Pieces Together

Now that we have all the individual pieces completed, let’s put them together in a method called PlayGame:

void PlayGame(GameTime gameTime, TouchCollection touchState)
{
    ProcessTouches (touchState);
    CheckForGameOver (gameTime);
    CalculateCellsToChange (gameTime);
    MakeMonkeysVisible ();
    IncreaseLevel (gameTime);
}

Order usually doesn’t matter. However, you generally want to check for user input first to make the game feel more responsive and ensure that the monkey has been on the screen for nearly the whole five seconds.

Executing Game Logic

Now that the majority of logic for MonkeyTap is complete, we need a way to continuously update the game as it executes. Remember, Update is used to update any game logic you have while your game executes (gathering user input or updating the world), so that’s where our PlayGame method should go. Replace the current code for your Update method with the following:

protected override void Update (GameTime gameTime)
{
    // For mobile, this logic will close the Game when the Back button is pressed
    // Exit() is obsolete on iOS
    #if !__IOS__ &&  !__TVOS__
    if (GamePad.GetState (PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
        Keyboard.GetState ().IsKeyDown (Keys.Escape)) {
        Exit ();
    }
    #endif

    // Custom logic from us
    var touchState = TouchPanel.GetState ();
    switch (currentState) {
        case GameState.Start:
            if (touchState.Count > 0) {
                currentState = GameState.Playing;
            }
            break;
        case GameState.Playing:
            PlayGame (gameTime, touchState);
            break;
        case GameState.GameOver:
            tapToRestartTimer -= gameTime.ElapsedGameTime;
            if (touchState.Count > 0 && tapToRestartTimer.TotalMilliseconds < 0) {
                currentState = GameState.Start;
                score = 0;
                increaseLevelTimer = TimeSpan.FromMilliseconds (0);
                gameTimer = TimeSpan.FromMilliseconds (0);
                cellsToChange = 1;
                maxCells = 1;
                for (int i = 0; i < grid.Count; i++) {
                    grid [i].Reset ();
                }
            }
            break;
    }

    base.Update (gameTime);
}

First, we call the TouchPanel.GetState method to grab the current touch state. We’ll pass this along to the PlayGame method, which will use it to handle user input. The switch statement controls our game’s state. Remember, we default to GameState.Start. The GameState.Playing case simply calls the game logic we previously wrote, while GameState.GameOver case checks to see if the user has tapped to restart the game and, if they have, reset all our fields to their initial value, clear the grid, and transition back to the GameState.Start state.

Drawing MonkeyTap

If you were to run MonkeyTap now, you wouldn’t see much other than the grid of monkeys you saw when you started. Although the game logic would be running behind the scenes, the user interface isn’t being updated after the game begins running. The correct place for all of this logic is the Draw method, which should be used exclusively to draw any graphics that need displaying.

protected override void Draw (GameTime gameTime)
{
    graphics.GraphicsDevice.Clear (Color.SaddleBrown);
            
    // Calculate the center of the screen
    var center = graphics.GraphicsDevice.Viewport.Bounds.Center.ToVector2();
    
    // Calculate half the width of the screen
    var half = graphics.GraphicsDevice.Viewport.Width / 2;
            
    // Calculate aspect ratio of the MonkeyTap logo
    var aspect = (float)logo.Height / logo.Width;
           
    // Calculate position of logo on screen
    var rect = new Rectangle ((int)center.X - (half /2) , 0, half, (int)(half * aspect));

    spriteBatch.Begin ();
    
    // Draw the background
    spriteBatch.Draw (background, destinationRectangle: graphics.GraphicsDevice.Viewport.Bounds, color: Color.White);
    
    // Draw MonkeyTap logo
    spriteBatch.Draw (logo, destinationRectangle: rect, color: Color.White);
    
    // Draw a grid of squares
    foreach (var square in grid) {
        spriteBatch.Draw (monkey, destinationRectangle: square.DisplayRectangle, color: Color.Lerp (Color.TransparentBlack, square.Color, square.Transition));
    }
            
    // If the game is over, draw the score and game over text in the center of screen.
    if (currentState == GameState.GameOver) {
        // Measure the text so we can center it correctly
        var v = new Vector2(font.MeasureString (gameOverText).X /2 , 0);
        spriteBatch.DrawString (font, gameOverText, center - v, Color.OrangeRed);
                
        var t = string.Format (scoreText, score);
        // Measure the text so we can center it correctly
        v = new Vector2(font.MeasureString (t).X /2 , 0);
        // We can use the font.LineSpacing to draw on the line underneath the "Game Over" text
        spriteBatch.DrawString (font, t, center  + new Vector2(-v.X, font.LineSpacing), Color.White);
    }
    
    // If the game is starting over, add "Tap to Start" text
    if (currentState == GameState.Start) {
        // Measure the text so we can center it correctly
        var v = new Vector2(font.MeasureString (tapToStartText).X /2 , 0);
        spriteBatch.DrawString (font, tapToStartText, center - v, Color.White);
    }

    spriteBatch.End ();

    base.Draw (gameTime);
}

One important thing to remember when building both apps and games is that you must take into account the various screen sizes and form factors of the devices in the world, from mobile phones to TV screens. Unlike app development, we aren’t provided with dynamic layout engines like AutoLayout or layout containers like Android’s LinearLayout to dynamically display game items. The ViewPort property exposes properties to help us figure out how big a cell should be on the screen.

Text must be centered on the screen, so we can calculate the center using:

var center = graphics.GraphicsDevice.Viewport.Bounds.Center.ToVector2();

The ViewPort.Bounds property is a rectangle type which contains a Center property that can be used to draw text in the center of the screen:

var v = new Vector2(font.MeasureString (tapToStartText).X /2 , 0);
spriteBatch.DrawString (font, tapToStartText, center - v, Color.White);

We can use SpriteFont.MeasureString to calculate the size of the text, which returns a vector with the width (x) and height (y). We can then take that value away from the center, so that when we draw the string it ends up in the wrong place. SpriteFont also exposes a useful property named LineSpacing, which we can utilize to make sure that when we draw text vertically, it’s properly spaced; we use this to display the user’s score underneath the “Game Over” text.

Run the game now and you should have a fully functional MonkeyTap game for Android. If you run the code, you should be able to play the game all the way through:

An example of a mobile game built with Xamarin and MonoGame.

Wrapping Up

You just built your first game for Android using MonoGame! Some bits can be a little tricky, but once you understand the major pieces in play (get it?), it starts to make sense. If we wanted, we could add an iOS version of MonkeyTap by simply adding a new project to the MonkeyTap solution that references the Shared Project we created and deleting the Game1 class that is autocreated. Boom—just like that, you’ve added iOS support for your game in a matter of seconds!

In this brief series, we took a look at how to create a user interface for your game, as well as how to add logic to create a Whack-a-Mole style game called MonkeyTap. You can download the full source code for MonkeyTap (assets, game logic, and all) here, or download MonkeyTap directly onto your Android device from the Google Play store. Be sure to let us know what awesome games you are creating with MonoGame and Xamarin!

0 comments

Discussion is closed.

Feedback usabilla icon