獨立遊戲開發心得:逃跑騎士(程式篇)

整個心得分成程式 美術 音樂,三個部份來說明,該篇為程式部分。

前言

一開始做技術評估,我原先只會html跟CSS還有jquery,為了快速驗證構想加上本次開發的類型為音樂相關遊戲,透過Gemini 跟 ChatGpt 交叉比對建議,最後選擇了網頁做為載體,原因是音樂相關的處理透過網頁較容易,發發速度也較快,之前也有試著開發安卓App,並上架到google play,有興趣可以看一下我的練習作品,兩款加起來也投注了快半年的時間。

魂的召喚塔
魂的召喚塔
遺忘之村

回到正題,由於之前沒有開發過網頁遊戲,我的第一個動作是快速驗證我要用到的技術框架的可行性,我快速的製作了一款測試音訊相關的應用程式,哼唱鴨鴨,花了我9天的時間,驗證pitchy、 tone.js、 vexflow 是否符合我的需求,學習zustand 做狀態控制,並簡單了解了tailwindcss的運作。

然後,接著是惡夢的開始,理想與現實有著差距,第一個構想是透過聲音控制子彈的發射,然而,面臨兩個問題 :

1.遊戲難度過高,其實人類很難準確地發出自己想要的絕對音高。

2.音訊輸入處理的判定十分困難,因為人的聲音其實是抖動的波形,提取出麥克風輸入時需要很複雜的技術。

後來只好簡化,改為用聲音輸入來切換武器跟升級武器,不過我仍然覺得不太滿意,所以沒有繼續開發,不過在其間也算是把聲音輸入的處理給練習了,額外多學了好幾個:Web Audio API、音高偵測Pitchfinder、Howler聲音播放

然後後端的部分我摸得不太熟,但也是在模糊的狀態下把功能正常做完,用的是.NET平台  Web API,然後搭配MySQL資料庫,選擇ORM 框架 EF Core,也因為有上傳分數功能,做了一個上傳分數的簡易Api。還額外學了 Jwt token,製作google登錄,之前在Unity開發也有做,其實感覺上較簡單,網頁的第三方登錄反而麻煩了一點,因為我做前後端分離,要先處理跨域協定,然後調整Program.cs。

接下來是本專案的程式部分啦

逃跑騎士-音樂之國的繼承者

程式-前端

延續之前練習使用的技術React + Tailwind,前端方面幾乎都是使用之前比較熟悉的,搭配學到的 eventemitter3

遊戲事件

這裡也採了一個坑,因為我是混合式,介面用react,遊戲運行用phaser,一開始事件用了只能在phaser起作用的自訂的GameEvents,然後用phaser內建的事件發射器,導致有些沒辦法透過zustand(zustand可以跨react跟phaser)的資訊很難傳遞,後來改用GlobalBusEventsg使用eventemitter3 之後,才能跨react跟phaser溝通,但由於此時代碼已經非常多,重構起來非常費力,學乖了一開始就用全局事件,不要用phaser內建的事件發射器,一開始多輕鬆,後面就多痛苦。

狀態管理

Zustand做非機密的狀態控制,一開始是資料都先存localStorage,等功能確定沒問題,再把重要資訊改為上傳到後端,這樣的話還能支援本地遊玩跟登入模式,不過這裡我也是採了個坑,一開始沒有把store分得較細,導致物品,卡片,裝備等等的store都跟玩家放在一起,維護起來蠻混亂的,建議一開始就都分開會比較好,雖然到了後端我仍然將他們都轉成不同的表格,但這樣看起來就會有點前後端不一致。

遊戲邏輯

基本上都是交由AI撰寫比較多,我的目標是讓他好維護,好看懂,所以我進良遵循自己領悟的設計原則

層級 (Layer)職責 (Role)物品系統 (Item System)任務系統 (Quest System)
1. Data (數據)保存狀態 (State)ItemStore (Zustand)
– inventory: []
– gold: 100
QuestStore (Zustand)
– activeQuests: []
– completedIds: []
2. Logic (邏輯)規則運算 (Manager)ItemManager
– dropItem(loots)
– consumeItem(id)
SpawnManager (生成邏輯)
QuestManager
– acceptQuest(id)
– checkProgress(event)
– completeQuest(id)
3. View (介面)UI 呈現 (React)InventoryUI
– 顯示格子與 Icon
– 裝備介面
QuestLogUI
– 顯示任務列表
– 顯示進度條 (0/10)
4. Entity (實體)遊戲內物件 (Phaser)ItemEntity (Sprite)
– 地上的掉落物
– ItemComponent (拾取行為)
(無專屬實體)
– 但可能關聯到 NPC Entity (頭頂驚嘆號)

介面(Interface)

定義這個東西是誰,要包含哪些東西,沒啥特別好說的,唯一要注意的是最好將行為跟數據的介面分開,除非你非常確定他不會再擴充,或是很簡單。

數據

這邊用了太多ts直接定義,導致後續維護很困擾,後來我學會一開始就使用excel做資料,然後自己寫個簡單的python轉json匯入使用。

管理器

基本上就是橋梁,可以負責溝通很多不同的東西,看你賦予給他的職責,通常物件的管理器,我還寫了生成的邏輯在內部,更細一點可以再拆出去。

而系統的管理器不生成,畢竟他也沒有物件,他負責調用store中的方法,實現更複雜的務邏輯運算,例如使用物品,管理器需要從ItemStore調用目前該玩家的物品數量,然後使用store中增加物品的方法,更新ItemStore狀態,然後讓ItemStore發送全局事件,負責同步後端的管理器執行上傳到後端的動作,然後因為ItemStore使用zustand技術的關係,UI就會直接更新了

組件

