FPS Camera
Let’s go ahead and create a camera that the player can actually control. This time, we’ll adopt a camera made popular by PC first-person shooters, where the player’s looking direction is controlled by the mouse, and the WASD keys move forward and back and strife side-to-side.
The FPS Camera Class
Let’s start by defining our class, FPSCamera
:
/// <summary>
/// A camera controlled by WASD + Mouse
/// </summary>
public class FPSCamera : ICamera
{
}
Private Fields
This camera is somewhat unique in it partially the splits vertical from horizontal axes; the vertical axis only controls the angle the player is looking along, while the horizontal axis informs both looking and the direction of the player’s movement. Thus, we’ll need to track these angles separately, and combine them when needed:
// The angle of rotation about the Y-axis
float horizontalAngle;
// The angle of rotation about the X-axis
float verticalAngle;
We also need to keep track of the position of the camera in the world:
// The camera's position in the world
Vector3 position;
And we need to know what the previous state of the mouse was:
// The state of the mouse in the prior frame
MouseState oldMouseState;
And an instance of the Game
class:
// The Game this camera belongs to
Game game;
Public Properties
We need to define the View
and Projection
matrices to meet our ICamera
inteface requirements:
/// <summary>
/// The view matrix for this camera
/// </summary>
public Matrix View { get; protected set; }
/// <summary>
/// The projection matrix for this camera
/// </summary>
public Matrix Projection { get; protected set; }
We’ll keep the setters protected, as they should only be set from within the camera (or a derived camera).
We also will provide a Sensitivity
value for fine-tuning the mouse sensitivity; this would likely be adjusted from a menu, so it needs to be public:
/// <summary>
/// The sensitivity of the mouse when aiming
/// </summary>
public float Sensitivity { get; set; } = 0.0018f;
We’ll likewise expose the speed property, as it may be changed in-game to respond to powerups or special modes:
/// <summary>
/// The speed of the player while moving
/// </summary>
public float Speed { get; set; } = 0.5f;
The Constructor
Constructing the FPSCamera
requires a Game
instance, and an initial position:
/// <summary>
/// Constructs a new FPS Camera
/// </summary>
/// <param name="game">The game this camera belongs to</param>
/// <param name="position">The player's initial position</param>
public FPSCamera(Game game, Vector3 position)
{
this.game = game;
this.position = position;
}
Inside the constructor, we’ll initialize our angles to 0
(alternatively, you might also add a facing angle to the constructor so you can control both where the player starts and the direction they face):
this.horizontalAngle = 0;
this.verticalAngle = 0;
We’ll also set up our projection matrix:
this.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, game.GraphicsDevice.Viewport.AspectRatio, 1, 1000);
And finally, we’ll center the mouse in the window, and save its state:
Mouse.SetPosition(game.Window.ClientBounds.Width / 2, game.Window.ClientBounds.Height / 2);
oldMouseState = Mouse.GetState();
The Update Method
The Update()
method is where the heavy lifting of the class occurs, updating the camera position and calculating the view matrix. There’s a lot going on here, so we’ll assemble it line-by-line, discusing each as we add it:
/// <summary>
/// Updates the camera
/// </summary>
/// <param name="gameTime">The current GameTime</param>
public void Update(GameTime gameTime)
{
}
First up, we’ll grab current input states:
var keyboard = Keyboard.GetState();
var newMouseState = Mouse.GetState();
Then we’ll want to handle movement. Before we move the camera, we need to know what direction it is currenlty facing. We can represent this with a Vector3
in that direction, which we calculate by rotating a forward vector by the horizontal angle:
// Get the direction the player is currently facing
var facing = Vector3.Transform(Vector3.Forward, Matrix.CreateRotationY(horizontalAngle));
Then we can apply forward and backward movement along this vector when the W or S keys are pressed:
// Forward and backward movement
if (keyboard.IsKeyDown(Keys.W)) position += facing * Speed;
if (keyboard.IsKeyDown(Keys.S)) position -= facing * Speed;
The A and D keys provide strifing movement, movement perpendicular to the forward vector. We can find this perpendicular vector by calculating the cross product of the facing and up vectors:
// Strifing movement
if (keyboard.IsKeyDown(Keys.A)) position += Vector3.Cross(Vector3.Up, facing) * Speed;
if (keyboard.IsKeyDown(Keys.D)) position -= Vector3.Cross(Vector3.Up, facing) * Speed;
That wraps up moving the camera’s position in the world. Now we need to tackle where the camera is looking. This means adusting the vertical and horizontal angles based on mouse movement this frame (which we caculate by subtracing the new mouse position from the old):
// Adjust horizontal angle
horizontalAngle += Sensitivity * (oldMouseState.X - newMouseState.X);
// Adjust vertical angle
verticalAngle += Sensitivity * (oldMouseState.Y - newMouseState.Y);
From these angles, we can calculate the direction the camera is facing, by rotating a forward-facing vector in both the horizontal and vertical axes:
direction = Vector3.Transform(Vector3.Forward, Matrix.CreateRotationX(verticalAngle) * Matrix.CreateRotationY(horizontalAngle));
With that direction, we can now calculate the view matrix using Matrix.CreateLookAt()
. The target vector is the direction vector added to the position:
// create the veiw matrix
View = Matrix.CreateLookAt(position, position + direction, Vector3.Up);
Lastly, we reset the mouse state. First we re-center the mouse, and then we save its new centered state as our old mouse state. This centering is important in Windowed mode, as it keeps our mouse within the window even as the player spins 360 degrees or more. Otherwise, our mouse would pop out of the window, and could interact with other windows while the player is trying to play our game.
// Reset mouse state
Mouse.SetPosition(game.Window.ClientBounds.Width / 2, game.Window.ClientBounds.Height / 2);
oldMouseState = Mouse.GetState();
This does mean that you can no longer use the mouse to close the window, so it is important to have a means to exit the game. By default, the Game1
class uses hitting the escape key to do this. In full games you’ll probably replace that functionality with a menu that contains an exit option.
Refactoring the Game Class
Of course, to use this camera, you’ll need to replace the CirclingCamera
references in Game1
with our FPSCamera
implementation. So you’ll define a private FPSCamera
reference:
// The game camera
FPSCamera camera;
Initialize it with its starting position in the LoadContent()
method:
// Initialize the camera
camera = new FPSCamera(this, new Vector3(0, 3, 10));
Update it in the Update()
method (which isn’t really a change):
// Update the camera
camera.Update(gameTime);
And provide it to the crates in the Draw()
method (again, this shouldn’t be a change from the CirclingCamera
implementation):
// Draw some crates
foreach(Crate crate in crates)
{
crate.Draw(camera);
}
Now if you run the game, you should be able to move around the scene using WASD keys and the mouse.