Difference between revisions of "Dino"

From TRCCompSci - AQA Computer Science
Jump to: navigation, search
(Add class variables)
 
(17 intermediate revisions by the same user not shown)
Line 1: Line 1:
 +
=Original Tutorial=
 +
Based on this original tutorial:
 +
 +
[https://docs.microsoft.com/en-us/windows/uwp/get-started/get-started-tutorial-game-mg2d Microsoft 2d MonoGame tutorial]
 +
 
=Set up MonoGame project=
 
=Set up MonoGame project=
  
Line 6: Line 11:
 
Now you’ve created the project, open the Game1.cs file from the Solution Explorer. This is where the bulk of the game logic is going to go. Many crucial methods are automatically generated here when you create a new MonoGame project. Let’s quickly review them:
 
Now you’ve created the project, open the Game1.cs file from the Solution Explorer. This is where the bulk of the game logic is going to go. Many crucial methods are automatically generated here when you create a new MonoGame project. Let’s quickly review them:
  
*public Game1() The constructor. We aren’t going to change this method at all for this tutorial.
+
*'''public Game1()''' The constructor. We aren’t going to change this method at all for this tutorial.
*protected override void Initialize() Here we initialize any class variables that are used. This method is called once at the start of the game.
+
*'''Initialize()''' Here we initialize any class variables that are used. This method is called once at the start of the game.
*protected override void LoadContent() This method loads content (eg. textures, audio, fonts) into memory before the game starts. Like Initialize, it’s called once when the app starts.
+
*'''LoadContent()''' This method loads content (eg. textures, audio, fonts) into memory before the game starts. Like Initialize, it’s called once when the app starts.
*protected override void UnloadContent() This method is used to unload non content-manager content. We don’t use this one at all.
+
*'''UnloadContent()''' This method is used to unload non content-manager content. We don’t use this one at all.
*protected override void Update(GameTime gameTIme) This method is called once for every cycle of the game loop. Here we update the states of any object or variable used in the game. This includes things like an object’s position, speed, or color. This is also where use input is handled. In short, this method handles every part of the game logic except drawing objects on screen. protected override void Draw(GameTime gameTime) This is where objects are drawn on the screen, using the positions given by the Update method.
+
*'''Update(GameTime gameTIme)''' This method is called once for every cycle of the game loop. Here we update the states of any object or variable used in the game. This includes things like an object’s position, speed, or color. This is also where use input is handled. In short, this method handles every part of the game logic except drawing objects on screen.  
 +
*'''Draw(GameTime gameTime)''' This is where objects are drawn on the screen, using the positions given by the Update method.
  
 
=Draw a sprite=
 
=Draw a sprite=
 
So you’ve run your fresh MonoGame project and found a nice blue sky—let’s add some ground. In MonoGame, 2D art is added to the app in the form of “sprites.” A sprite is just a computer graphic that is manipulated as a single entity. Sprites can be moved, scaled, shaped, animated, and combined to create anything you can imagine in the 2D space.
 
So you’ve run your fresh MonoGame project and found a nice blue sky—let’s add some ground. In MonoGame, 2D art is added to the app in the form of “sprites.” A sprite is just a computer graphic that is manipulated as a single entity. Sprites can be moved, scaled, shaped, animated, and combined to create anything you can imagine in the 2D space.
1. Download a texture
 
  
For our purposes, this first sprite is going to be extremely boring. Click here to download this featureless green rectangle.
+
===Download a texture===
2. Add the texture to the Content folder
+
For our purposes, this first sprite is going to be extremely boring. [https://drive.google.com/file/d/0Bw-0YEA_JX9gSWt2Vnd1SFYtVTQ/view?usp=sharing Click here to download] this featureless green rectangle.
  
Open the Solution Explorer
+
===Add the texture to the Content folder===
Right click Content.mgcb in the Content folder and select Open With. From the popup menu select Monogame Pipeline, and select OK.
+
 
In the new window, Right-Click the Content item and select Add -> Existing Item.
+
Open the Solution Explorer, Right click Content.mgcb in the Content folder and select Open With. From the popup menu select Monogame Pipeline, and select OK. In the new window, Right-Click the Content item and select Add -> Existing Item. Locate and select the green rectangle in the file browser
Locate and select the green rectangle in the file browser
 
 
Name the item “grass.png” and select Add.
 
Name the item “grass.png” and select Add.
3. Add class variables
+
 
 +
===Add class variables===
  
 
To load this image as a sprite texture, open Game1.cs and add the following class variables.
 
To load this image as a sprite texture, open Game1.cs and add the following class variables.
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
const float SKYRATIO = 2f/3f;
 
const float SKYRATIO = 2f/3f;
 
float screenWidth;
 
float screenWidth;
 
float screenHeight;
 
float screenHeight;
 
Texture2D grass;
 
Texture2D grass;
 +
</syntaxhighlight>
 +
 
The SKYRATIO variable tells us how much of the scene we want to be sky versus grass—in this case, two-thirds. screenWidth and screenHeight will keep track of the app window size, while grass is where we’ll store our green rectangle.
 
The SKYRATIO variable tells us how much of the scene we want to be sky versus grass—in this case, two-thirds. screenWidth and screenHeight will keep track of the app window size, while grass is where we’ll store our green rectangle.
4. Initialize class variables and set window size
+
 
 +
===Initialize class variables and set window size===
  
 
The screenWidth and screenHeight variables still need to be initialized, so add this code to the Initialize method:
 
The screenWidth and screenHeight variables still need to be initialized, so add this code to the Initialize method:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.FullScreen;
+
graphics.IsFullScreen = true;
  
screenHeight = (float)ApplicationView.GetForCurrentView().VisibleBounds.Height;
+
screenHeight = (float)graphics.PreferredBufferHeight;
screenWidth = (float)ApplicationView.GetForCurrentView().VisibleBounds.Width;
+
screenWidth = (float)graphics.PreferredBufferWidth;
  
 
this.IsMouseVisible = false;
 
this.IsMouseVisible = false;
 +
</syntaxhighlight>
 +
 
Along with getting the screen’s height and width, we also set the app’s windowing mode to Fullscreen, and make the mouse invisible.
 
Along with getting the screen’s height and width, we also set the app’s windowing mode to Fullscreen, and make the mouse invisible.
5. Load the texture
+
 
 +
===Load the texture===
  
 
To load the texture into the grass variable, add the following to the LoadContent method:
 
To load the texture into the grass variable, add the following to the LoadContent method:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
grass = Content.Load<Texture2D>("grass");
 
grass = Content.Load<Texture2D>("grass");
6. Draw the sprite
+
</syntaxhighlight>
 +
 
 +
===Draw the sprite===
  
 
To draw the rectangle, add the following lines to the Draw method:
 
To draw the rectangle, add the following lines to the Draw method:
CSharp
+
<syntaxhighlight lang=csharp>
 
 
Copy
 
 
GraphicsDevice.Clear(Color.CornflowerBlue);
 
GraphicsDevice.Clear(Color.CornflowerBlue);
 
spriteBatch.Begin();
 
spriteBatch.Begin();
Line 66: Line 74:
 
   (int)screenWidth, (int)screenHeight), Color.White);
 
   (int)screenWidth, (int)screenHeight), Color.White);
 
spriteBatch.End();
 
spriteBatch.End();
 +
</syntaxhighlight>
 +
 
Here we use the spriteBatch.Draw method to place the given texture within the borders of a Rectangle object. A Rectangle is defined by the x and y coordinates of its top left and bottom right corner. Using the screenWidth, screenHeight, and SKYRATIO variables we defined earlier, we draw the green rectangle texture across the bottom one-third of the screen. If you run the program now you should see the blue background from before, partially covered by the green rectangle.
 
