Create a 2D Camera in XNA GS 4.0

A couple of weeks ago I started a project to create a game in XNA, the main purpose being to teach myself the platform.  The goal is to create a hero siege game with RTS style input and gameplay, with a couple of queues from hero arenas like League of Legends.  If you’ve played many mods for Warcraft 3 or Starcraft 2 you may know the basic idea: You control a hero or two and try to defend your castle from waves of enemies that try to destroy it.  Your hero gains new abilities and levels as they kill enemies, and generally you buy items or earn them in mini-games which make your hero more powerful and able to keep pace with the increasingly tough enemy waves.

One of the things I wanted to have for this game was the ability to have a game map that was larger than the game’s viewport.  This way I could have a map that is large enough to be interesting, but still be able to zoom in and scroll around so the game elements are not too tiny.  To do this we need to understand the basics of matrix transformations.

The basic idea is that we can take a position on our world map (the large map which includes everything) and apply a transformation matrix to it and get what the position would be for a given viewport size and camera position.  It may sound complicated, but XNA has a lot of built in support for doing these types of transformations.  Still, it is good to have an understanding of it and I found Steve’s writeup on the subject to be very helpful:

Matrix Basics

Since this game is going to only be in 2D, I will be able to take advantage of XNA’s SpriteBatch class which has the ability to accept transformations for anything it draws. What I need is a class where I can store the camera’s current position, rotation, and zoom which can output the combined transformation matrix.  Once I have the matrix I will then pass it to SpriteBatch and let it do the heavy lifting.

public class Camera2d
{
   private const float zoomUpperLimit = 1.5f;
   private const float zoomLowerLimit = .5f;

   private float _zoom;
   private Matrix _transform;
   private Vector2 _pos;
   private float _rotation;
   private int _viewportWidth;
   private int _viewportHeight;
   private int _worldWidth;
   private int _worldHeight;        

   public Camera2d(Viewport viewport, int worldWidth, 
      int worldHeight, float initialZoom)
   {
      _zoom = initialZoom;
      _rotation = 0.0f;
      _pos = Vector2.Zero;
      _viewportWidth = viewport.Width;
      _viewportHeight = viewport.Height;
      _worldWidth = worldWidth;
      _worldHeight = worldHeight;
   }

   #region Properties

   public float Zoom
   {
       get { return _zoom; }
       set
       {
           _zoom = value;
           if (_zoom < zoomLowerLimit)
              _zoom = zoomLowerLimit;
           if (_zoom > zoomUpperLimit)
              _zoom = zoomUpperLimit;
       }
   }

   public float Rotation
   {
       get { return _rotation; }
       set { _rotation = value; }
   }

   public void Move(Vector2 amount)
   {
       _pos += amount;
   }

   public Vector2 Pos
   {
       get { return _pos; }
       set
       {
           float leftBarrier = (float)_viewportWidth *
                  .5f / _zoom;
           float rightBarrier = _worldWidth -
                  (float)_viewportWidth * .5f / _zoom;
           float topBarrier = _worldHeight -
                  (float)_viewportHeight * .5f / _zoom;
           float bottomBarrier = (float)_viewportHeight *
                  .5f / _zoom;
           _pos = value;
           if (_pos.X < leftBarrier)
               _pos.X = leftBarrier;
           if (_pos.X > rightBarrier)
               _pos.X = rightBarrier;
           if (_pos.Y > topBarrier)
               _pos.Y = topBarrier;
           if (_pos.Y < bottomBarrier)
               _pos.Y = bottomBarrier;
        }
   }

   #endregion

   public Matrix GetTransformation()
   {
     _transform = 
        Matrix.CreateTranslation(new Vector3(-_pos.X, -_pos.Y, 0)) *
        Matrix.CreateRotationZ(Rotation) *
        Matrix.CreateScale(new Vector3(Zoom, Zoom, 1)) *
        Matrix.CreateTranslation(new Vector3(_viewportWidth * 0.5f,
            _viewportHeight * 0.5f, 0));

       return _transform;
   }
}

Some quick notes about this class.  I have an upper and low bounds for my zoom so the game will be locked in a reasonable view size.  I am also confining my camera’s position to the world map’s bounds.

The most complicated part of this code is calculating the matrix transformation.   As the article above makes clear a huge benefit of calculating these transforms with matrices is that the properties of that math (Linear Algebra for anyone looking to learn more) let us simply multiply the transforms to get the cumulative transform.  Plus, XNA has built in classes to handle all the math for us.

