[ Unreal Engine 5 / #22 Command Pattern ]

SeungWoo·2024년 10월 16일
0

[ Ureal Engine 5 / 수업 ]

목록 보기
23/31
post-thumbnail

커맨드 패턴 ( Command Pattern )

특정 작업을 객체로 캡슐화하여 실행, 취소 및 재실행할 수 있도록 하는 패턴입니다. Unreal Engine의 게임 입력 처리 또는 특정 작업 실행과 관련된 기능에서 사용할 수 있습니다

  • 캡슐화란

    객체 지향 프로그래밍에서 다음 2가지 측면
    • 객체의 속성(data fields)과 행위(메서드, methods)를 하나로 묶고,
    • 실제 구현 내용 일부를 외부에 감추어 은닉한다.

  • 커맨드 패턴의 장점

    • 보안과 안정성 : 객체의 내부 상태와 구현 세부사항을 감춤으로써 외부에서의 무분별한 접근을 방지합니다. 이는 데이터 무결성을 보호하고 의도치 않은 변경으로부터 객체를 보호하는 데 도움이 됩니다.
    • 추상화 : 객체의 내부 구현을 감추면 외부에 제공되는 인터페이스만 공개됩니다. 이를 통해 외부 사용자는 해당 객체가 어떻게 작동하는지에 대해 신경 쓰지 않고 인터페이스를 통해 상호 작용할 수 있습니다.
    • 모듈화와 유지보수성 : 객체가 외부에 영향을 미치지 않고 내부 구현을 변경할 수 있으므로, 객체의 내부 변경이 다른 부분에 영향을 미치지 않고 개선할 수 있습니다. 이는 코드를 모듈화하여 유지보수성을 향상시키는 데 도움이 됩니다.
    • 재사용성 : 객체의 외부 인터페이스를 사용하여 동일한 객체를 다양한 상황에서 재사용할 수 있습니다. 외부에 노출된 인터페이스를 통해 객체의 재사용성을 높일 수 있습니다.
  • 커맨드 패턴의 구성 요소

    • Command 인터페이스 : 실행할 명령을 정의하는 인터페이스로, 일반적으로 Execute() 메서드를 가집니다.
    • ConcreteCommand 클래스 : Command 인터페이스를 구현하는 구체적인 클래스입니다. 실제로 실행될 메서드(행동)를 정의하고, 해당 메서드를 호출할 Receiver 객체를 참조합니다.
    • Receiver 클래스 : 실제로 작업을 수행하는 객체입니다. ConcreteCommand 클래스는 Receiver 객체의 메서드를 호출하여 작업을 수행합니다.
    • Invoker 클래스 : ConcreteCommand 객체를 보유하고, 명령을 실행하거나 취소하는 역할을 합니다.
    • Client : ConcreteCommand 객체와 Receiver 객체를 생성하고, Invoker에 연결하는 클라이언트입니다.

  • ConcreteCommand 클래스와 Receiver클래스 통합해도 무방 하다. 해당 패턴을 형태로 쫌더 다양하게 분해하는 작업이 필요하다면 ConcreteCommand와 Receiver클래스를 분해해도 무방 하다

  • UnrealInterface C++ 클래스 하나를 만든다 "Command"

Command.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Command.generated.h"

UINTERFACE(MinimalAPI)
class UCommand : public UInterface
{
	GENERATED_BODY()
};

class PUZZLESTUDY_API ICommand
{
	GENERATED_BODY()

public:
	// 명령 실행
	virtual void Excute() = 0;
	// 명령 취소 --> Puzzle 게임 현상 ~= 블록 되돌아가기
	virtual void Undo() = 0;

	//virtual ~ICommand() = default;
};

  • None으로 만들 인터페이스를 상속받아 기능 구현을 넣을 생각이다

SwapTileCommand.h

#pragma once

#include "CoreMinimal.h"
#include "Command.h"

class ATile;

class PUZZLESTUDY_API SwapTileCommand : public ICommand
{
public:
	SwapTileCommand();
	~SwapTileCommand();

	SwapTileCommand(ATile* InFirstTile, ATile* InSecondTile);
    
private:
	ATile* FirstTile;
	ATile* SecondTile;
	FName FirstTileOrigineType;
	FName SecondTileOrigineType;
    
public:
	// ICommand의 인터페이스 함수들 Imprement ~ 
	virtual void Excute() override;
	virtual void Undo() override;
};

SwapTileCommand.cpp

#include "SwapTileCommand.h"
#include "Tile.h"

SwapTileCommand::SwapTileCommand()
{
}

SwapTileCommand::~SwapTileCommand()
{
}

SwapTileCommand::SwapTileCommand(ATile* InFirstTile, ATile* InSecondTile)
	: FirstTile(InFirstTile), SecondTile(InSecondTile)
{
	FirstTileOrigineType = FirstTile->TileType;
	SecondTileOrigineType = SecondTile->TileType;
}

void SwapTileCommand::Excute()
{
	// Swap , 위치 Vector ( Location ) , 행력 ( Row, Col -> int , int ) 
	FName Temp = FirstTile->TileType;
	FirstTile->TileType = SecondTile->TileType;
	SecondTile->TileType = Temp;
}

void SwapTileCommand::Undo()
{
	FirstTile->TileType = FirstTileOrigineType;
	SecondTile->TileType = SecondTileOrigineType;
}
  • 두 개의 타일을 교환하고, 그 상태를 되돌릴 수 있도록 설계
  • 코드 설명
    • FirstTile와 SecondTile: 교환할 두 타일의 포인터입니다.
    • FirstTileOriginalType와
    • SecondTileOriginalType : 교환 전에 두 타일의 원래 타입을 저장해둡니다. 이를 통해 Undo() 시 원래 상태로 복원할 수 있습니다.
    • Execute(): 두 타일의 타입을 교환하는 명령을 실행
    • Undo(): 교환된 두 타일을 원래 상태로 되돌리는 기능을 제공

  • Invoke의 형태는 Actor든 Pawn이든 상관없다 단지 그냥 은닉성으로 해당 Command를 실행시키고 저장하는 부분이기 때문에 나는 만만한 Actor로 만들었다

TileCommandInvoker.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Command.h"
#include "TileCommandInvoker.generated.h"

UCLASS()
class PUZZLESTUDY_API ATileCommandInvoker : public AActor
{
	GENERATED_BODY()
	
private:
	TArray<ICommand*> CommandHistory;

public:	
	ATileCommandInvoker();
	~ATileCommandInvoker();

	// 커맨드 실행, History( TArray )에 저장
	void ExcuteCommand(ICommand* Command);

	// 마지막 명령을 취소 ( Undo )
	void UndoLastCommand();
};

TileCommandInvoker.cpp

#include "TileCommandInvoker.h"

ATileCommandInvoker::ATileCommandInvoker()
{
	PrimaryActorTick.bCanEverTick = true;
}

ATileCommandInvoker::~ATileCommandInvoker()
{
}

void ATileCommandInvoker::ExcuteCommand(ICommand* Command)
{
	// 예시가 단순해서 ( X ), 캡슐화와 커맨드 패턴을 잘 지향하였다면, 거의 디커플링이 완벽
	Command->Excute();
	CommandHistory.Push(Command); 
	// add 의 alias가 push
}

void ATileCommandInvoker::UndoLastCommand()
{
	// 삭제 안하고, '없음' '공백' 
	// 되돌아가기 -> 되돌아갈 히스토리가 있어야된다
	if (CommandHistory.Num() != 0)
	{
		ICommand* LastCommand = CommandHistory.Last();
		LastCommand->Undo();

		CommandHistory.Pop();
		//CommandHistory.RemoveAll();
	}
}
  • 이제 명령을 실제로 실행하고, 필요 시 되돌리기(Undo)를 지원하는 Invoker를 만듭니다. 플레이어가 타일을 교환하면 SwapTilesCommand를 실행하고, 잘못된 선택을 한 경우 되돌릴 수 있음
    • CommandHistory: 실행된 명령들을 저장하는 스택입니다. 나중에 되돌리기(Undo) 기능을 지원하기 위해 사용
    • ExecuteCommand() : 명령을 실행하고, 명령 히스토리에 저장
    • UndoLastCommand() : 마지막으로 실행된 명령을 취소하고, 스택에서 제거
    • 메모리 관리 : 명령 히스토리의 명령 객체를 삭제하여 메모리 누수를 방지
profile
This is my study archive

0개의 댓글