Command Pattern

命令模式(Command Pattern):當你想要追蹤一系列特定動作時,可以使用命令模式(command pattern)。如果你玩過帶有撤銷(undo)/重做(redo)功能或是能夠在列表中保留輸入歷史的遊戲,你很可能已經見過命令模式。想像一個策略遊戲,玩家可以在實際執行之前計劃多個動作,這就是命令模式。

在設計模式中,命令模式有以下角色參與其中

  • Command:抽象命令,可能為一個介面(Interface)或是抽象類(Abstract Class),例如ICommand
  • ConcreteCommand:具體命令,即實作抽象命令的Class
  • Invoker:命令的調用者,發起者,管理者
  • Receiver:接收者,實際執行命令者,被Command訪問與操作
  • 使用命令的客戶端(Client)

命令模式特點:

  • 命令模式不直接呼叫方法(method)而是會將單個或多個方法封裝為命令物件(command object)。
  • 命令物件會存放到一個容器(像是Stack或是Queue)中,讓你可以在想要的時間點去執行或是撤銷這些命令物件。
  • 新增命令不會影響其他地方,符合了開閉原則(Open-closed principle)
  • 除了撤銷(undo)/重做(redo)功能,也可以用來實現回放(Play back)功能

接收者(Receiver)類:PlayerMover,在命令物件執行時真正執行動作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PlayerMover : MonoBehaviour
{
[SerializeField] private LayerMask obstacleLayer;

private const float boardSpacing = 1f;
private PlayerPath playerPath;
public PlayerPath PlayerPath => playerPath;

private void Start()
{
playerPath = gameObject.GetComponent<PlayerPath>();
}

public void Move(Vector3 movement)
{
Vector3 destination = transform.position + movement;
transform.position = destination;
}

public bool IsValidMove(Vector3 movement)
{
return !Physics.Raycast(transform.position, movement, boardSpacing, obstacleLayer);
}
}

定義一個抽象命令介面(ICommand)

1
2
3
4
5
public interface ICommand
{
void Execute();
void Undo();
}

定義一個具體命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();
}
}

定義一個使用命令的命令调用者類CommandInvoker,它只負責執行(ExecuteCommand())和撤銷(UndoCommand())命令。

  • 它含有一個容器(undoStack),用來儲存一系列的命令。
  • 當執行命令時,從容器中取出一個命令物件執行方法(Execute)
  • 當撤消命令時,從容器中取出一個命令物件,並執行撤銷方法(Undo)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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.Push(command);
    _redoStack.Clear();
    }

    public static void UndoCommand()
    {
    if (_undoStack.Count > 0)
    {
    ICommand activeCommand = _undoStack.Pop();
    _redoStack.Push(activeCommand);
    activeCommand.Undo();
    }
    }

    public static void RedoCommand()
    {
    if (_redoStack.Count > 0)
    {
    ICommand activeCommand = _redoStack.Pop();
    _undoStack.Push(activeCommand);
    activeCommand.Execute();
    }
    }
    }

使用命令的客戶端(Client)類,InputManager,它透過CommandInvoker執行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
if (playerMover.IsValidMove(movement))
{
ICommand command = new MoveCommand(playerMover, movement);
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(); }
}

Reference: https://github.com/Unity-Technologies/game-programming-patterns-demo/tree/main/Assets/9%20Command

評論