Using Animated Pieces in a Board-based Game with XNA 4.0

Published February 25, 2012 by Kurt Jaegers, posted by Gaiiden
Do you see issues with this article? Let us know.
Advertisement
This article by Kurt Jaegers, author of XNA 4.0 Game Development by Example: Beginner's Guide, enhances a board-based puzzle game called Flood Control in XNA 4.0 using animation. In this article, we will cover:

  • Animating the rotation of pieces when manipulated by the player
  • Gradually fading out pieces of completed scoring chains
  • Animating the falling of pieces into place on the board

All of these enhancements will give the player a better game experience, as well as give us the opportunity to learn more about how the SpriteBatch class can be used for animation.

Animated pieces


We will define three different types of animated pieces: rotating, falling, and fading. The animation for each of these types will be accomplished by altering the parameters of the SpriteBatch.Draw() call.

Classes for animated pieces


In order to represent the three types of animated pieces, we will create three new classes. Each of these classes will inherit from the GamePiece class, meaning they will contain all of the methods and members of the GamePiece class, but add additional information to support the animation.

[indent=1]Child classes
Child classes inherit all of their parent's members and methods. The RotatingPiece class can refer to the pieceType and suffix of the piece without recreating them within RotatingPiece itself. Additionally, child classes can extend the functionality of their base class, adding new methods and properties or overriding old ones. In fact, Game1 itself is a child of the Micrsoft.Xna.Game class, which is why all of the methods we use (Update(), Draw(), LoadContent(), and so on) are declared as "override".

Let's begin by creating the class we will use for rotating pieces.

Time for action - rotating pieces


Open your existing Flood Control project in Visual C# Express if it is not already active.

Add a new class to the project called "RotatingPiece".

Add "using Microsoft.Xna.Framework;" to the using area at the top of the class.

Update the declaration of the class to read class RotatingPiece : GamePiece.

Add the following declarations to the RotatingPiece class:

public bool clockwise;

public static float rotationRate = (MathHelper.PiOver2 / 10);
private float rotationAmount = 0;
public int rotationTicksRemaining = 10;

Add a property to retrieve the current rotation amount:

public float RotationAmount
{
get
{
if (clockwise)
return rotationAmount;
else
return (MathHelper.Pi*2) - rotationAmount;
}
}

Add a constructor for the RotatingPiece class:

public RotatingPiece(string pieceType, bool clockwise)
: base(pieceType)
{
this.clockwise = clockwise;
}

Add a method to update the piece:

public void UpdatePiece()
{
rotationAmount += rotationRate;
rotationTicksRemaining = (int)MathHelper.Max(0,
rotationTicksRemaining-1);
}

What just happened?


In step 2, we modified the declaration of the RotatingPiece class by adding : GamePiece to the end of it. This indicates to Visual C# that the RotatingPiece class is a child of the GamePiece class.

The clockwise variable stores a "true" value if the piece will be rotating clockwise and "false" if the rotation is counter-clockwise.

When a game piece is rotated, it will turn a total of 90 degrees (or pi/2 radians) over 10 animation frames. The MathHelper class provides a number of constants to represent commonly used numbers, with MathHelper.PiOver2 being equal to the number of radians in a 90 degree angle. We divide this constant by 10 and store the result as the rotationRate for use later. This number will be added to the rotationAmount float, which will be referenced when the animated piece is drawn.

[indent=1]Working with radians
All angular math is handled in radians from XNA's point of view. A complete (360 degree) circle contains 2*pi radians. In other words, one radian is equal to about 57.29 degrees. We tend to relate to circles more often in terms of degrees (a right angle being 90 degrees, for example), so if you prefer to work with degrees, you can use the MathHelper.ToRadians() method to convert your values when supplying them to XNA classes and methods.

The final declaration, rotationTicksRemaining, is reduced by one each time the piece is updated. When this counter reaches zero, the piece has finished animating.

When the piece is drawn, the RotationAmount property is referenced by a spriteBatch. Draw() call and returns either the rotationAmount property (in the case of a clockwise rotation) or 2*pi (a full circle) minus the rotationAmount if the rotation is counter-clockwise.