Here we use the spriteBatch.Draw method to place the given texture within the borders of a Rectangle object. A Rectangle is defined by the x and y coordinates of its top left and bottom right corner. Using the screenWidth, screenHeight, and SKYRATIO variables we defined earlier, we draw the green rectangle texture across the bottom one-third of the screen. If you run the program now you should see the blue background from before, partially covered by the green rectangle.
Green rectangle
+
 
Scale to high DPI screens
+
=Scale to high DPI screens=
 
If you’re running Visual Studio on a high pixel-density monitor, like those found on a Surface Pro or Surface Studio, you may find that the green rectangle from the steps above doesn’t quite cover the bottom third of the screen. It’s probably floating above the bottom-left corner of the screen. To fix this and unify the experience of our game across all devices, we will need to create a method that scales certain values relative to the screen’s pixel density:
 
If you’re running Visual Studio on a high pixel-density monitor, like those found on a Surface Pro or Surface Studio, you may find that the green rectangle from the steps above doesn’t quite cover the bottom third of the screen. It’s probably floating above the bottom-left corner of the screen. To fix this and unify the experience of our game across all devices, we will need to create a method that scales certain values relative to the screen’s pixel density:
CSharp
+
<syntaxhighlight lang=csharp>
 
 
Copy
 
 
public float ScaleToHighDPI(float f)
 
public float ScaleToHighDPI(float f)
 
{
 
{
   DisplayInformation d = DisplayInformation.GetForCurrentView();
+
   //Lines from original tutorial code need reference i can't find
   f *= (float)d.RawPixelsPerViewPixel;
+
  //Used static value of 0.5 instead
 +
  //DisplayInformation d = DisplayInformation.GetForCurrentView();
 +
   //f *= (float)d.RawPixelsPerViewPixel;
 +
  f *= (float)0.5;
 
   return f;
 
   return f;
 
}
 
}
Next replace the initializations of screenHeight and screenWidth in the Initialize method with this:
+
</syntaxhighlight>
CSharp
+
 
 +
=Build the SpriteClass=
  
Copy
 
screenHeight = ScaleToHighDPI((float)ApplicationView.GetForCurrentView().VisibleBounds.Height);
 
screenWidth = ScaleToHighDPI((float)ApplicationView.GetForCurrentView().VisibleBounds.Width);
 
If you’re using a high DPI screen and try to run the app now, you should see the green rectangle covering the bottom third of the screen as intended.
 
Build the SpriteClass
 
 
Before we start animating sprites, we’re going to make a new class called “SpriteClass,” which will let us reduce the surface-level complexity of sprite manipulation.
 
Before we start animating sprites, we’re going to make a new class called “SpriteClass,” which will let us reduce the surface-level complexity of sprite manipulation.
1. Create a new class
 
  
In the Solution Explorer, right-click MonoGame2D (Universal Windows) and select Add -> Class. Name the class “SpriteClass.cs” then select Add.
+
===Create a new class===
2. Add class variables
+
 
 +
In the Solution Explorer, right-click MonoGame2D and select Add -> Class. Name the class “SpriteClass.cs” then select Add. In the using section add the following references:
 +
 
 +
<syntaxhighlight lang=csharp>
 +
using System;
 +
using Microsoft.Xna.Framework;
 +
using Microsoft.Xna.Framework.Graphics;
 +
using Microsoft.Xna.Framework.Input;
 +
</syntaxhighlight>
 +
 
 +
===Add class variables===
  
 
Add this code to the class you just created:
 
Add this code to the class you just created:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
public Texture2D texture
 
public Texture2D texture
 
{
 
{
 
   get;
 
   get;
 +
  set;
 
}
 
}
  
Line 143: Line 159:
 
   set;
 
   set;
 
}
 
}
 +
</syntaxhighlight>
 +
 
Here we set up the class variables we need to draw and animate a sprite. The x and y variables represent the sprite’s current position on the plane, while the angle variable is the sprite’s current angle in degrees (0 being upright, 90 being tilted 90 degrees clockwise). It’s important to note that, for this class, x and y represent the coordinates of the center of the sprite, (the default origin is the top-left corner). This is makes rotating sprites easier, as they will rotate around whatever origin they are given, and rotating around the center gives us a uniform spinning motion.
 
Here we set up the class variables we need to draw and animate a sprite. The x and y variables represent the sprite’s current position on the plane, while the angle variable is the sprite’s current angle in degrees (0 being upright, 90 being tilted 90 degrees clockwise). It’s important to note that, for this class, x and y represent the coordinates of the center of the sprite, (the default origin is the top-left corner). This is makes rotating sprites easier, as they will rotate around whatever origin they are given, and rotating around the center gives us a uniform spinning motion.
 
After this, we have dX, dY, and dA, which are the per-second rates of change for the x, y, and angle variables respectively.
 
After this, we have dX, dY, and dA, which are the per-second rates of change for the x, y, and angle variables respectively.
3. Create a constructor
+
 
 +
===Create a constructor===
  
 
When creating an instance of SpriteClass, we provide the constructor with the graphics device from Game1.cs, the path to the texture relative to the project folder, and the desired scale of the texture relative to its original size. We’ll set the rest of the class variables after we start the game, in the update method.
 
When creating an instance of SpriteClass, we provide the constructor with the graphics device from Game1.cs, the path to the texture relative to the project folder, and the desired scale of the texture relative to its original size. We’ll set the rest of the class variables after we start the game, in the update method.
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
public SpriteClass (GraphicsDevice graphicsDevice, string textureName, float scale)
 
public SpriteClass (GraphicsDevice graphicsDevice, string textureName, float scale)
 
{
 
{
Line 162: Line 180:
 
   }
 
   }
 
}
 
}
4. Update and Draw
+
</syntaxhighlight>
 +
 
 +
===Update and Draw===
  
 
There are still a couple of methods we need to add to the SpriteClass declaration:
 
There are still a couple of methods we need to add to the SpriteClass declaration:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
public void Update (float elapsedTime)
 
public void Update (float elapsedTime)
 
{
 
{
Line 180: Line 199:
 
   spriteBatch.Draw(texture, spritePosition, null, Color.White, this.angle, new Vector2(texture.Width/2, texture.Height/2), new Vector2(scale, scale), SpriteEffects.None, 0f);
 
   spriteBatch.Draw(texture, spritePosition, null, Color.White, this.angle, new Vector2(texture.Width/2, texture.Height/2), new Vector2(scale, scale), SpriteEffects.None, 0f);
 
}
 
}
The Update SpriteClass method is called in the Update method of Game1.cs, and is used to update the sprites x, y, and angle values based on their respective rates of change.
+
</syntaxhighlight>
The Draw method is called in the Draw method of Game1.cs, and is used to draw the sprite in the game window.
+
 
User input and animation
+
The Update SpriteClass method is called in the Update method of Game1.cs, and is used to update the sprites x, y, and angle values based on their respective rates of change. The Draw method is called in the Draw method of Game1.cs, and is used to draw the sprite in the game window.
Now we have the SpriteClass built, we’ll use it to create two new game objects, The first is an avatar that the player can control with the arrow keys and the space bar. The second is an object that the player must avoid
+
 
1. Get the textures
+
=User input and animation=
 +
Now we have the SpriteClass built, we’ll use it to create two new game objects, The first is an avatar that the player can control with the arrow keys and the space bar. The second is an object that the player must avoid.
 +
 
 +
===Get the textures===
 +
 
 +
