LeeTaes 공부노트

[UE Team Project/T.A.] 4. 플레이어 달리기 / 구르기 구현 본문

프로젝트/TimelessAdventure

[UE Team Project/T.A.] 4. 플레이어 달리기 / 구르기 구현

리태s 2024. 9. 29. 11:33
728x90
반응형

개요

이번 포스팅에서는 CombatComponent(전투)와 함께 구르기, 달리기 기능을 정리해보도록 하겠습니다.


CombatComponent 구조 설명

CombatComponent는 전투와 관련된 기능을 모아둔 컴포넌트입니다.

 

프로젝트 기획상 데쉬와 구르기 동작은 스테미너를 소모하고, 스테미너가 다 떨어졌다면 더이상 수행하지 못하는 동작입니다. 이를 구현하기 위해 스테미너와 관련된 모든 전투 요소들을 CombatComponent에서 구현하게 되었습니다.

 

전체적인 행동을 FSM 방식으로 관리하기 위해 상태를 구분하기 위한 열거형을 사용했습니다.


달리기 기능 구현

우선 입력을 받기 위한 IA_Dash와 이에 바인딩될 함수를 InputComponent에서 구현해주도록 합니다.

 

InputComponent.h

더보기
...

void DashStart();								// 달리기 시작
void DashEnd();									// 달리기 종료

...

UPROPERTY(EditAnywhere, Category = "InputAction")
TObjectPtr<class UInputAction> IA_Dash;

InputComponent.cpp

더보기
void UTA_InputComponent::AddInput(UInputComponent* PlayerInputComponent)
{
    if (!IsValid(OwnerPlayer)) return;

    UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);
	
    ...
    
    EnhancedInputComponent->BindAction(IA_Dash, ETriggerEvent::Started, this, &UTA_InputComponent::DashStart);
    EnhancedInputComponent->BindAction(IA_Dash, ETriggerEvent::Completed, this, &UTA_InputComponent::DashEnd);

}

void UTA_InputComponent::DashStart()
{
    if (!IsValid(OwnerPlayer->GetCombatComponent())) return;

    OwnerPlayer->GetCombatComponent()->DashStart();
}

void UTA_InputComponent::DashEnd()
{
    if (!IsValid(OwnerPlayer->GetCombatComponent())) return;

    OwnerPlayer->GetCombatComponent()->DashEnd();
}

기획상 달리기 상태일 때 Stamina가 소모되며, Stamina가 0이 되면 달리기를 하지 못하도록 만들어줘야 합니다.

 

CombatComponent의 Tick 함수에서 현재 상태에 따라 Stamina를 업데이트 해주도록 합니다.

또한, 앞으로 만들어줄 간단한 구르기 동작과 공격, 특수 동작에 따라 여러 조건들을 설정해주었습니다.

 

특히 구르기 동작을 수행할 때 Dash 키를 입력하는 경우 구르기 동작이 끝난 뒤 자연스럽게 Dash로 연결지어주기 위해 TempState를 임시로 저장하는 방식으로 구현하게 되었습니다.

 

CombatComponent.h

더보기
UENUM(BlueprintType)
enum class ECombatState : uint8
{
	CS_Idle,			// 기본
	CS_Dash,			// 달리기
	CS_Roll,			// 구르기 (회피)
	CS_Attack,			// 공격
	CS_Special,			// 특수 동작
};

...

public:
    // Walk
    void Walk(FVector ForwardDir, FVector RightDir, FVector2D MovementVector2D);
    // Dash Start
    void DashStart();
    // Dash End
    void DashEnd();

    ...
    
// Stat
private:
    void UseStamina(float InValue);

    // 체력
    UPROPERTY(EditAnywhere, Category = "Stat")
    float MaxStamina;

    UPROPERTY(VisibleAnywhere, Category = "Stat")
    float CurrentStamina;

    // 지속 체력 증가/감소량 
    UPROPERTY(EditAnywhere, Category = "Stat")
    float UseStaminaPercent;
    
    ...
    
private:
    // Change Combat State
    void ChangeState(ECombatState NewState);
    // Current Combat State
    ECombatState CombatState;
    // Cache - Prev Combat State
    ECombatState TempState;

CombatComponent.cpp

더보기
UTA_CombatComponent::UTA_CombatComponent()
{
    PrimaryComponentTick.bCanEverTick = true;

    // 멤버 변수 초기화
    MaxStamina = 10.0f;
    CurrentStamina = 0.0f;
    MaxHp = 100.0f;
    CurrentHp = 0.0f;
    UseStaminaPercent = 0.5f;	

    WalkSpeed = 300.0f;
    DashSpeed = 600.0f;

    CombatState = ECombatState::CS_Idle;
}

...

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

    Init();

    CurrentHp /= 2;
}

