left-icon

MonoGame Role-Playing Game Development Succinctly®
by Jim Perry and Charles Humphrey

Previous
Chapter

of
A
A
A

CHAPTER 2

Sprites and Animation

Sprites and Animation


The sprite

A sprite, in the context of this book and computer graphics, is a two-dimensional object that is a part of the current game scene. This could be anything from a floor or scenery tile, to an item in a shop, to our in-game avatar or character.

How will we use them?

As stated previously, a sprite can be a single floor tile, a bush, our in-game avatar, a potion, or an evil orc—pretty much any two-dimensional object representation in our game world. In most of these cases, the sprite is static (unmoving), but in others, this sprite needs to move around the screen or, even while not moving, be animated.

An example of a sprite that may need to move around or across the screen could be an arrow, a magic missile, or our player avatar moving from one place to the next. Again, some of these sprites need animation and others do not. An arrow or spear, for example, will probably have little to no animation, as this object will not have a long time to live on the screen. But our avatar will need to have a number of animations in order to walk, climb, run, and even attack and die. A static sprite could be something like a rock or a campfire, but the latter would likely be animated.

What is sprite animation?

This is where we take a number of textures, or frames, and sequentially draw them one after another in order to give the impression of animation. This is done the same way as old-school cartoons were drawn: one frame or cell at a time.


What is a sprite sheet?

This is a collection of sprite frames all stored on a single texture, or sheet. These frames can be extracted and put into individual keyframes to be used in an animation clip.

Player sprite sheet

Figure 1: Player sprite sheet

Animation

The code for the animation in this book is from my own MonoGame engine called VoidEngine. I am not sharing all the code from my engine here, but I am sharing snippets of how the animation is handled. You will be able to easily create your own animation classes from these examples, or feel free to use the VoidEngineLight assembly that comes with the code samples on GitHub in your own projects. If you do, please remember to give us a credit on your project.

What is keyframe animation?

This is how we can specify timings for each frame of our animation frames. This gives us a fair bit of control over the animation. Rather than just move from one frame to the next at a fixed pace, we can use the keyframe to specify how long a single frame is viewed until the animation player moves to the next sprite.


Extracting keyframes from a sprite sheet

There is nothing built into MonoGame to do this—we have had to write our own mechanism. We created a SpriteAnimationClipGenerator class to do this for us.

To make it work, we need to know the sprite sheet's dimensions and how many frames or slices are in there. We do this by passing them to the constructor of the class.

Code Listing 1: VoidEngineLight – SpriteAnimationClipGenerator

public SpriteAnimationClipGenerator(Vector2 spriteSheetDimensions, Vector2 slices)

{

    SpriteSheetDimensions = spriteSheetDimensions;

    Slices = slices;

}

We can now use this information to extract animation data, or clips, from the sprite sheet, and we do that in the Generate method.

Code Listing 2: VoidEngineLight – Generate function

public SpriteSheetAnimationClip Generate(string name, Vector2 start, Vector2 end, TimeSpan duration, bool looped)

As you can see from the method signature, we give the clip a name then specify the starting frame location, the last frame's location, the duration of the clip, and whether this frame should be played in a loop.

Now that we have this information, we are going to loop through the sprite sheet and calculate each frame in the animation clip. The first thing we need to do is decide in what direction we need to move in the sprite sheet along the x-axis and y-axis.

Code Listing 3: VoidEngineLight – Generate Function Step 1

// Are we going to be moving forward or backwards along the

// X-axis of the sprite sheet to get the animation frames?

if (start.X > end.X)

{

    xIncDec = -1;

    xCnt = (start.X - end.X) + 1;

}

else

    xCnt = (end.X - start.X) + 1;

// Are we going to be moving up or down along the

// Y-axis of the sprite sheet to get the animation frames?

if (start.Y > end.Y)

{

    yIncDec = -1;

    yCnt = (start.Y - end.Y) + 1;

}

else

    yCnt = (end.Y - start.Y) + 1;

We now know how we are going to move through the sprite sheet to calculate the data we need. Before we get going on that, we still need to calculate some more baseline values. We are creating simple animations with this, so we are splitting the time for each frame evenly over each frame in the clip. We calculate the base time for each frame like this:

