Astra 已经很擅长将我脑中的想法转化为玩法和美术方向。我一直在 Codex 中用它构建 Void Explorer。这是一款太空探索游戏,您可以从遥远的恒星一路飞到外星地表。
Void Explorer 的游戏实录集锦。
游戏中有 2,048 个恒星系统和超过 10,000 颗程序化生成的行星,其中包括与地球一样大的星球。每颗可见的恒星都是这个宇宙的一部分,都可以选作目标。您可以选中远处的一个光点,然后飞往那里。
您可以从太空接近一颗行星,穿过大气层,持续下降,直到飞行在海岸线上空。您可以降落、走出飞船、四处走走,然后再次起飞。要让这段旅程成为现实,就必须统筹解决尺度、地形、操控和渲染问题。
我在文中收录了一些开发过程中使用的提示,并做了精简和润色。它们展示了我如何描述自己的需求,以及技术实现如何由此展开。
从体验出发
我最初的需求描述说明了玩家应该能做些什么:
我能看到的地方都应该可以抵达。保持真实的距离,再通过尺度和速度的处理让旅行切实可行。我想从太空飞入行星的大气层,再一路降到地面。行星可以和地球一样大,所以我们需要程序化地形和分块渲染器。
这给 Astra 提供了一些具体约束。恒星不能只是画在背景上的光点。行星不能在我靠近时变成一个独立关卡。旅行必须跨越真实距离,再用虚构的脉冲航行和超空间引擎,让这些距离在游戏中可以实际跨越。
在游戏的大部分内容尚未构建之前,我还用图像生成来探索视觉风格。最初的概念图过于写实,接下来的方向又太简单。这是我提出的一次修改意见:
现在有点过于简化了。我们需要一个折中的方案:更好的配色、霓虹灯光,以及能营造深空感的更强对比。让我看看高速飞行时的样子,飞船周围要有恒星和尘埃。
得到满意的图像后,我让 Astra 保存它们,并将这个方向转化为游戏的参考图。我们为轨道飞行、高速飞行、进入大气层和降落都设定了具体目标。有了这些图像,评判下一个版本就容易多了。

生成的概念图,确立了游戏的配色和视觉方向。
让 Astra 提出架构方案
我设定约束,Astra 提出实现方案。应用使用 TypeScript 和 Vite,并用 Three.js 进行渲染。这样就能通过代码直接控制网格、材质、光照和程序化几何体,而这款游戏的大部分视觉工作都集中在这些方面。
第一个可玩版本的渲染器使用了 WebGL2。后来,我问 Three.js 是否限制了画面表现。Astra 建议保留模拟系统,将渲染器迁移到 Three.js 的 WebGPU 架构。我们保留了宇宙、导航和地形系统,只更换了渲染层。
渲染器使用 Three.js 的节点材质和 Three.js Shading Language(TSL),在代码中定义大气、水体和光照效果。
地形生成在 Web Workers 中运行,因此可以在负责输入和绘制的线程之外准备几何体。Vitest 覆盖了生成结果的可重复性、坐标运算和地形契约等测试,Playwright 则在浏览器中测试游戏。这让 Astra 能够检查底层系统,而我继续评估游戏的实际体验。
整个过程中,美术方向始终是我们追求的目标。行星周围的大气、星环的辉光、恒星发出的彩色光线,以及 Neon Phosphor 特效,共同塑造了游戏的视觉风格。这个滤镜增添了复古显示器的质感,同时保持导航信息清晰可读。

