Your First MonoGame Sprite

Welcome to MonoGame! In this tutorial you will create a new project from scratch, load an image through the Content Pipeline, draw it to the screen with SpriteBatch, move it with keyboard input, and keep it within the window bounds — all in under 100 lines of C#.

By the end you will understand the MonoGame game loop, texture loading, basic rendering, and frame-rate-independent movement.

💡 This tutorial targets the mgdesktopgl template, which runs on Windows, macOS, and Linux. All concepts apply equally to other MonoGame platforms.

Prerequisites

You will need the following installed before starting:

dotnet new install MonoGame.Templates.CSharp

Create a new project and confirm it runs:

dotnet new mgdesktopgl -o MySpriteGame
cd MySpriteGame
dotnet run

A cornflower-blue window should appear. That is your MonoGame game loop running at 60 updates per second.

Project Overview

The template creates two important files:

  • Game1.cs — the main game class. Override Initialize, LoadContent, Update, and Draw here.
  • Content/Content.mgcb — the Content Pipeline build manifest. All assets are registered here so the pipeline can compile them to the .xnb format.

📁 All assets — textures, audio files, fonts — must be added to the Content Pipeline. The pipeline compiles them into an optimised binary format and copies them to your output folder automatically.

Adding a Texture to the Content Pipeline

Create or download any small PNG image (64×64 pixels is ideal for testing) and save it as Content/player.png inside your project.

Open the MGCB Editor to register the asset:

dotnet mgcb-editor Content/Content.mgcb

In the editor select Edit → Add → Existing Item…, pick player.png, then save. Alternatively, add the entry directly to Content.mgcb by hand:

#begin player.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:player.png

The /build: line is the asset path relative to the Content/ folder. The string before .png is the asset name you will pass to Content.Load.

Loading the Texture in Code

Open Game1.cs. Add a field for the texture and the sprite’s position, then load the texture inside LoadContent:

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

namespace MySpriteGame;

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

    // The loaded texture for the player sprite
    private Texture2D _playerTexture;

    // Screen-space position of the sprite (top-left corner)
    private Vector2 _playerPosition;

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

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

        // Asset name matches the name in Content.mgcb — no path, no extension
        _playerTexture = Content.Load<Texture2D>("player");

        // Centre the sprite in the window on startup
        _playerPosition = new Vector2(
            GraphicsDevice.Viewport.Width  / 2f - _playerTexture.Width  / 2f,
            GraphicsDevice.Viewport.Height / 2f - _playerTexture.Height / 2f
        );
    }
}

⚠️ The string passed to Content.Load<T>() must match the asset name in Content.mgcb exactly — no file extension, and no leading Content/ path.

Drawing the Sprite with SpriteBatch

SpriteBatch collects 2D draw calls and submits them to the GPU in one efficient batch. The pattern is always:

  1. Call _spriteBatch.Begin() to open a new batch.
  2. Call one or more _spriteBatch.Draw() overloads.
  3. Call _spriteBatch.End() to flush the batch to the GPU.
protected override void Draw(GameTime gameTime)
{
    // Clear the back buffer to a solid colour each frame
    GraphicsDevice.Clear(Color.CornflowerBlue);

    _spriteBatch.Begin(
        sortMode:        SpriteSortMode.Deferred,
        blendState:      BlendState.AlphaBlend,   // correct for pre-multiplied alpha PNGs
        samplerState:    SamplerState.PointClamp, // crisp pixel art; use LinearClamp for smooth art
        depthStencilState: null,
        rasterizerState:   null
    );

    // Full nine-argument overload for clarity
    _spriteBatch.Draw(
        texture:         _playerTexture,
        position:        _playerPosition,
        sourceRectangle: null,              // null = draw the entire texture
        color:           Color.White,       // White = no tint; use other colours to tint
        rotation:        0f,                // radians
        origin:          Vector2.Zero,      // anchor point within the texture
        scale:           Vector2.One,       // 1:1 scale
        effects:         SpriteEffects.None,
        layerDepth:      0f                 // 0 = front, 1 = back (used with SpriteSortMode.BackToFront)
    );

    _spriteBatch.End();

    base.Draw(gameTime);
}

Build and run with dotnet run. Your sprite should now appear in the centre of the window.

💡 Pre-multiplied alpha: MonoGame’s Content Pipeline pre-multiplies alpha by default. Always use BlendState.AlphaBlend (the default) with such textures. Using NonPremultiplied will cause dark fringes around transparent edges.

Moving the Sprite with Keyboard Input

MonoGame uses a polling input model. Call Keyboard.GetState() each frame and query the result. Multiply any velocity by delta time (gameTime.ElapsedGameTime.TotalSeconds) to keep movement frame-rate-independent.

// Pixels per second the sprite moves
private const float MoveSpeed = 200f;

protected override void Update(GameTime gameTime)
{
    // Standard exit — gamepad Back button or Escape key
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed
        || Keyboard.GetState().IsKeyDown(Keys.Escape))
    {
        Exit();
    }

    KeyboardState ks = Keyboard.GetState();

    // Delta time: elapsed seconds since the last frame
    float delta = (float)gameTime.ElapsedGameTime.TotalSeconds;

    // WASD and arrow keys for movement
    if (ks.IsKeyDown(Keys.W) || ks.IsKeyDown(Keys.Up))
        _playerPosition.Y -= MoveSpeed * delta;

    if (ks.IsKeyDown(Keys.S) || ks.IsKeyDown(Keys.Down))
        _playerPosition.Y += MoveSpeed * delta;

    if (ks.IsKeyDown(Keys.A) || ks.IsKeyDown(Keys.Left))
        _playerPosition.X -= MoveSpeed * delta;

    if (ks.IsKeyDown(Keys.D) || ks.IsKeyDown(Keys.Right))
        _playerPosition.X += MoveSpeed * delta;

    base.Update(gameTime);
}

