Skip to content
Back to skills

Defold Core

ASecurity

Builds and debugs Defold 1.14 games in Lua: collections, game objects, input focus, msg.post messaging, factories, collection proxies and Bob builds. Use for .collection or .script files, on_input never firing, a proxy that never loads, or "could not be found when dispatching message". Not for LÖVE (`love2d-core`), HUD rules (`game-ui-ux`) or physics feel (`physics-tuning`).

  • 8,520 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 9, 2026
ai-agentspythongojavagitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned November 12, 2026

npx -y skills add gamedev-skills/awesome-gamedev-agent-skills --skill defold-core --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Defold Core?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Defold Core
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/gamedev-skills-defold-core/badge)](https://www.skillsdirectory.com/skills/gamedev-skills-defold-core)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: defold-core
description: >
  Builds and debugs Defold 1.14 games in Lua: collections, game objects, input focus, msg.post
  messaging, factories, collection proxies and Bob builds. Use for .collection or .script files,
  on_input never firing, a proxy that never loads, or "could not be found when dispatching
  message". Not for LÖVE (`love2d-core`), HUD rules (`game-ui-ux`) or physics feel
  (`physics-tuning`).
---

# 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.1**; they were built with Bob 1.14.1 and run on the 1.14.1
headless engine. An existing project keeps its Defold version (see [Version](#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

1. **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.
2. **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.
3. **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.
4. **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.
5. **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, mouse or screen, so for input, rendering or layout changes ask the user to
   run the game (editor **Project ▸ Build**, or the desktop bundle from the reference) and
   report what they see, 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`:

```ini
[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`.

```text
# 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

```lua
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. Since 1.14.1, `action.source`
is a hash (`mouse`, `touch`, `keyboard`, `gamepad`, `text` or `accelerometer`), which tells a
real mouse click from one simulated by touch.

### 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.

```lua
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

```lua
-- 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)

```lua
-- 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.

## Gotchas

- **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`.
- **An HTML5 build shows a black screen or shader errors only on old WebGL 1 devices, after
  moving to 1.14.1** → 1.14.1 stopped compiling GLES 2.0 / WebGL 1 shaders by default. Set
  `[shader] exclude_gles_sm100 = 0` in `game.project` if the game must still run there.
- **Bob fails at once with `UnsupportedClassVersionError … class file version 69.0`** →
  Bob 1.14.x needs Java 25; an older JDK is on the `PATH`.
- **`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

- Read `references/file-formats.md` before writing or repairing editor files by hand, or
  when a Bob error names a generated file: verified minimal `game.project`, `.collection`,
  `.go`, `.atlas`, `.input_binding` and script files, and how to trace errors to a source.
- Read `references/build-and-run.md` when you build, bundle or smoke-test: Java and
  `bob.jar` downloads, Bob commands, platforms and variants, the headless smoke test with
  timeout and log checks, ports, and manual pointers for GUI, sprites, tilemaps and
  collision objects.
- Run `scripts/check_defold_project.py` after editing resource files (workflow step 3);
  `--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.

## Version notes

Last verified: 2026-11-11.

- Defold 1.14.1 became the stable release on 2026-11-10 (sha1
  `ee283d4dad9fa451dd6399d83602e1dc25133681`); 1.14.0 was released on 2026-10-12.
- 1.14.1 changes that touch this skill: GLES 2.0 / WebGL 1 shaders are excluded by default
  (`[shader] exclude_gles_sm100` now defaults to 1), `action.source` in `on_input`, and the
  debug and headless engines print an OpenAPI URL for their engine service at startup.
- Bob 1.14.1 needs OpenJDK 25 (its classes are version 69).
- The example project in the references (player, factory, collection factory, proxy-loaded
  level), the smoke bundle and the quoted build and runtime errors were rebuilt with Bob
  1.14.1 and run on the 1.14.1 `dmengine_headless` with Temurin 25.0.5 on Linux x86_64.
  `action.source` was not run (the headless engine has no input); it comes from the
  release notes.
- Sources: [stable info.json](https://d.defold.com/stable/info.json),
  [1.14.1 release](https://github.com/defold/defold/releases/tag/1.14.1),
  [1.14.1 release notes (beta post)](https://forum.defold.com/t/defold-1-14-1-beta/83389),
  [GLES 2.0 default change (PR #13543)](https://github.com/defold/defold/pull/13543),
  [Bob manual](https://defold.com/manuals/bob/).

Files in this skill

  • SKILL.md12.6 KB
  • agents/openai.yaml229 B
  • references/build-and-run.md6.7 KB
  • references/file-formats.md7.2 KB
  • scripts/check_defold_project.py29 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in ai-agents →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.