About defold-core
defold-core is a skill for developing and checking Defold game projects. It covers project resources, Lua lifecycle and messaging, spawning and world loading, with a reference checker and Bob-based build and runtime checks.
Key features
- 01Defold project resource and file guidance
- 02Lua lifecycle, input, and messaging patterns
- 03Runtime spawning and world loading
- 04Project reference validation script
- 05Bob build and headless runtime checks
Use cases
- 01Add or repair game objects and scripts
- 02Trace missing resource and build errors
- 03Implement input, messages, or object spawning
- 04Load levels through collection proxies
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ gamedev-skills/ awesome-gamedev-agent-skills --skill defold-coreAfter installing, type/defold-corein Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
agents/openai.yamlreferences/build-and-run.mdfile-formats.mdscripts/check_defold_project.pyDetails
From the project READMEDefold Core
Structure, wire and verify a Defold game: editor resource files, the Lua script lifecycle, addressing and messages, runtime spawning and world loading, and a Bob build-and-run check. Examples target Defold 1.14.0; they were built with Bob 1.14.0 and run on the 1.14.0 headless engine. An existing project keeps its Defold version (see Version).
When to use
- Use when adding or fixing game objects, collections, scripts, input, messages, spawning or
level loading in a Defold project (
game.projectin the root,.collection,.go,.script,.gui_scriptfiles), or when Bob reports a missing resource.
When not to use: plain Lua or LÖVE projects (love2d-core); GUI layout, sprites,
tilemaps and physics depth beyond the pointers in the references; engine-agnostic design
handled by the related skills below.
Workflow
- Inspect first. Read
game.project([bootstrap] main_collection,[input] game_binding,[project] dependencies), the main collection, and the scripts near the request. Follow the project's folder layout, id naming and message names; do not reorganize. Note which Defold/Bob version CI or the team uses. - Edit the right file type. Game objects (
.go) hold components; collections (.collection) place game objects and other collections; logic goes in.script(game objects),.gui_script(GUI) or shared.luamodules. Editor files are protobuf text: keep their structure, and copy shapes fromreferences/file-formats.mdinstead of guessing fields. - Check references. Run
python scripts/check_defold_project.py <project-dir>(path relative to this skill folder; Python 3.10+, no Defold install needed). Fix everyERROR(missing or wrong-case/paths, agame.projectpath without the compiledcsuffix, arequirewhose file does not exist). Warnings cover files nothing uses yet, unfetched library dependencies and runtime-loaded files. Re-run until it exits 0. - Build with Bob.
java -jar bob.jar --root . --variant headless --archive resolve buildneeds JDK 25 and abob.jarmatching the project's Defold version. Exit 1 plus anERROR:line means a broken reference or a Lua syntax error (with file and line);references/file-formats.mdshows how to trace an error that names a generated file. - Run it. A build does not prove the game works. Run a headless bundle under a timeout
(
references/build-and-run.md) and read the log:ERROR:SCRIPT:lines are Lua runtime errors,ERROR:GAMEOBJECT:lines bad addresses or socket clashes. Require a line your test prints when it finishes; the exit code alone proves nothing. The headless engine has no keyboard or mouse, so check input changes in the editor or a desktop build, and say what you could not verify.
Patterns
Project and resource references
game.project is an INI file in the project root. Resource paths everywhere are
root-absolute (/main/player.go). In game.project they name the compiled file, so the
source path gets a trailing c:
[bootstrap]
main_collection = /main/main.collectionc
[input]
game_binding = /input/game.input_bindingc
Bob builds only what is reachable from game.project; /builtins/... ships with the engine.
Collections, game objects, components
A collection places instances and can override script properties per instance; a game object
lists file components and embedded components (escaped data: strings). Full verified
files are in 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 }
}
}
Script lifecycle, self and properties
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
Callbacks: init, final, fixed_update, update, late_update (after every update,
before render), on_message, on_input, on_reload. Update order between game
objects is unspecified. fixed_update runs 0..N times per frame at [engine] fixed_update_frequency; set [physics] use_fixed_timestep = 1 too so physics steps at the
same rate, and put physics-driven code there. self is userdata: store
fields on it, but pairs(self) and pprint(self) do not work. Property types: number,
boolean, hash, msg.url, vmath.vector3/vector4, vmath.quat, resources such as
resource.atlas("/main/main.atlas"), and (since 1.13.2) strings.
Input bindings and the input stack
.input_binding maps devices to action names (key_trigger, mouse_trigger, plus
gamepad, touch and text triggers). Every component that acquired focus sits on an input
stack; the last to acquire is on top and gets input first, and returning true from
on_input stops it there, which is how pause menus block gameplay input. Key actions report
pressed and released; track held keys yourself as above.
Addressing and messages
A URL is [socket:][path][#fragment]: "." is this game object, "#" this component,
"#sprite" a sibling component, "enemy#ai" a relative object, "/manager#controller" an
absolute one, "main:/loader#loader" an object in another world. Collections create naming
scopes, so a string id is resolved relative to the sender's collection; a pre-hashed id is
always absolute.
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
Messages are queued and dispatched later in the frame; the table is capped at 2 KB. A bad
address logs ERROR:GAMEOBJECT: Instance '/x' could not be found when dispatching message.
Factories and collection factories
-- 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
Position and rotation default to the spawning object's. go.delete() takes effect at the
end of the frame. Per-instance properties for a collection factory are keyed by id:
{ [hash("/leader")] = { hp = 5 } }.
Collection proxies (load another world)
-- 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
The loaded world's socket is the name: of the loaded collection file, so scripts in it
reply with msg.post("main:/loader#loader", ...) and its own URLs read
level1:/level#level. set_time_step { factor = 0, mode = 0 } pauses a proxy world.
Version
game.project has no engine version key. Use the Defold editor or bob.jar version the
project is built with (CI often pins a release sha1), and library.defold_min_version when the
project is a library. java -jar bob.jar --version prints the version and sha1. Keep that
version; do not migrate unless asked.
Pitfalls
- Bob:
the bootstrap.main_collection '/main/main.collectio' can't be found→ the setting lacks the compiled suffix and Bob stripped the last letter. Write/main/main.collectionc(same forinput.game_binding→.input_bindingc). - Bob:
ERROR: build/default/_generated_….factory:0: the prototype '/main/x.go' can't be found→ an embedded component in some.gopoints at a missing file. Search the sources for the quoted path (/main/x.go), not the generated name, or run the checker. on_inputnever fires → the object never sentacquire_input_focus, the action name differs from the binding,input.game_bindingpoints elsewhere, or a listener higher on the stack returnstrue.- Input dead inside a proxy-loaded level → the game object holding the collection proxy did not acquire input focus.
already a socket with the same nameand the proxy never sendsproxy_loaded→ the loaded collection'sname:equals an already loaded world (often both are"main"or"default"). Give every proxied collection a unique name.fixed_updatenever runs →[engine] fixed_update_frequencyis 0 (default 60). For physics also set[physics] use_fixed_timestep = 1, as the manual requires both.module 'modules.util' not foundat run time although Bob succeeded → arequireused slashes (require "modules/util"). Use the dotted path from the project root,require "modules.util"for/modules/util.lua.tostring(id)printshash: [<unknown:…>]→ hashes keep their source text only in debug builds. Compare hashes, never their strings.- A second engine logs
Failed to initialize Remotery, or a test run never ends → the debug engine opens an engine service (port 8001), a log server and a profiler port, and waits forever withoutsys.exit. Run one engine at a time and always use a timeout.
References
references/file-formats.md— verified minimalgame.project,.collection,.go,.atlas,.input_bindingand script files, and how to trace build errors back to a source file; read before writing editor files by hand.references/build-and-run.md— JDK 25 andbob.jar, Bob commands, platforms and variants, a headless smoke test with timeout and log checks, and pointers for GUI, sprites, tilemaps and collision objects.scripts/check_defold_project.py— read-only reference checker;--helplists every check.
Related skills
input-systems— rebinding, gamepad support and buffering on top of Defold bindings.game-ui-ux— HUD and menu design to build with GUI scenes and.gui_script.physics-tuning— fixed-timestep and collision tuning for collision objects.camera-systems— follow/deadzone logic, typically applied inlate_update.save-systems— save formats and slots; pair with Defold'ssys.save/sys.load.game-feel— juice built fromgo.animateand timers.platformer,level-design— genre and layout guidance that binds to this skill.love2d-core— the other Lua engine in this catalog, for LÖVE projects.