跳到正文

使用 Astra 构建游戏

本文目录文章开头

Astra 非常擅长将我脑海中的想法转化为游戏玩法和美术方向。我一直在 Codex 中使用它来构建 Void Explorer,这是一款太空探索游戏,你可以从遥远的恒星旅行到外星地表。

Void Explorer 的游戏玩法超集锦。

这款游戏拥有 2,048 个恒星系统和超过 10,000 颗程序生成的行星,其中包括与地球大小相当的世界。每一颗可见的恒星都属于这个宇宙,并且可以作为目标。你可以选择远处的一个光点并前往那里。

你可以从太空接近一颗行星,穿过它的大气层,不断下降,直到飞越海岸线。你可以着陆,走出飞船,四处走动,然后再次起飞。让这段旅程得以实现,意味着要同时解决尺度、地形、控制和渲染的问题。

我收录了一些构建过程中的提示词,为简洁和清晰起见进行了编辑。它们展示了我是如何描述我想要的东西,以及技术工作是如何由此展开的。

从体验开始

我最初的简报描述了玩家应该能够做什么:

我能看到的一切都应该可以到达。保持真实的距离,然后通过尺度和速度让旅行变得可行。我想从太空飞入行星的大气层并降落到地面。行星可以像地球一样大,所以我们需要程序化地形和分块渲染器。

这给了 Astra 一些具体的约束。一颗恒星不能只是背景中画的一个点。当我接近一颗行星时,它不能变成一个独立的关卡。旅行必须覆盖真实的距离,虚构的脉冲旅行和超空间引擎让这些距离变得可行。

我还使用图像生成来在构建游戏大部分内容之前确定外观。最初的构想太写实了。下一个方向又太简单了。这是我的一次修正:

现在这有点太简单了。我们需要一个折中方案:更好的颜色、霓虹灯光,以及能唤起深空感的更多对比度。给我看看高速飞行时的样子,飞船周围有星星和尘埃。

一旦我喜欢这些图像,我就让 Astra 保存它们,并将这个方向转化为游戏的参考。我们为轨道飞行、高速、大气层进入和着陆设定了具体的目标。这些图像让评估下一个构建版本变得容易得多。

Generated concept art of an ivory ship above a faceted cyan planet, with magenta accents and two warm suns

生成的概念艺术,确立了游戏的调色板和视觉方向。

让 Astra 提出架构

我设定了约束,Astra 提出了实现方案。该应用程序使用 TypeScript 和 Vite,渲染使用 Three.js。这让代码可以直接控制网格、材质、光照和程序化几何体,而这正是这款游戏大部分视觉工作的所在。

第一个可玩的渲染器使用 WebGL2。后来,我问 Three.js 是否限制了视觉效果。Astra 建议保留模拟部分,并将渲染器迁移到 Three.js 的 WebGPU 架构。我们保留了宇宙、导航和地形系统,同时更换了渲染层。

渲染器使用 Three.js 的节点材质和 Three.js 着色语言 (TSL) 在代码中定义大气、水和光照效果。

地形生成在 Web Workers 中运行,以便在处理输入和绘制的线程之外准备几何体。Vitest 覆盖了可重复生成、坐标数学和地形契约等内容。Playwright 在浏览器中测试游戏。这让 Astra 有了检查底层系统的方法,而我则继续评估游戏的手感。

艺术指导始终是目标。行星周围的大气、光环的辉光、恒星的彩色光芒以及 Neon Phosphor 效果共同塑造了这种外观。该滤镜在保持导航可读性的同时,增添了复古显示纹理。

Void Explorer gameplay showing the four-wing ship, a cyan planet, magenta rings, twin suns, and the navigation display

一张启用了 Neon Phosphor 效果的游戏画面截图。

为 Astra 提供检查和游玩的方式

我自己一直在玩这款游戏,但 Astra 也需要除了阅读我的描述之外的方式来调查问题。游戏公开了一个小型 JavaScript 接口,window.__VOID_EXPLORER__,浏览器测试可以调用它来检查当前状态:我正在接近哪个天体、飞行模式、地形就绪情况以及当前激活的摄像机。它还公开了渲染和流式传输计数器,包括绘制调用、三角形数量、排队的地形任务和缓冲的地形数据。

游戏为轨道、脉冲旅行、大气层下降和海岸着陆等场景设有命名测试场景。这样 Astra 就能回到一个有用的起点,而无需每次都飞越整个宇宙。Playwright 可以加载一个场景,等待地形就绪,截取屏幕截图,并检查其背后的状态。单独的旅程测试使用真实控件,并在游戏运行时记录位置和状态变化,因此设置着陆场景并不能替代对着陆本身的测试。

这些测试涵盖了着陆、离开飞船、行走、保存和重新加载、登船以及起飞。它们能发现仅靠截图无法揭示的问题,例如未就绪的碰撞表面,或在过渡期间飞船在位置之间跳变。