Here is an example of how we can use this class.  Start by initializing the camera class with the world and view sizes you want, and setting the camera’s position to your desired starting location.  Then, by passing the camera’s transform matrix to SpriteBatch.Begin in the Draw method of our game loop, we will only see the section of map where the camera is.

ScreenManager.SpriteBatch.Begin(SpriteSortMode.BackToFront,
                    null, null, null, null, null,
                    Camera.GetTransformation());

// Draw the background texture
ScreenManager.SpriteBatch.Draw(Textures["BG"],
   new Rectangle(0, 0, worldWidth, worldHeight),
   null, Color.White, 0f, Vector2.Zero, SpriteEffects.None, 1);

ScreenManager.SpriteBatch.End();

To moved the camera or adjust the zoom we just need to change the properties of the camera in the game loop’s Update method.

// Adjust zoom if the mouse wheel has moved
if (mouseStateCurrent.ScrollWheelValue > previousScroll)
   Camera.Zoom += zoomIncrement;
else if (mouseStateCurrent.ScrollWheelValue < previousScroll)
   Camera.Zoom -= zoomIncrement;

previousScroll = mouseStateCurrent.ScrollWheelValue;

// Move the camera when the arrow keys are pressed
Vector2 movement = Vector2.Zero;
Viewport vp = ScreenManager.Game.GraphicsDevice.Viewport;

if (keyboardState.IsKeyDown(Keys.Left))
   movement.X--;
if (keyboardState.IsKeyDown(Keys.Right))
   movement.X++;
if (keyboardState.IsKeyDown(Keys.Up))
   movement.Y--;
if (keyboardState.IsKeyDown(Keys.Down))
   movement.Y++;

Camera.Pos += movement * 20;

The last thing I want to mention is mouse (or touch) input.  Any coordinates we use for moving and keeping track of sprites in our world should be done using coordinates relative to the world, not our adjustable view.  This way we can do the transformation once when we call SpriteBatch.Begin.  However, we want the coordinates of our pointing device to be relative to what we are seeing in our view, not the whole world.

For example, if our camera is all the way zoomed in and is showing us the bottom right portion of our world, if we click in the top left corner of the zoomed in view and don’t adjust the position in any way, our game is going to assume we are using world coordinates like everything else and it will not be clicking on what you are looking at in the view.

We can’t just apply our camera’s transformation to our mouse coordinates because that transformation converts world coordinates to view coordinates.  We want the opposite: view into world.  Luckily we are using matrices to perform our transforms and XNA matrices have a built in method for performing the inverse of a matrix.

// Transform mouse input from view to world position
Matrix inverse = Matrix.Invert(Camera.GetTransformation());
Vector2 mousePos = Vector2.Transform(
   new Vector2(mouseStateCurrent.X, mouseStateCurrent.Y), inverse);

I also wanted to add a few more links to articles and discussions that I found useful when puzzling these concepts out.  Hopefully this will make this information easier to find if you need more help, and feel free to comment or ask questions in the comments.

http://www.david-amador.com/2009/10/xna-camera-2d-with-zoom-and-rotation/

http://stackoverflow.com/questions/712296/xna-2d-camera-engine-that-follows-sprite

About these ads
This entry was posted in XNA. Bookmark the permalink.

6 Responses to Create a 2D Camera in XNA GS 4.0

  1. I just wanted to thank you for your excellent writeup. This was a real help in a school XNA-project I’m currently working on. Especially helpful for the problem I was having was the end bit, that was a wonderfully small and simple solution to a rather complicated problem.

  2. m50blog says:

    Thank you so much. I was having a lot of problems dealing with the mouse in the new “game world” viewed through the camera, and that last part helped me so much! Thanks a lot!

  3. Pingback: Planetary Gravity Simulation! | Jack Elliott's Blog

  4. butelie says:

    Thanks very much for the tutorial. It’s hard to find people willing to help you… I wish you the best ;)

  5. Slocombe says:

    Thanks very much. I’d been wrestling for days trying to get my 2D view to work. It was the transformations that were the real block.

  6. Caleb says:

    Thanks for the tutorial! it helped me out a lot! I couldn’t figure how to apply the matrix transformations to the spritebatch and the last bit about getting the world coordinates of where the player clicked will also come in handy!
    Thank you!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s