How to use sprite sheets with MonoGame.Extended

Joachim Grill
Last updated:
GitHub
How to use sprite sheets with MonoGame.Extended

MonoGame.Extended is a set of utilities and extensions for MonoGame that makes it easier to make games. It provides additional functionality like sprite batching, texture atlases, bitmap fonts, cameras, input handling, and much more. In this tutorial, you will learn how to use sprite sheets with MonoGame.Extended.

The features demonstrated in this tutorial are available with

  • MonoGame.Extended 5.0.0 or newer
  • TexturePacker 7.9.0 or newer

Why use sprite sheets?

  • Loading all graphics at once instead of numerous single images can significantly reduce your app's loading time.
  • Using a sprite sheet also enhances the game's performance because textures only need to be set on the graphics device once, resulting in improved frame rates.

Setting up a MonoGame.Extended project

First, let's create a new MonoGame project and add MonoGame.Extended:

dotnet new mgdesktopgl -o DemoApp
cd DemoApp
dotnet add package MonoGame.Extended
dotnet add package MonoGame.Extended.Content.Pipeline

Running dotnet run will open a window with a blue background.

Creating a sprite sheet

The easiest way to create a sprite sheet is using TexturePacker — available for Windows, macOS and Linux:

Creating a sprite sheet with TexturePacker is straightforward:

  1. Drag and drop all the files you want to add to the left pane of TexturePacker
  2. Select the MonoGame.Extended framework and set the file path for the generated Data file. The Texture file can be left empty; TexturePacker will automatically save the PNG containing the sprite sheet next to the data file.
  3. Click Publish sprite sheet
Creating a sprite sheet with TexturePacker

The demo project including the sprite images is available on GitHub.

By default, pivot points are at position (0,0), i.e., in the top-left corner of each sprite. For character sprites that should stand on the ground, use TexturePacker's Pivot point editor to set the pivot points of the walk+turn sprites to bottom-center.

We recommend storing the individual sprite images in a root folder named assets, while only placing the sprite sheet data and texture file in the Content folder. This approach makes it easier to determine which files must be deployed later.

Configuring the Content Pipeline

To use sprite sheets with MonoGame.Extended, you need to configure the Content Pipeline Extension to process TexturePacker's JSON files. After installing, the DLL of the Content Pipeline Extension is located somewhere inside your NuGet cache: ~/.nuget/packages/.../MonoGame.Extended.Content.Pipeline.dll

This DLL must be referenced in the MGCB file of your project. Using a relative path to this location is not recommended: each time your project is moved to another directory, the relative path breaks and must be updated. It's better to copy the DLL to your project directory and reference this local copy.

Add these lines to your .csproj file to automatically copy the Content Pipeline DLLs to a local pipeline-references directory when starting a build (this MSBuild property tells the MonoGame.Extended package where to copy its pipeline DLLs):

DemoApp.csproj
<PropertyGroup>
  <MonoGameExtendedPipelineReferencePath>$(MSBuildThisFileDirectory)pipeline-references</MonoGameExtendedPipelineReferencePath>
</PropertyGroup>

After calling dotnet build a pipeline-references directory should appear in your project root, containing two DLLs. Then, add a reference to the MonoGame.Extended Content Pipeline in your Content/Content.mgcb file:

Content/Content.mgcb
#-------------------------------- References --------------------------------#
/reference:../pipeline-references/MonoGame.Extended.Content.Pipeline.dll

#---------------------------------- Content ---------------------------------#

Passing texture atlas to Content Pipeline

With dotnet mgcb-editor you can launch the MGCB Editor app. Use the menu item FileOpen to open your Content/Content.mgcb file. With EditAddExisting item... you can add the sprite sheet image and its corresponding JSON data file. If the content pipeline is configured correctly, the editor will automatically select the TexturePacker Importer+Processor for the JSON file. Otherwise, go back to the previous section and make sure that the pipeline reference is set correctly.

Use the MGCB editor to add the sprite sheet files to the content pipeline

Use Build to save the .mgcb file and process the assets. If the build fails, please check if you have selected the "MonoGame.Extended" framework in TexturePacker and if you are using the latest MonoGame.Extended version.

