Logo

Ljung

.dev

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).

Ground Actor

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:

cpp
1UPROPERTY()
2AActor* GroundActor;

Let's also add a function to manage it:

cpp
1void UpdateGroundActor();

In our cpp file we can add a default in the constructor as usual:

cpp
1USimpleMovementComponent::USimpleMovementComponent(): MoveSpeed(600.0f), JumpForce(420.0f), MaxCeilingStopAngle(5.0f),
2                                                      MaxStepUpHeight(20.0f),
3                                                      MinStepUpSteepness(10.0f),
4                                                      MaxStepDownHeight(20.0f),
5                                                      GroundActor(nullptr)
6{
7}

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:

cpp
1void USimpleMovementComponent::UpdateGroundActor()
2{
3  FHitResult GroundHit;
4  const bool bDidHit = CheckForGround(GroundHit);
5
6  if (bDidHit)
7  {
8    GroundActor = GroundHit.GetActor();
9  }
10  else
11  {
12    GroundActor = nullptr;
13  }
14}

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:

cpp
1void USimpleMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType,
2                                             FActorComponentTickFunction* ThisTickFunction)
3{
4
5  ...
6  
7  UpdateComponentVelocity();
8  UpdateGroundActor();
9}

Translation (movement)

Let's add another function that will handle adding any movement caused by our ground actor:

cpp
1void 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:

cpp
1void USimpleMovementComponent::AdjustFromGroundMovement(const float DeltaTime)
2{
3  if (GroundActor)
4  {
5    FRotator NewRotation = UpdatedCollider->GetComponentRotation();
6    FVector GroundActorLocationDelta = GroundActor->GetVelocity() * DeltaTime;
7
8    MoveUpdatedComponent(GroundActorLocationDelta, NewRotation, false);
9  }
10}

We then call this after our other movements and stepping functions in our DoMovement_Walking state:

cpp
1void USimpleMovementComponent::DoMovement_Walking(const float DeltaTime)
2{
3
4  ...
5
6  StepDown();
7
8  AdjustFromGroundMovement(DeltaTime);
9
10  const bool bIsGrounded = CheckForGround(Hit);
11
12  ...
13  
14}

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":

Rotation (yaw)

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:

cpp
1float 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:

cpp
1...
2
3if (bDidHit)
4{
5  GroundActor = GroundHit.GetActor();
6  GroundActorLastYaw = GroundActor->GetActorRotation().Yaw;
7}
8else
9{
10
11  ...

And now we can tackle the actual rotation logic by editing AdjustFromGroundMovement.

First we calculate the yaw delta similarly to the location:

cpp
1const float GroundActorCurrentYaw = GroundActor->GetActorRotation().Yaw;
2const 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:

cpp
1if (FMath::Abs(GroundActorYawDelta) > 0.0f)
2{
3  NewRotation.Yaw += GroundActorYawDelta;
4}

Moving with the rotation

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.

Implementation

Getting the initial offset S is done by taking our absolute world location and subtracting the world location of our platform:

cpp
1FVector SelfLocationRelativeToGroundActor = UpdatedCollider->GetComponentLocation() - GroundActor->
2        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:

cpp
1SelfLocationRelativeToGroundActor.Z = 0;

Next we compute an FRotator for our rotation delta:

cpp
1const 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):

cpp
1const FVector OffsetRelativeToGroundActor = DeltaRotator.
2        RotateVector(SelfLocationRelativeToGroundActor);

Then we calculate the offset relative to our current location Os by subtracting S from Og:

cpp
1const FVector RotationOffsetRelativeToSelf = OffsetRelativeToGroundActor -
2        SelfLocationRelativeToGroundActor;

We can then add this offset to our location delta for the entire adjustment function:

cpp
1GroundActorLocationDelta += RotationOffsetRelativeToSelf;

The entire AdjustFromGroundMovement now looks like this:

cpp
1void USimpleMovementComponent::AdjustFromGroundMovement(const float DeltaTime)
2{
3  if (GroundActor)
4  {
5    FRotator NewRotation = UpdatedCollider->GetComponentRotation();
6    FVector GroundActorLocationDelta = GroundActor->GetVelocity() * DeltaTime;
7    const float GroundActorCurrentYaw = GroundActor->GetActorRotation().Yaw;
8    const float GroundActorYawDelta = GroundActorCurrentYaw - GroundActorLastYaw;
9
10    if (FMath::Abs(GroundActorYawDelta) > 0.0f)
11    {
12      NewRotation.Yaw += GroundActorYawDelta;
13      FVector SelfLocationRelativeToGroundActor = UpdatedCollider->GetComponentLocation() - GroundActor->
14        GetActorLocation();
15      SelfLocationRelativeToGroundActor.Z = 0;
16
17      const FRotator DeltaRotator = FRotator::MakeFromEuler(FVector(0, 0, GroundActorYawDelta));
18      const FVector OffsetRelativeToGroundActor = DeltaRotator.
19        RotateVector(SelfLocationRelativeToGroundActor);
20      const FVector RotationOffsetRelativeToSelf = OffsetRelativeToGroundActor -
21        SelfLocationRelativeToGroundActor;
22      GroundActorLocationDelta += RotationOffsetRelativeToSelf;
23    }
24
25    MoveUpdatedComponent(GroundActorLocationDelta, NewRotation, false);
26  }
27}

And with that we rotate correctly on platforms:

Results

We can now utilize basic moving geometry in our level design.

Pitch/roll?

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.