在博弈遊戲業的三年裡,我發現有三個設計模式特別適合Unity開發:Observer解決通訊問題,Singleton管理全域資源,State Machine控制複雜邏輯。掌握這三劍客,你就能應對80%的遊戲開發挑戰。

📖 目錄


🎯 為什麼是這三個模式?

經過無數個專案的洗禮,我發現Unity遊戲開發中最常遇到的三大挑戰:

  1. 組件通訊混亂 → Observer Pattern解決
  2. 全域資源管理困難 → Singleton Pattern解決
  3. 複雜狀態邏輯難以維護 → State Machine Pattern解決

這三個模式就像RPG遊戲中的戰士、法師、牧師——各有專精,組合起來威力無窮。

👁️ Observer Pattern:事件驅動的優雅解耦

問題:組件通訊的義大利麵噩夢

還記得我剛入行時寫的災難代碼嗎?

// ❌ 緊密耦合的噩夢
public class Player : MonoBehaviour 
{
    void TakeDamage(int damage) 
    {
        health -= damage;
        
        // 直接操作其他物件,耦合度爆表
        FindObjectOfType<HealthBar>().UpdateDisplay(health);
        FindObjectOfType<AudioManager>().PlayHurtSound();
        FindObjectOfType<CameraShake>().StartShake();
        FindObjectOfType<ScoreManager>().UpdateScore(-10);
        
        if (health <= 0) 
        {
            FindObjectOfType<GameManager>().GameOver();
            FindObjectOfType<UIManager>().ShowDeathScreen();
        }
    }
}

這段代碼的災難性問題:

  • Player需要知道所有其他系統的存在
  • 新增系統時要修改Player代碼
  • 測試困難:需要模擬所有相關物件
  • 效能問題:每次都要FindObjectOfType

解決方案:事件驅動架構

// ✅ 優雅的Observer模式
public class Player : MonoBehaviour 
{
    // 定義事件
    public static event System.Action<int> OnPlayerDamaged;
    public static event System.Action OnPlayerDied;
    
    void TakeDamage(int damage) 
    {
        health -= damage;
        
        // 只負責發送事件,不管誰在監聽
        OnPlayerDamaged?.Invoke(health);
        
        if (health <= 0) 
        {
            OnPlayerDied?.Invoke();
        }
    }
}

// 各系統獨立監聽感興趣的事件
public class HealthBar : MonoBehaviour 
{
    void OnEnable() 
    {
        Player.OnPlayerDamaged += UpdateDisplay;
    }
    
    void OnDisable() 
    {
        Player.OnPlayerDamaged -= UpdateDisplay;
    }
    
    void UpdateDisplay(int newHealth) 
    {
        // 更新血量顯示
    }
}

public class AudioManager : MonoBehaviour 
{
    void OnEnable() 
    {
        Player.OnPlayerDamaged += PlayHurtSound;
        Player.OnPlayerDied += PlayDeathSound;
    }
    
    void OnDisable() 
    {
        Player.OnPlayerDamaged -= PlayHurtSound;
        Player.OnPlayerDied -= PlayDeathSound;
    }
    
    void PlayHurtSound(int health) { /* 播放受傷音效 */ }
    void PlayDeathSound() { /* 播放死亡音效 */ }
}

Unity中的Observer實戰技巧

技巧一:使用UnityEvent增強Inspector整合

public class GameEventSystem : MonoBehaviour 
{
    [Header("Player Events")]
    public UnityEvent<int> OnPlayerDamaged;
    public UnityEvent OnPlayerDied;
    public UnityEvent<int> OnScoreChanged;
    
    // 可以在Inspector中直接配置監聽者
}

技巧二:創建事件管理中心

// 統一的事件管理器
public static class GameEvents 
{
    // 玩家事件
    public static event System.Action<int> OnPlayerHealthChanged;
    public static event System.Action<Vector3> OnPlayerMoved;
    
    // 遊戲事件
    public static event System.Action<int> OnScoreChanged;
    public static event System.Action<string> OnLevelCompleted;
    
