從理論到實踐,讓你的遊戲程式碼更穩固

📚 目錄

  1. 前言:SOLID 原則在遊戲開發中的價值
  2. 單一職責原則:讓每個類別專心做好一件事
  3. 開放封閉原則:可擴展但不可修改
  4. 里氏替換原則:子類別要能完全替代父類別
  5. 介面隔離原則:小而專精的介面設計
  6. 依賴反轉原則:依賴抽象而非具體實作
  7. 實戰應用:老虎機系統的 SOLID 改造
  8. 總結與學習資源推薦

前言:SOLID 原則在遊戲開發中的價值

在我三年的遊戲前端工程師經驗中,SOLID 原則是我從「能寫程式」進化到「寫好程式」的關鍵轉捩點。這五個設計原則不是什麼高深理論,而是實戰中總結出來的「避坑指南」。

記得第一次聽到 SOLID 原則時,覺得很抽象很學術,不知道怎麼用在實際專案中。直到經歷過幾次「改一個小功能卻要動十幾個檔案」的痛苦,才真正體會到這些原則的價值。它們不是為了讓程式碼看起來更「專業」,而是為了讓程式碼更容易維護和擴展。

今天想用最接地氣的方式,分享 SOLID 原則在遊戲開發中的實際應用。不談深奥理論,只講實戰經驗。

重要提醒:SOLID 原則是指導方針,不是死板規則。盲目遵循規則不會讓你成為好程式設計師,重要的是理解背後的思維,知道什麼時候該用,什麼時候該變通。


單一職責原則:讓每個類別專心做好一件事

🎯 核心概念

單一職責原則(Single Responsibility Principle)說:一個類別應該只有一個改變的理由

簡單講就是:每個類別只做一件事,專心做好它。

🚨 反面教材:什麼都管的老虎機控制器

在我早期的老虎機專案中,有個災難性的類別:

SlotMachineController 包辦一切

  • 處理旋轉邏輯和結果計算
  • 管理金幣增減和存檔
  • 控制 UI 更新和動畫播放
  • 處理音效和特效觸發
  • 管理網路通訊和資料同步

這個類別超過 500 行,任何需求變更都要動到它: 策劃調整獎勵公式 → 要改這個類別 美術換旋轉動畫 → 要改這個類別
音效師調整音效 → 要改這個類別 後端改API → 要改這個類別

結果就是一個類別有五個改變的理由,完全違反單一職責原則。

✅ 正確做法:職責分離

重構後的清晰分工

SpinCalculator:專門計算旋轉結果和獎勵 WalletManager:專門管理金幣和存檔 SlotAnimationController:專門處理旋轉動畫 AudioManager:專門管理音效播放 NetworkManager:專門處理網路通訊

每個類別都有明確的單一職責,修改時只需要動對應的類別。

🎮 實際好處

維護簡單:要改獎勵計算,只需要看 SpinCalculator 測試容易:可以獨立測試每個功能模組 團隊協作:不同人可以並行開發不同模組 除錯高效:問題發生時很容易定位到對應模組


開放封閉原則:可擴展但不可修改

🎯 核心概念

開放封閉原則(Open-Closed Principle)說:軟體實體應該對擴展開放,對修改封閉

意思是:新增功能時透過擴展來實現,而不是修改現有程式碼。

🚨 反面教材:寫死的武器系統

在一個 RPG 專案中,我們最初的武器系統是這樣設計的:

WeaponController 用巨大的判斷式處理所有武器

if (weaponType == "劍") {
    攻擊力 = 100;
    攻擊速度 = 1.2f;
    播放劍攻擊動畫();
} else if (weaponType == "弓") {
    攻擊力 = 80;
    攻擊速度 = 0.8f;
    播放弓攻擊動畫();
} else if (weaponType == "法杖") {
    // 更多寫死的邏輯...
}

每次新增武器類型,都要修改 WeaponController,風險很高。

✅ 正確做法:介面 + 策略模式

用介面實現開放封閉

定義 IWeapon 介面,每種武器都實作這個介面。新增武器時,只需要新增一個實作類別,不用修改現有程式碼。

具體實作SwordWeapon:實作劍的攻擊邏輯 BowWeapon:實作弓的攻擊邏輯
StaffWeapon:實作法杖的攻擊邏輯