For the player’s avatar we’re going to use Microsoft’s very own ninja cat, riding on his trusty t-rex. [https://drive.google.com/file/d/0Bw-0YEA_JX9galZSM3F3X0otb2M/view?usp=sharing Click here to download the image].
 +
 
 +
Now for the obstacle that the player needs to avoid. What do ninja-cats and carnivorous dinosaurs both hate more than anything? Eating their veggies! [https://drive.google.com/file/d/0Bw-0YEA_JX9gTUdlcTVSZG1qQ28/view?usp=sharing Click here to download the image].
  
For the player’s avatar we’re going to use Microsoft’s very own ninja cat, riding on his trusty t-rex. Click here to download the image.
 
Now for the obstacle that the player needs to avoid. What do ninja-cats and carnivorous dinosaurs both hate more than anything? Eating their veggies! Click here to download the image.
 
 
Just as before with the green rectangle, add these images to Content.mgcb via the MonoGame Pipeline, naming them “ninja-cat-dino.png” and “broccoli.png” respectively.
 
Just as before with the green rectangle, add these images to Content.mgcb via the MonoGame Pipeline, naming them “ninja-cat-dino.png” and “broccoli.png” respectively.
2. Add class variables
+
 
 +
===Add class variables===
  
 
Add the following code to the list of class variables in Game1.cs:
 
Add the following code to the list of class variables in Game1.cs:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
SpriteClass dino;
 
SpriteClass dino;
 
SpriteClass broccoli;
 
SpriteClass broccoli;
Line 208: Line 232:
  
 
Random random;
 
Random random;
 +
</syntaxhighlight>
 +
 
dino and broccoli are our SpriteClass variables. dino will hold the player avatar, while broccoli holds the broccoli obstacle.
 
dino and broccoli are our SpriteClass variables. dino will hold the player avatar, while broccoli holds the broccoli obstacle.
 
spaceDown keeps track of whether the spacebar is being held down as opposed to pressed and released.
 
spaceDown keeps track of whether the spacebar is being held down as opposed to pressed and released.
Line 215: Line 241:
 
dinoSpeedX and dinoJumpY determine how fast the player avatar moves and jumps. score tracks how many obstacles the player has successfully dodged.
 
dinoSpeedX and dinoJumpY determine how fast the player avatar moves and jumps. score tracks how many obstacles the player has successfully dodged.
 
Finally, random will be used to add some randomness to the behavior of the broccoli obstacle.
 
Finally, random will be used to add some randomness to the behavior of the broccoli obstacle.
3. Initialize variables
+
 
 +
===Initialize variables===
  
 
Next we need to initialize these variables. Add the following code to the Initialize method:
 
Next we need to initialize these variables. Add the following code to the Initialize method:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
broccoliSpeedMultiplier = 0.5f;
 
broccoliSpeedMultiplier = 0.5f;
 
spaceDown = false;
 
spaceDown = false;
Line 229: Line 255:
 
dinoJumpY = ScaleToHighDPI(-1200f);
 
dinoJumpY = ScaleToHighDPI(-1200f);
 
gravitySpeed = ScaleToHighDPI(30f);
 
gravitySpeed = ScaleToHighDPI(30f);
 +
</syntaxhighlight>
 +
 
Note that the last three variables need to be scaled for high DPI devices, because they specify a rate of change in pixels.
 
Note that the last three variables need to be scaled for high DPI devices, because they specify a rate of change in pixels.
4. Construct SpriteClasses
+
 
 +
===Construct SpriteClasses===
  
 
We will construct SpriteClass objects in the LoadContent method. Add this code to what you already have there:
 
We will construct SpriteClass objects in the LoadContent method. Add this code to what you already have there:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
dino = new SpriteClass(GraphicsDevice, "Content/ninja-cat-dino.png", ScaleToHighDPI(1f));
 
dino = new SpriteClass(GraphicsDevice, "Content/ninja-cat-dino.png", ScaleToHighDPI(1f));
 
broccoli = new SpriteClass(GraphicsDevice, "Content/broccoli.png", ScaleToHighDPI(0.2f));
 
broccoli = new SpriteClass(GraphicsDevice, "Content/broccoli.png", ScaleToHighDPI(0.2f));
 +
</syntaxhighlight>
 +
 
The broccoli image is quite a lot larger than we want it to appear in the game, so we’ll scale it down to 0.2 times its original size.
 
The broccoli image is quite a lot larger than we want it to appear in the game, so we’ll scale it down to 0.2 times its original size.
5. Program obstacle behavior
+
 
 +
===Program obstacle behaviour===
  
 
We want the broccoli to spawn somewhere offscreen, and head in the direction of the player’s avatar, so they need to dodge it. To accomplish they, add this method to the Game1.cs class:
 
We want the broccoli to spawn somewhere offscreen, and head in the direction of the player’s avatar, so they need to dodge it. To accomplish they, add this method to the Game1.cs class:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
public void SpawnBroccoli()
 
public void SpawnBroccoli()
 
{
 
{
Line 274: Line 304:
 
   broccoli.dA = 7f;
 
   broccoli.dA = 7f;
 
}
 
}
 +
</syntaxhighlight>
 +
 
The first part of the of the method determines what off screen point the broccoli object will spawn from, using two random numbers.
 
The first part of the of the method determines what off screen point the broccoli object will spawn from, using two random numbers.
 
The second part determines how fast the broccoli will travel, which is determined by the current score. It will get faster for every five broccoli the player successfully dodges.
 
The second part determines how fast the broccoli will travel, which is determined by the current score. It will get faster for every five broccoli the player successfully dodges.
 +
 
The third part sets the direction of the broccoli sprite’s motion. It heads in the direction of the player avatar (dino) when the broccoli is spawned. We also give it a dA value of 7f, which will cause the broccoli to spin through the air as it chases the player.
 
The third part sets the direction of the broccoli sprite’s motion. It heads in the direction of the player avatar (dino) when the broccoli is spawned. We also give it a dA value of 7f, which will cause the broccoli to spin through the air as it chases the player.
6. Program game starting state
+
 
 +
===Program game starting state===
  
 
Before we can move on to handling keyboard input, we need a method that sets the initial game state of the two objects we’ve created. Rather than the game starting as soon as the app runs, we want the user to start it manually, by pressing the spacebar. Add the following code, which sets the initial state of the animated objects, and resets the score:
 
Before we can move on to handling keyboard input, we need a method that sets the initial game state of the two objects we’ve created. Rather than the game starting as soon as the app runs, we want the user to start it manually, by pressing the spacebar. Add the following code, which sets the initial state of the animated objects, and resets the score:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
public void StartGame()
 
public void StartGame()
 
{
 
{
Line 291: Line 324:
 
   score = 0;
 
   score = 0;
 
}
 
}
7. Handle keyboard input
+
</syntaxhighlight>
 +
 
 +
===Handle keyboard input===
  
 
Next we need a new method to handle user input via the keyboard. Add this this method to Game1.cs:
 
Next we need a new method to handle user input via the keyboard. Add this this method to Game1.cs:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
void KeyboardHandler()
 
void KeyboardHandler()
 
{
 
{
Line 335: Line 369:
 
   else dino.dX = 0;
 
   else dino.dX = 0;
 
}
 
}
 +
</syntaxhighlight>
 +
 
Above we have a series of four if-statements:
 
Above we have a series of four if-statements:
The first quits the game if the Escape key is pressed.
+
*The first quits the game if the Escape key is pressed.
The second starts the game if the Space key is pressed, and the game is not already started.
+
*The second starts the game if the Space key is pressed, and the game is not already started.
The third makes the dino avatar jump if Space is pressed, by changing its dY property. Note that the player cannot jump unless they are on the “ground” (dino.y = screenHeight * SKYRATIO), and will also not jump if the space key is being help down rather than pressed once. This stops the dino from jumping as soon as the game is started, piggybacking on the same keypress that starts the game.
+
*The third makes the dino avatar jump if Space is pressed, by changing its dY property. Note that the player cannot jump unless they are on the “ground” (dino.y = screenHeight * SKYRATIO), and will also not jump if the space key is being help down rather than pressed once. This stops the dino from jumping as soon as the game is started, piggybacking on the same keypress that starts the game.
Finally, the last if/else clause checks if the left or right directional arrows are being pressed, and if so changes the dino’s dX property accordingly.
+
*Finally, the last if/else clause checks if the left or right directional arrows are being pressed, and if so changes the dino’s dX property accordingly.
 +
 
 
Challenge: can you make the keyboard handling method above work with the WASD input scheme as well as the arrow keys?
 
Challenge: can you make the keyboard handling method above work with the WASD input scheme as well as the arrow keys?
8. Add logic to the Update method
+
 
 +
===Add logic to the Update method===
  
 
Next we need to add logic for all of these parts to the Update method in Game1.cs:
 
Next we need to add logic for all of these parts to the Update method in Game1.cs:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
 
float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
 
KeyboardHandler(); // Handle keyboard input
 
KeyboardHandler(); // Handle keyboard input
Line 381: Line 418:
 
   score++;
 
   score++;
 
}
 
}
9. Draw SpriteClass objects
+
</syntaxhighlight>
 +
 
 +