    // 老虎機特定事件
    public static event System.Action OnSpinStarted;
    public static event System.Action<int> OnWinAmountCalculated;
}

技巧三:避免記憶體洩漏

public class SafeEventListener : MonoBehaviour 
{
    void OnEnable() 
    {
        GameEvents.OnPlayerHealthChanged += HandleHealthChanged;
    }
    
    void OnDisable() 
    {
        // 重要:記得取消訂閱!
        GameEvents.OnPlayerHealthChanged -= HandleHealthChanged;
    }
    
    void HandleHealthChanged(int newHealth) 
    {
        // 處理邏輯
    }
}

👑 Singleton Pattern:愛恨交織的全域管理

Singleton的雙面性

Singleton是最具爭議的設計模式——用對了是神器,用錯了是災難。

✅ 適合Singleton的場景:

  • AudioManager:全遊戲只需要一個音效管理器
  • InputManager:輸入系統應該集中管理
  • SaveSystem:存檔系統需要全域存取
  • PoolManager:物件池管理器

❌ 不適合Singleton的場景:

  • Player:多人遊戲會有多個玩家
  • Enemy:遊戲中會有多個敵人
  • GameState:可能需要多個遊戲狀態實例

Unity中的Singleton最佳實作

// 安全的Singleton基底類別
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour 
{
    private static T _instance;
    private static readonly object _lock = new object();
    private static bool _applicationIsQuitting = false;
    
    public static T Instance 
    {
        get 
        {
            if (_applicationIsQuitting) 
            {
                Debug.LogWarning($"[Singleton] Instance '{typeof(T)}' already destroyed. Returning null.");
                return null;
            }
            
            lock (_lock) 
            {
                if (_instance == null) 
                {
                    _instance = FindObjectOfType<T>();
                    
                    if (_instance == null) 
                    {
                        GameObject singletonObject = new GameObject();
                        _instance = singletonObject.AddComponent<T>();
                        singletonObject.name = typeof(T).ToString() + " (Singleton)";
                        
                        DontDestroyOnLoad(singletonObject);
                    }
                }
                
                return _instance;
            }
        }
    }
    
    protected virtual void Awake() 
    {
        if (_instance == null) 
        {
            _instance = this as T;
            DontDestroyOnLoad(gameObject);
        }
        else if (_instance != this) 
        {
            Destroy(gameObject);
        }
    }
    
    protected virtual void OnApplicationQuit() 
    {
        _applicationIsQuitting = true;
    }
}

實戰案例:音效管理器

public class AudioManager : Singleton<AudioManager> 
{
    [Header("Audio Sources")]
    [SerializeField] private AudioSource musicSource;
    [SerializeField] private AudioSource sfxSource;
    
    [Header("Audio Clips")]
    [SerializeField] private AudioClip backgroundMusic;
    [SerializeField] private AudioClip spinSound;
    [SerializeField] private AudioClip winSound;
    [SerializeField] private AudioClip loseSound;
    
    protected override void Awake() 
    {
        base.Awake();
        
        // 初始化音效設定
        if (musicSource != null) 
        {
            musicSource.clip = backgroundMusic;
            musicSource.loop = true;
            musicSource.Play();
        }
    }
    
    public void PlaySFX(AudioClip clip) 
    {
        if (sfxSource != null && clip != null) 
        {
            sfxSource.PlayOneShot(clip);
        }
    }
    
    public void PlaySpinSound() => PlaySFX(spinSound);
    public void PlayWinSound() => PlaySFX(winSound);
    public void PlayLoseSound() => PlaySFX(loseSound);
}

// 其他腳本中的使用
public class SlotMachine : MonoBehaviour 
{
    public void Spin() 
    {
        // 簡潔的全域存取
        AudioManager.Instance.PlaySpinSound();
    }
}

Singleton的進階技巧

技巧一:延遲初始化避免循環依賴

public class GameManager : Singleton<GameManager> 
{
    private bool _isInitialized = false;
    
    void Start() 
    {
        if (!_isInitialized) 
        {
            Initialize();
        }
    }
    
