TRIGGER HAPPY

VIDEO

FSM Player Movement & Combat System
Unreal Engine
C++

ABOUT

Trigger Happy was the fourth and final group project I did at Futuregames.

I was in charge of the player character, coding its movement and combat.

ROLES: Gameplay Programmer

ENGINE: Unreal Engine 5.6

DURATION: 4 Weeks

TEAM SIZE: 10

INFO

The player character used for the game. You can slide, vault, dash, fire a gun, reload it, activate abilities, etc.

It was created with an incomplete prototype verison as a reference.

PLAYER CHARACTER

diagram of player functionality

The fundamental player functionality is split up into two different components, each using a finite state machine (FSM). One for movement and one for combat.

Both FSMs make use of a so-called “context”, holding information regarding what the player wants to do, is doing, and anything else relevant. This is the primary way state switching is handled.

Input is handled in blueprints and is used to set certain values in each state machine context.

movement system demonstration

The movement FSM has states for walking, crouching, sprinting, sliding, vaulting, and dashing.

The combat FSM has states for idle, firing the gun, reloading, melee, as well using a combat ability.

The ability state is hooked up to an ability component with a “current combat ability” - this is what activates upon switching to this state. It also holds unlocked passive abilities.

combat system demonstration

UWalkState.cpp: Enter() Tick() Move()

bool UWalkState::Enter(FMovementContext Context)
{
    //set character movement component max speed, acceleration, etc.
    
    float MoveSpeed = FallbackMoveSpeed;
    if (AttributeComponent) MoveSpeed = AttributeComponent->GetAttributeValue(AttributeTags::Attribute_Movement_WalkSpeed);
    
    if (OwnerCharacterMovementComponent) OwnerCharacterMovementComponent->MaxWalkSpeed = MoveSpeed;

    CustomCharMoveComp->OnWalkStateEnter.Broadcast();
    
    return true;
}

void UWalkState::Tick(float DeltaTime, FMovementContext Context)
{
    Super::Tick(DeltaTime, Context);

    if (!OwnerPawn) return;
    
    Move(Context.MoveInput);

    if (Context.bWantsToSprint && OwnerPawn->GetVelocity().Size() > 0)
        FSM->SetState(USprintState::StaticClass(), Context);

    if (Context.bWantsToCrouch)
        FSM->SetState(UCrouchState::StaticClass(), Context);

    if (Context.bWantsToDash)
        FSM->SetState(UDashState::StaticClass(), Context);
}

void UWalkState::Move(FVector2f MoveInput)
{
    FVector ForwardBack = OwnerPawn->GetActorForwardVector() * MoveInput.Y;
    FVector LeftRight = OwnerPawn->GetActorRightVector() * MoveInput.X;
    FVector ResultMoveDir = (ForwardBack + LeftRight).GetSafeNormal();
    
    OwnerPawn->AddMovementInput(ResultMoveDir);
}
    
SlideVaultState.cpp: Enter() Tick() Exit()

bool USlideVaultState::Enter(FMovementContext Context)
{
    if (!Context.bCanVault) return false;
    
    StartSlide(Context.VaultStartLocation);
    CustomCharMoveComp->OnSlideVaultStart.Broadcast();

    //set height
    if (OwnerCapsuleComponent)
    {
        OwnerCapsuleComponent->SetCapsuleHalfHeight(CurrentCapsuleHalfHeight);

        GroundOffset = (InitialCapsuleHalfHeight - CurrentCapsuleHalfHeight) / 2;

        FVector NewLocation = FVector(
            OwnerPawn->GetActorLocation().X,
            OwnerPawn->GetActorLocation().Y,
            OwnerPawn->GetActorLocation().Z - GroundOffset);
        
        OwnerPawn->SetActorLocation(NewLocation);
    }
    
    return true;
}

void USlideVaultState::Tick(float DeltaTime, FMovementContext Context)
{
    Super::Tick(DeltaTime, Context);

    if (TryWalkTransition(Context)) return;
}

bool USlideVaultState::Exit()
{
    StopSlide();
    CustomCharMoveComp->OnSlideVaultFinish.Broadcast();

    //set crouch height back to initial
    if (OwnerCapsuleComponent)
        OwnerCapsuleComponent->SetCapsuleHalfHeight(InitialCapsuleHalfHeight);

    FVector NewLocation = FVector(
        OwnerPawn->GetActorLocation().X,
        OwnerPawn->GetActorLocation().Y,
        OwnerPawn->GetActorLocation().Z + GroundOffset);

    OwnerPawn->SetActorLocation(NewLocation);

    CustomCharMoveComp->OnSlideVaultExecuted();
    
    return true;
}
CustomCharacterMovementComponent.cpp: BeginPlay()

