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:

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

]]>