启用 Neon Phosphor 特效的游戏截图。
让 Astra 能够检查和试玩
我一直在亲自试玩,但 Astra 也需要能独立调查问题的手段,不能只依靠我的描述。游戏提供了一个小型 JavaScript 接口 window.__VOID_EXPLORER__,浏览器测试可以调用它来检查当前状态:我正在接近哪个天体、当前飞行模式、地形是否就绪,以及当前使用的相机。它还提供渲染和流式加载计数器,包括绘制调用次数、三角形数量、排队中的地形任务数和已缓冲的地形数据量。
游戏为轨道飞行、脉冲航行、大气层下降和海岸降落准备了具名测试场景。这样,Astra 就能回到适合测试的起点,无须每次都飞越整个宇宙。Playwright 可以加载场景,等待地形就绪,截取画面,并检查画面背后的状态。另外还有独立的旅程测试,通过真实操控来运行游戏,记录过程中的位置和状态变化,确保不会用设置好一个降落场景来代替对降落过程本身的测试。
这些测试涵盖降落、离开飞船、行走、保存和重新加载、登船以及起飞。它们能发现单凭截图无法看出的问题,例如碰撞表面尚未就绪,或飞船在过渡过程中突然跳到另一个位置。
我也可以让 Astra 检查我正在试玩的浏览器标签页。当我问 Aurelia 的云层是否仍然可见时,它截取了我当前的画面,并读取屏幕上的飞行信息。这些检查只发生在特定时刻;Astra 并没有持续观察我飞行过程中的每一帧。
我们的迭代流程是:复现问题,检查截图和状态,追踪相关代码,进行修改,然后重新运行检查。如果再做一款游戏,我会尽早构建这些工具:几个可重复运行的场景、实用的状态和性能计数器,以及覆盖主要交互的浏览器测试。它们让 Astra 能独立调查问题并测试改动,而我继续评估画面和操控手感。
用多个尺度表示宇宙
拥有数千颗行星,并不意味着要加载数千个精细网格。一颗行星最初只是一组描述:半径、轨道、大气和种子。种子保证地形可以重复生成。游戏会在我正在接近的区域周围生成几何体,等我再次回来时,还能生成相同的景观。
坐标也需要同样谨慎地处理。以光年计的距离与站在飞船旁的一个人,处于截然不同的尺度。将这些巨大的位置数值直接传给 GPU,会丢失近地面场景所需的精度。
Astra 将物理位置与绘制时使用的位置分开。宇宙坐标由大整数网格单元和较小的单元内偏移量组成。渲染前,游戏会减去观察者的位置,让相机保持在原点,附近物体的坐标数值也随之变小。显示的距离和相对大小则保持一致。
运动中的物体还需要使用统一的时间。恒星、行星和卫星沿解析轨道运行,渲染器会用与观察者相同的模拟时刻来计算它们的位置。停放的飞船会随行星一起旋转。恒星的位置和颜色参与光照计算,因此双星日落中的两颗太阳,就是我在轨道上看到的那两颗恒星。
让下降过程连续衔接
在 Void Explorer 中从太空飞向一颗卫星的表面。
从太空到地面的过渡经历了多次迭代。一次以较小角度接近行星的经历,让我提出了下面这条提示:
当我以几乎平行于行星表面的方向接近时,速度仍然可能过快,而且地形加载时,行星几乎完全消失了。我们需要一起修正速度曲线和地形流式加载。能否用大气效果缓和远景 LOD 与精细地形之间的过渡?在接近过程中,行星始终都不应该消失。
飞行控制器现在会针对小角度接近单独设置速度上限,依据是当前高度,以及根据行星半径和重力估算出的轨道速度。接近地面时,它还会对前方地形采样,以调整制动。
游戏采用多个细节层级,即 LOD。在远处,行星是一个渲染开销很低的棱面球体,称为代理模型。随着它在屏幕上占据的面积增大,渲染器会逐步增加细节。接近表面时,则启用由立方体六个面投影到球面上构建的地形。每个面都可以反复细分为四个更小的正方形,这就是四叉树。
这样,游戏就能细化可见区域和飞行方向上的地形,而无须以满足步行需求的分辨率生成整个地球大小的表面。靠近地面时,局部地形块会在飞船和玩家周围提供更精细的几何体。
所有这些表示方式都对同一颗底层行星进行采样。一个共享函数组合了大陆、山脊、陨石坑和更细小的地势起伏,提供景观所需的高程、水体、生物群系等信号。粗细不同的网格以不同分辨率近似这些数据,并使用统一的配色规则,让行星外观保持一致。
不同表示之间的切换与生成同样重要。在六个粗略地形面全部就绪之前,远景行星会一直保持可见。地形瓦片也会保留在原处,直到它的四个子瓦片全部就绪。过渡期间,互补的像素遮罩将每个像素分配给旧表面或新表面,避免在替代模型加载时让整颗行星变得透明。
大气效果随实际高度变化,云层则在行星地貌上方移动,帮助轨道视角与地表景观衔接起来。渲染器只会在更精细地形已就绪的区域移除粗略覆盖层。不过,随着更精细的几何体出现,山体轮廓仍可能发生变化。
降落提出了更严格的要求:接触区域内的可见地面、碰撞三角形和危险液体区域,必须来自同一次生成,并同时就绪。行走时,游戏在一个小型局部物理世界中使用 Rapier。飞船的降落逻辑则会另外根据地形检查起落架支脚、坡度和净空。
这里有一个有意保留的限制:行星使用高度场,朝向表面的每个方向都只有一个高程值。这适合大尺度地貌,但无法提供洞穴、悬挑结构或可破坏的隧道。
测量慢帧背后的工作量
我的一些反馈同时涉及画面和性能,因为在同一次飞行中,我看到了这两类问题:
能否检查一下,当我从太空飞向地面时,行星是如何加载和渲染的?远处的球体与精细地形看起来不一致,过渡很突兀,尤其是在颜色变化时。我想弄清楚这些画面跳变的原因,以及哪些地方可以减少加载更多细节所需的工作量,让下降过程看起来更自然,运行也更流畅。
Astra 使用 Three.js 的渲染器计数器来检查绘制调用次数、三角形数量、几何体数量和纹理数量。一项 Playwright 测试收集了 90 个帧间隔,并在报告这些计数的同时,给出了平均耗时和各百分位耗时。对于视觉问题,测试还可以比较某个特效启用和禁用时的渲染像素,例如对比使用和不使用重叠地面遮罩时的地形。这有助于定位地面消失的原因,而不必只依靠一张出错场景的截图。
早期的一次飞船迭代说明了这些测量为什么有用。用 Blender 制作的第一个 AURORA 模型替换程序化生成的飞船后,场景的三角形数量增加了,绘制调用次数却减少了。Astra 在改动前后,使用 WebGPU 后端运行了相同的 90 帧轨道测试:
| 测量指标 | 改动前 | 改动后 |
|---|---|---|
| 场景绘制调用总次数 | 119 | 77 |
| 场景三角形总数 | 48,809 | 61,092 |
| 平均帧间隔 | 251.48 ms | 199.26 ms |
这次对比使用无头 Chromium 和 SwiftShader 软件渲染,测试时间早于下文展示的四翼飞船。它衡量的是该测试环境中的变化,并非我的 GPU 上的帧率。这些工具为 Astra 提供了浏览器计时数据、渲染图像和资源数量,但没有测量单条 GPU 指令或着色器的执行时间。
Astra 调查了加载和下降过程中的工作量:生成了多少几何数据,工作线程与渲染器之间传输了多少数据,以及地形任务在派上用场之前被丢弃的频率。
其中一项改动是根据远处行星在屏幕上的大小来分配细节。占满视野的行星需要立即呈现良好的轮廓,而小于一个像素的行星则不需要同等精细的几何结构。在对起始场景进行的受控对比中,调整后的策略使用了 7,040 个代理模型三角形,原先则为 51,200 个。两种策略使用相同的几何生成器,因此可以单独衡量加载决策的影响。
另一项改进则完全保留了地形三角形的数量。索引网格复用相邻三角形共享的顶点,无须重复存储其位置、颜色和法线数据。在地形质量设为“高”的测试中,六个地面层传输的缓冲区数据从约 35 MB 降至 15 MB,同时保留了 241,952 个三角形。地面和景物也被拆分为独立任务,因此地面几何数据可以先准备好,无须等待装饰物。
地形选择器也存在无效工作。它可能先请求一些区块,随后改变决策,将其丢弃,再请求替代区块。Astra 让这些细化决策更加稳定,并且只有在预算足以容纳全部四个子瓦片时才细化一个瓦片。在工作线程延迟固定的受控模拟中,经过六秒下降和随后三秒的稳定等待,被丢弃的任务从 6,074 个降至 13 个。