Code Listing 4: VoidEngineLight – Generate function step 2

// This is the base time each frame is made up of.

TimeSpan time = new TimeSpan(duration.Ticks / (long)(xCnt * yCnt));

We also want to know the size of each frame on the sheet so we can step to the next frame in the sheet correctly.

Code Listing 5: VoidEngineLight – Generate function step 3

// This is the size of a cell on the sprite sheet.

Vector2 cellSize = SpriteSheetDimensions / Slices;

Now we can start calculating the values for the frames in this animation clip.

Code Listing 6: VoidEngineLight – Generate function step 4

// Is this just one line off the sheet?

// If both start and end Y are the same, then it is a horizontal

// line of keyframes.

if (start.Y == end.Y)

{

    int y = (int)start.Y;

    for (int x = (int)start.X; xCnt > 0; x += xIncDec, xCnt--)

    {

        SpriteSheetKeyFrame frame = new SpriteSheetKeyFrame(new Vector2(x * cellSize.X, y * cellSize.Y), new TimeSpan(time.Ticks * frameCount++));

        frames.Add(frame);

    }

}

else if (start.X == end.X) // If both start and end X are the same, it's a vertical slice.

{

    int x = (int)start.X;

    for (int y = (int)start.Y; yCnt > 0; y += yIncDec, yCnt--)

    {

        SpriteSheetKeyFrame frame = new SpriteSheetKeyFrame(new Vector2(x * cellSize.X, y * cellSize.Y), new TimeSpan(time.Ticks * frameCount++));

        frames.Add(frame);

    }

}

else // If neither start or end X or Y are the same, then it's a block of frames.

{

    for (int y = (int)start.Y; yCnt > 0; y += yIncDec, yCnt--)

    {

        float xcnt = xCnt;

        for (int x = (int)start.X; xcnt > 0; x += xIncDec, xcnt--)

        {

            SpriteSheetKeyFrame frame = new SpriteSheetKeyFrame(new Vector2(x * cellSize.X, y * cellSize.Y), new TimeSpan(time.Ticks * frameCount++));

            frames.Add(frame);

        }

    }

}

First, we check if the start position is on the same vertical as the end position. If it is, then we know we only need to move across the sprite sheet horizontally.

If this is not the case, then we check to see if the start and end positions are the same horizontally, and if they are, we know we only have to move along the sprite sheet vertically.

If neither of these are true, then we are moving diagonally across the sprite sheet.

Regardless of our movement through the sheet, we are calculating each frame's data in the same way with this line:

Code Listing 7: VoidEngineLight – Calculated frame extraction

SpriteSheetKeyFrame frame = new SpriteSheetKeyFrame(new Vector2(x * cellSize.X, y * cellSize.Y), new TimeSpan(time.Ticks * frameCount++));

As you can see, we are calculating the frame’s position by multiplying the current X and Y positions by the cellSize X and Y. The duration of the frame is calculated using the current frame count and the base time we calculated earlier.

So, that's how we can generate an animation clip.

In our GameplayScreen Activate method, we are going to generate our sprite sheet for our simple player avatar.

Code Listing 8: GameplayScreen.cs

Texture2D spriteSheet = _content.Load<Texture2D>("Sprites/Test/TestSheet1");

SpriteAnimationClipGenerator sacg = new SpriteAnimationClipGenerator(new Vector2(spriteSheet.Width, spriteSheet.Height), new Vector2(2, 4));

The first thing we do is load up our sprite sheet from the content pipeline. We can then use the dimensions of this sprite sheet to help set up our SpriteAnimationClipGenerator. We could put this data into a custom content pipeline and load that up like we do the sprite sheet, but that is a little out of the scope of this book, so we will use our generator.

Once we have an instance of the SpriteAnimationClipGenerator, we can use it to extract the animations we want from the sheet.

Code Listing 9: Sprite cell definitions

Dictionary<string, SpriteSheetAnimationClip> spriteAnimationClips = new Dictionary<string, SpriteSheetAnimationClip>()

