[控场AI]
· 9 分钟阅读· 4,548 字

AI市长建城游戏GridFall:修复反光屋顶等三大问题

AI市长建城游戏GridFall:修复反光屋顶等三大问题

GridFall修复材质金属度、施工跳变与大世界性能三大问题,让城市安静下来。

GridFall 是一款由五个大模型 AI 市长共同运营的 Godot 4.7 城建游戏。开发者针对三个问题做了深度优化:所有 GLB 材质的金属度被错误设为 1.0,导致屋顶如镜面反射天空,通过材质名前缀在加载时按类别批量修正;施工阶段跳变源于视图仅在模拟 tick 时重绘且每阶段是固定画面,改为 tick 间补间并对阶段部件绑定进度区间,同时整体放慢建造节奏;世界从 96×96 扩大到 192×192 后,依靠 active 集合、邻域查询缓存、局部吞并搜索和树木分块低模替身维持约 60fps。代价是增长变慢,6 天测试人口从 3,136 降至 1,168,开发者认为更真实。项目代码与测试由 AI 编码代理 Droid 完成。

一个由五个AI市长共同运营的城市建造游戏GridFall,最近进行了一次深度优化。开发者发现,游戏里每一栋建筑的屋顶都像镜子一样反射天空,施工过程像幻灯片一样跳变,而他们想把世界扩大四倍。这三个看似独立的问题,背后各有清晰的成因,修复之后,整座城市终于变得「安静」了下来。

GridFall downtown after the fix

GridFall是什么

GridFall是一个用Godot 4.7开发的3D模拟城市类游戏,但它和传统城建游戏有个根本区别:你不建城,五个AI市长替你建。

每个市长运行在不同的大模型上,通过GMI Cloud推理,各自管理一个城区。游戏每天,它们会收到一张ASCII地图和各项统计数据,然后以JSON格式回复行动指令——修路、划区、盖楼、收税、吞并土地。模拟器会逐一检查每个行动,无效的会被拒绝并附上理由。城市里还住着约100名AI居民,他们生活、工作,也会去工地帮忙。此外,报纸、议会发言人和一个负责挑选灾难的「混乱导演」,也都跑在GMI的模型上。

游戏中的129个GLB建筑和道具,是在Blender里用Poly Haven的CC0贴图制作的。

问题一:每片屋顶都是镜子

从俯瞰视角看,所有屋顶都像抛光镀铬一样,亮得发蓝白,倒映着整片天空。

开发者把129个GLB文件里的所有材质导出检查了一遍,很快找到了根源:所有带贴图的材质进来时,metallic值都是1.0。而且它们的金属度贴图读取的是一张打包了AO/粗糙度/金属度(ARM)贴图的蓝色通道。于是渲染器把屋顶瓦片、砖块和混凝土全当成了金属——而金属在明亮天空下,自然会反射天空。

重新导出129个模型显然不现实。开发者选择了一个更聪明的方案:利用Blender管线中命名一致的材质前缀(Roof_*、Brick_*、Concrete_*、Metal_Corrugated_*等),让材质名自己说明它应该是什么表面。模型首次加载时,对每个共享材质执行一次修正:

  • 屋顶、砖、混凝土、灰泥、石头、木材、沥青等哑光材质:金属度归零、清除金属度贴图、镜面反射降到0.3
  • 屋顶、砾石、沥青、人行道:干脆清除粗糙度贴图,统一设为0.92的平坦哑光
  • 波纹金属板、覆层、百叶窗、集装箱:金属度降到0.35,粗糙度0.72
  • 玻璃和亮窗:金属度上限0.3,粗糙度下限0.14

调用方用一个材质实例ID集合做记录,无论多少建筑共用同一材质,它只会被调整一次。

开发者事后总结了两条经验。第一,导出材质数值比瞎猜快得多——「看起来发亮」可能是粗糙度、天空、屏幕空间反射或泛光造成的,但导出数据一眼就能看到砖块的metallic是1.0。第二,只清金属度贴图对屋顶不够,它们扫描出来的粗糙度贴图在低视角下依然显得光滑,所以屋顶、砾石和沥青需要直接压到0.92。

