引言
Defold 的工作单位是 Game Object(GO),但实际游戏极少只用一个 GO。几十上百个 GO 怎么组织?答案是 Collection(集合)。Collection 不仅把 GO 打包成层级树,还提供了运行时动态创建的能力——这就是 Factory(工厂)。本文系统讲 Defold Collection 与 Factory:先从 Collection 的层级结构和 .collection 文件讲起,再讲 Factory 组件动态生成单实例、collectionfactory 批量加载整组对象,最后覆盖实例生命周期管理(删除/销毁)、延迟实例化策略,以及如何用 Collection 实现动态关卡加载。
前置:/defold-editor-asset-pipeline/(编辑器与资源管理)。Game Object 与组件基础见 /defold-game-engine-introduction/。
目录
- 1. Collection 是什么?GO 的「文件夹」
- 2. Collection 层级与 .collection 文件结构
- 3. Factory 组件:动态创建单个 GO
- 4. collectionfactory:批量加载整组对象
- 5. 实例生命周期:创建、销毁与内存管理
- 6. 延迟实例化与动态关卡加载
- 7. 常见问题与调试
- 8. 速查表
- 相关阅读
- 延伸阅读
1. Collection 是什么?GO 的「文件夹」
Collection = 多个 Game Object 的层级容器,类比 Unity 的 Scene 或 Godot 的 Scene。
核心能力:
| 能力 | 说明 |
|---|---|
| 树形层级 | 支持嵌套 Collection,形成父子关系 |
| 统一管理 | 批量加载/卸载一组 GO |
| 变换传播 | 父节点移动/旋转/缩放 → 子节点同步 |
| 作用域隔离 | Collection 级变量、消息命名空间 |
Collection 与 Game Object 的关系:
["main.collection"] ← 根 Collection
├── ["player" Game Object]
│ ├── ["sprite" Component]
│ ├── ["collisionobject" Component]
│ └── ["script" Component]
├── ["enemies" Collection] ← 嵌套子 Collection
│ ├── ["enemy_1" Game Object]
│ ├── ["enemy_2" Game Object]
│ └── ["spawner" Game Object] ← 含 Factory 组件
└── ["ui" Collection]
├── ["hud" Game Object]
└── ["dialog" Game Object]
记忆:Collection 是「GO 的文件夹」,可以嵌套——子 Collection 的变换受父 Collection 影响,形成完整的场景树。
2. Collection 层级与 .collection 文件结构
.collection 文件 = Defold 的纯文本场景格式,JSON 结构,版本控制友好。
文件示例(简化版):
{
"name": "main",
"nodes": [
{
"name": "player",
"type": "go",
"position": [0, 0, 0],
"components": [
{ "id": "sprite" },
{ "id": "script" }
],
"children": ["enemies_collection", "ui_collection"]
},
{
"name": "enemies_collection",
"type": "collection",
"position": [100, 0, 0],
"nodes": [
{ "name": "enemy_1", "type": "go" },
{ "name": "enemy_2", "type": "go" }
]
}
]
}
Defold 编辑器会生成更完整的格式,但核心结构不变:nodes → type(go/collection) → components → children。
关键点:
1. Collection 可以套 Collection(嵌套 Collection)
2. 同一 Collection 内所有 GO、Component 共享消息命名空间
3. 跨 Collection 通信 → 用 msg.post(url),URL 包含 Collection 路径
URL 寻址:必须在 Collection 内唯一
/go_id#component_id
/collection_id/go_id#component_id
-- 引用同一 Collection 内的组件
msg.post("/player#script", "jump", { force = 10 })
-- 跨 Collection 引用
msg.post("/enemies_collection/enemy_1#script", "damage", { amount = 5 })
记忆:URL = /collection(go)…/go_id#component_id——本地通信可以省略 Collection 根路径,跨 Collection 必须带全路径。
3. Factory 组件:动态创建单个 GO
Factory = 运行时按模板动态生成 Game Object 实例。
应用场景:
- 子弹发射(每发一个 GO)
- 敌人生成(按波次刷怪)
- 道具掉落(随机生成物品)
- UI 弹窗(动态创建对话框)
3.1 添加 Factory 组件
- 在编辑器中选中 Game Object
- 右键 → Add Component → Factory
- 将 Prototype 属性指向一个
.go文件(模板)
说明:Prototype 即模板 GO,Factory 不创建 GO 本身,而是引用它作为克隆模板。
3.2 factory.create API
-- 基础用法:在 spawner 脚本中动态创建实例
function on_message(self, message_id, message, sender)
if message_id == hash("spawn_bullet") then
-- factory.create(factory_url, [position], [rotation], [properties])
local bullet = factory.create("#bullet_factory",
vmath.vector3(message.pos.x, message.pos.y, 0),
nil,
{
direction = message.direction,
speed = 800
}
)
end
end
参数详解:
| 参数 | 类型 | 说明 |
|---|---|---|
| factory_url | string | Factory 组件的 URL(如 #bullet_factory) |
| position | vector3 | 可选,实例在世界的初始位置 |
| rotation | quaternion | 可选,初始旋转 |
| properties | table | 可选,实例化后通过 init() 的 self 参数传入 |
说明:
properties通过脚本模板的init(self)中self遍历读取,不是所有属性都自动绑定。
3.3 工厂模板消耗弹药
-- factory.unload 与更多控制
function on_message(self, message_id, message, sender)
if message_id == hash("change_bullet_type") then
-- 切换模板(必须在运行时加载了对应原型)
factory.set_prototype("#bullet_factory", message.new_prototype)
end
end
Defold 的 Factory 通常绑定固定 Prototype,切换模板需求少。更常见是多个 Factory 分别对应不同模板。
4. collectionfactory:批量加载整组对象
collectionfactory = 批量创建一组 GO,对应加载一个 .collection 模板。
应用场景:
- 关卡切换(加载整关的对象集合)
- boss 战准备(加载 boss 及所有伴随对象)
- UI 场景切换(加载完整的 HUD 集合)
- 房间加载(Rogue 游戏的新房间生成)
4.1 配置 collectionfactory
-- collectionfactory.create 基础用法
function load_level(self, level_name)
-- 加载 level_1.collection 模板
local instances, id_map = collectionfactory.create("#level_factory",
vmath.vector3(0, 0, 0), -- 根位置
nil, -- 根旋转
{ level = level_name } -- 自定义属性
)
end
返回值解析:
instances = { -- 创建的 Game Object 实例 ID 列表
[1] = hash("/level_1/camera"),
[2] = hash("/level_1/player"),
[3] = hash("/level_1/enemy_01"),
...
}
id_map = { -- 如果模板中 GO 有 name → 返回对应的 ID
[hash("camera")] = hash("/level_1/camera"),
[hash("player")] = hash("/level_1/player")
}
记忆:collectionfactory 返回两个值——所有实例的列表 + 按名称映射的 ID Map——用 ID Map 可以快速找到特定 GO。
4.2 动态属性与脚本通信
function on_level_loaded(self, id_map)
-- 获取 player 的实例 URL
local player_id = id_map[hash("player")]
if player_id then
-- 向 player 发送初始参数
msg.post(player_id, "set_spawn", { x = 100, y = 200 })
end
-- 广播给所有实例
msg.post("/level_1", "level_ready")
end
4.3 动态替换 Prototype
-- 运行时切换 collection 原型
collectionfactory.set_prototype("#level_factory", "#collections/level_2.collection")
使用场景:同一 Factory 组件动态切换关卡模板,避免创建多个 Collection Factory。
5. 实例生命周期:创建、销毁与内存管理
5.1 实例创建后的管理
-- 保存创建的实例,用于后续销毁
self.bullets = self.bullets or {}
function fire(self)
local bullet_id = factory.create("#bullet_factory",
self.pos + vmath.vector3(0, 1, 0),
nil,
{ direction = self.facing }
)
table.insert(self.bullets, bullet_id)
end
5.2 销毁实例
-- 方式 1:销毁单个 GO
go.delete(bullet_id)
-- 方式 2:销毁整个 Collection(含所有子 GO)
go.delete("/level_1", true) -- true = 递归销毁所有子对象
-- 方式 3:在自身脚本中销毁(自毁)
go.delete()
销毁回调:
-- 在模板脚本的 final() 中执行清理
function final(self)
-- 移除 rigibody 约束、释放引用等
msg.post("/world#physics", "remove_body", { id = self.body_id })
print("Bullet destroyed: " .. tostring(self));
end
记忆:go.delete() 会触发目标对象脚本的 final()——这是做清理(解绑事件、释放资源)的关键时机。
5.3 对象池:复用而非销毁
频繁创建/销毁会导致 GC 压力和内存碎片。对象池方案:
-- 对象池管理器(pool.lua)
local M = { pool = {}, active = {} }
function M.get_bullet()
if #M.pool > 0 then
local id = table.remove(M.pool)
M.active[id] = true
msg.post(id, "reset")
return id
else
return factory.create("#bullet_factory")
end
end
function M.recycle_bullet(id)
M.active[id] = nil
msg.post(id, "hide")
table.insert(M.pool, id)
end
return M
6. 延迟实例化与动态关卡加载
6.1 延迟实例化策略
游戏不需要一次性加载所有对象,按需创建:
-- 视锥加载:只在可视区域附近创建敌人
function update(self, dt)
for _, spawner in ipairs(self.spawners) do
local dist = vmath.length(spawner.pos - self.camera_pos)
if dist < 800 and not spawner.spawned then
spawner.spawned = true
factory.create("#enemy_factory", spawner.pos)
elseif dist > 1000 and spawner.spawned then
spawner.spawned = false
-- 可选:销毁远处对象
end
end
end
6.2 动态关卡加载
-- level_manager.script:控制多个 Collection 的加载/卸载
function init(self)
self.current_level = nil
self.queued_level = nil
end
function load_level(self, level_collection_path)
-- 1. 卸载旧关卡
if self.current_level then
go.delete(self.current_level, true)
self.current_level = nil
end
-- 2. 动态加载新关卡 Collection
local instances, id_map = collectionfactory.create("#level_factory",
vmath.vector3(0, 0, 0),
nil,
{ level_path = level_collection_path }
)
self.current_level = id_map[hash("root")] or instances[1]
self.level_map = id_map
-- 3. 通知场景管理器
msg.post("/scene_manager#script", "level_loaded", { map = id_map })
end
function on_message(self, message_id, message, sender)
if message_id == hash("change_level") then
load_level(self, message.level_path)
end
end
加载过渡配合:
-- 配合 loading UI 做过渡动画
function load_level_with_transition(self, path)
msg.post("/ui#loading", "show")
timer.delay(0.3, false, function()
load_level(self, path)
timer.delay(0.2, false, function()
msg.post("/ui#loading", "hide")
end)
end)
end
记忆:动态关卡 = 先卸载旧 Collection + 再创建新 Collection + 过渡 UI——collectionfactory 让整个流程像切场景一样简单。
7. 常见问题与调试
| 问题 | 原因 | 解决 |
|---|---|---|
| factory.create 返回 nil | Prototype 未正确设置 | 检查 Factory 组件的 Prototype 属性 |
| 创建的 GO 消息收不到 | URL 拼写错误 | 用 pprint(url) 核对确切路径 |
| 大量对象导致卡顿 | 每帧创建/销毁 | 用对象池 + 批量延迟创建 |
| collectionfactory 报 key 冲突 | 同名的 GO ID 已存在 | 确保新 Collection 不重复命名根节点 |
| 子 GO 位置不对 | 父 Collection 变换未应用 | 检查 Collection 根节点的位置参数 |
调试技巧:
-- factory.create 后 print 实例 URL
local id = factory.create("#factory", pos)
print("Created instance:", id)
-- 查看场景树输出(Build 时 Debug 选项)
-- 或手动遍历已知实例
for _, bullet_id in ipairs(self.bullets) do
print("Active bullet:", bullet_id)
end
8. 速查表
| 需求 | API | 参数/说明 |
|---|---|---|
| 动态创建单个 GO | factory.create(url, pos, rot, props) | 返回实例 URL |
| 动态加载整组 GO | collectionfactory.create(url, pos, rot, props) | 返回实例列表 + ID Map |
| 设置 Prototype | factory.set_prototype(url, prototype) | 运行时切换模板 |
| 销毁 GO | go.delete(id, [recursive]) | recursive 递归销毁子对象 |
| 切换 Collection | 先 go.delete 旧 + 再 collectionfactory.create 新 | 配合过渡动画 |
| 对象池复用 | 隐藏(停用)+ 重置代替销毁 | 避免 GC 压力 |
| 按需加载 | 视锥距离 + 延迟实例化 | 大场景必备 |
一句话记忆:Collection 是 GO 的树形文件夹,factory 动态克隆单个 GO,collectionfactory 批量加载整组——生命周期管理靠 go.delete(清理走 final)、高频场景用对象池,动态关卡配合 collectionfactory.create + 卸载旧场景 + 过渡 UI。
相关阅读
- /defold-game-engine-introduction/ — Defold 核心概念:Game Object 与 Component
- /defold-editor-asset-pipeline/ — 编辑器、资源管理与 Collection 组织
- /defold-script-system-lua/ — Lua 脚本、消息路由与生命周期
- /defold-performance-optimization/ — 对象池与 Draw Call 优化
延伸阅读
- /defold-level-editor/ — 关卡编辑器与场景设计
- /defold-ui-gui/ — UI 场景管理与动态弹窗
- /defold-save-serialization/ — 关卡状态持久化
- /defold-multiplayer-sync/ — 多人游戏的对象同步
- [[game]] — 游戏引擎场景管理原理
-- ======================================
-- 完整可运行示例:Factory + Collection + 关卡管理
-- 将此代码放入 spawner.script
-- ======================================
go.property("bullet_speed", 800)
go.property("bullet_prototype", resource.atlas(""))
local bullet_pool = {}
local active_bullets = {}
local current_level = nil
function init(self)
self.pos = go.get_position()
self.spawn_timer = 0
self.spawn_interval = 0.5
print("[init] Spawner 已就绪,位置:", self.pos)
end
function update(self, dt)
self.spawn_timer = self.spawn_timer + dt
if self.spawn_timer >= self.spawn_interval then
self.spawn_timer = 0
spawn_bullet(self)
end
-- 移除超出边界的子弹
for id, _ in pairs(active_bullets) do
local p = go.get_position(id)
if p.y > 600 then
go.delete(id)
active_bullets[id] = nil
end
end
end
function spawn_bullet(self)
local bullet_id = factory.create("#bullet_factory",
self.pos + vmath.vector3(0, 1, 0),
nil,
{
direction = vmath.vector3(0, 1, 0),
speed = self.bullet_speed
}
)
active_bullets[bullet_id] = true
print("[spawn] 创建子弹:", bullet_id)
end
function load_next_level(self, level_collection)
-- 卸载旧关卡
if current_level then
go.delete(current_level, true)
current_level = nil
end
-- 加载新关卡 Collection
local instances, id_map = collectionfactory.create("#level_factory",
vmath.vector3(0, 0, 0),
nil,
{ level = level_collection }
)
if id_map and id_map[hash("level_root")] then
current_level = id_map[hash("level_root")]
elseif instances and #instances > 0 then
current_level = instances[1]
end
print("[load_level] 加载完成:", current_level)
end
function on_message(self, message_id, message, sender)
if message_id == hash("spawn_bullet") then
spawn_bullet(self)
elseif message_id == hash("change_level") then
load_next_level(self, message.level)
elseif message_id == hash("clear_bullets") then
for id, _ in pairs(active_bullets) do
go.delete(id)
end
active_bullets = {}
end
end
function final(self)
-- 清理所有活跃子弹
for id, _ in pairs(active_bullets) do
go.delete(id)
end
active_bullets = {}
print("[final] Spawner 已销毁,清理完成")
end
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。