「為什麼改個UI顏色,遊戲邏輯就壞了?」這是我在博弈遊戲公司最常聽到的哀嚎。問題不在於程式碼有Bug,而在於整個架構就像用膠帶黏起來的紙房子——碰一下就全倒了。

📖 目錄


🎭 用樂團理解架構設計的精髓

想像你正在欣賞一場交響樂演出:

小提琴組內部(高內聚):

  • 第一小提琴引導旋律
  • 第二小提琴提供和聲
  • 中提琴補強中音域
  • 大提琴奠定低音基礎

他們互相聆聽、配合默契,共同完成一段美妙的樂章。

樂團組間(低耦合):

  • 小提琴組不需要知道鼓手用什麼牌子的鼓棒
  • 木管組不用管弦樂組的具體指法
  • 大家只需要聽指揮的拍子,按樂譜演奏

這就是理想的軟體架構:內部緊密合作,外部優雅獨立。

💥 當架構崩壞:真實的災難現場

災難一:低內聚的角色控制器

還記得我剛進公司時接手的角色控制器:

public class CharacterController : MonoBehaviour 
{
    // 動畫相關
    public void Knockback() { 
        animator.SetTrigger("Knockback"); 
    }
    
    // 音效相關  
    public void Shout() { 
        audioSource.Play(); 
    }
    
    // 血量相關
    public void TakeDamage(int damage) { 
        health -= damage; 
    }
}

問題在哪? 這個類別實際上是三個不相關的功能硬湊在一起:

  • 動畫管理員
  • 音效播放器
  • 血量計算器

每個方法各自為政,就像三個人各唱各的歌。

災難二:高耦合的敵人AI

更可怕的是這個敵人AI:

public class Enemy : MonoBehaviour 
{
    void Update() 
    {
        var player = FindObjectOfType<Player>();
        
        // 直接操作Player的內部組件!
        player.GetComponent<Animator>().SetTrigger("TakeHit");
        player.GetComponent<AudioSource>().Play();
        player.health -= 10;
    }
}

這有多災難? 當玩家系統重構時:

  • 血量系統改成百分比制?Enemy要改
  • 動畫系統換成Timeline?Enemy要改
  • 音效改用AudioManager統一管理?Enemy還是要改

一個簡單的玩家系統調整,結果要修改20個不同的敵人腳本。

🎯 高內聚:讓組件像樂隊一樣合作

什麼是真正的高內聚?

高內聚意味著一個類別內的所有元素都為了同一個明確目標而協同工作。

重構後的角色控制器(高內聚版本):

public class CharacterController : MonoBehaviour 
{
    [SerializeField] private Animator animator;
    [SerializeField] private AudioSource audioSource;
    [SerializeField] private int health = 100;
    [SerializeField] private AudioClip hitSound;
    [SerializeField] private AudioClip deathSound;
    
    // 一個方法整合使用多個組件,共同完成「受擊」這個目標
    public void TakeHit(int damage) 
    {
        health -= damage;                          // 扣血
        animator.SetTrigger("TakeHit");           // 播受傷動畫
        audioSource.PlayOneShot(hitSound);       // 播受傷音效
        
        if (health <= 0) 
        {
            Die();  // 血量歸零時死亡
        }
    }
    
    private void Die() 
    {
        animator.SetTrigger("Die");               // 死亡動畫
        audioSource.PlayOneShot(deathSound);     // 死亡音效
        // 其他死亡相關邏輯...
    }
}

為什麼這樣更好?

  • 所有組件為了同一個目標(角色受擊反應)協同工作
  • 邏輯更完整:不會忘記播動畫或音效
  • 更容易理解:一看就知道這是處理受擊的邏輯

Unity中的高內聚實例:PayoutCalculator

在老虎機開發中,我們的獎金計算器就是高內聚的典型:

public class PayoutCalculator 
{
    private PayTable payTable;           // 獎金表
    private WinPattern[] winPatterns;    // 中獎模式
    private MultiplierTable multipliers; // 倍數表
    
    // 所有內部組件協同工作,完成獎金計算
    public int CalculatePayout(Symbol[] symbols) 
    {
        var patterns = FindWinningPatterns(symbols);
        var baseAmount = CalculateBaseAmount(patterns);  
        var finalAmount = ApplyMultipliers(baseAmount);
        return finalAmount;
    }
    
    // 所有私有方法都為同一個目標服務
    private WinPattern[] FindWinningPatterns(Symbol[] symbols) { }
    private int CalculateBaseAmount(WinPattern[] patterns) { }
    private int ApplyMultipliers(int baseAmount) { }
}

🔗 低耦合:組件間的優雅互動

什麼是低耦合?

低耦合意味著類別之間的依賴關係最小化。一個類別不應該知道另一個類別的內部實作細節。

災難級的高耦合UI:

public class GameUI : MonoBehaviour 
{
    public void UpdateCoinDisplay() 
    {
        // 這行代碼依賴了太多類別的內部結構
        var player = FindObjectOfType<Player>();
        var wallet = player.GetComponent<Wallet>();
        var coinManager = wallet.GetCoinManager();
        var coins = coinManager.GetCurrentCoins();
        
        coinText.text = coins.ToString();
    }
}