关于 metallic 与 ARM 打包贴图

在基于物理的渲染(PBR)中,metallic(金属度)决定一个表面是电介质(非金属)还是导体(金属)。非金属的金属度应为 0,其镜面反射颜色固定为白色、强度由粗糙度控制;金属的金属度应为 1,且其反射颜色由基础色(albedo)决定。把砖块、混凝土这类非金属材质的金属度设为 1.0,会让它们以金属的方式反射环境,在明亮天空下就呈现出镜面般的蓝白色。

ARM 贴图是一种常见的通道打包约定:同一张纹理的 R、G、B 通道分别存放环境光遮蔽(AO)、粗糙度(Roughness)和金属度(Metallic),以节省纹理采样和显存。因此材质读取金属度时并非使用独立贴图,而是取这张打包贴图的蓝色通道。这意味着修正时只改 metallic 标量还不够——只要金属度贴图仍被绑定,着色器就会继续从蓝通道取值覆盖标量,必须同时清除该贴图槽位才能生效。

问题二:施工在跳变,而不是逐步建造

在GridFall里,没有任何东西是瞬间出现的。每个建造指令都会开启一个工地,经历清理、挖掘、地基、框架、装修等阶段,由机械逐步完成。但玩家看到的却是:工地停在那里不动,然后突然跳到下一阶段。

原因有两个。

视图只在模拟tick时重绘。 模拟每个游戏日运行4个成长tick,每个tick承包商给每个工地加固定的工作量,视图按新的进度比例绘制。两个tick之间,画面纹丝不动。

修复思路是保持模拟的离散性,让视图在两个tick之间补间。每个工地记住上个tick获得的工作量,模拟器则给出当前tick已进行到的比例,视图每0.15秒计算一次平滑后的阶段进度。存档、AI的视野和测试仍然只看到完整的tick,只有绘制是平滑的。

每个阶段原本只是一张固定的图。 即使进度平滑了,「挖掘」阶段也只是一个凭空出现的坑。于是施工工具包增加了一个小助手:把工地的任意部件绑定到一段阶段进度区间,并声明它该如何变化。比如清理阶段树木逐渐消失、随后树桩被拔除;挖掘阶段土堆逐渐长高;地基阶段钢筋先出现、随后混凝土板铺满场地;拆除时旧楼缓缓下沉。

这里还踩了一个坑:永远不要把节点缩放到精确的零。退化的变换矩阵会扰乱剔除和变换计算,所以助手会把所有缩放钳制在至少0.02,并把几乎压扁的节点直接隐藏。

然后开发者把节奏整体放慢了。 原来的建造速度太快——警察局约一天,机场约四天。现在仅靠付费承包商,公园和道路需要1天,住宅2天,桥梁2.5天,警察局3天,医院约4天,高塔6天,煤电厂约9天,体育场12天,机场15天。公共建筑按造价缩放工期,有建造技能的居民仍可帮忙,但所有帮手加起来每tick最多贡献承包商一个班次的60%——在此之前,一群帮手一下午就能盖完一座体育场。每个城区同时最多只能跑10个私人住宅和商业工地,城市因此一小片一小片地稳步填充。

关于模拟 tick 与渲染帧的解耦

游戏主循环通常以固定步长推进模拟(tick),而渲染以显示器刷新率运行,二者频率并不一致。若视图只在模拟 tick 时更新,画面就会以 tick 为间隔跳变;tick 越稀疏,跳变越明显。常见做法是让渲染层在两个 tick 之间做插值:保存上一 tick 和当前 tick 的状态,按真实经过的时间在两者之间线性或缓动过渡,使画面连续。

这种做法要求模拟状态本身保持离散且可复现——存档、AI 决策、回放和自动化测试都应只读取 tick 边界上的确定状态,插值仅存在于表现层。GridFall 的做法正是如此:模拟仍以 4 个成长 tick/游戏日推进,视图每 0.15 秒计算一次平滑后的阶段进度,而 AI 视野与测试看到的仍是完整 tick,从而在获得流畅画面的同时不破坏模拟的确定性与可测试性。