{

    { "Idle", sacg.Generate("Idle", new Vector2(1, 0), new Vector2(1, 0), new TimeSpan(0, 0, 0, 0, 500), true) },

    { "WalkDown", sacg.Generate("WalkDown", new Vector2(0, 0), new Vector2(1, 0), new TimeSpan(0, 0, 0, 0, 500), true) },

    { "WalkLeft", sacg.Generate("WalkLeft", new Vector2(0, 1), new Vector2(1, 1), new TimeSpan(0, 0, 0, 0, 500), true) },

    { "WalkRight", sacg.Generate("WalkRight", new Vector2(0, 2), new Vector2(1, 2), new TimeSpan(0, 0, 0, 0, 500), true) },

    { "WalkUp", sacg.Generate("WalkUp", new Vector2(0, 3), new Vector2(1, 3), new TimeSpan(0, 0, 0, 0, 500), true) },

};

We now have five animation clips we can use to animate our player avatar, but how do we do that? All we have at the moment are clips—how do we play them?

Animation player

We can now generate animation clips, but for any given screen entity, we may have a number of animations that we want to play. Our player avatar, for example, will want to be able to walk left, right, up, and down, and take other actions. So, we need to store all these possible animations in one place and be able to play them as we need them. That's where our SpriteSheetAnimationPlayer comes in.

We have a few properties in this class.

Code Listing 10: VoidEngineLight – Animation player

public TimeSpan AnimationOffSet { get; set; }

protected bool _IsPlaying = false;

public bool IsPlaying { get { return _IsPlaying; } }

public Vector2 CurrentCell { get; set; }

public int CurrentKeyframe { get; set; }

public event AnimationStopped OnAnimationStopped;

protected SpriteSheetAnimationClip currentClip;

public SpriteSheetAnimationClip CurrentClip

{

    get { return currentClip; }

}

TimeSpan currentTime;

public TimeSpan CurrentTime

{

    get { return currentTime; }

}

public Dictionary<string, SpriteSheetAnimationClip> Clips { get; set; }

TimeSpan AnimationOffSet

To offset the timing of the animation, we can use this when we have several animated sprites on the screen at the same time. Rather than have them play the same frames at exactly the same time, we can offset each of them so they look a little more natural. A hallway with flaming torches is a good example: we would not want each flame playing the same animation at the same time.

bool IsPlaying

This indicates if the animation player is currently playing a clip. It’s handy if you want to trigger some environmental event when the animation is playing.

Vector2 CurrentCell

This is the current cell in the current clip. This is the bit we really need in order to extract the right frame from the sprite sheet, and it includes the coordinates for the frame we need to draw.

int CurrentKeyframe

This may be useful if we want to play a sound when a specific frame is reached.

event AnimationStopped OnAnimationStopped

This event can be subscribed to and is triggered when an animation stops. This is great for looping clips, and you can use this to chain one animation clip after another.

SpriteSheetAnimationClip CurrentClip

This is the clip that is currently in use or is ready to be used.

TimeSpan CurrentTime

This is the current time in the clip being played, and it will range from zero to the duration of the clip.

Dictionary<string, SpriteSheetAnimationClip> clips

This is the container for all the clips we will want to play for a given sprite sheet. We can use this to move from one clip, let’s say from Idle to WalkLeft.

The constructor for the SpriteSheetAnimationPlayer is pretty simple: we just pass it the clips we have just generated and the time offset we want to use.

Code Listing 11: VoidEngineLight – Animation player constructor

public SpriteSheetAnimationPlayer(Dictionary<string, SpriteSheetAnimationClip> clips = null, TimeSpan animationOffSet = new TimeSpan())

{

    AnimationOffSet = animationOffSet;

    Clips = clips;

}

We can now use the StartClip and StopClip to—you guessed it—start and stop the animation clips.

StartClip

Code Listing 12: Animation Player StartClip

public void StartClip(string name, int frame = 0)

{

    StartClip(Clips[name]);

}

public void StartClip(SpriteSheetAnimationClip clip, int frame = 0)

{

    if (clip != null && clip != currentClip)

    {

        currentTime = TimeSpan.Zero + AnimationOffSet;

        CurrentKeyframe = frame;

        currentClip = clip;

        _IsPlaying = true;

    }

}

As you can see, we have an overloaded function for StartClip since we may wish to start a clip by name, or if we already have the clip, just pass and use that.

