Skip to content
云风 edited this page Jan 24, 2024 · 4 revisions

预制件

预制件 (prefab) 是从 Unity 中借鉴来的术语,Ant 的预制件并不是 Unity 中预制件的等价物。

Ant 的预制件通常是对外部导入的标准资产格式文件所作的二次编辑后的东西。这些 Asset 包括但不限于模型、动画、特效、声音、摄像机、灯光,等等。预制件通常由编辑器生产,或是从通用资产格式(gltf/glb) 转换而来。但因为其数据 DataList 是一种纯文本格式易于人读写,所以也不排斥用手工编写和修改预制件的内容。尤其在引擎开发的快速迭代期,新增加的特性所需要的编辑器工具很可能没来得及完善,更需要手工编写一些信息。

Ant 引擎基于 ECS 架构,一切数据都存放在 Entity 的 Component 中。而预制件,本质上是用来描述一个或多个 Entity 的实例化参数。ECS 是一种面向数据的编程框架,数据结构由 Component 定义,直接面对开发者。它对数据,并不像 OOP 范式那样,把数据结构隐藏起在对象里面,以 API 的形式来构造对象。这种形式会让构造 Entity 变得非常繁杂,你需要把构成 Entity 的诸多 Component 的初始化数据给的面面俱到。所以,我们提供了预制件文件,它的后缀名为 .prefab ,内部通常会包含一个或多个 ECS 中 Entity 的实例化用的数据。

预制件的实例化

例如在 ant.test.simple 中,我们示范了两种构造 Entity 的方法。其一是直接调用 world:create_entity() :

world:create_entity {
	policy = {
		"ant.render|simplerender",
	},
	data = {
		scene = {
			t = {0, 0, 0, 1},
			s = {500, 1, 500, 0}
		},
		material = "/pkg/ant.resources/materials/mesh_shadow.material",
		visible_state = "main_view",
		simplemesh = imesh.init_mesh(ientity.plane_mesh()),
	}
}

我们并不推荐在代码中这样实例化 Entity 。因为,代码和数据应该分离,几乎不会有正式的游戏项目会将这些数据直接写在代码中。所以,通常这些数据会在一个 .prefab 文件里,这个文件由编辑器创作。

上面这种写法,实际上在运行时创建了一个只包含一个 entity 的动态预制件。而一般我们会使用开发期创建好的预制件文件,也就是第二种写法:

world:create_instance {
	prefab = "/pkg/ant.test.simple/resource/light.prefab"
}

这里用 light.prefab 创建了一盏灯。所有灯光的数据都放在 light.prefab 文件中:

---
policy:
    ant.sky|skybox
    ant.render|ibl
    ant.render|simplerender
data:
    visible_state: main_queue
    scene: {}
    ibl:
      LUT:
        size: 256
      intensity: 30000
      prefilter:
        size: 128
      source:
        tex_name: /pkg/ant.resources.test/sky/colorcube2x2.texture
        facesize: 512
    material: $path ./skybox.material
    render_layer: "background"
    simplemesh: false
    skybox:
        facesize: 512
---
policy:
  ant.render|light
data:
  scene:
    r: {0.5, 0.0, 0.0, 0.8660253}
    t: {0, 10, 0, 1}
  light:
    color: {1, 1, 1, 1}
    intensity: 120000
    intensity_unit: lux
    type: directional
  make_shadow: true
  visible: true

light.prefab 里不只一个 Entity ,这里有两个:一个天空盒,一盏方向灯。

world:create_instance()world:create_entity() 都是从预制件在运行时实例化 Entity ,前者会返回多个 entity id (用数组返回),后者返回一个 entity id 。


在 .prefab 文件中,多个 Entity 用 --- 分割。这是 datalist 的一种语法,用来表示一个列表,对应 Lua 中的数组。每段 --- 分隔的数据表示一个 entity 应当如何实例化。这些数据会有 4 类,这个例子里展示了两种:policy 和 data ;还有两种 tag 和 mount 下面也会介绍。

policy

这个字段表示它需要创建哪些 Component ,这是一个 ECS 中的概念。表示了一个 Component 的集合。