Alternative approach:

Instead of using the MGCB editor, you can edit Content.mgcb directly with a text editor. Add the following lines to the content section of your Content.mgcb file:

Content/Content.mgcb
#begin spritesheet-texture.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:spritesheet-texture.png

#begin spritesheet.json
/importer:TexturePackerJsonImporter
/processor:TexturePackerProcessor
/build:spritesheet.json

Using a sprite from a texture atlas

Let's start with the Game1.cs class generated by the MonoGame project template. Its default implementation displays a blue background. Let's load the sprite sheet and extract sprites from it:

Game1.cs
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGame.Extended.Graphics;

namespace DemoApp;

public class Game1 : Game
{
    private GraphicsDeviceManager _graphics;
    private SpriteBatch _spriteBatch;
    private Sprite _backgroundSprite;

    public Game1()
    {
        _graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
        IsMouseVisible = true;

        _graphics.PreferredBackBufferWidth = 1000;
        _graphics.PreferredBackBufferHeight = 650;
    }

    protected override void Initialize()
    {
        base.Initialize();
    }

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

        // Load sprite atlas from Content Pipeline
        Texture2DAtlas spriteAtlas = Content.Load<Texture2DAtlas>("spritesheet");

        // Create sprite from atlas
        _backgroundSprite = spriteAtlas.CreateSprite("Background");
    }

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

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        _spriteBatch.Begin();
        _spriteBatch.Draw(_backgroundSprite, Vector2.Zero);
        _spriteBatch.End();

        base.Draw(gameTime);
    }
}

We've loaded the sprite atlas using the Content Pipeline and created a sprite from it. The background sprite is now displayed instead of the solid blue color.

Screenshot of the demo app

Creating an animation

Now, we want to add an animated character to our scene. The SpriteSheet class manages the animation frames: We define the animation by adding frames with specific durations and setting it to loop. The AnimatedSprite handles the animation playback, its Update() method automatically selects the frame to display, depending on the current game time.

Game1.cs
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGame.Extended.Graphics;

namespace DemoApp;

public class Game1 : Game
{
    private GraphicsDeviceManager _graphics;
    private SpriteBatch _spriteBatch;
    private Sprite _backgroundSprite;
    private AnimatedSprite _capguySprite;
    private int _xPosition = 100;
    private double _speedInPixelsPerSecond = 150;

    public Game1()
    {
        _graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
        IsMouseVisible = true;

        _graphics.PreferredBackBufferWidth = 1000;
        _graphics.PreferredBackBufferHeight = 650;
    }

    protected override void Initialize()
    {
        base.Initialize();
    }

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

        // Load sprite atlas from Content Pipeline
        Texture2DAtlas spriteAtlas = Content.Load<Texture2DAtlas>("spritesheet");

        // Create sprite from atlas
        _backgroundSprite = spriteAtlas.CreateSprite("Background");

        // Create animation
        SpriteSheet animationSheet = new SpriteSheet("capguy", spriteAtlas);
        animationSheet.DefineAnimation("walk", builder =>
        {
            for (int i = 1; i <= 16; i++)
            {
                // Add frame using sprite name (e.g., "walk/0001", "walk/0002", etc.)
                builder.AddFrame($"walk/{i:D4}", TimeSpan.FromMilliseconds(40));
            }
            builder.IsLooping(true);
        });
        _capguySprite = new AnimatedSprite(animationSheet, "walk");
    }

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

        _capguySprite.Update(gameTime);
        _xPosition = _xPosition + (int)(gameTime.ElapsedGameTime.TotalSeconds * _speedInPixelsPerSecond);
        _xPosition = _xPosition % (_graphics.PreferredBackBufferWidth + 100);
        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        _spriteBatch.Begin();
        _spriteBatch.Draw(_backgroundSprite, Vector2.Zero);
        _spriteBatch.Draw(_capguySprite, new Vector2(_xPosition, 580));
        _spriteBatch.End();

        base.Draw(gameTime);
    }
}

The character now walks across the screen continuously. The complete example project is available on GitHub.