Command 패턴

JJW·2024년 12월 2일
0

Unity

목록 보기
8/34

오늘은 Command 패턴에 대해 알아보는 시간을 가지겠습니다.


Command 패턴이란 ?

  • 요청을 객체로 캡슐화하여, 행동 실행의 호출자와 수행자를 분리하고 행동을 기록하거나 취소할 수 있는 디자인 패턴입니다.
  • 즉, 이벤트가 발생했을 때 실행될 기능이 다양하면서도 변경이 필요한 경우에 이벤트를 발생시키는 클래스를 변경하지 않고 재사용하고자 할 때 유용합니다.

Command 특징

  • 행동 캡슐화
    • 명령을 하나의 객체로 표현하여 동적으로 행동을 변경하거나 저장이 가능합니다.
  • 행동 기록 및 취소
    • 명령을 히스토리로 관리하여 취소 기능을 구현할 수 있습니다.
  • 행동 실행 분리
    • 명령을 호출하는 객체와 실행하는 객체 간의 결합도를 낮춥니다.

주요 구성 요소

1. Command (명령 인터페이스)

public interface ICommand
{
    void Execute();
    void Undo();
}
  • 행동을 추상화한 인터페이스 또는 추상 클래스입니다.
  • 일반적으로 Execute()와 Undo() 메서드를 포함합니다.

2. ConcreteCommand (구체 명령 클래스)

public class MoveCommand : ICommand
{
    private Transform _character;
    private Vector3 _previousPosition;
    private Vector3 _newPosition;

    public MoveCommand(Transform character, Vector3 newPosition)
    {
        _character = character;
        _newPosition = newPosition;
    }

    public void Execute()
    {
        _previousPosition = _character.position; // 이전 위치 저장
        _character.position = _newPosition;      // 새로운 위치로 이동
    }

    public void Undo()
    {
        _character.position = _previousPosition; // 이전 위치로 되돌림
    }
}
  • Command 인터페이스를 구현하며, 특정 행동을 정의합니다.
  • 예를 들어, MoveCommand, AttackCommand 등이 있을 수 있습니다.

3. Invoker (호출자)

public class InputHandler : MonoBehaviour
{
    private Stack<ICommand> _commandHistory = new Stack<ICommand>();
    public Transform character;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.W))
        {
            ExecuteCommand(new MoveCommand(character, character.position + Vector3.up));
        }
        if (Input.GetKeyDown(KeyCode.S))
        {
            ExecuteCommand(new MoveCommand(character, character.position + Vector3.down));
        }
        if (Input.GetKeyDown(KeyCode.Z)) // Undo 명령
        {
            UndoCommand();
        }
    }

    void ExecuteCommand(ICommand command)
    {
        command.Execute();
        _commandHistory.Push(command); // 명령 저장
    }

    void UndoCommand()
    {
        if (_commandHistory.Count > 0)
        {
            ICommand lastCommand = _commandHistory.Pop();
            lastCommand.Undo();
        }
    }
}
  • 명령을 실행하거나 취소하는 역할을 담당합니다.
  • 예를 들어, 플레이어의 입력을 처리하는 역할

4. Receiver (수신자)

public class Character : MonoBehaviour
{
    public void Move(Vector3 position)
    {
        transform.position = position;
    }
}
  • 실제로 명령을 수행하는 객체입니다.
  • 캐릭터, 몬스터, 또는 UI 요소 등이 될 수 있습니다.

Command 패턴의 흐름

  • 사용자 입력
  • 명령 실행
  • 명령 기록
  • 실행 취소

위 코드를 예시로 들자면

  • Input.GetKeyDown(KeyCode.W) : 사용자 입력
  • Execute() : 명령 실행
  • _commandHistory.Push(command) : 명령 기록
  • UndoCommand() : 실행 취소