我也可以让 Astra 检查我正在游玩的浏览器标签页。当我问 Aurelia 的云层是否仍然可见时,它截取了我当前的视图,并读取了屏幕上的飞行信息。这些是在特定时刻进行的检查;Astra 并没有持续监视我飞行过程中的每一帧。

这个循环是:复现问题、检查截图和状态、追踪相关代码、做出修改,然后重新运行检查。在另一款游戏中,我会尽早构建这些工具:几个可重复的场景、有用的状态和性能计数器,以及针对主要交互的浏览器测试。它们让 Astra 能够独立地调查和测试更改,而我则继续审视外观和操控手感。

在多个尺度上表示宇宙

拥有数千颗行星并不意味着要加载数千个精细网格。一颗行星始于一个描述:它的半径、轨道、大气和种子。种子使其地形可重复生成。游戏会在我接近的区域周围生成几何结构,当我返回时,它可以再次生成相同的景观。

坐标也需要类似的谨慎处理。光年距离和站在飞船旁的人是截然不同的尺度。将这些巨大的位置直接发送到 GPU 会丢失在地面附近所需的精度。

Astra 将物理地址与用于绘制的位置分离开来。宇宙使用带有较小局部偏移的大型整数单元格。在渲染之前,游戏会减去观察者的位置,因此摄像机保持在原点,附近物体的坐标则很小。显示的距离和相对大小保持一致。

运动部件还需要在时间上保持一致。恒星、行星和卫星遵循解析轨道,渲染器在与观察者相同的模拟时间计算它们的位置。停泊的飞船会附着在自转的行星上。恒星的位置和颜色会输入到光照系统中,因此双星日落中的两颗太阳就是我从轨道上看到的那些恒星。

使下降过程连续

从太空飞向卫星表面(Void Explorer)。

从太空到地面的过渡经历了大量迭代。我的一个提示词来自以浅角度接近行星时:

当我几乎与行星平行接近时,我仍然可能进入得太快,行星在地形加载时几乎消失。我们需要同时修复速度曲线和地形流式加载。大气层能否缓和远处 LOD 与详细地形之间的过渡?行星在接近过程中绝不应消失。

飞行控制器现在会根据高度和基于行星半径与重力估算的轨道速度,对浅角度接近施加单独的速度上限。接近地面时,它还会采样前方地形以调整刹车。

游戏使用多个细节层次,即 LOD。从远处看,行星是一个廉价的多面球体,称为代理体(proxy)。随着它占据更多屏幕空间,渲染器会增加细节。接近表面时会激活由六个立方体面投影到球体上构建的地形。每个面可以反复分裂成四个更小的方块:即四叉树。

这样游戏就能在可见区域和行进方向上细化地形,而无需以步行分辨率生成地球大小的表面。接近地面时,局部区块会为飞船和玩家周围提供更精细的几何体。

所有这些表示都采样同一颗底层行星。一个共享函数将大陆、山脉、陨石坑和更小的起伏组合在一起。它提供海拔、水体、生物群系和其他景观使用的信号。粗略和精细网格以不同分辨率近似该数据,并使用共享的颜色策略来保持行星外观的一致性。

交接与生成同样重要。远处的行星会保持可见,直到所有六个粗略地形面都准备就绪。一个地形区块会保持原位,直到其全部四个子区块都准备就绪。在过渡期间,互补的像素蒙版会将每个像素分配给旧表面或新表面。这样就避免了在替换加载时让整颗行星变得透明。

大气层随物理高度变化,云层在行星地理上空移动。它们有助于将轨道视图与景观连接起来。渲染器只会在更精细的地形准备就绪时移除粗略覆盖。随着更精细的几何体出现,山脉轮廓仍可能改变。

着陆增加了一个更严格的要求:接触区域的可见地面、碰撞三角形和液体危险物必须全部来自同一次生成并同时就绪。行走使用 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 稳定了这些细化决策,只有在预算允许所有四个子区块时才细化一个区块。在具有固定工作线程延迟的受控模拟中,六秒的下降随后三秒的稳定产生了 13 个被丢弃的任务,而不是 6,074 个。

Line chart of discarded scheduled terrain jobs over nine simulated seconds. The previous selector reaches 6,074 jobs; the revised selector stays at 13. The camera stops after six seconds.

在一次受控模拟中,使用两个 worker 和 100 ms 的模拟 worker 延迟,被丢弃的计划任务从 6,074 个降至 13 个。这衡量的是地形调度,而非帧率。

仅靠 worker 并不能解决所有卡顿问题。主线程上的地形回退逻辑同样需要让出执行权。Astra 将生成过程拆分为可恢复的步骤,每帧预算约为 4 ms,这样大型地形任务就不必在一个不间断的块中完成。渲染器还独立于界面限制世界分辨率,从而在降低视觉质量时保持导航文字清晰。

