什麼是 Command Pattern?
Command Pattern(命令模式)是一種行為設計模式,它將請求封裝成物件,讓你可以用不同的請求來參數化客戶端、將請求排入佇列,並支援復原操作。
簡單來說,就是把「做什麼事」包裝成一個物件,而不是直接調用方法。
為什麼需要 Command Pattern?
想像你在玩老虎機遊戲,每次按下「旋轉」按鈕時,遊戲需要:
- 扣除下注金額
- 開始轉輪動畫
- 計算結果
- 更新UI顯示
傳統做法可能是這樣:
public void OnSpinButtonClick()
{
slot.DeductCredits();
slot.StartSpinAnimation();
slot.CalculateResults();
ui.UpdateDisplay();
}
這種做法的問題是什麼?緊密耦合。按鈕直接知道所有需要執行的細節,當需求變更時,修改會變得複雜。
Command Pattern 的解決方案
Command Pattern 把這個「旋轉老虎機」的動作包裝成一個命令物件:
// 命令介面
public interface ICommand
{
void Execute();
void Undo();
}
// 具體的旋轉命令
public class SpinCommand : ICommand
{
private readonly Slot slot;
private readonly bool freeSpin;
private int previousCredits;
public SpinCommand(Slot slot, bool freeSpin = false)
{
this.slot = slot;
this.freeSpin = freeSpin;
}
public void Execute()
{
previousCredits = slot.refs.credits.credits;
slot.spin(freeSpin);
}
public void Undo()
{
slot.refs.credits.setCredits(previousCredits);
slot.setState(SlotState.ready);
}
}
現在按鈕只需要:
public void OnSpinButtonClick()
{
var spinCommand = new SpinCommand(slot);
commandController.ExecuteCommand(spinCommand);
}
實際應用:老虎機的完整範例
讓我們看看如何在老虎機遊戲中完整實現 Command Pattern:
1. 定義不同類型的命令
// 改變下注金額命令
public class ChangeBetCommand : ICommand
{
private readonly SlotCredits credits;
private readonly bool increase;
private int previousBet;
public ChangeBetCommand(SlotCredits credits, bool increase)
{
this.credits = credits;
this.increase = increase;
}
public void Execute()
{
previousBet = credits.betPerLine;
credits.ChangeBetPerLine(increase);
}
public void Undo()
{
credits.betPerLine = previousBet;
}
}
// 改變下注線數命令
public class ChangeLinesCommand : ICommand
{
private readonly SlotCredits credits;
private readonly bool increase;
private int previousLines;
public ChangeLinesCommand(SlotCredits credits, bool increase)
{
this.credits = credits;
this.increase = increase;
}
public void Execute()
{
previousLines = credits.linesPlayed;
credits.changeLinesCountPlayed(increase);
}
public void Undo()
{
credits.linesPlayed = previousLines;
}
}
2. 建立命令控制器
public class SlotCommandController
{
private readonly Stack<ICommand> commandHistory = new Stack<ICommand>();
private readonly Queue<ICommand> commandQueue = new Queue<ICommand>();
// 執行命令並記錄歷史
public void ExecuteCommand(ICommand command)
{
command.Execute();
commandHistory.Push(command);
// 限制歷史記錄數量,避免記憶體洩漏
if (commandHistory.Count > 50)
{
var oldCommands = new Stack<ICommand>();
for (int i = 0; i < 50; i++)
{
oldCommands.Push(commandHistory.Pop());
}
commandHistory.Clear();
while (oldCommands.Count > 0)
{
commandHistory.Push(oldCommands.Pop());
}
}
}
// 復原上一個命令
public void UndoLastCommand()
{
if (commandHistory.Count > 0)
{
var lastCommand = commandHistory.Pop();
lastCommand.Undo();
}
}
// 排隊執行命令(用於自動遊戲)
public void QueueCommand(ICommand command)
{
commandQueue.Enqueue(command);
}
// 執行排隊的命令
public void ProcessQueuedCommands()
{
while (commandQueue.Count > 0)
{
var command = commandQueue.Dequeue();
ExecuteCommand(command);
}
}
}
3. 在遊戲中使用
public class SlotGUI : MonoBehaviour
{
[SerializeField] private Slot slot;
private SlotCommandController commandController;
void Start()
{
commandController = new SlotCommandController();
}
public void BetPlus_Click()
{
var command = new ChangeBetCommand(slot.refs.credits, true);
commandController.ExecuteCommand(command);
}
public void BetMinus_Click()
{
var command = new ChangeBetCommand(slot.refs.credits, false);
commandController.ExecuteCommand(command);
}
public void OnSpinButtonClick()
{
var command = new SpinCommand(slot);
commandController.ExecuteCommand(command);
}
public void OnUndoButtonClick()
{
commandController.UndoLastCommand();
}
}
Command Pattern 的優勢
1. 解耦合(Decoupling)
UI 元件不需要知道具體的業務邏輯細節,只需要建立對應的命令物件。
2. 可復原性(Undo/Redo)
每個命令都可以實現 Undo() 方法,輕鬆實現復原功能。
3. 可記錄性(Logging)
所有的操作都變成了物件,可以輕鬆記錄使用者行為:
public class LoggingCommandController : SlotCommandController
{
public override void ExecuteCommand(ICommand command)
{
Debug.Log($"Executing command: {command.GetType().Name} at {DateTime.Now}");
base.ExecuteCommand(command);
}
}
4. 可排隊執行(Queuing)
命令可以被排入佇列,用於實現自動遊戲功能:
public void StartAutoSpin(int count)
{
for (int i = 0; i < count; i++)
{
commandController.QueueCommand(new SpinCommand(slot));
}
commandController.ProcessQueuedCommands();
}
5. 巨集命令(Macro Commands)
可以將多個命令組合成一個複雜的操作:
public class MaxBetCommand : ICommand
{
private readonly List<ICommand> commands = new List<ICommand>();
public MaxBetCommand(SlotCredits credits)
{
commands.Add(new ChangeBetCommand(credits, true)); // 最大下注
commands.Add(new ChangeLinesCommand(credits, true)); // 最大線數
}
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();
}
}
}
何時使用 Command Pattern?
Command Pattern 特別適用於以下情況:
- 需要復原功能的應用:文字編輯器、圖像編輯軟體
- 需要記錄操作歷史:遊戲重播、操作審計
- 需要排隊處理請求:網路請求、批次處理
- UI 與業務邏輯解耦:複雜的使用者介面
注意事項
記憶體管理
過度使用 Command Pattern 可能會產生大量的小物件,需要注意記憶體使用:
// 使用物件池重複利用命令物件
public class CommandPool
{
private readonly Queue<SpinCommand> spinCommands = new Queue<SpinCommand>();
public SpinCommand GetSpinCommand(Slot slot, bool freeSpin = false)
{
if (spinCommands.Count > 0)
{
var command = spinCommands.Dequeue();
command.Reset(slot, freeSpin);
return command;
}
return new SpinCommand(slot, freeSpin);
}
public void ReturnSpinCommand(SpinCommand command)
{
spinCommands.Enqueue(command);
}
}
複雜度控制
不要為了使用 Command Pattern 而過度設計。簡單的操作可能不需要包裝成命令。
總結
Command Pattern 是一個強大的設計模式,它通過將請求封裝成物件,提供了極大的靈活性。在老虎機遊戲這樣的複雜系統中,它可以幫助我們:
- 建立更清晰的程式架構
- 實現復原功能
- 記錄和重播使用者操作
- 降低系統各部分之間的耦合度
當你的系統需要處理複雜的使用者互動,或者需要提供復原、記錄等進階功能時,Command Pattern 是一個值得考慮的解決方案。筆者我認為,針對老虎機遊戲前端開發而言,Command Pattern (命令模式)的應用不一定有其必要性,但如果牽扯到需要重播歷史遊戲畫面,那Command Pattern 就能派上用場。
記住,設計模式不是萬能藥,而是解決特定問題的工具。選擇合適的工具來解決合適的問題,才是優秀程式設計師的標誌。
🚀 進階學習資源
如果想深入學習更多遊戲開發的設計模式和架構技巧,強烈推薦以下兩門課程,可以幫助你/妳成為更出色的軟體工程師:
📖 Programming Design Patterns For Unity: Write Better Code
這門課程深入講解在 Unity 開發中最實用的設計模式,包括觀察者模式的進階應用、單例模式的正確使用方式、工廠模式在遊戲物件建立中的應用、狀態機模式在遊戲邏輯中的實作。每個模式都有完整實戰案例和重構範例,非常適合想提升程式碼品質的 Unity 開發者。
📖 Unity C# Scripting Intermediate - Upgrade Your C# Skills
這門課程專注於提升 C# 程式設計技能,幫你掌握中級開發者必備技能:升級 C# 腳本技能、實作不同的資料結構、向量數學的學習與實作、精通物件池技術、四元數的清晰概念、物件導向程式設計精進。