===Draw SpriteClass objects===
  
 
Finally, add the following code to the Draw method of Game1.cs, just after the last call of spriteBatch.Draw:
 
Finally, add the following code to the Draw method of Game1.cs, just after the last call of spriteBatch.Draw:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
broccoli.Draw(spriteBatch);
 
broccoli.Draw(spriteBatch);
 
dino.Draw(spriteBatch);
 
dino.Draw(spriteBatch);
 +
</syntaxhighlight>
 +
 
In MonoGame, new calls of spriteBatch.Draw will draw over any prior calls. This means that both the broccoli and the dino sprite will be drawn over the existing grass sprite, so they can never be hidden behind it regardless of their position.
 
In MonoGame, new calls of spriteBatch.Draw will draw over any prior calls. This means that both the broccoli and the dino sprite will be drawn over the existing grass sprite, so they can never be hidden behind it regardless of their position.
 
Try running the game now, and moving around the dino with the arrow keys and the spacebar. If you followed the steps above, you should be able to make your avatar move within the game window, and the broccoli should at an ever-increasing speed.
 
Try running the game now, and moving around the dino with the arrow keys and the spacebar. If you followed the steps above, you should be able to make your avatar move within the game window, and the broccoli should at an ever-increasing speed.
Player avatar and obstacle
+
 
Render text with SpriteFont
+
=Render text with SpriteFont=
 
Using the code above, we keep track of the player’s score behind the scenes, but we don’t actually tell the player what it is. We also have a fairly unintuitive introduction when the app starts up—the player sees a blue and green window, but has no way of knowing they need to press Space to get things rolling.
 
Using the code above, we keep track of the player’s score behind the scenes, but we don’t actually tell the player what it is. We also have a fairly unintuitive introduction when the app starts up—the player sees a blue and green window, but has no way of knowing they need to press Space to get things rolling.
 +
 
To fix both these problems, we’re going to use a new kind of MonoGame object called SpriteFonts.
 
To fix both these problems, we’re going to use a new kind of MonoGame object called SpriteFonts.
1. Create SpriteFont description files
+
 
 +
===Create SpriteFont description files===
  
 
In the Solution Explorer find the Content folder. In this folder, Right-Click the Content.mgcb file and select Open With. From the popup menu select MonoGame Pipeline, then press OK. In the new window, Right-Click the Content item and select Add -> New Item. Select SpriteFont Description, name it “Score” and press OK. Then, add another SpriteFont description named “GameState” using the same procedure.
 
In the Solution Explorer find the Content folder. In this folder, Right-Click the Content.mgcb file and select Open With. From the popup menu select MonoGame Pipeline, then press OK. In the new window, Right-Click the Content item and select Add -> New Item. Select SpriteFont Description, name it “Score” and press OK. Then, add another SpriteFont description named “GameState” using the same procedure.
2. Edit descriptions
+
 
 +
===Edit descriptions===
  
 
Right click the Content folder in the MonoGame Pipeline and select Open File Location. You should see a folder with the SpriteFont description files that you just created, as well as any images you’ve added to the Content folder so far. You can now close and save the MonoGame Pipeline window. From the File Explorer open both description files in a text editor (Visual Studio, NotePad++, Atom, etc).
 
Right click the Content folder in the MonoGame Pipeline and select Open File Location. You should see a folder with the SpriteFont description files that you just created, as well as any images you’ve added to the Content folder so far. You can now close and save the MonoGame Pipeline window. From the File Explorer open both description files in a text editor (Visual Studio, NotePad++, Atom, etc).
 +
 
Each description contains a number of values that describe the SpriteFont. We're going to make a few changes:
 
Each description contains a number of values that describe the SpriteFont. We're going to make a few changes:
In Score.spritefont, change the value from 12 to 36.
+
*In Score.spritefont, change the value from 12 to 36.
In GameState.spritefont, change the value from 12 to 72, and the value from Arial to Agency. Agency is another font that comes standard with Windows 10 machines, and will add some flair to our intro screen.
+
*In GameState.spritefont, change the value from 12 to 72, and the value from Arial to Agency. Agency is another font that comes standard with Windows 10 machines, and will add some flair to our intro screen.
3. Load SpriteFonts
+
 
 +
===Load SpriteFonts===
 +
 
 +
Back in Visual Studio, we’re first going to add a new texture for the intro splash screen. [https://drive.google.com/file/d/0Bw-0YEA_JX9gSVJfMXRvdEtXMGc/view?usp=sharing Click here to download the image].
  
Back in Visual Studio, we’re first going to add a new texture for the intro splash screen. Click here to download the image.
 
 
As before, add the texture to the project by right-clicking the Content and selecting Add -> Existing Item. Name the new item “start-splash.png”.
 
As before, add the texture to the project by right-clicking the Content and selecting Add -> Existing Item. Name the new item “start-splash.png”.
 +
 
Next, add the following class variables to Game1.cs:
 
Next, add the following class variables to Game1.cs:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
Texture2D startGameSplash;
 
Texture2D startGameSplash;
 
SpriteFont scoreFont;
 
SpriteFont scoreFont;
 
SpriteFont stateFont;
 
SpriteFont stateFont;
 +
</syntaxhighlight>
 +
 
Then add these lines to the LoadContent method:
 
Then add these lines to the LoadContent method:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
startGameSplash = Content.Load<Texture2D>("start-splash");
 
startGameSplash = Content.Load<Texture2D>("start-splash");
 
scoreFont = Content.Load<SpriteFont>("Score");
 
scoreFont = Content.Load<SpriteFont>("Score");
 
stateFont = Content.Load<SpriteFont>("GameState");
 
stateFont = Content.Load<SpriteFont>("GameState");
4. Draw the score
+
</syntaxhighlight>
 +
 
 +
===Draw the score===
  
 
Go to the Draw method of Game1.cs and add the following code just before spriteBatch.End();
 
Go to the Draw method of Game1.cs and add the following code just before spriteBatch.End();
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
spriteBatch.DrawString(scoreFont, score.ToString(),
 
spriteBatch.DrawString(scoreFont, score.ToString(),
 
new Vector2(screenWidth - 100, 50), Color.Black);
 
new Vector2(screenWidth - 100, 50), Color.Black);
 +
</syntaxhighlight>
 +
 
The code above uses the sprite description we created (Arial Size 36) to draw the player’s current score near the top right corner of the screen.
 
The code above uses the sprite description we created (Arial Size 36) to draw the player’s current score near the top right corner of the screen.
5. Draw horizontally centered text
+
 
 +
===Draw horizontally centered text===
  
 
When making a game, you will often want to draw text that is centered, either horizontally or vertically. To horizontally center the introductory text, add this code to the Draw method just before spriteBatch.End();
 
When making a game, you will often want to draw text that is centered, either horizontally or vertically. To horizontally center the introductory text, add this code to the Draw method just before spriteBatch.End();
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
if (!gameStarted)
 
if (!gameStarted)
 
{
 
{
Line 458: Line 508:
 
   screenHeight / 2), Color.White);
 
   screenHeight / 2), Color.White);
 
   }
 
   }
 +
