SQLDoom:在 SQL 数据库中完整运行原始 1993 年 Doom 引擎
TL;DR
SQLDoom 通过仅使用 SQL 查询,在 CedarDB 中完全运行原始 1993 年 Doom 的游戏逻辑和渲染器,保持经典的 35 Hz 游戏循环频率,并以高达 60 FPS 的速度输出完整的 320×200 帧缓冲区;一个轻量级的 Python 客户端负责处理输入、定时和位图显示,同一引擎还支持四人死亡竞赛服务器。
SQLDoom 是什么
SQLDoom 是对 Doom 第一章节共享版的忠实移植,将 所有 游戏逻辑、状态和渲染都移入由 CedarDB 数据库引擎执行的 SQL 语句中。唯一的外部代码是一个极简的 Python 脚本,其功能包括:
- 通过
pygame读取键盘/鼠标输入。 - 每隔 1/35 秒调用一次
SELECT doom_run_game_tic(...)以推进世界状态。 - 通过单个 SQL 查询请求新帧,并绘制返回的位图。
其余所有内容——怪物 AI、玩家移动、武器处理、BSP 遍历、墙壁/可视平面光栅化——均存在于表、存储过程和大量使用 CTE 的查询中。
架构概览
该系统分为两个独立的处理管道:
- 游戏逻辑管道 – 以固定的 35 Hz 运行,更新关系型游戏状态表,并不返回任何内容,仅返回更新后的状态。
- 渲染管道 – 一个纯函数型 SQL 查询,读取当前状态表并输出包含每个像素
(x, y, rgb)的 64 000 行结果。
Python (pygame) ↔️ SQL 逻辑表 ↔️ SQL 渲染器 → 位图 → 显示
这种分离使得客户端可以按需请求帧(作者报告最高可达 60 FPS),而逻辑部分仍严格锁定在原始的 28.6 ms 每帧预算内。
将 Doom 数据加载到关系模型中
Doom 的 WAD 格式已能清晰映射到关系表中:
VERTEXES→vertexesLINEDEFS→linedefsSIDEDEFS→sidedefsSECTORS→sectorsTHINGS→things
一个 1000 行的 Python 导入器解析 WAD 文件,并在笔记本电脑上约 18 秒内填充约 110 个表。关系模式使得关卡几何结构可直接查询,博客文章中包含的鸟瞰 ASCII 渲染查询即为示例。
游戏循环实现
一个名为 doom_run_game_tic 的存储过程封装了整个游戏循环。内部,CedarScript(一种类似 PL/pgSQL 的语言)构建一个 位掩码计划,决定本帧需要运行哪些子系统。随后,该过程调用一系列函数,例如:
doom_cs_move– 玩家和怪物移动doom_cs_doors– 门的开启/关闭doom_cs_weapon– 武器状态、视线扫描、伤害计算doom_cs_monsters– 怪物 AI 状态机doom_cs_sector_fx– 区域效果(地板、天花板)
怪物 AI 示例展示了递归 CTE 如何计算视线、距离,并通过 CASE 表达式决定下一状态,最终通过单个 UPDATE 更新 monster_ai 表。
性能:最坏情况(E4M1,46 个清醒怪物)耗时 10.45 ms(约占 28.6 ms 预算的 37%)。典型情况(6 个怪物)平均耗时 2.15 ms(约占 8%)。整个逻辑共约 5 900 行 SQL,少于原始 C 源码的约 9 000 行。
SQL 中的渲染管道
一个包含 89 个 CTE 的单一 SQL 查询生成完整的帧缓冲区。该管道模拟了 Doom 的经典步骤:
- BSP 遍历 – 预计算的
node_path_steps物化视图将每个根到子区域路径编码为 40 位sort_key。按此键排序即可获得无需递归的从前到后的子区域序列。 - 墙壁光栅化 –
generate_series将每个墙段展开为屏幕列,再展开为每个像素的片段。 - 可视平面(地板/天花板)填充 – 窗口函数计算每列剩余未裁剪区域,然后输出天花板/地板跨度。
- 精灵渲染 – 为广告牌生成类似片段。
- 深度解析 – 所有候选片段打包进 64 位
winner_key,其中最高有效位编码深度、表面优先级和稳定平局解决机制。MIN(winner_key)按像素选择可见片段。
时间分解:
- 墙壁光栅化 – ~1.7 ms
- 可视平面填充 – ~3 ms
- 深度解析与像素级工作 – ~8.2 ms(最大开销)
- 平均帧总耗时 – ~16 ms,Ryzen 7 7840U 上可实现 ~60 FPS;繁忙场景降至原始 35 FPS。
通过数据库实现多人游戏
由于整个世界存在于事务性数据库中,多人游戏几乎是免费的:
- 一个单一的“裁判”进程为所有玩家驱动 35 Hz 时钟。
- 玩家输入通过
SECURITY DEFINER函数api_input提交,该函数对值进行限制并插入mp_inputs表。 - 每帧在
BEGIN … COMMIT块内运行,确保每个客户端看到的要么是帧前状态,要么是帧后状态——无部分更新。 - 基于角色的访问控制限制玩家仅能使用 API 函数,防止任意表操作。
公共服务器在每客户端 3 核配置下运行四人死亡竞赛,稳定 35 FPS;16 核机器可轻松托管额外比赛。
为何数据库可能是合适的选择
- 数据驱动设计 – 武器、怪物和动画帧作为表中的行,使得检查和实时调整变得轻而易举。
- 原子性与一致性 – 每帧是一个事务,消除了经典的异步错误。
- 访问控制 – 细粒度权限可仅向玩家暴露预期的 API。
- ECS 映射 – 实体变为行,组件变为表,系统变为基于集合的
UPDATE/INSERT语句,利用数据库的并行执行引擎。
编译开销
CedarDB 在执行前将复杂查询编译为 LLVM IR。与原始 C 代码的汇编对比显示,SQL 生成的代码使用 117 条指令,而原始 C 仅 48 条,额外指令主要归因于将结果写回表中。考虑到多层抽象,这一差距并不显著。
社区反响(Hacker News 精选)
"我惊讶于在 SQL 中表达复杂游戏逻辑竟然如此简单。游戏逻辑仅约 5900 行 SQL。" – bob1029
"代码行数少于原始 C,同时滥用查询计划作为状态机,这是工程上的极致滥用。我爱它。" – soltanov
"游戏状态是一个 SQL 表,这让我想起了自己一年的优化经历……原子性保证了状态一致性……为动作游戏做这种读写循环?纯粹的愚蠢,但我觉得挺有趣的。" – noduerme
这些评论凸显了人们对 SQL 表达能力的惊讶、将查询计划器用作状态机的优雅,以及在实时动作游戏中可扩展性权衡的思考。
快速上手
- 安装 CedarDB 社区版。
- 安装 Python 依赖:
psycopg2和pygame。 - 获取共享版
doom1.wad(例如:apt install doom-wad-shareware)。 - 克隆仓库
https://github.com/cedardb/sqldoom,并按照 README 加载 WAD 和启动 Python 驱动程序。
或者,加入原始博客文章中链接的欧洲或美国公共服务器进行对战。
总结
SQLDoom 证明了关系型数据库可以托管一个功能完整、实时的 3D 引擎:游戏逻辑自然地融入基于集合的更新,基于 BSP 的渲染可用 CTE 和窗口函数表达,事务性保证使多人游戏变得简单。尽管数据库并非生产游戏的实用图形引擎,但该项目是一个令人信服的概念验证,突破了 SQL 可表达性的边界。
Sources
相关
- 项目
- 项目
- 项目
- Dispatch
- 项目