这些地形测量结果表明几何体更少、传输数据更少、被丢弃的工作更少。它们并非硬件帧率基准测试。浏览器实机测试对于着色器编译、GPU 上传、运动效果以及过渡的实际观感仍然很重要。

我的输入来自游玩时的反馈:哪些地方感觉慢、哪些地方看起来不对、以及我想改进什么。Astra 追踪代码、编写可重复的地形测试脚本并记录测量结果。在我认可方向后,它实施了更改并重新运行相同的测试以比较结果。它在没有我逐步指导的情况下完成了这项调查和实施,而我则持续审查游戏的观感和手感。

将概念美术转化为游戏资产

宇宙大部分是在代码中生成的:行星、地形、云、光环和恒星。玩家飞船是主要的人工制作 3D 资产,我对它采取了不同的方法。

我使用生成的概念图和 Blender 渲染图在飞船进入游戏前进行审查。当机翼与我心目中的样子不符时,我要求提供更有用的参考:

从多个角度生成这艘飞船的清晰视图。注意前翼和后翼。我们将使用这些图像来重做 3D 模型。我不喜欢当前模型上那种连在一起的圆润机翼。

我挑选了具有四个独立机翼、对称形状且翼尖带有粉色灯光的参考图。然后我让 Astra 在 Blender 中构建一个新模型,保持游戏的艺术风格,并将其加入游戏。

这涉及将参考图转化为几何体、审查轮廓和材质,以及准备运行时资产。Blender 源文件包含 193 个可编辑网格。导出的飞船有 14,968 个三角形,分为八个不透明材质批次。我可以在模型中要求更多细节,而 Astra 则控制导出的渲染成本。

Generated top-view concept showing four separate wings, pink wingtip lights, an ivory hull, and a green canopy

我批准的生成飞船概念图。

Blender render of the ship's ceramic hull, four separate wings, and twin cyan engines

游戏中所用模型的 Blender 渲染图。

尝试程序化水面

我还花了很多时间在 Sunwake 中尝试水面效果,这是一款驾驶小船穿越程序化海洋的游戏。我想要有棱角的波浪、平滑的运动以及彩色玻璃般的色彩。Astra 在 Three.js 中构建了一个自定义水面渲染器,用同一个波浪模型驱动可见的海面和船只的浮力。船体随波浪起伏、纵摇和横摇,同时局部模拟承载涟漪和泡沫,船只留下尾迹和浪花。后来我让 Astra 在 Blender 中构建一艘更精细的船并将其导入游戏。这与 Void Explorer 的方法相同:一个程序化环境加上一艘人工制作的载具,通过模拟连接在一起。

Sunwake gameplay showing an orange boat leaving a foamy wake across blue faceted waves under a pink sunrise

Sunwake 将程序化海洋与在 Blender 中构建的人工船只结合在一起。

Hollowflux 则走了另一条路:一款围绕发光地下河构建的小型 2D 动作角色扮演游戏。洞穴、角色和装备全部用代码绘制,不使用导入的精灵图集。我不断要求 Astra 让行走、冲刺和攻击对水面的扰动更加逼真。一个由单元格组成的网格追踪水位、水流、泡沫和电荷。脚步会留下涟漪,长矛突刺划出狭窄的尾迹,锤击则荡开一圈圈波纹。水流沿岸边卷曲并将泡沫带向下游,同时这股水流也会推动玩家和敌人。水还会影响战斗:鳗鱼的放电会通过相连的水域传导,因此踏上干燥的石面可以让玩家免受这种放电的伤害。

A pixel adventurer wades through a glowing teal river in Hollowflux, surrounded by ripples, creatures, and dark cave walls

Hollowflux 用代码绘制游戏美术,其中的水会对移动和战斗做出反应。

构建与分享游戏

我喜欢与 Astra 合作的一点是,我可以从我想要的体验出发,用图片让视觉方向变得具体,然后通过实际游玩来给出反馈。关于一颗消失的星球的抱怨,可以引发流式传输、几何结构和移动方式的改动。关于更逼真的水的请求,可以变成一个由视觉和玩法共享的模拟系统。我仍然需要判断它感觉是否正确,但 Astra 可以将这些决定贯彻到代码、资源和测试中。

分享浏览器游戏也很简单。通过 Sites 插件,你可以这样要求 Astra:

以公开访问权限发布,任何人都可以直接在浏览器中游玩。

你可以在浏览器中游玩 Void Explorer、Sunwake 和 Hollowflux。

如果你心中有一个游戏创意,试着用 Astra 构建一个可玩的小版本。从你希望感觉正确的一件事开始,给它一些视觉参考,然后测试结果。一艘船、一个房间或一个简单的交互就足以起步。玩一玩,具体说明你想改变什么,然后继续在此基础上构建。

阅读显示设置

调整正文排版

当前浏览器记住选择,应用于文章正文。手机上正文宽度以屏幕可用空间为限。

图表完整性提示

普通文章遇到图表保存问题时显示提示。