The sprite now moves at exactly 200 pixels per second regardless of whether the game is running at 30 FPS or 144 FPS. Delta-time scaling is one of the most important habits to build as a game developer.

💡 Why delta time? Without it, movement speed depends on frame rate. On a fast machine the sprite races; on a slow machine it crawls. Multiplying by delta converts units per frame into units per second.

Keeping the Sprite On Screen

Without clamping, the sprite will slide off the edges of the window. MathHelper.Clamp(value, min, max) constrains a value to a range. Subtract the sprite dimensions so the right and bottom edges stay visible:

// Add this after the movement block, still inside Update()

int screenW = GraphicsDevice.Viewport.Width;
int screenH = GraphicsDevice.Viewport.Height;

// Clamp X so left edge >= 0 and right edge <= screen width
_playerPosition.X = MathHelper.Clamp(
    _playerPosition.X,
    0f,
    screenW - _playerTexture.Width
);

// Clamp Y so top edge >= 0 and bottom edge <= screen height
_playerPosition.Y = MathHelper.Clamp(
    _playerPosition.Y,
    0f,
    screenH - _playerTexture.Height
);

💡 If the sprite uses a non-zero origin (e.g. centred), adjust the clamp range accordingly: origin.X becomes the minimum X, and screenW - (_playerTexture.Width - origin.X) becomes the maximum.

Complete Game1.cs

Here is the entire Game1.cs combining all of the above steps:

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

namespace MySpriteGame;

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

    private Texture2D _playerTexture;
    private Vector2   _playerPosition;

    private const float MoveSpeed = 200f;

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

    protected override void LoadContent()
    {
        _spriteBatch   = new SpriteBatch(GraphicsDevice);
        _playerTexture = Content.Load<Texture2D>("player");

        _playerPosition = new Vector2(
            GraphicsDevice.Viewport.Width  / 2f - _playerTexture.Width  / 2f,
            GraphicsDevice.Viewport.Height / 2f - _playerTexture.Height / 2f
        );
    }

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

        KeyboardState ks    = Keyboard.GetState();
        float         delta = (float)gameTime.ElapsedGameTime.TotalSeconds;

        if (ks.IsKeyDown(Keys.W) || ks.IsKeyDown(Keys.Up))    _playerPosition.Y -= MoveSpeed * delta;
        if (ks.IsKeyDown(Keys.S) || ks.IsKeyDown(Keys.Down))  _playerPosition.Y += MoveSpeed * delta;
        if (ks.IsKeyDown(Keys.A) || ks.IsKeyDown(Keys.Left))  _playerPosition.X -= MoveSpeed * delta;
        if (ks.IsKeyDown(Keys.D) || ks.IsKeyDown(Keys.Right)) _playerPosition.X += MoveSpeed * delta;

        int screenW = GraphicsDevice.Viewport.Width;
        int screenH = GraphicsDevice.Viewport.Height;

        _playerPosition.X = MathHelper.Clamp(_playerPosition.X, 0f, screenW - _playerTexture.Width);
        _playerPosition.Y = MathHelper.Clamp(_playerPosition.Y, 0f, screenH - _playerTexture.Height);

        base.Update(gameTime);
    }

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

        _spriteBatch.Begin();
        _spriteBatch.Draw(_playerTexture, _playerPosition, Color.White);
        _spriteBatch.End();

        base.Draw(gameTime);
    }
}

✅ The three-argument Draw(texture, position, color) overload is sufficient for simple cases. Switch to the nine-argument overload when you need rotation, scale, source rectangles, or custom origins.

Bonus: Tinting and Scaling

The color parameter acts as a multiplicative tint. Pass anything other than Color.White to colour the sprite at runtime without loading a new texture:

_spriteBatch.Begin();

// Original sprite — no tint, full size
_spriteBatch.Draw(_playerTexture, new Vector2(50, 50), Color.White);

// Red-tinted sprite at 50% opacity
_spriteBatch.Draw(_playerTexture, new Vector2(150, 50), Color.Red * 0.5f);

// 2x scaled sprite, drawn around its centre
Vector2 centre = new Vector2(_playerTexture.Width / 2f, _playerTexture.Height / 2f);
_spriteBatch.Draw(
    texture:         _playerTexture,
    position:        new Vector2(300, 100),
    sourceRectangle: null,
    color:           Color.White,
    rotation:        0f,
    origin:          centre,   // anchor at texture centre
    scale:           2f,       // uniform 2x scale
    effects:         SpriteEffects.None,
    layerDepth:      0f
);

_spriteBatch.End();

Color.Red * 0.5f multiplies each component (R, G, B, A) by 0.5, halving the opacity.

Next Steps

You now have a working, moving, bounded sprite. Here are some natural progressions from here:

  • Add a second texture and draw it as a background at a lower layerDepth
  • Add simple AABB collision with Rectangle.Intersects()
  • Explore the Sprite Animation article to add a walk cycle
  • Explore the Input Handling article to add gamepad support
  • Explore the Audio Basics article to add footstep sounds