data

这个字段表示用来实例化这个 Entity 用到的所有 Component 的实例化参数。我们将其称为 entity 的模板。

tag

预制件模块在加载 .prefab 文件后,会得到一个 entity 的实例化参数数组。用户在某些 api 中可以访问这个数组。但光用数组下标是不够的,我们可以给具体的某个 entity 模板起名字,方法就是加上 tag : name 这个字段。注:此处的 tag 不是 luaecs 中的 tag 。

tag 用一个字符串标识 entity 。多个 entity 模板可以有相同的 tag ,应该把 tag 理解上某种删选器。在对应 api 中,可以取到 tag 名对应的数组,里面可以找到所有打上这个 tag 的 entity 模板。

mount

这是用来表示场景树的。所有用 scene 这个 component 的 entity 模板都会存在于游戏场景中。游戏场景是用树结构表达的,每个节点都可以受一个父节点的 scene 的状态影响。mount 可以在实例化当前 entity 时,让它的 scene 的父亲指向同一个 prefab 中的另一个 entity 。注:父亲必须排在孩子的前面。

mount : 1 表示当前 entity 的父亲为该 prefab 的第一个 entity 。mount 后必须是一个 base 1 的数字,通常第一个节点是整个 prefab 的根,mount : 1 表示这个 entity 被挂接在这个预制件的根上。

一个预制件可以有多个场景根,有 scene 字段没有 mount 字段的 entity 模板都是根节点,在实例化时会创建为平级的场景节点。

Entity 的构造过程

Entity 是构造是异步的。在调用完 world:create_entity() 后,只返回了该 entity 的 id ,但这个 entity 还不可用。构造 entity 分成三个阶段:

第一阶段:通过 entity 模板,将由 policy 指定的所需每个 component 分别从模板实例化。该模板是由 create_entity 传入,或由 create_instance 从 .prefab 文件读入。在这个阶段,每个 component 都是单独初始化的,相互不会发生关联。

第二阶段:调用定义在 ECS pipeline 中的 entity_init 这个 stage 。任何系统,如果需要在创建 entity 时对多个有关联的 component 做一些初始化工作,可以在系统中定义 entity_init 这个 stage ,里面通过 world:select "INIT component_name:update" 筛选出刚刚创建出来的包含 component_name 的组件,进行初始化。这里的 INIT 是一个引擎定义的 ecs tag ,对于每个 entity 只会在创建阶段存在一帧。过了 entity_init 这个 stage 后,这个 tag 就会从 entity 身上消失。

第三阶段:当所有 entity_init stage 完成后,引擎认为整个 entity 已经初始化完成。这时,引擎会依次调用新创建出来的 entity 身上的 on_ready 组件中的函数。

on_ready 是一个可选 Lua 组件,它里面只包含一个 Lua 函数。

在 ant.test.features 里就有这么一个例子:

PC:create_entity {
	policy = {
		"ant.render|hitch_object",
	},
	data = {
		hitch = {
			group = test_gid,
		},
		visible_state = "main_view",
		scene = {
			t = {5, 2, 0, 1}
		},
		view_visible = true,
		on_ready = function (e)
			w:extend(e, "view_visible?in")
			print(e.view_visible)
		end
	}
}

它创建了一个挂钩 (hitch) ,把它设置为可见的 (view_visible) 。并在 on_ready 中输出了一行调试信息:查看 view_visible 这个 ecs 的 tag 是否正确设置。

这里,on_ready 是一个 lua 组件,里面保存的是 lua 函数。Lua 函数不能被持久化为模板,所以,我们不能在 .prefab 里定义它。通过 world:create_instance() 实例化一个预制件时,就无法为每个 entity 模板定义 on_ready 组件(函数)。取而代之的是,我们可以通过 create_instance() 的参数传入 on_ready 函数。

预制件的实例化即所有其包含的 entity 模板的实例化。所有 entity 其实都是一起完成的实例化过程。create_instance() 如果参数中包含一个 on_ready 函数,它会在这个 entity 集的最后加入一个只包含有 on_ready 组件的 entity 。因为这个 entity 会在最后完成实例化,所以它的 on_ready 函数被调用时,所有的 entity 都已经实例化了。

