개인 공부/디자인패턴

디자인패턴 커맨드 & 유니티 커스텀 에디터

smallship 2024. 7. 16. 17:51

커맨드 패턴은 객체지향 디자인 패턴으로, 요청을 객체로 캡슐화하여 매개변수화된 메서드 호출, 메서드 실행을 취소할 수 있는 기능을 지원합니다. 캡슐화를 통해 재사용성을 높이고 클래스간의 의존성을 제거한다.

이 패턴은 요청을 발생시키는 호출자(Invoker), 명령을 받아서 수행하는 수신자(Receiver), 요청을 캡슐화하여 저장하는 커맨드(Command), 명령을 구현하는 구상 커맨드(ConcreteCommand), 구상 커맨드 객체를 만들고 설정하는 클라이언트(Client)로 구성된다.

 

  • 호출자 (Invoker):
    • 요청을 발생시키는 객체.
    • 커맨드 객체를 생성하고 실행하는 역할.
  • 수신자 (Receiver):
    • 실제로 요청을 처리하는 객체.
    • 커맨드 객체가 호출될 때 수행될 동작을 구현.
  • 커맨드 (Command):
    • 요청을 캡슐화하는 인터페이스.
    • 실행할 메서드(Execute())와 필요한 경우 실행 취소할 메서드(Undo())를 정의.
  • 구상 커맨드 (ConcreteCommand):
    • 실제로 수신자 객체의 메서드를 호출하는 구체적인 커맨드 클래스..
    • Command 인터페이스를 구현하며, 수신자 객체와 실행할 메서드를 포함.
  • 클라이언트 (Client):
    • 커맨드 객체를 생성하고, 호출자에게 제공하여 실행.
    • 일반적으로 호출자와 수신자 객체를 생성하고, 구상 커맨드 객체를 만들어 호출자에게 전달.

 

 

커맨드 패턴의 구조

 


유니티에서 직접 활용해보자.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface ICommand
{
    void SetOwner(GameObject go);
    void Excute();
    void Undo();

    void Redo();
}

public abstract class Command : ICommand
{
    protected GameObject Owner;

    public void SetOwner(GameObject go)
    {
        Owner = go;
    }
    public abstract void Excute();
    public abstract void Undo();

    public abstract void Redo();
}

public class MoveCommandX : Command
{
    private Vector3 movePosition = new Vector3(10, 0, 0);
    
    public override void Excute()
    {
        Owner.transform.position += movePosition;
    }

    public override void Undo()
    {
        Owner.transform.position += -movePosition;
    }

    public override void Redo()
    {
        Owner.transform.position += movePosition;
    }
}
public class MoveCommandY : Command
{
    private Vector3 movePosition = new Vector3(0, 10, 0);
    
    public override void Excute()
    {
        Owner.transform.position += movePosition;
    }

    public override void Undo()
    {
        Owner.transform.position += -movePosition;
    }
    
    public override void Redo()
    {
        Owner.transform.position += movePosition;
    }
}

public class MoveCommandZ : Command
{
    private Vector3 movePosition = new Vector3(0, 0, 10);
    
    public override void Excute()
    {
        Owner.transform.position += movePosition;
    }

    public override void Undo()
    {
        Owner.transform.position += -movePosition;
    }
    
    public override void Redo()
    {
        Owner.transform.position += movePosition;
    }
}

public class RotateCommandX : Command
{
    private float rotateAmount = 10f; 

    public override void Excute()
    {
        Owner.transform.Rotate(Vector3.right, rotateAmount);
    }

    public override void Undo()
    {
        Owner.transform.Rotate(Vector3.right, -rotateAmount);
    }

    public override void Redo()
    {
        Owner.transform.Rotate(Vector3.right, rotateAmount);
    }
}

public class RotateCommandY : Command
{
    private float rotateAmount = 10f; 

    public override void Excute()
    {
        Owner.transform.Rotate(Vector3.up, rotateAmount);
    }

    public override void Undo()
    {
        Owner.transform.Rotate(Vector3.up, -rotateAmount);
    }

    public override void Redo()
    {
        Owner.transform.Rotate(Vector3.up, rotateAmount);
    }
}

public class RotateCommandZ : Command
{
    private float rotateAmount = 10f; 

