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:
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:
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