Humility

아무리 노력해도 최고가 되지 못할 수 있다⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀그럼에도 노력하는자가 가장 겸손한 것 아닌가

공부하는 블로그

유니티/디자인패턴

[Unity] 유니티 C#) Command 패턴

새벽_글쓴이 2024. 12. 3. 10:25
반응형

커맨드(Command) 패턴

요청을 객체의 형태로 캡슐화하여 매개변수화하고, 요청을 큐에 저장하거나

로그로 기록하고, 작업 취소 등의 기능을 지원하는 디자인 패턴

 

커맨드 패턴의 핵심 구성 요소

Command (명령 인터페이스)

public interface ICommand
{
    void Execute();  // 실행 메서드
    void Undo();    // 실행 취소 메서드
}

 

Concrete Command (구체적인 명령)

public class JumpCommand : ICommand
{
    private Player player;  // Receiver

    public JumpCommand(Player player)
    {
        this.player = player;
    }

    public void Execute()
    {
        player.Jump();  // 실제 동작 수행
    }

    public void Undo()
    {
        player.ResetJump();
    }
}

 

Invoker (호출자)

public class InputHandler
{
    private ICommand command;

    public void SetCommand(ICommand command)
    {
        this.command = command;
    }

    public void ExecuteCommand()
    {
        command.Execute();
    }
}

 

Receiver (수신자)

public class Player
{
    public void Jump() { /* 실제 점프 동작 */ }
    public void ResetJump() { /* 점프 취소 */ }
}

 

구조적 특징

캡슐화

요청을 객체로 캡슐화하여 매개변수화 가능
동작과 수신자가 분리됨

 

확장성

새로운 커맨드를 쉽게 추가할 수 있음
기존 코드 수정 없이 새로운 기능 구현 가능

 

단일책임원칙

각 커맨드는 하나의 동작만 담당
실행과 실행 취소 로직이 한 곳에 모임

 

핵심개념

명령의 객체화

동작을 객체로 표현
매개변수로 전달 가능
저장 및 지연 실행 가능

 

의존성 분리

// 직접 호출
player.Jump();  // 강한 결합

// 커맨드 패턴 사용
ICommand jumpCommand = new JumpCommand(player);
inputHandler.SetCommand(jumpCommand);  // 느슨한 결합

 

실행 이력 관리

public class CommandHistory
{
    private Stack<ICommand> undoStack = new Stack<ICommand>();
    
    public void ExecuteCommand(ICommand command)
    {
        command.Execute();
        undoStack.Push(command);
    }

    public void Undo()
    {
        if (undoStack.Count > 0)
        {
            var command = undoStack.Pop();
            command.Undo();
        }
    }
}

 

주요 장점

 

  • 동작의 캡슐화
  • 실행 취소/다시 실행 기능 구현 용이
  • 매크로 커맨드 구현 가능
  • 동작의 지연 실행 가능

 

사용 사례

 

  • 게임 입력 시스템
  • 에디터 도구
  • UI 동작
  • 게임 리플레이 시스템

 

사용 예시

커맨드 인터페이스 정의

public interface ICommand
{
    void Execute();
    void Undo();
}

 

구체적인 커맨드 클래스들

public class MoveCommand : ICommand
{
    private Transform target;
    private Vector3 moveDirection;
    private float moveDistance;
    private Vector3 previousPosition;

    public MoveCommand(Transform target, Vector3 direction, float distance)
    {
        this.target = target;
        this.moveDirection = direction.normalized;
        this.moveDistance = distance;
    }

    public void Execute()
    {
        previousPosition = target.position;
        target.Translate(moveDirection * moveDistance);
    }

    public void Undo()
    {
        target.position = previousPosition;
    }
}

public class RotateCommand : ICommand
{
    private Transform target;
    private Vector3 rotation;
    private Quaternion previousRotation;

    public RotateCommand(Transform target, Vector3 rotation)
    {
        this.target = target;
        this.rotation = rotation;
    }

    public void Execute()
    {
        previousRotation = target.rotation;
        target.Rotate(rotation);
    }

    public void Undo()
    {
        target.rotation = previousRotation;
    }
}

public class ScaleCommand : ICommand
{
    private Transform target;
    private Vector3 scale;
    private Vector3 previousScale;

