CedarDB 以純 SQL 重做 Doom:邏輯與渲染全在資料庫
CedarDB Blog(Lukas Vogel) · 2026-09-22
去年的 DOOMQL 用光線投射(raycasting)畫畫面,其實更像 Wolfenstein 3D;這次的 SQLDoom 改用 Doom 原本的 BSP 樹,把 1993 年版 Doom 的遊戲邏輯與渲染器整個移進 CedarDB。Python 只負責讀鍵盤、計時、顯示資料庫回傳的點陣圖。
作者在 原文中訂了幾條規則:渲染與遊戲迴圈必須是純 SQL,只允許在資料庫內用 UDF,客戶端不得碰遊戲邏輯。結果是遊戲邏輯維持原版的 35 Hz,渲染器在作者的筆電上最高約 60 FPS,多人死亡競賽(四個名額)也能跑。
原本的問題
光線投射很適合 SQL 的集合運算,但畫出來的不像 Doom:沒有 Doom 的貼圖牆面、任意角度的牆與高低不同的地板。Doom 能做到這些,靠的是 BSP 樹讓深度排序變便宜,並由前往後作畫、記錄已經畫過的像素。這套演算法大量依賴可變狀態(ceilingclip、floorclip 兩個每欄一格的陣列),而 SQL 沒有迴圈也沒有可變狀態。
採用的方法
遊戲邏輯:資料庫裡約有 110 張表與一百多個函式,每個 tic 由 SELECT doom_run_game_tic(...) 觸發,內部用 CedarDB 的 cedarscript(語法接近 PL/pgSQL)依序執行移動、死亡、怪物、物理等步驟。怪物 AI 是一個 WITH RECURSIVE 加 CASE 的狀態機,最後用一條 UPDATE 套用。作者統計遊戲邏輯約 5900 行 SQL,原版 C 約 9000 行。
BSP 走訪:載入時預先算好從根到每個子區(subsector)的路徑,走前面記 0、走後面記 1,把這些決定壓進一個 bigint,一個 sum() ... order by 就取代整段遞迴下降:
SUM(CASE WHEN st.side = fs.front_side THEN 0::bigint
ELSE (1::bigint << (40 - st.depth)) END) AS sort_key作者說 40 位元夠用,因為最深的 BSP 樹(E4M8)只有 32 層。
地板與天花板(visplane):把「某欄中某一片牆」稱為 panel,每欄依深度排序後,用視窗函式 ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING 算出「較近的 panel 留下的剩餘區間」,等於把可變的 clip 陣列改寫成排序加聚合。
深度決勝:牆、平面、精靈可能在同一像素重疊,而 Doom 原本靠精密的繪製順序避開 z-buffer。作者重現不了,改成蠻力:全部產生,再把深度、優先序、顏色打包成一個 bigint,用 MIN 選出勝者。
實際效果
| 項目 | SQLDoom | 備註 |
|---|---|---|
| 渲染管線 | 約 1300 行 SQL、89 個 CTE | linux_doom 渲染引擎約 3300 行 C |
| 最慢的 tic | 10.45 ms | E4M1、46 隻怪物同時衝向玩家,約占 28.6 ms 預算的 37% |
| 一般 tic | 平均 2.15 ms | 6 隻怪物清醒時,約占預算 8% |
| 深度決勝 | 平均 8.2 ms | 整個管線最貴,超過一幀的三分之一 |
| 牆 / 地板天花板 | 平均 1.7 ms / 約 3 ms | Ryzen 7 PRO 7840U |
編譯後的比較也有數字:原版 C 的物體移動邏輯編成 48 條指令,SQLDoom 經 LLVM 產生 117 條,其中 42 條是把結果寫回資料表。
影響範圍
這不是要你在資料庫裡做遊戲引擎,作者自己也說用資料庫渲染 Doom 顯然是壞主意。值得資料庫與後端工程師看的是三個副產品:武器、怪物屬性與動畫狀態機都是資料表,改霰彈槍的彈丸數不用重新載入;交易讓每個 tic 要嘛全部生效要嘛不生效,四位玩家看到一致的世界;權限只開放 api_input 這類 SECURITY DEFINER 函式,其餘全部 revoke,輸入值在函式內被限制在合法範圍。
想自己跑的人需要 CedarDB Community Edition、裝了 psycopg2 與 pygame 的 Python,以及 Doom 的 IWAD(共享軟體版 doom1.wad 可自由散布,足夠玩第一章)。多人部分,作者表示每個客戶端約 3 核心可穩定 35 FPS。原始碼在 github.com/cedardb/sqldoom,文中同時提供公開的 EU 與 US 伺服器可直接加入對戰。