The constructor in step 7 illustrates how the parameters passed to a constructor can be forwarded to the class' parent constructor via the :base specification. Since the GamePiece class has a constructor that accepts a piece type, we can pass that information along to its constructor while using the second parameter (clockwise) to update the clockwise member that does not exist in the GamePiece class. In this case, since both the clockwise member and the clockwise parameter have identical names, we specify this.clockwise to refer to the clockwise member of the RotatingPiece class. Simply clockwise in this scope refers only to the parameter passed to the constructor.

[indent=1]this notation
You can see that it is perfectly valid C# code to have method parameter names that match the names of class variables, thus potentially hiding the class variables from being used in the method (since referring to the name inside the method will be assumed to refer to the parameter). To ensure that you can always access your class variables even when a parameter name conflicts, you can preface the variable name with this. when referring to the class variable. this. indicates to C# that the variable you want to use is part of the class, and not a local method parameter.

Lastly, the UpdatePiece() method simply increases the rotationAmount member while decreasing the rotationTicksRemaining counter (using MathHelper.Max() to ensure that the value does not fall below zero).

Time for action - falling pieces


Add a new class to the Flood Control project called "FallingPiece".

Add using Microsoft.Xna.Framework; to the using area at the top of the class.

Update the declaration of the class to read class FallingPiece : GamePiece

Add the following declarations to the FallingPiece class:

public int VerticalOffset;
public static int fallRate = 5;

Add a constructor for the FallingPiece class:

public FallingPiece(string pieceType, int verticalOffset)
: base(pieceType)
{
VerticalOffset = verticalOffset;
}

Add a method to update the piece:

public void UpdatePiece()
{
VerticalOffset = (int)MathHelper.Max(
0,
VerticalOffset - fallRate);
}

What just happened?


Simpler than a RotatingPiece, a FallingPiece is also a child of the GamePiece class. A falling piece has an offset (how high above its final destination it is currently located) and a falling speed (the number of pixels it will move per update).

As with a RotatingPiece, the constructor passes the pieceType parameter to its base class constructor and uses the verticalOffset parameter to set the VerticalOffset member. Note that the capitalization on these two items differs. Since VerticalOffset is declared as public and therefore capitalized by common C# convention, there is no need to use the "this" notation, since the two variables technically have different names.

Lastly, the UpdatePiece() method subtracts fallRate from VerticalOffset, again using the MathHelper.Max() method to ensure the offset does not fall below zero.

Time for action - fading pieces


Add a new class to the Flood Control project called "FadingPiece".

Add using Microsoft.Xna.Framework; to the using area at the top of the class.

Update the declaration of the class to read class FadingPiece : GamePiece

Add the following declarations to the FadingPiece class:

public float alphaLevel = 1.0f;
public static float alphaChangeRate = 0.02f;

Add a constructor for the FadingPiece class:

public FadingPiece(string pieceType, string suffix)
: base(pieceType, suffix)
{
}

Add a method to update the piece:

public void UpdatePiece()
{
alphaLevel = MathHelper.Max(
0,
alphaLevel - alphaChangeRate);
}

What just happened?


The simplest of our animated pieces, the FadingPiece only requires an alpha value (which always starts at 1.0f, or fully opaque) and a rate of change. The FadingPiece constructor simply passes the parameters along to the base constructor.

When a FadingPiece is updated, alphaLevel is reduced by alphaChangeRate, making the piece more transparent.

Managing animated pieces


Now that we can create animated pieces, it will be the responsibility of the GameBoard class to keep track of them. In order to do that, we will define a Dictionary object for each type of piece.

A Dictionary is a collection object similar to a List, except that instead of being organized by an index number, a dictionary consists of a set of key and value pairs. In an array or a List, you might access an entity by referencing its index as in dataValues[2] = 12; With a Dictionary, the index is replaced with your desired key type. Most commonly this will be a string value. This way, you can do something like fruitColors["Apple"]="red";

Time for action - updating GameBoard to support animated pieces


In the declarations section of the GameBoard class, add three dictionaries:

public Dictionary fallingPieces =
new Dictionary();
public Dictionary rotatingPieces =
new Dictionary();
public Dictionary fadingPieces =
new Dictionary();

Add methods to the GameBoard class to create new falling piece entries in the dictionaries:

public void AddFallingPiece(int X, int Y,
string PieceName, int VerticalOffset)
{
fallingPieces[X.ToString() + "_" + Y.ToString()] = new
FallingPiece(PieceName, VerticalOffset);
}

public void AddRotatingPiece(int X, int Y,
string PieceName, bool Clockwise)
{
rotatingPieces[X.ToString() + "_" + Y.ToString()] = new
RotatingPiece(PieceName, Clockwise);
}

public void AddFadingPiece(int X, int Y, string PieceName)
{
fadingPieces[X.ToString() + "_" + Y.ToString()] = new
FadingPiece(PieceName,"W");
}

Add the ArePiecesAnimating() method to the GameBoard class:

{
if ((fallingPieces.Count == 0) &&
(rotatingPieces.Count == 0) &&
(fadingPieces.Count == 0))
{
return false;
}
else
{
return true;
}
}

Add the UpdateFadingPieces() method to the GameBoard class:

private void UpdateFadingPieces()
{
Queue RemoveKeys = new Queue();

foreach (string thisKey in fadingPieces.Keys)
{
fadingPieces[thisKey].UpdatePiece();
if (fadingPieces[thisKey].alphaLevel == 0.0f)
RemoveKeys.Enqueue(thisKey.ToString());
}
while (RemoveKeys.Count > 0)
fadingPieces.Remove(RemoveKeys.Dequeue());
}

Add the UpdateFallingPieces() method to the GameBoard class:

private void UpdateFallingPieces()
{
Queue RemoveKeys = new Queue();
foreach (string thisKey in fallingPieces.Keys)
{
fallingPieces[thisKey].UpdatePiece();
if (fallingPieces[thisKey].VerticalOffset == 0)
RemoveKeys.Enqueue(thisKey.ToString());
}
while (RemoveKeys.Count > 0)
fallingPieces.Remove(RemoveKeys.Dequeue());
}

Add the UpdateRotatingPieces() method to the GameBoard class:

private void UpdateRotatingPieces()
{
Queue RemoveKeys = new Queue();
foreach (string thisKey in rotatingPieces.Keys)
{
rotatingPieces[thisKey].UpdatePiece();
if (rotatingPieces[thisKey].rotationTicksRemaining == 0)
RemoveKeys.Enqueue(thisKey.ToString());
}
while (RemoveKeys.Count > 0)
rotatingPieces.Remove(RemoveKeys.Dequeue());
}

Add the UpdateAnimatedPieces() method to the GameBoard class:

public void UpdateAnimatedPieces()
{
if (fadingPieces.Count == 0)
{
UpdateFallingPieces();
UpdateRotatingPieces();
}
else
{
UpdateFadingPieces();
}
}

What just happened?


After declaring the three Dictionary objects, we have three methods used by the GameBoard class to create them when necessary. In each case, the key is built in the form "X_Y", so an animated piece in column 5 on row 4 will have a key of "5_4". Each of the three Add... methods simply pass the parameters along to the constructor for the appropriate piece types after determining the key to use.

When we begin drawing the animated pieces, we want to be sure that animations finish playing before responding to other input or taking other game actions (like creating new pieces). The ArePiecesAnimating() method returns "true" if any of the Dictionary objects contain entries. If they do, we will not process any more input or fill empty holes on the game board until they have completed.

The UpdateAnimatedPieces() method will be called from the game's Update() method and is responsible for calling the three different update methods above (UpdateFadingPiece(), UpdateFallingPiece(), and UpdateRotatingPiece()) for any animated pieces currently on the board. The first line in each of these methods declares a Queue object called RemoveKeys. We will need this because C# does not allow you to modify a Dictionary (or List, or any of the similar "generic collection" objects) while a foreach loop is processing them.

A Queue is yet another generic collection object that works like a line at the bank. People stand in a line and await their turn to be served. When a bank teller is available, the first person in the line transacts his/her business and leaves. The next person then steps forward. This type of processing is known as FIFO, or First In, First Out.

Using the Enqueue() and Dequeue() methods of the Queue class, objects can be added to the Queue (Enqueue()) where they await processing. When we want to deal with an object, we Dequeue() the oldest object in the Queue and handle it. Dequeue() returns the first object waiting to be processed, which is the oldest object added to the Queue.

Collection classes


C# provides a number of different "collection" classes, such as the Dictionary, Queue, List, and Stack objects. Each of these objects provides different ways to organize and reference the data in them. For information on the various collection classes and when to use each type, see the following MSDN entry: http://msdn.microsof.../6tc79sx1(VS.80).aspx

Each of the update methods loops through all of the keys in its own Dictionary and in turn calls the UpdatePiece() method for each key. Each piece is then checked to see if its animation has completed. If it has, its key is added to the RemoveKeys queue. After all of the pieces in the Dictionary have been processed, any keys that were added to RemoveKeys are then removed from the Dictionary, eliminating those animated pieces.

If there are any FadingPieces currently active, those are the only animated pieces that UpdateAnimatedPieces() will update. When a row is completed, the scoring tiles fade out, the tiles above them fall into place, and new tiles fall in from above. We want all of the fading to finish before the other tiles start falling (or it would look strange as the new tiles pass through the fading old tiles).

Fading pieces


In the discussion of UpdateAnimatedPieces(), we stated that fading pieces are added to the board whenever the player completes a scoring chain. Each piece in the chain is replaced with a fading piece.

Time for action - generating fading pieces


In the Game1 class, modify the CheckScoringChain() method by adding the following call inside the foreach loop before the square is set to "Empty":

gameBoard.AddFadingPiece(
(int)ScoringSquare.X,
(int)ScoringSquare.Y,
gameBoard.GetSquare(
(int)ScoringSquare.X,
(int)ScoringSquare.Y));

What just happened?


Adding fading pieces is simply a matter of getting the square (before it is replaced with an empty square) and adding it to the FadingPieces dictionary. We need to use the (int) typecasts because the ScoringSquare variable is a Vector2 value, which stores its X and Y components as floats.

Falling pieces


Falling pieces are added to the game board in two possible locations: From the FillFromAbove() method when a piece is being moved from one location on the board to another, and in the GenerateNewPieces() method, when a new piece falls in from the top of the game board.

Time for action - generating falling pieces


Modify the FillFromAbove() method of the GameBoard class by adding a call to generate falling pieces right before the rowLookup = -1; line:

AddFallingPiece(x, y, GetSquare(x, y),
GamePiece.PieceHeight *(y-rowLookup));

Update the GenerateNewPieces() method by adding the following call right after the RandomPiece(x,y) line:

AddFallingPiece(x, y, GetSquare(x, y),
GamePiece.PieceHeight * GameBoardHeight);

What just happened?


When FillFromAbove() moves a piece downward, we now create an entry in the FallingPieces dictionary that is equivalent to the newly moved piece. The vertical offset is set to the height of a piece (40 pixels) times the number of board squares the piece was moved. For example, if the empty space was at location 5,5 on the board, and the piece above it (5,4) is being moved down one block, the animated piece is created at 5,5 with an offset of 40 pixels (5-4 = 1, times 40).

When new pieces are generated for the board, they are added with an offset equal to the height (in pixels) of the game board, determined by multiplying the GamePiece.PieceHeight value by the GameBoardHeight. This means they will always start above the playing area and fall into it.

Rotating pieces


The last type of animated piece we need to deal with adding during play is the rotation piece. This piece type is added whenever the user clicks on a game piece.

Time for action - modify Game1 to generate rotating pieces


Update the HandleMouseInput() method in the Game1 class to add rotating pieces to the board by adding the following inside the if (mouseState.LeftButton == ButtonState.Pressed) block before gameBoard.RotatePiece() is called:

gameBoard.AddRotatingPiece(x, y,
gameBoard.GetSquare(x, y), false);

Still in HandleMouseInput(), add the following in the same location inside the if block for the right mouse button:

gameBoard.AddRotatingPiece(x, y,
gameBoard.GetSquare(x, y), true);