在 ant.test.simple 里有这样一段实例代码:

local prefab = world:create_instance {
	prefab = "/pkg/ant.test.simple/resource/miner-1.glb|mesh.prefab",
	on_ready = function ()
		local mq = w:first "main_queue camera_ref:in"
		local ce <close> = world:entity(mq.camera_ref, "camera:in")
		local dir = math3d.vector(0, -1, 1)
		if not icamera.focus_prefab(ce, entities, dir) then
			error "aabb not found"
		end
	end
}
entities = prefab.tag['*']

它通过一个包含在 miner-1.glb 中的预制件 mesh.prefab 实例化了一组 entity 。.glb 文件是通用格式 gltf 的二进制形式,通过 Asset 的自动编译过程后,内部的模型变成了一个叫 mesh 的预制件。注:mesh.prefab 是由引擎固定的名字,所有 gltf 文件中的所有模型网格,都一定会被转换为这个名字的预制件。

当预制件被实例化后,这个自定义的 on_ready() 会将主摄像机调整为朝向它 (focus_prefab) 。

entities 是在 create_instance() 函数返回后就获得的。它返回了一个 table ,里面有若干 tag 集合。'*' 是一个默认的 tag 集合,.prefab 文件中每个 entity 模板都可以有多个 tag 。每个 entity 模板,无论是否有其它 tag ,都至少会有一个名为 * 的 tag 。

entities = prefab.tag['*']

这一行的意义是,取出 prefab 实例化后所有的 entity id 数组。

create_instance 的返回值

world:create_instance() 返回了一个 table ,里面有四个字段:

  • tag 在上面解释过,它是一组组 tag 集合,每个集合是一个 entity id 的数组。tag 可以是 .prefab 文件中为 entity 模板标记上的字符串,同一个 entity 模板可以有多个不同的 tag 。无论有没有标记特别的 tag ,引擎都会为每个 entity 模板标记一个叫做 * 的 tag 。
  • group 是一个可选数字 id ,默认为 0 。Ant 引擎会给每个 entity 创建时都分配一个分组(之后不能修改),有很多功能都可以以 Group 形式工作,这在管理 Scene 中有海量 entity 时非常有用。我们可以在这里为预制件中每个 entity 指定一个 group id 。
  • noparent 没有被 mount 到其它 entity 上的 entity 都被归在这个集合下。换句话说,它记录了这个预制件实例化后的根节点。
  • proxy 上面提到,我们可以为这个预制件实例化过程添加一个带有 on_ready 组件的动态 entity 。proxy 指的就是它。

Entity 的销毁

对于单独 entity ,通常是用 world:create_entity() 构造出来的,可以使用 world:remove_entity() 销毁。

如果是由 world:create_instance() 实例化的一组 entity ,应该使用 world:remove_instance() 销毁。但你也可以单独用 world:remove_entity() 销毁其中的一部分。由于 entity 的 id 是唯一的,在修会后也不会被复用,所以即使用 remove_entity 先销毁了部分 entity ,再调用 remove_instance 也是安全的。即:同一个 entity id 可以被重复销毁。

销毁 entity 也是异步的,当调用完 remove_entityremove_instance 后,entity 并没有马上从 ecs 的 world 中抹掉。它们找当前帧的末尾才真正删除。如果你需要在 entity 真正被销毁时做一些事情,应该在你的 ECS 系统中添加一个叫做 entity_remove 的 stage ,在该 stage 中利用 world:select "REMOVED component_name:in" 删选出当前帧被删掉的组件,并做一些收尾的工作。

这里的 REMOVED 和前面的 INIT 类似,但是引擎定义的 ecs tag 。所有在当前帧被移除的 entity 都拥有这个 tag 。所以,我们可以利用 select 函数在 entity_remove 阶段筛选出来。这一帧结束后,这个 tag 连同整个 entity 都会从 world 中消失,只有 entity 的 64 位 id 保留下来,不会被后来的 entity 复用。

Clone this wiki locally