用 EGE 在 CPU 上重写 3D 跑酷:Cube Runner 立方体跑酷

如果只看一张截图,你可能会以为这是某个轻量级 3D 引擎做出来的小游戏:镜头在弯曲的隧道里高速前进,远处的方块迎面扑来,玩家需要不断旋转跑道,在越来越密集的障碍之间寻找缺口。

但这个 Demo 没有使用现成的 3D 游戏引擎,也没有依赖模型、贴图或其他外部素材。透视投影、三角形光栅化、深度缓冲、雾效、方块光照、碰撞检测和游戏状态,全部写在一个 C++ 源文件里,最终画面直接交给 EGE 显示。

先通过下面这段约 8 秒的实机片段感受速度、转向、障碍逼近和碰撞反馈:

这是一份适合“边玩边学”的 EGE 范例:你可以先挑战自己的最高分,再沿着源码逐层拆开一个小型 3D 游戏究竟由哪些部分组成。

从早期 iPhone 经典,到浏览器里的 3D 隧道

Cube Runner 这个名字,会唤起不少早期智能手机玩家的记忆。

2008 年,开发者 Andy Qua 在 App Store 初期推出了面向 iPhone 和 iPod touch 的《Cube Runner》。玩法非常直接:利用手机的重力感应控制飞行器,在布满方块的场地中高速穿行。它是免费游戏,画面简洁,控制灵敏,还支持下载和分享自制关卡。多年后,App Store 上的玩家评论依然经常提到它带来的怀旧感,而开发者也持续为它加入复古模式、第一人称视角、手柄支持等功能。

不过,本篇介绍的 EGE Demo 并不是对 iOS 版本的官方移植。

它直接参考的是 Game5 制作的浏览器实验 Cuberunner。这个项目于 2015 年进入 Chrome Experiments,使用 WebGL 构建了一条可以不断旋转的 3D 隧道:玩家控制小方块绕着隧道移动,躲避迎面而来的彩色障碍。

EGE 版本延续了这种“隧道旋转 + 方块躲避”的核心体验,同时重新实现了几何、软件光栅器、HUD 和游戏逻辑,没有复用原项目的模型、贴图或音频资源。换句话说,它更像一次从玩法出发的 C++ / EGE 重写,而不是把浏览器代码简单搬进桌面程序。

可以把这三段历史这样理解:

  1. 2008 年的 iPhone《Cube Runner》,代表了早期移动游戏中“规则简单、依靠反应、不断挑战高分”的经典体验;
  2. 2015 年 Game5 的 Cuberunner,把相似的躲避乐趣放进旋转的 WebGL 隧道;
  3. 现在的 EGE Demo,则进一步用 C++ 和 CPU 软件渲染重新完成这套视觉与玩法。

目前没有可靠证据表明 Game5 版本与 Andy Qua 的手机游戏存在官方授权或移植关系,因此本文只把前者作为 EGE 源码的直接参考,把后者作为同名经典游戏的时代背景。

游戏怎么玩?

玩家控制的是画面下方的黄色小方块。它的位置基本固定,真正旋转的是整条隧道。这样既保留了高速前进的视觉效果,也让操作始终保持直观:把安全的轨道转到玩家脚下即可。

操作方式

操作 功能
A / D 或方向键 向左、向右旋转隧道
按住鼠标左键并拖动 连续转向
P 暂停或继续
R 从头重新开始
Esc 退出游戏

键盘转向不是生硬地跳到下一条轨道,而是先计算目标角速度,再让当前角速度平滑靠近目标。松开按键后,转速也会快速衰减,因此操作既灵敏,又不会显得突然。

三次机会,八种关卡

游戏开始时有三点生命。撞上障碍物会扣除一点生命,并获得短暂的无敌时间;无敌期间,玩家方块会闪烁,避免连续碰撞瞬间耗尽生命。生命归零后进入游戏结束状态,按 R 就能马上再来一局。