問題分析:

  • UI知道Player的內部結構
  • UI知道Wallet的實作方式
  • UI知道CoinManager的存在
  • 任何一個環節改變,UI都要跟著改

優雅的低耦合解決方案:

// 定義簡潔的介面
public interface ICoinProvider 
{
    int GetCurrentCoins();
    event System.Action<int> OnCoinsChanged;
}

// Player實作介面,隱藏內部複雜性
public class Player : MonoBehaviour, ICoinProvider 
{
    private Wallet wallet;
    
    public int GetCurrentCoins() => wallet.GetCoins();
    public event System.Action<int> OnCoinsChanged;
    
    // Player內部處理錢幣變化邏輯
    private void OnWalletChanged(int newAmount) 
    {
        OnCoinsChanged?.Invoke(newAmount);
    }
}

// UI只知道介面,不知道內部實作
public class GameUI : MonoBehaviour 
{
    [SerializeField] private Text coinText;
    private ICoinProvider coinProvider;
    
    void Start() 
    {
        coinProvider = FindObjectOfType<Player>();
        coinProvider.OnCoinsChanged += UpdateCoinDisplay;
    }
    
    private void UpdateCoinDisplay(int newCoins) 
    {
        coinText.text = $"Coins: {newCoins}";
    }
}

這樣設計的好處:

  • UI只依賴ICoinProvider介面
  • Player內部錢幣系統怎麼重構都不影響UI
  • 容易測試:可以創建假的ICoinProvider來測試UI
  • 容易擴展:任何實作ICoinProvider的類別都能提供錢幣資訊

📏 墨忒得耳法則:只和朋友說話

這個法則有個更直白的名字:「不要和陌生人說話」

法則violation災難實例

在老虎機UI開發中,我見過這樣的災難代碼:

public class SlotMachineUI : MonoBehaviour 
{
    public void UpdateDisplay() 
    {
        // 違反得墨忒耳法則的鏈式調用
        var rounds = player.GetGun().GetAmmo().GetRoundsLeft();
        ammoText.text = rounds.ToString();
    }
}

這有什麼問題?

  • UI依賴Player
  • UI還依賴Gun
  • UI還依賴Ammo
  • UI還依賴RoundsLeft的具體實作

如果Gun改用不同的彈藥系統,或者Ammo的結構調整,UI就會崩潰。

優雅的解決方案

// Player提供高層次的介面
public class Player : MonoBehaviour 
{
    private Gun gun;
    
    // Player負責隱藏內部複雜性
    public int GetCurrentAmmo() 
    {
        return gun.GetAmmo().GetRoundsLeft();
    }
}

// UI只與直接朋友(Player)交談
public class SlotMachineUI : MonoBehaviour 
{
    public void UpdateDisplay() 
    {
        var rounds = player.GetCurrentAmmo();
        ammoText.text = rounds.ToString();
    }
}

🎰 實戰重構:老虎機系統大改造

重構前:災難級架構

// 全域狀態到處飛
public static class GameGlobals 
{
    public static int PlayerCoins = 1000;
    public static bool IsAutoPlay = false;
}

// 低內聚:什麼都管的萬能類別
public class SlotMachine : MonoBehaviour 
{
    void Update() 
    {
        // 輸入檢測、金幣扣除、音效播放、UI更新全擠在一起
        if (Input.GetKeyDown(KeyCode.Space)) 
        {
            GameGlobals.PlayerCoins -= 10;
            GetComponent<AudioSource>().Play();
            GameObject.Find("CoinText").GetComponent<Text>().text = 
                GameGlobals.PlayerCoins.ToString();
            // ... 50行混雜邏輯
        }
    }
}

這個架構的問題:

  • 全域狀態讓測試變困難
  • 單一類別負責太多職責
  • 高耦合:直接操作其他物件的組件
  • 難以擴展:新功能只能繼續塞進同一個類別

重構後:高內聚低耦合架構

// 高內聚的錢幣管理
public class PlayerWallet 
{
    private int coins = 1000;
    public event System.Action<int> OnCoinsChanged;
    
    public int Coins => coins;
    
    public bool TrySpendCoins(int amount) 
    {
        if (coins >= amount) 
        {
            coins -= amount;
            OnCoinsChanged?.Invoke(coins);
            return true;
        }
        return false;
    }
}

// 高內聚的老虎機控制器
public class SlotMachine : MonoBehaviour 
{
    [SerializeField] private PlayerWallet playerWallet;
    [SerializeField] private ReelController reelController;
    [SerializeField] private AudioManager audioManager;
    [SerializeField] private int spinCost = 10;
    
    void Update() 
    {
        if (Input.GetKeyDown(KeyCode.Space) && CanSpin()) 
        {
            PerformSpin();
        }
    }
    
    private bool CanSpin() => 
        !reelController.IsSpinning && playerWallet.Coins >= spinCost;
    
