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 calledmCurrentState
- Add a private
ChangeState
function that takes in aMoveState
and setsmCurrentState
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
:
Vector3
s formVelocity
,mAcceleration
, andmPendingForces
(you don’t need to initialize these, since the default constructor just sets them to all zeroes)float mMass
(initialize to1.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:
- Newton says acceleration = Force / mass. So, set
mAcceleration
tomPendingForces * (1.0f / mMass)
- Update
mVelocity
based onmAcceleration
and delta time - Update the owner’s position based on
mVelocity
and delta time - 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) - Set
mPendingForces
toVector3::Zero
(this means that every frame, we have to callAddForce
for each force we want to affect the nextPhysicsUpdate
)
Now in ProcessInput
:
- For W/S movement, rather than setting the forward speed, you need to instead use
AddForce
. Forward W beAddForce
in the direction of owner’s forward scaled by700.0f
, and S shouldAddForce
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 by700.0f
, and A shouldAddForce
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
forxyVelocity
with the x/y components ofmVelocity
- If the length of
xyVelocity
is greater than a max speed of400.0f
, you want to change the length ofxyVelocity
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, multiplyxyVelocity.x
by a braking factor of0.9f
- Do the same calculation (separately) for the y-components of
mAcceleration
andxyVelocity
- This will fix the problem with not braking when letting go
- If
- Finally, update
mVelocity.x
/y
to bexyVelocity.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
andxyVelocity.x
have opposite signs OR the x-component ofmAcceleration
is near zero, apply the braking factor toxyVelocity.x
- Similarly, if
mAcceleration.y
andxyVelocity.y
have opposite signs OR the y-component ofmAcceleration
is near zero, you should apply the braking factor toxyVelocity.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
formGravity
, initialized to(0.0f, 0.0f, -980.0f)
- At the start of
UpdateFalling
(before the call toPhysicsUpdate
), useAddForce
to add the force of gravity - Change the
PlayerMove
constructor so it callsChangeState(Falling)
at the start, rather than setting toOnGround
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:
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
notVector2
- 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 CollisionComponent
s).
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
to0.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 returnCollSide::Bottom
, it means you hit your head, so setmVelocity.z
to0.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 toFalling
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.