#3 用鼠标建造监狱,以及会自己“长”出来的房间
· Nick
第二个里程碑完成了,第一次有了可以看的东西。Godot 客户端现在架在模拟核心之上,你可以用鼠标建造一座单层监狱:铺地板,画墙,放门,标记房间,拆除。一切都是普通方块和纯色,里面也还没有人。但它是一个地方,一个你可以用相机绕着走的地方。
现在能做什么
你开始新游戏,选择地图大小(边长 64、128 或 256 个瓦片)和可选的种子,就会得到一张空网格。相机是固定倾角的正交相机,严格按 90° 为步进旋转,朝着光标缩放,可以用键盘或鼠标中键平移,还有一个用于建造的俯视模式。如果相机能自由旋转,墙会把房间挡住,所以它不能自由旋转。
地板和拆除按矩形操作。墙沿着轮廓线绘制(按住 Shift 则画直线)。门只需一次点击,会根据相邻的墙确定方向,还有一个按键可以旋转它。拖动时,光标下的区域会显示为虚影:绿色瓦片会被建造,红色瓦片会被跳过,标签会说明数量和原因。松手之前不会向世界发送任何东西,如果核心表示一块瓦片都不会变,那就什么也不发送。右键或 Escape 可取消。
世界模型里有多个楼层,所以有一个楼层选择器,当前楼层以上的楼层会被隐藏。房间有自己的工具,下文会讲。调试叠加层会显示区块边界、可通行瓦片、光标下的瓦片,以及一个耗时面板。界面上的每个词都是英文。
预览就是真实结果
与结果不一致的虚影,比没有虚影更糟。所以客户端不会去猜建造命令会做什么。核心可以针对同一片区域回答“如果执行这条命令会怎样”,逐块瓦片回答,并且对每一块会拒绝的瓦片给出原因。填充预览的这项检查,和命令真正执行时用的是同一条规则,所以两者不会走偏。客户端只负责把答案画出来。只有当影响答案的东西发生变化时,它才会再次询问核心:工具、拖动的区域、楼层,或者地图本身。
同样的原则也塑造了客户端的其余部分:它读取世界的状态并发送命令,自己没有任何模拟逻辑。目前核心在主线程上 tick,点击产生的命令会在下一帧开始、世界 tick 之前应用。如果等世界里住满了人、tick 耗时开始飙升,计划是把核心搬到它自己的线程上,并为何时这样做设定一个实测的阈值。
会自己被识别出来的房间
我考察了两种做房间的方式。在 Prison Architect 里,你画一个区域,它就成为拥有自己瓦片的独立对象。这很容易理解,但放到监狱里就不对劲了:整个牢房区会变成一个区域,而每次你重建一面墙,就得重新涂一遍。另一种方式是自动找出封闭的区域,但那样你就没法标记一个四周没有墙的放风场。
我选的是一种混合方案。唯一存储的东西,是每块瓦片上的房间类型:牢房、食堂、浴室、医务室、探视室、放风场等等。其余的一切都由墙推算出来。一个空间是同一楼层上、由边相连的一组开放瓦片;如果它不接触地图边缘或空洞,它就是封闭的。一个房间是同一个空间内所有同一类型的瓦片。墙和门是边界,涂在墙下面的类型会被保留但被忽略,所以把墙拆掉后它又会回来。
这样一来,无需任何额外说明,行为就很自然。在房间中间砌一堵墙,它就变成两个同类型的房间。把墙拆掉,它们就合并。在原来有墙的地方放一扇门,什么都不会变。无效的房间不会被删除,因为那恰好违背了“毫无痛苦”;它会以红色显示,并附上原因:未封闭、与另一种类型共用、太小,或者没有门。你可以涂一个矩形,一键填满整个封闭空间,或者清除它。
房间列表是派生数据,所以不会被保存,也不属于世界哈希;只有每块瓦片上的类型才是。它在命令之后和系统之后重建,检查一个没有变化的世界不花任何代价。在这个里程碑中,发生变化的楼层会整层重新计算,在 256×256 的地图上大约需要半毫秒;按区块逐块来做,可以等到寻路的区域需要同样的机制时再说。我给它定的规则是:增量重建必须得到与完整重建相同的结果。
只重建变化的部分,楼层切片零成本
地图是按每个楼层的每个区块画成一个网格,而不是每面墙一个对象。核心里的每个区块都有版本计数器,渲染器会记住它上次绘制的是哪个版本。世界变化时,只有版本更新的区块才会进入队列,队列按每帧的时间预算来处理,所以一次大改动会分摊到几帧,而不是让某一帧卡住。涂画房间类型不会提升地图版本,所以根本不会唤醒网格构建器。
楼层切片遵循同样的“少做事”的思路。当前楼层以上的楼层干脆隐藏起来。当前楼层的墙由着色器压到一米高,用的是一个共享材质和一个高度值,所以切换楼层或切换完整墙体不会重建任何东西。在四个楼层之间来回切换,网格重建的次数为零。
十厘米的缝隙
当我在最终检查中试用第一座单层监狱时,上一层有一块地板,从侧面看,墙的顶端和它上方的楼板之间有一条细细的亮线。在墙角处,你能看到地板悬在半空。我先怀疑是楼层切片,接着怀疑是 z-fighting,再然后怀疑是平滑处理。
都不是。地板是一个没有厚度的平面四边形,画在其所在楼层高度再加十厘米的位置。下层的墙恰好升到三米。上一层的地板则从三米零十厘米的高度开始。缝隙有十厘米宽,透过它能看到墙上被照亮的顶面。水平方向上一切都对得严丝合缝,这也是我花了一阵子才看出来的原因。
修复方法是让地板成为有真实厚度的楼板,向下延伸到它所在楼层的起点。顶面的高度不变,所以用鼠标选瓦片以及所有叠加层都不受影响。在同一区块内,地板或墙相接的地方会跳过侧面,而在区块边界处则会绘制侧面,因此一个区块的网格从不依赖它的邻居。这样一来,楼板的端部会留下一圈十厘米的暗色边缘。我看了修改前后的截图,决定保留它。它看起来就像地板的边缘,而它本来也就是。
经得起考验吗
这个里程碑的最后一项任务,是用鼠标建一座单层监狱,看看用起来难不难受。大体上不难受。除了上面的缝隙,这次试用还发现了一些又小又烦人的问题:用鼠标选另一个工具时,工具按钮不会取消高亮;以及无法用 Escape 退出“新游戏”对话框。两个都已修复。暂停菜单已经列在以后要考虑的想法清单上。
接下来
接下来是 M2,移动与日常作息。世界将获得区域与可达性、跨楼层的寻路,以及用于人群的流场。核心终于每个 tick 都有了真正的活要干,基准测试的门槛也将不再沉睡。参见路线图。