Table of Contents

Chapter 08: The Sprite Class

Explore creating a reusable Sprite class to efficiently sprites and their rendering properties, including position, rotation, scale, and more.

In Chapter 07, you learned how to use texture atlases to optimize rendering performance. While this solved the issue of texture swapping, managing individual sprites and their properties becomes increasingly complex as your game grows. Even in our simple example with just a slime and a bat, we would eventually need to track various properties for each sprite:

  • Color mask for tinting.
  • Origin for rotation and scale.
  • Scale for size adjustments.
  • Rotation for orientation.
  • Sprite effects to flip horizontally and/or vertically.
  • Layer depth for draw order layering.

Imagine scaling this up to dozens of sprites, each with multiple instances on screen. Tracking all these properties through individual variables quickly becomes unmanageable. In this chapter, we'll solve this by creating a class that encapsulates sprite information and handles rendering.

The Sprite Class

A sprite in our game represents a visual object created from a texture region along with its rendering properties. While multiple sprites might use the same texture region (like multiple enemies of the same type), each sprite can have unique properties that control how it appears on screen; its position, rotation, scale, and other visual characteristics.

By creating a Sprite class, we can encapsulate both the texture region and its rendering parameters into a single, reusable component. This not only makes our code more organized but also makes it easier to manage multiple instances of the same type of sprite.

In the Graphics directory within the MonoGameLibrary project, add a new file named Sprite.cs. Add the following code for the foundation of the Sprite class to the Sprite.cs file:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace MonoGameLibrary.Graphics;

public class Sprite { }

Properties

The Sprite class will utilize properties that mirror the parameters used in SpriteBatch.Draw so the rendering parameter for each sprite is self contained. Add the following properties:

/// <summary>
/// Gets or Sets the source texture region represented by this sprite.
/// </summary>
public TextureRegion Region { get; set; }

/// <summary>
/// Gets or Sets the color mask to apply when rendering this sprite.
/// </summary>
/// <remarks>
/// Default value is Color.White
/// </remarks>
public Color Color { get; set; } = Color.White;

/// <summary>
/// Gets or Sets the amount of rotation, in radians, to apply when rendering this sprite.
/// </summary>
/// <remarks>
/// Default value is 0.0f
/// </remarks>
public float Rotation { get; set; } = 0.0f;

/// <summary>
/// Gets or Sets the scale factor to apply to the x- and y-axes when rendering this sprite.
/// </summary>
/// <remarks>
/// Default value is Vector2.One
/// </remarks>
public Vector2 Scale { get; set; } = Vector2.One;

/// <summary>
/// Gets or Sets the xy-coordinate origin point, relative to the top-left corner, of this sprite.
/// </summary>
/// <remarks>
/// Default value is Vector2.Zero
/// </remarks>
public Vector2 Origin { get; set; } = Vector2.Zero;

/// <summary>
/// Gets or Sets the sprite effects to apply when rendering this sprite.
/// </summary>
/// <remarks>
/// Default value is SpriteEffects.None
/// </remarks>
public SpriteEffects Effects { get; set; } = SpriteEffects.None;

/// <summary>
/// Gets or Sets the layer depth to apply when rendering this sprite.
/// </summary>
/// <remarks>
/// Default value is 0.0f
/// </remarks>
public float LayerDepth { get; set; } = 0.0f;

/// <summary>
/// Gets the width, in pixels, of this sprite. 
/// </summary>
/// <remarks>
/// Width is calculated by multiplying the width of the source texture region by the x-axis scale factor.
/// </remarks>
public float Width => Region.Width * Scale.X;

/// <summary>
/// Gets the height, in pixels, of this sprite.
/// </summary>
/// <remarks>
/// Height is calculated by multiplying the height of the source texture region by the y-axis scale factor.
/// </remarks>
public float Height => Region.Height * Scale.Y;

The TextureRegion property works to provide the texture and source rectangle when rendering the sprite. Other properties directly correspond to SpriteBatch.Draw parameters with the same default values, making it easy to understand how each property affects the sprite's appearance.

Tip

The calculated Width and Height properties make it easier to position sprites relative to each other without manually applying scale factors.

Constructors

The Sprite class will provide two ways to create a new sprite. Add the following constructors:

/// <summary>
/// Creates a new sprite.
/// </summary>
public Sprite() { }

/// <summary>
/// Creates a new sprite using the specified source texture region.
/// </summary>
/// <param name="region">The texture region to use as the source texture region for this sprite.</param>
public Sprite(TextureRegion region)
{
    Region = region;
}

The default constructor creates an empty sprite that can be configured later, while the parameterized constructor allows you to specify the source texture region for the sprite.

Methods

Finally, the Sprite class provides the following two methods:

/// <summary>
/// Sets the origin of this sprite to the center
/// </summary>
public void CenterOrigin()
{
    Origin = new Vector2(Region.Width, Region.Height) * 0.5f;
}