    private void Initialize() 
    {
        // 確保其他Singleton已經準備好
        if (AudioManager.Instance != null && UIManager.Instance != null) 
        {
            _isInitialized = true;
            // 執行初始化邏輯
        }
        else 
        {
            // 延遲到下一幀再試
            Invoke(nameof(Initialize), 0.1f);
        }
    }
}

技巧二:可配置的Singleton

public class PoolManager : Singleton<PoolManager> 
{
    [SerializeField] private PoolConfig poolConfig;
    
    protected override void Awake() 
    {
        base.Awake();
        
        if (poolConfig != null) 
        {
            InitializePools();
        }
    }
    
    // 支援運行時重新配置
    public void UpdateConfig(PoolConfig newConfig) 
    {
        poolConfig = newConfig;
        InitializePools();
    }
}

🤖 State Machine:複雜邏輯的終極武器

問題:if-else地獄

我見過最可怕的老虎機邏輯是這樣的:

// ❌ 狀態邏輯的噩夢
public class SlotMachine : MonoBehaviour 
{
    public bool isSpinning;
    public bool isInBonus;
    public bool isAutoPlay;
    public bool isPaused;
    
    void Update() 
    {
        if (isSpinning) 
        {
            if (isInBonus) 
            {
                if (isPaused) 
                {
                    // 獎金模式暫停邏輯
                }
                else 
                {
                    // 獎金模式旋轉邏輯
                }
            }
            else 
            {
                if (isAutoPlay) 
                {
                    // 自動旋轉邏輯
                }
                else 
                {
                    // 手動旋轉邏輯
                }
            }
        }
        else 
        {
            // 更多嵌套的if-else...
        }
    }
}

這段代碼的問題:

  • 狀態組合爆炸:2^4 = 16種可能狀態
  • 邏輯難以理解和維護
  • 新增狀態需要修改很多地方
  • 容易產生非法狀態組合

解決方案:狀態機模式

// ✅ 清晰的狀態機設計
public abstract class SlotMachineState 
{
    protected SlotMachine machine;
    
    public SlotMachineState(SlotMachine machine) 
    {
        this.machine = machine;
    }
    
    public virtual void Enter() { }
    public virtual void Update() { }
    public virtual void Exit() { }
    
    // 處理輸入事件
    public virtual void OnSpinPressed() { }
    public virtual void OnPausePressed() { }
}

// 具體狀態實作
public class IdleState : SlotMachineState 
{
    public IdleState(SlotMachine machine) : base(machine) { }
    
    public override void Enter() 
    {
        Debug.Log("進入閒置狀態");
        machine.ShowSpinButton(true);
    }
    
    public override void OnSpinPressed() 
    {
        if (machine.CanSpin()) 
        {
            machine.ChangeState(new SpinningState(machine));
        }
    }
}

public class SpinningState : SlotMachineState 
{
    public SpinningState(SlotMachine machine) : base(machine) { }
    
    public override void Enter() 
    {
        Debug.Log("開始旋轉");
        machine.ShowSpinButton(false);
        machine.StartReelSpin();
    }
    
    public override void Update() 
    {
        if (machine.AllReelsStopped()) 
        {
            machine.ChangeState(new ResultState(machine));
        }
    }
}

public class ResultState : SlotMachineState 
{
    public ResultState(SlotMachine machine) : base(machine) { }
    
    public override void Enter() 
    {
        Debug.Log("計算結果");
        int payout = machine.CalculatePayout();
        
        if (payout > 0) 
        {
            machine.ChangeState(new WinState(machine, payout));
        }
        else 
        {
            machine.ChangeState(new IdleState(machine));
        }
    }
}

public class WinState : SlotMachineState 
{
    private int winAmount;
    
    public WinState(SlotMachine machine, int amount) : base(machine) 
    {
        winAmount = amount;
    }
    
    public override void Enter() 
    {
        Debug.Log($"贏得 {winAmount} 金幣!");
        machine.PlayWinAnimation();
        machine.AddCoins(winAmount);
        
        // 3秒後回到閒置狀態
        machine.StartCoroutine(ReturnToIdle());
    }
    