</syntaxhighlight>
 +
 
First we create two Strings, one for each line of text we want to draw. Next, we measure the width and height of each line when printed, using the SpriteFont.MeasureString(String) method. This gives us the size as a Vector2 object, with the X property containing its width, and Y its height.
 
First we create two Strings, one for each line of text we want to draw. Next, we measure the width and height of each line when printed, using the SpriteFont.MeasureString(String) method. This gives us the size as a Vector2 object, with the X property containing its width, and Y its height.
 +
 
Finally, we draw each line. To center the text horizontally, we make the X value of it’s position vector equal to screenWidth / 2 - textSize.X / 2
 
Finally, we draw each line. To center the text horizontally, we make the X value of it’s position vector equal to screenWidth / 2 - textSize.X / 2
 
Challenge: how would you change the procedure above to center the text vertically as well as horizontally?
 
Challenge: how would you change the procedure above to center the text vertically as well as horizontally?
 +
 
Try running the game. Do you see the intro splash screen? Does the score count up each time the broccoli respawns?
 
Try running the game. Do you see the intro splash screen? Does the score count up each time the broccoli respawns?
Intro splash
+
 
Collision detection
+
=Collision detection=
 
So we have a broccoli that follows you around, and we have a score that ticks up each time a new one spawns—but as it is there is no way to actually lose this game. We need a way to know if the dino and broccoli sprites collide, and if when they do, to declare the game over.
 
So we have a broccoli that follows you around, and we have a score that ticks up each time a new one spawns—but as it is there is no way to actually lose this game. We need a way to know if the dino and broccoli sprites collide, and if when they do, to declare the game over.
1. Rectangular collision
+
 
 +
===Rectangular collision===
  
 
When detecting collisions in a game, objects are often simplified to reduce the complexity of the math involved. We are going to treat both the player avatar and broccoli obstacle as rectangles for the purpose of detecting collision between them.
 
When detecting collisions in a game, objects are often simplified to reduce the complexity of the math involved. We are going to treat both the player avatar and broccoli obstacle as rectangles for the purpose of detecting collision between them.
 
Open SpriteClass.cs and add a new class variable:
 
Open SpriteClass.cs and add a new class variable:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
const float HITBOXSCALE = .5f;
 
const float HITBOXSCALE = .5f;
 +
</syntaxhighlight>
 +
 
This value will represent how “forgiving” the collision detection is for the player. With a value of .5f, the edges of the rectangle in which the dino can collide with the broccoli—often call the “hitbox”—will be half of the full size of the texture. This will result in few instances where the corners of the two textures collide, without any parts of the images actually appearing to touch. Feel free to tweak this value to your personal taste.
 
This value will represent how “forgiving” the collision detection is for the player. With a value of .5f, the edges of the rectangle in which the dino can collide with the broccoli—often call the “hitbox”—will be half of the full size of the texture. This will result in few instances where the corners of the two textures collide, without any parts of the images actually appearing to touch. Feel free to tweak this value to your personal taste.
 +
 
Next, add a new method to SpriteClass.cs:
 
Next, add a new method to SpriteClass.cs:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
public bool RectangleCollision(SpriteClass otherSprite)
 
public bool RectangleCollision(SpriteClass otherSprite)
 
{
 
{
Line 486: Line 542:
 
   return true;
 
   return true;
 
}
 
}
 +
</syntaxhighlight>
 +
 
This method detects if two rectangular objects have collided. The algorithm works by testing to see if there is a gap between any of the side sides of the rectangles. If there is any gap, there is no collision—if no gap exists, there must be a collision.
 
This method detects if two rectangular objects have collided. The algorithm works by testing to see if there is a gap between any of the side sides of the rectangles. If there is any gap, there is no collision—if no gap exists, there must be a collision.
2. Load new textures
+
 
 +
===Load new textures===
  
 
Then, open Game1.cs and add two new class variables, one to store the game over sprite texture, and a Boolean to track the game’s state:
 
Then, open Game1.cs and add two new class variables, one to store the game over sprite texture, and a Boolean to track the game’s state:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
Texture2D gameOverTexture;
 
Texture2D gameOverTexture;
 
bool gameOver;
 
bool gameOver;
 +
</syntaxhighlight>
 +
 
Then, initialize gameOver in the Initialize method:
 
Then, initialize gameOver in the Initialize method:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
gameOver = false;
 
gameOver = false;
 +
</syntaxhighlight>
 +
 
Finally, load the texture into gameOverTexture in the LoadContent method:
 
Finally, load the texture into gameOverTexture in the LoadContent method:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
gameOverTexture = Content.Load<Texture2D>("game-over");
 
gameOverTexture = Content.Load<Texture2D>("game-over");
3. Implement “game over” logic
+
</syntaxhighlight>
 +
 
 +
===Implement “game over” logic===
  
 
Add this code to the Update method, just after the KeyboardHandler method is called:
 
Add this code to the Update method, just after the KeyboardHandler method is called:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
if (gameOver)
 
if (gameOver)
 
{
 
{
Line 519: Line 580:
 
   broccoli.dA = 0;
 
   broccoli.dA = 0;
 
}
 
}
 +
</syntaxhighlight>
 +
 
This will cause all motion to stop when the game has ended, freezing the dino and broccoli sprites in their current positions.
 
This will cause all motion to stop when the game has ended, freezing the dino and broccoli sprites in their current positions.
 
Next, at the end of the Update method, just before base.Update(gameTime), add this line:
 
Next, at the end of the Update method, just before base.Update(gameTime), add this line:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
if (dino.RectangleCollision(broccoli)) gameOver = true;
 
if (dino.RectangleCollision(broccoli)) gameOver = true;
 +
</syntaxhighlight>
 +
 
This calls the RectangleCollision method we created in SpriteClass, and flags the game as over if it returns true.
 
This calls the RectangleCollision method we created in SpriteClass, and flags the game as over if it returns true.
4. Add user input for resetting the game
+
 
 +
===Add user input for resetting the game===
  
 
Add this code to the KeyboardHandler method, to allow the user to reset them game if they press Enter:
 
Add this code to the KeyboardHandler method, to allow the user to reset them game if they press Enter:
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
if (gameOver && state.IsKeyDown(Keys.Enter))
 
if (gameOver && state.IsKeyDown(Keys.Enter))
 
   StartGame();
 
   StartGame();
 
   gameOver = false;
 
   gameOver = false;
 
}
 
}
5. Draw game over splash and text
+
</syntaxhighlight>
 +
 
 +
===Draw game over splash and text===
  
 
Finally, add this code to the Draw method, just after the first call of spriteBatch.Draw (this should be the call that draws the grass texture).
 
Finally, add this code to the Draw method, just after the first call of spriteBatch.Draw (this should be the call that draws the grass texture).
CSharp
 
  
Copy
+
<syntaxhighlight lang=csharp>
 
if (gameOver)
 
if (gameOver)
 
{
 
{
Line 555: Line 620:
 
   spriteBatch.DrawString(stateFont, pressEnter, new Vector2(screenWidth / 2 - pressEnterSize.X / 2, screenHeight - 200), Color.White);
 
   spriteBatch.DrawString(stateFont, pressEnter, new Vector2(screenWidth / 2 - pressEnterSize.X / 2, screenHeight - 200), Color.White);
 
}
 
}
 +
</syntaxhighlight>
 +
 
Here we use the same method as before to draw text horizontally centered (reusing the font we used for the intro splash), as well as centering gameOverTexture in the top half of the window.
 
Here we use the same method as before to draw text horizontally centered (reusing the font we used for the intro splash), as well as centering gameOverTexture in the top half of the window.
 +
 