Command 패턴의 장단점

  • 장점
    • 명령을 모듈화하여 재사용성 증가
    • 행동 기록 및 되돌리기 기능 제공
    • 호출자와 수신자 간 결합도를 낮춤
  • 단점
    • 간단한 로직에도 클래스가 늘어날 수 있음
    • 히스토리 관리 시 메모리 사용량이 증가할 수 있음

테스트

흐름도에서 예측한 것 처럼 흘러갑니다.
하지만 InputKey를 받을 때마다 New Command가 생성되어 성능 문제가 발생할 것 같습니다...


Command 패턴 최적화

Object Pooling 사용

- Command Pool 클래스

public class CommandPool<T> where T : ICommand, new()
{
    private Stack<T> _pool = new Stack<T>();

    public T Get()
    {
        if (_pool.Count > 0)
        {
            return _pool.Pop();
        }
        return new T();
    }

    public void Release(T command)
    {
        _pool.Push(command);
    }
}
  • T 형식으로 ICommand를 구현한 모든 클래스 타입을 받을 수 있도록 구현하였으며 뒤에 new()를 사용하여 매개 변수 없는 기본 생성자를 가져야 하도록 하였습니다.
  • Get(),Release()는 간단하게 풀을 가져오거나 반환하도록 하였으며 Stack이 비어있으면 새로운 객체를 생성하도록 하였습니다.

- Command 클래스

public class MoveCommand : ICommand
{
    private Transform _character;
    private Vector3 _previousPosition;
    private Vector3 _newPosition;

    public void Initialize(Transform character, Vector3 newPosition)
    {
        _character = character;
        _newPosition = newPosition;
    }

    public void Execute()
    {
        _previousPosition = _character.position;
        _character.position = _newPosition;
    }

    public void Undo()
    {
        _character.position = _previousPosition;
    }
}
  • 기존 생성자 함수는 주석처리 해주어야 합니다.
  • 기존 코드에서 Initialize(Transform,Vector3)을 추가해줍니다.

- InputHandler 클래스

public class InputHandler : MonoBehaviour
{
    private Stack<ICommand> _commandHistory = new Stack<ICommand>();
    private CommandPool<MoveCommand> _commandPool = new CommandPool<MoveCommand>();
    public Transform character;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.W))
        {
            Move(Vector3.up);
        }
        if (Input.GetKeyDown(KeyCode.S))
        {
            Move(Vector3.down);
        }
        if (Input.GetKeyDown(KeyCode.Z)) // Undo 명령
        {
            UndoCommand();
        }
    }

    void Move(Vector3 direction)
    {
        MoveCommand command = _commandPool.Get();
        command.Initialize(character, character.position + direction);
        command.Execute();
        _commandHistory.Push(command);
    }

    void UndoCommand()
    {
        if (_commandHistory.Count > 0)
        {
            ICommand lastCommand = _commandHistory.Pop();
            lastCommand.Undo();
            _commandPool.Release((MoveCommand)lastCommand);
        }
    }
}
  • New Command() 사용 부분이 없어지고 Initialize()로 대체되었으며
  • ExecuteCommand(ICommand) -> Move(Vector3)로 대체되었습니다.
  • 그 외에도 여러 방법이 있습니다..
  • ex) Command 재사용, 타이머를 걸어서 명령 실행 빈도를 조절 및 필터링을 걸어 이전 명령과 동일한 명령 기록하지 않도록 처리..

느낀 점

Ctrl + Z 키를 누른 것처럼 이전에 실행했던 행동으로 돌아가는게 신기하면서도 즐거웠습니다.
새로운 명령을 추가할 때에도 큰 어려움이 없을 것 같고 AI 행동 패턴에 적용하면 재밌을 것 같습니다. 이전 시간에 배운 Observer 패턴과 같이 조합하면 AI 설계에 굉장히 도움이 될 것 같습니다!

  • 제가 조사한 내용이 맞지 않거나 잘못 된 경우에 댓글로 잘못된 점 지적해주시면 감사합니다 ! (´._.`)
profile
Unity 게임 개발자를 준비하는 취업준비생입니다..

0개의 댓글