不靠遊戲引擎,能不能做出一場完整對戰?
圖學課裡做出一個畫面,和讓玩家在同一場對戰裡移動、塗地、連線、計分,是不同層次的問題。我想確認的是:如果不把渲染、音效和網路交給現成引擎,能不能仍然把一個可玩的 3D 多人遊戲從底層接起來。
把畫面和遊戲狀態拆開,又讓它們對得上
我用 C++17 與 OpenGL 4.5 建立自製渲染器和 GLSL 效果。原始碼按 engine、scene、network、gameplay、splat、components 與 gui 分模組,讓渲染基礎、場景生命週期、網路同步、遊戲規則、墨水、可重用元件和介面各自有可追蹤的邊界。
每一幀的呼叫順序如圖所示:Timer.Tick 之後是 NetworkManager.Update,再由 active scene 處理封包、更新與渲染,最後完成 GUI frame/UI、SwapBuffers 與 PollEvents;封包因此會在目前 active scene 被消費,而不是繞過場景狀態直接改畫面。
OpenGL 路徑以 Shader、VAO、VBO、EBO、Texture 和 MeshRenderer 組成。GameWorld 按固定順序繪製帶墨水紋理的地板與障礙物,再繪製牆、道具、角色、拋射物、粒子和陰影,讓透明度、遮擋和場景深度有一致的處理位置。
墨水系統同時保留視覺和邏輯兩種表示:拋射物命中後,先由 WorldToUV 把世界座標轉為表面 UV,再由 SplatPainter 寫入 FBO 的墨水結果,並更新 CPU 端的 SplatMap 網格;地板與障礙物各有一張 100×100 網格,供地面狀態、碰撞和分數相關判定使用。這個同步點很重要:如果只更新 GPU 紋理,玩家看到的顏色會和 CPU 判定的世界狀態分離。
網路層使用 Valve GameNetworkingSockets 的 listen-server 架構;主機同時擁有本地玩家,封包由 active scene 處理。用戶端移動訊息每 0.05 秒送往伺服器,伺服器的世界狀態和遊戲狀態則每 0.5 秒送回用戶端。
| 模組 | 在一場對戰裡回答的問題 |
|---|---|
| GPU SplatMap/GLSL | 哪些表面被塗上了什麼顏色? |
| CPU 網格與 GameWorld | 玩家能不能通過、目前地面屬於哪一隊、分數如何計算? |
| Valve GameNetworkingSockets | 不同玩家的狀態怎麼在連線中交換? |
| miniaudio/ImGui | 聲音、登入與大廳介面怎麼進入同一個遊戲流程? |
網路層使用 Valve GameNetworkingSockets,其他部分則由渲染、遊戲邏輯、場景、音效和介面模組組合起來。我沒有把每個功能都做到最大;設計核心是讓登入、大廳與遊戲場景之間的狀態轉換沿用同一套世界資料。
可玩的結果,也有明確的範圍
最後完成雙隊 3D 塗地對戰的展示流程:玩家可以在同一場遊戲裡移動、發射拋射物、看到 GPU 墨水效果,並由 CPU 網格與網路狀態共同推進遊戲。登入、大廳與遊戲場景的轉換也沿用同一套世界資料,因此展示的是自製渲染器、即時遊戲狀態、連線協定和工具介面的整合,而不只是一個獨立 shader demo。
規格把單局設定為 180 秒,但目前 spec 註明廣播的 timeRemaining 固定為 180;因此這裡把它描述成對戰設定,不宣稱已經有完全 authoritative 的倒數計時。規格也註明 CPU 分數百分比的 denominator 有 bug,所以我不把展示中的百分比當成精確的 CPU 佔地率。這是以 Windows 為目標的 listen-server 學習/作品集專案,也沒有把它包裝成可支援所有遊戲類型或商用網路可靠性的引擎。
我會帶走的檢查點
我會把「同一個狀態的兩種表示」當成之後做即時系統時的固定檢查點:畫面正確,不代表 CPU 判定和網路狀態也正確。這個專案讓我更具體地理解,系統整合的難點常在模組交界,而不是某一個單獨的圖形效果。