Tiny-Splatoon:從零打造的 3D 多人塗地遊戲

不依賴遊戲引擎,以 C++17、OpenGL 與自製墨水系統完成可連線的雙隊塗地對戰。

C++17 OpenGL 4.5 GLSL GPU SplatMap GameNetworkingSockets miniaudio Dear ImGui

不靠遊戲引擎,能不能做出一場完整對戰?

圖學課裡做出一個畫面,和讓玩家在同一場對戰裡移動、塗地、連線、計分,是不同層次的問題。我想確認的是:如果不把渲染、音效和網路交給現成引擎,能不能仍然把一個可玩的 3D 多人遊戲從底層接起來。

把畫面和遊戲狀態拆開,又讓它們對得上

Tiny-Splatoon 命中後的 GPU 墨水與 CPU 網格同步流程

我用 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 被消費,而不是繞過場景狀態直接改畫面。

Tiny-Splatoon 每幀主迴圈的呼叫順序

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 判定和網路狀態也正確。這個專案讓我更具體地理解,系統整合的難點常在模組交界,而不是某一個單獨的圖形效果。

查看原始碼 ・ 觀看遊戲展示影片