Defold Collections、Factories 与动态实例化

系统讲解 Defold 的 Collection 与 Factory 机制:层级关系与 .collection 文件结构、Factory 组件动态生成 Game Object 实例、collectionfactory 批量加载、实例生命周期管理、延迟实例化与动态关卡加载。

引言

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 的「文件夹」

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 组件

  1. 在编辑器中选中 Game Object
  2. 右键 → Add Component → Factory
  3. 将 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_urlstringFactory 组件的 URL(如 #bullet_factory)
positionvector3可选,实例在世界的初始位置
rotationquaternion可选,初始旋转
propertiestable可选,实例化后通过 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 返回 nilPrototype 未正确设置检查 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参数/说明
动态创建单个 GOfactory.create(url, pos, rot, props)返回实例 URL
动态加载整组 GOcollectionfactory.create(url, pos, rot, props)返回实例列表 + ID Map
设置 Prototypefactory.set_prototype(url, prototype)运行时切换模板
销毁 GOgo.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

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「defold」更多文章

  1. Defold Tilemap 碰撞与关卡设计
  2. Defold 性能分析与调试工具链
  3. Defold IAP 与广告接入:内购流程、Store 验证与广告变现