2D Graphics, Coordinates, and Game State
After completing this chapter, you will be able to:
Introduction
Most games these days use graphics in some form or another to represent or communicate their state, and 2D games are no different. Whether they use a texture used to represent a hero character or particles to show an explosion, 2D games also rely on graphics and effects to bring their game world to life. This chapter will cover some of the basic building blocks needed to achieve the desired graphics and effects for your game.
As in general for this book, you can follow along with the sections using the example projects contained in the Chapter03SourceCode folder. An even better approach is to create your own project and follow along by inserting the required code as you step through the procedures. Building the projects yourself will give you both hands-on development experience and better insight into the concepts covered. It is also important to note that most of the projects build upon one another and therefore often reuse code from prior sections.
The Game Window
The first aspect to address is the game window. A game window is commonly displayed in one of two modes: windowed mode or full-screen mode. In windowed mode, you can specify the desired resolution of your game window, and it will be contained in the operating system’s (Windows) default window. Full-screen mode will fit your game to the entire screen—even if the window size does not match, which can cause the game to look stretched or pixelated unless you plan for and handle the full-screen situation carefully.
The Game Window Size project
This project demonstrates how to toggle between full-screen mode and windowed mode. Figure 3-1 shows an example of the project running in windowed mode.
Figure 3-1. Running the Game Window Size project in windowed mode
The project’s controls are as follows:
The goals of the project are as follows:
public class Game1 : Game
{
GraphicsDeviceManager mGraphics;
SpriteBatch mSpriteBatch;
// Prefer window size
const int kWindowWidth = 1000;
const int kWindowHeight = 700;
...
}
public Game1()
{
mGraphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// Set preferred window size
mGraphics.PreferredBackBufferWidth = kWindowWidth;
mGraphics.PreferredBackBufferHeight = kWindowHeight;
...
}
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (InputWrapper.Buttons.Back == ButtonState.Pressed)
this.Exit();
// "A" to toggle to full-screen mode
if (InputWrapper.Buttons.A == ButtonState.Pressed)
{
if (!mGraphics.IsFullScreen)
{
mGraphics.IsFullScreen = true;
mGraphics.ApplyChanges();
}
}
// "B" toggles back to windowed mode
if (InputWrapper.Buttons.B == ButtonState.Pressed)
{
if (mGraphics.IsFullScreen)
{
mGraphics.IsFullScreen = false;
mGraphics.ApplyChanges();
}
}
}
While modifying the window size and state is useful, it is not very interesting without any graphical objects in the game to show the changes. Next, you’ll see what it takes to add some graphical objects to the game.
Textured Primitives
Most games use images, or textures, to represent characters or objects. With proper support, images or textures provide a straightforward way of getting assets or art into the game. In general, you can use textures for a wide variety of purposes within the game, including background representation and UI layout. This section guides you through the process of implementing a project designed to support textures.
The Textured Primitive project
This project demonstrates how to import textures and manipulate their position and size. It also demonstrates how to select the textures you want to control. Figure 3-2 shows the project running.
Figure 3-2. Running the Textured Primitive project
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Abstracting TexturedPrimitive into a class enables you to easily reuse, modify, and build upon the object in the future, which is the purpose of object-oriented programming.
Creating the TexturedPrimitive class
public class Game1 : Game
{
static public SpriteBatch sSpriteBatch; // Drawing support
static public ContentManager sContent; // Loading textures
static public GraphicsDeviceManager sGraphics; // Current display size
...
}
Note Variables that begin with the letter s (for example, sStaticVariable) indicate that they are static.
You have successfully created a new C# class file within the project. Next, you’ll define functionality and behavior for this new class.
Adding custom functionally and behavior to TexturedPrimitive enables it to handle its own draw and update cycles, which keeps the object self-contained and simpler to manage.
Adding TexturedPrimitive functionality and behavior
protected Texture2D mImage; // The UWB-JPG.jpg image to be loaded
protected Vector2 mPosition; // Center position of image
protected Vector2 mSize; // Size of the image to be drawn
Note Variables that begin with the letter m are instance variables. These variables are accessible within the scope of the class.
public TexturedPrimitive(String imageName, Vector2 position, Vector2 size)
{
mImage = Game1.sContent.Load<Texture2D>(imageName);
mPosition = position;
mSize = size;
}
public void Update(Vector2 deltaTranslate, Vector2 deltaScale)
{
mPosition += deltaTranslate;
mSize += deltaScale;
}
public void Draw()
{
// Defines where and size of the texture to show
Rectangle destRect = new Rectangle((int)mPosition.X, (int)mPosition.Y,
(int)mSize.X, (int)mSize.Y);
Game1.sSpriteBatch.Draw(mImage, destRect, Color.White);
}
Now that you have completed the TexturedPrimitive class, it can be used within the Game1 class.
Using the TexturedPrimitive class
public class Game1 : Game
{
static public SpriteBatch sSpriteBatch; // Drawing support
static public ContentManager sContent; // Loading textures
static public GraphicsDeviceManager sGraphics; // Current display size
// Preferred window size
const int kWindowWidth = 1000;
const int kWindowHeight = 700;
const int kNumObjects = 4;
// Work with the TexturedPrimitive class
TexturedPrimitive[] mGraphicsObjects; // An array of objects
int mCurrentIndex = 0;
...
}
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
Game1.sSpriteBatch = new SpriteBatch(GraphicsDevice);
// Create the primitives.
mGraphicsObjects = new TexturedPrimitive[kNumObjects];
mGraphicsObjects[0] = new TexturedPrimitive("UWB-JPG", // Image file name
new Vector2(10, 10), // Position to draw
new Vector2(30, 30)); // Size to draw
mGraphicsObjects[1] = new TexturedPrimitive("UWB-JPG",
new Vector2(200, 200), new Vector2(100, 100));
mGraphicsObjects[2] = new TexturedPrimitive("UWB-PNG",
new Vector2(50, 10), new Vector2(30, 30));
mGraphicsObjects[3] = new TexturedPrimitive("UWB-PNG",
new Vector2(50, 200), new Vector2(100, 100));
}
protected override void Draw(GameTime gameTime)
{
// Clear to background color
GraphicsDevice.Clear(Color.CornflowerBlue);
Game1.sSpriteBatch.Begin(); // Initialize drawing support
// Loop over and draw each primitive
foreach (TexturedPrimitive p in mGraphicsObjects)
{
p.Draw();
}
Game1.sSpriteBatch.End(); // Inform graphics system we are done drawing
base.Draw(gameTime);
}
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
...
// "A" to toggle to full screen
...
// "B" toggles back to window
...
// Button X to select the next object to work with
if (InputWrapper.Buttons.X == ButtonState.Pressed)
mCurrentIndex = (mCurrentIndex + 1) % kNumObjects;
// Update currently working object with thumb sticks.
mGraphicsObjects[mCurrentIndex].Update(
InputWrapper.ThumbSticks.Left,
InputWrapper.ThumbSticks.Right);
base.Update(gameTime);
}
Observations
With all the necessary components of the TexturedPrimitive project completed, it is time to build and run the project while making some observations. Run the project and experiment with its functionality. Observe the behavior of the images when moving each thumbstick separately, as well as when toggling to and from full-screen mode. In particular, you should make the following observations:
In the upcoming projects, you will learn the necessary concepts to address and correct each of these behaviors to support a more intuitive experience for the user, as well as a more straightforward game world for you to develop in.
Coordinate System and Camera
As you saw at the end of the previous section, the TexturedPrimitive project was successful in adding multiple images to the game world and supporting straightforward interaction with the images. However, it also shed light on several issues that arise when using the default pixel space. These and many other issues that will arise by using default pixel space can be addressed by creating your own user-defined coordinate system.
For the purposes of this discussion, pixel space describes the area that the game window occupies in units of pixels. Each pixel in the game window represents a point in a discrete coordinate system where the origin lies in the top-left corner of the window, the x-axis points toward the right, and the y-axis points downward. A user-defined coordinate system is a Cartesian coordinate system created by you to suit your needs for the project. The user-defined coordinate system is different from the pixel space in two unique ways: first, the unit of the user-defined system is independent of pixels; second, the y-axis of the user-defined system points upward. The user-defined coordinate system acts as a layer between the default pixel space and you, the developer, in order to standardize the game world across different window sizes and views.
Note The terms user-defined coordinate system and user-defined coordinate space will be used interchangeably from here on.
Creating a user-defined coordinate system makes your job as a developer easier from a technical standpoint as well as a design standpoint. For example, imagine you are creating a game that uses human-sized characters. Under a user-defined coordinate system, you could use a meter as your basic unit and design you characters to be two units tall. However, under a pixel-space system, a character becomes an arbitrary amount of pixels tall. The problem becomes even worse when considering effects like camera zooming, since the character’s pixel height has already been specified. A user-defined coordinate system allows the flexibility of adding new functionality while maintain the core functionality needed to standardize the game’s look.
The User-Defined Coordinate System project
This project builds infrastructure support for a user-defined coordinate system and allows you to move away from pixel space. This infrastructure support includes defining a new Camera class and building new functionality in the TexturedPrimitive class. The functionality of the project is the same as the Textured Primitive project. However, as a developer you will be able to specify the position and size of an object independent from pixel resolution. Figure 3-3 shows the project running.
Figure 3-3. Running the User-Defined Coordinate System project
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Understanding pixel space and user-defined space
Before we go into the details of creating a user-defined coordinate system, we’ll first discuss the default pixel space, which is shown in Figure 3-4. Upon inspection, you’ll notice that the origin lies in the top-left corner of the game window rather than the bottom-left corner. Additionally, the y-axis increases in value in the downward direction rather than upward, as would generally be expected. It is also important to note that we use Wp to represent the width of the game window and Hp to represent the height of the game window.
Figure 3-4. Default pixel space
Now that you know the layout of the default pixel space, you can begin to design the user-defined coordinate system to reflect a more intuitive layout. To start, it is a good idea to first specify the desired user-defined space. Usually this is done by creating a camera region that views a specified section of the game world. In Figure 3-5 you can see an example of a user-defined space where the light-blue rectangle represents the camera region (or camera window) that should be displayed in the game window. You can see outside the camera window that the desired user-defined space has its origin located in the bottom-left corner and a y-axis that increases in the upward direction. The camera window itself is defined by its bottom-left corner (Xc and Yc), its width (Wc), and its height (Hc).
Figure 3-5. The user-defined coordinate system
With the pixel space and the desired user space defined, you can now begin addressing the task of converting from the user space to that pixel space. To transition to pixel space, you first need to shift, or translate, the camera window to the origin, as shown in Figure 3-6. This is done by subtracting Xc and Yc from the camera window’s position.
Figure 3-6. User-defined coordinate system with a translated origin
Next, the user-defined space needs to be resized (or scaled) to match the width and height of the pixel space. This can be achieved by multiplying the width Wc by Wp/Wc and the height Hc by Hp/Hc, which a leaves you with Wp , Hp for your width and height. You can see this in Figure 3-7.
Figure 3-7. User-defined coordinate system with scaling being applied
The last step for converting to pixel space is to flip the y-axis so that the origin lies in the top-left corner. This can be done by taking the height of the pixel space and subtracting the y value from it. This leaves you with an origin in the top-left corner of the window, as shown in Figure 3-8.
Figure 3-8. User-defined coordinate system with an inverted y-axis
Now that you know the process of converting to pixel space, take a look at the following formulas, where camera positions are represented with (Cx , Cy) in the user-defined space and the corresponding pixel space positions are presented as (Px , Py):
If you follow the equation for the y values, you can see each step in the process of conversion happen in order. First, you translate to the origin by subtracting Yc from Cy. Next, you scale to match the pixel space’s size by multiplying by (Hp /Hc). Finally, the y-axis is flipped with subtraction by Hp. The formula is similar for the x-axis values, except there’s no need to flip the horizontal axis.
Now that you have an understanding of the process and mathematics involved in converting from user-defined space to pixel space, you’ll take a look at how a Camera class can be created to support this conversion. To proceed, continue using the Textured Primitive project from earlier in the chapter as camera support is added, or follow along with the 3.UserDefinedCoordinateSystem example provided in the Chapter03SourceCode folder.
Creating a user-defined coordinate system
namespace Book_Example
{
static public class Camera
{
...
}
}
static private Vector2 sOrigin = Vector2.Zero; // Origin of the world
static private float sWidth = 100f; // Width of the world
static private float sRatio = -1f; // Ratio between camera window and pixel
static private float cameraWindowToPixelRatio()
{
if (sRatio < 0f)
sRatio = (float) Game1.sGraphics.PreferredBackBufferWidth / sWidth;
return sRatio;
}
static public void SetCameraWindow(Vector2 origin, float width)
{
sOrigin = origin;
sWidth = width;
}
Note Notice that only the width of camera window can be specified. This is because once the width is defined, the height of the camera window depends on the actual height of the game window. Allowing only the width to be modified guarantees that the camera can draw to the entire game window with the proper aspect ratio.
static public void ComputePixelPosition(Vector2 cameraPosition, out int x, out int y)
{
float ratio = cameraWindowToPixelRatio();
// Convert the position to pixel space
x = (int)(((cameraPosition.X - sOrigin.X) * ratio) + 0.5f);
y = (int)(((cameraPosition.Y - sOrigin.Y) * ratio) + 0.5f);
y = Game1.sGraphics.PreferredBackBufferHeight - y;
}
static public Rectangle ComputePixelRectangle(Vector2 position, Vector2 size)
{
float ratio = cameraWindowToPixelRatio();
// Convert size from camera window space to pixel space.
int width = (int)((size.X * ratio) + 0.5f);
int height = (int)((size.Y * ratio) + 0.5f);
// Convert the position to pixel space
int x, y;
ComputePixelPosition(position, out x, out y);
// Reference position is the center
y -= height / 2;
x -= width / 2;
return new Rectangle(x, y, width, height);
}
Using the Camera class
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
Game1.sSpriteBatch = new SpriteBatch(GraphicsDevice);
// Define camera window bounds
Camera.SetCameraWindow(new Vector2(10f, 20f), 100f);
// Create the primitives
mGraphicsObjects = new TexturedPrimitive[kNumObjects];
mGraphicsObjects[0] = new TexturedPrimitive("UWB-JPG",
new Vector2(15f, 25f), new Vector2(10f, 10f));
mGraphicsObjects[1] = new TexturedPrimitive("UWB-JPG",
new Vector2(35f, 60f), new Vector2(50f, 50f));
mGraphicsObjects[2] = new TexturedPrimitive("UWB-PNG",
new Vector2(105f, 25f), new Vector2(10f, 10f));
mGraphicsObjects[3] = new TexturedPrimitive("UWB-PNG",
new Vector2(90f, 60f), new Vector2(35f, 35f));
// NOTE: Since the creation of TexturedPrimitive involves loading of textures,
// the creation should occur in or after LoadContent()
}
public void Draw()
{
// Defines where and size of the texture to show
Rectangle destRect = Camera.ComputePixelRectangle(Position, Size);
Game1.sSpriteBatch.Draw(mImage, destRect, Color.White);
}
Build and run the project. The results should be the same as those shown previously in Figure 3-3, in pixel space. Now let’s take a look at user-defined space. Figure 3-9 shows the game world in the user-defined space.
Figure 3-9. User-defined coordinate system within the pixel space
Now try toggling buttons A and B to switch between windowed and full-screen modes. Notice how the relative sizes of the objects remain constant—an object that occupies half of the game window will continue to occupy the half in full-screen mode. As a game developer, you can now focus on designing the relative sizes and positions of your game objects without concerning yourself with the eventual pixel resolution of the game window.
It is important to recognize the difference in the camera’s width and position between coordinate spaces. The camera only covers 100 units in width in the user-defined coordinate space, while in pixel space it covers 1,000 pixels. Similarly, the camera’s origin is located in the bottom-left corner, at (10, 20), while the game window’s origin is located in the top-left corner, at (0, 0). This indicates that you have successfully decoupled the user-defined coordinate system from the game window’s pixel space.
Font Output
A valuable tool that many games use for a variety of tasks is text output. This is because text provides an efficient way to communicate information to the user as well as you, the developer. For example, it can be used to communicate the game’s story, the player’s score, or the debugging information during development. However, unlike in console programming, the game window that MonoGame provides does not include direct text output support. Fortunately, this can be easily remedied by using the SpriteFont class to create a custom class to produce output of text to the game window.
The SpriteFont class takes your desired font and converts the characters into a texture during build time. To use the SpriteFont class, a font file, such as Arial.spritefont, must be created, this file must then be conveted into the xnb format, and finally added to the project. Similar to images, fonts should be added to the content folder before they are used. This is because, like images, the font file is an external resource.
Note SpriteFonts are XML files that can be generated from existing fonts on your PC. In order to create or customize your own SpriteFont, refer to http://msdn.microsoft.com/library/bb447673.aspx. For more information on the SpriteFont class and its properties, refer to http://msdn.microsoft.com/library/bb447759.aspx. It is also important to note that while you can create SpriteFonts from fonts that exist on your PC, you may need permission to use those fonts in your game.
Before you create a class to support fonts, you should first find the Arial.spritefont font description resource file in the Chapter03SourceCodeImagesAndFontUsed folder and convert this file into Arial.xnb. This conversion can be accomplished by invoking the XNAFormatter program from the XNB Builder utility you have downloaded and unzipped as part of the “Downloads and Installations” operations you performed in Chapter 1. Once converted, you can then include the Arial.xnb file into the MonoGame project in a similar fashion as image files we have worked with: either by right-clicking over the Content folder and navigating to Add Existing Item, or by dragging and dropping the file into the Content folder.
Note As with images, if you choose to drag and drop the Arial.xnb file into the Content folder, you must remember to bring up the Properties window and change the Build Action field to Content, and Copy to Output Directory to Copy if Newer.
This project demonstrates how to draw text to the game screen by using a FontSprite asset. Two types of print function are supported. One prints to the top-left corner of the screen, and the other prints the message to any specified location in user-defined coordinate space. Printing to a location is useful when it is necessary to print text on an object; notice that the currently selected texture in Figure 3-10 is identified.
Figure 3-10. Running the Font Output project
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Creating the FontSupport class
static private SpriteFont sTheFont = null;
static private Color sDefaultDrawColor = Color.Black;
static private Vector2 sStatusLocation = new Vector2(5, 5);
static private void LoadFont()
{
// For demo purposes, loads Arial.spritefont
if (null == sTheFont)
sTheFont = Game1.sContent.Load<SpriteFont>("Arial");
}
Note Notice that the Load() function does not specify the extension (.spritefont), only the font name (Arial).
static private Color ColorToUse(Nullable<Color> c)
{
return (null == c) ? sDefaultDrawColor : (Color)c;
}
static public void PrintStatus(String msg, Nullable<Color> drawColor)
{
LoadFont();
Color useColor = ColorToUse(drawColor);
// Compute top-left corner as the reference for output status
Game1.sSpriteBatch.DrawString(sTheFont, msg, sStatusLocation, useColor);
}
static public void PrintStatusAt(Vector2 pos, String msg, Nullable<Color> drawColor)
{
LoadFont();
Color useColor = ColorToUse(drawColor);
int pixelX, pixelY;
Camera.ComputePixelPosition(pos, out pixelX, out pixelY);
Game1.sSpriteBatch.DrawString(sTheFont, msg,
new Vector2(pixelX, pixelY), useColor);
}
A full listing of the FontSupport class is shown in Listing 3-1.
Listing 3-1. The complete FontSupport class
static public class FontSupport
{
static private SpriteFont sTheFont = null;
static private Color sDefaultDrawColor = Color.Black;
static private Vector2 sStatusLocation = new Vector2(5, 5);
static private void LoadFont()
{
// for demo purposes, loads Arial.spritefont
if (null == sTheFont)
sTheFont = Game1.sContent.Load<SpriteFont>("Arial");
}
static private Color ColorToUse(Nullable<Color> c)
{
return (null == c) ? sDefaultDrawColor : (Color)c;
}
static public void PrintStatusAt(Vector2 pos, String msg, Nullable<Color> drawColor)
{
LoadFont();
Color useColor = ColorToUse(drawColor);
int pixelX, pixelY;
Camera.ComputePixelPosition(pos, out pixelX, out pixelY);
Game1.sSpriteBatch.DrawString(sTheFont, msg,
new Vector2(pixelX, pixelY), useColor);
}
static public void PrintStatus(String msg, Nullable<Color> drawColor)
{
LoadFont();
Color useColor = ColorToUse(drawColor);
// compute top-left corner as the reference for output status
Game1.sSpriteBatch.DrawString(sTheFont, msg, sStatusLocation, useColor);
}
}
Using and observing the FontSupport class
To use the FontSupport class, all you need to do is call the PrintStatus and PrintStatusAt functions inside the Draw() function of Game1. PrintStatus will print the message to the top-left corner of the game window, and PrintStatusAt will print the message at your specified location. It’s that simple.
protected override void Draw(GameTime gameTime)
{
// Clear to background color
...
// Loop over and draw each primitive
...
// Print out text message to echo status
FontSupport.PrintStatus("Selected object is:" + mCurrentIndex +
" Location=" + mGraphicsObjects[mCurrentIndex].Position, null);
FontSupport.PrintStatusAt(mGraphicsObjects[mCurrentIndex].Position, "Selected", Color.Red);
...
}
If you build and run the program, you can see it behave as expected (as shown previously in Figure 3-10). It is important to understand that the PrintStatus() function never converts the position of its message into the user-defined coordinate space. This means that it will remain at the top-left corner of the game window, even when the camera is repositioned.
A Simple Game Object
Now that you have an understanding of how the user-defined coordinate system functions via the Camera class and can now output text to the game window, it is time to create your first game object. Let’s start simple and create a ball with some basic functionality, such as multiple instantiation, movement control, and bounds-collision detection.
The Simple Game Object project
This project demonstrates how to create soccer balls that collide with the edge of the screen. Figure 3-11 shows the project running.
Figure 3-11. Running the Simple Game Object project
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Before you can implement bounds support, you need an understanding of what the bounds of an object entail. Take a look at Figure 3-12. It shows the bounds of a texture represented as MinBound and MaxBound. MinBound is in the bottom-left corner and MaxBound is in the top-right corner of the texture. This is a fairly common way to represent the bounds of a rectangular object, as it uses only two points. With the bounds of an object defined, you can add behavior such as collision.
Figure 3-12. The bounds and center position of the texture
You can calculate the values of these bounds by using the position and size of the texture, which are already known. Let’s try it.
Modifying the classes to include bounds support
public Vector2 MinBound { get { return mPosition - (0.5f * mSize); } }
public Vector2 MaxBound { get { return mPosition + (0.5f * mSize); } }
/// Accessors to the camera window bounds
static public Vector2 CameraWindowLowerLeftPosition
{ get { return sOrigin; } }
static public Vector2 CameraWindowUpperRightPosition
{ get { return sOrigin + new Vector2(sWidth, sHeight); } }
Adding collision detection support
Note For more information on enums, please refer to http://msdn.microsoft.com/library/sbbt4032.aspx.
// Support collision with the camera bounds
public enum CameraWindowCollisionStatus {
CollideTop = 0,
CollideBottom = 1,
CollideLeft = 2,
CollideRight = 3,
InsideWindow = 4
};
static public CameraWindowCollisionStatus CollidedWithCameraWindow(TexturedPrimitive prim)
{
Vector2 min = CameraWindowLowerLeftPosition;
Vector2 max = CameraWindowUpperRightPosition;
if (prim.MaxBound.Y > max.Y)
return CameraWindowCollisionStatus.CollideTop;
if (prim.MinBound.X < min.X)
return CameraWindowCollisionStatus.CollideLeft;
if (prim.MaxBound.X > max.X)
return CameraWindowCollisionStatus.CollideRight;
if (prim.MinBound.Y < min.Y)
return CameraWindowCollisionStatus.CollideBottom;
return CameraWindowCollisionStatus.InsideWindow;
}
static public Random sRan; // For generating random numbers
public Game1()
{
...
Game1.sRan = new Random();
}
Since all the necessary supporting code is now complete, it is time to create the SoccerBall class.
Creating the SoccerBall class
public class SoccerBall : TexturedPrimitive
{
...
}
private Vector2 mDeltaPosition; // Change current position by this amount
public SoccerBall(Vector2 position, float diameter) :
base("Soccer", position, new Vector2(diameter, diameter))
{
mDeltaPosition.X = (float) (Game1.sRan.NextDouble()) * 2f - 1f;
mDeltaPosition.Y = (float) (Game1.sRan.NextDouble()) * 2f - 1f;
}
public float Radius {
get { return mSize.X * 0.5f; }
set { mSize.X = 2f * value; mSize.Y = mSize.X;}
}
public void Update()
{
Camera.CameraWindowCollisionStatus status = Camera.CollidedWithCameraWindow(this);
switch (status) {
case Camera.CameraWindowCollisionStatus.CollideBottom:
case Camera.CameraWindowCollisionStatus.CollideTop:
mDeltaPosition.Y *= -1;
break;
case Camera.CameraWindowCollisionStatus.CollideLeft:
case Camera.CameraWindowCollisionStatus.CollideRight:
mDeltaPosition.X *= -1;
break;
}
Position += mDeltaPosition;
}
That is all that is currently needed for the SoccerBall class. It is now ready to be used. A full code listing of SoccerBall .cs is shown in Listing 3-2.
Listing 3-2. The complete SoccerBall class
public class SoccerBall : TexturedPrimitive
{
private Vector2 mDeltaPosition; // Change current position by this amount
/// <summary>
/// Constructor of SoccerBall
/// </summary>
/// <param name="position">center position of the ball</param>
/// <param name="diameter">diameter of the ball</param>
public SoccerBall(Vector2 position, float diameter) :
base("Soccer", position, new Vector2(diameter, diameter))
{
mDeltaPosition.X = (float) (Game1.sRan.NextDouble()) * 2f - 1f;
mDeltaPosition.Y = (float) (Game1.sRan.NextDouble()) * 2f - 1f;
}
// Accessors
public float Radius
{
get { return mSize.X * 0.5f; }
set { mSize.X = 2f * value; mSize.Y = mSize.X;}
}
/// <summary>
/// Compute the soccer ball's movement in the camera window
/// </summary>
public void Update()
{
Camera.CameraWindowCollisionStatus status =
Camera.CollidedWithCameraWindow(this);
switch (status) {
case Camera.CameraWindowCollisionStatus.CollideBottom:
case Camera.CameraWindowCollisionStatus.CollideTop:
mDeltaPosition.Y *= -1;
break;
case Camera.CameraWindowCollisionStatus.CollideLeft:
case Camera.CameraWindowCollisionStatus.CollideRight:
mDeltaPosition.X *= -1;
break;
}
Position += mDeltaPosition;
}
}
All that’s left is to use the SoccerBall in your game. You can do this in the Game1 class.
Using the SoccerBall class
TexturedPrimitive mUWBLogo;
SoccerBall mBall;
Vector2 mSoccerPosition = new Vector2(50, 50);
float mSoccerBallRadius = 3f;
protected override void LoadContent()
{
...
// Create the primitives
mUWBLogo = new TexturedPrimitive("UWB-PNG", new Vector2(30, 30), new Vector2(20, 20));
mBall = new SoccerBall(mSoccerPosition, mSoccerBallRadius*2f);
}
protected override void Update(GameTime gameTime)
{
...
mUWBLogo.Update(InputWrapper.ThumbSticks.Left, Vector2.Zero);
mBall.Update();
mBall.Update(Vector2.Zero, InputWrapper.ThumbSticks.Right);
if (InputWrapper.Buttons.A == ButtonState.Pressed)
mBall = new SoccerBall(mSoccerPosition, mSoccerBallRadius*2f);
...
}
protected override void Draw(GameTime gameTime)
{
// Clear to background color
...
mUWBLogo.Draw();
mBall.Draw();
// Print out text message to echo status
FontSupport.PrintStatus("Ball Position:" + mBall.Position, null);
FontSupport.PrintStatusAt(mUWBLogo.Position,
mUWBLogo.Position.ToString(), Color.White);
FontSupport.PrintStatusAt(mBall.Position, "Radius" + mBall.Radius, Color.Red);
...
}
Build and run the program. The output should look similar to Figure 3-11. Although the program should run, upon using it you’ll notice many unintended quirks. For example, if the size of the ball is changed when near the window’s boundary, the ball will get stuck on the boundary. Additionally, sometimes the ball will travel outside the window’s bounds and essentially disappear. You can circumvent this by creating a new ball when the A button is pressed; however, this is not ideal. To understand and solve these problems, you need to delve into some mathematics and physics (which we will discuss in the next chapter).
Simple Game State
Now that you have successfully added simple behaviors to the TexturedPrimitive class, it is time to explore how to support simple game-like interactions, or a game state. However, before digging into the details of creating a project with a game state, we’ll first help organize your thoughts.
Thus far, you have been modifying Game1.cs to handle the initializing, loading, updating, and drawing of your project. Ideally, it is a good practice to avoid using Game1.cs directly to prevent complexity in functions like initialization or draw functions, which can cause wasted debugging time further down the road. Additionally, a new class is needed to assist in the management of the game state.
You can isolate your interaction with Game1.cs and handle the management of the game state by creating a separate class that acts as an interface between you and Game1.cs. We will refer to this class as MyGame. The MyGame game state class will contain the Update() and Draw() functions that that will handle the updating and drawing of every object in your game. These two functions provide you with the functionality needed from Game1.cs for your game. Another aspect worth noting from the previous project (Simple Game Object) is the practice of subclassing. In that project, you subclassed the SoccerBall class from TexturedPrimitive to take advantage of the predefined behavior. Doing this afforded you the functionality of the TexturedPrimitive class and the ability to add new or specialized behaviors to SoccerBall. The subclassing not only avoided unnecessary code, but also reduced the potential of future bugs and the need to refactor your code. It is important to grasp this concept, because you will encounter it many times throughout the remainder of your time as a developer, as well as throughout the remainder of this book.
The Simple Game State project
This project demonstrates how to move a character or hero around the screen to collect (or net) basketballs. The basketballs will be generated randomly and continuously increase in size over time. Once the basketballs have reached a certain size, they will explode. Netting enough basketballs before they explode will give you the win, and allowing too many basketballs explode will make you lose. Figure 3-13 shows the project running.
Figure 3-13. Running the Simple Game State project
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Add the following resources, which can be found in the Chapter03SourceCodeImagesAndFontsUsed folder, into your content project before you begin:
Adding TexturedPrimitive collision detection support
Before creating new objects and defining a game state, open the TexturedPrimitive class and add the following code. The PrimitivesTouches() function detects whether two TexturedPrimitive objects overlap in space. This is accomplished by comparing the distance between the two objects’ positions against half of the combined width of both objects. A straightforward way of understanding this function is to notice that it is using a simple comparison of the distance between the objects, while accounting for their size, to see if the two objects overlap.
public bool PrimitivesTouches(TexturedPrimitive otherPrim)
{
Vector2 v = mPosition - otherPrim.Position;
float dist = v.Length();
return (dist < ((mSize.X / 2f) + (otherPrim.mSize.X / 2f)));
}
private const float kIncreaseRate = 1.001f;
private Vector2 kInitSize = new Vector2(5, 5);
Private const float kFinalSize = 15f;
public BasketBall() : base("BasketBall")
{
mPosition = Camera.RandomPosition();
mSize = kInitSize;
}
public bool UpdateAndExplode()
{
mSize *= kIncreaseRate;
return mSize.X > kFinalSize;
}
That is all that is needed for the BasketBall class. Listing 3-3 shows a full code listing of the BasketBall class.
Listing 3-3. The complete BasketBall class
public class BasketBall : TexturedPrimitive
{
// Change current position by this amount
private const float kIncreaseRate = 1.001f;
private Vector2 kInitSize = new Vector2(5, 5);
private const float kFinalSize = 15f;
public BasketBall() : base("BasketBall")
{
mPosition = Camera.RandomPosition();
mSize = kInitSize;
}
public bool UpdateAndExplode()
{
mSize *= kIncreaseRate;
return mSize.X > kFinalSize;
}
}
Creating the game state object
// Hero stuff ...
TexturedPrimitive mHero;
Vector2 kHeroSize = new Vector2(15, 15);
Vector2 kHeroPosition = Vector2.Zero;
// Basketballs ...
List<BasketBall> mBBallList;
TimeSpan mCreationTimeStamp;
int mTotalBBallCreated = 0;
// this is 0.5 seconds
const int kBballMSecInterval = 500;
// Game state
int mScore = 0;
int mBBallMissed=0, mBBallHit=0;
const int kBballTouchScore = 1;
const int kBballMissedScore = -2;
const int kWinScore = 10;
const int kLossScore = -10;
TexturedPrimitive mFinal = null;
Now it’s time to add the constructor, update, and draw functions.
public MyGame()
{
// Hero ...
mHero = new TexturedPrimitive("Me", kHeroPosition, kHeroSize);
// Basketballs
mCreationTimeStamp = new TimeSpan(0);
mBBallList = new List<BasketBall>();
}
public void UpdateGame(GameTime gameTime)
{
#region Step a.
if (null != mFinal) // Done!!
return;
#endregion Step a.
...
}
public void UpdateGame(GameTime gameTime)
{
...
#region Step b.
// Hero movement: right thumb stick
mHero.Update(InputWrapper.ThumbSticks.Right);
// Basketball ...
for (int b = mBBallList.Count-1; b >= 0; b--)
{
if (mBBallList[b].UpdateAndExplode())
{
mBBallList.RemoveAt(b);
mBBallMissed++;
mScore += kBballMissedScore;
}
}
#endregion Step b.
...
}
public void UpdateGame(GameTime gameTime)
{
...
#region Step c.
for (int b = mBBallList.Count - 1; b >= 0; b--)
{
if (mHero.PrimitivesTouches(mBBallList[b]))
{
mBBallList.RemoveAt(b);
mBBallHit++;
mScore += kBballTouchScore;
}
}
#endregion Step c.
...
}
public void UpdateGame(GameTime gameTime)
{
...
#region Step d.
// Check for new basketball condition
TimeSpan timePassed = gameTime.TotalGameTime;
timePassed = timePassed.Subtract(mCreationTimeStamp);
if (timePassed.TotalMilliseconds > kBballMSecInterval)
{
mCreationTimeStamp = gameTime.TotalGameTime;
BasketBall b = new BasketBall();
mTotalBBallCreated++;
mBBallList.Add(b);
}
#endregion Step d.
...
}
public void UpdateGame(GameTime gameTime)
{
...
#region Step e.
// Check for winning condition ...
if (mScore > kWinScore)
mFinal = new TexturedPrimitive("Winner",
new Vector2(75, 50), new Vector2(30, 20));
else if (mScore < kLossScore)
mFinal = new TexturedPrimitive("Looser",
new Vector2(75, 50), new Vector2(30, 20));
#endregion Step e.
}
Although it’s not trivial, you can usually divide the update function of the game state into distinct logical steps according to game rules. The five steps just described are (1) check whether an update is necessary, (2) update all game objects, (3) handle interactions among all game objects, (4) spawn new enemies, and (5) check for a win-or-loss condition. These steps can serve as a template for many simple games.
public void DrawGame()
{
mHero.Draw();
foreach (BasketBall b in mBBallList)
b.Draw();
if (null != mFinal)
mFinal.Draw();
// Drawn last to always show up on top
FontSupport.PrintStatus("Status: " +
"Score=" + mScore +
" Basketball: Generated( " + mTotalBBallCreated +
") Collected(" + mBBallHit + ") Missed(" + mBBallMissed + ")", null);
}
Modifying Game1 to support the game state
The last task you need to accomplish is to initialize and connect the game state (MyGame) in Game1.cs. As shown in the following code, you can easily achieve this by declaring a new MyGame object, initializing it in the LoadContent function, updating it in the update function of Game1, and drawing it in the draw function. In addition, notice that we added the ability to start a new game by instantiating a new MyGame object if the A button is pressed.
public class Game1 : Game
{
...
MyGame mTheGame;
public Game1()
{
...
}
protected override void LoadContent()
{
...
mTheGame = new MyGame();
}
protected override void Update(GameTime gameTime)
{
...
mTheGame.UpdateGame(gameTime);
if (InputWrapper.Buttons.A == ButtonState.Pressed)
mTheGame = new MyGame();
...
}
protected override void Draw(GameTime gameTime)
{
...
mTheGame.DrawGame();
...
}
}
Your game state project is now complete. Try building and running the program. It should look similar to Figure 3-13, shown previously. Remember to test the program by winning, losing, and starting a new game to verify the behaviors of the game.
Summary
In this chapter, you have learned many of the basic graphics and effects principles needed to create your own customized game. This includes modifying the game window, creating textured primitives, and outputting text to the game window.
Another important concept that was touched on was the user-defined coordinate system. The user-defined coordinate system provides the flexibility and functionality needed to properly control what the user sees of the game world. It also provides the customization needed for a wide variety of games.
Lastly, you have seen what is required to provide an object with simple behavior and your game with a simple game state. Game state and object behavior are critical pieces needed to create a game from both technical and design standpoints. The way an object behaves in the game world and how it interacts with the user and game state is often referred to as gameplay. You will see how to create more advanced object behavior and game states in the upcoming chapters.
Quick Reference
To |
Do this |
---|---|
Change the game window size | Modify mGraphics.PreferredBackBufferWidth and mGraphics.PreferredBackBufferHeight. Remember to call the mGraphics.ApplyChanges() function to activate the window size change. |
Change the window to a full-screen display | Set mGraphics.IsFullScreen to either true or false. Once again, remember to call the mGraphics.ApplyChanges() function to activate the change. |
Draw and interact with multiple copies of the same graphical object | Define a class to abstract the desired behaviors—for example, the TexturedPrimitive class. Remember that it is convenient to define both Update() and Draw() functions for your class. These functions can be called from the Update() and Draw() functions for updating and drawing of your objects. |
Work with your own game object class | 1. Define instance variables to represent your game objects. 2. Instantiate and initialize the game objects in LoadContent() . 3. Poll the user and change the game objects in Update() . 4. Draw the game objects in Draw() . |
Work with the user-defined coordinate system | Allow the user to define the desired coordinate space by specifying the position for the lower-left corner and the width that the game window represents. Define functions to transform the user-defined coordinate space to pixel coordinate space by translating, scaling, and flipping the y-axis. In the case of this chapter, this functionality is defined in the Camera class. The SetCameraWindow() function allows the user to define the coordinate system. The ComputePixelPosition() and ComputePixelRectangle() functions implement the transform of positions and rectangles from user-defined to pixel coordinate space. |
Draw in the user-defined coordinate system | 1. Define a coordinate space by calling the Camera.SetCameraWindow() function. 2. Define all game objects in the defined coordinate space. 3. Before drawing a game object, remember to call the Camera.ComputePixelRectangle() function to transform from user-defined to pixel coordinate space. |
Load a font into the project | Follow the same steps as loading an image: right-click over the ContentProject and locate the font file. In general, these are the same steps to follow to load any external files or resources into your project. |
Draw fonts in your game | You can work with the FontSupport class and call the PrintStatus() function to output text to the top-left corner of the game window, or call the PrintStatusAt() function to print text to a user-defined coordinate system position. |
Define specific behavior for a game object | One convenient approach is to define a subclass from the TexturedPrimitive class: 1. Add a new file into the project. 2. Implement the behavior (for instance, by changing the Update() function). 3. Reuse the Draw() function if possible. 4. Work with your new class. |
Avoid complex code in the Game1.cs file | Create a separate GameState object to create, maintain, and control your own game state. |
Implement the Update() function of your GameState object | Follow these simple steps: 1. Check for the need to update (for example, for pausing or ending the game). 2. Call the Update() function for all game objects. 3. Handle interactions among all relevant game objects. 4. Spawn new enemies if necessary. 5. Check for a win-or-loss condition. |