Skip to main content

AnimatedSprite

Up to date

This page is up to date for MonoGame.Extended 4.0.4. If you find outdated information, please open an issue.

In the previous document about SpriteSheets we went over how to create a SpriteSheet, define animations, and retrieve the animations from it. Doing this only gives use the SpriteSheetAnimation instance for that animation, which we then have to create an AnimationController with to manage that single animation.

However, typically a SpriteSheet is going to contain several animations related to a single concept, like all of the animations for a player. To better manage controlling the animations from the SpriteSheet we can use the AnimatedSprite class.

Let's use the same example adventurer character from the SpriteSheet document.

Animated Pixel Adventurer by rvros; Licensed for free and commercial use.

Creating an AnimatedSprite

To create an AnimatedSprite first a SpriteSheet needs to be created with the animations defined. Building off of our previous example, it would look like this

protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);

Texture2D adventurerTexture = Content.Load<Texture2D>("adventurer");
Texture2DAtlas atlas = Texture2DAtlas.Create("Atlas/adventurer", adventurerTexture, 50, 37);
SpriteSheet spriteSheet = new SpriteSheet("SpriteSheet/adventurer", atlas);

spriteSheet.DefineAnimation("attack", builder =>
{
builder.IsLooping(false)
.AddFrame(regionIndex: 0, duration: TimeSpan.FromSeconds(0.1))
.AddFrame(1, TimeSpan.FromSeconds(0.1))
.AddFrame(2, TimeSpan.FromSeconds(0.1))
.AddFrame(3, TimeSpan.FromSeconds(0.1))
.AddFrame(4, TimeSpan.FromSeconds(0.1))
.AddFrame(5, TimeSpan.FromSeconds(0.1));
});

spriteSheet.DefineAnimation("idle", builder =>
{
builder.IsLooping(true)
.AddFrame(6, TimeSpan.FromSeconds(0.1))
.AddFrame(7, TimeSpan.FromSeconds(0.1))
.AddFrame(8, TimeSpan.FromSeconds(0.1))
.AddFrame(9, TimeSpan.FromSeconds(0.1));
});

spriteSheet.DefineAnimation("run", builder =>
{
builder.IsLooping(true)
.AddFrame(10, TimeSpan.FromSeconds(0.1))
.AddFrame(11, TimeSpan.FromSeconds(0.1))
.AddFrame(12, TimeSpan.FromSeconds(0.1))
.AddFrame(13, TimeSpan.FromSeconds(0.1))
.AddFrame(14, TimeSpan.FromSeconds(0.1))
.AddFrame(15, TimeSpan.FromSeconds(0.1));
});
}

This creates the Texture2DAtlas using the Texture2DAtlas.Create method to automatically generate the regions, creates a SpriteSheet using the atlas, then defines the animations for the attack, idle, and run animations. Note that the attack animation is set to false for looping. This will be important later.

Now that we have the SpriteSheet defined, let's use it to create an AnimatedSprite

private AnimatedSprite _adventurer;

protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);

Texture2D adventurerTexture = Content.Load<Texture2D>("adventurer");
Texture2DAtlas atlas = Texture2DAtlas.Create("Atlas/adventurer", adventurerTexture, 50, 37);
SpriteSheet spriteSheet = new SpriteSheet("SpriteSheet/adventurer", atlas);

spriteSheet.DefineAnimation("attack", builder =>
{
builder.IsLooping(true)
.AddFrame(regionIndex: 0, duration: TimeSpan.FromSeconds(0.1))
.AddFrame(1, TimeSpan.FromSeconds(0.1))
.AddFrame(2, TimeSpan.FromSeconds(0.1))
.AddFrame(3, TimeSpan.FromSeconds(0.1))
.AddFrame(4, TimeSpan.FromSeconds(0.1))
.AddFrame(5, TimeSpan.FromSeconds(0.1));
});