    public override void Excute()
    {
        Owner.transform.Rotate(Vector3.forward, rotateAmount);
    }

    public override void Undo()
    {
        Owner.transform.Rotate(Vector3.forward, -rotateAmount);
    }

    public override void Redo()
    {
        Owner.transform.Rotate(Vector3.forward, rotateAmount);
    }
}


public class CommandManager
{
    public GameObject Owner;

    private Stack<ICommand> History = new Stack<ICommand>();
    private Stack<ICommand> RedoHistory = new Stack<ICommand>();

    public void Excute(ICommand command)
    {
        command.SetOwner(Owner);
        command.Excute();
        History.Push(command);
        RedoHistory.Clear(); // 새로운 명령이 실행되면 Redo 기록을 초기화
    }

    public void Undo()
    {
        if (History.Count > 0)
        {
            ICommand commandToUndo = History.Pop();
            commandToUndo.Undo();
            RedoHistory.Push(commandToUndo); // Undo된 명령을 Redo 기록에 추가
        }
    }

    public void Redo()
    {
        if (RedoHistory.Count > 0)
        {
            ICommand commandToRedo = RedoHistory.Pop();
            commandToRedo.Excute(); // Redo 기록에 있는 명령을 다시 실행
            History.Push(commandToRedo); // 실행된 명령을 다시 History에 추가
        }
    }
}

public class CommandPattern : MonoBehaviour
{
    private CommandManager _commandManager = new();
 
    void Start()
    {
        _commandManager.Owner = this.gameObject;
    }
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            _commandManager.Excute(new MoveCommandX());
        }
        
        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            _commandManager.Excute(new MoveCommandY());
        }

        if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            _commandManager.Excute(new MoveCommandZ());
        }
        
        if (Input.GetKeyDown(KeyCode.Alpha4))
        {
            _commandManager.Excute(new RotateCommandX());
        }
        
        if (Input.GetKeyDown(KeyCode.Alpha5))
        {
            _commandManager.Excute(new RotateCommandY());
        }
        
        if (Input.GetKeyDown(KeyCode.Alpha6))
        {
            _commandManager.Excute(new RotateCommandZ());
        }
        
        if (Input.GetKeyDown(KeyCode.U))
        {
            _commandManager.Undo();
        }

        if (Input.GetKeyDown(KeyCode.R))
        {
            _commandManager.Redo();
        }
    }
}

 

 

 

  • 인터페이스 및 추상 클래스: ICommand 인터페이스와 Command 추상 클래스는 명령 객체들이 구현해야 하는 메서드들을 정의한다. SetOwner, Excute, Undo, Redo 메서드들이 각각 필요한 동작을 정의하고 있다.
  • 구체적인 명령 클래스: MoveCommandX, MoveCommandY, MoveCommandZ, RotateCommandX, RotateCommandY, RotateCommandZ는 Command 추상 클래스를 상속받아 각각의 특정 동작(Excute, Undo, Redo)을 구현한다.
  • CommandManager 클래스: CommandManager 클래스는 ICommand 객체들을 관리하고, 실행(Excute), 되돌리기(Undo), 다시 실행(Redo)하는 기능을 구현한다. Stack을 사용하여 실행한 명령들과 되돌린 명령들을 관리한다.
  • CommandPattern 클래스: CommandPattern 클래스는 CommandManager 객체를 생성하고, Update 메서드에서 특정 키 입력(KeyCode.Alpha1, KeyCode.Alpha2 등)에 따라 다양한 명령 객체들을 생성하여 CommandManager에 전달한다.
  • Owner 할당: CommandPattern에서 _commandManager.Owner = this.gameObject;를 통해 명령들이 실행될 대상이 되는 게임 오브젝트를 설정하고 있다.

게임 오브젝트를 생성하고 스크립트를 할당해주었다. 키 입력에 따라 이동 회전을 하고 undo와 redo 또한 잘 작동한다.

 

 


커스텀 에디터를 만들어서 해당 기능을 구현해보자.

using UnityEditor;
using UnityEngine;

public class EditorTool : EditorWindow
{
    public GameObject cubeObject;
    public CommandManager _commandManager;

