UE4

[UE4] 상태패턴으로 캐릭터 효율적 관리 구상해보기

Honey Badger 2023. 4. 5. 22:38

언리얼에서 여러가지 다양한 애니메이션(스킬, 달리기, 구르기, 점프공격, 점프, 공격, 피격, idle, 죽음)을 연결하다보니 AnimBlueprint의 StateMachine을 사용하게 되었고, 캐릭터의 다양한 상태를 코드적으로 구현해 관리하면 애니메이션이나 다른 부분을 구현할때에도 도움이 되지 않을까 생각해서 구상해보기로 했다. 

 

 

1. 순수가상클래스 State 정의하기.

 

먼저, 상태를 정의하는 State Class를 작성한다. 

// State.h

#pragma once

class AMyCharacter;

class State
{
public:
    virtual ~State() {}
	
    virtual void Enter(AMyCharacter* Character) {}
    virtual void Update(AMyCharacter* Character, float DeltaTime) {}
    virtual void Exit(AMyCharacter* Character) {}
};

State 클래스는 3개의 가상 함수를 제공한다. 이 함수들은 캐릭터의 상태에 따라 다른 동작을 수행하도록 State클래스를 상속받는 클래스에서 구현된다. 

 

 

 

 

 

2. State를 상속받은 다양한 State 정의하기.

 

다음으로, 각 상태를 구현하는 클래스들을 작성한다. 예를 들어 IdleState를 구현하는 클래스를 작성해보겠다. 

// IdleState.h

#pragma once

#include "State.h"

class AMyCharacter;

class IdleState : public State
{
public:
    virtual void Enter(AMyCharacter* Character) override;
    virtual void Update(AMyCharacter* Character, float DeltaTime) override;
    virtual void Exit(AMyCharacter* Character) override;
};

// IdleState.cpp

#include "IdleState.h"
#include "MyCharacter.h"

void IdleState::Enter(AMyCharacter* Character)
{
    // Idle 상태 진입시 수행할 동작 구현.
    Character->PlayIdleAnimation(); //캐릭터의 Idle 애니메이션 재생.
}

void IdleState::Update(AMyCharacter* Character, float DeltaTime)
{
    // Idle 상태 업데이트 동작
    if (Character->IsMoving()) //캐릭터가 움직이고 있으면,
    {
        Character->ChangeState(Character->GetRunState()); //Run 상태로 전환.
    }
    else if (Character->IsJumping()) //캐릭터가 점프하는 중이면,
    {
        Character->ChangeState(Character->GetJumpState()); //점프 상태로 전환.
    }
    else if (Character->IsAttacking()) //캐릭터가 공격하는 중이면, 
    {
        Character->ChangeState(Character->GetAttackState()); //공격 상태로 전환. 
    }
}

void IdleState::Exit(AMyCharacter* Character)
{
    // Idle 상태 탈출시 수행할 동작
}

위와 같은 방식으로 각각의 State 클래스들을 구현한 다음, State 클래스를 사용하여 캐릭터의 상태를 관리하는 StateMachine 클래스를 작성한다. 

 

 

 

 

 

3. State 클래스들을 관리하는 State Machine Class 정의하기.

// StateMachine.h

#pragma once

#include "State.h"

class AMyCharacter;

class StateMachine
{
public:
    void SetCurrentState(State* NewState);
    void Update(AMyCharacter* Character, float DeltaTime);

private:
    State* CurrentState = nullptr;
};

// StateMachine.cpp

#include "StateMachine.h"

//기존의 상태를 나가고, 인자로 들어온 새로운 상태를 CurrentState로 설정한다. 
void StateMachine::SetCurrentState(State* NewState)
{
    if (CurrentState)
    {
        CurrentState->Exit();
    }

    CurrentState = NewState;

    if (CurrentState)
    {
        CurrentState->Enter();
    }
}

// 현재 상태의 Update를 실행해준다. 
void StateMachine::Update(AMyCharacter* Character, float DeltaTime)
{
    if (CurrentState)
    {
        CurrentState->Update(Character, DeltaTime);
    }
}

 

이제 캐릭터 클래스에서 StateMachine 객체를 멤버 변수로 선언하고 각 State 객체를 생성 및 초기화한다. 이후 캐릭터의 상태가 변경될 때마다 StateMachine의 SetCurrentState() 함수를 호출하여 다음과 같이 상태를 변경하면 된다. 

 

 

 

 

 

 

