從理論到實踐,讓你的遊戲程式碼更穩固
📚 目錄
- 前言:SOLID 原則在遊戲開發中的價值
- 單一職責原則:讓每個類別專心做好一件事
- 開放封閉原則:可擴展但不可修改
- 里氏替換原則:子類別要能完全替代父類別
- 介面隔離原則:小而專精的介面設計
- 依賴反轉原則:依賴抽象而非具體實作
- 實戰應用:老虎機系統的 SOLID 改造
- 總結與學習資源推薦
前言: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 介面
- BasicPayoutRule、BonusPayoutRule、JackpotPayoutRule 分別實作
- 新增獎勵規則只需要新增實作類別
介面隔離原則的應用:
- 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 開發經驗!
請先 登入 以發表留言。