Unity公式が公開しているプログラミングパターンを読み取ってみた！～その9. Command～

# はじめに
Unity公式がプログラミングパターンのデモを説明書と共に全12種公開していたので、1つずつ読み取っていきたいと思います！
英語の説明書でしたので間違った解釈をしているかもしれませんが、ご了承下さい！
最初の5種は、SOLID原則に沿ったデモとなっており、コードのみが公開されています。
残り7種は、Scene付きのデモが入っていますので、ZIPファイルをダウンロードして試してみてください。
今回は、9つ目のCommandについてです。

https://github.com/Unity-Technologies/game-programming-patterns-demo

https://resources.unity.com/games/level-up-your-code-with-game-programming-patterns

# 環境
OS : Windows10
Unity バージョン : 2021.3.8f1

# Commandパターンとは
Commandパターンとは要求自体をオブジェクトにして、それらを複数組み合わせて使えるようにするパターンです。要求をオブジェクトにしているため、履歴管理が容易になるという特性から、ゲームでは入力履歴を保持する部分でよく使用されるそうです。

# デモ
デモは以下のような動作になっています。
![ProgrammingPatternsDemo_-_CommandPattern_-_Windows__Mac__Linux_-_Unity_2021_3_8f1_Personal__DX11__2023-02-25_12-56-23_Trim_AdobeExpress (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368729/da199033-57a5-2189-1b38-782f21b2ef0d.gif)
押したボタンの方向にPマークが軌跡を作りながら移動します。
undoボタンは履歴上の1つ前の動作を取り消し、redoボタンは履歴上の1つ次の動作を行うようになっています。ブラウザの「戻る」と「進む」ボタンの挙動と一緒だとイメージしていただければ分かりやすいです。

Hierarchy内には入力情報を受け取るInputManagerがアタッチされています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368729/b9ddeb14-ce4e-a7c4-2371-7bac6039185a.png)

InputManagerには、各ボタンを押したときの処理が書かれています。
方向ボタンを押したときは、要求(どの方向に動かすか)をオブジェクト化しています。
要求をオブジェクト化した後に直ぐに実行してもいいのですが、undo, redoボタンのために履歴管理をする必要があります。そのため一度CommandInvokerを介して履歴を作っています。
```C#
public class InputManager : MonoBehaviour
{
    [Header("Button Controls")]
    [SerializeField] Button forwardButton;
    [SerializeField] Button backButton;
    [SerializeField] Button leftButton;
    [SerializeField] Button rightButton;
    [SerializeField] Button undoButton;
    [SerializeField] Button redoButton;

    [SerializeField] private PlayerMover player;

    private void Start()
    {
        // ボタン押したときの処理を登録
        forwardButton.onClick.AddListener(OnForwardInput);
        backButton.onClick.AddListener(OnBackInput);
        rightButton.onClick.AddListener(OnRightInput);
        leftButton.onClick.AddListener(OnLeftInput);
        undoButton.onClick.AddListener(OnUndoInput);
        redoButton.onClick.AddListener(OnRedoInput);
    }

    private void RunPlayerCommand(PlayerMover playerMover, Vector3 movement)
    {
        if (playerMover == null)
        {
            return;
        }
        // 移動する方向が動けるかどうか。壁があるとfalse
        if (playerMover.IsValidMove(movement))
        {
            // 要求(どの方向に動かすか)をオブジェクト化
            ICommand command = new MoveCommand(playerMover, movement);

            // ここで要求をそのまま実行することはできるが、履歴管理したいためCommandInvokerを介す
            CommandInvoker.ExecuteCommand(command);
        }
    }

    private void OnLeftInput()
    {
        RunPlayerCommand(player, Vector3.left);
    }

    private void OnRightInput()
    {
        RunPlayerCommand(player, Vector3.right);
    }

    private void OnForwardInput()
    {
        RunPlayerCommand(player, Vector3.forward);
    }

    private void OnBackInput()
    {
        RunPlayerCommand(player, Vector3.back);
    }

    private void OnUndoInput()
    {
        CommandInvoker.UndoCommand();
    }

    private void OnRedoInput()
    {
        CommandInvoker.RedoCommand();
    }
}
```
要求をオブジェクト化するMoveCommandは以下のようになっています。
ICommandをインターフェースとして持っていて、Execute、Undoがオブジェクト化されています。
```C#
public class MoveCommand : ICommand
{
    private PlayerMover _playerMover;
    private Vector3 _movement;

    public MoveCommand(PlayerMover player, Vector3 moveVector)
    {
        this._playerMover = player;
        this._movement = moveVector;
    }

    public void Execute()
    {
        // 軌跡を作成する処理
        _playerMover?.PlayerPath.AddToPath(_playerMover.transform.position + _movement);

        // 動かす処理
        _playerMover.Move(_movement);
    }
    
    public void Undo()
    {
        // 反対向きに動かす処理
        _playerMover.Move(-_movement);

        // 軌跡を削除する処理
        _playerMover?.PlayerPath.RemoveFromPath(); 
    }
}

public interface ICommand
{
    // オブジェクト化される要求
    public void Execute();
    public void Undo();
}
```
履歴管理をするために作成されたCommandInvokerは次のようになります。
ExecuteCommand内では、オブジェクト化した要求を実行する処理と_undoStackに履歴を作る処理が書かれています。また、オブジェクト化した要求を実行後は、redoすることができないため_redoStack内の履歴は削除されます。
UndoCommandは、undoボタンが押された時の処理が書かれています。_undoStackの履歴内にある先頭の要求を取得し、_redoStack内に入れます。履歴管理が終わると取得した要求を実行しています。
RedoCommandは、redoボタンが押された時の処理が書かれています。UndoCommandと真逆のことを行っていて、_redoStackの履歴内にある先頭の要求を取得し、_undoStack内に入れます。履歴管理が終わると取得した要求を実行しています。
```C#
public class CommandInvoker
{
    private static Stack<ICommand> _undoStack = new Stack<ICommand>();

    private static Stack<ICommand> _redoStack = new Stack<ICommand>();

    public static void ExecuteCommand(ICommand command)
    {
        // オブジェクト化した要求をここで実行
        command.Execute();
        // _undoStackにオブジェクト化した要求をStackしておく
        _undoStack.Push(command);
        // _redoStackにStackしてあったオブジェクト化した要求は削除
        _redoStack.Clear();
    }

    public static void UndoCommand()
    {
        if (_undoStack.Count > 0)
        {
            // _undoStackの先頭のオブジェクト化された要求を取得
            ICommand activeCommand = _undoStack.Pop();
            // 取得したオブジェクト化された要求を_redoStackにStackしておく
            _redoStack.Push(activeCommand);
            // 取得したオブジェクト化された要求をここで実行
            activeCommand.Undo();
        }
    }

    public static void RedoCommand()
    {
        if (_redoStack.Count > 0)
        {
            // _redoStackの先頭のオブジェクト化された要求を取得
            ICommand activeCommand = _redoStack.Pop();
            // 取得したオブジェクト化された要求を_undoStackにStackしておく
            _undoStack.Push(activeCommand);
            // 取得したオブジェクト化された要求をここで実行
            activeCommand.Execute();
        }
    }
}
```

# まとめ
Commandは、要求をオブジェクト化し、複数組み合わせて使えるようにするパターンです。今回のデモでは、ExecuteとUndoの2パターンしか要求はありませんでしたが、例えば、特定のボタンを連続してクリックすると、コンボの動きなども実装することが容易になります。
ただし、どこまで要求をオブジェクト化させるかを検討しないと構造が複雑になる可能性があります。