Tutorial 5 – Creating a 2D Camera with Pan and Zoom in MonoGame

This is part 5 in a set of tutorials for using RogueSharp and MonoGame to create a basic Roguelike game. It is meant to serve as an introduction to the RogueSharp library.

If you missed part 4 of the tutorial you can find it here: https://roguesharp.wordpress.com/2014/06/09/tutorial-4-roguelike-pathfinding-using-roguesharp-and-monogame/

Goal

In this tutorial we will add a Camera class to our game that can be manipulated with the keyboard. Additionally when the Player is moving, the camera should always remain centered on the Player. The camera will also provide the ability to zoom in and out. This post is in response to a comment from Joe asking how to do this.

Refactoring LayerDepth

Before we get started with the camera class, lets fix those magic numbers for the sprite layer depth that we had in all of our SpriteBatch.Draw( … ). We’re going to refactor those into a class so that we don’t have to memorize those numbers.

Add a new class named LayerDepth.cs with the following code:

public static class LayerDepth
{
  public static readonly float Cells = 0.8f;
  public static readonly float Paths = 0.6f;
  public static readonly float Figures = 0.5f;
}

Now everyplace we have a spriteBatch.Draw with float specifying the layer depth like 0.8f, replace it with a static member variable from this class like LayerDepth.Cells

Follow along with all the replacements on the Bitbucket code

Removing Sprite Scaling Code

Next we are going to go through all of our SpriteBatch.Draw( … ) calls and remove all of the hardcoded scale variables. We were scaling all of our Sprites to 1/4 size by specifying a Scale of 0.25f.

You can follow along with these removals on the Bitbucket code but basically it will look like the following code:

// Old code with scaling
spriteBatch.Draw( _wall, position, null, null, null, 0.0f,
   new Vector2( scale, scale ), tint, SpriteEffects.None, LayerDepth.Cells );

// New code with scale set to Vector2.One which means no scaling
spriteBatch.Draw( _wall, position, null, null, null, 0.0f,
   Vector2.One, tint, SpriteEffects.None, LayerDepth.Cells );

Move Map and Sprite Sizes to Global

The last thing we are going to refactor is all of the map and sprite sizes that we have throughout the code. We’ll put those in Global.cs and then reference them from the rest of the game when necessary.

// In Global.cs add the following.
public static readonly int MapWidth = 50;
public static readonly int MapHeight = 30;
public static readonly int SpriteWidth = 64;
public static readonly int SpriteHeight = 64;

Now everywhere in the code where we had these numbers we’ll replace them with these static variables from Global.cs.

Follow along in the code on Bitbucket here

After we are finished with all this work, if we start our game we’ll most likely see a completely black screen. If you hit the “spacebar” we’ll see the non-scaled top left corner of the map something like this:

Top Right Map Corner

Map Without Scaling

Adding New Keys to InputState

In order for the player to be able to manipulate the camera by hand we need to be able to capture more key presses for controlling the camera. I’m going to use “WASD” keys for panning the camera and “,.” keys for zooming in and out. Feel free to change these to anything you want. The important thing is that we update our InputState.cs class to be able to handle the keys.

public bool IsScrollLeft( PlayerIndex? controllingPlayer )
{
   PlayerIndex playerIndex;
   return IsKeyPressed( Keys.A, controllingPlayer, out playerIndex );
}
public bool IsScrollRight( PlayerIndex? controllingPlayer )
{
   PlayerIndex playerIndex;
   return IsKeyPressed( Keys.D, controllingPlayer, out playerIndex );
}
public bool IsScrollUp( PlayerIndex? controllingPlayer )
{
   PlayerIndex playerIndex;
   return IsKeyPressed( Keys.W, controllingPlayer, out playerIndex );
}
public bool IsScrollDown( PlayerIndex? controllingPlayer )
{
   PlayerIndex playerIndex;
   return IsKeyPressed( Keys.S, controllingPlayer, out playerIndex );
}
public bool IsZoomOut( PlayerIndex? controllingPlayer )
{
   PlayerIndex playerIndex;
   return IsNewKeyPress( Keys.OemPeriod, controllingPlayer, out playerIndex );
}

