Sprites, Camera, Action!
After completing this chapter, you will be able to:
This chapter covers many of the additional concepts that are commonly needed to create full-featured and complete game projects, but were not introduced in previous chapters. These concepts include animation via sprite sheets, in-game camera manipulation, and audio for background music and cue effects. Animation, camera manipulation, and audio are often unnecessary for creating core game mechanics; however, including them creates a richer experience for end users by providing them with customized feedback for specific actions within your game.
Sprite animation
In games, you often want to have your characters or game objects appear as if they are in motion. While the previous chapters explained how to move game objects around the screen to create movement, because the images themselves were static they probably felt somewhat lifeless. To address this problem, you can use animation. As you may know, you create an animation by rapidly displaying related images in succession to create a sense of motion—and animation is no different in games. Typically, you use animation for simple motions such as walking and running, as well as more complex motions such as fighting strikes or dance moves. This section shows how you can add motion to your objects by using a sequence of images and a sprite sheet.
The Sprite Animation project
This project demonstrates how to move two animated characters around the screen. You are able to adjust the animation speed of each character in order to create a more fluid sense of movement across the screen. You can see an example of this project running in Figure 7-1.
Figure 7-1. The Sprite Animation project, zoomed in
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Add the following resource, which can be found in the Chapter07SourceCodeResources folder, into your content project before you begin:
To achieve animation, games often use what are known as sprites, or more specifically, sprite sheets. A sprite sheet is an image that contains an animation separated into one or more rows and columns (R×C). For example, in Figure 7-2 you can see a 4×2 sprite sheet that contains two separate animations. The animations depict a character walking right and left. In this example, the animations are separated into separate rows; however, this is not always the case. The organization of a sprite sheet is generally handled by its creator.
Figure 7-2. The individual frames needed to depict a game character walking right and left
For a more specific example, take a look at the top row of the sprite sheet in Figure 7-2. The images in this row depict the character walking to the right. To achieve this effect, each character image must to be displayed from left to right, in the sequence 1, 2, 3, 4. After the last character image has been displayed, the animation is looped back to the first image in the row. You can see an example of this in Figure 7-3.
Figure 7-3. A sprite animation sequence that loops
Now that you know the general usage that sprite sheets can provide, here’s a quick look at the math you need to achieve this type of behavior.Given that the sprite sheet is 4×2 and the image has a resolution of 256×128 pixels, you can deduce that each sprite has a resolution of 256/4×128/2 or 64×64 pixels.
While that works great for this particular sprite sheet example, you’ll need a more generalized equation to implement a class that can handle sprite sheets that contain varying numbers of sprites and resolutions.
However, this is still fairly simple to calculate. Given that the pixel resolution of the sprite sheet is equal to P×Q, where P represents the pixel width and Q represents the pixel height, then each sprite’s resolution within the sprite sheet is equal to P/R×Q/C, where R and C represent the numbers of rows and columns, respectively.
For example, if a sprite sheet has a pixel resolution of 256×256 and its rows and columns are 4×4, then each sprite has a resolution of 256/4×256/2, or 64×64 pixels. In general, sprite sheets are composed of sprites of equal size to allow easy targeting of each individual sprite. Sprite sheets with varying-size sprites do exist; however, you then need a more sophisticated calculation to single out each sprite.
With the basic knowledge of sprite sheets under your belt, you can now tackle the creation of a sprite class.
Creating the SpritePrimitive class
Note The padding of a sprite sheet depends upon its creator. Generally, a sprite sheet contains consistent padding around all sides.
public class SpritePrimitive :GameObject
{
private int mNumRow, mNumColumn, mPaddings;
private int mSpriteImageWidth, mSpriteImageHeight;
#region Per Animation setting
...
#endregion
...
}
private int mUserSpecifedTicks;
private int mCurrentTick;
private int mCurrentRow, mCurrentColumn;
private int mBeginRow, mEndRow;
private int mBeginCol, mEndCol;
public SpritePrimitive(String image, Vector2 position, Vector2 size,
int rowCounts, int columnCount, int padding) :
base(image, position, size)
{
mNumRow = rowCounts;
mNumColumn = columnCount;
mPaddings = padding;
mSpriteImageWidth = mImage.Width / mNumRow;
mSpriteImageHeight = mImage.Height / mNumColumn;
mUserSpecifedTicks = 1;
mCurrentTick = 0;
mCurrentRow = 0;
mCurrentColumn = 0;
mBeginRow = mBeginCol = mEndRow = mEndCol = 0;
}
public int SpriteBeginRow
{
get { return mBeginRow; }
set { mBeginRow = value; mCurrentRow = value; }
}
public int SpriteEndRow
{
get { return mEndRow; }
set { mEndRow = value; }
}
public int SpriteBeginColumn
{
get { return mBeginCol; }
set { mBeginCol = value; mCurrentColumn = value; }
}
public int SpriteEndColumn
{
get { return mEndCol; }
set { mEndCol = value; }
}
public int SpriteAnimationTicks
{
get { return mUserSpecifedTicks; }
set { mUserSpecifedTicks = value; }
}
public void SetSpriteAnimation(int beginRow, int beginCol, int endRow, int endCol, int tickInterval)
{
mUserSpecifedTicks = tickInterval;
mBeginRow = beginRow;
mBeginCol = beginCol;
mEndRow = endRow;
mEndCol = endCol;
mCurrentRow = mBeginRow;
mCurrentColumn = mBeginCol;
mCurrentTick = 0;
}
public override void Update()
{
base.Update();
mCurrentTick++;
if (mCurrentTick > mUserSpecifedTicks)
{
mCurrentTick = 0;
mCurrentColumn++;
if (mCurrentColumn > mEndCol)
{
mCurrentColumn = mBeginCol;
mCurrentRow++;
if (mCurrentRow > mEndRow)
mCurrentRow = mBeginRow;
}
}
}
public override void Draw()
{
// Define location and size of the texture
Rectangle destRect = Camera.ComputePixelRectangle(Position, Size);
int imageTop = mCurrentRow * mSpriteImageWidth;
int imageLeft = mCurrentColumn * mSpriteImageHeight;
// Define the area to draw from the spriteSheet
Rectangle srcRect = new Rectangle(
imageLeft + mPaddings,
imageTop + mPaddings,
mSpriteImageWidth, mSpriteImageHeight);
// Define the rotation origin
Vector2 org = new Vector2(mSpriteImageWidth/2, mSpriteImageHeight/2);
// Draw the texture
Game1.sSpriteBatch.Draw(
mImage,
destRect, // Area to be drawn in pixel space
srcRect, // Rect on the spriteSheet
Color.White, //
mRotateAngle, // Angle to rotate (clockwise)
org, // Image reference position
SpriteEffects.None, 0f);
if (null != Label)
FontSupport.PrintStatusAt(Position, Label, LabelColor);
}
Now you’ll modify the GameState class to use the SpritePrimitive class.
Modifying the GameState class
const int kSpriteSpeedFactor = 10; // Value of 1 maps to updates of 10 ticks
SpritePrimitive mHero, mAnotherHero;
SpritePrimitive mCurrent;
public GameState()
{
mHero = new SpritePrimitive(
"SimpleSpriteSheet",
new Vector2(20, 30), new Vector2(10, 10),
4, // Number of rows
2, // Number of columns
0); // Padding between images
mAnotherHero = new SpritePrimitive(
"SimpleSpriteSheet",
new Vector2(80, 30), new Vector2(10, 10),
4, // Number of rows
2, // Number of columns
0); // Padding between images
// Start mHero by walking left and mAnotherHero by walking right
mHero.SetSpriteAnimation(0, 0, 0, 3, 10); // Slowly
mAnotherHero.SetSpriteAnimation(1, 0, 1, 3, 5); // Twice as fast
mCurrent = mAnotherHero;
}
public void UpdateGame()
{
mHero.Update();
mAnotherHero.Update();
UserControlUpdate();
}
private void UserControlUpdate()
{
#region Selecting Hero
if (InputWrapper.Buttons.A == ButtonState.Pressed)
mCurrent = mHero;
if (InputWrapper.Buttons.B == ButtonState.Pressed)
mCurrent = mAnotherHero;
mCurrent.Position += InputWrapper.ThumbSticks.Right;
#endregion
#region Specifying rotation
if (InputWrapper.Buttons.X == ButtonState.Pressed)
mCurrent.RotateAngleInRadian += MathHelper.ToRadians(1);
if (InputWrapper.Buttons.Y == ButtonState.Pressed)
mCurrent.RotateAngleInRadian += MathHelper.ToRadians(-1);
#endregion
#region spriteSheet Update
if (InputWrapper.ThumbSticks.Left.X == 0)
{
mCurrent.SpriteEndColumn = 0; // Stops the animation
}
Else
{
float useX = InputWrapper.ThumbSticks.Left.X;
mCurrent.SpriteEndColumn = 3;
if (useX < 0)
{
mCurrent.SpriteBeginRow = 1;
mCurrent.SpriteEndRow = 1;
useX *= -1f;
}
else
{
mCurrent.SpriteBeginRow = 0;
mCurrent.SpriteEndRow = 0;
}
mCurrent.SpriteAnimationTicks = (int)((1f - useX) * kSpriteSpeedFactor);
}
#endregion
}
public void DrawGame()
{
mHero.Draw();
mAnotherHero.Draw();
}
In the preceding project, you saw how to use the principles of sprite sheets to create animations. However, if you use textures in various stages of the animation the pixel-accurate collision-detection function you created earlier will no longer function correctly. This is because your pixel collision-detection function assumes the textures being used are static. In this section, you will see how to implement pixel-accurate collision for animated sprites.
This project demonstrates how to move a character around the screen and collide it with pixel accuracy with several other objects. The character itself will be animated via a sprite sheet. The project displays the current collision status in the top-left corner of the screen. You can see an example of this project running in Figure 7-4.
Figure 7-4. Running the Sprite Collision project; the project accurately detects collisions, even for sprite characters
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Add the following resources, which can be found in the Chapter07SourceCodeResources folder, into your content project before you begin:
Modifying the TexturedPrimitive class
To support pixel-accurate collision detection for sprites, you’ll need to modify the TexturedPrimitive class. You do this by adding accessors for the sprite’s position and size on the sprite sheet. Set the accessors to the top-left corner of the image and sprite sheet size by default. Notice that in the following code, the accessors are defined as virtual. By defining these as virtual, you allow those classes that inherit from the TexturedPrimitive class to override the accessors to return their desired values.
protected virtual int SpriteTopPixel { get { return 0; } }
protected virtual int SpriteLeftPixel { get { return 0; } }
protected virtual int SpriteImageWidth { get { return mImage.Width; } }
protected virtual int SpriteImageHeight { get { return mImage.Height; } }
Modifying the SpritePrimitive class
Override the parent accessors to return the proper top-left position and size of the current sprite within the sprite sheet:
#region override to support per-pixel collision
protected override int SpriteTopPixel
{
get { return mCurrentRow * mSpriteImageHeight; }
}
protected override int SpriteLeftPixel
{
get { return mCurrentColumn * mSpriteImageWidth; }
}
protected override int SpriteImageWidth
{
get { return mSpriteImageWidth; }
}
protected override int SpriteImageHeight
{
get { return mSpriteImageHeight; }
}
#endregion
Next, you’ll modify the TexturedPrimitivePixelCollide partial class, which you created earlier to handle pixel-accurate collision for textures, so it can accommodate varying sprites.
Modifying the TexturedPrimitivePixelCollide partial class
public bool PixelTouches(TexturedPrimitive otherPrim, out Vector2 collidePoint)
{
...
if (touches)
{
...
int i = 0;
while ( (!pixelTouch) && (i<SpriteImageWidth) )
{
int j = 0;
while ( (!pixelTouch) && (j<SpriteImageHeight) )
{
...
}
...
}
}
...
}
private Color GetColor(int i, int j)
{
return mTextureColor[((j+SpriteTopPixel) * mImage.Width) + i + SpriteLeftPixel];
}
private Vector2 IndexToCameraPosition(int i, int j, Vector2 xDir, Vector2 yDir)
{
float x = i * Width / (float)(SpriteImageWidth - 1);
float y = j * Height / (float)(SpriteImageHeight- 1);
Vector2 r = Position
+ (x - (mSize.X * 0.5f)) * xDir
- (y - (mSize.Y * 0.5f)) * yDir;
return r;
}
private Vector2 CameraPositionToIndex(Vector2 p, Vector2 xDir, Vector2 yDir)
{
Vector2 delta = p - Position;
float xOffset = Vector2.Dot(delta, xDir);
float yOffset = Vector2.Dot(delta, yDir);
float i = SpriteImageWidth * (xOffset / Width);
float j = SpriteImageHeight * (yOffset / Height);
i += SpriteImageWidth / 2;
j = (SpriteImageHeight / 2) - j;
return new Vector2(i, j);
}
Note A dictionary is a data structure that contains a unique key for every value, thus providing fast lookups when looking for a specific value.
In the function that follows, you can see that along with the dictionary (sTextureData), the LoadColorInfo() function returns the necessary image data (an array of colors) depending upon the input parameters.
public partial class TexturedPrimitive
{
...
#region Static support for sharing color data across same image
static Dictionary<String, Color[]> sTextureData =
new Dictionary<string, Color[]>();
static private Color[] LoadColorInfo(String imageName, Texture2D image)
{
Color[] imageData = new Color[image.Width * image.Height];
image.GetData(imageData);
sTextureData.Add(imageName, imageData);
return imageData;
}
#endregion
...
}
private void ReadColorData()
{
if (sTextureData.ContainsKey(mImageName))
mTextureColor = sTextureData[mImageName];
else
mTextureColor = LoadColorInfo(mImageName, mImage);
}
Finally, you’ll modify the GameState class to use the newly created sprite collision.
public class GameState
{
const int kSpriteSpeedFactor = 10; // Value of 1 maps to updates of 10 ticks
SpritePrimitive mHero; // Hero sprite
const int kNumPlanes = 4;
TexturedPrimitive[] mPlane; // The planes
TexturedPrimitive mFlower; // The large background
TexturedPrimitive mCurrentPrim; // Refer to either plane or flower
// Support for displaying of collision
TexturedPrimitive mHeroTarget; // Where latest hero pixel collision happened
bool mHeroPixelCollision; // If there is a pixel collision for the hero
bool mHeroBoundCollision; // If there is an image-bound collision for the hero
public GameState()
{
...
}
...
}
public GameState()
{
// Set up the flower ...
mFlower = new TexturedPrimitive("Flower", new Vector2(50, 35), new Vector2(60, 60));
// Planes
mPlane = new TexturedPrimitive[kNumPlanes];
mPlane[0] =
new TexturedPrimitive("PatrolEnemy", new Vector2(10, 15), new Vector2(5, 10));
mPlane[1] =
new TexturedPrimitive("PatrolEnemy", new Vector2(90, 15), new Vector2(5, 10));
mPlane[2] =
new TexturedPrimitive("PatrolEnemy", new Vector2(90, 55), new Vector2(5, 10));
mPlane[3] =
new TexturedPrimitive("PatrolEnemy", new Vector2(10, 55), new Vector2(5, 10));
mHeroTarget = new TexturedPrimitive("Target", new Vector2(0, 0), new Vector2(3, 3));
mCurrentPrim = mPlane[0];
mHeroBoundCollision = false;
mHeroPixelCollision = false;
mHero = new SpritePrimitive(
"SimpleSpriteSheet",
new Vector2(20, 30), new Vector2(10, 10),
4, // Number of rows
2, // Number of columns
0); // Padding between images
// Start Hero by walking left and AnotherHero by walking right
mHero.SetSpriteAnimation(0, 0, 0, 3, 10);
}
public void UpdateGame()
{
mHero.Position += InputWrapper.ThumbSticks.Left;
mHero.Update();
CollisionUpdate();
UserControlUpdate();
}
private void CollisionUpdate()
{
Vector2 pixelCollisionPosition = Vector2.Zero;
#region Collide the hero with the flower
mHeroBoundCollision = mHero.PrimitivesTouches(mFlower);
mHeroPixelCollision = mHeroBoundCollision;
if (mHeroBoundCollision)
{
mHeroPixelCollision =
mHero.PixelTouches(mFlower, out pixelCollisionPosition);
if (mHeroPixelCollision)
mHeroTarget.Position = pixelCollisionPosition;
}
#endregion
#region Collide the hero with planes
int i = 0;
while ((!mHeroPixelCollision) && (i < kNumPlanes))
{
mHeroBoundCollision = mPlane[i].PrimitivesTouches(mHero);
mHeroPixelCollision = mHeroBoundCollision;
if (mHeroBoundCollision)
{
mHeroPixelCollision =
mPlane[i].PixelTouches(mHero, out pixelCollisionPosition);
if (mHeroPixelCollision)
mHeroTarget.Position = pixelCollisionPosition;
}
i++;
}
#endregion
}
private void UserControlUpdate()
{
#region Selecting Hero
if (InputWrapper.Buttons.A == ButtonState.Pressed)
mCurrentPrim = mFlower;
if (InputWrapper.Buttons.B == ButtonState.Pressed)
mCurrentPrim = mPlane[0];
mCurrentPrim.Position += InputWrapper.ThumbSticks.Right;
#endregion
#region Specifying hero rotation
if (InputWrapper.Buttons.X == ButtonState.Pressed)
mHero.RotateAngleInRadian += MathHelper.ToRadians(1);
if (InputWrapper.Buttons.Y == ButtonState.Pressed)
mHero.RotateAngleInRadian += MathHelper.ToRadians(-1);
#endregion
#region Specifying flower rotation
mCurrentPrim.RotateAngleInRadian += MathHelper.ToRadians(
InputWrapper.Triggers.Left);
mCurrentPrim.RotateAngleInRadian -= MathHelper.ToRadians(
InputWrapper.Triggers.Right);
#endregion
#region Sprite Sheet Update
...
#endregion
}
public void DrawGame()
{
mFlower.Draw();
mHero.Draw();
foreach (var p in mPlane)
p.Draw();
if (mHeroPixelCollision)
mHeroTarget.Draw();
FontSupport.PrintStatus("Collisions Bound(" + mHeroBoundCollision +
") Pixel(" + mHeroPixelCollision + ")", null);
}
Moving and zooming the camera
In the example games you’ve built thus far, the camera view has remained in a static position and at a static zoom level. In this section, you will learn how to add zoom-and-move features to your Camera class. By creating a more fully featured Camera class, you will be able to reproduce common camera effects seen in many 2D games. For example, zooming lets users focus their attention on a particular portion of the screen. Additionally, you can employ zoom to create a zoom-out effect that lets players see more of your game world on the screen at one time. Moving the camera provides the ability to pan to points of interest and attach the camera to your hero character or game object. By attaching the camera to your hero character, you can easily reproduce the first-person point of view commonly seen in side-scrolling games and top-down adventure games.
The Camera Zoom Move project
This project demonstrates how to manipulate the camera’s position and zoom level using the gamepad. You’ll see how the camera can move relative to both a large texture and a hero character. You control the hero character. The camera will chase the hero character to keep it within the game window. You can see an example of this project running in Figure 7-5.
Figure 7-5. The Camera Zoom Move project, zoomed in on a portion of the flower game object
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Modifying the Camera class
static public void MoveCameraBy(Vector2 delta)
{
sOrigin += delta;
}
Notice that the inner black rectangle is defined by variables labeled old—more specifically, an old origin, width, and height. To define the outer red rectangle, you must calculate all new values. You can calculate the new width and height easily by adding the change in width and height to the camera’s old width and height. In the image, this change is defined as dx and dy in the image. However, because the rectangle is centered, the total change is 2dx and 2dy. With that new information in hand, you can deduce the following:
Therefore, the following is true:
static public void ZoomCameraBy(float deltaX)
{
float oldW = sWidth;
float oldH = sHeight;
sWidth = sWidth + deltaX;
sRatio = -1f;
cameraWindowToPixelRatio();
float dx = 0.5f * (sWidth - oldW);
float dy = 0.5f * (sHeight - oldH);
sOrigin -= new Vector2(dx, dy);
}
Now that your Camera class supports the required manipulation, you can take advantage of the camera’s new functionality within the GameState class.
Modifying the GameState class
public GameState()
{
// Set up the flower ...
mFlower = new TexturedPrimitive("Flower",
new Vector2(50, 35), new Vector2(350, 350));
// Planes
mPlane = new TexturedPrimitive[kNumPlanes];
mPlane[0] = new TexturedPrimitive("PatrolEnemy",
new Vector2(20, -80), new Vector2(20, 40));
mPlane[1] = new TexturedPrimitive("PatrolEnemy",
new Vector2(150, -100), new Vector2(20, 40));
mPlane[2] = new TexturedPrimitive("PatrolEnemy",
new Vector2(150, 120), new Vector2(20, 40));
mPlane[3] = new TexturedPrimitive("PatrolEnemy",
new Vector2(20, 170), new Vector2(20, 40));
mHeroTarget = new TexturedPrimitive("Target", new Vector2(0, 0), new Vector2(3, 3));
mHeroBoundCollision = false;
mHeroPixelCollision = false;
mHero = new SpritePrimitive(
"SimpleSpriteSheet",
new Vector2(10, 10),
new Vector2(10, 10),
4, // Number of rows
2, // Number of columns
0); // Padding between images
// Start Hero by walking left and AnotherHero by walking toward right
mHero.SetSpriteAnimation(0, 0, 0, 3, 10); // Slowly
}
public void UpdateGame()
{
// Change the hero position by thumbstick
Vector2 heroMoveDelta = InputWrapper.ThumbSticks.Left;
mHero.Position += heroMoveDelta;
mHero.Update();
CollisionUpdate();
// Back hero out of the collision!
if (mHeroPixelCollision)
mHero.Position -= heroMoveDelta;
HeroMovingCameraWindow();
UserControlUpdate();
}
private void HeroMovingCameraWindow()
{
Camera.CameraWindowCollisionStatus status = Camera.CollidedWithCameraWindow(mHero);
Vector2 delta = Vector2.Zero;
Vector2 cameraLL = Camera.CameraWindowLowerLeftPosition;
Vector2 cameraUR = Camera.CameraWindowUpperRightPosition;
const float kChaseRate = 0.05f;
float kBuffer = mHero.Width * 2f;
switch (status)
{
case Camera.CameraWindowCollisionStatus.CollideBottom:
delta.Y = (mHero.Position.Y - kBuffer - cameraLL.Y) * kChaseRate;
break;
case Camera.CameraWindowCollisionStatus.CollideTop:
delta.Y = (mHero.Position.Y + kBuffer - cameraUR.Y) * kChaseRate;
break;
case Camera.CameraWindowCollisionStatus.CollideLeft:
delta.X = (mHero.Position.X - kBuffer - cameraLL.X) * kChaseRate;
break;
case Camera.CameraWindowCollisionStatus.CollideRight:
delta.X = (mHero.Position.X + kBuffer - cameraUR.X) * kChaseRate;
break;
}
Camera.MoveCameraBy(delta);
}
private void UserControlUpdate()
{
#region Specifying hero rotation
...
#endregion
#region Sprite Sheet Update
...
#endregion
#region Camera Control
// Zooming in/out with buttons A and B
if (InputWrapper.Buttons.A == ButtonState.Pressed)
Camera.ZoomCameraBy(5);
if (InputWrapper.Buttons.B == ButtonState.Pressed)
Camera.ZoomCameraBy(-5);
// Move the camera with right thumbstick
Camera.MoveCameraBy(InputWrapper.ThumbSticks.Right);
#endregion
}
public void DrawGame()
{
mFlower.Draw();
foreach (var p in mPlane)
p.Draw();
mHero.Draw();
if (mHeroPixelCollision)
mHeroTarget.Draw();
FontSupport.PrintStatus("Collisions Bound(" +
mHeroBoundCollision + ") Pixel(" +
mHeroPixelCollision + ")", null);
}
Adding audio
In general, audio effects used in games fall into two categories. The first category is background audio, which includes music or ambient effects, and is often used to bring atmosphere or emotion to different portions of the game. The second category is sound effects. Sound effects are useful for all sorts of things, from notifying users of game actions to hearing the footfalls of your hero character. Usually, sound effects represent a specific action, triggered either by the user or by the game itself. Such sound effects are often thought of as an audio cue.
One important difference between these two types of audio is how you control them. Sound effects or cues cannot be stopped or have their volume adjusted once they have started; therefore, cues are generally very short. On the other hand, background audio can be started and stopped at will, and you can adjust the volume during playback. These capabilities are useful for fading background audio in or out depending on the current GameState class, or for stopping the background track completely and starting another one.
In this project, as in the previous one, you can move the hero character as well as the camera; however, this version triggers sound effects as various actions are performed. Additionally, a background track will be played at the start of the project. You can see an example of this project running in Figure 7-6.
Figure 7-6. Running the Audio project; it looks the same as the previous project, but includes both background music and audio cues
The project’s controls are as follows:
The goals of the project are as follows:
The steps for creating the project are as follows:
Like Font resources, as covered in Chapter 3, audio files must first be converted into xnb format before they can be included in a MonoGame project. Once again, we can use the XNAFormatter program to convert the following resources found in the Chapter07SourceCodeResources folder:
Once converted, the corresponding xnb files can be included in the MonoGame project. Right click over the Content folder and go to Add Existing Items, or simply drag and drop the xnb files into the Content folder.
Note Like with images and fonts, if you choose to drag and drop the files into the Content folder, you must remember to bring up the Properties window of each audio resource and change the Build Action field to Content, and Copy to Output Directory to Copy if newer.
Creating the AudioSupport class
static public class AudioSupport
{
// Audio effect files
private static Dictionary<String, SoundEffect> sAudioEffects =
new Dictionary<string,SoundEffect>();
// Constant background audio
private static SoundEffectInstance sBackgroundAudio = null;
...
}
static private SoundEffect FindAudioClip(String name)
{
SoundEffect sound = null;
if (sAudioEffects.ContainsKey(name))
sound = sAudioEffects[name];
else
{
sound = Game1.sContent.Load<SoundEffect>(name);
if (null != sound)
sAudioEffects.Add(name, sound);
}
return sound;
}
static public void PlayACue(String cueName)
{
SoundEffect sound = FindAudioClip(cueName);
if (null != sound)
sound.Play();
}
static private void StartBg(String name, float level)
{
SoundEffect bgm = FindAudioClip(name);
sBackgroundAudio = bgm.CreateInstance();
sBackgroundAudio.IsLooped = true;
sBackgroundAudio.Volume = level;
sBackgroundAudio.Play();
}
static private void StopBg()
{
if (null != sBackgroundAudio)
{
sBackgroundAudio.Pause();
sBackgroundAudio.Stop();
sBackgroundAudio.Volume = 0f;
sBackgroundAudio.Dispose();
}
sBackgroundAudio = null;
}
static public void PlayBackgroundAudio(String bgAudio, float level)
{
StopBg();
if (("" != bgAudio) || (null != bgAudio))
{
level = MathHelper.Clamp(level, 0f, 1f);
StartBg(bgAudio, level);
}
}
Now that the audio support class is finished, you can use it within the GameState class by playing effects for various actions.
public GameState()
{
...
// Begin background audio
AudioSupport.PlayBackgroundAudio("Mind_Meld", 0.4f);
}
public void UpdateGame()
{
...
// Collide the hero with the flower
mHeroBoundCollision = mHero.PrimitivesTouches(mFlower);
mHeroPixelCollision = mHeroBoundCollision;
if (mHeroBoundCollision)
{
mHeroPixelCollision = mHero.PixelTouches(mFlower, out pixelCollisionPosition);
if (mHeroPixelCollision)
{
AudioSupport.PlayACue("Bounce");
}
}
...
}
public void UpdateGame()
{
...
int i = 0;
while ((!mHeroPixelCollision) && (i < kNumPlanes))
{
mHeroBoundCollision = mPlane[i].PrimitivesTouches(mHero);
mHeroPixelCollision = mHeroBoundCollision;
if (mHeroBoundCollision)
{
mHeroPixelCollision =
mPlane[i].PixelTouches(mHero, out pixelCollisionPosition);
if (mHeroPixelCollision)
{
AudioSupport.PlayACue("Wall");
}
}
i++;
}
...
}
Summary
In this chapter, you have seen how to add animation, camera manipulation, and audio to your game project. By implementing these concepts, you can provide players with an enriched experience as they interact with your game. Animation lets you give motion and add realism to your game objects by iterating through a sequence of images. Animation gives visual feedback to the user for things such as player movements and actions.
Additionally, by implementing camera manipulation, you are able to create larger and more interesting game worlds, as well as direct the player’s attention to specific game elements. You accomplish this with basic move and zoom functionality.
Finally, you saw how to implement two different types of audio: background music and audio cues (or sound effects). You can use background music to set the mood or atmosphere of the game, and you control its playback at run time. You use audio cues or sound effects to augment a specific action of the player or the game. Audio cues, once started, cannot be modified.
Quick reference
To |
Do this |
---|---|
Create simple animation for an object | Identify the corresponding begin and end (row, column) positions on the sprite sheet. |
Instantiate a SpritePrimitive object | 1. Identify and include the desired sprite sheet image in your project, making sure you take note of the file name. 2. Instantiate a SpritePrimitive game object with the corresponding file name. 3. Define the size and position of the game object. 4. Configure the game object to correspond to the amount of rows, columns, and padding on the sprite sheet. |
Define a sprite animation with SpritePrimitive | Call the SetSpriteAnimation() function, as follows: 1. Identify the beginning and end (row, column) of the animation. 2. Define the speed of the animation as a function of the number of Update() function calls. |
Ensure sprite animation is shown | Call the Update() function of the SpritePrimitive object in GameState.UpdateGame(). |
Support pixel-accurate collision for an animated sprite | Override the texture size and position functions of the TexturedPrimitive class to return the corresponding information for one sprite. Having the TexturedPrimitivePixelCollide class refer to these new definitions results in pixel-accurate collision support for individual sprites. |
Move a camera | Call Camera.MoveCameraBy(deltaMovement), where deltaMovement describes the amount that the camera position will be moved in the x and y directions. |
Zoom the camera in or out | Call Camera.ZoomCameraBy(deltaSize), where a positive deltaSize causes the camera to zoom out (see more of the game window), and a negative value causes the camera to zoom into the game window. |
Have the camera follow an object | Continuously detect the collisions between the object and the camera (by calling Camera.CollidedWithCameraWindow()) and move the camera position accordingly. Refer to Chapter 3 for the details of Camera.CollidedWithCameraWindow(). |
Start background audio | Call AudioSupport.PlayBackgroundAudio("file name", level). |
Stop background audio | Call AudioSupport.PlayBackgroundAudio(null, 0). |
Play an audio cue | Call AudioSupport.PlayACue("cueName"). |