MonoGame is a free open source, cross platform gaming Framework. Developers using this framework can target Windows 10, MacOS, Linux, Android, iOS and more, all using the same code base. This session will take you through the process of building a sample arcade game from scratch.
In this module, you will see how to:
- Process your game assets to make sure they are optimized for your target platform
- Load game assets
- Draw images to the screen
- Handle input from the keyboard
- Play music and sounds
The following is required to complete this module:
- Visual Studio Community 2015 or greater.
- MonoGame v3.5 or greater.
Throughout the module document, you'll be instructed to insert code blocks. For your convenience, most of this code is provided as Visual Studio Code Snippets, which you can access from within Visual Studio 2015 to avoid having to add it manually. To install the code snippets run the setup script:
- Open Windows Explorer and browse to the module's Source folder.
- Right-click Setup.cmd and select Run as administrator to launch the setup process that will configure your environment and install the Visual Studio code snippets for this module.
- If the User Account Control dialog box is shown, confirm the action to proceed.
Note: Each exercise is accompanied by a starting solution located in the Begin folder of the exercise, when applicable, that allows you to follow each exercise independently of the others. Please be aware that the code snippets that are added during an exercise are missing from these starting solutions and may not work until you've completed the exercise. Inside the source code for an exercise, you'll also find an End folder, when applicable, containing a Visual Studio solution with the code that results from completing the steps in the corresponding exercise. You can use these solutions as guidance if you need additional help as you work through this module.
This module includes the following exercise:
Estimated time to complete this module: 60 minutes
Note: When you first start Visual Studio, you must select one of the predefined settings collections. Each predefined collection is designed to match a particular development style and determines window layouts, editor behavior, IntelliSense code snippets, and dialog box options. The procedures in this module describe the actions necessary to accomplish a given task in Visual Studio when using the General Development Settings collection. If you choose a different settings collection for your development environment, there may be differences in the steps that you should take into account.
In this exercise you will create a game for Windows 10 using MonoGame. Since we only have an hour we'll be modifying an existing project to add functionality. The existing project is a space invaders style game. It is currently missing the player ship, input, and its sound effects, so when you run the project you will only see aliens. During the course of this Code Lab you will be adding new code to add these features.
-
In Visual Studio 2015 go to File->Open Project.
-
Navigate to Source\Ex1\Begin.
-
Open AlienAttackUniversal.sln solution.
-
Once the project has loaded make sure your selected build configuration is set to Debug and x86.
-
Hit F5 or click the Run Local Machine button.
The game should run, but at the moment only the aliens are present, and the player is missing.
No game would be complete without graphics and sound. Fortunately, this session comes with a complete set of assets for you to use. In order to get the maximum performance for your, game you will need to pre-process the assets into an optimized format for the target platform, in this case Windows 10. This can easily be done using the MonoGame Pipeline Tool. Its purpose is to take assets like PNG and WAV files and compress/optimize them for the platform you are targeting. This will allow you to take advantage of things like texture compression which, in turn, will allow you to include more graphics.
Most of the content has already been added to the game, but some bits are missing. In this section we will use the Pipeline tool to add these assets to the game.
-
Click the Content folder in the Solution Explorer to expand it.
-
Double click on Content.mgcb. This should open the file in the MonoGame Pipeline Tool. If the file opens in the code editor, right-click on Content.mgcb and click Open With, and then select MonoGame Pipeline Tool.
-
Right-click on the gfx folder in the pipeline tool and click Add->Existing Item.
-
Navigate to Source\Ex1\Begin\Content\gfx\player.png and click Open.
-
Right-click on the sfx folder in the pipeline tool and click Add->Existing Item.
-
Navigate to Begin\AlienAttackUniversal\Content\sfx\playerShot.wav and click Open.
-
Select File->Save to save the changes.
That is how easy it is to add content to the game. All of the content is included automatically in you project as long as it is in the Content.mcgb file. If you have files that you just want to copy over (like XML files), you can also add those to the Content.mgcb file but set the Build Action to "Copy" and they will also be included without changes.
The project already has a Player class located in the Sprites directory, however its implementation is empty. Let's fix that.
-
Double-click on the Player.cs item in Solution Explorer. Notice that this class inherits from the project's base Sprite class. Feel free to take a look at the base class, noting that it contains various properties like Position and Velocity.
-
We now need to load the textures for this sprite. In the constructor, call the LoadContent method from the base Sprite class as shown:
public Player() { LoadContent(AlienAttackGame.Instance.Content, "gfx\\player"); }
If you look at the LoadContent method, you will see that it uses MonoGame's ContentManager class to load the item located at "gfx\player" as a Texture2D, which is the exact file we just added to the Content Pipeline Tool.
public virtual void LoadContent(ContentManager contentManager, string name) { // load single frame Frames = new Texture2D[1]; Frames[0] = contentManager.Load<Texture2D>(name); Width = Frames[0].Width; Height = Frames[0].Height; }
As you can see this method makes use of the ContentManager class to load the texture. The ContentManager is the main way to load any kind of asset which has been processed by the content Pipeline. An instance is created on the Game class and is accessible via the .Content property. As you can see, we pass in
AlienAttackGame.Instance.Content
into the LoadContent method.The ContentManager provides a generic Load method for loading assets. You can use this to load Texture2D, SpriteFont and other content types. It also includes type checking, so if you attempt to load a song into a Texture2D, for example, the manager will throw an exception.
-
Now that the content is loaded, the sprite needs to be drawn. In the Player class, you will find an overridden Draw method which we can fill in with the proper code. This method will be passed a SpriteBatch object. A SpriteBatch does exactly what its name implies: it draws a batch of sprites to the screen. We will just add the player's Texture2D to that list by writing the following code:
public override void Draw(GameTime gameTime, SpriteBatch spriteBatch) { spriteBatch.Draw(Frames[0], Position); }
This code uses the Frames array from the Sprite class and draws the first (and only) element in that array. Additionally, we pass in the current Position at which to draw the Sprite
-
At this point, go ahead and run the game. You should now see that there is a player ship at the bottom of the screen, but it still doesn't move or shoot...
-
Open AlienAttackGame.cs from Solution Explorer.
-
Navigate to the Update method. This method is called once per frame in order for you to update anything in the game world before it is drawn to the screen. This is where we will read input and move the ship's position.
-
At the top of the Update method add the following to get the keyboard's current state:
_keyboardState = Keyboard.GetState();
-
At the bottom of the Update method, right before the call to base.Update, add the following code, which will save the current frame's keyboard state so we can compare it on the next frame:
_lastKeyboard = _keyboardState;
-
Now, let's read the arrow keys to determine if we should move the ship left or right. Add the following code inside the Update method after the call to Keyboard.GetState:
if (_keyboardState.IsKeyDown(Keys.Left) && _player.Position.X > 0) left = true; else if (_keyboardState.IsKeyDown(Keys.Right) && _player.Position.X + _player.Width < ScreenWidth) right = true;
This chunk of code uses the IsKeyDown method from the KeyboardState class to determine if a keyboard key is held down. We check if the Left arrow key is down (Keys.Left) and, if it is, we set the local left boolean to true. If the Right arrow key is down (Keys.Right), we set the local right boolean to true. You will see why we did it this way, shortly...
-
Next, let's add some code which handles these boolean values. Inside the null reference check for _player, add the following code:
if (_player != null) { if (left && _player.Position.X > 0) _player.Velocity = -PlayerVelocity; else if (right && _player.Position.X + _player.Width < ScreenWidth) _player.Velocity = PlayerVelocity; else _player.Velocity = Vector2.Zero; _player.Update(gameTime); }
This code checks to see if either left or right are set to true, and if so, the Velocity property on the _player object is set appropriately (negative or positive/left or right). If neither is set to true, the Velocity property is set to Vector2.Zero, which won't move the ship in either direction.
-
Now, run the game again. You should now be able to move the ship left and right with the arrow keys!
Our next task is to get our ship to shoot.
-
Back in our Update method, just below the keyboard checks for the arrow keys, let's add some code to check the state of the space bar, and fire a shot when it's pressed:
if((_keyboardState.IsKeyDown(Keys.Space) && !_lastKeyboard.IsKeyDown(Keys.Space))) fire = true;
This code will again use the IsKeyDown method to check whether the space bar is pressed (Keys.Space), since the last frame, and sets the fire boolean appropriately.
-
Now, we need to handle the case when fire is true. Back in the spot where we added the code to check for left and right, we can add a check for fire as shown:
if(fire && gameTime.TotalGameTime.TotalMilliseconds - _lastShotTime > ShotTime) { AddPlayerShot(); _lastShotTime = gameTime.TotalGameTime.TotalMilliseconds; }
This checks that fire is true and whether it has been more than ShotTime since the last time a shot was fired. This ensures the player can't just hold the space bar down and fire shots continuously. If those conditions are met, we call the AddPlayerShot method, and then save off the current time in the _lastShotTime member for comparison on subsequent frames.
- At this point, you can run the game again and you will be able to fire shots from the player ship and destroy the aliens. See the HandleCollisions and UpdatePlayerShots methods for more information on how the game does collision detection and moves the shots up the screen.
Our game now has keyboard support, but it would be nice to add touch input support for devices with a touch screen (tablets, phones, laptops, etc.). Let's add that now.
-
Navigate to the Initialize method and add the following line of code before the call to base.Initialize:
TouchPanel.EnabledGestures = GestureType.Tap | GestureType.HorizontalDrag;
This tells MonoGame to listen for Tap and Horizontal Drag gestures on the screen, while ignoring all others.
-
Next, we need to handle the gestures. This is done back in our Update method. Below the code that reads the space bar with IsKeyDown, add the following:
while(TouchPanel.IsGestureAvailable) { GestureSample gs = TouchPanel.ReadGesture(); if(gs.GestureType == GestureType.HorizontalDrag) { if(gs.Delta.X < 0) left = true; else if(gs.Delta.X > 0) right = true; } if(gs.GestureType == GestureType.Tap) fire = true; }
Hopefully it is now clear why we setup the left/right/fire boolean variables. This chunk of code does several things. First, it loops while TouchPanel.IsGestureAvailable is true. Gestures are actually stored in a queue to be parsed and handled by your code. We get the next gesture in the list by calling TouchPanel.ReadGesture. This returns a GestureSample object, which we use to figure out what gesture the user performed. If it's a HorizontalDrag gesture, we set those same left and right variables to true, accordingly. If it's a Tap gesture, we set the fire boolean.
- Run the game again and drag your finger horizontally or tap to move or fire. Note that this won't work using a mouse as the TouchPanel object ONLY responds to actual touch inputs.
Now that we can fire shots up the screen, it would be nice if there was a sound effect to accompany the action. First, we need to load the proper effect using the Content object, just like we did with the player ship graphic.
-
Navigate to the LoadContent method.
-
Add the following code to load the PlayerShot sound effect:
_playerShot = Content.Load<SoundEffect>("sfx\\playerShot");
-
Now that we have the effect loaded, we just need to play it when the shot is fired. Navigate back to the Update method, and add the following line of code following the call to AddPlayerShot:
_playerShot.Play();
That's it! Now the game will fire shots and play the sound effect accordingly.
Throughout the game, we have been keeping score based on the player destroying the enemy ships. For every ship destroyed, the player earns 100 points, which is stored in the _score member variable. Let's draw this to the screen.
-
Navigate to the Draw method
-
Near the bottom of the method, prior to the call to _spritebatch.End, add the following lines of code:
Vector2 scoreSize = _font.MeasureString("Score: " + _score); _spriteBatch.DrawString(_font, "Score: " + _score, ScorePosition - scoreSize/2.0f, Color.Aqua);
This code uses a SpriteFont to draw text to the screen. A SpriteFont already exists in the project and is included in the Content Pipeline Tool. This is a simple XML file that defines the font to use, its size, and other characteristics. When the Pipeline Tool runs at compile time, a MonoGame-specific file is created which contains all of the alphanumeric symbols of the font as you specified. In code, we can simply load that font, and use the SpriteFont class to draw text to the screen.
In the example above, the first line of code uses the **MeasureString** method to determine the width and height of the rectangle that would be necessary to draw the string passed in. The second line of code calls **DrawString** to actually draw the text to the screen, using the previously returned size to place it centered on the screen.
The game is completely playable at this stage, but having some background music might make the game a bit more interesting. Let's add some.
-
In the LoadContent method, load the content named sfx\theme as a Song object. Song objects are not loaded completely into memory. Instead, they can be streamed from disk, saving memory. Typically, this is used with larger sound files, such as background or theme music:
_theme = Content.Load<Song>("sfx\\theme");
-
At the very top of the Update method, add the code to start the music playing if it isn't already. This uses the MediaPlayer object, which is used to play sounds loaded as type Song.
if(MediaPlayer.State == MediaState.Stopped) { MediaPlayer.IsRepeating = true; MediaPlayer.Play(_theme); }
It's as simple as that. Go ahead and run the game again and enjoy the new tune!
MonoGame is an extremely flexible framework that allows you to write a game in any way you see fit. It does not force you to do things in a particular way. As a result, there are many different ways to write a game. This module should have given you a grasp of:
- Processing and Loading Content
- Drawing Textures
- Playing Sound and Music
- Drawing Text
Check out the much longer README.md document in the Complete directory in this repo to see how this game was built from absolute scratch, with a bit more context on proper game architecture.