void UCustomCharacterMovementComponent::BeginPlay()
{
    Super::BeginPlay();

    //Initial Setting
    Owner = GetOwner();
    if (!Owner) return;

    Pawn = Cast<APawn>(GetOwner());
    if (!Pawn) return;

    if (Owner)
        AttributeComponent = Owner->GetComponentByClass<UAttributeComponent>();

    //Initial Value
    float DashCharges = FallbackMaxDashCharges;
    if (AttributeComponent)
        DashCharges = AttributeComponent->GetAttributeValue(AttributeTags::Attribute_Dash_MaxCharges);

    CurrentDashCharges = DashCharges;
    CurrentDashCooldownTime  = 0;
    CurrentSlideCooldownTime = 0;
    CurrentVaultCooldownTime = 0;

    InputContext.bMeleeDashUnlocked  = false;
    InputContext.bMeleeSlideUnlocked = false;

    //Movement Finite State Machine
    // fsm setup
    MovementFSM = NewObject<UMovementFiniteStateMachine>();
    MovementFSM->Init(Owner, Pawn, this);

    UWalkState* WalkState = NewObject<UWalkState>();
    USprintState* SprintState = NewObject<USprintState>();
    UCrouchState* CrouchState = NewObject<UCrouchState>();
    UDashState* DashState = NewObject<UDashState>();
    USlideState* SlideState = NewObject<USlideState>();
    URegularVaultState* RegularVaultState = NewObject<URegularVaultState>();
    USlideVaultState* SlideVaultState = NewObject<USlideVaultState>();

    MovementFSM->SetupStates({
        WalkState,
        SprintState,
        CrouchState,
        DashState,
        SlideState,
        RegularVaultState,
        SlideVaultState
    });

    WalkState->Init(FallbackWalkSpeed);
    SprintState->Init(
        FallbackSprintSpeed, VaultTraceLength, VaultTraceRadius, MovementTraceChannel,
        SprintToVaultHeightRequirement, RegularVaultableTagName, SlideVaultableTagName);
    CrouchState->Init(
        CrouchDownSpeed, UncrouchSpeed, FallbackCrouchSpeed, CrouchHeightMultiplier);
    DashState->Init(
        FallbackDashSpeed, DashDuration, AccelerationDuringDash, DashFallSpeedInAir, DashGravityScale,
        bCanDashInAir, VaultTraceLength, VaultTraceRadius, MovementTraceChannel,
        DashToVaultHeightRequirement, RegularVaultableTagName, SlideVaultableTagName,
        MaxDashHitCapacity, FallbackDashHitDamage, FallbackForwardHitKnockbackStrengthForSurvivingEnemies,
        UpwardHitKnockbackStrengthForSurvivingEnemies, FallbackForwardHitKnockbackStrengthForDyingEnemies);
    SlideState->Init(
        FallbackSlideStrength, SlideFrictionLevel, FallbackSlideDuration, SlideHeightMultiplier,
        MaxSlideHitCapacity, FallbackSlideHitDamage, FallbackForwardSlideHitKnockbackStrengthForSurvivingEnemies,
        UpwardSlideHitKnockbackStrengthForSurvivingEnemies, FallbackForwardSlideHitKnockbackStrengthForDyingEnemies,
        SlideHitTraceLength, SlideHitTraceRadius);
    RegularVaultState->Init();
    SlideVaultState->Init(
        SlideVaultStrength, SlideVaultFrictionLevel, SlideVaultHeightMultiplier, SlideVaultableTagName, MovementTraceChannel);

    // inital state set
    MovementFSM->SetState(UWalkState::StaticClass(), InputContext);

    //Components
    if (AActor* OwnerActor = MovementFSM->GetOwnerActor())
    {
        UCharacterMovementComponent* CharMoveComp = OwnerActor->GetComponentByClass<UCharacterMovementComponent>();
        CharMoveComp->BrakingDecelerationFalling = FallSpeedInAir;
        CharMoveComp->GravityScale = GravityScale;
    }

    // Subscribe to AttributeValue Changed for max charges to increase current charges by the positive delta of the change
    //Subscribe to Events
    AttributeComponent->OnAttributeValueChanged.AddDynamic(this, &UCustomCharacterMovementComponent::OnAttributeValueChanged);
}
  
MOVEMENT SYSTEM CODE
(.cpp files)

reflection on player character

This project was not my first time making a player character, but it was my first time making one with a state machine architecture. I learned how useful they can be for controlling the flow of actions outside of AI.

Since it was my first time, there are some things I wish I did differently. The biggest one being where I kept my combat and movement functions. I originally kept the functions in each state, but they definitely should have lived in their respective components, where states call whatever function they need from their parent component. Once I realized more global information needed to be in the component, like ticking a melee cooldown, it was too late to refactor properly.

A smaller issue was having two movement components - unreal engine 5’s own and the custom one I wrote. Since my custom component was using unreal’s pre-built movement functions anyways, I should have just made mine inherit from unreal’s.

Lastly, I wish I could have had more time to work on the abilities. It’s done in a pretty messy way, where it’s a little hard to read the scripts and understand how they tick down, manage cooldowns, etc. Because this was only a four-week project, I ended up with not enough time to properly plan out the system.

let’s connect

LIKE WHAT YOU SEE?


MORE PROJECTS