Custom Character Movement
Used Unreal Engine Version: 4.22
There are several ways how to implement custom movement in Unreal Engine. I show you one of them: Brachiating. It is a special movement and uses no physics. It will give you the knowledge to go and try to make your own movements.
As a base we use AThirdPersonCharacter
which already supports movement types like walking, crouching, running, jumping, swimming and flying, but not - you guessed it - brachiating.
Implementation
One way to implement it, is extending UCharacterMovementComponent
. I already showed you how to do this. But since this is rather for physics based movement, I decided to go the other way, by using the already existing delegate MovementModeChangedDelegate
. This delegate broacasts events each time the movement mode changes.
So, what do we need? How do we trigger the new movement mode and how do we leave it? Here is a short overview how our brachiation should work:
- when jumping on a ledge, our character should grab it and hang in there
- when walking slowly over a ledge, our character should hold on it instead of falling down
- when trying to move forward, our character should jump up over the ledge
- when trying to move backwards, our character should let go and fall down
- when trying to move over the end of a ledge, our character should fall down
Lots of rules, but it gives us a good idea about what to implement.
Keep in mind, that this is only one way how to do it and just a starting point for you. You’ll have to tweak values and positions to find the perfect fit for your character.
Triggers
We have to give our character the ability to know, when to start brachiating. For this I used various trigger volumes.
In the image you can see them. I added some opaque boxes so I can see what happens, when being in game. As soon as everything works fine, we’ll remove them.
The design of our brachiation trigger:
- a box trigger to activate brachiation
- a brachiation direction
- a brachiation normal (the direction our character faces when brachiating)
- threshold values to define a maximum velocity, above which brachiation will not be triggered (allows us to walk over an edge without dangling from it)
We’ll set this up in C++. Create a new class and name it BrachiationTrigger
:
/* BrachiationTrigger.h */
#pragma once
#include "CoreMinimal.h"
#include "Engine/TriggerBox.h"
#include "BrachiationTrigger.generated.h"
// Forward Declarations
class UBoxComponent;
UCLASS()
class BEATEMDOWN_API ABrachiationTrigger : public AActor
{
GENERATED_BODY()
public:
// .............................................................. Properties
/** Maximum falling velocity, at which player can get hold of the ledge. */
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Trigger")
float FallThroughVelocityThreshold = 100.0f;
/** Maximum velocity, at which player can get hold of the ledge, when
jumping through it from beneath. */
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Trigger")
float JumpThroughVelocityThreshold = 100.0f;
/** Maximum velocity, at which player can get hold of the ledge, when
jumping or running at it horizontally. */
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Trigger")
float RunThroughVelocityThreshold = 100.0f;
/** The axis along which the player can brachiate. */
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Geometry")
FVector BrachiationAxis = FVector(1,0,0);
/** The direction in which a brachiating player is looking. */
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Geometry")
FVector BrachiationFaceDirection = FVector(0, -1, 0);
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
USceneComponent* Root = nullptr;
/** Shape of the ledge, where character may brachiate. */
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite/*, Category = "Collision"*/)
UBoxComponent* BrachiationLedge = nullptr;
/** Rotator to facing direction in world coordinates. */
UFUNCTION(BlueprintCallable)
FRotator GetFaceDirectionWorldRotation();
/** Brachiation direction in world coordinates. */
UFUNCTION(BlueprintCallable)
FVector GetBrachiationWorldAxis();
/** Character face direction while brachiating in world coordinates. */
UFUNCTION(BlueprintCallable)
FVector GetBrachiationWorldFaceDirection();
// .............................................................. Constructor
ABrachiationTrigger();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
};
Now that we’ve defined everything in the header file, let’s implement it in the cpp file:
/* BrachiationTrigger.cpp */
#include "BrachiationTrigger.h"
#include "Components/BoxComponent.h"
FRotator ABrachiationTrigger::GetFaceDirectionWorldRotation()
{ return GetActorRotation() + BrachiationFaceDirection.ToOrientationRotator(); }
FVector ABrachiationTrigger::GetBrachiationWorldAxis()
{ return GetActorRotation().RotateVector(BrachiationAxis); }
FVector ABrachiationTrigger::GetBrachiationWorldFaceDirection()
{ return GetActorRotation().RotateVector(BrachiationFaceDirection); }
ABrachiationTrigger::ABrachiationTrigger()
{
PrimaryActorTick.bCanEverTick = true;
// Create a separate root component, so we can place the
// trigger volume relatively to it
Root = CreateDefaultSubobject<USceneComponent>(FName("Root"));
RootComponent = Root;
// Create a box trigger and keep a reference, so we can edit it in editor
BrachiationLedge = CreateDefaultSubobject<UBoxComponent>(
FName("Brachiation Ledge"));
BrachiationLedge->InitBoxExtent(FVector(100, 5, 5));
BrachiationLedge->SetGenerateOverlapEvents(true);
BrachiationLedge->SetCollisionProfileName(FName("Trigger"));
// Setup Hierarchy
RootComponent->SetRelativeLocation(FVector::ZeroVector);
BrachiationLedge->AttachToComponent(
RootComponent,
FAttachmentTransformRules::KeepRelativeTransform);
BrachiationLedge->SetRelativeLocation(FVector::ZeroVector);
}
void ABrachiationTrigger::BeginPlay()
{ Super::BeginPlay(); }
void ABrachiationTrigger::Tick(float DeltaTime)
{ Super::Tick(DeltaTime); }
I know, it’s lots of stuff. But take your time and try to understand everything. The next step is to base a new Blueprint on this class and add some debugging shapes to it. So right click in editor and derive a new Blueprint class and name it BrachiationTrigger_BP
.
Then add two new Arrow
components. We’ll use them to visualize brachiation direction and facing normal:
We’ll always want our arrows to point in the directions of the both variables. So we set up their rotations in the construction script:
Congratulations! Our brachiation trigger is ready. You can add a box component and always set it’s extent and position to the trigger box, so you can see the trigger in game (see my first screenshot). Now it’s time to teach our character how to brachiate!
The Character
This one is a little bit more complex. I guess there are simpler ways to do it, but it’s a good starting point and you can try to make it simpler from there. We’ll base everything on the ThirdPersonCharacter
Unreal provides us with in the third person game template (use the C++ template). Simply copy it and add some stuff. I am not allowed to show you Epic’s original code (see the contract when using Unreal Engine) but I show you what I added. Since we’re starting with the same code file, you should get it done.
First we need a new enumeration type ECustomMovementTypeEnum
, to be able to add more custom movement modes. Add it to the top of the header file and add as many movement modes as you want. We’ll only implement one for now:
/* ThirdPersonCharacter.h */
UENUM(BlueprintType)
enum class ECustomMovementTypeEnum : uint8
{
MOVE_None UMETA(DisplayName = "none"),
MOVE_ClimbingLadder UMETA(DisplayName = "Climbing Ladder"),
MOVE_Brachiating UMETA(DisplayName = "Brachiating")
};
Add ABrachiationTrigger
as forward declaration at the top of the header file:
/* ThirdPersonCharacter.h */
// Forward Declarations
class ABrachiationTrigger;
Then add some properties for us to keep track of our movement:
- a boolean
bBrachiating
which is true, when the character is brachiating, - a boolean
bDynamicallyBrachiating
, which is true when the character is not just hanging on the ledge but additionally moving sideways, - a variable storing our current movement type,
- two pointers to two box triggers we’ll add to our character, to overlap with our Brachiation Trigger:
/* ThirdPersonCharacter.h */
public:
// .............................................................. Properties
/** Set true, when colliding with a brachiation trigger and brachiating
along an edge. */
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Movement Type")
bool bBrachiating = false;
/** Set true, when actively brachiating. False, when just hanging on
the ledge. */
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Movement Type")
bool bDynamicallyBrachiating = false;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Movement Type")
FVector CurrentBrachiationDirection = FVector::ZeroVector;
UPROPERTY(BlueprintReadWrite, VisibleAnywhere, Category = "Movement Type")
ECustomMovementTypeEnum CurrentCustomMovementType =
ECustomMovementTypeEnum::MOVE_None;
UPROPERTY(BlueprintReadWrite, EditAnywhere)
UPrimitiveComponent* BrachiationColliderHold = nullptr;
UPROPERTY(BlueprintReadWrite, EditAnywhere)
UPrimitiveComponent* BrachiationColliderWalkOverLedge = nullptr;
Now let’s create the two new triggers in the cpp-File and have a look at them in the derived Blueprint Character:
/* ThirdPersonCharacter.cpp */
AThirdPersonCharacter::AThirdPersonCharacter
(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
/* Already existing Unreal Code. Do not remove it. */
// ...
// Create a trigger in front of our characters head, which
// triggers brachiation when overlapping a ledge
auto BrachiationBox = CreateDefaultSubobject<UBoxComponent>(
FName("Brachiation Collider Hands"));
BrachiationBox->SetBoxExtent(FVector(10, 20, 10));
BrachiationBox->AttachToComponent(
RootComponent,
FAttachmentTransformRules::KeepRelativeTransform);
BrachiationBox->SetRelativeLocation(FVector(15, 0, 50));
BrachiationBox->SetGenerateOverlapEvents(true);
BrachiationBox->SetCollisionProfileName(FName("Trigger"));
BrachiationColliderHold = BrachiationBox;
// Create a second one at it's back to trigger brachiation
// when slowly walking over a ledge.
BrachiationBox = CreateDefaultSubobject<UBoxComponent>(
FName("BrachiationColliderWalkOverLedge"));
BrachiationBox->SetBoxExtent(FVector(10, 20, 10));
BrachiationBox->AttachToComponent(
RootComponent,
FAttachmentTransformRules::KeepRelativeTransform);
BrachiationBox->SetRelativeLocation(FVector(-15, 0, -10));
BrachiationBox->SetGenerateOverlapEvents(true);
BrachiationBox->SetCollisionProfileName(FName("Trigger"));
BrachiationColliderWalkOverLedge = BrachiationBox;
}
In our derived Blueprint it should lool like this (if you have none, create a new Blueprint class, base it on ThirdPersonCharacter
and provide your character’s mesh):
Now we have to add some behaviour to those trigger boxes. We’ll implement our event callback functions. In there we check if the overlapping actor is a ABrachiationTrigger
and our own overlapping component is one of our character brachiation triggers and then we initialize the brachiation:
Define the function in the header:
/* ThirdPersonCharacter.h */
/** Delegate for Shape's overlap begin event. */
UFUNCTION()
void OnTriggerComponentOverlapBegin
(
UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep, const
);
/** Delegate for Shape's overlap end event. */
UFUNCTION()
void OnTriggerComponentOverlapEnd
(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex
);
UFUNCTION()
void OnChangedMovementMode
(
class ACharacter* Character,
EMovementMode PrevMovementMode,
uint8 PreviousCustomMode
);
and implement them:
/* ThirdPersonCharacter.cpp */
void AThirdPersonCharacter::OnTriggerComponentOverlapBegin
(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult
){
// Prevent self collision
if (!OtherActor || (OtherActor == this)) { return; }
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++ Brachiation Trigger
if (OtherActor->GetClass()->IsChildOf(ABrachiationTrigger::StaticClass()))
{
// Check if overlapping component is BrachiationLedge
auto Trigger = Cast<ABrachiationTrigger>(OtherActor);
if ((OverlappedComp == BrachiationColliderHold ||
OverlappedComp == BrachiationColliderWalkOverLedge) &&
OtherComp == Trigger->BrachiationLedge)
{ StartBrachiating(Trigger, SweepResult); }
}
}
void AThirdPersonCharacter::OnTriggerComponentOverlapEnd
(
UPrimitiveComponent* OverlappedComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex
){
// Prevent self collision
if (!OtherActor || (OtherActor == this)) { return; }
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++ Brachiation Trigger
if (OtherActor->GetClass()->IsChildOf(ABrachiationTrigger::StaticClass()))
{
// Check if overlapping component is BrachiationLedge
auto Trigger = Cast<ABrachiationTrigger>(OtherActor);
if (OverlappedComp == BrachiationColliderHold &&
OtherComp == Trigger->BrachiationLedge)
{ EndBrachiating(false); }
}
}
void AThirdPersonCharacter::OnChangedMovementMode
(
ACharacter* Character,
EMovementMode PrevMovementMode,
uint8 PreviousCustomMode
){
auto CharacterMovementComp
= FindComponentByClass<UCharacterMovementComponent>();
auto CurrentMovementMode = CharacterMovementComp->MovementMode;
auto CurrentCustomMovementMode = CharacterMovementComp->CustomMovementMode;
switch ((ECustomMovementTypeEnum)CurrentCustomMovementMode)
{
case ECustomMovementTypeEnum::MOVE_Brachiating:
UE_LOG(LogTemp, Warning,
TEXT("Movement Mode changed to BRACHIATING"));
break;
case ECustomMovementTypeEnum::MOVE_ClimbingLadder:
UE_LOG(LogTemp, Warning,
TEXT("Movement Mode changed to CLIMBING LADDER"));
break;
default:
break;
}
}
Now let’s implement our brachiation functions StartBrachiating(...)
, EndBrachiating()
and AnimateBrachiation(...)
:
/* ThirdPersonCharacter.h */
public:
void AnimateStartBrachiating(float DeltaTime);
void StartBrachiating(ABrachiationTrigger* Trigger, const FHitResult& Hit);
void EndBrachiating(bool JumpUp);
private:
float BrachiationStartTime = 0.0f;
float BrachiationMinTime = 0.3f;
FVector BrachiationStartPosition = FVector::ZeroVector;
bool bBrachiationStartAnimationRunning = false;
bool bAnimating = false;
In StartBrachiating(...)
we check if all preconditions are met. Is the character slow enough to brachiate? Are all components there? Then we set some booleans to tell other functions they should not interfere with brachiation. At the start we move our character smoothly into our starting position and block everything else for a while. As soon as this has finished, our character will be able to move along the ledge or leave it again. For that we need to remember the starting time, to keep a minimum duration of brachiating.
Then we calculate the closest point on the ledge to the characters hands brachiation trigger and center the character at this points, to make sure his hands are exactly on the ledge:
/* ThirdPersonCharacter.cpp */
void AThirdPersonCharacter::StartBrachiating
(ABrachiationTrigger* Trigger, const FHitResult& Hit)
{
// Check, if velocity matches the constrictions
// If Walking over the ledge
float Z = GetVelocity().Z;
// Get a absolute version of the velocity vector
FVector AbsoluteVelocity = FVector(
FMath::Abs(GetVelocity().X),
FMath::Abs(GetVelocity().Y),
FMath::Abs(GetVelocity().Z));
// Extract the vertical velocity
float VerticalVelocity = FMath::Abs(Z);
// Extract the horizontal velocity
float HorizontalVelocity
= FVector(AbsoluteVelocity.X, AbsoluteVelocity.Y, 0).Size();
// Check our velocity thresholds
if (VerticalVelocity > HorizontalVelocity) {
if (Z > 0 && VerticalVelocity > Trigger->JumpThroughVelocityThreshold)
{ return; }
if (Z < 0 && VerticalVelocity > Trigger->FallThroughVelocityThreshold)
{ return; } }
else {
if ( HorizontalVelocity > Trigger->RunThroughVelocityThreshold)
{ return; } }
if (!Trigger || !GetCharacterMovement()) { return; }
/* If we came here, everything is fine and brachiating can start */
// Stop Character by setting velocity to zero
GetCharacterMovement()->Launch(FVector(0, 0, 0));
bAnimating = true; // switch to animation status
bBrachiationStartAnimationRunning = true; // set it to start animation
// remember, when we started brachiating
BrachiationStartTime = GetWorld()->GetTimeSeconds();
// Set status to: BRACHIATING
bBrachiating = true;
// Rotate Character to ledges facing direction
SetActorRotation(Trigger->GetFaceDirectionWorldRotation());
CurrentBrachiationDirection = Trigger->GetBrachiationWorldAxis();
// Set actor location so that players hands are centered on the ledge
float CapsuleRadius = GetCapsuleComponent()->GetScaledCapsuleRadius();
FVector ClosestPointOnLedge = UKismetMathLibrary::FindClosestPointOnLine(
GetActorLocation(),
Trigger->GetActorLocation(),
Trigger->GetBrachiationWorldAxis());
BrachiationStartPosition = FVector
(
ClosestPointOnLedge.X - Trigger->GetBrachiationWorldFaceDirection().X
* (BrachiationColliderHold->RelativeLocation.X * 1.5f),
ClosestPointOnLedge.Y - Trigger->GetBrachiationWorldFaceDirection().Y
* (BrachiationColliderHold->RelativeLocation.X * 1.5f),
ClosestPointOnLedge.Z - BrachiationColliderHold->RelativeLocation.Z
);
GetCharacterMovement()->SetMovementMode(
EMovementMode::MOVE_Custom,
(uint8)ECustomMovementTypeEnum::MOVE_Brachiating);
}
To move our character smoothly into place, when starting the new movement, we have AnimateStartBrachiating(...)
which is called from our tick method:
/* ThirdPersonCharacter.cpp */
void AThirdPersonCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// Smooth Animations
if (bBrachiating) { AnimateStartBrachiating(DeltaTime); }
}
void AThirdPersonCharacter::AnimateStartBrachiating(float DeltaTime)
{
if (bBrachiationStartAnimationRunning)
{
FVector DestinationPosition = FMath::Lerp<FVector>(
GetActorLocation(),
BrachiationStartPosition, DeltaTime*20);
if ((BrachiationStartPosition - DestinationPosition).Size() < 0.01f)
{
bBrachiationStartAnimationRunning = false; // Close enough
bAnimating = false;
DestinationPosition = BrachiationStartPosition;
}
SetActorLocation(DestinationPosition);
}
}
In EndBrachiating(...)
we stop all animations, leave the ledge and change the movement mode back to falling or jumping:
/* ThirdPersonCharacter.cpp */
void AThirdPersonCharacter::EndBrachiating(bool JumpUp)
{
if (!bBrachiating) { return; }
bAnimating = false;
bBrachiationStartAnimationRunning = false;
bBrachiating = false;
bDynamicallyBrachiating = false;
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Falling, 0);
if (JumpUp) // Jump up from Ledge
{ GetCharacterMovement()->Launch(FVector(0, 0, 1000)); }
else // Fall down from ledge
{ GetCharacterMovement()->Launch(FVector(0, 0, -100)); }
}
Last but not least we need to add our movement function itself:
/* ThirdPersonCharacter.h */
protected:
void Brachiate(float Value);
And call it in the MoveRight(...)
function:
/* ThirdPersonCharacter.cpp */
void AThirdPersonCharacter::MoveRight(float Value)
{
if (bAnimating) { return; } // Block input while animating
if (bBrachiating)
{
Brachiate(Value);
return;
}
// Existing code ...
}
void AThirdPersonCharacter::Brachiate(float Value)
{
if (bAnimating) { return; } // Block input while animating
if (FMath::Abs(Value) > 0.001f) { bDynamicallyBrachiating = true; }
else { bDynamicallyBrachiating = false; }
if ((Controller != NULL) && (Value != 0.0f))
{
// find out which way is right
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(9, Rotation.Yaw, 0);
// get right vector
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// add movement in this direction
SetActorLocation(GetActorLocation() + Value*100
* CurrentBrachiationDirection
* GetWorld()->GetDeltaSeconds());
}
}
But how do we end our brachiation actively? By navigating for- or backwards:
/* ThirdPersonCharacter.cpp */
void AThirdPersonCharacter::MoveForward(float Value)
{
if (bAnimating) { return; } // Block input while animating
// No moving forward on brachiation
if (bBrachiating)
{
// Allow leaving brachiation only after minimal brachiation time
if (GetWorld()->GetTimeSeconds() - BrachiationStartTime < BrachiationMinTime)
{ return; }
if (Value > 0.5f) { EndBrachiating(true); }
else if (Value < -0.5f) { EndBrachiating(false); }
return;
}
// Existing code ...
}
This is pretty much it. All we have to do now, is to entangle our animation in the animation blueprint. I’ll give you a screenshot and hope you could follow.
Animation Blueprint
Now simply place your brachiation triggers in the world and jump at them. Play with the threshold values and the character trigger positions.
Hope you had fun and learnt something :)
Written with StackEdit.
Unreal Engine C++ Tutorial Series
Want more? Have a look into the following tutorials!
Comments
Post a Comment