/// <summary>
/// Submit this sprite for drawing to the current batch.
/// </summary>
/// <param name="spriteBatch">The SpriteBatch instance used for batching draw calls.</param>
/// <param name="position">The xy-coordinate position to render this sprite at.</param>
public void Draw(SpriteBatch spriteBatch, Vector2 position)
{
    Region.Draw(spriteBatch, position, Color, Rotation, Origin, Scale, Effects, LayerDepth);
}
  • CenterOrigin: Sets the origin point of the sprite to its center.

    Note

    The origin needs to be set based on the width and height of the source texture region itself, regardless of the scale the sprite is rendered at.

  • Draw: Uses the TextureRegion property to submit the sprite for rendering using the properties of the sprite itself.

Create Sprites With The TextureAtlas Class

While the GetRegion method of the TextureAtlas class we created in Chapter 07 works well for retrieving regions, creating sprites requires multiple steps:

  1. Get the region by name.
  2. Store it in a variable.
  3. Create a new sprite with that region.

We can simplify this process by adding a sprite creation method to the TextureAtlas class. Open TextureAtlas.cs and add the following method:

/// <summary>
/// Creates a new sprite using the region from this texture atlas with the specified name.
/// </summary>
/// <param name="regionName">The name of the region to create the sprite with.</param>
/// <returns>A new Sprite using the texture region with the specified name.</returns>
public Sprite CreateSprite(string regionName)
{
    TextureRegion region = GetRegion(regionName);
    return new Sprite(region);
}

Using the Sprite Class

Let's adjust our game now to use the Sprite class instead of just the texture regions. Replace the contents of Game1.cs with the following:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGameLibrary;
using MonoGameLibrary.Graphics;

namespace DungeonSlime;

public class Game1 : Core
{
    // Defines the slime sprite.
    private Sprite _slime;

    // Defines the bat sprite.
    private Sprite _bat;

    public Game1() : base("Dungeon Slime", 1280, 720, false)
    {

    }

    protected override void Initialize()
    {
        // TODO: Add your initialization logic here

        base.Initialize();
    }

    protected override void LoadContent()
    {
        // Create the texture atlas from the XML configuration file
        TextureAtlas atlas = TextureAtlas.FromFile(Content, "images/atlas-definition.xml");

        // Create the slime sprite from the atlas.
        _slime = atlas.CreateSprite("slime");

        // Create the bat sprite from the atlas.
        _bat = atlas.CreateSprite("bat");

        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            Exit();

        // TODO: Add your update logic here

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        // Clear the back buffer.
        GraphicsDevice.Clear(Color.CornflowerBlue);

        // Begin the sprite batch to prepare for rendering.
        SpriteBatch.Begin(samplerState: SamplerState.PointClamp);

        // Draw the slime sprite.
        _slime.Draw(SpriteBatch, Vector2.One);

        // Draw the bat sprite 10px to the right of the slime.
        _bat.Draw(SpriteBatch, new Vector2(_slime.Width + 10, 0));

        // Always end the sprite batch when finished.
        SpriteBatch.End();

        base.Draw(gameTime);
    }
}

The key changes in this implementation are:

  • The _slime and _bat members were changed from TextureRegion to Sprite.
  • In LoadContent the _slime and _bat sprites are now created using the new TextureAtlas.CreateSprite method.
  • In Draw, the draw calls were updated to use the Sprite.Draw method.

Running the game now will produce the same result as in the previous chapter.

Figure 8-1: The slime and bat sprites being rendered in the upper-left corner of the game window
Figure 8-1: The slime and bat sprites being rendered in the upper-left corner of the game window

Try adjusting the various properties available for the slime and the bat sprites to see how they affect the rendering.

Conclusion

In this chapter, we created a reusable Sprite class that encapsulates the properties for each sprite that we would render. The TextureAtlas class was updated to simplify sprite creation based on the Sprite class we created.

In the next chapter, we'll build upon the Sprite class to create an AnimatedSprite class that will allow us to bring our sprites to life through animation.

Test Your Knowledge

  1. What is the benefit of using a Sprite class instead of managing texture regions directly?

    The Sprite class encapsulates all rendering properties (position, rotation, scale, etc.) into a single, reusable component. This makes it easier to manage multiple instances of the same type of sprite without having to track properties through individual variables.

  2. Why do the Width and Height properties of a Sprite take the Scale property into account?

    The Width and Height properties account for scaling to make it easier to position sprites relative to each other without having to manually calculate the scaled dimensions. This is particularly useful when sprites are rendered at different scales.

  3. When using the CenterOrigin method, why is the origin calculated using the region's dimensions rather than the sprite's scaled dimensions?

    The origin needs to be set based on the texture region's actual dimensions because it represents the point around which scaling and rotation are applied. Using the scaled dimensions would result in incorrect positioning since the origin would change based on the current scale factor.

  4. What advantage does the TextureAtlas.CreateSprite method provide over using GetRegion?

    The CreateSprite method simplifies sprite creation by combining multiple steps (getting the region, storing it, creating a sprite) into a single method call. This reduces code repetition and makes sprite creation more straightforward.