WeaponController:只需要調用 currentWeapon.Attack(),不需要知道具體是什麼武器。

🎮 實際好處

安全擴展:新增武器不會影響現有武器的邏輯 降低風險:不用修改已測試過的程式碼 提高效率:策劃可以要求新武器,不用擔心破壞現有功能


里氏替換原則:子類別要能完全替代父類別

🎯 核心概念

里氏替換原則(Liskov Substitution Principle)說:子類別物件應該能夠替換其父類別物件,而不改變程式的正確性

簡單講:子類別不能改變父類別的基本行為契約。

🚨 反面教材:破壞性的繼承

在卡牌遊戲專案中遇到的問題:

基礎卡牌類別

class Card {
    cost: 法力消耗
    play(): 打出卡牌
}

問題的子類別

class FreeCard extends Card {
    play() {
        // 免費卡牌不消耗法力,但改變了基本行為
        if (法力 < cost) {
            // 正常卡牌會失敗,但免費卡牌強制成功
            強制打出卡牌();
        }
    }
}

這個設計破壞了里氏替換原則,因為 FreeCard 改變了「法力不足時無法打出卡牌」的基本契約。

✅ 正確做法:保持行為一致性

重新設計繼承關係

不要用繼承來表達「免費」這個概念,而是用組合模式:

Card 類別:維持基本的打出邏輯不變 CostModifier 組件:處理法力消耗的修改 FreeCostModifier:將法力消耗設為 0 的修改器

這樣所有卡牌都遵循相同的打出邏輯,只是法力計算不同。

🎮 實際好處

行為可預測:任何 Card 類型都有相同的基本行為 除錯容易:不會有「看起來一樣但行為不同」的詭異狀況 系統穩定:可以安全地在任何地方替換不同的卡牌類型


介面隔離原則:小而專精的介面設計

🎯 核心概念

介面隔離原則(Interface Segregation Principle)說:不應該強迫客戶端依賴它們不使用的介面

簡單講:把大介面拆成小介面,讓每個客戶端只依賴它真正需要的部分。

🚨 反面教材:萬能的玩家介面

在 MMORPG 專案中的問題設計:

IPlayer 介面包含所有功能

interface IPlayer {
    // 基本屬性
    getName(), getLevel(), getHP()
    
    // 戰鬥相關
    attack(), defend(), useSkill()
    
    // 社交相關  
    sendMessage(), addFriend(), joinGuild()
    
    // 商店相關
    buyItem(), sellItem(), checkInventory()
    
    // 管理功能
    banPlayer(), mutePlayer(), teleport()
}

問題是 UI 系統只需要顯示基本屬性,卻被迫依賴所有其他功能。

✅ 正確做法:分離介面

按功能分離介面

IPlayerInfo:getName(), getLevel(), getHP() - 給 UI 系統用 ICombatable:attack(), defend(), useSkill() - 給戰鬥系統用 ISocialPlayer:sendMessage(), addFriend(), joinGuild() - 給社交系統用 IShopCustomer:buyItem(), sellItem(), checkInventory() - 給商店系統用

每個系統只依賴它需要的介面,不會被不相關的功能污染。

🎮 實際好處

依賴清晰:看介面就知道這個系統需要什麼功能 變更安全:修改社交功能不會影響戰鬥系統 測試簡單:可以針對特定介面進行 Mock 測試


依賴反轉原則:依賴抽象而非具體實作

🎯 核心概念

依賴反轉原則(Dependency Inversion Principle)說:高層模組不應該依賴低層模組,兩者都應該依賴抽象

簡單講:不要直接依賴具體的實作類別,而要依賴介面或抽象類別。

🚨 反面教材:直接綁定的音效系統

在早期專案中的問題:

遊戲邏輯直接依賴具體的音效實作

class GameController {
    private UnityAudioSource audioSource;
    
    void playWinSound() {
        audioSource.clip = winSound;
        audioSource.Play();
    }
}

問題是當我們想換成 FMOD 音效系統時,要修改所有使用音效的地方。

✅ 正確做法:依賴抽象介面

定義音效介面

interface IAudioManager {
    playSound(soundName);
    playMusic(musicName);
    setVolume(volume);
}

遊戲邏輯依賴介面

class GameController {
    private IAudioManager audioManager;
    
    void playWinSound() {
        audioManager.playSound("win");
    }
}