问题三:世界大四倍,依然跑60帧

初始城市是32×32格,市长们通过吞并16×16的地块向外扩张。原本世界是96×96,现在扩大到192×192,共36,864格,是原来的四倍。

难点不在网格本身,而在每一个遍历整个网格的循环。开发者的优化分四步:

  • 只有已宣称的土地是活跃的。 模拟本来就有active集合(已宣称土地加3格边缘),但地震、洪水、城市生活、HUD、AI摘要和大部分视图处理仍在遍历每一格。把它们迁移到sim.active上,大世界就便宜了。
  • 缓存邻域查询。 犯罪率和地价计算每个tick都要为每一格问「2格内有没有水」,按格缓存后从14毫秒降到1.6毫秒。完整模拟步进现在约55毫秒,每个游戏日跑4次。
  • 只在城市附近搜索。 吞并搜索只查看城区的包围盒周围,约0.7毫秒。
  • 树木分块,远处用廉价版本。 世界大部分是森林,树木按48×48格分块绘制,每块带一个远景低模替身,利用可见性范围切换。块大小从24提到48,draw call减半;森林格除非着火,否则不再拥有自己的节点。

在开发者的RTX 3050笔记本上,默认视角约56–61 fps(约3,650次draw call),完全拉远时约65 fps。

城市边缘还做了「边疆感」处理:测量员会测绘到城市边界外两格,更远处笼罩在一层随城市扩张而消散的薄雾中。吞并成本也降低了——首块1,500美元,此后每块加400美元,市长们会被提示持续扩张。

旧存档仍然兼容。存档格式v3存储世界尺寸,加载v2存档时会把每个格子索引重映射到更大的网格里——工地、事件、损毁道路,以及每个居民的家、工作地和当前位置。

关于 draw call 与可见性范围

draw call 是 CPU 向 GPU 提交一次绘制命令的开销。场景中独立节点越多、材质切换越频繁,draw call 越高,CPU 侧瓶颈越明显。把大量小对象合并为按区块(chunk)组织的批次,能显著减少提交次数;再配合可见性范围(visibility range)机制——同一对象准备高模与低模两个版本,距离超过阈值时切换到低模甚至隐藏——可以在保持观感的同时压低几何与绘制成本。

GridFall 将树木按 48×48 格分块并附带远景替身,正是这一思路:块越大,批次越少、draw call 越低,而低模替身保证了远处森林不会因简化而穿帮。需要注意的是,分块过大会削弱视锥剔除的粒度,因此块尺寸需要在 draw call 数量与剔除效率之间取平衡。

代价与测试

更慢的建造意味着更慢的增长。在6天的无头冒烟测试中,人口现在是1,168,低于改动前的3,136。开发者对此表示接受:「一座要两周才能建好机场的城市,感觉更像一座城市。」

五个无头测试套件(模拟、建筑、居民、扩张、施工)加一个冒烟测试全部通过,后者离线运行六天,包含地震、维修队、覆盖层和存档往返。施工测试现在校验真实耗时:警察局12个tick,体育场在居民建造者参与下40个tick,仅承包商则需48个tick。

这个项目是怎么做出来的

值得一提的是,GridFall是借助Factory的Droid编码代理构建的——它编写了Godot代码、Blender资产管线和测试。这次更新中,开发者只是提出了那三个问题,并设定了目标(更慢更真实的建造时间、192×192的边疆),Droid找出了成因、完成修改、跑测试并截图。这篇文章本身也是AI起草的,由开发者审阅。

项目代码开源于GitHub。对于一个AI代理主导的模拟游戏来说,它和Settlement一样,试图让玩家看清「每个代理为什么这么做」——这也是GridFall公开每个市长的私人想法和被拒行动的原因。

对于关注AI代理、程序化生成和游戏性能优化的开发者来说,GridFall的这次修复提供了三个可复用的经验:用数据dump代替猜测、离散模拟与平滑视图分离、以及让数据结构(而非循环)决定性能上限。

#GridFall#AI城市建造游戏#Godot 4.7#材质金属度#施工动画平滑#游戏性能优化#AI市长#世界扩展
分享:

相关推荐