What just happened?


Recall that the only difference between a clockwise rotation and a counter-clockwise rotation (from the standpoint of the AddRotatingPiece() method) is a true or false in the final parameter. Depending on which button is clicked, we simply add the current square (before it gets rotated, otherwise the starting point for the animation would be the final position) and "true" for right mouse clicks or "false" for left mouse clicks.

Calling UpdateAnimatedPieces()


In order for the UpdateAnimatedPieces() method of the GameBoard class to run, the game's Update() method needs to be modified to call it.

Time for action - updating Game1 to update animated pieces


Modify the Update() method of the Game1 class by replacing the current case statement for the GameState.Playing state with:

case GameStates.Playing:
timeSinceLastInput +=
(float)gameTime.ElapsedGameTime.TotalSeconds;
if (gameBoard.ArePiecesAnimating())
{
gameBoard.UpdateAnimatedPieces();
}
else
{
gameBoard.ResetWater();

for (int y = 0; y < GameBoard.GameBoardHeight; y++)
{
CheckScoringChain(gameBoard.GetWaterChain(y));
}
gameBoard.GenerateNewPieces(true);

if (timeSinceLastInput >= MinTimeSinceLastInput)
{
HandleMouseInput(Mouse.GetState());
}
}
break;

What just happened?


This method is very similar to its previous incarnation. In this instance, we check to see if there are outstanding animated pieces to process. If there are, UpdateAnimatedPieces() is run. If no animated pieces currently exist, the previous behaviour of the GameStates. Playing case is executed.

Drawing animated pieces


Our animated pieces are almost completed. In fact, they all function right now but you cannot see them because we have not yet updated Draw() to take them into account.

Time for action - update Game1 to draw animated pieces


Add methods to the Game1 class to draw each potential type of game piece (animated and non-animated):

private void DrawEmptyPiece(int pixelX, int pixelY)
{
spriteBatch.Draw(
playingPieces,
new Rectangle(pixelX, pixelY,
GamePiece.PieceWidth, GamePiece.PieceHeight),
EmptyPiece,
Color.White);
}

private void DrawStandardPiece(int x, int y,
int pixelX, int pixelY)
{
spriteBatch.Draw(
playingPieces, new Rectangle(pixelX, pixelY,
GamePiece.PieceWidth, GamePiece.PieceHeight),
gameBoard.GetSourceRect(x, y),
Color.White);
}

private void DrawFallingPiece(int pixelX, int pixelY,
string positionName)
{
spriteBatch.Draw(
playingPieces,
new Rectangle(pixelX, pixelY -
gameBoard.fallingPieces[positionName].VerticalOffset,
GamePiece.PieceWidth, GamePiece.PieceHeight),
gameBoard.fallingPieces[positionName].GetSourceRect(),
Color.White);
}

private void DrawFadingPiece(int pixelX, int pixelY,
string positionName)
{
spriteBatch.Draw(
playingPieces,
new Rectangle(pixelX, pixelY,
GamePiece.PieceWidth, GamePiece.PieceHeight),
gameBoard.fadingPieces[positionName].GetSourceRect(),
Color.White *
gameBoard.fadingPieces[positionName].alphaLevel);
}

private void DrawRotatingPiece(int pixelX, int pixelY,
string positionName)
{
spriteBatch.Draw(
playingPieces,
new Rectangle(pixelX + (GamePiece.PieceWidth / 2),
pixelY + (GamePiece.PieceHeight / 2),
GamePiece.PieceWidth,
GamePiece.PieceHeight),
gameBoard.rotatingPieces[positionName].GetSourceRect(),
Color.White,
gameBoard.rotatingPieces[positionName].RotationAmount,
new Vector2(GamePiece.PieceWidth / 2,
GamePiece.PieceHeight / 2),
SpriteEffects.None, 0.0f);
}

Modify the Draw() method of the Game1 class by replacing the for loop that currently draws the playing pieces with:

