Difference between revisions of "Monkey Tap"
(→Adding Game Logic) |
|||
Line 197: | Line 197: | ||
=Adding Game Logic= | =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. 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 | + | 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. 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 Game1.cs variable declaration section with the following values: |
<syntaxhighlight lang=csharp> | <syntaxhighlight lang=csharp> | ||
Line 208: | Line 208: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Copy and paste the following | + | Copy and paste the following below the enum you have just created in the Game1 class: |
<syntaxhighlight lang=csharp> | <syntaxhighlight lang=csharp> |
Revision as of 18:04, 21 October 2017
Contents
- 1 File -> New Game
- 2 Anatomy of a Game
- 3 Adding Game Assets
- 4 Creating MonkeyTap User Interface
- 5 Building the MonkeyTap User Interface
- 6 Using the new GridCell class
- 7 Adding Game Logic
- 8 Process User Touch
- 9 Check If The Game Is Over
- 10 Calculate Monkeys to Show for Level
- 11 Calculate Level Difficulty
- 12 Show Monkeys
- 13 Putting the Pieces Together
- 14 Drawing MonkeyTap
File -> New Game
To get started, we first need to make sure we have both Xamarin and the latest development build of MonoGame installed. For this tutorial, we will be using Xamarin Studio, though the exact same steps apply when building games with MonoGame in Visual Studio as well.
First, we need to create a shared project where our game logic will reside that can also be shared with all target platforms we want our game to run on. Within Xamarin Studio, choose File -> New Solution -> MonoGame -> Library -> MonoGame Shared Library and name the project MonkeyTap. We must add projects for all of the platforms we wish to target.
Add a “MonoGame for Android Application” project to the solution and name it MonkeyTap.Android. If our game needed to take advantage of native Android features, such as NFC, the platform-specific project is the place to do so. Because our game doesn’t utilize any of these components, we can delete the Game1 class created in the Android project and add a reference to the shared project we just created earlier.
Hit F5 or Cmd+Enter to run the app on your selected emulator or the Xamarin Android Player. You should see a lovely blue screen. Now that our solution is properly configured, it’s time to get started writing our game!
Anatomy of a Game
Many different components work together to create a game. The Game1 class contains the main logic for our game, and is made up five main methods. Each serves its own purpose in making sure a game functions properly, from displaying art and playing sound effects to responding to user input and executing game logic. Game1 is made up of five main methods:
- Constructor
- Initialize
- LoadContent
- Update
- Draw
The constructor and Initialize methods are used to perform any initializations the game needs before starting to run. LoadContent is for loading up any game content, such as textures, sounds, shaders, and other graphical or audio components. Update is used to update any game logic you have while your game executes (gathering user input or updating the world), while Draw should exclusively be used to draw any graphics that need displaying.
Adding Game Assets
No game is complete without textures and sound effects, known in game development as “content” or “assets”. Most gaming frameworks have a content pipeline, which is just used to take raw assets and turn them into an optimized format for your game. The Content.mgcb file in the Content folder is MonoGame’s content pipeline. Anything added to this file will be optimized and included in your final application package.
All game assets should be shared between any target platforms. Drag the Content directory from the Android project to the shared project. Ensure the Build Action of the Content.mgcb file is set to MonoGameContentReference. If this build action does not appear in the drop down, right-click the Content.mcgb file, select Properties, and manually enter the text to MonoGameContentReference. Download the MonkeyTap assets and extract them into the Content folder in the Shared Project.
MonoGame has a special Pipeline Editor to make it super easy to work with game assets. Double-click the Content.mgcb file to open the Pipeline Editor. Next, add the asset files you just downloaded as seen below:
Add textures and sounds to your game with the MonoGame Pipeline Editor.
Now that our game content is optimized for use in our application, we can use the assets in our game.
Creating MonkeyTap User Interface
Add the following to the using section of Game1.cs:
using System.Collections.Generic;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
Now in Game1.cs add the following below the SpriteBatch spriteBatch; line in the variable declarations:
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D monkey;
Texture2D background;
Texture2D logo;
SpriteFont font;
SoundEffect hit;
Song title;
Next, we need to load our assets. Add the following code to your LoadContent method, which is where all assets should be loaded in MonoGame:
monkey = Content.Load<Texture2D> ("monkey");
background = Content.Load<Texture2D> ("background");
logo = Content.Load<Texture2D> ("logo");
font = Content.Load<SpriteFont> ("font");
hit = Content.Load<SoundEffect> ("hit");
title = Content.Load<Song> ("title");
MediaPlayer.IsRepeating = true;
MediaPlayer.Play (title);
All of our content is now loaded, from textures to audio. Now that we have loaded our content, it’s time to draw the user interface on the screen. The SpriteBatch class is used to draw 2D images and text. To make rendering as efficient as possible, drawing is batched together and sprites must be drawn between the Begin and End methods of SpriteBatch. Update the Draw method to draw our monkey texture on the screen using the spriteBatch field we just created:
protected override void Draw (GameTime gameTime)
{
graphics.GraphicsDevice.Clear (Color.CornflowerBlue);
spriteBatch.Begin ();
spriteBatch.Draw (monkey, Vector2.Zero);
spriteBatch.End ();
base.Draw (gameTime);
}
Hit F5 or Cmd+Enter to run the application. You should see the monkey smiling with his banana and hear the music from Jason Farmer we set to play in LoadContent. Now that our game assets are loading, let’s build the rest of the user interface for playing MonkeyTap!
Building the MonkeyTap User Interface
Traditional Whack-a-Mole style games have moles that randomly appear on screen and must be tapped to disappear. Rather than randomly rendering monkeys on the screen, we can use a grid to help ensure that the monkeys don’t overlap to provide a consistent user experience. The grid is made up of multiple cells, each of which contains a rectangle, color, countdown timer, and transition value that will be used to fade the monkey in.
Click on the Project tab, and select Add Class, give your new class the name GridCell. Copy the using section from Game1.cs and add to the using section of your new class:
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
Copy and paste the following GridCell class into the your new class file:
public class GridCell
{
public Rectangle DisplayRectangle;
public Color Color;
public TimeSpan CountDown;
public float Transition;
public GridCell ()
{
Reset ();
}
public bool Update(GameTime gameTime)
{
if (Color == Color.White) {
Transition += (float)gameTime.ElapsedGameTime.TotalMilliseconds / 100f;
CountDown -= gameTime.ElapsedGameTime;
if (CountDown.TotalMilliseconds <= 0) {
return true;
}
}
return false;
}
public void Reset ()
{
Color = Color.TransparentBlack;
CountDown = TimeSpan.FromSeconds (5);
Transition = 0f;
}
public void Show ()
{
Color = Color.White;
CountDown = TimeSpan.FromSeconds (5);
}
}
The Reset method is used to reset the cell back to its default state where the monkey is hidden. This is called when the user taps a monkey. Show is used to set the timer to five seconds when the monkey appears onscreen. The Update method is called each frame to update the countdown timer, and helps us to figure out if a user has not tapped the monkey within the given five second timeframe.
Using the new GridCell class
Now that cells are defined, let’s define the grid. This should go in the variable declaration section of Game1.cs, create a new List field called grid:
List<GridCell> grid = new List<GridCell>();
Add the following code to the LoadContent method of Game1.cs to calculate the display rectangles for the cell:
var viewport = graphics.GraphicsDevice.Viewport;
var padding = (viewport.Width / 100);
var gridWidth = (viewport.Width - (padding * 5)) / 4;
var gridHeight = gridWidth;
for (int y = padding; y < gridHeight*5; y+=gridHeight+padding) {
for (int x = padding; x < viewport.Width-gridWidth; x+=gridWidth+padding) {
grid.Add (new GridCell () {
DisplayRectangle = new Rectangle (x, y, gridWidth, gridHeight)
});
}
}
If you want your game to look good on all form factors, you need to take screen size into account, rather than hardcode positional values. GraphicsDevice.ViewPort provides us with a dynamic way to work with different form factors. In the code above, we add 10% of the screen width as padding to between the cells and calculate a width and height for the grid using the same property. We then loop through the (x,y) coordinates for each row and column and calculate the display rectangle.
Replace the Draw method of Game1.cs with the code to draw our monkeys:
protected override void Draw (GameTime gameTime)
{
graphics.GraphicsDevice.Clear (Color.SaddleBrown);
spriteBatch.Begin ();
foreach (var square in grid)
spriteBatch.Draw (monkey, destinationRectangle: square.DisplayRectangle, color: Color.White);
spriteBatch.End ();
base.Draw (gameTime);
}
Finally, we want our game to run only in Portrait, so add the following code to the constructor of Game1.cs:
graphics.SupportedOrientations = DisplayOrientation.Portrait;
Run the app, and you should see a grid full of monkeys! If the window is landscape the line just added to the constructor doesn't work, so instead enter the code below into the constructor:
//graphics.SupportedOrientations = DisplayOrientation.Portrait;
graphics.PreferredBackBufferWidth = 400;
graphics.PreferredBackBufferHeight = 700;
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. 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 Game1.cs variable declaration section with the following values:
enum GameState
{
Start,
Playing,
GameOver
}
Copy and paste the following below the enum you have just created in 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.