void UTA_CombatComponent::Init()
{
    // 값 초기화 (체력, HP)
    CurrentHp = MaxHp;
    CurrentStamina = MaxStamina;

    // 이동속도 초기화
    OwnerPlayer->GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
}

void UTA_CombatComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    if (!OwnerPlayer) return;

    // 현재 Dash 상태인 경우
    if (CombatState == ECombatState::CS_Dash)
    {
        // 지속 스테미너 감소
        CurrentStamina -= DeltaTime;
        if (CurrentStamina < 0.0f)
        {
            CurrentStamina = 0.0f;
            // Idle 상태로 변경
            ChangeState(ECombatState::CS_Idle);
        }
    }
    // 현재 Idle상태인 경우
    else if (CombatState == ECombatState::CS_Idle)
    {
        // 지속 스테미너 회복
        CurrentStamina += DeltaTime * UseStaminaPercent;
        if (CurrentStamina >= MaxStamina)
        {
            CurrentStamina = MaxStamina;
        }
    }
    
    // TEST : 로그
    {
        FString Name;

        switch (CombatState)
        {
        case ECombatState::CS_Idle:
            Name = TEXT("Idle");
            break;
        case ECombatState::CS_Dash:
            Name = TEXT("Dash");
            break;
        case ECombatState::CS_Roll:
            Name = TEXT("Roll");
            break;
        case ECombatState::CS_Attack:
            Name = TEXT("Attack");
            break;
        case ECombatState::CS_Special:
            Name = TEXT("Special");
            break;
        default:
            break;
        }

        GEngine->AddOnScreenDebugMessage(3, 1.0f, FColor::Green, FString::Printf(TEXT("CurState : %s"), *Name));
        GEngine->AddOnScreenDebugMessage(3, 1.0f, FColor::Green, FString::Printf(TEXT("CurHP : %.1f, MaxHP : %.1f"), CurrentHp, MaxHp));
        GEngine->AddOnScreenDebugMessage(1, 1.0f, FColor::Green, FString::Printf(TEXT("CurHealth : %.1f"), CurrentStamina));
        GEngine->AddOnScreenDebugMessage(2, 1.0f, FColor::Green, FString::Printf(TEXT("Rate      : %.1f"), CurrentStamina / MaxStamina));
	}
}

void UTA_CombatComponent::Walk(FVector ForwardDir, FVector RightDir, FVector2D MovementVector2D)
{
    if (!IsValid(OwnerPlayer)) return;
    // 공격 상태인 경우 반환
    if (CombatState == ECombatState::CS_Attack) return;

    OwnerPlayer->AddMovementInput(ForwardDir, MovementVector2D.X);
    OwnerPlayer->AddMovementInput(RightDir, MovementVector2D.Y);
}

void UTA_CombatComponent::DashStart()
{
    if (!IsValid(OwnerPlayer)) return;
    // 플레이어가 Roll/Attack 상태인 경우
    if (CombatState == ECombatState::CS_Roll || CombatState == ECombatState::CS_Attack)
    {
        // 데쉬 상태 임시 저장 후 반환
        TempState = ECombatState::CS_Dash;
        return;
    }

    // 플레이어의 상태가 Idle이 아닌 경우 Dash 불가
    if (CombatState != ECombatState::CS_Idle) return;

    // 상태 변경 (Dush)
    ChangeState(ECombatState::CS_Dash);
}

void UTA_CombatComponent::DashEnd()
{
    if (!IsValid(OwnerPlayer)) return;
    
    // 현재 상태가 Dash인 경우
    if (CombatState == ECombatState::CS_Dash)
    {
        // 상태 변경 (Idle)
        ChangeState(ECombatState::CS_Idle);
        return;
    }

    // 현재 상태가 Roll/Attack 상태인 경우
    if (CombatState == ECombatState::CS_Roll || CombatState == ECombatState::CS_Attack)
    {
        // 기본 상태 임시 저장 후 반환
        TempState = ECombatState::CS_Idle;
        return;
    }
}

...

void UTA_CombatComponent::ChangeState(ECombatState NewState)
{
    if (CombatState == NewState) return;

    // 변경된 상태에 따라 이동속도를 조절합니다.
    switch (NewState)
    {
    case ECombatState::CS_Idle:
        OwnerPlayer->GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
        break;

    case ECombatState::CS_Dash:
        OwnerPlayer->GetCharacterMovement()->MaxWalkSpeed = DashSpeed;
        break;
    }

    CombatState = NewState;
}

점프 / 구르기 기능 구현

기획상 구르기 행동을 수행할 때는 피격당하지 않는 무적 상태로 정했습니다.

또한 점프 중이 아닌 대부분의 상황에서 구르기가 가능해야 하며, 키보드 입력에 따라 즉시 원하는 방향으로 굴러줘야 합니다.

 

우선 입력을 받기 위해 입력 액션들(IA_Jump, IA_Roll)을 제작해 코드와 바인딩시켜주도록 하겠습니다.

 