    public ScaleCommand(Transform target, Vector3 scale)
    {
        this.target = target;
        this.scale = scale;
    }

    public void Execute()
    {
        previousScale = target.localScale;
        target.localScale = scale;
    }

    public void Undo()
    {
        target.localScale = previousScale;
    }
}

 

커맨드 매니저 (실행취소 / 다시 실행 기능 포함)

public class CommandManager : MonoBehaviour
{
    private Stack<ICommand> undoStack = new Stack<ICommand>();
    private Stack<ICommand> redoStack = new Stack<ICommand>();

    public void ExecuteCommand(ICommand command)
    {
        command.Execute();
        undoStack.Push(command);
        redoStack.Clear(); // 새 커맨드가 실행되면 redo 스택을 비움
    }

    public void Undo()
    {
        if (undoStack.Count > 0)
        {
            ICommand command = undoStack.Pop();
            command.Undo();
            redoStack.Push(command);
        }
    }

    public void Redo()
    {
        if (redoStack.Count > 0)
        {
            ICommand command = redoStack.Pop();
            command.Execute();
            undoStack.Push(command);
        }
    }
}

 

입력 핸들러

public class InputHandler : MonoBehaviour
{
    private CommandManager commandManager;
    private Transform playerTransform;

    private void Start()
    {
        commandManager = GetComponent<CommandManager>();
        playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
    }

    private void Update()
    {
        // 이동 커맨드
        if (Input.GetKeyDown(KeyCode.W))
        {
            ICommand moveCommand = new MoveCommand(playerTransform, Vector3.forward, 1f);
            commandManager.ExecuteCommand(moveCommand);
        }

        // 회전 커맨드
        if (Input.GetKeyDown(KeyCode.R))
        {
            ICommand rotateCommand = new RotateCommand(playerTransform, new Vector3(0, 90, 0));
            commandManager.ExecuteCommand(rotateCommand);
        }

        // Undo/Redo
        if (Input.GetKeyDown(KeyCode.Z))
        {
            commandManager.Undo();
        }
        if (Input.GetKeyDown(KeyCode.Y))
        {
            commandManager.Redo();
        }
    }
}

 

사용 예시

// 커맨드 실행
ICommand moveCommand = new MoveCommand(playerTransform, Vector3.forward, 1f);
commandManager.ExecuteCommand(moveCommand);

// 실행 취소
commandManager.Undo();

// 다시 실행
commandManager.Redo();

 

확장 예시

public class MacroCommand : ICommand
{
    private List<ICommand> commands = new List<ICommand>();

    public void AddCommand(ICommand command)
    {
        commands.Add(command);
    }

    public void Execute()
    {
        foreach (var command in commands)
        {
            command.Execute();
        }
    }

    public void Undo()
    {
        for (int i = commands.Count - 1; i >= 0; i--)
        {
            commands[i].Undo();
        }
    }
}

 

결론

사용해야 하는 경우

 

  • 실행 취소/다시 실행(Undo/Redo) 기능이 필요할 때
  • 작업을 큐에 저장하고 나중에 실행해야 할 때
  • 동작의 기록이나 로깅이 필요할 때
  • 동작을 매개변수화하여 객체로 전달하고 싶을 때
  • 복잡한 동작을 간단한 동작들로 분리하고 싶을 때

 

주요 장점

 

  • 단일 책임 원칙 준수: 각 커맨드는 하나의 동작만 담당
  • 높은 확장성: 새로운 커맨드를 쉽게 추가 가능
  • 유연한 실행 취소 기능: 각 동작의 실행 취소 로직을 캡슐화
  • 동작의 조합이 용이: 매크로 커맨드를 통한 복잡한 동작 구현 가능

 

주의사항

 

  • 커맨드가 많아질수록 클래스 수가 증가
  • 간단한 동작에 대해서는 과도한 설계가 될 수 있음
  • 메모리 사용량 증가 가능성 (특히 실행 취소 기능 구현 시)

 

커맨드 패턴은 복잡한 동작을 관리하고, 실행 취소 기능이 필요하거나,
동작을 객체로 다루어야 할 때 매우 유용한 디자인 패턴이다
반응형