4. 캐릭터 클래스에서 State 패턴 적용시키기.

// MyCharacter.h

#pragma once

#include "State.h"
#include "StateMachine.h"

class AMyCharacter : public ACharacter
{
public:
    AMyCharacter();

    void ChangeState(State* NewState); //캐릭터의 상태를 변경한다.
    
    //각 상태의 Get함수. 
    State* GetIdleState() const; 
    State* GetRunState() const;
    State* GetJumpState() const;
    State* GetAttackState() const;
	
    //각 상태를 판단하는 함수.
    bool IsMoving() const;
    bool IsJumping() const;
    bool IsAttacking() const;

protected:
    virtual void Tick(float DeltaTime) override;

private:
	//상태를 관리하는 객체.
    StateMachine StateMachine;
    
    //각 상태 클래스 정의.
    State* IdleState = nullptr;
    State* RunState = nullptr;
    State* JumpState = nullptr;
    State* AttackState = nullptr;

	//상태 판별할 변수 정의.
    bool bIsMoving = false;
    bool bIsJumping = false;
    bool bIsAttacking = false;
};

// MyCharacter.cpp

#include "MyCharacter.h"

AMyCharacter::AMyCharacter()
{
    PrimaryActorTick.bCanEverTick = true;

    // State 객체 초기화
    IdleState = new IdleState();
    RunState = new RunState();
    JumpState = new JumpState();
    AttackState = new AttackState();

    // StateMachine 객체 초기화
    StateMachine.SetCurrentState(IdleState);
}

//캐릭터의 상태를 변경하는 함수.
void AMyCharacter::ChangeState(State* NewState)
{
    StateMachine.SetCurrentState(NewState);
}

State* AMyCharacter::GetIdleState() const
{
    return IdleState;
}

State* AMyCharacter::GetRunState() const
{
    return RunState;
}

State* AMyCharacter::GetJumpState() const
{
    return JumpState;
}

State* AMyCharacter::GetAttackState() const
{
    return AttackState;
}

bool AMyCharacter::IsMoving() const
{
    return bIsMoving;
}

bool AMyCharacter::IsJumping() const
{
    return bIsJumping;
}

bool AMyCharacter::IsAttacking() const
{
    return bIsAttacking;
}

void AMyCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // 캐릭터의 상태에 따라서 bIsMoving, bIsJumping, bIsAttacking 값 변경

    // StateMachine 업데이트
    StateMachine.Update(this, DeltaTime);
}

위 코드의 Tick()함수에서 캐릭터의 상태를 업데이트하고, StateMachine 객체의 Update()함수를 호출하면 StateMachine 객체는 현재 상태에 맞는 State 객체의 Update()함수를 호출하게 된다. 각 State 클래스에서는 캐릭터의 상태에 따라 다른 동작을 구현한다. 예를 들어, RunState 클래스에서는 캐릭터의 이동 속도를 높이는 동작을 다음과 같이 구현할 수 있다.

 

 

 

 

 

4. State 적용 예시.

// RunState.cpp

#include "RunState.h"
#include "MyCharacter.h"

void RunState::Enter(AMyCharacter* Character)
{
    // 이동 속도 높임.
    Character->GetCharacterMovement()->MaxWalkSpeed = 600.f;
}

void RunState::Update(AMyCharacter* Character, float DeltaTime)
{
    if (!Character->IsMoving())
    {
        // 이동 중지 시 IdleState로 전환
        Character->ChangeState(Character->GetIdleState());
    }
    else if (Character->IsJumping())
    {
        // 점프 시 JumpState로 전환
        Character->ChangeState(Character->GetJumpState());
    }
    else if (Character->IsAttacking())
    {
        // 공격 시 AttackState로 전환
        Character->ChangeState(Character->GetAttackState());
    }
}

void RunState::Exit(AMyCharacter* Character)
{
    // 이동 속도 원래대로
    Character->GetCharacterMovement()->MaxWalkSpeed = 400.f;
}

RunState 클래스에서는 Enter()함수에서 캐릭터의 이동 속도를 높이는 로직을 구현하였다.. Exit()함수에서는 이동 속도를 원래대로 돌려놓는다. 이와 같이 각 State 클래스에서는 상태변환 및 해당 상태에서 필요한 동작을 구현한다. 이를 통해 캐릭터의 상태를 효율적으로 관리할 수 있게 된다.