And we’re done! Try running the game again. If you followed the steps above, the game should now end when the dino collides with the broccoli, and the player should be prompted to restart the game by pressing the Enter key.
 
And we’re done! Try running the game again. If you followed the steps above, the game should now end when the dino collides with the broccoli, and the player should be prompted to restart the game by pressing the Enter key.
Game over
 

Latest revision as of 10:56, 27 October 2017

Original Tutorial

Based on this original tutorial:

Microsoft 2d MonoGame tutorial

Set up MonoGame project

Go to File -> New -> Project Under the Visual C# project templates, select MonoGame and MonoGame Windows Project, Name your project “MonoGame2D" and select OK. If you followed the steps above, you should see an empty blue window after the project finishes building.

Method overview

Now you’ve created the project, open the Game1.cs file from the Solution Explorer. This is where the bulk of the game logic is going to go. Many crucial methods are automatically generated here when you create a new MonoGame project. Let’s quickly review them:

  • public Game1() The constructor. We aren’t going to change this method at all for this tutorial.
  • Initialize() Here we initialize any class variables that are used. This method is called once at the start of the game.
  • LoadContent() This method loads content (eg. textures, audio, fonts) into memory before the game starts. Like Initialize, it’s called once when the app starts.
  • UnloadContent() This method is used to unload non content-manager content. We don’t use this one at all.
  • Update(GameTime gameTIme) This method is called once for every cycle of the game loop. Here we update the states of any object or variable used in the game. This includes things like an object’s position, speed, or color. This is also where use input is handled. In short, this method handles every part of the game logic except drawing objects on screen.
  • Draw(GameTime gameTime) This is where objects are drawn on the screen, using the positions given by the Update method.

Draw a sprite

So you’ve run your fresh MonoGame project and found a nice blue sky—let’s add some ground. In MonoGame, 2D art is added to the app in the form of “sprites.” A sprite is just a computer graphic that is manipulated as a single entity. Sprites can be moved, scaled, shaped, animated, and combined to create anything you can imagine in the 2D space.

Download a texture

For our purposes, this first sprite is going to be extremely boring. Click here to download this featureless green rectangle.

Add the texture to the Content folder

Open the Solution Explorer, Right click Content.mgcb in the Content folder and select Open With. From the popup menu select Monogame Pipeline, and select OK. In the new window, Right-Click the Content item and select Add -> Existing Item. Locate and select the green rectangle in the file browser Name the item “grass.png” and select Add.

Add class variables

To load this image as a sprite texture, open Game1.cs and add the following class variables.

const float SKYRATIO = 2f/3f;
float screenWidth;
float screenHeight;
Texture2D grass;

The SKYRATIO variable tells us how much of the scene we want to be sky versus grass—in this case, two-thirds. screenWidth and screenHeight will keep track of the app window size, while grass is where we’ll store our green rectangle.

Initialize class variables and set window size

The screenWidth and screenHeight variables still need to be initialized, so add this code to the Initialize method:

graphics.IsFullScreen = true;

screenHeight = (float)graphics.PreferredBufferHeight;
screenWidth = (float)graphics.PreferredBufferWidth;

this.IsMouseVisible = false;

Along with getting the screen’s height and width, we also set the app’s windowing mode to Fullscreen, and make the mouse invisible.

Load the texture

To load the texture into the grass variable, add the following to the LoadContent method:

grass = Content.Load<Texture2D>("grass");

Draw the sprite

To draw the rectangle, add the following lines to the Draw method:

GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(grass, new Rectangle(0, (int)(screenHeight * SKYRATIO),
  (int)screenWidth, (int)screenHeight), Color.White);
spriteBatch.End();

Here we use the spriteBatch.Draw method to place the given texture within the borders of a Rectangle object. A Rectangle is defined by the x and y coordinates of its top left and bottom right corner. Using the screenWidth, screenHeight, and SKYRATIO variables we defined earlier, we draw the green rectangle texture across the bottom one-third of the screen. If you run the program now you should see the blue background from before, partially covered by the green rectangle.

Scale to high DPI screens

If you’re running Visual Studio on a high pixel-density monitor, like those found on a Surface Pro or Surface Studio, you may find that the green rectangle from the steps above doesn’t quite cover the bottom third of the screen. It’s probably floating above the bottom-left corner of the screen. To fix this and unify the experience of our game across all devices, we will need to create a method that scales certain values relative to the screen’s pixel density:

public float ScaleToHighDPI(float f)
{
  //Lines from original tutorial code need reference i can't find
  //Used static value of 0.5 instead
  //DisplayInformation d = DisplayInformation.GetForCurrentView();
  //f *= (float)d.RawPixelsPerViewPixel;
  f *= (float)0.5;
  return f;
}

Build the SpriteClass

Before we start animating sprites, we’re going to make a new class called “SpriteClass,” which will let us reduce the surface-level complexity of sprite manipulation.

Create a new class

In the Solution Explorer, right-click MonoGame2D and select Add -> Class. Name the class “SpriteClass.cs” then select Add. In the using section add the following references:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

Add class variables

Add this code to the class you just created:

public Texture2D texture
{
  get;
  set;
}

public float x
{
  get;
  set;
}

public float y
{
  get;
  set;
}

public float angle
{
  get;
  set;
}

public float dX
{
  get;
  set;
}

public float dY
{
  get;
  set;
}

public float dA
{
  get;
  set;
}

public float scale
{
  get;
  set;
}

Here we set up the class variables we need to draw and animate a sprite. The x and y variables represent the sprite’s current position on the plane, while the angle variable is the sprite’s current angle in degrees (0 being upright, 90 being tilted 90 degrees clockwise). It’s important to note that, for this class, x and y represent the coordinates of the center of the sprite, (the default origin is the top-left corner). This is makes rotating sprites easier, as they will rotate around whatever origin they are given, and rotating around the center gives us a uniform spinning motion. After this, we have dX, dY, and dA, which are the per-second rates of change for the x, y, and angle variables respectively.

Create a constructor

When creating an instance of SpriteClass, we provide the constructor with the graphics device from Game1.cs, the path to the texture relative to the project folder, and the desired scale of the texture relative to its original size. We’ll set the rest of the class variables after we start the game, in the update method.

public SpriteClass (GraphicsDevice graphicsDevice, string textureName, float scale)
{
  this.scale = scale;
  if (texture == null)
  {
    using (var stream = TitleContainer.OpenStream(textureName))
    {
      texture = Texture2D.FromStream(graphicsDevice, stream);
    }
  }
}

Update and Draw

There are still a couple of methods we need to add to the SpriteClass declaration:

public void Update (float elapsedTime)
{
  this.x += this.dX * elapsedTime;
  this.y += this.dY * elapsedTime;
  this.angle += this.dA * elapsedTime;
}

public void Draw (SpriteBatch spriteBatch)
{
  Vector2 spritePosition = new Vector2(this.x, this.y);
  spriteBatch.Draw(texture, spritePosition, null, Color.White, this.angle, new Vector2(texture.Width/2, texture.Height/2), new Vector2(scale, scale), SpriteEffects.None, 0f);
}

The Update SpriteClass method is called in the Update method of Game1.cs, and is used to update the sprites x, y, and angle values based on their respective rates of change. The Draw method is called in the Draw method of Game1.cs, and is used to draw the sprite in the game window.

User input and animation

Now we have the SpriteClass built, we’ll use it to create two new game objects, The first is an avatar that the player can control with the arrow keys and the space bar. The second is an object that the player must avoid.

Get the textures

For the player’s avatar we’re going to use Microsoft’s very own ninja cat, riding on his trusty t-rex. Click here to download the image.

Now for the obstacle that the player needs to avoid. What do ninja-cats and carnivorous dinosaurs both hate more than anything? Eating their veggies! Click here to download the image.