关卡每 30 秒切换一次,共有八种障碍模式:

  1. 基础彩色方块;
  2. 速度更快的红色方块;
  3. 更高、更厚的石块;
  4. 向隧道中心伸出的长方体;
  5. 组合出现的彩色障碍;
  6. 连续占据多条轨道的方块墙;
  7. 带有连续追击节奏的障碍组合;
  8. 宽阔屏障与高速方块的混合挑战。

完成八个阶段后,游戏进入下一轮,基础前进速度继续提高。得分则随存活时间不断增加。规则并不复杂,但当速度越来越快、障碍组合越来越紧密时,“再多撑十秒”就会变得很有吸引力。

不使用 3D 引擎,画面是怎么出现的?

这个 Demo 最值得学习的部分,是它没有调用现成的 3D 场景系统,而是自己完成了一个精简的软件渲染管线。

1. 先在三维空间中描述点

程序使用一个很小的 Vec3 结构表示三维坐标,并实现向量加减、缩放、点积、叉积和单位化。这些基础运算足以计算方块顶点、表面法线和光照方向。

一只方块由 8 个顶点、6 个面组成。每个四边形面再拆成两个三角形,于是无论玩家、普通障碍还是大型屏障,最终都能交给同一套三角形绘制函数处理。

2. 把三维坐标投影到屏幕

透视投影的直观规律是“近大远小”。程序根据视场角计算焦距,再用深度 z 的倒数缩放横纵坐标:

这里的垂直视场角为 75 度。距离摄像机过近的点会被近平面剔除,远处的物体则逐渐融入深色背景,形成速度游戏常见的纵深和雾感。

3. 用重心坐标填充三角形

三角形投影到屏幕后,程序计算它的包围盒,并逐像素判断采样点是否位于三角形内部。三个边函数得到的权重既可以完成覆盖测试,也可以插值深度。

每个像素还对应一个深度缓冲值。只有更靠近摄像机的新像素才能覆盖旧像素,因此隧道、方块和网格线才能保持正确的前后遮挡关系。

这就是一个小而完整的软件光栅器:

4. 直接写入 EGE 图像缓冲区

EGE 的 newimage() 创建离屏图像,getbuffer() 取得像素缓冲区。软件渲染器直接把 ARGB 像素写进这块内存,完成一帧后再通过 putimage() 显示到窗口。

这样避免了“先渲染到自己的数组、再整帧复制到 EGE 图像”的额外开销。HUD 则在画面提交后用 EGE 原生字体接口绘制,所以 Windows/MSVC 版本可以直接显示“生命、关卡、帧率”等中文信息。

一条会转弯的十二边形隧道

隧道横截面由 12 条轨道组成,纵深方向保留 24 排网格。程序根据角度计算圆周上的点,再把相邻轨道和相邻深度之间的四个点连成网格面。

如果隧道永远笔直,速度感很快就会变得单调。这里还为远处的隧道中心加入了一条二次曲线:距离超过一定范围后,隧道逐渐向下弯曲。玩家看不到终点,只能看到网格和障碍不断从弯道深处出现。

为了制造前进效果,程序不需要让摄像机真的无限移动。它只累计已经前进的距离,再用“距离对单格长度取余”的方式移动最近一排网格。当一排从身后消失时,远处会自然补上新的一排,形成连续不断的隧道。

八种障碍,不只是随机扔方块

如果障碍完全随机,游戏很容易出现无解局面,也很难产生节奏。Cube Runner 把障碍生成拆成八个阶段,每个阶段有自己的颜色、尺寸、速度、组合方式和间距。

例如:

  • 快速方块会使用更高的移动倍率,让玩家必须提前反应;
  • 高障碍向隧道中心延伸,视觉压迫感更强;
  • 连续方块占据相邻轨道,要求玩家迅速判断缺口;
  • 大型屏障会横跨多条轨道,同时配合另一个高速障碍制造选择压力。