The first thing we do is ensure the clip we are working with is valid—that is to say it's not null and it's not the clip we are currently working with. We then set the current time to zero, plus any time offset we want to use. We set the frame to the frame we want to use, the current clip to the one passed in, and finally, set IsPlaying to true. This sets up our clip to be played, and will kick off the logic in our Update method (we will come back to that in a little while).

StopClip

Code Listing 13: Animation player StopClip

public void StopClip()

{

    if (currentClip != null && IsPlaying)

    {

        _IsPlaying = false;

        if (OnAnimationStopped != null)

            OnAnimationStopped(currentClip);

    }

}

As with StartClip, the first thing we do is check the clip we are working with and check that we are currently playing. If we have a valid clip and we are indeed playing, we simply set IsPlaying to false, and if anything is subscribed to our OnAnimationStopped event, we let it know it's stopped.

Update

Code Listing 14: Animation player Update

public void Update(TimeSpan time)

{

    if (currentClip != null)

        GetCurrentCell(time);

}

This is almost where the magic happens. Again, we check whether the current clip is valid. If it is, then we call GetCurrentCell, passing the elapsed game time, and this is where all the work is done.

GetCurrentCell

Code Listing 15: Animation player GetCurrentCell

protected void GetCurrentCell(TimeSpan time)

{

    time += currentTime;

    // If we reached the end, loop back to the start.

    while (time >= currentClip.Duration)

        time -= currentClip.Duration;

    if ((time < TimeSpan.Zero) || (time >= currentClip.Duration))

        throw new ArgumentOutOfRangeException("time");

    if (time < currentTime)

    {

        if (currentClip.Looped)

            CurrentKeyframe = 0;

        else

        {

            CurrentKeyframe = currentClip.Keyframes.Count - 1;

            StopClip();

        }

    }

    currentTime = time;

    // Read keyframe matrices.

    IList<SpriteSheetKeyFrame> keyframes = currentClip.Keyframes;

    while (CurrentKeyframe < keyframes.Count)

    {

        SpriteSheetKeyFrame keyframe = keyframes[CurrentKeyframe];

        // Stop when we've read up to the current time position.

        if (keyframe.Time > currentTime)

            break;

        // Use this keyframe.

        CurrentCell = keyframe.Cell;

        CurrentKeyframe++;

    }

}

As you can see, this is the workhorse of the SpriteSheetAnimationPlayer class. The first thing we do is add the clip’s current time to the elapsed time passed in. If that time value is longer than or equal to the clip duration, then we need to set the time back to the start of the clip.

If our time ends up being less than zero or greater than the clip duration, we need to throw a controlled exception to indicate there is an issue with the time. If we left it to run, we would get an ArgumentOutOfRangeException thrown when we try to get the current cell later, so we might as well know about it sooner rather than later.

If time is now less than the current time, then we must have reached the end and looped back to the start. So, we check if this clip is looped, and then we just set the current frame to 0 so we can start the animation again. If it’s not, then we set the current frame to the last frame in the clip and call the StopClip function. As we saw in the previous code listing, this will stop the clip and inform any subscribers to the event that it has stopped.

If we’ve come this far, we are still playing the animation clip. We get a list of the keyframes from the current clip and loop through them to find the current cell coordinates. We do this by checking if the keyframe's time is greater than the current time. If it is, then the last frame is the frame we are currently on, so we break out of the loop here. If it isn't, then this frame could be the frame we are on, so we store it and move on the current keyframe.

Animation clips

In both the SpriteAnimationClipGenerator and the SpriteSheetAnimationPlayer we have spoken about animation clips, but what do they look like in code?

Code Listing 16: VoidEngineLight – Animation clip

public class SpriteSheetAnimationClip

{

    public string Name { get; set; }

    public bool Looped { get; set; }

    public TimeSpan Duration { get; set; }

    public List<SpriteSheetKeyFrame> Keyframes { get; set; }

    public SpriteSheetAnimationClip() { }

    public SpriteSheetAnimationClip(string name, TimeSpan duration, List<SpriteSheetKeyFrame> keyframes, bool looped = true)

    {

        Name = name;

        Duration = duration;

        Keyframes = keyframes;

        Looped = looped;

    }

    public SpriteSheetAnimationClip(SpriteSheetAnimationClip clip)

