关于 defold-core
defold-core 是用于开发和检查 Defold 游戏项目的技能,涵盖项目资源、Lua 生命周期与消息、对象生成和世界加载,并提供引用检查及基于 Bob 的构建和运行验证。
主要功能
- 01Defold 项目资源与文件指南
- 02Lua 生命周期、输入和消息模式
- 03运行时对象生成与世界加载
- 04项目引用检查脚本
- 05Bob 构建与无头运行检查
适用场景
- 01添加或修复游戏对象与脚本
- 02排查缺失资源和构建错误
- 03实现输入、消息或对象生成
- 04通过 collection proxy 加载关卡
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ gamedev-skills/ awesome-gamedev-agent-skills --skill defold-core安装后在 Claude Code 中输入/defold-core确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlreferences/build-and-run.mdfile-formats.mdscripts/check_defold_project.py详细介绍
Defold Core
构建 Defold 游戏的结构、连接与验证:编辑器资源文件、Lua 脚本生命周期、寻址与消息、运行时生成与世界加载,以及 Bob 构建和运行检查。 示例适用于 Defold 1.14.0;它们使用 Bob 1.14.0 构建,并在 1.14.0 无头引擎上运行。现有项目保留其 Defold 版本(参见版本)。
何时使用
- 在 Defold 项目中添加或修复游戏对象、集合、脚本、输入、消息、生成或关卡加载时使用(根目录中有
game.project,以及.collection、.go、.script、.gui_script文件),或 Bob 报告资源缺失时使用。
何时不使用: 纯 Lua 或 LÖVE 项目(love2d-core);参考资料中提示以外的 GUI 布局、精灵、瓦片地图和物理深度内容;以及以下相关技能处理的与引擎无关的设计。
工作流
- 先检查。 阅读
game.project([bootstrap] main_collection、[input] game_binding、[project] dependencies)、主集合以及请求涉及的脚本。遵循项目的文件夹布局、id 命名和消息名称;不要重新组织。记录 CI 或团队使用的 Defold/Bob 版本。 - 编辑正确的文件类型。 游戏对象(
.go)包含组件;集合(.collection)放置游戏对象和其他集合;逻辑写在.script(游戏对象)、.gui_script(GUI)或共享的.lua模块中。编辑器文件是 protobuf 文本:保留其结构,并从references/file-formats.md复制形状,不要猜测字段。 - 检查引用。 运行
python scripts/check_defold_project.py <project-dir>(相对于此技能文件夹的路径;需要 Python 3.10+,无需安装 Defold)。修复所有ERROR(缺失或大小写错误的/paths、缺少已编译c后缀的game.project路径、文件不存在的require)。警告涉及尚未使用的文件、未获取的库依赖项以及运行时加载的文件。反复运行,直到退出码为 0。 - 使用 Bob 构建。
java -jar bob.jar --root . --variant headless --archive resolve build需要 JDK 25,以及与项目 Defold 版本匹配的bob.jar。退出码为 1 且出现ERROR:行,表示引用损坏或 Lua 语法错误(含文件和行号);references/file-formats.md说明了如何追踪错误中提到的生成文件。 - 运行。 构建成功并不能证明游戏可正常运行。在超时限制下运行无头包(
references/build-and-run.md)并查看日志:ERROR:SCRIPT:行表示 Lua 运行时错误,ERROR:GAMEOBJECT:行表示地址错误或 socket 冲突。确认出现测试结束时输出的日志行;退出码本身无法证明任何事情。无头引擎没有键盘或鼠标,因此请在编辑器或桌面构建中检查输入变更,并说明哪些内容无法验证。
模式
项目和资源引用
game.project 是项目根目录中的 INI 文件。所有资源路径都是以根目录为起点的绝对路径(/main/player.go)。在 game.project 中,路径指向已编译文件,因此源路径末尾需要添加 c:
[bootstrap]
main_collection = /main/main.collectionc
[input]
game_binding = /input/game.input_bindingc
Bob 只会构建从 game.project 可达的内容;/builtins/... 随引擎提供。
集合、游戏对象、组件
集合放置实例,并可按实例覆盖脚本属性;游戏对象列出文件组件和嵌入式组件(转义后的 data: 字符串)。完整且已验证的文件见 references/file-formats.md。
# main.collection (excerpt): place player.go and override its script's speed
instances {
id: "player"
prototype: "/main/player.go"
component_properties {
id: "player"
properties { id: "speed" value: "250.0" type: PROPERTY_TYPE_NUMBER }
}
}
脚本生命周期、self 和属性
go.property("speed", 200) -- top level only, literal default; editable per instance
local LEFT = hash("left") -- hash ids once, not every frame
local RIGHT = hash("right")
local FIRE = hash("fire")
function init(self)
msg.post(".", "acquire_input_focus") -- no focus, no on_input calls
self.held = {} -- per-instance state lives on self
end
function update(self, dt)
local dir = (self.held[RIGHT] and 1 or 0) - (self.held[LEFT] and 1 or 0)
if dir ~= 0 then
local pos = go.get_position()
pos.x = pos.x + dir * self.speed * dt -- dt-scaled: same speed at any frame rate
go.set_position(pos)
end
end
function on_input(self, action_id, action)
if action_id == LEFT or action_id == RIGHT then
if action.pressed then self.held[action_id] = true
elseif action.released then self.held[action_id] = nil end
return true -- consumed: listeners lower on the stack skip it
elseif action_id == FIRE and action.pressed then
factory.create("#bulletfactory", go.get_position(), nil,
{ velocity = vmath.vector3(0, 600, 0) })
return true
end
end
function final(self)
msg.post(".", "release_input_focus")
end
回调:init、final、fixed_update、update、late_update(每次 update 后、渲染前)、on_message、on_input、on_reload。游戏对象之间的更新顺序未指定。每帧会按 [engine] fixed_update_frequency 运行 0..N 次 fixed_update;还要设置 [physics] use_fixed_timestep = 1,使物理步进频率相同,并将由物理驱动的代码放在这里。self 是 userdata:可以在其上存储字段,但 pairs(self) 和 pprint(self) 不起作用。属性类型:number、boolean、hash、msg.url、vmath.vector3/vector4、vmath.quat、资源(例如 resource.atlas("/main/main.atlas")),以及(从 1.13.2 开始)字符串。
输入绑定和输入栈
.input_binding 将设备映射到动作名称(key_trigger、mouse_trigger,以及 gamepad、touch 和 text triggers)。每个获取了焦点的组件都会进入输入栈;最后获取焦点的组件位于栈顶,并最先接收输入;从 on_input 返回 true 会在该组件处停止输入传递,这就是暂停菜单拦截游戏输入的方式。按键动作会报告 pressed 和 released;如上所述,自行跟踪按住的按键。
地址和消息
URL 格式为 [socket:][path][#fragment]:"." 表示当前游戏对象,"#" 表示当前组件,"#sprite" 表示同级组件,"enemy#ai" 表示相对对象,"/manager#controller" 表示绝对对象,"main:/loader#loader" 表示另一个世界中的对象。集合会创建命名范围,因此字符串 id 会相对于发送方所在的集合解析;预先哈希的 id 始终是绝对地址。
msg.post("/manager#controller", "enemy_spotted", { id = go.get_id() })
function on_message(self, message_id, message, sender)
if message_id == hash("enemy_spotted") then
print("from", sender, "id", message.id)
end
end
消息会排队,并在帧稍后分发;表的大小上限为 2 KB。地址无效时会记录 ERROR:GAMEOBJECT: Instance '/x' could not be found when dispatching message。
工厂和集合工厂
-- factory component "#bulletfactory" -> one game object; properties override go.property values
local id = factory.create("#bulletfactory", pos, nil, { velocity = vmath.vector3(0, 600, 0) })
-- collectionfactory component "#wavefactory" -> table of collection ids -> new instance ids
local ids = collectionfactory.create("#wavefactory", vmath.vector3(480, 500, 0))
local leader = ids[hash("/leader")] -- e.g. hash: [/collection0/leader]
go.delete(ids, true) -- later: delete them and their children
位置和旋转默认使用生成对象的位置和旋转。go.delete() 会在帧结束时生效。集合工厂的每个实例属性按 id 设置键:
{ [hash("/leader")] = { hp = 5 } }。
集合代理(加载另一个世界)
-- excerpt from the loader script; the collection's "#levelproxy" component is a collectionproxy
function init(self)
msg.post(".", "acquire_input_focus") -- else the loaded world gets no input
msg.post("#levelproxy", "async_load") -- or "load"
end
function on_message(self, message_id, message, sender)
if message_id == hash("proxy_loaded") then
msg.post(sender, "init")
msg.post(sender, "enable")
elseif message_id == hash("level_done") then
msg.post("#levelproxy", "unload") -- disables and finalizes first
elseif message_id == hash("proxy_unloaded") then
-- safe to load the next level here
end
end
已加载世界的 socket 是已加载集合文件的 name:,因此其中的脚本会用 msg.post("main:/loader#loader", ...) 回复,其自身的 URL 则写作 level1:/level#level。set_time_step { factor = 0, mode = 0 } 会暂停代理世界。
版本
game.project 不包含引擎版本键。请使用项目构建时所用的 Defold 编辑器或 bob.jar 版本(CI 通常会固定一个发行版 sha1);如果项目是库,则使用 library.defold_min_version。java -jar bob.jar --version 会打印版本和 sha1。保持使用该版本;除非有人要求,否则不要迁移。
常见问题
- Bob:
the bootstrap.main_collection '/main/main.collectio' can't be found→ 设置缺少编译后的后缀,Bob 去掉了最后一个字母。请写成/main/main.collectionc(input.game_binding同理,应为.input_bindingc)。 - Bob:
ERROR: build/default/_generated_….factory:0: the prototype '/main/x.go' can't be found→ 某个.go中的嵌入式组件指向了不存在的文件。请搜索源文件中的引号路径(/main/x.go),而不是生成的名称,或运行检查器。 on_input从不触发 → 对象从未发送acquire_input_focus,动作名称与绑定不一致,input.game_binding指向了其他位置,或者调用栈中更高层的监听器返回了true。- 通过代理加载的关卡中输入失效 → 持有集合代理的游戏对象未获取输入焦点。
- 出现
already a socket with the same name,且代理从不发送proxy_loaded→ 已加载集合的name:与已加载的世界同名(通常两者都是"main"或"default")。请为每个代理集合指定唯一名称。 fixed_update从不运行 →[engine] fixed_update_frequency为 0(默认值为 60)。对于物理模拟,还要设置[physics] use_fixed_timestep = 1,因为手册要求同时设置这两项。- Bob 构建成功,但运行时出现
module 'modules.util' not found→require使用了斜杠(require "modules/util")。对于/modules/util.lua,请使用从项目根目录开始的点分路径require "modules.util"。 tostring(id)输出hash: [<unknown:…>]→ 哈希仅在调试构建中保留其源文本。请比较哈希,不要比较它们的字符串。- 第二个引擎记录
Failed to initialize Remotery,或测试运行始终不结束 → 调试引擎会打开引擎服务(端口 8001)、日志服务器和分析器端口,并在未调用sys.exit时无限等待。一次只运行一个引擎,并始终使用超时。
参考资料
references/file-formats.md— 已验证的最小game.project、.collection、.go、.atlas、.input_binding和脚本文件,以及如何将构建错误追溯到源文件;手动编写编辑器文件前请先阅读。references/build-and-run.md— JDK 25 和bob.jar、Bob 命令、平台和变体、带超时和日志检查的无头冒烟测试,以及 GUI、 精灵、瓦片地图和碰撞对象的参考资料。scripts/check_defold_project.py— 只读参考检查器;--help会列出所有检查项。
相关技能
input-systems— 在 Defold 绑定的基础上实现重新绑定、手柄支持和输入缓冲。game-ui-ux— 使用 GUI 场景和.gui_script构建 HUD 和菜单。physics-tuning— 为碰撞对象调整固定时间步长和碰撞参数。camera-systems— 跟随和死区逻辑,通常在late_update中应用。save-systems— 存档格式和槽位;与 Defold 的sys.save/sys.load配合使用。game-feel— 使用go.animate和计时器增强游戏手感。platformer、level-design— 提供与此技能配套的类型和布局指导。love2d-core— 本目录中的另一款 Lua 引擎,适用于 LÖVE 项目。