在使用两个工作线程、模拟工作线程延迟为 100 ms 的受控模拟中,被丢弃的已调度任务从 6,074 个降至 13 个。这里衡量的是地形调度,而非帧率。
仅靠工作线程无法解决所有卡顿。主线程上的地形生成回退逻辑也需要让出执行时间。Astra 将生成过程拆分为可恢复执行的步骤,每帧预算约为 4 ms,这样大型地形任务就不必在一次不间断的执行中全部完成。渲染器还将游戏世界的分辨率上限与界面分开控制,使导航文字在降低画质时仍保持清晰。
这些地形测量结果表明,几何数据量、传输数据量以及被丢弃的工作都减少了。它们并不是硬件帧率基准测试。要检验着色器编译、GPU 数据上传、运动表现以及过渡的实际视觉效果,仍然需要在浏览器中试玩。
我提供的是试玩反馈:哪里感觉慢、哪里看起来不对,以及我想改进什么。Astra 追踪代码、编写可重复运行的地形测试脚本,并记录测量结果。我认可方向后,它就落实改动,再次运行相同的测试来比较结果。我不需要规定每一步,它就能完成调查和实现,而我则持续检查游戏的视觉效果和操作体验。
将概念图转化为游戏资产
这个宇宙大部分由代码生成,包括行星、地形、云层、星环和恒星。玩家飞船是主要的单独建模制作的 3D 资产,我对它采用了不同的制作方式。
在将飞船放入游戏之前,我通过生成的概念图和 Blender 渲染图来检查它的效果。当机翼不符合我的设想时,我要求提供更有用的参考图:
请从多个角度生成这艘飞船的清晰视图,注意前翼和后翼。我们会用这些图像重新制作 3D 模型。我不喜欢当前模型上那种连在一起、圆润的机翼。
我选了一张参考图,图中的飞船有四片独立机翼,外形对称,翼尖带有粉色灯光。接着,我让 Astra 在 Blender 中制作一个新模型,保持游戏的美术风格,并将其加入游戏。
这个过程包括根据参考图构建几何结构、检查轮廓和材质,以及准备运行时资产。Blender 源文件包含 193 个可编辑网格。导出的飞船有 14,968 个三角形,分为八个不透明材质批次。我可以要求增加模型细节,同时让 Astra 控制导出模型的渲染开销。