public bool IsZoomIn( PlayerIndex? controllingPlayer )
{
   PlayerIndex playerIndex;
   return IsNewKeyPress( Keys.OemComma, controllingPlayer, out playerIndex );
}

The code so far

Introducing the Camera Class

Next we need to add a new class to our solution named “Camera.cs” and add the following code below to it. This is a large class and I’m going to try something different by putting comments before the interesting lines instead of describing the code afterwords by referencing line-numbers like I did in previous tutorials. Let me know which approach you like better for future articles.

public class Camera
{
   // Construct a new Camera class with standard zoom (no scaling)
   public Camera()
   {
      Zoom = 1.0f;
   }

   // Centered Position of the Camera in pixels.
   public Vector2 Position { get; private set; }
   // Current Zoom level with 1.0f being standard
   public float Zoom { get; private set; }
   // Current Rotation amount with 0.0f being standard orientation
   public float Rotation { get; private set; }

   // Height and width of the viewport window which we need to adjust
   // any time the player resizes the game window.
   public int ViewportWidth { get; set; }
   public int ViewportHeight { get; set; }

   // Center of the Viewport which does not account for scale
   public Vector2 ViewportCenter
   {
      get
      {
         return new Vector2( ViewportWidth * 0.5f, ViewportHeight * 0.5f );
      }
   }

   // Create a matrix for the camera to offset everything we draw,
   // the map and our objects. since the camera coordinates are where
   // the camera is, we offset everything by the negative of that to simulate
   // a camera moving. We also cast to integers to avoid filtering artifacts.
   public Matrix TranslationMatrix
   {
      get
      {
         return Matrix.CreateTranslation( -(int) Position.X,
            -(int) Position.Y, 0 ) *
            Matrix.CreateRotationZ( Rotation ) *
            Matrix.CreateScale( new Vector3( Zoom, Zoom, 1 ) ) *
            Matrix.CreateTranslation( new Vector3( ViewportCenter, 0 ) );
      }
   }

   // Call this method with negative values to zoom out
   // or positive values to zoom in. It looks at the current zoom
   // and adjusts it by the specified amount. If we were at a 1.0f
   // zoom level and specified -0.5f amount it would leave us with
   // 1.0f - 0.5f = 0.5f so everything would be drawn at half size.
   public void AdjustZoom( float amount )
   {
      Zoom += amount;
      if ( Zoom < 0.25f )
      {
         Zoom = 0.25f;
      }
   }

   // Move the camera in an X and Y amount based on the cameraMovement param.
   // if clampToMap is true the camera will try not to pan outside of the
   // bounds of the map.
   public void MoveCamera( Vector2 cameraMovement, bool clampToMap = false )
   {
      Vector2 newPosition = Position + cameraMovement;

      if ( clampToMap )
      {
         Position = MapClampedPosition( newPosition );
      }
      else
      {
         Position = newPosition;
      }
   }

   public Rectangle ViewportWorldBoundry()
   {
      Vector2 viewPortCorner = ScreenToWorld( new Vector2( 0, 0 ) );
      Vector2 viewPortBottomCorner =
         ScreenToWorld( new Vector2( ViewportWidth, ViewportHeight ) );

      return new Rectangle( (int) viewPortCorner.X,
         (int) viewPortCorner.Y,
         (int) ( viewPortBottomCorner.X - viewPortCorner.X ),
         (int) ( viewPortBottomCorner.Y - viewPortCorner.Y ) );
   }

   // Center the camera on specific pixel coordinates
   public void CenterOn( Vector2 position )
   {
      Position = position;
   }

   // Center the camera on a specific cell in the map
   public void CenterOn( Cell cell )
   {
      Position = CenteredPosition( cell, true );
   }

