在博弈遊戲業的三年裡,我發現有三個設計模式特別適合Unity開發:Observer解決通訊問題,Singleton管理全域資源,State Machine控制複雜邏輯。掌握這三劍客,你就能應對80%的遊戲開發挑戰。
📖 目錄
- 為什麼是這三個模式?
- Observer Pattern:事件驅動的優雅解耦
- Singleton Pattern:愛恨交織的全域管理
- State Machine:複雜邏輯的終極武器
- 三劍客組合技:實戰整合
- 避免過度設計的陷阱
- 進階學習方向
- 總結:三劍客的協同作戰
🎯 為什麼是這三個模式?
經過無數個專案的洗禮,我發現Unity遊戲開發中最常遇到的三大挑戰:
- 組件通訊混亂 → Observer Pattern解決
- 全域資源管理困難 → Singleton Pattern解決
- 複雜狀態邏輯難以維護 → 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層巢狀時
🚀 進階學習方向
掌握了這三個核心模式後,可以進一步學習:
- Command Pattern - 實作撤銷/重做功能 (什麼是Command Pattern?讓我以Unity老虎機遊戲為例,深入淺出地告訴妳)
- Strategy Pattern - 動態切換演算法
- Decorator Pattern - 動態增強物件功能
- Object Pool Pattern - 優化記憶體管理
📚 深化理解
這些模式的實戰應用來自我在博弈遊戲業的豐富經驗,結合《Programming Design Patterns for Unity: Write Better Code》課程的深度理論。
課程特別強調的是模式的適用場景和何時不該使用,這種批判性思維比盲目套用模式更寶貴。
🎯 總結:三劍客的協同作戰
- Observer Pattern 讓系統解耦,各組件獨立運作
- Singleton Pattern 提供全域存取點,管理共享資源
- State Machine Pattern 組織複雜邏輯,讓狀態變化清晰可控
這三個模式就像遊戲中的職業組合:
- Observer是刺客:快速、靈活、專注單一目標
- Singleton是法師:強大但需要謹慎使用
- State Machine是戰士:穩定可靠,應對複雜局面
記住:模式是工具,不是目標。用對了是神器,用錯了是災難。
當你能夠自然地選擇和組合這些模式時,你就已經從代碼工人進化為軟體架構師了。
你在專案中最常用哪個設計模式?或者踩過哪些模式濫用的坑?歡迎分享你的經驗!

💎 瀞怡・高端精緻茶莊 TG:cy34y LINE:tw8497 大G:tw8497 https://t.me/mylove5200 這裡沒有亂槍打鳥的名單 每一位女孩,都是瀞怡親自挑選、親自了解 不追求數量,只堅持品質 我們相信: ⚪️ 一次好的體驗,比十次隨便的更重要 ⚪️ 彼此尊重,才能享受真正的互動 ⚪️ 貴一點沒關係,但要值得 瀞怡不是生意人,更像一位懂你的朋友 聆聽、理解、推薦屬於你當下需要的人選 讓你在忙碌生活中,還能遇見讓心動的她 嚴選・高質感・用心安排 如果你也重視體驗和品質 歡迎找瀞怡聊聊,這裡永遠為懂得的人留一盞燈
💎 瀞怡・高端精緻茶莊 TG:cy34y LINE:tw8497 大G:tw8497 https://t.me/mylove5200 這裡沒有亂槍打鳥的名單 每一位女孩,都是瀞怡親自挑選、親自了解 不追求數量,只堅持品質 我們相信: ⚪️ 一次好的體驗,比十次隨便的更重要 ⚪️ 彼此尊重,才能享受真正的互動 ⚪️ 貴一點沒關係,但要值得 瀞怡不是生意人,更像一位懂你的朋友 聆聽、理解、推薦屬於你當下需要的人選 讓你在忙碌生活中,還能遇見讓心動的她 嚴選・高質感・用心安排 如果你也重視體驗和品質 歡迎找瀞怡聊聊,這裡永遠為懂得的人留一盞燈