へっぽこ日記

Unity関係のつぶやきまとめ。マイコンとかもいじります。

Commandパターンを試す

はじめに

Commandパターンを試す記事です。 以前から存在は知っていたのですが、実際に試したことはなかったので、実際に試してみました。

サンプルプロジェクト github.com

そもそもCommandパターンとは

処理の呼び出しと実装側を分けることを目的としたデザインパターンで、処理で呼び出される命令をオブジェクト単位で分けて実装するとのこと。オブジェクト単位で管理することで、UndoやRedoが実現しやすくなるみたいです。

登場する役割としては以下のものがあります

名前 役割
Command 命令(オブジェクト単位)
Receiver 依頼を受け取り、実行される。処理対象
Invoker 命令を実行する
Client 命令呼び出しを依頼

流れとしては、ClientがCommandをNew()して、CommandをInvokerに渡し、Invokerの実行メソッドを呼ぶことでInvokerにCommandを依頼します。Invokerでは、渡されたCommandオブジェクトをスタックにあげて管理します。その後に、Commandの実行関数を呼び、Receiverに定義されたメソッドを呼ばれることでClientがしたい事を実現する と云った感じです。

上記で色々言いましたが、Commandパターンを実装する方法は様々なようで、Wikipediaでは

Commandが内包する処理の記述は開発者に一任されている。全ての処理をCommand内に記述するパターン[4]、あるいはcommandインスタンス生成時に実行対?象(Receiver)を受け取りcommand.Execute()時にreceiver.action()コールのみをおこなう、すなわち実行をReceiverに委譲してCommandは繋ぎに徹するパターン もある[5]。尚 Receiver は Commandパターンの要件ではない。

Command パターン - Wikipedia とあるように、色々あります。次で説明するサンプルは、Receiverを委譲してCommandはつなぎに徹するパターンで制作しました。

実際に試してみる

今回は、公式が出しているサンプルを基に以下の物を作りました。移動とRedoができるサンプルです。

ICommand

ここでCommandの命名メソッドを定義しています。

public interface ICommand
{
   void Execute();

   void Undo();
}

IReceiver

public interface IReceiver
{
   void Move(Vector3 pos);
}

Client

ここでボタンとLeftMove(),RightMove(),Redo()を結び付けて、右・左どちらの方向に移動したいのかを指示できるようにしています。

public class Client : MonoBehaviour
{
    [SerializeField] private Button _rightMoveButton;
    [SerializeField] private Button _leftMoveButton;
    [SerializeField] private Button _undoButton;

    [SerializeField] private PlayerReceiver _receiver;
    
    private void Start()
    {
        _rightMoveButton.onClick.AddListener(LeftMove);
        _leftMoveButton.onClick.AddListener(RightMove);
        _undoButton.onClick.AddListener(Undo);
    }

    private void LeftMove()
    {
        ICommand command = new MoveCommand(_receiver,Vector3.left);
        Invoker.Execute(command);
    }

    private void RightMove()
    {
        ICommand command = new MoveCommand(_receiver,Vector3.right);
        Invoker.Execute(command);
    }

    private void Undo()
    {
        Invoker.Undo();
    }
}

Invoker

ここで、命令を実行しています。Stackを使ってCommandをまとめて管理しており、このStackを使ってRedoの実装を可能にしています。

public class Invoker
{
    private static Stack<ICommand> _commands = new Stack<ICommand>();
    
    public static void Execute(ICommand command)
    {
        command.Execute();
        _commands.Push(command);
    }

    public static void Undo()
    {
        if (_commands.Count > 0)
        {
            ICommand command = _commands.Pop();
            command.Undo();
        }
    }
}

PlayerReceiver

ここでIReceiverを実装しています。Clientから受けた指示を基に、命令を実行します。

public class PlayerReceiver : MonoBehaviour,IReceiver
{
    public void Move(Vector3 pos)
    {
        transform.position += pos;
    }
}

MoveCommand

ここでICommandを実装していています。あくまでReceiverを繋ぐことに徹しています。

public class MoveCommand : ICommand
{
    private PlayerReceiver _receiver;
    private Vector3 _pos;

    public MoveCommand(PlayerReceiver receiver,Vector3 pos)
    {
        _receiver = receiver;
        _pos = pos;
    }

    public void Execute()
    {
        _receiver.Move(_pos);
    }

    public void Undo()
    {
        _receiver.Move(-_pos);
    }
}

おわりに

Commandパターンは、パズルゲームやリバーシで使われているそうで、今回は実装しませんでしたが、Undo機能も追加出来たりと色々できます。

是非、読者登録をしていただくと助かります!