Just as before with the green rectangle, add these images to Content.mgcb via the MonoGame Pipeline, naming them “ninja-cat-dino.png” and “broccoli.png” respectively.

Add class variables

Add the following code to the list of class variables in Game1.cs:

SpriteClass dino;
SpriteClass broccoli;

bool spaceDown;
bool gameStarted;

float broccoliSpeedMultiplier;
float gravitySpeed;
float dinoSpeedX;
float dinoJumpY;
float score;

Random random;

dino and broccoli are our SpriteClass variables. dino will hold the player avatar, while broccoli holds the broccoli obstacle. spaceDown keeps track of whether the spacebar is being held down as opposed to pressed and released. gameStarted tells us whether the user has started the game for the first time. broccoliSpeedMultiplier determines how fast the broccoli obstacle moves across the screen. gravitySpeed determines how fast the player avatar accelerates downward after a jump. dinoSpeedX and dinoJumpY determine how fast the player avatar moves and jumps. score tracks how many obstacles the player has successfully dodged. Finally, random will be used to add some randomness to the behavior of the broccoli obstacle.

Initialize variables

Next we need to initialize these variables. Add the following code to the Initialize method:

broccoliSpeedMultiplier = 0.5f;
spaceDown = false;
gameStarted = false;
score = 0;
random = new Random();
dinoSpeedX = ScaleToHighDPI(1000f);
dinoJumpY = ScaleToHighDPI(-1200f);
gravitySpeed = ScaleToHighDPI(30f);

Note that the last three variables need to be scaled for high DPI devices, because they specify a rate of change in pixels.

Construct SpriteClasses

We will construct SpriteClass objects in the LoadContent method. Add this code to what you already have there:

dino = new SpriteClass(GraphicsDevice, "Content/ninja-cat-dino.png", ScaleToHighDPI(1f));
broccoli = new SpriteClass(GraphicsDevice, "Content/broccoli.png", ScaleToHighDPI(0.2f));

The broccoli image is quite a lot larger than we want it to appear in the game, so we’ll scale it down to 0.2 times its original size.

Program obstacle behaviour

We want the broccoli to spawn somewhere offscreen, and head in the direction of the player’s avatar, so they need to dodge it. To accomplish they, add this method to the Game1.cs class:

public void SpawnBroccoli()
{
  int direction = random.Next(1, 5);
  switch (direction)
  {
    case 1:
      broccoli.x = -100;
      broccoli.y = random.Next(0, (int)screenHeight);
      break;
    case 2:
      broccoli.y = -100;
      broccoli.x = random.Next(0, (int)screenWidth);
      break;
    case 3:
      broccoli.x = screenWidth + 100;
      broccoli.y = random.Next(0, (int)screenHeight);
      break;
    case 4:
      broccoli.y = screenHeight + 100;
      broccoli.x = random.Next(0, (int)screenWidth);
      break;
  }

  if (score % 5 == 0) broccoliSpeedMultiplier += 0.2f;

  broccoli.dX = (dino.x - broccoli.x) * broccoliSpeedMultiplier;
  broccoli.dY = (dino.y - broccoli.y) * broccoliSpeedMultiplier;
  broccoli.dA = 7f;
}

The first part of the of the method determines what off screen point the broccoli object will spawn from, using two random numbers. The second part determines how fast the broccoli will travel, which is determined by the current score. It will get faster for every five broccoli the player successfully dodges.

The third part sets the direction of the broccoli sprite’s motion. It heads in the direction of the player avatar (dino) when the broccoli is spawned. We also give it a dA value of 7f, which will cause the broccoli to spin through the air as it chases the player.

Program game starting state

Before we can move on to handling keyboard input, we need a method that sets the initial game state of the two objects we’ve created. Rather than the game starting as soon as the app runs, we want the user to start it manually, by pressing the spacebar. Add the following code, which sets the initial state of the animated objects, and resets the score:

public void StartGame()
{
  dino.x = screenWidth / 2;
  dino.y = screenHeight * SKYRATIO;
  broccoliSpeedMultiplier = 0.5f;
  SpawnBroccoli();  
  score = 0;
}

Handle keyboard input

Next we need a new method to handle user input via the keyboard. Add this this method to Game1.cs:

void KeyboardHandler()
{
  KeyboardState state = Keyboard.GetState();

  // Quit the game if Escape is pressed.
  if (state.IsKeyDown(Keys.Escape))
  {
    Exit();
  }

  // Start the game if Space is pressed.
  if (!gameStarted)
  {
    if (state.IsKeyDown(Keys.Space))
    {
      StartGame();
      gameStarted = true;
      spaceDown = true;
      gameOver = false;
    }
    return;
  }            
  // Jump if Space is pressed
  if (state.IsKeyDown(Keys.Space) || state.IsKeyDown(Keys.Up))
  {
    // Jump if the Space is pressed but not held and the dino is on the floor
    if (!spaceDown && dino.y >= screenHeight * SKYRATIO - 1) dino.dY = dinoJumpY;

    spaceDown = true;
  }
  else spaceDown = false;

  // Handle left and right
  if (state.IsKeyDown(Keys.Left)) dino.dX = dinoSpeedX * -1;

  else if (state.IsKeyDown(Keys.Right)) dino.dX = dinoSpeedX;
  else dino.dX = 0;
}

Above we have a series of four if-statements:

  • The first quits the game if the Escape key is pressed.
  • The second starts the game if the Space key is pressed, and the game is not already started.
  • The third makes the dino avatar jump if Space is pressed, by changing its dY property. Note that the player cannot jump unless they are on the “ground” (dino.y = screenHeight * SKYRATIO), and will also not jump if the space key is being help down rather than pressed once. This stops the dino from jumping as soon as the game is started, piggybacking on the same keypress that starts the game.
  • Finally, the last if/else clause checks if the left or right directional arrows are being pressed, and if so changes the dino’s dX property accordingly.

Challenge: can you make the keyboard handling method above work with the WASD input scheme as well as the arrow keys?

Add logic to the Update method

Next we need to add logic for all of these parts to the Update method in Game1.cs:

float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
KeyboardHandler(); // Handle keyboard input
// Update animated SpriteClass objects based on their current rates of change
dino.Update(elapsedTime);
broccoli.Update(elapsedTime);

// Accelerate the dino downward each frame to simulate gravity.
dino.dY += gravitySpeed;

// Set game floor so the player does not fall through it
if (dino.y > screenHeight * SKYRATIO)
{
  dino.dY = 0;
  dino.y = screenHeight * SKYRATIO;
}

// Set game edges to prevent the player from moving offscreen
if (dino.x > screenWidth - dino.texture.Width/2)
{
  dino.x = screenWidth - dino.texture.Width/2;
  dino.dX = 0;
}
if (dino.x < 0 + dino.texture.Width/2)
{
  dino.x = 0 + dino.texture.Width/2;
  dino.dX = 0;
}

// If the broccoli goes offscreen, spawn a new one and iterate the score
if (broccoli.y > screenHeight+100 || broccoli.y < -100 || broccoli.x > screenWidth+100 || broccoli.x < -100)
{
  SpawnBroccoli();
  score++;
}

Draw SpriteClass objects

Finally, add the following code to the Draw method of Game1.cs, just after the last call of spriteBatch.Draw:

broccoli.Draw(spriteBatch);
dino.Draw(spriteBatch);

In MonoGame, new calls of spriteBatch.Draw will draw over any prior calls. This means that both the broccoli and the dino sprite will be drawn over the existing grass sprite, so they can never be hidden behind it regardless of their position. Try running the game now, and moving around the dino with the arrow keys and the spacebar. If you followed the steps above, you should be able to make your avatar move within the game window, and the broccoli should at an ever-increasing speed.

Render text with SpriteFont

