Since the Tic-Tac-Toe example was insufficient to continue our exploration of immersive characters, we will need to create a new workspace. I want to be free to explore the concepts of emotive expression, not constrained by game rules, designs, pre-conceptions, etc. This limits us to creating a novel game space and novel game characters, and that means we need to explore how to create a game space and game characters. What follows in this chapter is an introduction to basic video game engine and gameplay code that will enable us to move on in our pursuit of better VGAI.
This section introduces the wonderful and magical world of 2D sprites. A sprite is an image or animation that is integrated into a larger scene. In the early days of the video-game industry, 2D sprites were often used to give the illusion of 3D rendering or used in conjunction with hardware to speed up graphics rendering.
To pull this off, we need to be on the same page with a few concepts. First, I’ve defined the term sprite (refer to the preceding paragraph), and I will use that term almost exclusively from now on. Second, a sprite sheet is simply a graphics file that can be indexed conceptually, thus allowing a single file to contain all the necessary images to draw a sprite in any position. An example of a sprite sheet is shown in Figure 3.1. We can animate a sprite using this sprite-sheet approach by displaying the first figure on the left, waiting the appropriate amount of time, displaying the middle image, waiting again, and finally displaying the figure on the right. If the animation needs to loop or play again, we simply start over with the figure on the left.
However, we can also animate sprites by simply moving the figure’s position on the screen. This will not provide us with a custom movement (like the “walk animation” in Figure 3.1), but will allow the sprite to move around the screen. Therefore, animation of 2D sprites can be accomplished by “walking” the sprite sheet to display the right image at the right time to present the illusion of a moving figure, or through the use of procedural animation techniques (i.e., moving the image around the screen programmatically).
In modern 3D rendering environments, 2D sprites are used to optimize rendering and for special effects. The sprites are often referred to as billboards in a 3D environment, because the polygons on which they are displayed (often flat rectangles) are positioned so that they always face the player, just as billboards near a freeway are positioned to always face the driver. On most modern video cards, the functionality of creating pure 2D art assets no longer exists. Instead, 2D images are simply billboarded, often by the programmer. XNA, however, provides an encapsulated process for creating billboard graphics, and we will create our examples using these methods.
Valued Reader, I can sense a looming question in your mind. You want to know why I don’t simply create a 3D game. It’s a simple question with several simple answers, not the least of which being that I believe understanding a 2D graphics system is much easier for a non-graphics guru (like me!), and the concepts we will be exploring directly scale to 3D. Add to that the fact that when I look outside my window, I see an ephemeral image of an old man in brown robes, and he says to me, “Use 2D, Luke!” Probably the best answer, however, is that XNA encapsulates all the 3D graphics techniques that we would have to use to make a 2D game using any other language. Because of that simplification, we can pretty much ignore all the heavy graphics and focus on the subject of the book: making characters feel like characters.
We will discuss how to animate sprites in various sections of this chapter and will discuss sprite sheets in more detail in Chapter 5, “Gidgets, Gadgets, and Gizbots.”
The first thing we should talk about in this section is a pretty major architecture decision. In order to make the background art move, we could implement a fixed camera and move your environment in front of the camera, or we could fix the environment assets in place and cause the camera to move. This boils down to the question of whether we want to keep track of the camera position as it moves through the environment space, or whether we want to keep track of the asset positions.
At first glance, it might seem simpler to fix the art and move the camera, but certain ugly issues rear their heads. For instance, if we want to loop our background images to provide an “infinite space” feel, we would have to move the camera all the way to edge of our asset chain, and then quickly move the camera and all mobile assets back to the first stage (see Figure 3.2). There are certainly ways to do this, but it is complicated and a not very pretty solution.
Instead, we will implement a scrolling background architecture. We will fix the camera in place and move the background past the camera by changing the texture’s position in every update cycle. When we add mobile assets (mobs) to the picture in the next section, we will add some minor complications in tracking and control, but we will address those later.
We will begin development of the scrolling background architecture by creating a project called SpriteEnvironment in XNA. Our next step will be to create a public sprite class that is specialized for a scrolling background, called BackgroundSprite.cs.
What we are doing here is not VGAI, but it is important that we learn how to perform tasks like moving sprites around in a simple system so that when we move on to adding VGAI, we can focus exclusively on the VGAI aspects rather than trying to figure out this animation and gameplay stuff, and all the confusing AI stuff at the same time. Plus, this will be fun!
There are certain members any sprite object will likely need: the sprite texture, a tint color, a scale, a position, and a rotation. In addition, a background sprite also needs a speed, a direction, and possibly a z-depth value (although we will not use z-depth in these examples). Add those members to your class and include properties for the texture, position, scale, direction, speed, and tint. In other words, do this:
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace SpriteEnvironment { /// <summary> /// This class represents a billboard (2D sprite) that will scroll in a given /// direction at a given speed. /// </summary> public class BackgroundSprite { #region members private Texture2D spriteTexture; //the texture to display private Rectangle srcRectangle; //a rectangle used in our Draw() call that represents where inside the texture to copy the pixels from private Vector2 position; //a point that represents the upper-left corner of the sprite private float scale; //a scale percentage private float rotation; //degrees of rotation private Vector2 direction; //a vector represting the direction of the scrolling texture private float speed; //the speed at which the texture scrolls private float depth; //the z-depth of the texture private Color tintColor; //the color to tint the sprite with
#endregion #region properties public Texture2D Texture { get { return spriteTexture; } } public Vector2 Position { get { return position; } set { position = value; } } public float Scale { get { return scale; } set { scale = value; } } public Vector2 Direction { get { return direction; } set { direction = value; } } public float Speed { get { return speed; } set { speed = value; } } public Color Tint { set { tintColor = value; } } #endregion
Our class needs a general constructor, like this one:
#region constructors / init /// <summary> /// This constructor method initializes the mandatory attributes for the /// class /// </summary> /// <param name="text">The Texture2D object to skin the sprite with</param> /// <param name="pos">The starting position of the sprite</param> /// <param name="sc">The initial scale of the sprite's texture</param>
/// <param name="spd">The initial speed with which the sprite will scroll /// </param> /// <param name="dir">The initial direction in which the sprite will /// scroll</param> public BackgroundSprite(Texture2D text, Vector2 pos, float sc, float spd, Vector2 dir) { spriteTexture = text; position = pos; scale = sc; speed = spd; direction = dir; srcRectangle = new Rectangle(0, 0, spriteTexture.Width, sprite Texture. Height); rotation = 0f; depth = 0f; tintColor = Color.White; } #endregion
All that’s left is two methods: Update()
and Draw()
. Both are simple. The Update()
method needs only to update the position of the texture based on the direction and speed attributes in conjunction with the elapsed game time. The Draw()
method will call a version of the spriteBatch.Draw()
method. The code should look like this:
#region utilities /// <summary> /// Updates the position of the sprite by multiplying the direction vector, /// the speed scalar value, and the elapsed time /// </summary> /// <param name="gametime"></param> public void Update(GameTime gametime) { Position += direction * speed * (float)gametime.ElapsedGameTime. TotalSeconds; } /// <summary> /// Draws the sprite /// </summary> /// <param name="sb"></param> public void Draw(SpriteBatch sb)
{ sb.Draw(spriteTexture, position, srcRectangle, tintColor, rotation, Vector2.Zero, scale, SpriteEffects.None, depth); } #endregion } }
Voilà. That was certainly simple! All that’s left is to create some art assets and modify the Game1.cs code to implement the scrolling background. I want to create a layered scrolling background to represent outer space and to provide a feeling of distance. I need a texture that represents an interior view of a spaceship with a window in the hull (see Figure 3.3), multiple textures representing stars at various distances (see Figures 3.4–3.12), and finally some textures to represent objects very far away—i.e., other galaxies (see Figures 3.13 and 3.14). Why space? Simply because it’s pretty easy to create the artwork!
Why so many images in each layer? In this case, the effect we are trying to create is that of a seamlessly scrolling image. To do that, we need to be able to start showing the next image as soon as part of the current image begins to scroll off the screen. Since each of the background images is the actual width of the screen, ignoring any procedural scaling we may throw at it (see that, Valued Reader? Foreshadowing in a textbook!), we could conceivably get away with just two images for each field. As you will see, however, that gets pretty monotonous, not to mention that it becomes obvious that you are simply scrolling two images over and over. And we don’t use an animated sprite in the form of a sprite sheet here because we need to handle the case in which the last image begins to scroll off the screen, and we need to start drawing the first image again; we’d have to have two copies of it anyway to overcome this issue.
Let’s start small and easy. Begin by adding just the ship over a static black background field. The latter part is easy; just edit out the cornflower blue color from the first line of the default Draw()
method in Game1.cs as we’ve done before. It bears repeating that cornflower blue is simply the work of the devil.
Now, on to the fun part (or small part, if you prefer). We need to instantiate a few members into Game1, namely a BackgroundSprite
object for the ship, a local instance of the Random
class, and a default scale value. Make the changes in bold below:
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; BackgroundSprite Ship; Random rand; float scaler = 0.55f;
With that accomplished, we can move on to the Game1 constructor to set the screen size to match our art assets and to instantiate the Random
object. The code is simple and should look like this:
public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; //control the size of the game window--1024x768 graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; rand = new Random(); }
The next step is to instantiate the BackgroundSprite
object we are storing the ship in. We’ll do that in the LoadContent()
method thusly:
protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); //the ship background environment, which doesn't scroll Ship = new BackgroundSprite (Content.Load<Texture2D>("Ship"), new Vector2(0, 0), 1.0f, 0.0f, Vector2.Zero); }
All we are doing here is loading the texture for the ship layer, giving it a position of (0,0), a scale of 1.0, a speed of 0.0 (we will not actually move this layer), and a direction vector of (0,0). Since this layer will be static (it will always be where we just put it), we don’t need any changes to the Update()
method of Game1. Inside Draw()
, however, we need to set up the spriteBatch
object in order to pass it in to the BackgroundSprite
object’s Draw()
method. The code will look like this:
protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(); //draw the ship Ship.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); }
Compile it and run it. I know, I know, a boring, non-scrolling background. Let’s make it more interesting by adding a layer of stars. We’ll use the near star field and add a scrolling background. To do so, we need a BackgroundSprite
array to represent the star field. We will need to instantiate the array and each object in the array, update them, and then draw them. In the member list above the ship variable, add a BackgroundSprite
array declaration:
BackgroundSprite[] NearStarLayer; BackgroundSprite Ship;
Next, inside the Initialize()
method, we need to instantiate the array:
protected override void Initialize() { //initialize the background sprite arrays NearStarLayer = new BackgroundSprite[3]; base.Initialize(); }
Instantiating the objects inside the array gets a little more complicated. Again, we will handle this inside the LoadContent()
method, and we will add some local variables to help. We will also finally make the Random
object we’ve been carrying earn its pay.
protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); // local helper variables (so we don't have to retype these values // eighty-billion times while tuning) float nearSpd = 9.0f; Vector2 thatWay = new Vector2(-180,0); int r = rand.Next(100); //the nearest "stars" layer--wonder if we can get an autograph or two? NearStarLayer[0] = new BackgroundSprite(Content.Load<Texture2D> ("StarsNear1"), new Vector2(0, 0), 1.0f, nearSpd, thatWay); //randomize the scale value to provide some variance in the layer NearStarLayer[0].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); NearStarLayer[1] = new BackgroundSprite(Content.Load<Texture2D> ("StarsNear2"), new Vector2(1024, 0), 1.0f, nearSpd, thatWay); NearStarLayer[1].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); NearStarLayer[2] = new BackgroundSprite(Content.Load<Texture2D> ("StarsNear3"), new Vector2(2048, 0), 1.0f, nearSpd, thatWay); NearStarLayer[2].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); //the ship background environment, which doesn't scroll Ship = new BackgroundSprite(Content.Load<Texture2D>("Ship"), new Vector2(0, 0), 1.0f, 0.0f, Vector2.Zero); }
You can see that we added a local variable named nearSpd
. We do this to make tuning easier; we can set this value once and then make changes in a single place to affect the speed of all NearStarLayer
objects. We do the same thing with the direction vector we will pass in. We are using the Random
object to do some scale-fu to make the scrolling background less predictable. To avoid very tiny scales, we simply use the scaler
variable defined in our member list as a minimum size by taking the maximum value between the random value we pull out of the Random
member and the scaler
value as defined. In other words, the scaler
value is the minimum scale limit.
You can see that the textures are arranged such that the left edge of the next texture is one pixel to the right of the right edge of the current texture. To update the movement of these textures, we move each one by the same amount in the left direction relative to the screen. We will do this in the Update()
method by walking through the array and calling the object’s Update()
method directly. The only special case we have to handle is the case in which the last image in the array is moving such that it no longer covers the entire screen (e.g., it has moved one step to the left after covering the whole screen). We handle that by grabbing the first image in the array and positioning it one pixel to the right of the last image’s right edge. We will also perform the same trick with the scale as we did at initialization. This is what the code looks like:
protected override void Update(GameTime gameTime) { int r = rand.Next(100); //step through all of the "near star" sprites and move them for (int i = 0; i < 3; i++) { //check to see if the sprite is completely off the screen if (NearStarLayer[i].Position.X < -(NearStarLayer[i].Texture.Width * NearStarLayer[i].Scale)) { // find the sprite that should be immediately to the left (it isn't, // it's already moved to the right side of the screen) int refVal = i - 1; //make 0 wrap back to 2 if (refVal < 0) { refVal = 2; } //create a new position for this sprite--immediately to the right of //the refVal sprite Vector2 newPos = new Vector2(); newPos.X = NearStarLayer[refVal].Position.X + (NearStarLayer [refVal].Texture.Width * NearStarLayer[refVal].Scale); newPos.Y = NearStarLayer[i].Position.Y; NearStarLayer[i].Position = newPos; //create a new random scale for the sprite
NearStarLayer[i].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); } //update the object itself NearStarLayer[i].Update(gameTime); } base.Update(gameTime); }
All that’s left is to call the BackgroundSprite.Draw()
method for each object in the array. This is as easy as walking through the array:
protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(); //draw the background layers--ORDER IS IMPORTANT: farthest first! for (int i = 0; i < 3; i++) { NearStarLayer[i].Draw(spriteBatch); } Ship.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); }
Compile it and run it. At least this version is a bit more fun! To see the final version in all its layered-up glory, repeat the steps we performed to add the near layer, the middle layer, the far layer, and the galaxy layer. To cut down on the “Oh gee, there’s that same galaxy again” factor, I added some blank layers between the galaxy images. That, coupled with the scaling we are already doing, makes it okay for our purposes here. We can get as fancy-schmancy as we want with unlimited resources and an artist!
Here’s the code for the final version. Play around with the timing and make it look right to you.
using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace SpriteEnvironment { /// <summary> /// This is the main type for your game /// </summary> public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; //our scrolling sprites BackgroundSprite[] GalaxyLayer; BackgroundSprite[] FarStarLayer; BackgroundSprite[] MidStarLayer; BackgroundSprite[] NearStarLayer; BackgroundSprite Ship; //a few helpers Random rand; float scaler = 0.55f; public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; //control the size of the game window--1024x768 graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; //initialize the Random instance rand = new Random(); } /// <summary> /// Allows the game to perform any initialization it needs to before /// starting to run. /// This is where it can query for any required services and load any /// non-graphic
/// related content. Calling base. Initialize will enumerate through any /// components /// and initialize them as well. /// </summary> protected override void Initialize() { //initialize the background sprite arrays GalaxyLayer = new BackgroundSprite[5]; FarStarLayer = new BackgroundSprite[3]; MidStarLayer = new BackgroundSprite[3]; NearStarLayer = new BackgroundSprite[3]; base.Initialize(); } /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); //local helper variables (so we don't have to retype these values //eighty-billion times while tuning) float galSpd = 0.75f; float farSpd = 1.0f; float midSpd = 4.0f; float nearSpd = 9.0f; Vector2 thatWay = new Vector2(-180, 0); Texture2D blank = Content.Load<Texture2D>("blank1024_768"); int r = rand.Next(100); //the farthest away layer GalaxyLayer[0] = new BackgroundSprite(Content.Load<Texture2D> ("Galaxy1"), new Vector2(0, 0), 1.0f, galSpd, thatWay); //randomize the scale value to provide some variance in the layer GalaxyLayer[0].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); GalaxyLayer[1] = new BackgroundSprite(blank, new Vector2(1024, 0), 1.0f, galSpd, thatWay); GalaxyLayer[1].Scale = Math.Max((float)(r) / 100.0f, scaler);
r = rand.Next(100); GalaxyLayer[2] = new BackgroundSprite(Content.Load<Texture2D> ("Galaxy2"), new Vector2(2048, 0), 1.0f, galSpd, thatWay); GalaxyLayer[2].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); GalaxyLayer[3] = new BackgroundSprite(blank, new Vector2(3073, 0), 1.0f, galSpd, thatWay); GalaxyLayer[3].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); GalaxyLayer[4] = new BackgroundSprite(blank, new Vector2(4096, 0), 1.0f, galSpd, thatWay); GalaxyLayer[4].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); //the farthest "stars" layer FarStarLayer[0] = new BackgroundSprite(Content.Load<Texture2D> ("StarsFar1"), new Vector2(0, 0), 1.0f, farSpd, thatWay); //randomize the scale value to provide some variance in the layer FarStarLayer[0].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); FarStarLayer[1] = new BackgroundSprite(Content.Load<Texture2D> ("StarsFar2"), new Vector2(1024, 0), 1.0f, farSpd, thatWay); FarStarLayer[1].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); FarStarLayer[2] = new BackgroundSprite(Content.Load<Texture2D> ("StarsFar3"), new Vector2(2048, 0), 1.0f, farSpd, thatWay); FarStarLayer[2].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); //the middle distant "stars" layer MidStarLayer[0] = new BackgroundSprite(Content.Load<Texture2D> ("StarsMid1"), new Vector2(0, 0), 1.0f, midSpd, thatWay); //randomize the scale value to provide some variance in the layer MidStarLayer[0].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); MidStarLayer[1] = new BackgroundSprite(Content.Load<Texture2D> ("StarsMid2"), new Vector2(1024, 0), 1.0f, midSpd, thatWay); MidStarLayer[1].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); MidStarLayer[2] = new BackgroundSprite(Content.Load<Texture2D> ("StarsMid3"), new Vector2(2048, 0), 1.0f, midSpd, thatWay); MidStarLayer[2].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100);
//the nearest "stars" layer--wonder if we can get an autograph or two? NearStarLayer[0] = new BackgroundSprite(Content.Load<Texture2D> ("StarsNear1"), new Vector2(0, 0), 1.0f, nearSpd, thatWay); //randomize the scale value to provide some variance in the layer NearStarLayer[0].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); NearStarLayer[1] = new BackgroundSprite(Content.Load<Texture2D> ("StarsNear2"), new Vector2(1024, 0), 1.0f, nearSpd, thatWay); NearStarLayer[1].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); NearStarLayer[2] = new BackgroundSprite(Content.Load<Texture2D> ("StarsNear3"), new Vector2(2048, 0), 1.0f, nearSpd, thatWay); NearStarLayer[2].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); //the ship background environment, which doesn't scroll Ship = new BackgroundSprite(Content.Load<Texture2D>("Ship"), new Vector2(0, 0), 1.0f, 0.0f, Vector2.Zero); } /// <summary> /// UnloadContent will be called once per game and is the place to unload /// all content. /// </summary> protected override void UnloadContent() { // TO DO: Unload any non ContentManager content here } /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { int r = rand.Next(100); //step through all of the "galaxy" sprites and move them for (int i = 0; i < 5; i++) { //check to see if the sprite is completely off the screen
if (GalaxyLayer[i].Position.X < -(GalaxyLayer[i].Texture.Width * GalaxyLayer[i].Scale)) { //find the sprite that should be immediately to the left (it //isn't, it's already moved to the right side of the screen) int refVal = i - 1; //make 0 wrap back to 4 if (refVal < 0) { refVal = 4; } //create a new position for this sprite--immediately to the //right of the refVal sprite Vector2 newPos = new Vector2(); newPos.X = GalaxyLayer[refVal].Position.X + (GalaxyLayer [refVal].Texture.Width * GalaxyLayer[refVal].Scale); newPos.Y = GalaxyLayer[i].Position.Y; GalaxyLayer[i].Position = newPos; //create a new random scale for the sprite GalaxyLayer[i].Scale = Math.Max((float)(r) / 100.0f, scaler); //randomly assign a color if (r < 25) { GalaxyLayer[i].Tint = Color.Orchid; } else if (r < 50) { GalaxyLayer[i].Tint = Color.Orange; } else if (r < 75) { GalaxyLayer[i].Tint = Color.MediumAquamarine; } else if (r < 95) { GalaxyLayer[i].Tint = Color.LavenderBlush; } else { GalaxyLayer[i].Tint = Color.BlueViolet; } r = rand.Next(100);
} //update the object itself GalaxyLayer[i].Update(gameTime); } //step through all of the "far star" sprites and move them for (int i = 0; i < 3; i++) { //check to see if the sprite is completely off the screen if (FarStarLayer[i].Position.X < -(FarStarLayer[i].Texture. Width * FarStarLayer[i].Scale)) { //find the sprite that should be immediately to the left (it //isn't, it's already moved to the right side of the screen) int refVal = i - 1; //make 0 wrap back to 2 if (refVal < 0) { refVal = 2; } //create a new position for this sprite--immediately to the //right of the refVal sprite Vector2 newPos = new Vector2(); newPos.X = FarStarLayer[refVal].Position.X + (FarStar Layer[refVal].Texture.Width * FarStarLayer[refVal]. Scale); newPos.Y = FarStarLayer[i].Position.Y; FarStarLayer[i].Position = newPos; //create a new random scale for the sprite FarStarLayer[i].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); } //update the object itself FarStarLayer[i].Update(gameTime); } //step through all of the "middle star" sprites and move them for (int i = 0; i < 3; i++) { //check to see if the sprite is completely off the screen
if (MidStarLayer[i].Position.X < -(MidStarLayer[i].Texture. Width * MidStarLayer[i].Scale)) { //find the sprite that should be immediately to the left (it //isn't, it's already moved to the right side of the screen) int refVal = i - 1; //make 0 wrap back to 2 if (refVal < 0) { refVal = 2; } //create a new position for this sprite--immediately to the //right of the refVal sprite Vector2 newPos = new Vector2(); newPos.X = MidStarLayer[refVal].Position.X + (MidStarLayer [refVal].Texture.Width * MidStarLayer[refVal].Scale); newPos.Y = MidStarLayer[i].Position.Y; MidStarLayer[i].Position = newPos; //create a new random scale for the sprite MidStarLayer[i].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); } //update the object itself MidStarLayer[i].Update(gameTime); } //step through all of the "near star" sprites and move them for (int i = 0; i < 3; i++) { //check to see if the sprite is completely off the screen if (NearStarLayer[i].Position.X < -(NearStarLayer[i]. Texture.Width * NearStarLayer[i].Scale)) { //find the sprite that should be immediately to the left (it //isn't, it's already moved to the right side of the screen) int refVal = i - 1; //make 0 wrap back to 2 if (refVal < 0) { refVal = 2; }
//create a new position for this sprite--immediately to the //right of the refVal sprite Vector2 newPos = new Vector2(); newPos.X = NearStarLayer[refVal].Position.X + (NearStarLayer[refVal].Texture.Width * NearStarLayer [refVal].Scale); newPos.Y = NearStarLayer[i].Position.Y; NearStarLayer[i].Position = newPos; //create a new random scale for the sprite NearStarLayer[i].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); } //update the object itself NearStarLayer[i].Update(gameTime); } base.Update(gameTime); } /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(); //draw the background layers--ORDER IS IMPORTANT: farthest first! for (int i = 0; i < 5; i++) { GalaxyLayer[i].Draw(spriteBatch); } for (int i = 0; i < 3; i++) { FarStarLayer[i].Draw(spriteBatch); }
for (int i = 0; i < 3; i++) { MidStarLayer[i].Draw(spriteBatch); } for (int i = 0; i < 3; i++) { NearStarLayer[i].Draw(spriteBatch); } //draw the ship Ship.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } } }
Now that we all understand the basics of handling 2D sprites, let’s put things in context by building a very basic 2D sprite engine for the game we will be building throughout the rest of the book (which, of course, will ultimately allow us to explore the concepts of immersion, believability, and personality in character AI). To begin, we need to set up a new project called 2DSpriteGame in XNA. In this project, we will begin to organize the code base a bit, as it will grow to be pretty complicated by the time we are finished with it. As such, let’s create a directory structure as shown in Figure 3.15 (ignore the content of the directories in the image—that’s for later!).
You will note in Figure 3.15 that I’ve also imported a bunch of stuff from the SpriteEnvironment project we completed in the last section, namely all the artwork and the BackgroundSprite
class. Also, take note of the namespace. By default, Visual Studio will replace any numeric first character in a namespace with an underscore, which tends to really screw up the readability. Yeah, I could have named this project something else and avoided the problem entirely, but hey, I’m stubborn. Instead, right-click on the _DSpriteGame
part of the namespace, select Refactor, and then select Rename. Next, type 2DSpriteGame
and click OK.
Now, open BackgroundSprite.cs. You should see this as the namespace:
namespace SpriteEnvironment
Simply change it to this:
namespace _2DSpriteGame.Environment.Sprite
Whoa, Nelly! What’s with the periods and extra words? Why isn’t it just the following?
namespace _2DSpriteGame
The answer is because we’ve imposed a directory structure on the code, and that directory structure should be mirrored in the namespace. Simple, yeah? This also means we will have to add a few more using
statements in various places. We could cheat a bit and cut some of the using
statements mirroring only the top-level subdirectories in the namespace (e.g., making every class inside the Environment
directory a member of the _2DSpriteGame.Environment
namespace). We could also simply flatten the entire namespace by making every class inside the project a member of the _2DSpriteGame
namespace. Both of those approaches would be lazy, however. While the early bird gets the worm, a rolling stone can’t be lazy if it expects to be rewarded with Twinkies.
It would also be a good practice to not use a blanket using
statement to include the subdirectories. In other words, in C#, many coders build a rigorous namespace and then wave the magic “using
statement” wand to make it all disappear, which can make navigating through the code somewhat difficult. Obviously, this problem could be fixed by not including the subdirectory’s namespace, but that means any time you want to use a file in that subdirectory, you have to do something like:
_2DSpriteGame.Environment.Sprite.BackgroundSprite
Let’s face it: That’s just ugly and tedious. What’s the answer then? A namespace alias. Basically, the relationship between a namespace and a namespace alias is similar to the difference between an absolute path (e.g, C:TopDirectory MiddleDirectoryBottomDirectorFile.txt) and a relative path (e.g., Bottom-DirectoryFile.txt) in the file structure. In C#, using a namespace alias is almost like using one in C++ … but, of course, it has to be different.
We can create a namespace alias in C# by implementing a special kind of using
statement—in our case, something like this:
using Sprite = _2DSpriteGame.Environment.Sprite;
Of course, we could name the alias anything we want, but I prefer to name it after the subdirectory so I know exactly where to look when I see it in code. Speaking of which, if we do this alias-fu, it means that from within any class that implements the idea, the _2DSpriteGame.Environment.Sprite
level is accessible merely by adding sprite
. before the class name. For example, to get to our BackgroundSprite
class, we need to do this:
Sprite.BackgroundSprite
Yes, it’s more typing than if we use the magic-wand method, but any complicated code base is much easier to deal with if you decide to follow this namespace scheme.
Okay, kicking the soapbox aside…. Except for the change to the namespace, our BackgroundSprite
class is perfect the way we wrote it in the other project. However, we don’t want to put all the controller logic for the scrolling backgrounds into the Game1 class. Instead, let’s create a class called BackgroundManager.cs in the Environment folder (and the _2DSpriteGame.Environment
namespace).
This section will also not directly address VGAI. Instead, we are again setting the stage for later VGAI development. The concept of manager classes, which we are on the cusp of introducing to the code base, will be a huge help when we begin to add the VGAI. They act as central points of contact for the rest of the code in the project and contain all of the objects the manage in a single place, allowing us better, cleaner control of execution timing, etc.
Inside the class, we’ll have the usual suspects for using
statements, plus a namespace alias for the Sprite directory. It should look like this:
using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; namespace _2DSpriteGame.Environment {
Once that is set up, we need to re-create the members we added to the Game1.cs code in the SpriteEnvironment project, like this:
public class BackgroundManager { #region members private Sprite.BackgroundSprite[] GalaxyLayer; private Sprite.BackgroundSprite[] FarStarLayer; private Sprite.BackgroundSprite[] MidStarLayer; private Sprite.BackgroundSprite[] NearStarLayer; private Sprite.BackgroundSprite Ship; //helper members private Random rand;
I also want to be able to change the direction of the scrolling if need be. To accomplish that, add the following to the members list directly after the Random
declaration:
private Vector2 left = new Vector2(-180, 0); private Vector2 right = new Vector2(180, 0);
//constants private const float scaler = 0.55f; private const float galSpd = 0.75f; private const float farSpd = 1.0f; private const float midSpd = 4.0f; private const float nearSpd = 9.0f; #endregion
This class needs no properties for now, so the properties region should be left blank.
The intent of the class is to offload all the functionality we built into the Game1.cs class into the other project. To do that, the BackgroundManager
class needs to instantiate the arrays, instantiate all the BackgroundSprite
objects (which includes loading their content), and handle both the Update()
and Draw()
calls for all the objects. Fortunately, we’ve already figured out how to do all this; only minor updates are needed at this time to accommodate the new name for the left direction. Here’s the balance of the code:
#region properties //none necessary #endregion #region constructors / init public BackgroundManager() { //initialize the background sprite arrays GalaxyLayer = new Sprite.BackgroundSprite[5]; FarStarLayer = new Sprite.BackgroundSprite[3]; MidStarLayer = new Sprite.BackgroundSprite[3]; NearStarLayer = new Sprite.BackgroundSprite[3]; rand = new Random(); } public void LoadContent(ContentManager Content) { //local helper variables (so we don't have to retype these values //eighty-billion times while tuning) Texture2D blank = Content.Load<Texture2D>("Environment/blank1024_768"); int r = rand.Next(100); //the farthest-away layer
GalaxyLayer[0] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/Galaxy1"), new Vector2(0, 0), 1.0f, galSpd, left); //randomize the scale value to provide some variance in the layer GalaxyLayer[0].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); GalaxyLayer[1] = new Sprite.BackgroundSprite(blank, new Vector2(1024, 0), 1.0f, galSpd, left); GalaxyLayer[1].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); GalaxyLayer[2] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/Galaxy2"), new Vector2(2048, 0), 1.0f, galSpd, left); GalaxyLayer[2].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); GalaxyLayer[3] = new Sprite.BackgroundSprite(blank, new Vector2(3073, 0), 1.0f, galSpd, left); GalaxyLayer[3].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); GalaxyLayer[4] = new Sprite.BackgroundSprite(blank, new Vector2(4096, 0), 1.0f, galSpd, left); GalaxyLayer[4].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); //the farthest "stars" layer FarStarLayer[0] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/StarsFar1"), new Vector2(0, 0), 1.0f, farSpd, left); //randomize the scale value to provide some variance in the layer FarStarLayer[0].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); FarStarLayer[1] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/StarsFar2"), new Vector2(1024, 0), 1.0f, farSpd, left); FarStarLayer[1].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); FarStarLayer[2] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/StarsFar3"), new Vector2(2048, 0), 1.0f, farSpd, left); FarStarLayer[2].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); //the middle distant "stars" layer MidStarLayer[0] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/StarsMid1"), new Vector2(0, 0), 1.0f, midSpd, left); //randomize the scale value to provide some variance in the layer MidStarLayer[0].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100);
MidStarLayer[1] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/StarsMid2"), new Vector2(1024, 0), 1.0f, midSpd, left); MidStarLayer[1].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); MidStarLayer[2] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/StarsMid3"), new Vector2(2048, 0), 1.0f, midSpd, left); MidStarLayer[2].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); //the nearest "stars" layer--wonder if we can get an autograph or two? NearStarLayer[0] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/StarsNear1"), new Vector2(0, 0), 1.0f, nearSpd, left); //randomize the scale value to provide some variance in the layer NearStarLayer[0].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); NearStarLayer[1] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/StarsNear2"), new Vector2(1024, 0), 1.0f, nearSpd, left); NearStarLayer[1].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); NearStarLayer[2] = new Sprite.BackgroundSprite(Content.Load<Texture2D> ("Environment/StarsNear3"), new Vector2(2048, 0), 1.0f, nearSpd, left); NearStarLayer[2].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); //the ship background environment, which doesn't scroll Ship = new Sprite.BackgroundSprite(Content.Load<Texture2D>("Environment/ Ship"), new Vector2(0, 0), 1.0f, 0.0f, Vector2.Zero); } #endregion #region utility methods public void Update(GameTime gameTime) { int r = rand.Next(100); //step through all of the "galaxy" sprites and move them for (int i = 0; i < 5; i++) { //check to see if the sprite is completely off the screen if (GalaxyLayer[i].Position.X < -(GalaxyLayer[i].Texture.Width * GalaxyLayer[i].Scale)) { //find the sprite that should be immediately to the left (it isn't, //it's already moved to the right side of the screen)
int refVal = i - 1; //make 0 wrap back to 4 if (refVal < 0) { refVal = 4; } //create a new position for this sprite--immediately to the right of //the refVal sprite Vector2 newPos = new Vector2(); newPos.X = GalaxyLayer[refVal].Position.X + (GalaxyLayer[refVal]. Texture.Width * GalaxyLayer[refVal].Scale); newPos.Y = GalaxyLayer[i].Position.Y; GalaxyLayer[i].Position = newPos; //create a new random scale for the sprite GalaxyLayer[i].Scale = Math.Max((float)(r) / 100.0f, scaler); //randomly assign a color if (r < 25) { GalaxyLayer[i].Tint = Color.Orchid; } else if (r < 50) { GalaxyLayer[i].Tint = Color.Orange; } else if (r < 75) { GalaxyLayer[i].Tint = Color.MediumAquamarine; } else if (r < 95) { GalaxyLayer[i].Tint = Color.LavenderBlush; } else { GalaxyLayer[i].Tint = Color.BlueViolet; } r = rand.Next(100); } //update the object itself GalaxyLayer[i].Update(gameTime); }
//step through all of the "far star" sprites and move them for (int i = 0; i < 3; i++) { //check to see if the sprite is completely off the screen if (FarStarLayer[i].Position.X < -(FarStarLayer[i].Texture.Width * FarStarLayer[i].Scale)) { //find the sprite that should be immediately to the left (it isn't, //it's already moved to the right side of the screen) int refVal = i - 1; //make 0 wrap back to 2 if (refVal < 0) { refVal = 2; } //create a new position for this sprite--immediately to the right of //the refVal sprite Vector2 newPos = new Vector2(); newPos.X = FarStarLayer[refVal].Position.X + (FarStarLayer [refVal].Texture.Width * FarStarLayer[refVal].Scale); newPos.Y = FarStarLayer[i].Position.Y; FarStarLayer[i].Position = newPos; //create a new random scale for the sprite FarStarLayer[i].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); } //update the object itself FarStarLayer[i].Update(gameTime); } //step through all of the "middle star" sprites and move them for (int i = 0; i < 3; i++) { //check to see if the sprite is completely off the screen if (MidStarLayer[i].Position.X < -(MidStarLayer[i].Texture.Width * MidStarLayer[i].Scale)) { //find the sprite that should be immediately to the left (it isn't, //it's already moved to the right side of the screen) int refVal = i - 1; //make 0 wrap back to 2 if (refVal < 0)
{ refVal = 2; } //create a new position for this sprite--immediately to the right of //the refVal sprite Vector2 newPos = new Vector2(); newPos.X = MidStarLayer[refVal].Position.X + (MidStarLayer [refVal].Texture.Width * MidStarLayer[refVal].Scale); newPos.Y = MidStarLayer[i].Position.Y; MidStarLayer[i].Position = newPos; //create a new random scale for the sprite MidStarLayer[i].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); } //update the object itself MidStarLayer[i].Update(gameTime); } //step through all of the "near star" sprites and move them for (int i = 0; i < 3; i++) { //check to see if the sprite is completely off the screen if (NearStarLayer[i].Position.X < -(NearStarLayer[i].Texture.Width * NearStarLayer[i].Scale)) { //find the sprite that should be immediately to the left (it isn't, //it's already moved to the right side of the screen) int refVal = i - 1; //make 0 wrap back to 2 if (refVal < 0) { refVal = 2; } //create a new position for this sprite--immediately to the right of //the refVal sprite Vector2 newPos = new Vector2(); newPos.X = NearStarLayer[refVal].Position.X + (NearStarLayer [refVal].Texture.Width * NearStarLayer[refVal].Scale); newPos.Y = NearStarLayer[i].Position.Y; NearStarLayer[i].Position = newPos;
//create a new random scale for the sprite NearStarLayer[i].Scale = Math.Max((float)(r) / 100.0f, scaler); r = rand.Next(100); } //update the object itself NearStarLayer[i].Update(gameTime); } } public void Draw(SpriteBatch spriteBatch) { //draw the background layers--ORDER IS IMPORTANT: farthest first! for (int i = 0; i < 5; i++) { GalaxyLayer[i].Draw(spriteBatch); } for (int i = 0; i < 3; i++) { FarStarLayer[i].Draw(spriteBatch); } for (int i = 0; i < 3; i++) { MidStarLayer[i].Draw(spriteBatch); } for (int i = 0; i < 3; i++) { NearStarLayer[i].Draw(spriteBatch); } //draw the ship Ship.Draw(spriteBatch); } #endregion
To hook this new manager up to the Game1 class in this project, we just make a BackgroundManager
object:
Environment.BackgroundManager bgm;
Instantiate it:
protected override void Initialize() { bgm = new Environment.BackgroundManager(); base.Initialize(); }
Call its LoadContent()
method:
protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); bgm.LoadContent(Content); // TO DO: use this.Content to load your game content here }
And then call Update()
and Draw()
:
protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState. Pressed) this.Exit(); bgm.Update(gameTime); base.Update(gameTime); } /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); spriteBatch.Begin();
bgm.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); }
Bam! Working scrolling backgrounds inside our new and improved 2DSpriteGame engine.
We could continue down this merry path, adding all our new stuff to the Game1 class, but I can tell you, Valued Reader, that eventually, we will want some of these gadgets to be able to communicate with other gadgets (or even gidgets!) directly from within their own code. We can steal a popular OOP technique called a singleton to accomplish this. Basically, a singleton is a static instance of some class with a public static accessor method. This method restricts instantiation of a class to a single point and allows access of the instance from anywhere in the system.
Since we are going to need a GameStateManager
very shortly, we might as well create one and use it as our singleton. For now, the class is quite boring, and only holds an object of the BackgroundManager
class. We are going to change a bunch of stuff we just added in the Game1 class, but bear with me. The class is basic and looks like this:
using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace _2DSpriteGame.GameState { /// <summary> /// This class is a container for all manager objects used in the game. It will ///be stored as a singleton in the Game1 class and allows access to the manager ///objects through the use of properties. /// </summary> public class GameStateManager { #region members private Environment.BackgroundManager bgm; #endregion #region properties public Environment.BackgroundManager BGManager
{ get { return bgm; } } #endregion #region constructor / init public GameStateManager() { bgm = new Environment.BackgroundManager(); } public void LoadContent(ContentManager Content) { bgm.LoadContent(Content); } #endregion #region utilities public void Update(GameTime gameTime) { bgm.Update(gameTime); } public void Draw(SpriteBatch sb) { bgm.Draw(sb); } #endregion } }
It will be more useful later. Trust me, I’m a doctor.
In Game1, we will create the singleton like this:
#region members private GraphicsDeviceManager graphics; private SpriteBatch spriteBatch; //singleton member of this class for cross-class communication private static GameState.GameStateManager gsm; #endregion
#region properties public static GameState.GameStateManager GameStateMan { get { return gsm; } } #endregion
This will allow us to access the GameStateManager
object from anywhere in the system by making a call to Game1.GameStateMan
. That’s magic right there, as I’m sure you will agree shortly.
The rest of the Game1 class now needs to be updated; we will simply replace every method call to the old BackgroundManager
object to the same call in a GameStateManager
object—e.g., bgm.Update(gameTime);
becomes gsm.Update (gameTime);
.
Mr. Authorman, this is boring. We’ve already seen it.
Yes, Valued Reader, that is very true. Let’s do something new and improved (for only $19.99)! Let’s add a character class. In the beginning of this chapter, we wrote some code for a sprite sheet. Naturally, you might think I want you to import that code here, but you would be wrong.
Inside the Character directory, you should have a Sprite folder. Inside that folder, create a class called CharacterSprite
. The good news is that, with the exception of minor changes, this class will be very similar to the BackgroundSprite
class. The only real difference here comes from the functionality we added to allow the BackgroundSprite
to scroll. We will not allow the CharacterSprite
to do any kind of movement, and we can remove those bits from the class. Namely, we can remove the direction and speed members and their properties. Further, we can remove the Update()
method completely (trust me on this for now!). So the class looks like this:
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; namespace _2DSpriteGame.Character.Sprite { /// <summary> /// An implementation of a 2D sprite, specified for character objects within /// the game /// </summary> public class CharacterSprite
{ #region members private Texture2D spriteTexture; //the texture to display private Rectangle srcRectangle; //a rectangle used in our Draw() call that represents where inside the texture to copy the pixels from private Vector2 position; //a point that represents the upper-left corner of the sprite private float scale; //a scale percentage private float rotation; //degrees of rotation private float depth; //the z-depth of the texture private Color tintColor; //the color to tint the sprite with #endregion #region properties public Texture2D Texture { get { return spriteTexture; } } public Vector2 Position { get { return position; } set { position = value; } } public float Scale { get { return scale; } set { scale = value; } } public Color Tint { set { tintColor = value; } } #endregion #region constructors / init public CharacterSprite(Texture2D text, Vector2 pos, float sc) { spriteTexture = text; position = pos; scale = sc; srcRectangle = new Rectangle(0, 0, spriteTexture.Width, spriteTexture.Height);
rotation = 0f; depth = 0f; tintColor = Color.White; } #endregion #region utilities public void Draw(SpriteBatch sb) { sb.Draw(spriteTexture, position, srcRectangle, tintColor, rotation, Vector2.Zero, scale, SpriteEffects.None, depth); } #endregion } }
Great! We can now add a sprite texture to the game to represent any characters we want to add. Well, except for one small point: We have no art. Oh, and no character object. Technically, I guess we’re also missing a CharacterManager
. Okay, okay! We could add a sprite if we had one, though!
Let’s get to fixin’ the rest of those problems. I have created a sprite for us to use here. The name of this file is SumaFixed.png—it’s in the Content/Character folder for the project on the CD-ROM. The texture for the sprite is completely white so that we can add colors dynamically; however, I created a black version for inclusion here in Figure 3.16. That takes care of the no art problem … at least for now (insert maniacal laughter here).
Next, we should probably create a character gizbot. At the top level of the Character folder, create a public class called Character
. This class will be the logical storehouse and will also maintain all the presentation assets for any particular character we add to the game. As of right now, the only possible asset we can add to this class is a CharacterSprite
, but let’s not get all picky and stuff.
At the end of the day, a basic video-game character needs to keep track of a few things in its internal state: position, scale, a waypoint, movement speed, presentation assets, AI state, and attribute values. We will expand this as we need to, but now, it is sufficient (famous last promises soon to be broken).
For now, add position, scale, waypoint, and speed, both members and properties. We will come back to the “AI state” and attributes as we further define what it means to be a character in this game. For now, we will also add a CharacterSprite
object named idleBodyFixed
with no property. We also need a Color
object named bodyColor
, again with no property for now.
The class should look like this:
using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Content; namespace _2DSpriteGame.Character { /// <summary> /// An implementation of a single character for the game. This class will be /// the central storehouse for all assets and logical components for characters. /// </summary> public class Character { #region members //state attributes private Vector2 pos; private float scale; private Vector2 wp; private float speed; //assets private Sprite.CharacterSprite idleBodyFixed; private Color bodyColor; #endregion #region properties public Vector2 Position { get { return pos; } set { pos = value; } }
public float Scale { get { return scale; } set { scale = value; } } public Vector2 Waypoint { get { return wp; } set { wp = value; } } public float Speed { get { return speed; } set { speed = value; } } #endregion
Next, we need a constructor and the utility methods, Draw()
and Update()
. (See what I did there? I reversed those last two on you to keep it fresh! I’m good like that.) The constructor is really straightforward. We’re just going to pass in the values of the attributes we need here to get this basic class working: the texture, the starting position, the scale, the movement speed, and the tint color. It should look like this:
#region constructors / init public Character(Texture2D tex, Vector2 p, float s, float spd, Color bColor) { pos = p; scale = s; speed = spd; bodyColor = bColor; idleBodyFixed = new Sprite.CharacterSprite(tex, pos, scale); idleBodyFixed.Tint = bodyColor; } #endregion
The other utility methods are pretty easy to figure out as well. In Update()
, we need to update the position by first calculating the step size—i.e., multiplying the waypoint (destination), the speed, and the elapsed time—and then applying it properly in each axis. Once that is updated, we need to pass it down to the CharacterSprite
. That’s it. In Draw()
, we just need to pass the Draw()
call down to the CharacterSprite
, like this:
#region utility methods public void Update(GameTime gameTime) { //compute the size of the next step Vector2 step = Vector2.Normalize(Vector2.Subtract(pos, wp)) * speed * (float)gameTime.ElapsedGameTime.TotalSeconds; //if we are not within our "there" range, if ((Math.Max(2.0f, Math.Abs(pos.X - wp.X)) > 2.0f) || (Math.Max(2.0f, Math.Abs(pos.Y - wp.Y)) > 2.0f)) { pos -= step; } idleBodyFixed.Position = pos; } public void Draw(SpriteBatch sb) { idleBodyFixed.Draw(sb); } #endregion
That takes care of the basics for a character. We’ll be coming back to this class a lot, so get used to seeing it.
You have probably already guessed, and guessed correctly, that we will create a CharacterManager
class. In fact, the reader in the blue shirt is already happily coding away on his version—kudos to you, blue-shirted Valued Reader! Inside the Character folder, create the class and make it public.
We will use this manager class as a high-level container for every mobile character in the game. The architecture we are creating here relies on functionality as the primary segregation metric for each package. In other words, we are lumping like stuff with like stuff and further creating separation based on a functional hierarchy.
To get our game started, let’s build this class the same way we built the character class—with the bare essentials. Our container will simply be an array of Character
objects. We will need to restrict the update interval for action selection inside our characters until the appropriate amount of time has passed since the last action selection (otherwise, our characters will change behaviors too fast to be noticeable, much less believable), so we will also need some member variables to accomplish that. The last member we need is an object of the Random
class. We won’t need any properties, so we will create none:
using System; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; namespace _2DSpriteGame.Character { /// <summary> /// Initiates all the characters to be used in the game, updates the /// characters, and draws the character sprites in order. /// </summary> public class CharacterManager { #region members private Character[] toons; //container for the Character objects managed here //utility members private Random rand; private float deltaTime; //constants private const int NUM_TOONS = 10; private const float INTERVAL = 1000f / 2f; //this clamps changes to the waypoints to 2 fps #endregion #region properties //none needed #endregion
You can guess what’s coming next … the constructor:
#region constructors / init public CharacterManager() { toons = new Character[NUM_TOONS]; rand = new Random(); deltaTime = 0; }
public void LoadContent(ContentManager Content) { int y = rand.Next(248) + 410; //the walkable y-space in our environment int x = rand.Next(904) + 10; //the walkable x-space in our environment byte r = (byte)rand.Next(255); byte g = (byte)rand.Next(255); byte b = (byte)rand.Next(255); float spd = 150f; for (int i = 0; i < NUM_TOONS; i++) { toons[i] = new Character(Content.Load<Texture2D>("Character/ SumaFixed"), new Vector2(x, y), 1.0f, spd, new Color(r, g, b, 255)); y = rand.Next(248) + 410; //the walkable y-space in our environment x = rand.Next(904) + 10; //the walkable x-space in our environment r = (byte)rand.Next(255); g = (byte)rand.Next(255); b = (byte)rand.Next(255); } } #endregion
As you can see in the LoadContent()
method’s code, we did a lot of random initialization (and hence the Random
object). We took this step simply to make it easier to get something up and running on the screen. Eventually, we will modify this code pretty heavily to facilitate an actual game experience. For now, however, randomized values are our friends.
You might also notice that the values for the x,y coordinates we choose are not the entire screen. Basically, the ship art was constructed so that there is a floor (the darker gray) and a wall with a window (the lighter gray). The floor starts at pixel 500 on the y axis and spans the entire width. We want to keep the mobs from running right up to the edge, however, so we need to build in a 10 pixel margin. That effectively reduces our walkable area to 510–758 on the y axis and 10–1014 for the x axis. Beautiful, but the numbers I use add up to a range of 410–658 on the y axis and 10–914 on the x axis. What gives!? We have to adjust the range of the walkable area to compensate for the 100 × 100 pixel character texture.
As you know, Valued Reader, in XNA, (0,0) is in the upper-left corner. Once we add 100 to both the down direction (we do this so that we are constraining where the feet of the mob can go) and the right direction (to accommodate the fact that we are basing our position on the left side of the mob, so we need to be sure that the entire mob stays on the screen), we see that the effective range is 510–758 on the y axis and 10–1014 on the x axis.
Next up on the block are the two utility functions we always talk about: the Dread Pirate Update()
and his little friend Draw()
. In Update()
, we will be responsible for keeping track of the action selection interval, selecting new waypoints when necessary, completion and bounds checking, and finally, updating the individual characters. We add a bit of flair by allowing the character to stop (i.e., we only pick new waypoints 30 percent of the time). The code to do this looks like this:
#region utility methods public void Update(GameTime gameTime) { deltaTime += (float)gameTime.ElapsedGameTime.TotalMilliseconds; if (deltaTime > INTERVAL) { int y = rand.Next(248) + 410; //the walkable y-space in our environment int x = rand.Next(904) + 10; //the walkable x-space in our environment int chance = rand.Next(100); Vector2 dir = new Vector2(x, y); for (int i = 0; i < NUM_TOONS; i++) { if (chance < 30) { toons[i].Waypoint = dir; y = rand.Next(248) + 410; //the walkable y-space in our environment x = rand.Next(904) + 10; //the walkable x-space in our environment dir = new Vector2(x, y); } else
{ //stop for a while toons[i].Waypoint = toons[i].Position; } chance = rand.Next(100); } deltaTime = 0f; }
The next step in the update process is to perform completion and bounds checking (i.e., to validate the waypoint). We can accomplish this by comparing the current position of each mob to first the bounds of the walkable space (410–658 on the y axis and 10–914 on the x axis) and then by comparing the current position to the waypoint. We allow a threshold around the waypoint of plus or minus 2 pixels to count as arriving at the waypoint. This allows us to compensate for any variance created by when the test gets called.
If the mob is at one of the bounds, we will reverse the direction of the waypoint in the appropriate axis. If the mob has reached its destination, we will simply zero out the appropriate axis. After we have made the appropriate changes, we update the Character
object. The code for these tests follows:
Vector2 tPos; Vector2 wp; ; bool wpChanged; for (int i = 0; i < NUM_TOONS; i++) { //grab current position for checks tPos = toons[i].Position; wp = toons[i].Waypoint; wpChanged = false; //boundary checking if ((tPos.Y < 410) || (tPos.Y > 658)) { wp.Y *= -1; wpChanged = true; } if ((tPos.X < 10) || (tPos.X > 914))
{ wp.X += 1; wp.X *= -1; wpChanged = true; } //check to see if the destination has been reached if (( Math.Max(2.0f, Math.Abs(tPos.X - wp.X)) <= 2.0f ) && (Math.Max(2.0f, Math.Abs(tPos.Y - wp.Y)) <= 2.0f)) { wp.X = 0; wp.Y = 0; wpChanged = true; } if ( wpChanged ) { toons[i].Waypoint = wp; wpChanged = false; } toons[i].Update(gameTime); }
All that’s left in this manager class is the Draw()
method. It will be difficult to code, but if we work through the tears, we can get it done. See, what we have to do is make a loop that walks the array of Character
objects and calls the object’s Draw()
method. Whew! This is going to be tough!
public void Draw(SpriteBatch spriteBatch) { for (int i = 0; i < NUM_TOONS; i++) { toons[i].Draw(spriteBatch); } } #endregion
Ta-da! One high-powered CharacterManager
scratched off our to-do list. All we have to do is hook it up in the GameStateManager
class exactly as we did the BackgroundManager
class (new additions bolded):
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace _2DSpriteGame.GameState { /// <summary> /// This class is a container for all manager objects used in the game. It will /// be stored as a singleton in the Game1 class and allows access to the /// manager objects through the use of properties. /// </summary> public class GameStateManager { #region members private Environment.BackgroundManager bgm; private Character.CharacterManager cm; #endregion #region properties public Environment.BackgroundManager BGManager { get { return bgm; } } public Character.CharacterManager CharManager { get { return cm; } } #endregion #region constructor / init public GameStateManager() { bgm = new Environment.BackgroundManager(); cm = new Character.CharacterManager(); } public void LoadContent(ContentManager Content) { bgm.LoadContent(Content); cm.LoadContent(Content); } #endregion
#region utilities public void Update(GameTime gameTime) { bgm.Update(gameTime); cm.Update(gameTime); } public void Draw(SpriteBatch sb) { bgm.Draw(sb); cm.Draw(sb); } #endregion } }
Ding-ding-ding-ding! Run it, Valued Reader, and be awed. After you recover, stop it and run it again. All the little dudes will be different colors! Awesome! Celebrate with a beverage of your choice (or by sending me a brand-new Corvette—your choice).
The kind of game I have in mind for this example is one that relies on an indirect control mechanic (think The Sims), in which the player doesn’t directly move the mobs or directly perform actions, but instead tells the mob to move to a certain point or to perform some action. Additionally, in this game, the player character will not appear on the screen; the player plays himself, and will try to get the mobs in the game to meet certain criteria (for example, to all hate each other, or for certain mobs to like other mobs, etc.). As such, our next step is to add the ability to select one of these monsters and to know which one we’ve selected. I’d like to show which mob is selected by adding a glow around the one we’ve picked. To do this, we need an extra sprite texture.
Again, Valued Reader, this section will not address VGAI, but it does something equally as crucial to our work: It creates the little beasties … er, the characters we will add the VGAI to later. Developing the characters in advance and defining basic methods for control and movement will allow us to focus exclusively on adding VGAI in Chapter 4, “Indirect Control in a 2D Sprite-Based Game.”
In the Character
class, we need to add another CharacterSprite
and another Color
object. We also need to add a Boolean flag that tells the object whether to draw the glow mask or not. Like so:
//assets private Sprite.CharacterSprite idleBodyFixed; private Color bodyColor; private Sprite.CharacterSprite idleBodyFixedMask; private Color bodyMaskColor; private Boolean drawMask;
Only the Boolean needs a property, so we can add it to the properties list:
public Boolean DrawGlowMask { get { return drawMask; } set { drawMask = value; } }
We need to update the constructor to accommodate building the glow mask like so:
#region constructors / init public Character(Texture2D tex, Texture2D mask, Vector2 p, float s, float spd, Color bColor, Color bMaskColor) { pos = p; scale = s; speed = spd; bodyColor = bColor; bodyMaskColor = bMaskColor; drawMask = false; idleBodyFixed = new Sprite.CharacterSprite(tex, pos, scale); idleBodyFixed.Tint = bodyColor; idleBodyFixedMask = new Sprite.CharacterSprite(mask, pos, scale); idleBodyFixedMask.Tint = bodyMaskColor; } #endregion
Finally, we need to fix up those pesky utility methods. The only thing we even have to think about here is the order in which we want to draw the two textures. Since we are using the painter’s algorithm, we want the stuff that should always be visible on top. That means we draw the idleBodyFixedMask
first (provided we are flagged to do so). Here’s the code:
#region utility methods public void Update(GameTime gameTime) { //compute the size of the next step Vector2 step = Vector2.Normalize(Vector2.Subtract(pos, wp)) * speed * (float)gameTime.ElapsedGameTime.TotalSeconds; //if we are not within our "there" range, if ((Math.Max(2.0f, Math.Abs(pos.X - wp.X)) > 2.0f) || (Math.Max(2.0f, Math.Abs(pos.Y - wp.Y)) > 2.0f)) { pos -= step; } idleBodyFixed.Position = pos; idleBodyFixedMask.Position = pos; } public void Draw(SpriteBatch sb) { if (drawMask) { idleBodyFixedMask.Draw(sb); } idleBodyFixed.Draw(sb); } #endregion
To enable this extra feature in the rest of the code, we just need to make one change in the CharacterManager
class, and we need to import the art asset from the CD into the Character folder in Content. The art asset is named SumaFixedGlowMask.png. For testing, we will also add a random selector for now.
The necessary change is in the LoadContent()
method, and is simply this:
public void LoadContent(ContentManager Content) { int y = rand.Next(248) + 410; //the walkable y-space in our environment int x = rand.Next(904) + 10; //the walkable x-space in our environment byte r = (byte)rand.Next(255);
byte g = (byte)rand.Next(255); byte b = (byte)rand.Next(255); float spd = 150f; for (int i = 0; i < NUM_TOONS; i++) { toons[i] = new Character( Content.Load<Texture2D>("Character/SumaFixed"), Content.Load<Texture2D>("Character/SumaFixedGlowMask"), new Vector2(x, y), 1.0f, spd, new Color(r, g, b, 255), Color.GhostWhite); y = rand.Next(248) + 410; //the walkable y-space in our environment x = rand.Next(904) + 10; //the walkable x-space in our environment r = (byte)rand.Next(255); g = (byte)rand.Next(255); b = (byte)rand.Next(255); } toons[0].DrawGlowMask = true; //this is for randomly testing the glow mask for selected mobs }
The random selector must appear in the Update()
thread, like this:
public void Update(GameTime gameTime) { deltaTime += (float)gameTime.ElapsedGameTime.TotalMilliseconds; if (deltaTime > INTERVAL) { int y = rand.Next(248) + 410; //the walkable y-space in our environment int x = rand.Next(904) + 10; //the walkable x-space in our environment int chance = rand.Next(100); Vector2 dir = new Vector2(x, y); //this is for randomly testing the glow mask for selected mobs if (chance < 20)
{ //deselect the current mob for (int i = 0; i < NUM_TOONS; i++) { toons[i].DrawGlowMask = false; } //randomly select a new one toons[rand.Next(NUM_TOONS)].DrawGlowMask = true; }
Run it, but be prepared for a pretty astounding dramatic impact. You might want to have smelling salts on hand.
As it stands, we can add only a single waypoint for the mobs, and they travel to that waypoint before changing directions. We will need to change that. Part of our next step will be to add collision detection; once two mobs have figured out they are colliding (instead of occupying the same space, as they do now), they will need to be able to move around one another before returning to their original paths.
Also, if we have the capability of adding more waypoints, we can add increasingly more complicated paths for the mobs to follow. With more and more waypoints, we need a way to focus on only the next waypoint, while keeping the rest of the waypoints in order. Luckily, there is a relatively easy way to do this: Use a stack. We’re going to go one step further, however, in the interest of organization. Create a new class inside the Logic folder of the Character directory called CharacterPath.cs. This class will basically be a container for a waypoint stack and handle some path logic for us.
Obviously, this class needs a stack of Vector2
objects. Because each XNA stack object is a statically sized data structure, we also need a constant that represent the maximum number of waypoints we want to store. I consulted my Magic Eight Ball and learned that the number we need to keep track of is “Not at this time,” so I chose the number 24 at random. I’m also going to ask you to trust me (would I lie to you?!) that we will also need a Boolean to keep track of whether something is flat or not. With all that in mind, your code should look like this so far:
using System; using System.Collections.Generic; using Microsoft.Xna.Framework;
namespace _2DSpriteGame.Character.Logic { /// <summary> /// Contains a stack of waypoints for a character to follow, performs update /// processing on the current waypoint /// </summary> public class CharacterPath { #region members private Stack<Vector2> wpStack; private Boolean isFlat; //constants private const int MAX_PATH_SIZE = 24; #endregion
Next up, we need to create some of those dang property things. The isFlat
property is easy. We just need the get
portion. The next property we want is one that will allow us to push a Vector2
on the stack and will also perform a non-destructive pop from the stack. It should be called Waypoint
. We can code Waypoint
and IsFlat
like this:
#region properties public Vector2 Waypoint { get { if (wpStack.Count > 0) { return wpStack.Peek(); } else { return Vector2.Zero; } } set { wpStack.Push(value); } } public Boolean IsFlat { get { return isFlat; } } #endregion
Yeah. I pulled another fast one. What’s all that wpStack.Count
business? We can pop this stack to zero members (and will do so quite often), and as such, we need a way to not call a stack function on the empty stack. What about the fact that we’re passing back Vector2.Zero
? It’s harmless, as the only way we use these waypoints is in the position update calculation in the Character
class (just like the wp
member in the previous code).
Next, we need the ever-annoying duo: constructor and Update()
methods. All we will do inside the constructor is initialize an empty stack, like this:
#region constructors / inti public CharacterPath() { wpStack = new Stack<Vector2>(MAX_PATH_SIZE); } #endregion
Update()
will be a little more interesting, but not a whole lot, as it will simply maintain the isFlat
member and do destination checking (which we did inside the CharacterManager.Update()
method previously). We can handle all that thusly:
#region accessors public void Update(Vector2 pos) { if (wpStack.Count == 0) { isFlat = true; return; } else { if (isFlat) { isFlat = false; } } //grab current position for checks Vector2 wp = Waypoint; //check to see if the desitination has been reached if ((Math.Max(2.0f, Math.Abs(pos.X - wp.X)) <= 2.0f) && (Math.Max(2.0f, Math.Abs(pos.Y - wp.Y)) <= 2.0f))
{ AbortWaypoint(); } }
The only other method we need is one that will allow us to pop waypoints off the stack if we decide they are no longer valid. We need to first make sure the stack is not empty, and then call the Stack
class Pop()
method. Easy peasy:
public void AbortWaypoint() { if (wpStack.Count > 0) { wpStack.Pop(); } } #endregion
That’s it. Easy class, right?
We need to make a few changes to both the Character
and CharacterManager
classes. First, in Character
, we need to add a member of the CharacterPath
class and remove the old waypoint member:
public class Character
{
#region members
//state attributes
private Vector2 pos;
private float scale;
private float speed;
private Logic.CharacterPath path;
Next, we need to change the old Waypoint
property to pass through the property requests to the CharacterPath
object. Like this:
public Vector2 Waypoint { get { return path.Waypoint; } set { path.Waypoint = value; } }
We need to initialize the path member in the constructor:
#region constructors / init
public Character(Texture2D tex, Texture2D mask, Vector2 p, float s, float spd,
Color bColor, Color bMaskColor)
{
pos = p;
scale = s;
speed = spd;
bodyColor = bColor;
bodyMaskColor = bMaskColor;
drawMask = false;
idleBodyFixed = new Sprite.CharacterSprite(tex, pos, scale);
idleBodyFixed.Tint = bodyColor;
idleBodyFixedMask = new Sprite.CharacterSprite(mask, pos, scale);
idleBodyFixedMask.Tint = bodyMaskColor;
path = new Logic.CharacterPath();
}
#endregion
Finally, we need to make a few changes to the Update()
function to call the Update()
function of the CharacterPath
object, use the path.Waypoint
property in place of the old wp
member, and do the bounds-checking comparisons. (Note: we will be replacing this completely when we add collision detection, but for now….) The code looks like this:
#region utility methods public void Update(GameTime gameTime) { path.Update(pos); //compute the size of the next step Vector2 step = Vector2.Normalize(Vector2.Subtract(pos, path.Waypoint)) * speed * (float)gameTime.ElapsedGameTime.TotalSeconds; //if we are not within our "there" range, if ((Math.Max(2.0f, Math.Abs(pos.X - path.Waypoint.X)) > 2.0f) || (Math.Max(2.0f, Math.Abs(pos.Y - path.Waypoint.Y)) > 2.0f)) { pos -= step; } Boolean outOfBounds = false; if (pos.Y < 410)
{ pos.Y = 411; outOfBounds = true; } else if (pos.Y > 658) { pos.Y = 657; outOfBounds = true; } if (pos.X < 10) { pos.X = 11; outOfBounds = true; } else if (pos.X > 914) { pos.X = 913; outOfBounds = true; } if (outOfBounds) { path.AbortWaypoint(); } idleBodyFixed.Position = pos; idleBodyFixedMask.Position = pos; }
That’s it for the Character
class. Our last few changes will be in the CharacterManager
class, specifically in the LoadContent()
and Update()
methods. The only thing that’s new here is adding a random waypoint to each Character
object during the content-loading phase. The rest is just removing the destination checking and the bounds checking. Here it is:
public void LoadContent(ContentManager Content) { int y = rand.Next(248) + 410; //the walkable y-space in our environment int x = rand.Next(904) + 10; //the walkable x-space in our environment byte r = (byte)rand.Next(255); byte g = (byte)rand.Next(255);
byte b = (byte)rand.Next(255); float spd = 150f; for (int i = 0; i < NUM_TOONS; i++) { toons[i] = new Character( Content.Load<Texture2D>("Character/SumaFixed"), Content.Load<Texture2D>("Character/SumaFixedGlowMask"), new Vector2(x, y), 1.0f, spd, new Color(r, g, b, 255), Color.GhostWhite); y = rand.Next(248) + 410; //the walkable y-space in our environment x = rand.Next(904) + 10; //the walkable x-space in our environment toons[i].Waypoint = new Vector2(x, y); y = rand.Next(248) + 410; //the walkable y-space in our environment x = rand.Next(904) + 10; //the walkable x-space in our environment r = (byte)rand.Next(255); g = (byte)rand.Next(255); b = (byte)rand.Next(255); } toons[0].DrawGlowMask = true; //this is for randomly testing the glow mask for selected mobs } #endregion #region utility methods public void Update(GameTime gameTime) { deltaTime += (float)gameTime.ElapsedGameTime.TotalMilliseconds; if (deltaTime > INTERVAL) { int y = rand.Next(248) + 410; //the walkable y-space in our environment int x = rand.Next(904) + 10; //the walkable x-space in our environment
int chance = rand.Next(100); Vector2 dir = new Vector2(x, y); //this is for randomly testing the glow mask for selected mobs if (chance < 20) { //deselect the current mob for (int i = 0; i < NUM_TOONS; i++) { toons[i].DrawGlowMask = false; } //randomly select a new one toons[rand.Next(NUM_TOONS)].DrawGlowMask = true; } for (int i = 0; i < NUM_TOONS; i++) { if (chance < 60) { toons[i].Waypoint = dir; y = rand.Next(248) + 410; //the walkable y-space in our environment x = rand.Next(904) + 10; //the walkable x-space in our environment dir = new Vector2(x, y); } chance = rand.Next(100); } deltaTime = 0f; } for (int i = 0; i < NUM_TOONS; i++) { toons[i].Update(gameTime); } }
Run the code and be amazed at how all these changes made no discernable change to the game you see! Yay!
Ahem. Moving on to the next phase of adding collision detection, we need a bunch of Rectangle
objects. The first one we will add will set the bounds for the walkable area. Yeah, yeah, I know, we’ve already handled that, but there’s a basic rule we’re going to follow: Make everything work the same way as everything else.
Since this is a game-level boundary, we will stick it in the highest level of our environment code: BackgroundManager.cs. In our members list, under the helper members section, add a private Rectangle
object named bBox
. To make it easier to set, change, or maintain our walkable space, we’ll also add four integer constants to use when constructing the bounding box: x, y, h, and w. We’ve already figured out what the values for two of these constants should be: x = 10 and y = 410. The other two are the actual values we wanted for the bounds of the walkable space (as opposed to the 100-pixel value we used to accommodate the size of the texture): height = 348 and width = 1014. The members list now looks like this:
public class BackgroundManager= { #region members private Sprite.BackgroundSprite[] GalaxyLayer; private Sprite.BackgroundSprite[] FarStarLayer; private Sprite.BackgroundSprite[] MidStarLayer; private Sprite.BackgroundSprite[] NearStarLayer; private Sprite.BackgroundSprite Ship; //helper members private Random rand; private Vector2 left = new Vector2(-180, 0); private Vector2 right = new Vector2(180, 0); private Rectangle bBox; //constants private const float scaler = 0.55f; private const float galSpd = 0.75f; private const float farSpd = 1.0f; private const float midSpd = 4.0f; private const float nearSpd = 9.0f; private const int x = 10; private const int y = 410;
private const int w = 1014; private const int h = 348; #endregion
Next, initialize the Rectangle
in the constructor:
#region constructors / init public BackgroundManager() { GalaxyLayer = new Sprite.BackgroundSprite[5]; FarStarLayer = new Sprite.BackgroundSprite[3]; MidStarLayer = new Sprite.BackgroundSprite[3]; NearStarLayer = new Sprite.BackgroundSprite[3]; rand = new Random(); bBox = new Rectangle(x, y, w, h); }
Finally, we need to add a single utility method for bounds checking. It will need to return a bool
and we’ll name it InBounds
. In this method, we will use a built-in method of the Rectangle
class called Contains()
. Contains()
checks to make sure the object we pass in is wholly encompassed by the bBox
. Here’s the code:
#region utility methods /// <summary> /// Checks for collision between the local bounding box and the Rectangle object /// passed in. /// </summary> /// <param name="collider">Rectangle object used for collision detection</param> /// <returns>true iff the local bounding box fully contains the Rectangle object /// passed in</returns> public bool InBounds(Rectangle collider) { return bBox.Contains(collider); }
Wow. Who knew bounds checking could be so easy?
Next, we will take similar steps in the Character
class, but with the exception of an extra method called Hit
, an extra property to allow access to the bBox
member, and the values of w and h (we won’t need x and y because we are already tracking the position of the character). We will also have to update the position of the box as we update the position of the character.
The code is below, with bold-face text indicating the changes:
public class Character { #region members //state attributes private Vector2 pos; private float scale; private float speed; private Logic.CharacterPath path; private Rectangle bBox; //assets private Sprite.CharacterSprite idleBodyFixed; private Color bodyColor; private Sprite.CharacterSprite idleBodyFixedMask; private Color bodyMaskColor; private Boolean drawMask; //constants private const int w = 100; private const int h = 100; #endregion #region properties public Vector2 Position { get { return pos; } set { pos = value; } } public float Scale { get { return scale; } set { scale = value; } } public Vector2 Waypoint { get { return path.Waypoint; } set { path.Waypoint = value; } } public float Speed { get { return speed; }
set { speed = value; } } public Boolean DrawGlowMask { get { return drawMask; } set { drawMask = value; } } public Boolean IsFlat { get { return path.IsFlat; } } public Rectangle BoundingBox { get { return bBox; } } #endregion #region constructors / init public Character(Texture2D tex, Texture2D mask, Vector2 p, float s, float spd, Color bColor, Color bMaskColor) { pos = p; scale = s; speed = spd; bodyColor = bColor; bodyMaskColor = bMaskColor; drawMask = false; idleBodyFixed = new Sprite.CharacterSprite(tex, pos, scale); idleBodyFixed.Tint = bodyColor; idleBodyFixedMask = new Sprite.CharacterSprite(mask, pos, scale); idleBodyFixedMask.Tint = bodyMaskColor; path = new Logic.CharacterPath(); bBox = new Rectangle((int)pos.X, (int)pos.Y, w, h); } #endregion #region utility methods /// <summary> /// Basic containment check against the local bounding box and the Rectangle /// object passed in. /// </summary>
/// <param name="collider">Rectangle object for collision detection</param> /// <returns>true iff the local bounding box fully contains the parameter /// passed in</returns> public bool Contains(Rectangle collider) { return bBox.Contains(collider); } /// <summary> /// Basic intersection check against the local bounding box and the Rectangle /// object passed in. /// </summary> /// <param name="collider">Rectangle object for collision detection</param> /// <returns>true iff the local bounding box intersects the parameter passed /// in</returns> public bool Hit(Rectangle collider) { return bBox.Intersects(collider); } public void Update(GameTime gameTime) { path.Update(pos); pos += path.Waypoint * speed * (float)gameTime.ElapsedGameTime. TotalSeconds; Boolean outOfBounds = false; if (pos.Y < 410) { pos.Y = 411; outOfBounds = true; } else if (pos.Y > 658) { pos.Y = 657; outOfBounds = true; } if (pos.X < 10) { pos.X = 11; outOfBounds = true; }
else if (pos.X > 914) { pos.X = 913; outOfBounds = true; } if (outOfBounds) { path.AbortWaypoint(); } idleBodyFixed.Position = pos; idleBodyFixedMask.Position = pos; bBox.X = (int)pos.X; bBox.Y = (int)pos.Y; } public void Draw(SpriteBatch sb) { if (drawMask) { idleBodyFixedMask.Draw(sb); } idleBodyFixed.Draw(sb); } #endregion }
Now all that’s left is to use the bounds checking and collision detection. To do this, we need to make the BackgroundManager
object in the GameStateManager
class accessible to every Character
object in the game. To do this most easily, we will need to create a class that will perform all the collision detection for our game. If the scope of the game were bigger, we’d likely be doing this anyway, so we could implement some really nifty optimization for collision detection.
In the GameState directory, create a new folder called Utilities. Inside the new folder, create a public class named CollisionHandler.cs. This class only needs to hold a method for right now—the method that will handle the bounds checking and the behavior that will be triggered by a character trying to go out of bounds. So this class will have no members, no properties, and an empty constructor. Like so:
using System; using Microsoft.Xna.Framework;
namespace _2DSpriteGame.GameState.Utilities { /// <summary> /// Handles all the collision detection for the all collidable game objects /// </summary> public class CollisionHandler { #region members #endregion #region properties #endregion #region constructor / init public CollisionHandler() { } #endregion
Now for the fun part. We need to write a method that will check the bounds in the BackgroundManager
class against the bounds in every Character
contained in the CharacterManager
class. If we find one that is out of bounds, the first thing we need to do is to pop the waypoint.
We can access both of these classes through the GameStateManager
class from inside the method. Then we can walk through the array of Character
s contained in the CharacterManager
and check each one with the BackgroundManager. InBounds()
method. Check … er, whoops. Remember that private constant we made in the CharacterManager
? The one that dictates how many Character
objects we keep track of? We need it to be public here so we can set up a for
loop that iterates the correct number of times. Fine. Poof:
//constants
public const int NUM_TOONS = 10;
Now that we can iterate, we need a way to access the Character
at a given index. You know, like an old-school accessor method! Bam:
/// <summary> /// Returns the Character object at index passed in /// </summary> /// <param name="i">Index of the desired Character object</param> /// <returns>Character object at index i</returns>
public Character GetToon(int i) { if (i < 0 || i > NUM_TOONS) { return null; } return toons[i]; }
Awesome! We’re back in business! If one is out of bounds, we need to pop the current waypoint of the stack. Check … er, rats times two.
We built a method into the CharacterPath
class that would pop a waypoint off the stack, but we currently have no ability to do so from the Character
class. Easy solution: Create a pass-through method:
/// <summary> /// Removes the current top waypoint on the waypoint stack /// </summary> public void AbortWaypoint() { path.AbortWaypoint(); }
Yay! We have the technology…. Our CollisionHandler.CheckBounds()
method should look like this:
#region utility /// <summary> /// This method checks every character in the game for containment within the /// walkable area of the game /// </summary> public void CheckBounds() { Environment.BackgroundManager bgm = Game1.GameStateMan.BGManager; Character.CharacterManager cm = Game1.GameStateMan.CharManager; Character.Character worker; for (int i = 0; i < Character.CharacterManager.NUM_TOONS; i++) { worker = cm.GetToon(i);
//this is tricksy!! if the worker is out of bounds, AbortWaypoint should //be called if (!bgm.InBounds(worker.BoundingBox)) { worker.AbortWaypoint(); } } }
Now, to empower all this goodness, we need to add a member to the GameState Manager
class of the CollisionHandler
type, initialize it, and call the CheckBounds
method before we call Update()
in the Character
manager. Kind of like this:
using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace _2DSpriteGame.GameState { /// <summary> /// This class is a container for all manager objects used in the game. It will /// be stored as a singleton in the Game1 class and allows access to the /// manager objects through the use of properties. /// </summary> public class GameStateManager { #region members private Environment.BackgroundManager bgm; private Character.CharacterManager cm; private Utilities.CollisionHandler ch; #endregion #region properties public Environment.BackgroundManager BGManager { get { return bgm; } } public Character.CharacterManager CharManager { get { return cm; } } public Utilities.CollisionHandler CHandler
{ get { return ch; } } #endregion #region constructor / init public GameStateManager() { bgm = new Environment.BackgroundManager(); cm = new Character.CharacterManager(); ch = new Utilities.CollisionHandler(); } public void LoadContent(ContentManager Content) { bgm.LoadContent(Content); cm.LoadContent(Content); } #endregion #region utilities public void Update(GameTime gameTime) { bgm.Update(gameTime); //bounds checking before the characters move ch.CheckBounds(); cm.Update(gameTime); } public void Draw(SpriteBatch sb) { bgm.Draw(sb); cm.Draw(sb); } #endregion } }
Now crank it up and watch it burn! Let it run for a minute, and you will notice a couple of disturbing events: first, the Frankenstein-like movement when the characters are trying to get out of bounds, and second, the fact that they can keep right on truckin’ out of bounds, albeit slowly. This is because the code we put in above only removes the offending waypoint. The character still might be out of bounds, and still might roll another waypoint that takes it out of bounds. We need more “behavior” in our collision routine. We need to identify which edge is being breached and then move the character back in bounds and give it a way-point that takes it farther in the acceptable walk area.
Before we can do this, however, we need to expose the edges of the bounding boxes both in the BackgroundManager
class and in the Character
class. The good news is that we can perform both feats with the same four properties in each class. Both classes should get this code:
public int LEFT { get { return bBox.Left; } } public int RIGHT { get { return bBox.Right; } } public int TOP { get { return bBox.Top; } } public int BOTTOM { get { return bBox.Bottom; } }
With that code in place, we can now perform the behavior-fu to the CollisionHandler.CheckBounds()
method. If the Character
’s bottom edge is below BackgroundManager
’s bottom edge, then we want to set a new waypoint that is 205 pixels above BackgroundManager
’s bottom edge, and we want to move Character
so that it is 103 pixels above the boundary. We’re going to repeat this same algorithm for all the edges—set a waypoint 105 pixels (plus a 100-pixel offset for the width and height of the Character
bounding box) and then set Character
’s position 3 pixels inside the boundary, thusly:
//our next step is to handle the offending edge(s)… Vector2 wp = worker.Waypoint; if (worker.BOTTOM > bgm.BOTTOM) { wp = new Vector2(wp.X, bgm.BOTTOM - 88); worker.Position = new Vector2(worker.Position.X, bgm.BOTTOM - 83); }
else if (worker.TOP < bgm.TOP) { wp = new Vector2(wp.X, bgm.TOP + 15); worker.Position = new Vector2(worker.Position.X, bgm.TOP + 10); } if (worker.LEFT < bgm.LEFT) { wp = new Vector2(bgm.LEFT + 15, wp.Y); worker.Position = new Vector2(bgm.LEFT + 10, worker.Position.Y); } else if (worker.RIGHT > bgm.RIGHT) { wp = new Vector2(bgm.RIGHT - 105, wp.Y); worker.Position = new Vector2(bgm.RIGHT - 100, worker.Position.Y); } worker.Waypoint = wp;
The next bit of wonder we will add to this 2D powerhouse is character-to-character collision detection. We will accomplish this by another public method in the CollisionHandler
, named DoCollisions
.
There are several ways we can go here. First, we could just move the two characters apart by manually adjusting their positions. This is a pretty fast method of collision detection, and will always keep the two colliders apart, but is subject to deadlocks—e.g., one character is moving right at (10,10), the other is moving left at (100,10)—in which case the two colliders bounce off one another forever. It also tends to look bouncy—and as I’m sure you will agree, Valued Reader, bouncy is bad. That’s not to say that this method is not valid; it is a very valid method of collision detection and one that is quite often used. In fact, we just used it in the bounds-checking routine (although in a slightly modified form). The second method would be to simply alter the waypoints of each collider such that they are no longer on an intercept course (Mr. Sulu). While this works, it’s messy, and allows very fast-moving objects to pretend they can move through solid objects (without a transporter beam). Finally—and this is the cool one, because it’s the one I want to use—we can calculate a new direction (waypoint) for each character that will move them away from one another.
This new method will require only a reference to the CharacterManager
class (no silly BackgroundManager
required). We will do another loop through all the characters, but this time we need to nest another loop inside the first so that we can compare the current Character
object against all the rest. Once we identify collisions, we will calculate the difference between the x and y values of both the character we are testing collisions for (worker) and the character being collided with (hitter). Check this code out:
/// <summary> /// Handles collision detection between character objects. /// </summary> public void DoCollision() { Character.CharacterManager cm = Game1.GameStateMan.CharManager; Environment.BackgroundManager bgm = Game1.GameStateMan.BGManager; Character.Character worker; Character.Character hitter; //default character box sizes for testing int x = 30; int y = 20; int w = 50; int h = 60; Rectangle test; //iterate through the array of character objects for (int i = 0; i < Character.CharacterManager.NUM_TOONS - 1; i++) { //character to test with worker = cm.GetToon(i); //iterate through the remaining characters testing worker for collision for (int j = i + 1; j < Character.CharacterManager.NUM_TOONS; j++) { //character to test against hitter = cm.GetToon(j); //the collision test if (worker.Hit(hitter.BoundingBox)) { //figure out how far apart they are for creating a new waypoint float deltaX = (float)Math.Abs(worker.LEFT - hitter.LEFT)/2;
float deltaY = (float)Math.Abs(worker.TOP - hitter.TOP)/2; //create some temp waypoints Vector2 wwp = worker.Waypoint; Vector2 hwp = hitter.Waypoint; //determine which edges are colliding and create the //appropriate waypoints if (worker.LEFT < hitter.LEFT) { if (worker.TOP > hitter.TOP) { wwp = new Vector2(worker.LEFT - deltaX, worker.TOP + deltaY); hwp = new Vector2(hitter.LEFT + deltaX, hitter.TOP - deltaY); } else { wwp = new Vector2(worker.LEFT + deltaX, worker.TOP - deltaY); hwp = new Vector2(hitter.LEFT - deltaX, hitter.TOP + deltaY); } } else { if (worker.TOP < hitter.TOP) { wwp = new Vector2(worker.LEFT + deltaX, worker.TOP - deltaY); hwp = new Vector2(hitter.LEFT - deltaX, hitter.TOP + deltaY); } else { wwp = new Vector2(worker.LEFT + deltaX, worker.TOP + deltaY); hwp = new Vector2(hitter.LEFT - deltaX, hitter.TOP - deltaY); } }
//test to see if the waypoints are new, and if so, update the //character waypoint if (worker.Waypoint != wwp) { worker.Waypoint = wwp; } if (hitter.Waypoint != hwp) { hitter.Waypoint = hwp; } //now, test to make sure the waypoint is in bounds, dropping it //if it is not test = new Rectangle((int)worker.Waypoint.X + x, (int) worker.Waypoint.Y + y, w, h); //this is tricksy!! if the the new waypoint is out of bounds, //AbortWaypoint should be called if (!bgm.InBounds(test)) { worker.AbortWaypoint(); } test = new Rectangle((int)hitter.Waypoint.X + x, (int) hitter.Waypoint.Y + y, w, h); //this is tricksy!! if the the new waypoint is out of bounds, //AbortWaypoint should be called if (!bgm.InBounds(test)) { hitter.AbortWaypoint(); } } } } }
Yay! Bouncing baby, uh, frog-looking things that always stay on the screen and don’t try to walk through one another! Ain’t we just about cool. The answer, my Esteemed Fathomer, is yes, and we’re getting cooler, because we’re about to add a mouse to this little game of cat and … er … mouse. We work through how to add a mouse and listen for it in the MiceNMen examples in Appendix A, “Getting Started with Microsoft XNA,” so I’m going to go into slap-some-stink-in-a-class-file-fu mode and then link it into the game. The only thing we will be doing differently here is adding a Rectangle bBox
object to the mouse pointer so we can use these fancy collision-detection schemes to do cool stuffs.
I’m going to start by adding a MouseSprite
class in the _2DSpriteGame.GameState. Utilities
namespace. The class can be found below; you will note a lot of similarities to other sprite classes we’ve built. Some of you hecklers may be wondering why I am not creating a base class and then using inheritance to create subclasses. The answer is simply that while these classes may look alike for now, they will eventually get pretty divergent, and because I said so. So nyaaah!
Ahem. On to the code:
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; namespace _2DSpriteGame.GameState.Utilities { /// <summary> /// A 2D sprite implementation for a mouse pointer /// </summary> public class MouseSprite { #region members private int id; //an identifier for textures private Texture2D spriteTexture; //the texture to display private Rectangle srcRectangle; //a rectangle used in our Draw() call that represents where inside the texture to copy the pixels from private Vector2 position; //a point that represents the upper-left corner of the sprite private float scale; //a scale percentage private float rotation; //degrees of rotation private Color tintColor; //the color to tint the sprite with #endregion #region properties public int Identity { get { return id; } } public Texture2D Texture { get { return spriteTexture; } } public Vector2 Position
{ get { return position; } set { position = value; } } public float Scale { get { return scale; } set { scale = value; } } public Color Tint { set { tintColor = value; } } #endregion #region constructors / init public MouseSprite(int i, Texture2D text, Vector2 pos, float sc) { id = i; spriteTexture = text; position = pos; scale = sc; srcRectangle = new Rectangle(0, 0, spriteTexture.Width, spriteTexture.Height); rotation = 0f; tintColor = Color.White; } #endregion #region utilities public void Draw(SpriteBatch sb) { sb.Draw(spriteTexture, position, srcRectangle, tintColor, rotation, Vector2.Zero, scale, SpriteEffects.None, 0); } #endregion } }
Very simple, very easy. Now, let’s build a MouseHandler
class to do mouse-fu! What we want this class to handle is pretty basic for the most part (for now … mwaaahaahaahaahaahaa!). The class will contain various MouseSprite
objects to represent the different kinds of pointers we want in the game. It will also contain a Rectangle
bounding box for collision detection (we’ll talk about that in a minute), a Boolean value that dictates whether we should draw the mouse or not, and finally a magic integer that will keep track of which character we are currently hovering over (for click functionality). We will also do a bit of animation, so we want an interval constant and a constant that represents the number of MouseSprite
objects the class contains. To make selection easier, we will also add a public enumeration type (i.e., an enum
) to the namespace called MouseTypes
.
In terms of class behaviors, we will implement a constructor, a LoadContent()
method, an Update()
method (which is the workhorse of the class actually), and a small, guarded Draw()
method.
Here’s the code:
using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Graphics; namespace _2DSpriteGame.GameState.Utilities { /// <summary> /// This class takes care of a mouse input device, maintaining the state /// of the mouse and its art assets (complete with a bounding box for box-on-box /// collision detection) /// </summary> public class MouseHandler { #region members public enum MouseTypes { IDLE = 0, LEFTDOWN, LEFTCLICK1, LEFTCLICK2, LEFTCLICK3 } private MouseSprite mouse; private MouseSprite[] mice; private Rectangle bBox; private MouseState mouseState; private MouseState oldState;
private Boolean drawMouse; private float deltaT; private int mouseOver; //bounding constants private const int w = 1; private const int h = 1; //other constants private const int NUM_MICE = 5; //if more mice are added, remember to name them in the MouseTypes enum! private const float INTERVAL = 1000f / 16f; #endregion #region properties public Rectangle Mouse { get { return bBox; } } public Boolean DrawMouse { get { return drawMouse; } set { drawMouse = value; } } public int MouseOver { get { return mouseOver; } set { mouseOver = value; } } public int LEFT { get { return bBox.Left; } } public int RIGHT { get { return bBox.Right; } } public int TOP { get { return bBox.Top; } } public int BOTTOM
{ get { return bBox.Bottom; } } #endregion #region constructor / init public MouseHandler() { bBox = new Rectangle(0, 0, w, h); mice = new MouseSprite[NUM_MICE]; drawMouse = false; mouseOver = -1; } public void LoadContent(ContentManager content) { mice[(int)MouseTypes.IDLE] = new MouseSprite((int)MouseTypes.IDLE, content.Load<Texture2D>("Mouse/Mouse"), new Vector2(0, 0), 1.0f); mice[(int)MouseTypes.LEFTDOWN] = new MouseSprite((int) MouseTypes.LEFTDOWN, content.Load<Texture2D>("Mouse/ LeftMouse"), new Vector2(0, 0), 1.0f); mice[(int)MouseTypes.LEFTCLICK1] = new MouseSprite((int) MouseTypes.LEFTCLICK1, content.Load<Texture2D>("Mouse/ LeftMouseUp1"), new Vector2(0, 0), 1.0f); mice[(int)MouseTypes.LEFTCLICK2] = new MouseSprite((int)Mouse Types.LEFTCLICK2, content.Load<Texture2D>("Mouse/ LeftMouseUp2"), new Vector2(0, 0), 1.0f); mice[(int)MouseTypes.LEFTCLICK3] = new MouseSprite((int) MouseTypes.LEFTCLICK3, content.Load<Texture2D> ("Mouse/LeftMouseUp3"), new Vector2(0, 0), 1.0f); mouse = mice[(int)MouseTypes.IDLE]; } #endregion #region utilities public void Update(GameTime gameTime) { Character.CharacterManager cm = Game1.GameStateMan.CharManager; GUI.GuiManager gm = Game1.GameStateMan.GuiManager; //capture the last state of the mouse and the current state of the //mouse
oldState = mouseState; mouseState = Microsoft.Xna.Framework.Input.Mouse.GetState(); //check the state of the left button for graphics changes if (mouseState.LeftButton == ButtonState.Pressed && oldState. LeftButton == ButtonState.Released) { //new left button down state mouse = mice[(int)MouseTypes.LEFTDOWN]; } //handle left click animation if (mouse.Identity != (int)MouseTypes.IDLE && mouseState. LeftButton == ButtonState.Released) { deltaT += (float)gameTime.ElapsedGameTime.TotalMilliseconds; if (deltaT > INTERVAL) { if (mouse.Identity == (int)MouseTypes.LEFTCLICK1) { mouse = mice[(int)MouseTypes.LEFTCLICK2]; } else if (mouse.Identity == (int)MouseTypes.LEFTCLICK2) { mouse = mice[(int)MouseTypes.LEFTCLICK3]; } else if (mouse.Identity == (int)MouseTypes.LEFTCLICK3) { mouse = mice[(int)MouseTypes.IDLE]; } deltaT = 0f; } } //handle left click if (oldState.LeftButton == ButtonState.Pressed && mouseState. LeftButton == ButtonState.Released) { //a mouse click occurred mouse = mice[(int)MouseTypes.LEFTCLICK1]; deltaT = 0f;
if (mouseOver != -1) { cm.SetDrawMask(mouseOver); } } //update the position of the mouse with the new screen coordinates //(we'll handle collision detection later) bBox.X = mouseState.X; bBox.Y = mouseState.Y; mouse.Position = new Vector2(mouseState.X, mouseState.Y); } public void Draw(SpriteBatch sb) { if (drawMouse) { mouse.Draw(sb); } } #endregion } }
As you can see, the Draw()
method is guarded by the Boolean member, drawMouse
. We use this member at present to indicate whether the mouse is inside the walkable area for the characters. Later, we will add a few different mouse pointers as we add more functionality, and this method may change a bit. How will we set this Boolean? Collision detection.
Since we added the bounding-box code to MouseHandler
, we can collide its Rectangle
object against the walkable space defined in BackgroundManager
(just like we do with the characters) and set the value of the Boolean based on whether the mouse is in bounds. We’ll need a new method in the CollisionHandler
to do this:
/// <summary> /// Determines whether the mouse is inside the character walkable area of the game /// or not /// </summary> public void CheckMouseBounds()
{ Environment.BackgroundManager bgm = Game1.GameStateMan.BGManager; MouseHandler mh = Game1.GameStateMan.MHandler; //this is tricky!! if the mouse is out of bounds, we shouldn't draw our mouse //pointer if (!bgm.InBounds(mh.Mouse)) { mh.DrawMouse = false; } else { mh.DrawMouse = true; } }
That’s really simple code, and will be very fast in execution.
The MouseHandler.Update()
method may look big and complex, but in actuality all it does it check for a left click, perform various state changes if one is detected, and update the position of the MouseSprite
based on the mouse hardware polling. The state changes all rely on setting the current MouseSprite
that may be drawn to the correct type based on the mouse input.
In this section of the Update()
method, we are carrying out a bit of animation—the old school way. We are manually swapping the sprite for a new one based on the criteria we’re testing against:
//handle left-click animation if (mouse.Identity != (int)MouseTypes.IDLE && mouseState.LeftButton == ButtonState.Released) { deltaT += (float)gameTime.ElapsedGameTime.TotalMilliseconds; if (deltaT > INTERVAL) { if (mouse.Identity == (int)MouseTypes.LEFTCLICK1) { mouse = mice[(int)MouseTypes.LEFTCLICK2]; } else if (mouse.Identity == (int)MouseTypes.LEFTCLICK2) { mouse = mice[(int)MouseTypes.LEFTCLICK3]; }
else if (mouse.Identity == (int)MouseTypes.LEFTCLICK3) { mouse = mice[(int)MouseTypes.IDLE]; } deltaT = 0f; } }
This next bit of code is also pretty straightforward:
//handle left click if (oldState.LeftButton == ButtonState.Pressed && mouseState.LeftButton == ButtonState.Released) { //a mouse click occurred mouse = mice[(int)MouseTypes.LEFTCLICK1]; deltaT = 0f; if (mouseOver != -1) { cm.SetDrawMask(mouseOver); } }
What’s that you say? You want to know about that method I’m calling in the CharacterManager
class that doesn’t yet exist? Picky, picky. I can’t get anything by you, Valued Reader! Yes, I’m breakin’ the law here and doing something without talking to you about it first. We need something that lets us turn on that mad-good glowy effect when we select a character. In the CharacterManager
, we need to actually implement a method like that, because compilers are also picky and demand that I tell them what I’m doing before it will work. Basically, SetDrawMask()
is a simple function I added to the CharacterManager
that will set the DrawGlowMask
property of the all the characters to false
and then set the property in the character whose ID number we pass in to true
. Here’s the method:
/// <summary> /// Turns off the glow effect on all characters, then re-enables the glow for the /// character at index i /// </summary> /// <param name="i">Index value of the character that requires a glow effect </param> public void SetDrawMask(int i)
{ for (int j = 0; j < NUM_TOONS; j++) { toons[j].DrawGlowMask = false; } toons[i].DrawGlowMask = true; }
Now that the SetDrawMask()
method is written, and since we can fire it from a mouse click, we need to remove the chunk of code that randomly tested the glow mask. As you may recall, that was found in the CharacterManager.Update()
method. The new method should look like this:
/// <summary> /// Performs update processing on all characters managed, updates the left and /// right gui panels /// </summary> /// <param name="gameTime"></param> public void Update(GameTime gameTime) { deltaTime += (float)gameTime.ElapsedGameTime.TotalMilliseconds; if (deltaTime > INTERVAL) { int y = rand.Next(248) + 410; //the walkable y-space in our environment int x = rand.Next(904) + 10; //the walkable x-space in our environment int chance = rand.Next(100); Vector2 dir = new Vector2(x, y); for (int i = 0; i < NUM_TOONS; i++) { if (chance < 60) { toons[i].Waypoint = dir; y = rand.Next(248) + 410; //the walkable y-space in our environment x = rand.Next(904) + 10; //the walkable x-space in our environment dir = new Vector2(x, y); }
chance = rand.Next(100); } deltaTime = 0f; } for (int i = 0; i < NUM_TOONS; i++) { toons[i].Update(gameTime); } }
You might be asking yourself why I added the bounding-box junk and the ability to turn on glowy glows to the MouseHandler
class. The quick answer is because it will give us an easy way of selecting characters and an easy way of communicating which character has been selected to the player (the glow). All we are missing is a way of updating the mouseOver
member to the class we want to select. To do this, we’re going to use collision detection again.
All we need to do is test for collision against the mouse and all the characters on the screen. Since our walkable space is pretty small, and since the set of the characters is also small, we are going to test against everything and ignore optimizations we could perform (such as partitioning schemes, group collision, etc.). We need a new CollisionHandler
method, like this one:
/// <summary> /// Performs collision detection between the mouse and the characters to set /// which character has "mouse focus". /// </summary> public void CheckMouseCollision() { Character.CharacterManager cm = Game1.GameStateMan.CharManager; MouseHandler mh = Game1.GameStateMan.MHandler; mh.MouseOver = -1; Character.Character worker; for (int i = 0; i < Character.CharacterManager.NUM_TOONS; i++) { worker = cm.GetToon(i); if (worker.Hit(mh.Mouse))
{ //set up the glowy goodness on a left click mh.MouseOver = i; break; } } }
This method just finds the first mouse-character collision, sets the MouseOver
property in the MouseHandler
class, and then exits. This might lead to problems down the road (for instance, if we are trying to differentiate between two characters who occupy the same space). Did I say “might”? Remind me of that later….
Pretty cool, huh? We can enable this code by doing the following to the GameStateManager
class (changes in bold):
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace _2DSpriteGame.GameState
{
/// <summary>
/// This class is a container for all manager objects used in the game. It will
/// be stored as a singleton in the Game1 class and allows access to the
/// manager objects through the use of properties.
/// </summary>
public class GameStateManager
{
#region members
private Environment.BackgroundManager bgm;
private Character.CharacterManager cm;
private Utilities.CollisionHandler ch;
private Utilities.MouseHandler mh;
#endregion
#region properties
public Environment.BackgroundManager BGManager
{
get { return bgm; }
}
public Character.CharacterManager CharManager
{ get { return cm; } } public Utilities.CollisionHandler CHandler { get { return ch; } } public Utilities.MouseHandler MHandler { get { return mh; } } #endregion #region constructor / init public GameStateManager() { bgm = new Environment.BackgroundManager(); cm = new Character.CharacterManager(); ch = new Utilities.CollisionHandler(); mh = new Utilities.MouseHandler(); } public void LoadContent(ContentManager Content) { bgm.LoadContent(Content); cm.LoadContent(Content); mh.LoadContent(Content); } #endregion #region utilities public void Update(GameTime gameTime) { bgm.Update(gameTime); mh.Update(gameTime); //bounds checking before the characters move ch.CheckBounds(); ch.DoCollision(); ch.CheckMouseBounds(); ch.CheckMouseCollision(); cm.Update(gameTime); }
public void Draw(SpriteBatch sb)
{
bgm.Draw(sb);
cm.Draw(sb);
mh.Draw(sb);
}
#endregion
}
}
Now, what we’ve all been waiting for: RUN IT. Select a few characters and you will note you can’t deselect one without selecting another. There’s an easy fix to this, but for now we are going to run with it.
The last thing we need to do in this section is to build in a method of identifying characters to the player. Yes, we did build an über method for randomly assigning colors to each character as we build it, and we do have the glowy-goodness thing going on, but at the end of the day, we will need something a bit better.
To accomplish this task, I propose that we add a name tag that magically floats about the character’s head—you know, like in real life. Any seconds? Ah, you in the red lounge pants. Thank you. Now, for the vote…. I vote yes, therefore I win.
We build a fairly robust class for putting text on the screen in Appendix A. We’re going to steal and then adapt that code to do what we need for this example. First, add a new folder at the top level named GUI and add a new public class named ScreenText.cs inside it. This class is going to handle all that magical text stuff for us no matter what game unit we end up attaching it to.
The code for the new class is much the same as the old class (if you like that kind of music), with a few minor exceptions for cleaning up and to abstract it away from the TicTacToeUI package. Here it is, no big surprises:
using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Content; namespace _2DSpriteGame.GUI { /// <summary> /// This class keeps track of messages to be drawn to the screen, and the fonts /// with which to draw them
/// </summary> public class ScreenText { #region members //font controller helper public enum FontName { DEFAULT=0, DEFAULT_BOLD, SUBHEAD, SUBHEAD_BOLD, TITLE } //message data internal class Message { public String message; public double deltaT; public Vector2 position; public Color color; public int font; public Message(String m, double t) { message = m; deltaT = t; position = Vector2.Zero; color = Color.White; font = 0; } public Message(String m, double t, Vector2 p, Color c, int f) { message = m; deltaT = t; position = p; color = c; font = f; } } private SpriteFont[] fonts; private Message[] messages; private int currentMsg; private Boolean absPosition; private Vector2 pos;
//constants private const int MAX_FONTS = 5; //update the FontName enum if this number is increased! private const int MAX_MSGS = 32; private const double DEF_TIMER = 999999; #endregion #region properties public Boolean AbsolutePositioning { get { return absPosition; } set { absPosition = value; } } public Vector2 Position { get { return pos; } set { pos = value; } } #endregion #region constructors / init public ScreenText() { fonts = new SpriteFont[MAX_FONTS]; messages = new Message[MAX_MSGS]; currentMsg = -1; absPosition = true; } public void LoadContent(ContentManager Content) { //update as more fonts are added fonts[(int)ScreenText.FontName.DEFAULT] = Content.Load<SpriteFont> ("Fonts/Consolas12"); fonts[(int)ScreenText.FontName.DEFAULT_BOLD] = Content.Load <SpriteFont>("Fonts/Consolas12Bold"); fonts[(int)ScreenText.FontName.SUBHEAD] = Content.Load<SpriteFont> ("Fonts/Consolas14"); fonts[(int)ScreenText.FontName.SUBHEAD_BOLD] = Content.Load <SpriteFont>("Fonts/Consolas14Bold"); fonts[(int)ScreenText.FontName.TITLE] = Content.Load<SpriteFont> ("Fonts/Consolas18"); } #endregion
#region utilties /// <summary> /// print a message with all the defaults and with the default timer value /// </summary> /// <param name="message">The string to be printed</param> public void Print(String message) { Message m = new Message(message, DEF_TIMER); Add(m); } /// <summary> /// print a message with all the defaults and with custom timer value /// </summary> /// <param name="message">The string to be printed</param> /// <param name="t">time in millieseconds</param> public void Print(String message, double t) { Message m = new Message(message, t); Add(m); } /// <summary> /// print a message at a custom position, with a custom color and font, and /// with the default timer value /// </summary> /// <param name="message">The string to be printed</param> /// <param name="pos">The position of the message</param> /// <param name="c">The color of the text to draw</param> /// <param name="f">The index of the font to use</param> public void Print(String message, Vector2 pos, Color c, int f) { Message m = new Message(message, DEF_TIMER, pos, c, f); Add(m); } /// <summary> /// print a message at a custom position, with a custom color, font and /// timer value /// </summary> /// <param name="message">The string to be printed</param> /// <param name="t">time in millieseconds</param> /// <param name="pos">The position of the message</param> /// <param name="c">The color of the text to draw</param> /// <param name="f">The index of the font to use</param> public void Print(String message, double t, Vector2 pos, Color c, int f)
{ Message m = new Message(message, t, pos, c, f); Add(m); } /// <summary> /// dump all the messages and reset the list /// </summary> public void Clear() { messages = new Message[MAX_MSGS]; currentMsg = -1; } /// <summary> /// add a new message in the proper place in the array (last!) /// </summary> /// <param name="m">The message to add</param> private void Add(Message m) { currentMsg++; if (currentMsg >= MAX_MSGS) { for (int i = 1; i < MAX_MSGS; i++) { messages[i - 1] = messages[i]; } currentMsg = MAX_MSGS - 1; } messages[currentMsg] = m; } /// <summary> /// remove the message at index i /// </summary> /// <param name="i">The index of the message to remove</param> public void RemoveAt(int i) { if (currentMsg == -1) { return; } if (messages[i] != null)
{ messages[i] = null; } for (int j = i; j < MAX_MSGS; j++) { messages[j - 1] = messages[j]; } currentMsg--; } /// <summary> /// remove the newest message in the list /// </summary> public void RemoveNewest() { if (currentMsg == -1) { return; } if (messages[currentMsg] != null) { messages[currentMsg] = null; } currentMsg--; } /// <summary> /// remove the oldest message in the list /// </summary> public void RemoveOldest() { if (currentMsg == -1) { return; } for (int i = 1; i < MAX_MSGS; i++) { messages[i - 1] = messages[i]; } currentMsg--; } /// <summary> /// update the message list /// </summary> /// <param name="gameTime"></param>
public void Update(GameTime gameTime) { double tElapsed = gameTime.ElapsedGameTime.TotalMilliseconds; //step through the messages for (int i = 0; i < MAX_MSGS; i++) { //if a message is set to expire if (messages[i].deltaT != 999999) { //update its time left in this world messages[i].deltaT -= tElapsed; //test to see if it is still viable if (messages[i].deltaT < 0) { //and kill it if necessary RemoveAt(i); i--; } } } } /// <summary> /// draw the messages to the screen /// </summary> /// <param name="sb"></param> public void Draw(SpriteBatch sb) { //step through the messages list if (currentMsg != -1) { Vector2 drawPos; for (int i = 0; i <= currentMsg; i++) { //and draw them all if ( absPosition ) { drawPos = messages[i].position; } else { drawPos = messages[i].position + pos; }
sb.DrawString(fonts[messages[i].font], messages[i]. message, drawPos, messages[i].color); } } } #endregion } }
The biggest change to that code is removing the dynamic data structures (dictionaries and lists) in favor of static structures (arrays) for the sake of efficiency (generally speaking, dynamic data structures are bad in runtime code), and to accommodate that change, some basic accessor functions. I changed where the content gets loaded to follow our model a little better and just implemented the fonts to support our basic needs.
Wow, that’s pretty simple. We will use an object of this class any time we want to cover up that beautiful artwork I sweated blood over with ugly text. Further, we can instantiate a bunch of these objects for any kind of text we want, and we will have a central code base to maintain.
In this case, to add a name to each character, we simply need to add String
and ScreenText
objects to the Character
class, like so:
public class Character { #region members //state attributes private Vector2 pos; private float scale; private float speed; private Logic.CharacterPath path; private Rectangle bBox; private String name; private GUI.ScreenText nameSprite;
and then hook it up correctly like so:
public Character(Texture2D tex, Texture2D mask, Vector2 p, float s, float spd,
Color bColor, Color bMaskColor, String n)
{
pos = p;
scale = s;
speed = spd;
bodyColor = bColor; bodyMaskColor = bMaskColor; drawMask = false; name = n; idleBodyFixed = new Sprite.CharacterSprite(tex, pos, scale); idleBodyFixed.Tint = bodyColor; idleBodyFixedMask = new Sprite.CharacterSprite(mask, pos, scale); idleBodyFixedMask.Tint = bodyMaskColor; path = new Logic.CharacterPath(); bBox = new Rectangle((int)pos.X, (int)pos.Y, w, h); nameSprite = new GUI.ScreenText(); } public void Update(GameTime gameTime) { path.Update(pos); nameSprite.RemoveNewest(); nameSprite.Print(name, new Vector2(pos.X + 32, pos.Y - 12), Color.Snow, (int)GUI.ScreenText.FontName.DEFAULT); if (path.Waypoint == Vector2.Zero) return; //compute the size of the next step Vector2 step = Vector2.Normalize(Vector2.Subtract(pos, path.Waypoint)) * speed * (float)gameTime.ElapsedGameTime.TotalSeconds; //if we are not within our "there" range, if ((Math.Max(2.0f, Math.Abs(pos.X - path.Waypoint.X)) > 2.0f) || (Math.Max(2.0f, Math.Abs(pos.Y - path.Waypoint.Y)) > 2.0f)) { pos -= step; } idleBodyFixed.Position = pos; idleBodyFixedMask.Position = pos; bBox.X = (int)pos.X + x; bBox.Y = (int)pos.Y + y; } public void Draw(SpriteBatch sb)
{
if (drawMask)
{
idleBodyFixedMask.Draw(sb);
}
idleBodyFixed.Draw(sb);
nameSprite.Draw(sb);
}
However, if you run it at this point, you will get some interesting errors. The error messages lie to you, but I’m here to tell you the truth: Remember that LoadContent()
method in ScreenText
? Yeah, well, we need to call it or we will be trying to draw a font to the screen with no fonts loaded…. We don’t have a LoadContent()
method in the Character
class, so we need to add one:
public void LoadContent(ContentManager Content) { nameSprite.LoadContent(Content); }
and then call it from CharacterManager
’s LoadContent()
method. We will also need to add an array of names to CharacterManager
. The changes are in bold below:
public class CharacterManager { #region members private Character[] toons; private String[] names; //utility members private Random rand; private float deltaTime; //constants public const int NUM_TOONS = 10; //make sure to add the same number of names in LoadContent! private const float INTERVAL = 1000f / 2f; //this clamps changes to the waypoints to 2 fps #endregion #region properties //none needed #endregion
#region constructors / init public CharacterManager() { toons = new Character[NUM_TOONS]; names = new String[NUM_TOONS]; rand = new Random(); deltaTime = 0; } public void LoadContent(ContentManager Content) { names[0] = "Xret"; names[1] = "Biix"; names[2] = "Laka"; names[3] = "Potr"; names[4] = "Mati"; names[5] = "Dibu"; names[6] = "Fhet"; names[7] = "Raas"; names[8] = "Tyeu"; names[9] = "Skao"; int y = rand.Next(248) + 410; //the walkable y-space in our environment int x = rand.Next(904) + 10; //the walkable x-space in our environment byte r = (byte)rand.Next(255); byte g = (byte)rand.Next(255); byte b = (byte)rand.Next(255); float spd = 150f; for (int i = 0; i < NUM_TOONS; i++) { toons[i] = new Character( Content.Load<Texture2D>("Character/SumaFixed"), Content.Load<Texture2D>("Character/SumaFixedGlowMask"), new Vector2(x, y), 1.0f, spd, new Color(r, g, b, 255), Color.GhostWhite, names[i]);
y = rand.Next(248) + 410; //the walkable y-space in our environment
x = rand.Next(904) + 10; //the walkable x-space in our environment
toons[i].Waypoint = new Vector2(x, y);
toons[i].LoadContent(Content);
y = rand.Next(248) + 410; //the walkable y-space in our environment
x = rand.Next(904) + 10; //the walkable x-space in our environment
r = (byte)rand.Next(255);
g = (byte)rand.Next(255);
b = (byte)rand.Next(255);
}
}
Voilà, like magic, names will appear over all the characters.
In the next chapter, we will deal with adding some in direct control of characters, add some basic GUI goodness, and talk finite state machine control. But now, put your feet up, crack a cold one, and contemplate a world with a mouse, collision detection—based character selection—and yet no game-development reality shows.
No, you can’t vote me off the island.
18.217.15.45