什麼是 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 特別適用於以下情況:

  1. 需要復原功能的應用:文字編輯器、圖像編輯軟體
  2. 需要記錄操作歷史遊戲重播、操作審計
  3. 需要排隊處理請求:網路請求、批次處理
  4. 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# 腳本技能、實作不同的資料結構、向量數學的學習與實作、精通物件池技術、四元數的清晰概念、物件導向程式設計精進。

文章標籤
全站熱搜
創作者介紹
創作者 傑克的遊戲宇宙 的頭像
傑克的遊戲宇宙

傑克淺談遊戲邏輯

傑克的遊戲宇宙 發表在 痞客邦 留言(0) 人氣(43)