Using the code above, we keep track of the player’s score behind the scenes, but we don’t actually tell the player what it is. We also have a fairly unintuitive introduction when the app starts up—the player sees a blue and green window, but has no way of knowing they need to press Space to get things rolling.

To fix both these problems, we’re going to use a new kind of MonoGame object called SpriteFonts.

Create SpriteFont description files

In the Solution Explorer find the Content folder. In this folder, Right-Click the Content.mgcb file and select Open With. From the popup menu select MonoGame Pipeline, then press OK. In the new window, Right-Click the Content item and select Add -> New Item. Select SpriteFont Description, name it “Score” and press OK. Then, add another SpriteFont description named “GameState” using the same procedure.

Edit descriptions

Right click the Content folder in the MonoGame Pipeline and select Open File Location. You should see a folder with the SpriteFont description files that you just created, as well as any images you’ve added to the Content folder so far. You can now close and save the MonoGame Pipeline window. From the File Explorer open both description files in a text editor (Visual Studio, NotePad++, Atom, etc).

Each description contains a number of values that describe the SpriteFont. We're going to make a few changes:

  • In Score.spritefont, change the value from 12 to 36.
  • In GameState.spritefont, change the value from 12 to 72, and the value from Arial to Agency. Agency is another font that comes standard with Windows 10 machines, and will add some flair to our intro screen.

Load SpriteFonts

Back in Visual Studio, we’re first going to add a new texture for the intro splash screen. Click here to download the image.

As before, add the texture to the project by right-clicking the Content and selecting Add -> Existing Item. Name the new item “start-splash.png”.

Next, add the following class variables to Game1.cs:

Texture2D startGameSplash;
SpriteFont scoreFont;
SpriteFont stateFont;

Then add these lines to the LoadContent method:

startGameSplash = Content.Load<Texture2D>("start-splash");
scoreFont = Content.Load<SpriteFont>("Score");
stateFont = Content.Load<SpriteFont>("GameState");

Draw the score

Go to the Draw method of Game1.cs and add the following code just before spriteBatch.End();

spriteBatch.DrawString(scoreFont, score.ToString(),
new Vector2(screenWidth - 100, 50), Color.Black);

The code above uses the sprite description we created (Arial Size 36) to draw the player’s current score near the top right corner of the screen.

Draw horizontally centered text

When making a game, you will often want to draw text that is centered, either horizontally or vertically. To horizontally center the introductory text, add this code to the Draw method just before spriteBatch.End();

if (!gameStarted)
{
  // Fill the screen with black before the game starts
  spriteBatch.Draw(startGameSplash, new Rectangle(0, 0,
  (int)screenWidth, (int)screenHeight), Color.White);

  String title = "VEGGIE JUMP";
  String pressSpace = "Press Space to start";

  // Measure the size of text in the given font
  Vector2 titleSize = stateFont.MeasureString(title);
  Vector2 pressSpaceSize = stateFont.MeasureString(pressSpace);

  // Draw the text horizontally centered
  spriteBatch.DrawString(stateFont, title,
  new Vector2(screenWidth / 2 - titleSize.X / 2, screenHeight / 3),
  Color.ForestGreen);
  spriteBatch.DrawString(stateFont, pressSpace,
  new Vector2(screenWidth / 2 - pressSpaceSize.X / 2,
  screenHeight / 2), Color.White);
  }

First we create two Strings, one for each line of text we want to draw. Next, we measure the width and height of each line when printed, using the SpriteFont.MeasureString(String) method. This gives us the size as a Vector2 object, with the X property containing its width, and Y its height.

Finally, we draw each line. To center the text horizontally, we make the X value of it’s position vector equal to screenWidth / 2 - textSize.X / 2 Challenge: how would you change the procedure above to center the text vertically as well as horizontally?

Try running the game. Do you see the intro splash screen? Does the score count up each time the broccoli respawns?

Collision detection

So we have a broccoli that follows you around, and we have a score that ticks up each time a new one spawns—but as it is there is no way to actually lose this game. We need a way to know if the dino and broccoli sprites collide, and if when they do, to declare the game over.

Rectangular collision

When detecting collisions in a game, objects are often simplified to reduce the complexity of the math involved. We are going to treat both the player avatar and broccoli obstacle as rectangles for the purpose of detecting collision between them. Open SpriteClass.cs and add a new class variable:

const float HITBOXSCALE = .5f;

This value will represent how “forgiving” the collision detection is for the player. With a value of .5f, the edges of the rectangle in which the dino can collide with the broccoli—often call the “hitbox”—will be half of the full size of the texture. This will result in few instances where the corners of the two textures collide, without any parts of the images actually appearing to touch. Feel free to tweak this value to your personal taste.

Next, add a new method to SpriteClass.cs:

public bool RectangleCollision(SpriteClass otherSprite)
{
  if (this.x + this.texture.Width * this.scale * HITBOXSCALE / 2 < otherSprite.x - otherSprite.texture.Width * otherSprite.scale / 2) return false;
  if (this.y + this.texture.Height * this.scale * HITBOXSCALE / 2 < otherSprite.y - otherSprite.texture.Height * otherSprite.scale / 2) return false;
  if (this.x - this.texture.Width * this.scale * HITBOXSCALE / 2 > otherSprite.x + otherSprite.texture.Width * otherSprite.scale / 2) return false;
  if (this.y - this.texture.Height * this.scale * HITBOXSCALE / 2 > otherSprite.y + otherSprite.texture.Height * otherSprite.scale / 2) return false;
  return true;
}

This method detects if two rectangular objects have collided. The algorithm works by testing to see if there is a gap between any of the side sides of the rectangles. If there is any gap, there is no collision—if no gap exists, there must be a collision.

Load new textures

Then, open Game1.cs and add two new class variables, one to store the game over sprite texture, and a Boolean to track the game’s state:

Texture2D gameOverTexture;
bool gameOver;

Then, initialize gameOver in the Initialize method:

gameOver = false;

Finally, load the texture into gameOverTexture in the LoadContent method:

gameOverTexture = Content.Load<Texture2D>("game-over");

Implement “game over” logic

Add this code to the Update method, just after the KeyboardHandler method is called:

if (gameOver)
{
  dino.dX = 0;
  dino.dY = 0;
  broccoli.dX = 0;
  broccoli.dY = 0;
  broccoli.dA = 0;
}

This will cause all motion to stop when the game has ended, freezing the dino and broccoli sprites in their current positions. Next, at the end of the Update method, just before base.Update(gameTime), add this line:

if (dino.RectangleCollision(broccoli)) gameOver = true;

This calls the RectangleCollision method we created in SpriteClass, and flags the game as over if it returns true.

Add user input for resetting the game

Add this code to the KeyboardHandler method, to allow the user to reset them game if they press Enter:

if (gameOver && state.IsKeyDown(Keys.Enter))
  StartGame();
  gameOver = false;
}

Draw game over splash and text

Finally, add this code to the Draw method, just after the first call of spriteBatch.Draw (this should be the call that draws the grass texture).

if (gameOver)
{
  // Draw game over texture
  spriteBatch.Draw(gameOverTexture, new Vector2(screenWidth / 2 - gameOverTexture.Width / 2, screenHeight / 4 - gameOverTexture.Width / 2), Color.White);

  String pressEnter = "Press Enter to restart!";

  // Measure the size of text in the given font
  Vector2 pressEnterSize = stateFont.MeasureString(pressEnter);

  // Draw the text horizontally centered
  spriteBatch.DrawString(stateFont, pressEnter, new Vector2(screenWidth / 2 - pressEnterSize.X / 2, screenHeight - 200), Color.White);
}

Here we use the same method as before to draw text horizontally centered (reusing the font we used for the intro splash), as well as centering gameOverTexture in the top half of the window.

And we’re done! Try running the game again. If you followed the steps above, the game should now end when the dino collides with the broccoli, and the player should be prompted to restart the game by pressing the Enter key.