Skip to content
云风 edited this page Jan 24, 2024 · 1 revision

分组和挂钩

分组 (group) 是场景管理 (Scene) 的一部分。

当我们的游戏场景非常巨大时,将整个虚拟场景都加载进内存会有很大的开销。这个开销不仅仅是占用内存,引擎在渲染时也需要在一个更大的集合中找到待渲染的 entity 。虽然引擎有裁剪系统,可以剔除许多不在镜头内的 entity 。但如果开发者可以有更简单的方法对 entity 分类,就可以用更小的成本做这些事。

分组

例如,我们可以对游戏场景按地理位置划分区块。一个大的区块上的物件归在一组。每个区块上还可以按景观建筑(很远都可以看见)和细节物体(只有离近了才会注意到)进一步的分为两组。如果你愿意,可以把室外物件和室内物件也分开,如果摄像机在室外,就看不到室内的物件。

因为相比渲染器,开发者有更高阶的宏观知识知道这些分类,可以比渲染器用底层的细节去计算每个对象的空间信息要廉价的多。

ECS 中的数据管理模块 luaecs 在底层提供了 group 的支持。它允许在构造 entity 时给它设置一个 int32 类型的 id 。id 相同的 entity 属于同一个集合,可以更容易的将它们筛选出来。这个 group id 在 entity 创建后就不可更改。

可见性标签

每个 entity 都可以有一个可见性标签。它是一个 ecs 中的 tag view_visible 。开发者一般不需要直接控制这个 tag ,引擎的渲染模块会利用这个 tag 来决定一个 entity 是否需要处理。是它帮助我们筛选出整个 world 中的一个子集。换句话说,只有带有 view_visible 的 entity 才是引擎关心的,你可以创建很多 entity 放在内存中,但只给少部分加上这个 tag ,就能帮助引擎减轻负担。

所有 group id 为 0 的 entity 默认都带有可见性标签,它们默认就会被引擎处理到。非 0 group id 的 entity 默认会被引擎忽略。但可以运行时动态改变这个规则:

local ig = ecs.require "ant.group|group"
ig.enable(gid, "view_visible", true)

这样,就可以让 gid 默认变为可见的。如果想管理一个非常大的场景,这是一个常见优化手段:将物件分到不同的组里,当摄像机的位置发生变化时,计算哪些 group 是可见的,调用这个 enable 函数调整引擎中 group 的可见性。

挂钩

在很多游戏中,场景中的物件在运行过程中会改变外形。例如,一个角色原来是一只猫,可能游戏规则会把它变成一只老鼠。如果我们一开始用猫的 Prefab 实例化出一个 entity ,当它变成老鼠后,就需要删除原有的猫的实例,重新实例化一只老鼠出来。整个 entity 连同 entity id 都变了。

或许开发者希望这个 entity id 不要发生变化,有些其它的 entity (例如写有角色名字的名牌)还附着在上面,我们只是想更换表示这个角色的模型而已。这时,就可以使用挂钩。

原本 socket (插槽)可能是一个更常见的术语。但容易和网络 socket 混淆,所以我们选择了 hitch (挂钩) 来表示这个概念。表示一个 entity 本身没有可渲染的物件,但却是一个场景对象。可以挂接一组可渲染的物件在上面,继承这个 hitch 在场景中的空间状态。

hitch 有非常多的应用场合。比如,你可以给人物的背后设置一个挂钩,然后就可以在运行时把刀剑等武器挂在上面。在引擎的编辑器中,可以给其它软件制作的 gltf 资产增加 hitch ,生成 Prefab 。在预制件实例化时,就一起将这些 hitch 创建出来了。通过在预制件里标注 tag ,可以方便的找到这些预制的挂钩。

使用挂钩前,需要在创建 entity 或通过预制件实例化一组 entity 时,指定一个 group id 。默认情况下,引擎不会关心非 0 的 group id 。利用这个特性,只要指定合适 group id ,实例化出来的 entity 不会被引擎渲染。

在 hitch 上有一个属性叫 group ,默认为 0 。这表示这个 hitch 是空的。当给这个 group 设置其它值时,等价于在 hitch 的位置上,有一组对应的 entity 需要渲染。

world:create_instance {
	prefab = filename,
	group = 42,
}

这样就可以实例化一个 filename 文件中定义的预制件,赋予 42 这个分组。实际游戏代码中通常不会写这样的常量,开发者需要自己写一个 group id 管理模块来管理这些数字。你还可以实例化多个预制件,共享同一个 id ,它们会在同一个分组中。

world:create_entity {
	policy = { "ant.render|hitch_object" },
	data = {
		scene = { t = {0, 3, 0} },
		hitch = { group = 42 },
		visible_state = "main_view",
	}
}

这样,我们便创建了一个 hitch ,它一开始就挂接了 42 号 group 。hitch 这个组件中的 group 是运行时可以修改的。我们推荐用编辑器制作一个 hitch 的预制件来取代上面的代码。

相同物件的合并渲染

挂钩的另一个用途是提高渲染的效率。

多个 hitch 可以挂接相同的 group id 。如果场景中存在这样的多个挂有相同 group id 的 hitch ,渲染器就能够知道,有一组 entity 需要被渲染多次。它们除了世界矩阵不同,其它所有属性都是相同的。这可以帮助引擎合并相同的图形指令,使用合适的手段来优化这些渲染过程。

在同一个 group 中的 entity ,包括但不限于静态模型、带骨骼动画的模型、特效等。其优化手段在实现上各不相同。但这些优化细节使用者不需要了解。

如果多个物件需要有各自独立的状态,就不适合用 hitch 优化。例如,有一个人物模型,这个模型带有动画信息。若这个模型在场景上出现了两次,你需要独立控制两者运动动画的时间线。那么就不适合用两个 hitch 引用相同的人物模型;反之,如果两个人物永远做完全相同的动作,即看起来它们永远是一模一样的,就可以用 hitch 来引用两次。引擎也会对这样的用法做相应的优化。

大量的相同物件是最适合用 hitch 优化的。例如在我们制作的 factorio like 游戏中,场景中有大量飞行中的无人机。虽然无人机飞行时有一些动画表现,但我们可以接受它们飞起来是一模一样的,这里,就可以为所有无人机各创建一个 hitch ,这些 hitch 引用着同一个 group 。该 group 中只有一个无人机模型。在播放无人机飞行的动画时,也仅需要控制这一个 entity ,而不必处理很多对象。