First Person Shooter Cameras
So, it's been a long time since my last post. Sorry about that. We've been busy lately, which should put smiles on all your faces. I like my job, so I can't give any specifics, but we've got some stuff coming up we think you'll be excited about.
Anyway,I want to write a quick post about how to do a camera for a first person shooter. This won't be the king of cameras by any means, but hopefully it should be enough to give you a jumping off point. Basically, it's just going to have mouse look and the keyboard arrow keys will control the movement. To keep the code simple, I'm leaving out GamePad input, which you'll probably want to hook up if you're planning on using this on your 360 :)
Sorry about the sketchy formatting throughout this post; either Windows Live Writer is messed up, or I am, and I'm too tired to argue with it to see who's fault it is.
let's get the boring stuff out of the way first. The camera is a DrawableGameComponent, which means I can add it to my game's Components collection and it will get updated automatically. Although it will never draw itself, I've still made it a DrawableGameComponent, because I'll want to get at the GraphicsDevice every now and then.
Then we've got a bunch of different properties and fields that will store the data that's important to keeping the camera running. The camera has a position in the world and a movement speed, which controls how quickly the camera moves. It stores two vectors for up and forward, which other classes can use but not set. It's got pitch and yaw, which represent rotations around the X and Y axes, respectively.
Notice that I'm capping pitch between -89 and 89 degrees: almost straight up and down, but not quite. This makes the camera a lot simpler, both from a coding and from a usability standpoint. If you don't do this, it's possible to "flip" your camera over, and all of a sudden moving the mouse left turns the camera right, and things basically get topsyturvy.
The two most important members of this class are the View and Projection matrices. Without these two guys this whole class would basically be worthless. There's lots of explanations about these out there on the web, so if you find my explanation insufficient, don't panic. There's a ton of resources out there. For now, suffice to say that the view matrix contains information about the position and orientation of the camera. The projection matrix contains information about the aspect ratio of the camera, and how far it can see (the near and far clip planes.) Any time you want to draw a triangle, the triangle's vertices have to be transformed by the view and projection matrices.
So, here's the code for that bit:
public class FirstPersonShooterCamera : DrawableGameComponent
{
private Matrix view;
public Matrix View
{
get { return view; }
}
private Matrix projection;
public Matrix Projection
{
get { return projection; }
}
private Vector3 position = new Vector3(0,0,0);
public Vector3 Position
{
get { return position; }
}
private float yaw;
public float Yaw
{
get { return yaw; }
set { yaw = value; }
}
private float pitch;
public float Pitch
{
get { return pitch; }
set
{
pitch = MathHelper.Clamp(value,
-MathHelper.ToRadians(89), MathHelper.ToRadians(89));
}
}
private float movementSpeed = 100;
public float MovementSpeed
{
get { return movementSpeed; }
set { movementSpeed = value; }
}
private Vector3 up;
public Vector3 Up
{
get { return up; }
}
private Vector3 forward;
public Vector3 Forward
{
get { return forward; }
}
Got all that? Here's a short little blip, showing how the camera gets initialized (and why I need the GraphicsDevice.) The constructor is really simple; it chains to the base DrawableGameComponent, and then sets Visible to false, so the camera's Draw method will never get called. Similarly, Initialize calls the base class's initialize, and then calls UpdateProjectionMatrix. You might be wondering why UpdateProjectionMatrix can't be called from the constructor, but if you look, you'll see that the GraphicsDevice property is used, which won't be available until base.Initialize has finished.
Remember, the projection matrix is the one that controls the virtual "lens" of the camera, including the field of view, aspect ratio, and the far and near clip planes. 45 degrees is a pretty standard value for field of view, but you can get some pretty cool effects by messing with it. Lots of games will stretch this out really wide, which is really weird looking, but makes the player feel like he is moving REALLY fast. Burnout does this really well. Aspect ratio we calculate from the viewport width and height.
View distance we set to some values that make sense to me, 1 and 1000. Those two numbers could be totally different for your game, feel free to put in whatever you want. Basically, they mean that everything closer to the camera than 1 unit will be clipped, and likewise for anything further away than 1000. A common mistake is to make your far distance really really really far. After all, if you can afford to spend the time to render it, why would you want to clip out part of your beautiful world? Well, remember, your depth buffer has limited precision. (I think the framework's default is 24 bit depth buffer, 8 bit stencil.) Because of the way floating point works, you can either have large ranges of values, or precision in those values. The larger your range of values get, the smaller your precision is. What's that mean? If you're not careful, setting your farclip to something very large can cause a loss of precision, which causes z-fighting.
A more robust camera would have field of view, viewport, and clip planes as class properties, and their setters would invoke UpdateProjectionMatrix.
public FirstPersonShooterCamera(Game game) : base(game)
{
Visible = false;
}
public override void Initialize()
{
base.Initialize();
UpdateProjectionMatrix();
}
private void UpdateProjectionMatrix()
{
Viewport vp = GraphicsDevice.Viewport;
projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45),
vp.Width / (float)vp.Height, 1.0f, 1000.0f);
}
Here's the last bit of code needed to make this guy work - the Update stuff. First, update needs to check the mouse for yaw and pitch changes. Using that information, we'll figure out the orientation of the camera, using the Matrix.CreateRotation functions and our new yaw and pitch values. Next, HandleMovement, which returns a Vector3, is used to check keyboard input. Up and down change the Vector3's Z values, left and right change X. This vector is then transformed by the camera's orientation, so that hitting left moves us to the camera's left, and not just to the left in the world.
One thing to notice in the HandleMovement function is that I normalize the move vector. If I didn't do this, and the user was hitting both Up and Left, the camera would move along both X and Z at MovementSpeed units per second. For a camera, this is probably not so much a problem, but if this were code that controlled a player, for example, it wouldn't take the smart players long to realize that they can move faster when they are moving diagonally.
Finally, in HandleYawPitch, I'm using a nullable MouseState. Those are really neat, and let me keep track of whether or not I have a valid value for lastMouseState, so I know if the delta values will make any sense. I use nullables a lot for this kind of pattern, where I compare "lastSomething" to "somethingNow."
public override void Update(GameTime gameTime)
{
float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
HandleYawPitch(dt);
Matrix cameraOrientation =
Matrix.CreateRotationX(pitch) * Matrix.CreateRotationY(yaw);
Vector3 movement = HandleMovement(dt);
Vector3.Transform(ref movement, ref cameraOrientation, out movement);
position += movement;
view = Matrix.CreateLookAt(position,
position + cameraOrientation.Forward, Vector3.Up);
up = cameraOrientation.Up;
forward = cameraOrientation.Forward;
base.Update(gameTime);
}
private Vector3 HandleMovement(float dt)
{
KeyboardState keyboardState = Keyboard.GetState();
Vector3 move = Vector3.Zero;
if (keyboardState.IsKeyDown(Keys.Up))
{
move.Z -= 1;
}
if (keyboardState.IsKeyDown(Keys.Down))
{
move.Z += 1;
}
if (keyboardState.IsKeyDown(Keys.Left))
{
move.X -= 1;
}
if (keyboardState.IsKeyDown(Keys.Right))
{
move.X += 1;
}
if (move.LengthSquared() != 0)
{
move.Normalize();
}
move *= movementSpeed * dt;
return move;
}
MouseState? lastMouseState;
private void HandleYawPitch(float dt)
{
MouseState mouseState = Mouse.GetState();
if (lastMouseState != null)
{
Yaw -= (mouseState.X - lastMouseState.Value.X) * dt;
Pitch -= (mouseState.Y - lastMouseState.Value.Y) * dt;
}
lastMouseState = mouseState;
}
So, that's it. A simple camera (look ma! no quaternions!) which you might find useful, if only for just getting around in the world while you're debugging. There's definitely some improvements that can be made to make the camera more flexible. Also, I've got a whole CameraManager system that someone out there might find interesting, but that's for another time. Sorry the post was so short, but it's time to get some sleep, and then it's back to work tomorrow. I've got some more stuff that I can't tell you about to do :)
Comments
Anonymous
February 02, 2007
Great post. One question: in which cases would it be more handy to use "quaternions"?Anonymous
February 07, 2007
The comment has been removedAnonymous
February 07, 2007
Thanks for the answer. "Otherwise you can get gimbal lock. You'll have to web search for more information on what that is ..." So I did. As a rule I first always pay a visit to http://en.Wikipedia.org, but I've also found a pretty clear explanation of the phenomenon at this site: http://www.anticz.com/eularqua.htm (ok, 3DSMax-proned but clear).Anonymous
January 09, 2008
So I recently updated my XNA project to 2.0 to take advantage of the cool networking libraries and such