A project using this tutorial series is tracked on GitHub. The revision after this part is c36e06c.
Hej!
This part will handle what happens when we are grounded on geometry that can move. Currently the geometry will move away without us following with it. For horizontally moving geometry we will probably just fall off, and with vertically moving geometry it will clip through us. After this part we will solve both issues which will enable us to create features such as moving platforms and elevators. As a bonus we will also handle platform yaw rotation (Z-axis) but not pitch/roll (more explained why later).
We first need to keep track of what is directly underneath us so that we can use that to apply additional translation and rotation.
Let's add a new variable to our header:
UPROPERTY()
AActor* GroundActor;
Let's also add a function to manage it:
void UpdateGroundActor();
In our cpp file we can add a default in the constructor as usual:
USimpleMovementComponent::USimpleMovementComponent(): MoveSpeed(600.0f), JumpForce(420.0f), MaxCeilingStopAngle(5.0f),
MaxStepUpHeight(20.0f),
MinStepUpSteepness(10.0f),
MaxStepDownHeight(20.0f),
GroundActor(nullptr)
{
}
And the implementation of UpdateGroundActor
will utilize our CheckForGround
function to determine if we're currently standing on a valid actor and assign GroundActor
to that:
void USimpleMovementComponent::UpdateGroundActor()
{
FHitResult GroundHit;
const bool bDidHit = CheckForGround(GroundHit);
if (bDidHit)
{
GroundActor = GroundHit.GetActor();
}
else
{
GroundActor = nullptr;
}
}
This variable should be kept up-to-date each frame so it is appropriate to add a call to it next to UpdateComponentVelocity()
in TickComponent
which work in a similar way:
void USimpleMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
...
UpdateComponentVelocity();
UpdateGroundActor();
}
Let's add another function that will handle adding any movement caused by our ground actor:
void AdjustFromGroundMovement(const float DeltaTime);
The implementation is fairly straightforward; check the velocity of GroundActor
and move our component accordingly. This will move us with the ground:
void USimpleMovementComponent::AdjustFromGroundMovement(const float DeltaTime)
{
if (GroundActor)
{
FRotator NewRotation = UpdatedCollider->GetComponentRotation();
FVector GroundActorLocationDelta = GroundActor->GetVelocity() * DeltaTime;
MoveUpdatedComponent(GroundActorLocationDelta, NewRotation, false);
}
}
We then call this after our other movements and stepping functions in our DoMovement_Walking
state:
void USimpleMovementComponent::DoMovement_Walking(const float DeltaTime)
{
...
StepDown();
AdjustFromGroundMovement(DeltaTime);
const bool bIsGrounded = CheckForGround(Hit);
...
}
We can now stand on a moving object and we will move together with it!
We can also still step up and down on a moving stair-like object:
And we can use vertical "elevators":
There's no GetVelocity
equivalent of rotation (that I know of), so instead we need to track the rotation of our ground actor ourselves. We can do this by adding an additional variable to our header:
float GroundActorLastYaw;
We also set a default of 0.0f
as usual in our constructor.
We then edit our UpdateGroundActor
function to also track the yaw:
...
if (bDidHit)
{
GroundActor = GroundHit.GetActor();
GroundActorLastYaw = GroundActor->GetActorRotation().Yaw;
}
else
{
...
And now we can tackle the actual rotation logic by editing AdjustFromGroundMovement
.
First we calculate the yaw delta similarly to the location:
const float GroundActorCurrentYaw = GroundActor->GetActorRotation().Yaw;
const float GroundActorYawDelta = GroundActorCurrentYaw - GroundActorLastYaw;
Then we check if the delta is not zero at which point we need to handle changes.
When we're standing on something that rotates we also need to rotate with it so we continue to face the "same" direction. We can do this by simply adding the yaw delta to our own rotation:
if (FMath::Abs(GroundActorYawDelta) > 0.0f)
{
NewRotation.Yaw += GroundActorYawDelta;
}
This works as perfectly as long as we're standing on the same origin point of the rotating platform, but once we're standing just slightly off we also need to handle a change to our own location relative to the platform, so we continue to stand on the same spot on the platform itself.
If we imagine a platform (top view), where we (blue open circle) stand on its edge:
And that platform rotates 90°:
Then in addition to rotating our "look at" direction we also need to translate our location.
To do that we can calculate our initial location as an offset relative to the platform's origin (e.g. we are 100 units on the X-axis away from the center of the platform), rotate that offset by the change of rotation that we want to apply, and the add the newly rotated offset back onto our initial location.
Here's a picture to help visualize:
The initial offset (self location relative to ground origin) is denoted S
.
The rotated offset that is relative to ground origin is denoted Og
.
The rotated offset relative to self is denoted Os
, which is the final value that we want to apply to our current location.
Getting the initial offset S
is done by taking our absolute world location and subtracting the world location of our platform:
FVector SelfLocationRelativeToGroundActor = UpdatedCollider->GetComponentLocation() - GroundActor->
GetActorLocation();
We also want to ignore changes in height (Z-axis) since we're only handling yaw for now so we want to act as if we're working on a 2D-plane:
SelfLocationRelativeToGroundActor.Z = 0;
Next we compute an FRotator
for our rotation delta:
const FRotator DeltaRotator = FRotator::MakeFromEuler(FVector(0, 0, GroundActorYawDelta));
To get the rotated offset Og
we call RotateVector
on said FRotator
passing the location that we want to rotate (our current offset S
):
const FVector OffsetRelativeToGroundActor = DeltaRotator.
RotateVector(SelfLocationRelativeToGroundActor);
Then we calculate the offset relative to our current location Os
by subtracting S
from Og
:
const FVector RotationOffsetRelativeToSelf = OffsetRelativeToGroundActor -
SelfLocationRelativeToGroundActor;
We can then add this offset to our location delta for the entire adjustment function:
GroundActorLocationDelta += RotationOffsetRelativeToSelf;
The entire AdjustFromGroundMovement
now looks like this:
void USimpleMovementComponent::AdjustFromGroundMovement(const float DeltaTime)
{
if (GroundActor)
{
FRotator NewRotation = UpdatedCollider->GetComponentRotation();
FVector GroundActorLocationDelta = GroundActor->GetVelocity() * DeltaTime;
const float GroundActorCurrentYaw = GroundActor->GetActorRotation().Yaw;
const float GroundActorYawDelta = GroundActorCurrentYaw - GroundActorLastYaw;
if (FMath::Abs(GroundActorYawDelta) > 0.0f)
{
NewRotation.Yaw += GroundActorYawDelta;
FVector SelfLocationRelativeToGroundActor = UpdatedCollider->GetComponentLocation() - GroundActor->
GetActorLocation();
SelfLocationRelativeToGroundActor.Z = 0;
const FRotator DeltaRotator = FRotator::MakeFromEuler(FVector(0, 0, GroundActorYawDelta));
const FVector OffsetRelativeToGroundActor = DeltaRotator.
RotateVector(SelfLocationRelativeToGroundActor);
const FVector RotationOffsetRelativeToSelf = OffsetRelativeToGroundActor -
SelfLocationRelativeToGroundActor;
GroundActorLocationDelta += RotationOffsetRelativeToSelf;
}
MoveUpdatedComponent(GroundActorLocationDelta, NewRotation, false);
}
}
And with that we rotate correctly on platforms:
We can now utilize basic moving geometry in our level design.
If the platform we're standing on rotates on an axis besides the Z-axis (yaw) we will currently clip through. The reason is that our character shouldn't directly rotate pitch or roll since we always want to stay upright. Instead, this will be handled by much more generalized logic that handles "encroaching" geometry which we will look at next.