障碍并不是凭空“闪现”在眼前,而是从远端生成,沿 z 轴向玩家移动。碰撞检测只需要同时判断两个条件:障碍是否进入玩家所在的深度范围,以及它旋转后的角度范围是否覆盖玩家。

这套设计很适合新手学习,因为它把关卡难度拆成了几个容易控制的参数:

  • 障碍间距;
  • 障碍尺寸;
  • 占据的轨道数量;
  • 相对移动速度;
  • 同时生成的障碍组合;
  • 每轮游戏的整体速度。

只改这些参数,就可以做出完全不同的节奏。

多线程软件渲染

1280×720 分辨率下,一帧接近 92 万个像素。为了让 CPU 软件渲染保持流畅,程序会根据硬件线程数选择 1、2 或 4 个渲染线程,并把屏幕按水平方向切成多个区域。

每个线程只写自己的像素带,因此不会同时修改同一个像素,也不需要给每次绘制加锁。主线程完成自己的区域后,等待其他渲染线程结束,再统一显示这一帧。

这不是大型引擎采用的完整并行渲染架构,但代码集中、概念清楚,很适合通过一个 Demo 理解任务分发、条件变量和无冲突分区这些基础思想。

为什么这个 Demo 适合新手?

乍看之下,三维投影、光栅化和多线程好像都很难。但 Cube Runner 的优势是:你不需要一次学完所有内容,也能先获得一个可以玩的结果。

推荐按照下面的顺序阅读源码:

  1. 先看 main(),了解 EGE 窗口、输入和每帧循环;
  2. 再看 Game::update(),理解得分、速度、转向和障碍移动;
  3. 阅读 spawnPattern(),尝试修改关卡组合;
  4. 阅读 drawTube()drawBox(),理解场景怎样由三角形组成;
  5. 最后再进入 project()rasterTriangle(),研究软件 3D 渲染。

第一次修改可以从这些小目标开始:

  • 给玩家和障碍换一套颜色;
  • 修改生命数量或每关持续时间;
  • 设计第九种障碍模式;
  • 增加最高分保存;
  • 加入碰撞音效和背景音乐;
  • 让隧道向左右弯曲,而不只是向下;
  • 加入第一人称或无尽练习模式。

每完成一个小目标,你都会更熟悉游戏状态、实时输入、二维像素和三维数学之间的关系。

编译和运行

Cube Runner 已经包含在 EGE 仓库的 demo/game_cube_runner.cpp 中。在 Windows 开发环境中获取最新源码并完成 EGE 项目配置后,可以构建 game_cube_runner 目标。

仓库的 VS Code 任务已经提供 Debug 和 Release 两种运行入口;对应的 Release 命令为:

程序默认使用 1280×720 分辨率,也可以通过参数指定其他尺寸:

建议优先使用 Release 模式运行。软件光栅化需要处理大量像素,Debug 构建的帧率通常会明显低于 Release。

现在,挑战一下自己的最高分

Cube Runner 的规则几秒钟就能理解,但想在高速障碍之间稳定穿行并不容易。它保留了早期休闲游戏最迷人的特点:没有复杂的养成和菜单,只需要专注于眼前的轨道、下一组障碍,以及不断增长的分数。

更重要的是,这不仅是一个“能玩”的 Demo。它还把向量、投影、光栅化、深度缓冲、游戏循环、碰撞检测和并发渲染放进了一份可以逐行阅读的 C++ 源码里。

如果你刚开始学习 EGE,不妨先运行游戏、挑战一次最高分,然后改一种颜色、设计一种障碍。很多图形编程知识,正是在“我想让它再好玩一点”的过程中真正学会的。

源码与背景资料

完整源代码

下面是当前 EGE 仓库中的完整 demo/game_cube_runner.cpp。将它放在已配置好的 EGE Demo 工程中,即可构建并运行;如果仓库后续继续更新,请以文首链接指向的最新版为准。

文章分类 范例 标签: , , , ,