    private IEnumerator ReturnToIdle() 
    {
        yield return new WaitForSeconds(3f);
        machine.ChangeState(new IdleState(machine));
    }
}

// 狀態機控制器
public class SlotMachine : MonoBehaviour 
{
    private SlotMachineState currentState;
    
    void Start() 
    {
        ChangeState(new IdleState(this));
    }
    
    void Update() 
    {
        currentState?.Update();
    }
    
    public void ChangeState(SlotMachineState newState) 
    {
        currentState?.Exit();
        currentState = newState;
        currentState?.Enter();
    }
    
    // 輸入處理
    void OnSpinButtonPressed() 
    {
        currentState?.OnSpinPressed();
    }
    
    void OnPauseButtonPressed() 
    {
        currentState?.OnPausePressed();
    }
    
    // 狀態機需要的功能方法
    public bool CanSpin() { /* 檢查是否可以旋轉 */ return true; }
    public void StartReelSpin() { /* 開始轉軸旋轉 */ }
    public bool AllReelsStopped() { /* 檢查是否全部停止 */ return true; }
    public int CalculatePayout() { /* 計算獎金 */ return 0; }
    public void PlayWinAnimation() { /* 播放獲勝動畫 */ }
    public void AddCoins(int amount) { /* 增加金幣 */ }
    public void ShowSpinButton(bool show) { /* 顯示/隱藏旋轉按鈕 */ }
}

Unity狀態機的進階技巧

技巧一:視覺化狀態機除錯

public class SlotMachine : MonoBehaviour 
{
    [Header("Debug")]
    [SerializeField] private string currentStateName;
    
    public void ChangeState(SlotMachineState newState) 
    {
        currentState?.Exit();
        currentState = newState;
        currentState?.Enter();
        
        // 在Inspector中顯示當前狀態
        currentStateName = newState.GetType().Name;
    }
}

技巧二:狀態轉換表

public class StateTransitionTable 
{
    private Dictionary<(Type from, string trigger), Type> transitions;
    
    public StateTransitionTable() 
    {
        transitions = new Dictionary<(Type, string), Type>
        {
            { (typeof(IdleState), "SPIN"), typeof(SpinningState) },
            { (typeof(SpinningState), "REELS_STOPPED"), typeof(ResultState) },
            { (typeof(ResultState), "WIN"), typeof(WinState) },
            { (typeof(ResultState), "LOSE"), typeof(IdleState) },
            { (typeof(WinState), "TIMEOUT"), typeof(IdleState) }
        };
    }
    
    public Type GetNextState(Type currentState, string trigger) 
    {
        transitions.TryGetValue((currentState, trigger), out Type nextState);
        return nextState;
    }
}

技巧三:狀態資料傳遞

public abstract class SlotMachineState 
{
    protected SlotMachine machine;
    public Dictionary<string, object> StateData { get; private set; }
    
    public SlotMachineState(SlotMachine machine) 
    {
        this.machine = machine;
        StateData = new Dictionary<string, object>();
    }
    
    public void SetData(string key, object value) 
    {
        StateData[key] = value;
    }
    
    public T GetData<T>(string key) 
    {
        if (StateData.TryGetValue(key, out object value)) 
        {
            return (T)value;
        }
        return default(T);
    }
}

// 使用範例
public class WinState : SlotMachineState 
{
    public override void Enter() 
    {
        int winAmount = GetData<int>("winAmount");
        string winType = GetData<string>("winType");
        
        Debug.Log($"贏得 {winAmount} 金幣!類型:{winType}");
    }
}

⚔️ 三劍客組合技:實戰整合

現在讓我們看看如何將三個模式組合使用,創造更強大的系統:

// 整合Observer + Singleton + State Machine
public class GameManager : Singleton<GameManager> 
{
    [Header("State Machine")]
    private GameState currentState;
    
    [Header("Events")]
    public static event System.Action<GameState> OnStateChanged;
    public static event System.Action<int> OnScoreChanged;
    