spriteSheet.DefineAnimation("idle", builder =>
{
builder.IsLooping(true)
.AddFrame(6, TimeSpan.FromSeconds(0.1))
.AddFrame(7, TimeSpan.FromSeconds(0.1))
.AddFrame(8, TimeSpan.FromSeconds(0.1))
.AddFrame(9, TimeSpan.FromSeconds(0.1));
});

spriteSheet.DefineAnimation("run", builder =>
{
builder.IsLooping(true)
.AddFrame(10, TimeSpan.FromSeconds(0.1))
.AddFrame(11, TimeSpan.FromSeconds(0.1))
.AddFrame(12, TimeSpan.FromSeconds(0.1))
.AddFrame(13, TimeSpan.FromSeconds(0.1))
.AddFrame(14, TimeSpan.FromSeconds(0.1))
.AddFrame(15, TimeSpan.FromSeconds(0.1));
});

_adventurer = new AnimatedSprite(spriteSheet, "idle");
}

Updating the AnimatedSprite

The AnimatedSprite needs to be updated each frame so it can track the progressof the animation and change frames when the duration for the current frame has passed

protected override void Update(GameTime gameTime)
{
_adventurer.Update(gameTime);
}

Drawing the AnimatedSprite

The AnimatedSprite class is a child class of the Sprite class, so drawing it is done the same way, by just passing it to the SpriteBatch.Draw overload.

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);

_spriteBatch.Begin(samplerState: SamplerState.PointClamp);
int scale = 3;
_spriteBatch.Draw(_adventurer, _adventurer.Origin * scale, 0, new Vector2(scale));
_spriteBatch.End();

base.Draw(gameTime);
}

The result of drawing the AnimatedSprite from the example code above.

Using Animation Event Triggers

Internally the AnimatedSprite uses the IAnimationController to control and manage the playback of the current animation. The IAnimationController interface provides an event that can be subscribed to that will trigger on various events.

For instance, in our example above, we set the attack animation to non looping. So let's update our code so that when we press the enter key, it performs the attack animation.

private KeyboardListener _keyboardListener;

protected override void Initialize()
{
_keyboardListener = new KeyboardListener();
_keyboardListener.KeyPressed += (sender, eventArgs) =>
{
if (eventArgs.Key == Keys.Enter && _adventurer.CurrentAnimation == "idle")
{
_adventurer.SetAnimation("attack");
}
};
}

protected override void Update(GameTime gameTime)
{
_keyboardListener.Update(gameTime);
_adventurer.Update(gameTime);
}

Now, if we run our sample and press the Enter key, the attack animation will play, but then when it ends, nothing happens. This is because we told it to be a non-looping animation when we defined it.

When we hit enter to set the attack animation, the attack animation plays, but since it's non-looping, it stops and does nothing after.

Instead, we would like to tell it that when the animation completes, it should go back to the idle animation. We can do this using the IAnimationController.OnAnimationEvent event.

Modify the code to the following

protected override void Initialize()
{
_keyboardListener = new KeyboardListener();
_keyboardListener.KeyPressed += (sender, eventArgs) =>
{
if (eventArgs.Key == Keys.Enter && _adventurer.CurrentAnimation == "idle")
{
_adventurer.SetAnimation("attack").OnAnimationEvent += (sender, trigger) =>
{
if (trigger == AnimationEventTrigger.AnimationCompleted)
{
_adventurer.SetAnimation("idle");
}
};
}
};
base.Initialize();
}

If we run the sample now, when we press Enter, the attack animation will play. Once the animation completes, it will trigger the animation event, which we're now checking for and change it back to using the idle animation.

The result of the code change to detect the animation completed event and switch from attack to idle animation from that.

Conclusion

The AnimatedSprite class provides a way to manage multiple animations from a single SpriteSheet. By encapsulating the animation logic within the AnimatedSprite, it simplifies the process of updating, drawing, and controlling animations. This makes it easier to handle transitions and events within your animations. Using the IAnimationController interface and its event triggers, you can create animations that react to game events and user inputs.