Defold 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.project in the root, .collection, .go,
.script, .gui_script files), 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 .lua modules. Editor files are
protobuf text: keep their structure, and copy shapes from
references/file-formats.md instead 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 every ERROR
(missing or wrong-case /paths, a game.project path without the compiled c suffix,
a require whose 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 build needs JDK 25 and a bob.jar matching the project's Defold version. Exit 1 plus
an ERROR: line means a broken reference or a Lua syntax error (with file and line);
references/file-formats.md shows 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 for input.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 .go points at a missing file. Search the
sources for the quoted path (/main/x.go), not the generated name, or run the checker.
on_input never fires → the object never sent acquire_input_focus, the action name
differs from the binding, input.game_binding points elsewhere, or a listener higher on
the stack returns true.
- 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 name and the proxy never sends proxy_loaded → the
loaded collection's name: equals an already loaded world (often both are "main" or
"default"). Give every proxied collection a unique name.
fixed_update never runs → [engine] fixed_update_frequency is 0 (default 60). For
physics also set [physics] use_fixed_timestep = 1, as the manual requires both.
module 'modules.util' not found at run time although Bob succeeded → a require used
slashes (require "modules/util"). Use the dotted path from the project root,
require "modules.util" for /modules/util.lua.
tostring(id) prints hash: [<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 without sys.exit. Run one engine at a time and always use a timeout.
References
references/file-formats.md — verified minimal game.project, .collection, .go,
.atlas, .input_binding and 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 and bob.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; --help lists 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 in late_update.
save-systems — save formats and slots; pair with Defold's sys.save/sys.load.
game-feel — juice built from go.animate and 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.