    private int score = 0;
    
    protected override void Awake() 
    {
        base.Awake();
        ChangeState(new MenuState(this));
    }
    
    public void ChangeState(GameState newState) 
    {
        currentState?.Exit();
        currentState = newState;
        currentState?.Enter();
        
        // 發送狀態變更事件(Observer模式)
        OnStateChanged?.Invoke(currentState);
    }
    
    public void AddScore(int points) 
    {
        score += points;
        // 發送分數變更事件(Observer模式)
        OnScoreChanged?.Invoke(score);
    }
    
    void Update() 
    {
        currentState?.Update();
    }
}

// UI系統監聽遊戲狀態變化
public class UIManager : MonoBehaviour 
{
    [SerializeField] private Text scoreText;
    [SerializeField] private GameObject menuPanel;
    [SerializeField] private GameObject gamePanel;
    
    void OnEnable() 
    {
        GameManager.OnStateChanged += HandleStateChanged;
        GameManager.OnScoreChanged += HandleScoreChanged;
    }
    
    void OnDisable() 
    {
        GameManager.OnStateChanged -= HandleStateChanged;
        GameManager.OnScoreChanged -= HandleScoreChanged;
    }
    
    void HandleStateChanged(GameState newState) 
    {
        menuPanel.SetActive(newState is MenuState);
        gamePanel.SetActive(newState is PlayingState);
    }
    
    void HandleScoreChanged(int newScore) 
    {
        scoreText.text = $"Score: {newScore}";
    }
}

💡 避免過度設計的陷阱

何時不需要這些模式?

簡單專案: 如果你的遊戲邏輯很簡單,可能不需要完整的狀態機。一個簡單的enum狀態就足夠了。

// 簡單遊戲的簡化狀態管理
public enum GameState { Menu, Playing, Paused, GameOver }

public class SimpleGameManager : MonoBehaviour 
{
    public GameState currentState = GameState.Menu;
    
    void Update() 
    {
        switch (currentState) 
        {
            case GameState.Menu:
                HandleMenuState();
                break;
            case GameState.Playing:
                HandlePlayingState();
                break;
            // ...
        }
    }
}

原型階段: 在驗證概念的階段,直接寫功能比設計完美的架構更重要。

判斷是否需要模式的標準

  • Observer: 當你發現多處FindObjectOfType時
  • Singleton: 當你需要全域存取且確定只有一個實例時
  • State Machine: 當你的if-else超過3層巢狀時

🚀 進階學習方向

掌握了這三個核心模式後,可以進一步學習:

  1. Command Pattern - 實作撤銷/重做功能 (什麼是Command Pattern?讓我以Unity老虎機遊戲為例,深入淺出地告訴妳)
  2. Strategy Pattern - 動態切換演算法
  3. Decorator Pattern - 動態增強物件功能
  4. Object Pool Pattern - 優化記憶體管理

📚 深化理解

這些模式的實戰應用來自我在博弈遊戲業的豐富經驗,結合《Programming Design Patterns for Unity: Write Better Code》課程的深度理論。

課程特別強調的是模式的適用場景何時不該使用,這種批判性思維比盲目套用模式更寶貴。

🎯 總結:三劍客的協同作戰

  • Observer Pattern 讓系統解耦,各組件獨立運作
  • Singleton Pattern 提供全域存取點,管理共享資源
  • State Machine Pattern 組織複雜邏輯,讓狀態變化清晰可控

這三個模式就像遊戲中的職業組合:

  • Observer是刺客:快速、靈活、專注單一目標
  • Singleton是法師:強大但需要謹慎使用
  • State Machine是戰士:穩定可靠,應對複雜局面

記住:模式是工具,不是目標。用對了是神器,用錯了是災難。

當你能夠自然地選擇和組合這些模式時,你就已經從代碼工人進化為軟體架構師了。


你在專案中最常用哪個設計模式?或者踩過哪些模式濫用的坑?歡迎分享你的經驗!

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

傑克淺談遊戲邏輯

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