我认可的生成版飞船概念图。

游戏中所用模型的 Blender 渲染图。
尝试程序化水体
我还花了很多时间在 Sunwake 中尝试水体效果。这款游戏让玩家驾驶小船穿行于程序化生成的海洋。我想要棱面分明的波浪、流畅的运动,以及彩色玻璃般的色彩。Astra 在 Three.js 中构建了自定义水体渲染器,用同一套波浪模型驱动可见海面和船只浮力。船体随波浪升沉、俯仰和横摇,局部模拟则负责涟漪和泡沫,船只驶过时会留下尾流并激起水花。后来,我让 Astra 在 Blender 中制作一艘细节更丰富的船,并将其导入游戏。这与 Void Explorer 的思路相同:通过模拟,将程序化环境与单独建模制作的载具连接起来。

Sunwake 将程序化海洋与在 Blender 中单独建模制作的船只结合起来。
Hollowflux 则沿着另一个方向展开:这是一款围绕发光地下河构建的小型 2D 动作 RPG。洞穴、角色和装备都由代码绘制,没有导入精灵图集。我不断让 Astra 改进行走、冲刺和攻击对水面的扰动,让效果更真实可信。由单元格组成的网格跟踪水面高度、水流、泡沫和电荷。脚步留下涟漪,长矛刺击划出狭窄的尾流,锤击则激起一圈圈波纹。水流沿河岸回旋,将泡沫带向下游,同时也推动玩家和敌人。水还会影响战斗:鳗鱼释放的电流会沿相连的水体传播,因此玩家踏上干燥的石头就能避开这次电击。

Hollowflux 用代码绘制游戏美术画面,其中的水体会对移动和战斗作出反应。
制作与分享游戏
我喜欢与 Astra 合作的一点是,我可以从自己想要的体验出发,用图像明确视觉方向,再根据实际试玩提供反馈。一次关于行星消失的反馈,可能带来流式加载、几何结构和移动逻辑的改动。一个让水体更真实可信的要求,可能变成视觉效果和游戏玩法共用的模拟系统。我仍然需要判断体验是否合适,但 Astra 能将这些决定落实到代码、资产和测试中。
分享浏览器游戏也很简单。使用站点插件,您可以这样请求 Astra:
发布时设为公开访问,任何人都可以直接在浏览器中游玩。
您可以在浏览器中游玩 Void Explorer、Sunwake 和 Hollowflux。
如果您心里一直有个游戏点子,不妨试着用 Astra 做出一个可玩的小版本。从一个您想打磨好的体验点入手,提供几张视觉参考图,再测试结果。一艘船、一个房间,或一次交互,都足以作为起点。亲自试玩,具体说明想改什么,再以此为基础继续构建。