3D Level and FPS Controls
First, you need to make some changes so the code compiles.
In PlayerMove
:
- Change it so
PlayerMove
inherits fromMoveComponent
again - Remove the
#include "VehicleMove.h"
and instead#include "MoveComponent.h"
- Delete any member variables and any functions beyond the constructor,
Update
, andProcessInput
- Remove any #includes for files that don’t exist anymore
- Delete all the code inside of the constructor,
Update
, andProcessInput
- In
Update
, add a single call toMoveComponent::Update
In Player
:
- Remove the code that creates and sets a
MeshComponent
(since the first-person player won’t have one) - Remove the code that sets the scale to
0.75f
(so it will go back to the default of1.0f
) - Remove the code that creates a
PlayerUI
(since that won’t exist for now) - Remove the call to
SnapToIdeal
(or wherever you make that call) - Create a
CollisionComponent
with size(50, 100, 50)
- Add a
public
function calledHasGun
that returns abool
. For now, just returnfalse
(we’ll fix this later) - Remove any #includes for files that don’t exist anymore
In Game
:
- Remove any variables of types that don’t exist anymore, and any functions/code that were specific to Mario Kart’s racing like the start timer
- In
LoadData
, remove everything other than the part that creates and sets the projection matrix and the call tomAudio->CacheAllSounds()
. This includes removing the line that creates thePlayer
as we will do that in a different spot. - If you do not have a
Player*
member variable inGame
, add one and apublic
function calledGetPlayer()
which returns that pointer as well as aSetPlayer
function to set the pointer - Add a vector of
Actor*
calledmColliders
. We need this separate vector so we can specifically check against the actors we want the player to collide against (which is NOT all the actors). - An
AddColider
function that adds anActor*
tomColliders
-
A
RemoveCollider
function that removes aActor*
frommColliders
-
A getter function that returns
mColliders
by reference - Add the following two public functions, which we will update later:
class Portal* GetBluePortal() { return nullptr; } class Portal* GetOrangePortal() { return nullptr; }
In CameraComponent
:
- Remove everything related to the spring/follow camera
- Remove the
SnapToIdeal
function - Change
Update
so it just makes aLookAt
matrix where the eye is the owner’s position, the target is a point 50 units in front of the owner, and the up isVector3::UnitZ
. Then callSetViewMatrix
like before.
Next, make a new Actor
subclass called Block
, it needs:
- A constructor and destructor
- A scale of 64
- A
MeshComponent
using"Assets/Meshes/Cube.gpmesh"
-
A
CollisionComponent
component with size(1.0f, 1.0f, 1.0f)
- Call
AddCollider
in the constructor andRemoveCollider
in the destructor.
Finally, you need to add support for quaternions in actor. To do this, you need make the follow changes in Actor
:
- Add a member variable
Quaternion mQuat;
- Add a
void SetQuat(const Quaternion& quat)
function that setsmQuat
, and aconst Quaternion& GetQuat() const
function that returns it - In
CalcWorldTransform
, after multiplying by the rotation matrix, and before multiplying by the translation matrix, multiply byMatrix4::CreateFromQuaternion(mQuat)
- Add a
Vector3 GetQuatForward() const
function which is implemented as in the slides
Your code should now successfully compile and run, though you won’t see anything yet. To exit the game, you will have to use the ESC key because the game window will capture the mouse.
Level Loading
For this game, we’re going to use a custom JSON file format. The starting code includes some of the JSON loading code to start out.
At the end of Game::LoadData
, call LevelLoader::Load
(it’s a static function), passing in this
and "Assets/Lab09.json"
(you’ll need to include LevelLoader.h
)
Your code should still compile and run. You should see some blocks in the level.
Spawning Player and Camera
In LevelLoader.cpp, there’s a TODO
for spawning the player around line 47. Right above it, you’ll see there’s an if
for a "Block"
and how a block actor is constructed. You want to do the same thing, except create a Player*
instead of a Block*
. Don’t forget to do the actor =
part (as that pointer needs to be set for the actor’s position, scale, and rotation to be set correctly later in this function).
When this player is created, you also need to call SetPlayer(actor)
on game
, so that later on we can get the Player*
when needed.
Now when you run, you’ll see a game world which looks like this:
Props
Another type of actor in the game world is a Prop
. Here is an example of one of the props in the Lab09.json
level file:
{
"type":"Prop",
"pos":[500.0, -50.0, 150.0],
"scale":[100.0, 2.0, 500.0],
"mesh":"Assets/Meshes/Cube.gpmesh",
"texture":15,
"alpha":true,
"collision":true
},
The type
is what we’re checking in the if
statement in LoadActor
in LevelLoader.cpp. The pos
, scale
and texture
properties are already accounted for in the given code (under where there’s the comment about setting properties of the actor).
However, the mesh
, alpha
, and collision
properties are unique to Prop
:
mesh
is a string which contains the model file name to use for the prop (required property)alpha
is a bool that determines if the model should render with transparency (optional property – if not specified, defaults to false)collision
is a bool that determines if the model should have collision (optional property – if not specified, defaults to false)
Loading in the Properties
First, add an else if
case to LoadActor
for where type == "Prop"
. In here, you can get the additional properties using the various GetXXXFromJSON
functions.
For example, the following would set the usesAlpha
variable to the value of the alpha
property (and usesAlpha
will remain false
if alpha
is not specified):
bool usesAlpha = false;
GetBoolFromJSON(actorValue, "alpha", usesAlpha);
You must initialize basic types to some default value prior to the call to GetXXXFromJSON
because if the property does not exist, the value is not touched (and thus would be uninitialized).
You can use very similar code to get the value of the collision
bool property. For mesh
, use the GetStringFromJSON
function instead.
You should confirm you are correctly getting the values of alpha
, collision
, and mesh
. To do this, add a breakpoint inside the type == "Prop"
case and step over each GetXXXFromJSON
call. For the first Prop
in the level file, confirm that you end up with an alpha
of false, collision
of true, and a mesh
that’s "Assets/Meshes/Sign.gpmesh"
.
Prop Class
Now make a new Actor
subclass called Prop
. You likely will want to add those three additional properties as constructor arguments (or alternatively, add functions to setup the mesh/collision).
The Prop
needs to create a MeshComponent
where:
- The second parameter to the constructor is the
alpha
property value. - After the
MeshComponent
is created, set its mesh to theMesh*
that’s specified by themesh
property
The second parameter for the MeshComponent
constructor determines whether the mesh has transparency, in which case it has to be rendered differently than opaque meshes.
Next, if collision
is true:
- You need to create a
CollisionComponent
- The
Mesh*
has three functions for getting its dimensions:GetWidth()
,GetHeight()
, andGetDepth()
. CallSetSize
on theCollisionComponent
, passing in the parameters in that order. - Call
AddCollider
to add thisProp
to the game’s list of colliders
Finally, the destructor of Prop
needs to remove the collider, which you can do in one of two ways:
- Always call
RemoveCollider
, but just make it soRemoveCollider
will just not do anything if the actor isn’t in the vector - Only call
RemoveCollider
if you know theProp
has collision
Loading the Prop
Back and LevelLoader
, hook up creating the Prop
(with the additional properties) in LoadActor
(make sure you set actor
, too).
You should now see a sign on the left, a W with an arrow on the ground, and a piece of glass further in the distance:
If the W on the ground doesn’t show up at all and/or is rotated oddly, it likely means your Quaternion
code in Actor
wasn’t setup correctly.
Moving the Player
Now you’ll implement controls that are pretty standard PC first-person controls using the keyboard and mouse. For the moment, we’ll just leverage the existing MoveComponent
code before switching to the force-based movement on the next page.
Forward and Back
In PlayerMove::ProcessInput
, make it so that:
W
sets the forward speed to350.0f
S
sets forward speed to-350.0f
- If neither of
W
/S
is pressed, set forward speed to0.0f
- If both
W
andS
are pressed, set forward speed to0.0f
- (You should do this like how you did in Lab 2 where you only call
SetForwardSpeed
once to simplify the conditions)
You should now be able to move forward/back with W
/S
. Since collisions aren’t implemented yet, you’ll be able to go through walls:
Strafing
In PC first-person controls, A
/D
strafe (move) left/right as opposed to rotating the actor.
In Actor
, add a GetRight()
function that returns the right vector of the Actor
. This function is identical to GetForward()
, except pass the angle you want to pass into sin/cos is mRotation + Math::PiOver2
.
In MoveComponent
:
- Add a private
float mStrafeSpeed
, initialized to0.0f
, and make a getter and setter for it - In
Update
, add code that updates the position of the owner based on its right vector, the strafe speed, and delta time (this should be separate from the code that updates the position based on the forward speed)
Next, in PlayerMove::ProcessInput
, make it so that:
D
sets strafe speed to350.0f
A
sets strafe speed to-350.0f
- If neither
A
/D
is pressed, set strafe speed to0.0f
- If both
A
andD
are pressed, set strafe speed to0.0f
- (Again, do this with only one call to
SetStrafeSpeed
)
You should now be able to move right with D
and left with A
, and still move forward/back with W
/S
:
Mouse Look
Right now, ProcessInput
for both Actor
and Component
only take in the keyboard state, which means we have no way to pass along the state of the mouse.
Getting Mouse Data
We want to turn on SDL’s relative mouse mode so that every frame we get the relative motion of the mouse rather than an absolute coordinate.
To turn this on, in Game::Initialize
, after the call to mRenderer->Initialize
, add this code:
// On Mac, tell SDL that CTRL+Click should generate a Right Click event
SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1");
// Enable relative mouse mode
SDL_SetRelativeMouseMode(SDL_TRUE);
// Clear any saved values
SDL_GetRelativeMouseState(nullptr, nullptr);
The in Game::ProcessInput
, right before the loop that calls ProcessInput
on each actor, add the following code to get the state of the mouse:
int x = 0;
int y = 0;
Uint32 mouseButtons = SDL_GetRelativeMouseState(&x, &y);
Vector2 relativeMouse(x, y);
This will save the state of the mouse buttons in the mouseButtons
variable and the relative mouse movement in the relativeMouse
vector.
Adding Mouse Data to ProcessInput
In order for Actor
and Component
to have access to these mouse values, you need to change the declaration of Actor::ProcessInput
to add two more parameters, like this:
void ProcessInput(const Uint8* keyState, Uint32 mouseButtons, const Vector2& relativeMouse);
You will also need to change the implementation code in Actor.cpp
to match the new signature.
Next, you also need to change the declaration/implementation of Actor::OnProcessInput
, Component::ProcessInput
, and PlayerMove::ProcessInput
to also have these two extra parameters.
Then, in the implementation of Actor::ProcessInput
, you will need to update the calls to OnProcessInput
and each component’s ProcessInput
to pass along the parameters.
Finally, back in Game::ProcessInput
, you will need to update the call to ProcessInput
on each actor to pass in the mouseButtons
and relativeMouse
.
Confirm that your code compiles after making these changes. If it doesn’t compile, it probably just means you missed adding the parameters somewhere.
Mouse Yaw
Now in PlayerMove::ProcessInput
update the angular speed every frame using this:
float angularSpeed = relativeMouse.x / 500.0f * Math::Pi * 10.0f;
SetAngularSpeed(angularSpeed);
This divides by 500.0f
to get the estimate of the maximum x-mouse movement per frame and then multiplies by a radians/second angular speed.
Confirm that you can use the mouse to rotate the camera. Make sure your WASD movement still works properly even when you’re rotated. (For example, W always move in the direction the camera is facing):
Mouse Pitch
In first-person games, you can also use the mouse to pitch up the camera’s view, but this motion does not change the pitch of the player.
In CameraComponent
:
- Add floats
mPitchAngle
andmPitchSpeed
, initialize them to0.0f
- Add a getter/setter for pitch speed
- Add a getter for pitch angle
- Add a member variable to save the camera forward vector (as well as a getter)
Back in PlayerMove::ProcessInput
, use the same equation you used for angularSpeed
for the pitchSpeed
, except use relativeMouse.y
instead of x
. Then set the pitch speed on the Player’s CameraComponent
Next, at the start of CameraComponent::Update
:
- Update
mPitchAngle
based onmPitchSpeed
and delta time - You need to clamp pitch angle so that it can never go lower than
-Math::Pi / 2.1f
and never go higher thanMath::Pi / 2.1f
(this makes it so you can look up/down almost 90 degrees but not quite)
The calculation for the target position is now completely different. You must make two different rotation matrices: one for pitch and one for yaw (think about which of the X, Y, Z rotations each corresponds to). You should consult the Week 10, Lecture 1 slides for further info on this.
- For the rotation matrix for pitch, use the
mPitchAngle
- For the rotation matrix for yaw, use the owner’s rotation
- Multiply these rotation matrices (
pitch
*yaw
) to get a combined rotation matrix - Use
Vector3::Transform
to transformVector3::UnitX
(which is<1, 0, 0>
) by the combined matrix, and save this in your camera forward member variable - Now use the camera forward member variable (instead of the player forward) to calculate the target position you pass to
CreateLookAt
. For “SomePositiveValue” from the slides, make sure you use50.0f
(or your camera will ultimately be very slightly different from the expected which will cause the input replay to diverge when creating portals).
You should now be able to pitch the camera up/down with the mouse, and all other player controls should continue to work correctly. It should look something like this:
Once you’ve pushed this code, you’re ready to move on to part 2.