   private Vector2 CenteredPosition( Cell cell, bool clampToMap = false )
   {
      var cameraPosition = new Vector2( cell.X * Global.SpriteWidth,
         cell.Y * Global.SpriteHeight );
      var cameraCenteredOnTilePosition =
         new Vector2( cameraPosition.X + Global.SpriteWidth / 2,
             cameraPosition.Y + Global.SpriteHeight / 2 );
      if ( clampToMap )
      {
         return MapClampedPosition( cameraCenteredOnTilePosition );
      }

      return cameraCenteredOnTilePosition;
   }

   // Clamp the camera so it never leaves the visible area of the map.
   private Vector2 MapClampedPosition( Vector2 position )
   {
      var cameraMax = new Vector2( Global.MapWidth * Global.SpriteWidth -
          ( ViewportWidth / Zoom / 2 ),
          Global.MapHeight * Global.SpriteHeight -
          ( ViewportHeight / Zoom / 2 ) );

      return Vector2.Clamp( position,
         new Vector2( ViewportWidth / Zoom / 2, ViewportHeight / Zoom / 2 ),
         cameraMax );
   }

   public Vector2 WorldToScreen( Vector2 worldPosition )
   {
      return Vector2.Transform( worldPosition, TranslationMatrix );
   }

   public Vector2 ScreenToWorld( Vector2 screenPosition )
   {
      return Vector2.Transform( screenPosition,
          Matrix.Invert( TranslationMatrix ) );
   }

   // Move the camera's position based on input
   public void HandleInput( InputState inputState,
      PlayerIndex? controllingPlayer )
   {
      Vector2 cameraMovement = Vector2.Zero;

      if ( inputState.IsScrollLeft( controllingPlayer ) )
      {
         cameraMovement.X = -1;
      }
      else if ( inputState.IsScrollRight( controllingPlayer ) )
      {
         cameraMovement.X = 1;
      }
      if ( inputState.IsScrollUp( controllingPlayer ) )
      {
         cameraMovement.Y = -1;
      }
      else if ( inputState.IsScrollDown( controllingPlayer ) )
      {
         cameraMovement.Y = 1;
      }
      if ( inputState.IsZoomIn( controllingPlayer ) )
      {
         AdjustZoom( 0.25f );
      }
      else if ( inputState.IsZoomOut( controllingPlayer ) )
      {
         AdjustZoom( -0.25f );
      }

      // When using a controller, to match the thumbstick behavior,
      // we need to normalize non-zero vectors in case the user
      // is pressing a diagonal direction.
      if ( cameraMovement != Vector2.Zero )
      {
         cameraMovement.Normalize();
      }

      // scale our movement to move 25 pixels per second
      cameraMovement *= 25f;

      MoveCamera( cameraMovement, true );
   }
}

Whew! That’s a lot of code. We should go ahead and hook it up to see all this work pay off.

And here is the link to the code on Bitbucket

Hooking up the Camera

Lets first add a line to our Global.cs

public static readonly Camera Camera = new Camera();  

This will let us access the Camera from anywhere.

Now we’ll want to update Game1.cs first by setting the Camera Viewport Width and Height in the Intialize() method.

Global.Camera.ViewportWidth = graphics.GraphicsDevice.Viewport.Width;
Global.Camera.ViewportHeight = graphics.GraphicsDevice.Viewport.Height;

We’ll also want to center the Camera on the Player’s starting cell by adding the following code to the LoadContent() method of Game1.cs

Global.Camera.CenterOn( startingCell )

In the Update( … ) method of Game1.cs we want to to call the HandleInput( … ) method on the Camera class to take care of any pan or zoom key presses from the player.

Global.Camera.HandleInput( _inputState, PlayerIndex.One );

Also in the Update( … ) method of Game1.cs in the block where we handle player input, we want to add a bit of code to re-center the Camera on the Player after the Player moves.

Global.Camera.CenterOn( _map.GetCell( _player.X, _player.Y ) );

And the very last bit is the best. When we call the Draw( … ) method of Game1.cs and make our spriteBatch.Begin( … ) call we want to pass in the Camera’s translation matrix. That’s all we have to do to get everything to draw appropriately and at the correct scale.

// The old spriteBatch.Begin call looked like this...
spriteBatch.Begin( SpriteSortMode.BackToFront, BlendState.AlphaBlend );

// Replace it with this call, with the final parameter being the Camera's translation matrix
spriteBatch.Begin( SpriteSortMode.BackToFront, BlendState.AlphaBlend, 
    null, null, null, null, Global.Camera.TranslationMatrix );

If you run the game now you’ll have full camera control.

Controlling the Camera

Controlling the Camera

The complete code

Advertisements

10 thoughts on “Tutorial 5 – Creating a 2D Camera with Pan and Zoom in MonoGame

  1. Joe S

    I first found these tutorials about a month ago, and have been following them since. Making a roguelike (with graphics) is something that I’ve really been wanting to do for a while now, and this has easily been one of the better series that I’ve found for getting started on that, so I’m really looking forward to the continuation of these tutorials. Keep up the great work!

    Reply
    1. Faron Bracy Post author

      Thank you for commenting here. It’s really encouraging for me to hear that others are getting use out of this tutorial series. I wish I could say that I had all the posts planned out ahead of time, but I’m more or less making them up as I go. If you have a certain topic that you want me to cover please let me know.

      Reply
  2. Pingback: Tutorial 6 – Roguelike Combat using RogueSharp and MonoGame | Creating a Roguelike Game in C#

  3. caneastrale

    Hey man sorry to bother you again but I still have some troubles understanding some aspects of your camera since I’m not a so experienced developer.
    What I’m having trouble with is drawing some simple HUD which will display things like name and player attributes; i succeeded in importing fonts and drawing them but the thing is: they just stay there in the map.
    When I draw them I enter the coordinates via a Vector2, but the drawn text just stays at the coordinates. I thought of something like moving them along with camera movements or something like that but can’t find a way to implement it.
    Let me know if you need the code or some images of what I’m seeing.

    Reply
    1. Faron Bracy Post author

      Hello,
      If I understand what you are saying, you want a health bar or some text like the player or monster name to stay with the player or monster as the character moves around the screen.

      In order to do this you should draw the text or other HUD elements in the same spriteBatch as you are drawing the character. This would also be the same spriteBatch that has the transformation matrix from the camera passed in to it.

      For example assuming that you are following along with the tutorial and if you wanted to draw the text “Rogue” above the player you could edit the Draw method of Player.cs to look like this.

      public void Draw( SpriteBatch spriteBatch, SpriteFont font )
      {
      spriteBatch.Draw( Sprite, new Vector2( X * Sprite.Width, Y * Sprite.Height ), null, null, null, 0.0f, Vector2.One,
      Color.White, SpriteEffects.None, LayerDepth.Figures );
      spriteBatch.DrawString( font, “Rogue”, new Vector2( X * Sprite.Width, (Y * Sprite.Height) – 60 ), Color.Red );
      }

      Which would result in something like this -> http://screencast.com/t/NjIqD9gu

      Hope that I am understanding you correctly and that this helps accomplish what you are looking for.

      Reply
    1. Faron Bracy Post author

      Sorry I didn’t understand what you were attempting to do. The screen recording really helps. What you want to do in that case is the opposite of what I mentioned. Somewhere in your Draw method of your main class you should have a SpriteBatch.Begin that takes the camera’s transformation matrix. The key part to getting your text where you want is to not draw your text between that Begin and End that uses the transformation matrix. Instead use that to draw your player and all your tiles. After calling SpriteBatch.End, follow that up with a new SpriteBatch.Begin that does not use the transformation matrix. Draw your text as a part of that.

      Here is an example of code from my own game that does this:
      http://bit.ly/1U8LCmG

      And a video to show the result:
      http://screencast.com/t/awvEGpbHtbbz

      Reply
  4. Pingback: 2DCamera Monogame | Crash N Build

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