    // 高內聚:協調所有相關組件完成旋轉
    private void PerformSpin() 
    {
        if (playerWallet.TrySpendCoins(spinCost)) 
        {
            reelController.StartSpin();
            audioManager.PlaySpinSound();
        }
    }
}

// 低耦合的UI系統
public class GameUI : MonoBehaviour 
{
    [SerializeField] private Text coinText;
    [SerializeField] private PlayerWallet playerWallet;
    
    void Start() 
    {
        playerWallet.OnCoinsChanged += UpdateCoinDisplay;
        UpdateCoinDisplay(playerWallet.Coins);
    }
    
    private void UpdateCoinDisplay(int newAmount) 
    {
        coinText.text = $"Coins: {newAmount}";
    }
}

重構後的優勢:

  • 容易測試:每個組件都可以獨立測試
  • 容易理解:每個類別職責明確
  • 容易修改:修改一個組件不會影響其他組件
  • 容易擴展:新功能可以作為新組件加入

🔍 實戰判斷:你的代碼健康嗎

內聚度自檢清單

❌ 低內聚警告信號:

  • 一個類別超過200行
  • 方法只使用類別中的一兩個變數
  • 類別名稱有「Manager」、「Controller」、「Helper」
  • 很難用一句話描述這個類別的作用

✅ 高內聚健康指標:

  • 類別大小適中(通常50-150行)
  • 方法之間互相調用,使用多個類別變數
  • 類別名稱明確表達單一職責
  • 新人能快速理解類別的用途

耦合度自檢清單

❌ 高耦合警告信號:

  • 修改一個類別需要同時修改其他3個以上的類別
  • 代碼中出現長串的點操作:a.b.c.d.e()
  • 類別直接實例化或尋找其他類別
  • 單元測試需要設置很多mock物件

✅ 低耦合健康指標:

  • 類別通過介面或事件通訊
  • 依賴關係通過建構函數或屬性注入
  • 可以輕鬆替換實作而不影響其他代碼
  • 單元測試簡潔明了

💡 何時可以妥協?現實世界的考量

雖然高內聚低耦合是理想目標,但有時需要務實考量:

可以妥協的情況

原型開發階段: 快速驗證概念比完美架構更重要。可以先寫出能跑的代碼,驗證想法後再重構。

效能關鍵路徑: 在Update()等每幀執行的代碼中,有時直接存取比通過介面調用更高效。

Unity序列化需求: Unity的Inspector需要public欄位或[SerializeField]屬性,有時會影響封裝設計。

團隊技能水平: 過度複雜的架構可能增加團隊學習成本,適中的複雜度更實用。

妥協的判斷標準

  • 如果團隊成員經常因為某段代碼爭論,可能需要重構
  • 如果新功能開發時間超出預期50%,可能架構有問題
  • 如果新人需要超過一週才能理解某個系統,可能過於複雜

🚀 下一步:掌握組合的力量

了解了高內聚低耦合的精髓後,下一步就是學習如何在Unity中實踐這些原則:

即將探索的主題

  1. 組合模式深度解析 - 為什麼Unity的GameObject-Component系統如此強大?
  2. 觀察者模式實戰 - 用事件系統實現完美解耦
  3. 設計模式三劍客 - Observer、Singleton、State Machine的正確用法

實踐建議

從小處開始:

  • 下次寫代碼時,問自己:「這個類別在做幾件事?」
  • 看到鏈式調用時,想想:「我是不是依賴了太多陌生人?」
  • 發現全域變數時,考慮:「有沒有更優雅的方式?」

漸進式重構: 不要試圖一次重構整個專案。選擇一個小模組,應用這些原則,看看效果如何。成功的小改動會帶來信心,推動更大的改善。

📚 持續精進之路

這些架構原則是我在博弈遊戲業三年實戰的精華總結,也深受《Programming Design Patterns for Unity: Write Better Code》這門課程啟發。

為什麼推薦這門課程?

  • 不只教你「怎麼做」,更教你「為什麼這樣做」
  • 大量Unity實戰案例,不是紙上談兵
  • 重點教你「何時不該用」某個模式——這是很多教程缺乏的批判思維

🎯 結語:從義大利麵到交響樂

記住,好的遊戲架構不是一蹴而就的。它像培養一支優秀樂團一樣,需要時間、耐心和不斷的練習。

今天就開始:

  • 檢查你目前專案中耦合度最高的那個類別
  • 識別一個可以提高內聚度的重構機會
  • 尋找一個可以用介面替代直接依賴的地方

當你的代碼讓團隊成員看了會心一笑,而不是皺眉頭時,你就已經在通往架構大師的路上了。

最重要的原則:寫代碼如作曲,每個組件都應該在正確的位置,奏出和諧的樂章。


你的專案中有哪些「義大利麵代碼」讓你頭痛?或者你有什麼成功的重構經驗想分享?歡迎在評論區討論!

創作者介紹
創作者 傑克淺談遊戲邏輯 的頭像
傑克的遊戲宇宙

傑克淺談遊戲邏輯

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