CombatComponent에서 세부 내용을 구현해주도록 합니다.

 

CombatComponent.h

더보기
...

public:
    // Rolling Start
    void RollStart(FVector2D InMovementVector);
    // Rolling End
    void RollEnd(class UAnimMontage* Montage, bool bInterrupted);
    // Jump
    void CombatJump();
  
 ...
 
 private:
    // Roll Montage
    UPROPERTY(EditAnywhere, Category = "Anims")
    TObjectPtr<class UAnimMontage> RollMontage;

CombatComponent.cpp

더보기
void UTA_CombatComponent::RollStart(FVector2D InMovementVector)
{
    if (!IsValid(OwnerPlayer)) return;
    if (CombatState == ECombatState::CS_Roll || CombatState == ECombatState::CS_Special) return;
    if (OwnerPlayer->GetCharacterMovement()->IsFalling()) return;

    // 현재 체력에서 구르기가 가능한 경우
    if (GetHealthPercent() > RollHealthPercent)
    {
        // 체력 사용
        UseStamina(RollHealthPercent);

        // 임시 상태 저장
        if (CombatState == ECombatState::CS_Idle || CombatState == ECombatState::CS_Dash)
        {
            TempState = CombatState;
        }

        // 상태 변경
        ChangeState(ECombatState::CS_Roll);

        // 임시 저장된 상태에 따라 재생 계수 설정 (걷기 : 1, 달리기 : 1.3)
        float Mult = (TempState == ECombatState::CS_Dash) ? 1.3f : 1.0f;

        UAnimInstance* AnimInstance = OwnerPlayer->GetMesh()->GetAnimInstance();
        if (AnimInstance)
        {
            // Controller rotation Yaw 값 저장
            const FRotator Rotation = OwnerPlayer->Controller->GetControlRotation();
            const FRotator YawRotation(0, Rotation.Yaw, 0);

            // Yaw값을 기준으로 전방과 우측 방향 가져오기 (Y: forward, X : right)
            const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
            const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);

            // 전방, 우측 방향을 기준으로 회전해야 하는 방향 구하기 (입력 값)
            FRotator TargetRot = (ForwardDirection * InMovementVector.X + RightDirection * InMovementVector.Y).Rotation();

            // 마지막으로 입력된 방향에 따라 즉시 회전
            OwnerPlayer->SetActorRotation(TargetRot);

            // 구르기 몽타주 재생
            AnimInstance->Montage_Play(RollMontage, Mult);

            // 구르기 몽타주 종료 시 호출될 함수 바인딩
            FOnMontageEnded EndDelegate;
            EndDelegate.BindUObject(this, &UTA_CombatComponent::RollEnd);
            AnimInstance->Montage_SetEndDelegate(EndDelegate, RollMontage);
        }
    }
}

void UTA_CombatComponent::RollEnd(UAnimMontage* Montage, bool bInterrupted)
{
    // 임시 저장된 상태로 상태 변경
    ChangeState(TempState);
}

void UTA_CombatComponent::CombatJump()
{
    if (!OwnerPlayer) return;
    if (CombatState == ECombatState::CS_Roll || CombatState == ECombatState::CS_Attack || CombatState == ECombatState::CS_Special) return;

    OwnerPlayer->Jump();
}

void UTA_CombatComponent::UseStamina(float InValue)
{
    // 스테미너 감소
    CurrentStamina -= (InValue * MaxStamina);

    if (CurrentStamina <= 0.0f)
    {
        CurrentStamina = 0.0f;
        // Idle 상태로 변경
        ChangeState(ECombatState::CS_Idle);
    }
}

간단히 요약하면 점프 기능은 Character에 있는 Jump() 함수를 사용해 구현했으며, 구르기 기능은 수행 시 입력받은 마지막 이동 Vector를 기준으로 전방 방향을 찾아 플레이어를 회전시키고 구르기 몽타주를 재생하는 방식으로 구현하였습니다.

 

또한, 구르기 기능은 20%의 스테미너를 즉시 소비하게 만들었으며, 남은 스테미너의 퍼센트가 그보다 적은 경우 수행하지 못하도록 만들었습니다.


애니메이션 세팅

코드로 모든 작업을 마쳤으므로, 기본적인 IdleWalkRun BlendSpace를 생성해주고, 이를 AnimationBlueprint에 추가해주면 됩니다.

BlendSpace1D
LocoMotion (ABP)

아래 링크를 통해 기본적인 AnimInstance Class와 ABP, BlendSpace를 설정하는 방법에 대해 확인할 수 있습니다.

 

https://velog.io/@apth1023/4.-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98-Basic-Dash-Roll

 

[UE5 C++] 플레이어 애니메이션 - Basic, Dash, Roll

플레이어 기본 상태의 애니메이션 구현하기 (Basic, Dash, Roll)

velog.io


결과

 

728x90
반응형