    [MenuItem("Window/Custom Editor Tool")] // 메뉴에 추가될 경로 설정
    public static void ShowWindow()
    {
        GetWindow<EditorTool>("Editor Tool"); // 에디터 창을 열고 타이틀을 설정
    }

    void OnEnable()
    {
        _commandManager = new CommandManager();
    }

    void OnGUI()
    {
        GUILayout.Label("Custom Editor Tool", EditorStyles.boldLabel);

        cubeObject = EditorGUILayout.ObjectField("Cube Object", cubeObject, typeof(GameObject), true) as GameObject;

        // 버튼들을 생성합니다.
        if (GUILayout.Button("Move Command X"))
        {
            if (_commandManager != null && cubeObject != null)
            {
                _commandManager.Owner = cubeObject;
                _commandManager.Excute(new MoveCommandX());
            }
            else
            {
                Debug.LogWarning("CommandManager or cubeObject is not assigned.");
            }
        }

        if (GUILayout.Button("Move Command Y"))
        {
            if (_commandManager != null && cubeObject != null)
            {
                _commandManager.Owner = cubeObject;
                _commandManager.Excute(new MoveCommandY());
            }
            else
            {
                Debug.LogWarning("CommandManager or cubeObject is not assigned.");
            }
        }

        if (GUILayout.Button("Move Command Z"))
        {
            if (_commandManager != null && cubeObject != null)
            {
                _commandManager.Owner = cubeObject;
                _commandManager.Excute(new MoveCommandZ());
            }
            else
            {
                Debug.LogWarning("CommandManager or cubeObject is not assigned.");
            }
        }

        if (GUILayout.Button("Rotate Command X"))
        {
            if (_commandManager != null && cubeObject != null)
            {
                _commandManager.Owner = cubeObject;
                _commandManager.Excute(new RotateCommandX());
            }
            else
            {
                Debug.LogWarning("CommandManager or cubeObject is not assigned.");
            }
        }

        if (GUILayout.Button("Rotate Command Y"))
        {
            if (_commandManager != null && cubeObject != null)
            {
                _commandManager.Owner = cubeObject;
                _commandManager.Excute(new RotateCommandY());
            }
            else
            {
                Debug.LogWarning("CommandManager or cubeObject is not assigned.");
            }
        }

        if (GUILayout.Button("Rotate Command Z"))
        {
            if (_commandManager != null && cubeObject != null)
            {
                _commandManager.Owner = cubeObject;
                _commandManager.Excute(new RotateCommandZ());
            }
            else
            {
                Debug.LogWarning("CommandManager or cubeObject is not assigned.");
            }
        }

        if (GUILayout.Button("Undo"))
        {
            if (_commandManager != null)
            {
                _commandManager.Undo();
            }
            else
            {
                Debug.LogWarning("CommandManager is not assigned.");
            }
        }

        if (GUILayout.Button("Redo"))
        {
            if (_commandManager != null)
            {
                _commandManager.Redo();
            }
            else
            {
                Debug.LogWarning("CommandManager is not assigned.");
            }
        }
    }
}

 

스크립트를 작성해준다.

 

 

스크립트는 Editor 폴더를 만들어서 그 안에서 작성해야 한다.

Window 탭에 Custom Ediotr Tool이 생겼다.

 

에디터가 정상적으로 실행되는것을 확인할 수 있다.

 

Cube 오브젝트를 할당해주고 실행, 잘 작동하는것을 확인할 수 있다.


 

유니티에서 커맨드를 활용하여 직접 실행(Execute) 취소(Undo) 재실행(Redo)을 구현해보았다. 또한 커스텀 에디터를 만들어 해당 기능들을 연동시켰다. 커스텀 에디터는 프로젝트 관리에 용이하고 또한 사용자 정의 인터페이스를 만들 수 있어 상황에 따라 사용하면 좋을것같다.

'개인 공부 > 디자인패턴' 카테고리의 다른 글

유니티 옵저버  (1) 2024.07.19
유니티 FSM  (0) 2024.07.17
디자인패턴 책임 연쇄 패턴 (Chain of Responsibility)  (0) 2024.07.16
디자인패턴 컴포지트  (0) 2024.07.15
디자인패턴 프록시  (0) 2024.07.15