這個是最一開始最煩的,因為Unity的組件跟UI的組件完全是不同的東西,可是他們英文都一樣(component),一開始很容易搞混,簡單來說:


Unity的component是可重複使用的功能,我都稱他為組件

在開發phaser遊戲過程中,我仍然是用了非常多組件,基本的健康組件,移動組件,攻擊組件,技能組件(搭配策略模式跟子類)。

UI的component是可重複的樣式,我都稱他為元件

然而,因為我在這邊偷懶的關係,很多UI重複的地方都沒有元件化,後續要改雖然也是AI改啦,但我發現AI其實比較能調整的是非元件化的代碼,雖然看起來很醜,但是AI直接修改樣式方面效果比較好,請他去修改元件通常出來會不如預期,不過元件化後人工比較好維護,也比較能達成一致性,但在一開始追求好看的樣式,我會覺得要先直接請AI寫代碼,然後再轉成元件,而不是直接寫元件套進去。

UI 介面

基本上Gemini能寫得很不錯,只要記得寫完後把喜歡的樣式轉成元件即可,不然後續要調整視覺一致性會想哭。

React Hooks 在遊戲開發中的角色

在本專案中,我採用 Zustand 進行全局狀態管理,並結合 React Hooks 來控制遊戲容器GameContainer的生命週期與 UI 互動。以下是關鍵的技術實作:

1. useEffect:遊戲生命週期的核心驅動

這是連接 React 與 Phaser 的橋樑。在 GameContainer 中,我利用 useEffect 來監聽 gameKeylevelKey 的變化:

  • 啟動遊戲:當組件掛載 (Mount) 時,初始化 new Phaser.Game(config)
  • 銷毀清理:利用 useEffectreturn 清理函式,在頁面切換或組件卸載時自動呼叫 game.destroy(true),防止記憶體洩漏或重複創建 Canvas。
  • 事件監聽:在 MainGamePage 中,用來訂閱 GlobalBus 的遊戲結束事件,確保 UI 能正確響應遊戲內部的邏輯。
2. useRef:持有遊戲實例 (重要)

GameContainer 中,我使用了兩個關鍵的 Ref:

  • gameRef:用來綁定 HTML div 元素,告訴 Phaser 遊戲畫面要渲染在哪裡。
  • gameInstance:用來儲存 Phaser 的遊戲實體。因為 Phaser 實體不需要參與 React 的渲染流 (Render Flow),使用 useRef 可以讓我們操作遊戲物件而不會觸發 React 不必要的重新渲染。
3. useState:控制純 UI 的顯示邏輯

雖然大部分遊戲數據都在 Zustand 中,但對於「僅與當前頁面有關」的 UI 狀態,我仍使用 useState

4. useCallback:穩定回調函式

用於優化效能與相依性管理。

  • 例如:handleNavigate 函式。為了將它放入 useEffect 的依賴陣列中而不造成無窮迴圈,使用 useCallback 確保該函式的參照 (Reference) 保持穩定。
5. Zustand Hooks (useGameStore 等):取代傳統 Context

我沒有使用 React 內建的 Context API,而是使用 Zustand 生成的 Hooks(如 useGameStore, usePlayerStore)。

這讓我能在任何 UI 組件(如血條、任務列表)中直接讀取遊戲數據,且 Zustand 會自動處理效能優化,只有當訂閱的數值改變時,該組件才會重新渲染。

程式-後端

我後端真的就比較不熟悉了,現在屬於還在摸索的狀態,使用

MySQL (資料庫)
.Net Web Api(ASP.NET Core)
ORM 框架 EF Core
Jwt (JSON Web Token)

MySQL (資料庫)

這邊比較單純,就是建表,加上也有plgin可以直接裝在Vscode,有時候也可以直接操作,不過還是建議用語法,建比較快,雖然後面我就使用EF Core直接建了,定義好 C# 的類別 (Class),EF Core 就會自動幫你翻譯成 MySQL 的 SQL 語法並建立資料庫表,更快,但也有衍生一些問題,主要就是我常寫完class忘記更新EF Core要看的DbContext

ORM 框架 EF Core

既然都提到資料庫就順便講這個,我覺得他很方便是會有類似專屬於資料庫的版本紀錄,會出現Migrations資料夾,我最早的一次是上一款聲音射擊遊戲,然後覺得很方便,就延到這款逃跑騎士了,一直到12月才建表處理後端。

.Net Web Api(ASP.NET Core)

我一開始覺得這個技術框架選用名稱超長,所以還得先瞭解這在幹嘛,他是

.NET: 表明這是基於 Microsoft 的 .NET 平台。

Web API: 指明是提供 HTTP 服務的 API 應用程式,而不是傳統的網頁應用程式 (MVC) 或其他類型的應用程式。

(ASP.NET Core): 強調使用的是 ASP.NET Core 這個現代、跨平台且高性能的 Web 框架,而不是舊版的 .NET Framework (例如 ASP.NET Web API 2)。

好,加起來就變超長,這邊的話我一開始都寫controller + 用剛才的 models就完事,一直到LinePay交易跟物品合成才把處理邏輯寫到service,導致前面的controller 都蠻腫的,應該要遵循“Skinny Controller, Fat Service” (瘦控制器,胖服務) 原則才對,然後拿資料我目前也是直接寫在controller ,但應該要寫在Repositories,這樣換資料庫才不會很痛苦。

目前就像是餐廳服務生controller 坎啜爾包辦所有工作,拿食材(Repositories),點/出菜,兼廚師(Service)>< 下個專案要改進。

Models: 給資料庫看的。

DTOs: 給前端看的。

上面兩者記得分開放,我一開始覺得他們都是資料結構就放一起,後面要全部搬。


已發佈

作者:

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *