scroll

Key Blog

  • Key Homepage>
  • Blog>
  • [ue5] 지면의 경사에 맞춰 캐릭터를 기울이는 방법과 설명
  • [UE5] How to Tilt a Character According to the Ground Slope and Explanation

    @kiikey4(Key Zhao)

    [UE5] How to Tilt a Character According to the Ground Slope and Explanation

    Last updated on Oct 20, 2024

    Posted on Oct 17, 2024

    0

    Overview

    This article introduces how to implement a process in C++ that tilts a character according to the slope of the ground.

    If you want to implement it in Blueprint, please refer to this article:
    UE4 Aligning Characters with Ground Slope

    Environment

    • Rider 2024.2.6
    • Unreal Engine 5.4

    References

    Main Content

    When a character walks on a slope, it looks like this if the tilt is not adjusted.
    MouseRunOnBoardWithoutAlignFloor_cn0zje
    The character's head is buried in the slope, making it look unnatural.

    Now, let's implement this in C++.

    Steps

    1. Perform a raycast downward from the character.
    2. If the raycast hits the ground (slope), obtain the normal of the ground (slope).
    3. Use the obtained normal to calculate the tilt of the ground (slope).
    4. Rotate the character according to the tilt of the ground (slope).

    Implementing the AlignFloor() Function in the Player Class

    The AlignFloor() function will be called every 0.1 seconds using a timer (it can also be called in Tick, but for optimization, it's set to 0.1 seconds. Visually, I don’t think a frequency of 0.1 seconds will be noticeable).

    PlayerCharacter.h
    1private: 2 void AlignFloor() const; 3 4 FTimerHandle AlignFloorTimerHandle;
    PlayerCharacter.cpp
    1 2void APlayerCharacter::BeginPlay() 3{ 4 Super::BeginPlay(); 5 6 GetWorldTimerManager().SetTimer(AlignFloorTimerHandle, this, &APlayerCharacter::AlignFloor, 0.1f, true); 7} 8 9void APlayerCharacter::AlignFloor() const 10{ 11 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 12 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 13 FHitResult HitResult; 14 FCollisionQueryParams CollisionQueryParams; 15 CollisionQueryParams.AddIgnoredActor(this); 16 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 17 CollisionQueryParams); 18 if (IsHit) 19 { 20 FVector FloorNormal = HitResult.ImpactNormal; 21 FVector RightVector = GetActorRightVector(); 22 FVector UpVector = GetActorUpVector(); 23 float SlopePitch; 24 float SlopeRoll; 25 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 26 SlopePitch = -SlopePitch; 27 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 28 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 29 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 30 GetMesh()->SetWorldRotation(FloorRotation); 31 } 32}

    This completes the implementation of tilting the character according to the slope of the ground!

    Explanation

    Performing a Raycast Downward from the Character

    PlayerCharacter.cpp
    1void APlayerCharacter::AlignFloor() const 2{ 3 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 4 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 5 FHitResult HitResult; 6 FCollisionQueryParams CollisionQueryParams; 7 CollisionQueryParams.AddIgnoredActor(this); 8 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 9 CollisionQueryParams); 10 if (IsHit) 11 { 12 FVector FloorNormal = HitResult.ImpactNormal; 13 FVector RightVector = GetActorRightVector(); 14 FVector UpVector = GetActorUpVector(); 15 float SlopePitch; 16 float SlopeRoll; 17 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 18 SlopePitch = -SlopePitch; 19 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 20 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 21 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 22 GetMesh()->SetWorldRotation(FloorRotation); 23 } 24}

    In this part, a raycast check is performed downward from slightly above the character.
    const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector;

    The + 1.f is to ensure there is a distance from the ground. Without this, the character would be positioned at the same height as the ground, and the raycast might not hit the ground correctly (this actually occurred in my environment).

    If the raycast hits the ground, obtain the ground normal (FloorNormal).

    Use the obtained normal to calculate the tilt of the ground.

    PlayerCharacter.cpp
    1void APlayerCharacter::AlignFloor() const 2{ 3 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 4 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 5 FHitResult HitResult; 6 FCollisionQueryParams CollisionQueryParams; 7 CollisionQueryParams.AddIgnoredActor(this); 8 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 9 CollisionQueryParams); 10 if (IsHit) 11 { 12 FVector FloorNormal = HitResult.ImpactNormal; 13 FVector RightVector = GetActorRightVector(); 14 FVector UpVector = GetActorUpVector(); 15 float SlopePitch; 16 float SlopeRoll; 17 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 18 SlopePitch = -SlopePitch; 19 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 20 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 21 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 22 GetMesh()->SetWorldRotation(FloorRotation); 23 } 24}

    This section includes some mathematical content, so please read on only if you are interested. If you're not, feel free to skip ahead.

    Consider a scenario where the character is a mouse standing on a slope. The raycast hits the slope, and the normal is obtained.

    Obtaining the Ground (Slope) Normal

    FVector FloorNormal = HitResult.ImpactNormal;

    What is a normal?

    A line that is perpendicular to the tangent plane at a given point on a curved surface.

    Calculating the Slope Angle

    Obtain the character's right vector and up vector.

    1 //... 2 FVector RightVector = GetActorRightVector(); 3 FVector UpVector = GetActorUpVector(); 4 //...

    Use the function from UKismetMathLibrary to obtain the slope angle (SlopePitch).
    UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll);

    Upon inspecting the internal workings of this function, the following calculations are performed.

    KismetMathLibary.cpp
    1void UKismetMathLibrary::GetSlopeDegreeAngles(const FVector& MyRightYAxis, const FVector& FloorNormal, const FVector& UpVector, float& OutSlopePitchDegreeAngle, float& OutSlopeRollDegreeAngle) 2{ 3 const FVector FloorZAxis = FloorNormal; 4 const FVector FloorXAxis = MyRightYAxis ^ FloorZAxis; 5 const FVector FloorYAxis = FloorZAxis ^ FloorXAxis; 6 7 OutSlopePitchDegreeAngle = 90.f - FMath::RadiansToDegrees(FMath::Acos(FloorXAxis | UpVector)); 8 OutSlopeRollDegreeAngle = 90.f - FMath::RadiansToDegrees(FMath::Acos(FloorYAxis | UpVector)); 9}

    Using a diagram for explanation might make it easier to understand.

    For example, the right side view of a mouse standing on a slope is shown below.

    CalculateSlope_f2t9dk

    △ The triangle represents the mouse.

    FloorZ is the normal of the slope (normal vector). FloorX is calculated using the cross product of the mouse's right vector and FloorZ (the normal vector), resulting in the upward direction vector of the slope.

    The caret (^) in FVector denotes the cross product operator.

    The result of the cross product of two vectors is a vector that is perpendicular to both of them.

    For more on cross product: Wikipedia - Cross Product

    Next, calculating the cross product of FloorZ and FloorX gives us the "mouse's right direction vector."

    const FVector FloorYAxis = FloorZAxis ^ FloorXAxis;

    By calculating the dot product between the slope's upward direction vector (FloorX) and the mouse's upward vector (Up), and taking the Acos of the result, we can determine angle a. Subtracting this angle from 90 degrees gives us the slope angle (SlopePitch).

    OutSlopePitchDegreeAngle = 90.f - FMath::RadiansToDegrees(FMath::Acos(FloorXAxis | UpVector));

    The | in FVector denotes the dot product operator.

    For more on dot product: Wikipedia - Dot Product
    The dot product between vectors u and v, with angle θ between them, can be expressed as:
    u⋅v=∣u∣∣v∣cosθ

    Rotating the Character According to the Slope's Angle
    PlayerCharacter.cpp
    1void APlayerCharacter::AlignFloor() const 2{ 3 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 4 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 5 FHitResult HitResult; 6 FCollisionQueryParams CollisionQueryParams; 7 CollisionQueryParams.AddIgnoredActor(this); 8 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 9 CollisionQueryParams); 10 if (IsHit) 11 { 12 FVector FloorNormal = HitResult.ImpactNormal; 13 FVector RightVector = GetActorRightVector(); 14 FVector UpVector = GetActorUpVector(); 15 float SlopePitch; 16 float SlopeRoll; 17 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 18 SlopePitch = -SlopePitch; 19 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 20 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 21 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 22 GetMesh()->SetWorldRotation(FloorRotation); 23 } 24}

    By rotating the mouse's Mesh roll in the opposite direction, the character naturally adjusts to the slope's tilt.

    Roll, Pitch, Yaw

    Result

    You can check how the character properly responds to the slope in the demo below.

    Finally

    In this article, we explained how to rotate a character according to the ground's slope. If you notice any mistakes, please let us know in the comments.

    0

    Comments

    No comments

    Let's comment your feeling