for (int x = 0; x < GameBoard.GameBoardWidth; x++)
for (int y = 0; y < GameBoard.GameBoardHeight; y++)
{
int pixelX = (int)gameBoardDisplayOrigin.X +
(x * GamePiece.PieceWidth);
int pixelY = (int)gameBoardDisplayOrigin.Y +
(y * GamePiece.PieceHeight);

DrawEmptyPiece(pixelX, pixelY);

bool pieceDrawn = false;

string positionName = x.ToString() + "_" + y.ToString();

if (gameBoard.rotatingPieces.ContainsKey(positionName))
{
DrawRotatingPiece(pixelX, pixelY, positionName);
pieceDrawn = true;
}

if (gameBoard.fadingPieces.ContainsKey(positionName))
{
DrawFadingPiece(pixelX, pixelY, positionName);
pieceDrawn = true;
}

if (gameBoard.fallingPieces.ContainsKey(positionName))
{
DrawFallingPiece(pixelX, pixelY, positionName);
pieceDrawn = true;
}

if (!pieceDrawn)
{
DrawStandardPiece(x, y, pixelX, pixelY);
}
}

Try it out! Run your game and complete a few rows.

What just happened?


To keep things organized, we have split the drawing of each of the different potential piece types into its own small method. These methods (DrawEmptyPiece(), DrawStandardPiece(), DrawFallingPiece(), DrawFadingPiece(), and DrawRotatingPiece()) each contain only a single statement to draw the piece.

Before we look at how each of the pieces is actually drawn, let's examine the way we determine which of these methods to call when drawing a piece. The structure of the drawing loop is still the same as it was before we added animated pieces: each square on the board is looped through, with a blank square being drawn first in each position.

After the blank space, a new Boolean value called pieceDrawn is declared and set to false. If an animated piece occupies a square, only the animated piece will be drawn, and not the underlying game piece.

The reason for this is that when the user clicks on the mouse button to rotate a piece, in memory the piece is rotated immediately. The animated piece that the user sees is inserted into the drawing process so it looks like the piece is turning. If both the animated piece and the real underlying piece were to be drawn, the final rotation position would be visible overlaid on top of the rotating piece while the rotation animation was playing.

The positionName string contains the dictionary key for the space we are currently drawing (in "X_Y" format). We use this to check each of the animated piece dictionaries to see if they contain an entry for that key.

If they do, the animated piece is drawn and the pieceDrawn variable is set to true. If the piece still has not been drawn after all of the dictionaries have been checked, the base piece is drawn just as it was before.

SpriteBatch overloads


Both falling and fading pieces are drawn using the SpriteBatch.Draw() overload that we are already familiar with; where a Texture2D, destination Rectangle, source Rectangle, and Color are specified when drawing. By multiplying our base drawing color (white) by the alpha value for a fading piece, we cause the whole piece to be drawn partially transparent. As the time passes, the alpha value will reach zero, and the piece will be fully transparent.

However, rotated pieces need to use an extended version of the SpriteBatch.Draw() call. The first four parameters are the same as our existing Draw() calls. To these parameters, we add a float for the rotation amount, a Vector2 for the origin around which the rotation takes place, a SpriteEffects property (set to SpriteEffects.None in this case) and a sorting depth (set to 0, or the top level).

When using a rotation with this form of the SpriteBatch.Draw() call, it is necessary to specify the point around which the sprite should be rotated. If we were to set the origin to Vector2.Zero (equivalent to 0, 0) the sprite would rotate around the upper left corner of the image, swinging into the spaces of other tiles on the board. The center point of the sprite is specified in local sprite coordinates (as opposed to screen coordinates, or even coordinates within the texture the sprite is being pulled from). The local coordinates of the sprite range from 0, 0 in the upper left corner to the height and width of the sprite in the lower right. In our case, the lower right corner of the sprite is GamePiece.PieceWidth, GamePiece.PieceHeight, or 40, 40.

By specifying new Vector2(GamePiece.PieceWidth/2, GamePiece.PieceHeight/2) we are setting the origin to the center of the sprite, meaning it will rotate in place as expected.

Summary


In the above article we covered:

  • Animating the rotation of pieces when manipulated by the player
  • Gradually fading out pieces of completed scoring chains
  • Animating the falling of pieces into place on the board
Cancel Save
0 Likes 1 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement