Tag Archives: Camera

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