UE5 C++으로 Command 패턴 활용하기

seunghyun·2024년 7월 26일
0

Command 패턴이란

보통 프로그래밍에서 요청 (또는 메시지) 이란, 나중에 실행했으면 하는 행동을 표현한다. 서비스에 비동기적으로 API를 호출하는 것과 비슷하다고 보면 된다. (이와 달리 이벤트는 이미 발생한 사건을 주로 표현한다)

요청과 그 요청이 실행되는 작업 사이에 레이어를 추가하여, 각 작업을 추상화된 명령 객체로 처리하는 디자인 패턴이다. 이를 통해 작업의 실행과 취소를 보다 유연하게 관리할 수 있다.

구체적으로는, 요청을 실행하는 방법을 객체로 캡슐화하여, 요청을 매개변수화하고 요청을 큐에 저장하거나 로그를 기록하며, 작업의 실행을 취소할 수 있게 한다.


언제 어울릴까?

  • 예를 들어, 유닛의 이동 명령을 큐에 저장하고 실행하거나, 플레이어의 입력을 기록하여 리플레이 기능을 구현할 수 있다.

  • 또는, GOAP(Goal-Oriented Action Planning): AI 디자인 방법으로, AI가 목표를 달성하기 위한 일련의 명령을 사전에 생성하고 이를 순차적으로 실행하는 데 사용될 수 있다.

실사례를 찾아보자

  • 클래식 게임 Braid에서는 명령 큐를 사용해 시간을 되돌리는 기능을 구현했다. 이 게임은 명령 큐를 통해 플레이어의 모든 행동을 기록하고, 시간을 되감아 이전 상태로 복원할 수 있다.
  • Forza Horizon 시리즈에서는 시간 되감기 기능이 포함되어 있다. 이 기능은 명령 큐를 사용하여 플레이어의 입력을 저장하고, 필요할 경우 특정 시점으로 되돌릴 수 있다.

  • Trials 시리즈와 같은 게임에서는 플레이어의 모든 입력을 저장하여 리플레이 기능을 구현한다. 이 기능은 명령 큐를 사용해 각 프레임의 입력을 기록하고, 동일한 입력을 반복하여 동일한 결과를 재현할 수 있다.

  • Super Smash Bros 이 게임에서는 리플레이 기능을 제공하기 위해 명령 큐를 사용한다. 플레이어의 모든 입력을 기록하고, 이를 기반으로 리플레이를 재생할 수 있다.


Command 패턴의 기본 구조

Command 패턴의 기본 클래스는 추상 클래스로, Command(), Execute(), Undo() 메서드를 가지도록 할 수 있다. 이 추상 클래스를 상속받아 구체적인 명령 클래스를 구현하며, 각 클래스는 필요한 객체 참조를 포함하여 작업을 실행하고 취소할 수 있도록 한다.

명령 추가, 실행, 취소 및 분기

명령은 리스트에 저장되어 작업의 실행 및 취소를 관리한다. 작업이 수행되면 해당 명령 객체가 리스트에 추가되며, 사용자가 취소 키를 누르면 마지막 명령이 취소된다. 새로운 명령이 추가되면 취소된 명령 이후의 명령은 잘려나간다.
(Windows에서 Ctrl+Z 키로 Undo를 하는 것처럼)

명령 리스트의 관리 과정

  • 작업이 실행되면 명령 객체가 리스트에 추가됨
  • 취소 키를 누르면 마지막 명령이 취소됨
  • 새로운 명령이 추가되면 취소된 명령 이후의 명령이 제거됨

이 방식을 아래와 같은 그림으로 정리했다.

위에서 언급한 것처럼 이 방식은 게임에서 유닛의 행동을 리스트/스택/큐에 저장하고 차례대로 실행하는 데 사용될 수 있다.


UE5 C++에서의 Command 패턴 구현

C++로 Command 패턴을 구현하기 위해 기본 명령 클래스를 정의하고, 이를 상속받는 구체적인 명령 클래스를 작성해서 유닛의 이동 명령을 큐에 저장하고 순차적으로 실행하는 예시를 작성했다.

Command.h (Command 패턴의 기본 클래스)
모든 명령의 기본 클래스이다.

#pragma once
#include "CoreMinimal.h"
#include "Command.generated.h"

UCLASS(Abstract) // 이 클래스가 추상 클래스임을 나타냄
class RTS_AI_API UCommand : public UObject
{
    GENERATED_BODY()

public:
    virtual void Execute();
};

Command_UnitMove.h (구체적인 명령 클래스 - 유닛 이동)

UCLASS()
class RTS_AI_API UCommand_UnitMove : public UCommand
{
    GENERATED_BODY()

public:
    void Init(AActor* unit, FVector moveLocation);
    virtual void Execute() override; // 이동 명령을 실행하는 함수

private:
    TObectPtr<AActor> _unit; // 이동할 유닛 객체
    FVector _moveLocation; // 이동 위치를 저장
};

Command_UnitMove.cpp (구현 파일)

void UCommand_UnitMove::Init(AActor* unit, FVector moveLocation)
{
    _unit = unit;
    _moveLocation = moveLocation;
}

void UCommand_UnitMove::Execute()
{
    Super::Execute();
    IControllableUnit::Execute_SetMoveLocation(_unit, _moveLocation);
}

QueueMoveLocation_Implementation 함수 (명령 큐 관리 함수)
유닛이 이동 중인지 확인하고, 이동 중이 아니면 이동 위치를 설정하고 이동을 시작한다.
이동 중이면 이동 명령을 큐에 추가한다.

void AEliteUnit::QueueMoveLocation_Implementation(FVector targetLocation)
{
    if (!_isMoving)
    {
        _AIController->GetBlackboardComponent()->SetValueAsVector("MoveToLocation", targetLocation);
        _isMoving = true;
        return;
    }

    TObectPtr<UCommand_UnitMove> moveCommand = NewObject<UCommand_UnitMove>(this);
    moveCommand->Init(this, targetLocation);
    _CommandQueue.Enqueue(moveCommand); // 이동 명령을 큐에 추가
}

MoveLocationReached_Implementation 함수 (이동 완료 후 명령 실행 함수)
명령 큐를 사용하여 여러 명령을 차례로 실행할 수 있다.
이동 명령이 완료되면 다음 명령을 큐에서 꺼내 실행한다.

void AEliteUnit::MoveLocationReached_Implementation()
{
    _isMoving = false;
    if (!_CommandQueue.IsEmpty())
    {
        TObectPtr<UCommand> command;
        _CommandQueue.Dequeue(command); // 명령 큐에서 다음 명령을 꺼냄
        command->Execute(); // 꺼낸 명령을 실행
    }
}
profile
game client programmer

0개의 댓글