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

Theme: auto

Platforming in 3D

Because the player’s movement behavior will become complex as we add features, rather than just having a lot of conditions in PlayerMove::Update, we’ll create a state machine for the different states the player can be in.

In PlayerMove:

  • Declare an enum MoveState with three values: OnGround, Jump, Falling
  • Create a MoveState member variable called mCurrentState
  • Add a private ChangeState function that takes in a MoveState and sets mCurrentState to the passed in value (we’ll do more here later)
  • In the constructor, call ChangeState(OnGround)

Next, add the following three private functions to PlayerMove. They should each take in a float for delta time:

  • UpdateOnGround
  • UpdateJump
  • UpdateFalling

Change PlayerMove::Update so it just has a switch on mCurrentState and calls the corresponding UpdateWhatever function for that state. It should not call MoveComponent::Update anymore.

Make sure your code compiles. Your movement won’t work anymore, and you will only be able to pitch the camera.

Adding Forces

Now we’re going to setup the player motion to use forces as we outlined in lecture. To do this, you need to add the following member variables to PlayerMove:

  • Vector3s for mVelocity, mAcceleration, and mPendingForces (you don’t need to initialize these, since the default constructor just sets them to all zeroes)
  • float mMass (initialize to 1.0f)

Next, add declarations for these (private) functions to PlayerMove:

  • void PhysicsUpdate(float deltaTime)
  • void AddForce(const Vector3& force)

The implementation for AddForce is just mPendingForces += force. (The pending forces is the sum of all the forces we want to apply on a frame)

For now, PhysicsUpdate should do the following:

  1. Newton says acceleration = Force / mass. So, set mAcceleration to mPendingForces * (1.0f / mMass)
  2. Update mVelocity based on mAcceleration and delta time
  3. Update the owner’s position based on mVelocity and delta time
  4. Update the owner’s rotation based on angular speed and delta time (you can just copy the angular speed update code from MoveComponent, as we won’t be using angular forces)
  5. Set mPendingForces to Vector3::Zero (this means that every frame, we have to call AddForce for each force we want to affect the next PhysicsUpdate)

Now in ProcessInput:

  • For W/S movement, rather than setting the forward speed, you need to instead use AddForce. Forward W be AddForce in the direction of owner’s forward scaled by 700.0f, and S should AddForce the same except scaled by -700.0f. (This means if both W and S are pushed, both forces will get added and cancel each other out).
  • For strafing, rather than setting the strafe speed, D should AddForce in the direction of owner’s right scaled by 700.0f, and A should AddForce the same except scaled by -700.0f
  • Keep the mouse code as is

Now in UpdateOnGround, UpdateJump, and UpdateFalling, call PhysicsUpdate.

Your code will compile and your motion will sort of work, but you’ll notice it’s not really playable at all.

The reason it’s unplayable is because:

  • If you tap a direction (like forward) and let go, you just keep moving in that direction
  • If you hold down a direction, your velocity will quickly reach ridiculous speeds
  • Switching directions is very unresponsive because of both issues

Fixing XY-Velocity

We need to limit the maximum x/y velocity of the player so that the player can’t just quickly reach a ridiculous speed.

To do this, we need to add some approximation of braking/friction of your movement. If the player presses forward and lets go, you want to reduce the “forward” velocity so the player stops very soon after letting go (this is similar to how we did drag in Mario Kart, though not quite the same). We only need to worry about limiting the velocity in the x/y directions, not z.

Create a new private member function in PlayerMove called FixXYVelocity, it should:

  • Create a temporary Vector2 for xyVelocity with the x/y components of mVelocity
  • If the length of xyVelocity is greater than a max speed of 400.0f, you want to change the length of xyVelocity to be exactly max speed. (One way to do this is normalize the vector and multiply by max speed). This fixes the problem with moving too fast
  • If the current state is OnGround, you also need to apply braking:
    • If Math::NearlyZero(mAcceleration.x) is true, multiply xyVelocity.x by a braking factor of 0.9f
    • Do the same calculation (separately) for the y-components of mAcceleration and xyVelocity
    • This will fix the problem with not braking when letting go
  • Finally, update mVelocity.x/y to be xyVelocity.x/y (since you’ve fixed them!)

You want to call FixXYVelocity in PhysicsUpdate, immediately after updating mVelocity but before updating the owner’s position.

Now movement should feel a lot better! However, switching between directions may still feel unresponsive. For example, if you hold down the A key and then immediately switch to D, you will take a while to start strafing in the opposite direction.