    {

        Name = clip.Name;

        Duration = clip.Duration;

        SpriteSheetKeyFrame[] frames = new SpriteSheetKeyFrame[clip.Keyframes.Count];

        clip.Keyframes.CopyTo(frames, 0);

        Keyframes = new List<SpriteSheetKeyFrame>();

        Keyframes.AddRange(frames);

        Looped = clip.Looped;

    }

}

They are pretty much just a storage class for the data required for a given clip, including a list of all its keyframe data.

Keyframes

Code Listing 17: VoidEngineLight – Keyframe

public class SpriteSheetKeyFrame

{

    public Vector2 Cell { get; set; }

    public TimeSpan Time { get; set; }

    public SpriteSheetKeyFrame() { }

    public SpriteSheetKeyFrame(Vector2 cell, TimeSpan time)

    {

        Cell = cell;

        Time = time;

    }

}

Again, this is a storage class for the keyframe data. The Cell gives the starting X and Y position in the sheet and the Time gives the duration the frame is displayed.

Playing animation clips

We now have a set of animation clips and an animation player that can give us the data at runtime in order to draw the cell we want as an animation plays, but we have no way of bringing these together.

If we return to our GameplayScreen class and the Activate method, we can see we have a Sprite class.

Code Listing 18: GameplayScreen.cs

playerAvatar = new Sprite(spriteSheet, new Point(32, 40), new Point(16, 20));

playerAvatar.animationPlayer = new SpriteSheetAnimationPlayer(spriteAnimationClips);

playerAvatar.StartAnimation("Idle");

This code initializes our player avatar, passing in the sprite sheet we used to generate the animation clips, the size we want to render our player at, and the physical cell size in pixels. We then give it an instance of the SpriteSheetAnimationPlayer populated with our extracted animation clips, and finally, tell it so start the Idle animation clip.

Animation in action

Sprite class

Code Listing 19: Sprite.cs

public class Sprite

{

    public Vector2 Position { get; set; }

    public Point CellSize { get; set; }

    public Point Size { get; set; }

    public Texture2D spriteTexture { get; set; }

    protected SpriteSheetAnimationPlayer _animationPlayer;

    public SpriteSheetAnimationPlayer animationPlayer

    {

        get { return _animationPlayer; }

        set

        {

            if (_animationPlayer != value && _animationPlayer != null)

                _animationPlayer.OnAnimationStopped -= OnAnimationStopped;

            _animationPlayer = value;

            _animationPlayer.OnAnimationStopped += OnAnimationStopped;

        }

    }

    public Color Tint { get; set; }

    protected Rectangle sourceRect

    {

        get

        {

            if (animationPlayer != null)

                return new Rectangle((int)animationPlayer.CurrentCell.X, (int)animationPlayer.CurrentCell.Y, CellSize.X, CellSize.Y);

            else

            {

                if (CellSize == Point.Zero)

                    CellSize = new Point(spriteTexture.Width, spriteTexture.Height);

                return new Rectangle(0,0, CellSize.X, CellSize.Y);

            }

        }

    }

    public Sprite(Texture2D spriteSheetAsset, Point size, Point cellSize)

    {

        spriteTexture = spriteSheetAsset;

        Tint = Color.White;

        Size = size;

        CellSize = cellSize;

    }

    protected virtual void OnAnimationStopped(SpriteSheetAnimationClip clip)

    {

        return;   

    }

    public virtual void StartAnimation(string animation)

    {

        if (animationPlayer != null)

            animationPlayer.StartClip(animation);

    }

    public virtual void StopAnimation()

    {

        if (animationPlayer != null)

            animationPlayer.StopClip();

    }

    public virtual void Update(GameTime gameTime)

    {

        if (animationPlayer != null)

            animationPlayer.Update(gameTime.ElapsedGameTime);

    }

    public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)

    {

        spriteBatch.Draw(spriteTexture, new Rectangle((int)Position.X, (int)Position.Y, (int)Size.X, (int)Size.Y), sourceRect, Tint);

    }

}

The Sprite class is used as the base class for all our in-game sprites. It has a number of properties:

Vector2 Position

This is the position of the sprite on the screen.

Point CellSize

This is the physical size of a given cell in our sprite sheet in pixels.

Point Size

