A project using this tutorial series is tracked on GitHub. The revision after this part is 88c48a4.
Hej!
In this part of the the tutorial series we will look at implementing gravity, and at the end we will have a our character move down slopes and fall down from edges!
Gravity is actually quite simple. It is simply a constant force being applied "downwards". For Unreal this is negative Z, but it can be anything you want (for example making a game with planets and their own gravity!).
Disclaimer: I'm not that good at physics so things might not be physically accurate...but they don't have to be! The nice thing about games is that it only have to seem realistic (or not). With that said, we'll try to make use of forces, velocity, acceleration etc. so that it hopefully makes sense.
Also, for this tutorial we won't bother with drag (air resistance) or anything else besides the basics.
First, since we're introducing an entirely new force unrelated to our input, we should rename our horizontal movement input for clarity and move the velocity update to the move section:
/** SimpleMovementComponent::TickComponent */
...
// Calculate input force
const FVector& Input = ConsumeInputVector().GetClampedToMaxSize2D(1.0f);
const FVector HorizontalMovement = Input * MoveSpeed;
// Move
const FRotator& Rotation = UpdatedComponent->GetComponentRotation();
Velocity += HorizontalMovement;
...
The effect is the same but now we have a bit cleaner structure to work with.
From the movement component, gravity can be fetched by calling GetGravityZ()
. This is a nice helper that grabs the current gravity. This can be overridden per world but by default it uses the global gravity value set in Project Settings -> Engine -> Physics, which by default is -980.0
. Notice that it is already nicely expressed as a negative value, so we can multiply it with FVector::UpVector
directly to get a downwards force.
Note: -980.0 is the same as Earths average gravitational acceleration expressed in Unreal units, equal to 9.8m/s^2.
Let's add a section just before the move step where we calculate our vertical movement:
// Apply gravity
const float GravityForce = GetGravityZ();
const FVector VerticalMovement = FVector::UpVector * GravityForce;
And add it to our velocity in our move step:
Velocity += HorizontalMovement + VerticalMovement;
We also need to reset our Z velocity whenever we hit ground, otherwise it will constantly accumulate and cause lots of trouble, so set it to 0 within our hit check:
if (Hit.IsValidBlockingHit())
{
Velocity.Z = 0;
...
}
Now let's try it:
Ok, this kind of works...but we're falling way too fast. The reason? Well we've actually been cheating/simplifying how (horizontal) movement is implemented.
Both horizontal and vertical movement are represented by variables that correspond to cm/s we want to move. The difference is that gravity actually accumulates! In real life things don't immediately start falling at max speed. They accelerate.
Realizing this and analyzing the code we can actually see that we're adding the speed of 9.8m/s every tick. Instead we want to accumulate so that we roughly equal a speed of 9.8m/s after 1 second (that is, 9.8m/s/s). The reason it's different with horizontal movement is that we're resetting XY velocity each frame (in thie
Luckily once we figure this out the fix is very simple; we just need to multiple our gravity force by DeltaTime
one extra time:
const float GravityForce = GetGravityZ() * DeltaTime;
const FVector VerticalMovement = FVector::UpVector * GravityForce * DeltaTime;
We should now fall at a much more acceptable rate:
If you find this too slow or too fast you can always add an extra multiplier to GravityForce
and tweak it to your liking.
An optional but recommended step is to also factor in terminal velocity. This means that we won't accumulate speed indefinitely and instead clamp our falling speed. To do that we simply grab our current physics volume and use its TerminalVelocity
. In case we're not in any specific physics volume it falls back to the global value set in Project Settings -> Engine -> Physics which by default is 4000.0
. We can then clamp our velocity Z (after applying our movement):
const float TerminalVelocity = GetPhysicsVolume()->TerminalVelocity * DeltaTime;
...
Velocity += HorizontalMovement + VerticalMovement;
Velocity.Z = FMath::Max(Velocity.Z, -TerminalVelocity);
We also need to include the physics volume header at the top:
#include "SimpleMovementComponent.h"
#include "GameFramework/PhysicsVolume.h"
(I've added an on screen debug message to help visualize)
And, as an added bonus, we can now also walk down slopes (albeit a bit choppy - we will fix that later on!):
Our final tick function now looks like this:
void USimpleMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (ShouldSkipUpdate(DeltaTime))
{
return;
}
if (!PawnOwner || !UpdatedComponent)
{
return;
}
// Reset velocity
Velocity.X = 0;
Velocity.Y = 0;
// Calculate force
const FVector& Input = ConsumeInputVector().GetClampedToMaxSize2D(1.0f);
const FVector HorizontalMovement = Input * MoveSpeed;
// Apply gravity
const float GravityForce = GetGravityZ();
const FVector VerticalMovement = FVector::UpVector * GravityForce * DeltaTime;
// Move
const FRotator& Rotation = UpdatedComponent->GetComponentRotation();
const float TerminalVelocity = GetPhysicsVolume()->TerminalVelocity;
Velocity += HorizontalMovement + VerticalMovement;
Velocity.Z = FMath::Max(Velocity.Z, -TerminalVelocity);
const FVector MovementDelta = Velocity * DeltaTime;
FHitResult Hit;
SafeMoveUpdatedComponent(MovementDelta, Rotation, true, Hit);
if (Hit.IsValidBlockingHit())
{
Velocity.Z = 0;
HandleImpact(Hit, DeltaTime, MovementDelta);
SlideAlongSurface(MovementDelta, 1.0f - Hit.Time, Hit.Normal, Hit, true);
}
UpdateComponentVelocity();
}
And that's it! Next up we will look at a more active application of gravity: jumping! 🪂