To fix this final issue, you need to add additional cases to the conditions when you apply the braking in OnGround:

  • If mAcceleration.x and xyVelocity.x have opposite signs OR the x-component of mAcceleration is near zero, apply the braking factor to xyVelocity.x
  • Similarly, if mAcceleration.y and xyVelocity.y have opposite signs OR the y-component of mAcceleration is near zero, you should apply the braking factor to xyVelocity.y

Switching rapidly between two opposite directions should now feel pretty good. It should now play like this:

Falling

Now we’ll add support for falling in PlayerMove:

  • Add a member variable Vector3 for mGravity, initialized to (0.0f, 0.0f, -980.0f)
  • At the start of UpdateFalling (before the call to PhysicsUpdate), use AddForce to add the force of gravity
  • Change the PlayerMove constructor so it calls ChangeState(Falling) at the start, rather than setting to OnGround

The player will now immediately fall through the world at game start. As you’re falling, should still be able to move with WASD, as well.

GetMinOverlap in 3D

Now you must implement a version of GetMinOverlap that works in 3D. You’ll want to consult the relevant slides for this part.

First, add Front and Back to the CollSide enum in CollisionComponent.h.

Keep in mind that which axis corresponds to which side of the block (“other”) is completely different in our 3D coordinate system. Use this diagram as a reference:

GetMinOverlap sides in 3D

Using your 2D GetMinOverlap function from labs 3/4/5/6 as a starting point, implement the 3D version:

  • Remember the offset is now a Vector3 not Vector2
  • Fix the sides for your old [top/bottom/left/right]Dist local variables, since they are different now in 3D
  • Add [front/back]Dist local variables. Think about which axis is front/back in this case
  • When picking the minimum collision, you need to check all six options now
  • If you select a Z side as the min overlap side, you should set offset.z
  • Make sure you return the correct CollSide based on the selected min overlap, according to the diagram above

We have unit tests for your 3D GetMinOverlap function which will automatically run if you push your CollisionComponent.cpp file to GitHub. Alternatively, if you want to run/debug the unit tests locally, you can clone this repo and copy your CollisionComponent.cpp file into it. Then run the tests target to see the results. Either way, you should confirm that your unit tests successfully pass, as it is required for the spec.

FixCollision

Next, in PlayerMove.h, add a #include for CollisionComponent.h. Then, add the following private member function declaration:

CollSide FixCollision(CollisionComponent* self, CollisionComponent* collider);

In FixCollision, call GetMinOverlap() on self (passing in collider), and save it in a local variable. If the side isn’t CollSide::None, fix the position of the player based on offset.

Finally, FixCollision should return the CollSide it received from GetMinOverlap.

Collision in UpdateFalling

Now in PlayerMove::UpdateFalling, after the call to PhysicsUpdate, loop over all the colliders in the game and call FixCollision on each (you’ll have to get the CollisionComponents).

In the loop, you should detect if ANY of the FixCollision calls return Top AND the z-component of mVelocity is <= 0.0f, this means you landed. If you landed, then after the loop, you should should:

  • Set the z-component of mVelocity to 0.0f
  • Call ChangeState(OnGround)

Now when the game starts, you should no longer fall through the ground. However, you’ll still be able to walk through blocks while OnGround.

OnGround

In PlayerMove::UpdateOnGround, after the call to PhysicsUpdate, loop over all the colliders and call FixCollision on each. This will prevent the player from walking through blocks.

To add support for transitioning from falling to on ground, you also need to check to see if NONE of the FixCollision calls returned Top, which means you aren’t standing on the top of any block. In this case, call ChangeState(Falling).

You should not be able to walk through blocks anymore. If you walk forward into the pit, you should fall and land on the ground. It will look like this:

Jumping

In PlayerMove::UpdateJump, before the call to PhysicsUpdate, add the gravity force.

Then, after the call to PhysicsUpdate:

  • Loop over all colliders and call FixCollision on each. If any blocks return CollSide::Bottom, it means you hit your head, so set mVelocity.z to 0.0f
  • After fixing collision, check if the mVelocity.z <= 0.0f. If it is, that means you reached the apex of your jump, so you should change the state to Falling

Next, declare a Vector3 for mJumpForce set to (0.0f, 0.0f, 35000.0f).

Then in PlayerMove::ProcessInput, if you detect a leading edge of the space bar AND mCurrentState is OnGround:

  • Add the jump force
  • Change the state to Jump

You will now be able to jump around, like this:

Once you’ve pushed this code, you’re ready to move on to part 3.