具體實作可以隨時替換

  • UnityAudioManager:用 Unity 的音效系統實作
  • FMODAudioManager:用 FMOD 實作
  • SilentAudioManager:靜音版本,用於測試

🎮 實際好處

技術選型靈活:可以隨時替換不同的音效系統 測試友善:可以用 Mock 音效管理器進行單元測試 平台適配:不同平台可以用不同的音效實作


實戰應用:老虎機系統的 SOLID 改造

🎯 改造前的問題系統

原本的老虎機系統違反了多個 SOLID 原則:

SlotMachine 類別職責過多:計算結果、更新 UI、播放音效、處理存檔(違反單一職責) 新增獎勵類型要修改主邏輯:寫死的獎勵計算(違反開放封閉) 音效系統直接綁定 Unity:無法替換其他音效方案(違反依賴反轉)

✅ SOLID 改造後的清晰架構

單一職責原則的應用

  • SpinEngine:專門處理旋轉邏輯
  • PayoutCalculator:專門計算獎勵
  • SlotUI:專門處理 UI 更新
  • GameDataManager:專門處理存檔

開放封閉原則的應用

  • 定義 IPayoutRule 介面
  • BasicPayoutRuleBonusPayoutRuleJackpotPayoutRule 分別實作
  • 新增獎勵規則只需要新增實作類別

介面隔離原則的應用

  • ISpinnable:只有旋轉相關方法,給旋轉系統用
  • IDisplayable:只有顯示相關方法,給 UI 系統用
  • IAudible:只有音效相關方法,給音效系統用

依賴反轉原則的應用

  • SlotMachine 依賴 IAudioManager 介面,不依賴具體音效實作
  • SpinEngine 依賴 IRandomProvider 介面,可以切換不同的隨機數生成器

🎮 改造後的實際好處

開發效率提升:不同開發者可以並行開發不同模組 測試覆蓋度提高:每個模組都可以獨立測試 維護成本降低:修改某個功能不會影響其他功能 擴展能力增強:新增功能主要透過新增類別,而不是修改現有程式碼


總結與學習資源推薦

🎯 SOLID 原則的實戰價值

SOLID 原則不是為了讓程式碼看起來更「專業」,而是為了解決實際開發中的痛點:

單一職責:讓程式碼更容易理解和維護 開放封閉:讓新增功能更安全,降低破壞現有功能的風險 里氏替換:讓繼承關係更可靠,避免詭異的行為差異 介面隔離:讓依賴關係更清楚,降低系統耦合度 依賴反轉:讓系統更靈活,容易替換不同的技術方案

💡 應用建議

不要過度設計:小專案不需要完全遵循所有原則,要看情況調整 循序漸進:從單一職責開始實踐,逐步引入其他原則
重視實效:如果某個原則讓開發變複雜,要思考是否真的需要 團隊共識:團隊要對架構設計有共同理解,不要各自為政

記住:SOLID 原則是工具,不是目的。目的是寫出更好維護、更容易擴展的程式碼。

🚀 進階學習資源

如果想深入學習更多遊戲開發的設計模式和架構技巧,強烈推薦以下兩門課程:

📖 Programming Design Patterns For Unity: Write Better Code

這門課程深入講解在 Unity 開發中最實用的設計模式,包括觀察者模式的進階應用、單例模式的正確使用方式、工廠模式在遊戲物件建立中的應用、狀態機模式在遊戲邏輯中的實作。每個模式都有完整實戰案例和重構範例,非常適合想提升程式碼品質的 Unity 開發者。

📖 Unity C# Scripting Intermediate - Upgrade Your C# Skills

這門課程專注於提升 C# 程式設計技能,幫你掌握中級開發者必備技能:升級 C# 腳本技能、實作不同的資料結構、向量數學的學習與實作、精通物件池技術、四元數的清晰概念、物件導向程式設計精進。

這兩門課程相輔相成,能讓你從程式設計新手變成有架構思維的資深開發者。

希望這篇文章能幫助你理解 SOLID 原則的實際價值,寫出更穩固易維護的遊戲程式碼!


🔥 想要更多遊戲開發技巧?記得關注我們後續文章,會繼續分享更多實用的程式設計模式和 Unity 開發經驗!

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

傑克淺談遊戲邏輯

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