This is the size in pixels we want to draw our sprite.

Texture2D spriteTexture

This is our sprite sheet texture used to render our sprite.

SpriteSheetAnimationPlayer animationPlayer

This is a populated instance of the SpriteSheetAnimationPlayer we discussed previously. When we add an animation player, we wire up to its OnAnimationStopped event. If we already have a player, we unwire ourselves from its OnAnimationStopped event.

Color Tint

We can use this to “tint” the color of our sprites if we want.

Rectangle sourceRect

This is what we use to know what part of the sprite sheet we want to render. If there is no animation player, then we assume that it must be the whole texture we want to render. If there is an animation player, then we use the cell size that we have been given to set up its height and width.

The constructor for the class is simple enough. We pass in the sprite sheet, the size we want to render, and the cell size in the sprite sheet and store them for later use.

OnAnimationStopped

While not currently implemented here, it may be by derived classes.

StartAnimation

This method will start a given animation clip if we have an animation player.

StopAnimation

This function will stop an animation clip if we have an animation player.

Update

This method, if we have an animation player, calls its Update method.

Draw

Finally, we get to draw our sprite using the texture provided, set the destination rectangle on the screen, use our calculated source rectangle, and tint the sprite based on the tint color we have.

Moving our character

We now have all the elements in place for us to be able to animate and move our player avatar. Return to the GameplayScreen class; this time we will go into its HandleInput method.

Code Listing 20: GameplayScreen.cs HandleInput

if (input.IsKeyPressed(Keys.Down, ControllingPlayer, out player))

    playerAvatar.animationPlayer.StartClip("WalkDown");

else if (input.IsKeyPressed(Keys.Up, ControllingPlayer, out player))

    playerAvatar.animationPlayer.StartClip("WalkUp");

else if (input.IsKeyPressed(Keys.Left, ControllingPlayer, out player))

    playerAvatar.animationPlayer.StartClip("WalkLeft");

else if (input.IsKeyPressed(Keys.Right, ControllingPlayer, out player))

    playerAvatar.animationPlayer.StartClip("WalkRight");

else

    playerAvatar.animationPlayer.StartClip("Idle");

Here, if the screen is not paused, we can specify what animation we want to have played by our player avatar. We can press the Down arrow key to play the WalkDown animation, the Up arrow key to play the WalkUp animation, the Left arrow key to play the WalkLeft animation clip, and the Right arrow key to play the WalkRight animation clip. If there is no input, we play the Idle animation.

Now, in the Update method in the GameplayScreen class we can move our animated player avatar based on the animation being played.

Code Listing 21: GameplayScreen.cs update

float translateSpeed = 0.5f;

switch (playerAvatar.animationPlayer.CurrentClip.Name)

{

    case "WalkDown":

        if (!currentLevel.IsSolid(playerAvatar.Position + new Vector2(0, translateSpeed)))

            playerAvatar.Position += new Vector2(0, translateSpeed);

        break;

    case "WalkLeft":

        if (!currentLevel.IsSolid(playerAvatar.Position + new Vector2(-translateSpeed, 0)))

            playerAvatar.Position += new Vector2(-translateSpeed, 0);

        break;

    case "WalkRight":

        if (!currentLevel.IsSolid(playerAvatar.Position + new Vector2(translateSpeed, 0)))

            playerAvatar.Position += new Vector2(translateSpeed, 0);

        break;

    case "WalkUp":

        if (!currentLevel.IsSolid(playerAvatar.Position + new Vector2(0, -translateSpeed)))

            playerAvatar.Position += new Vector2(0, -translateSpeed);

        break;

    case "Idle":

        break;

}

If the screen is active, we call our player avatar’s Update method, set a translation speed, and then look to see what animation clip is being played. If the WalkDown animation is playing, then the player avatar moves down the screen. WalkLeft will move our player avatar to the left, and WalkRight moves the player avatar to the right. When the WalkUp animation is playing—you guessed it—the avatar is moving up the screen.

Finally, we do a quick check to make sure that our player avatar can't escape the screen.

What’s next

We now have a framework for creating and animating sprites within our game, whether that sprite is our player, an NPC, or even the environment and equipment. Now we need a framework for creating our player characters and giving them characteristics, skills, and classes.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.