Skip to main content Link Search Menu Expand Document (external link)

Theme: auto

3D Level and FPS Controls

First, you need to make some changes so the code compiles.

In PlayerMove:

  • Change it so PlayerMove inherits from MoveComponent again
  • Remove the #include "VehicleMove.h" and instead #include "MoveComponent.h"
  • Delete any member variables and any functions beyond the constructor, Update, and ProcessInput
  • Remove any #includes for files that don’t exist anymore
  • Delete all the code inside of the constructor, Update, and ProcessInput
  • In Update, add a single call to MoveComponent::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 of 1.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 called HasGun that returns a bool. For now, just return false (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 to mAudio->CacheAllSounds(). This includes removing the line that creates the Player as we will do that in a different spot.
  • If you do not have a Player* member variable in Game, add one and a public function called GetPlayer() which returns that pointer as well as a SetPlayer function to set the pointer
  • Add a vector of Actor* called mColliders. 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 an Actor* to mColliders
  • A RemoveCollider function that removes a Actor* from mColliders

  • 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 a LookAt matrix where the eye is the owner’s position, the target is a point 50 units in front of the owner, and the up is Vector3::UnitZ. Then call SetViewMatrix 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 and RemoveCollider 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 sets mQuat, and a const Quaternion& GetQuat() const function that returns it
  • In CalcWorldTransform, after multiplying by the rotation matrix, and before multiplying by the translation matrix, multiply by Matrix4::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:Level loaded

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 the Mesh* that’s specified by the mesh 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(), and GetDepth(). Call SetSize on the CollisionComponent, passing in the parameters in that order.
  • Call AddCollider to add this Prop 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 so RemoveCollider will just not do anything if the actor isn’t in the vector
  • Only call RemoveCollider if you know the Prop 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: Props loading

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 to 350.0f
  • S sets forward speed to -350.0f
  • If neither of W/S is pressed, set forward speed to 0.0f
  • If both W and S are pressed, set forward speed to 0.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 to 0.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 to 350.0f
  • A sets strafe speed to -350.0f
  • If neither A/D is pressed, set strafe speed to 0.0f
  • If both A and D are pressed, set strafe speed to 0.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 and mPitchSpeed, initialize them to 0.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 on mPitchSpeed 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 than Math::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.

  1. For the rotation matrix for pitch, use the mPitchAngle
  2. For the rotation matrix for yaw, use the owner’s rotation
  3. Multiply these rotation matrices (pitch * yaw) to get a combined rotation matrix
  4. Use Vector3::Transform to transform Vector3::UnitX (which is <1, 0, 0>) by the combined matrix, and save this in your camera forward member variable
  5. 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 use 50.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.