Skip to content

Tutorial Spawning Objects

johnjohto edited this page Jul 30, 2026 · 1 revision

Tutorial: Spawning Objects

Put something on the map that wasn't there when the map was built — anywhere the player happens to be standing.

The problem it solves

place_object moves an object the map already carries. That works beautifully for a scripted scene on a map you designed: pre-place the rival at the door, hide him, show him when the moment comes.

It does nothing for anything the player can do anywhere. If you want a campsite the player can pitch on any patch of grass, you would need six invisible objects pre-placed on every patch of grass in your game.

spawn_object creates one.

The commands

{"cmd": "spawn_object", "as": "camp_1", "sprite": "SPRITE_MONSTER", "at": [7, 9],
 "facing": "down", "talk": "script:camp_chatter"}
{"cmd": "face_object", "object": "camp_1", "dir": "left"}
{"cmd": "despawn_object", "object": "camp_1"}

as is the handle — the name you'll use to talk about this object afterwards. facing is optional and defaults to down.

From a script:

spawn_talker("camp_1", "SPRITE_MONSTER", player_x() + 1, player_y(), "left", "script:camp_chatter")
spawn_object("fire", "SPRITE_POKE_BALL", player_x(), player_y() + 1, "down")

The handle is just an object id

Once spawned, your handle behaves like any object the map shipped with: face_object turns it, place_object moves it, hide_object / show_object toggle it. Spawning was the only missing verb.

Talking to it

Two ways, and the difference matters.

Inline — the spawn carries its own dialogue. text is a literal line; talk names a script:

{"cmd": "spawn_object", "as": "camp_1", "sprite": "SPRITE_MONSTER", "at": [7, 9],
 "talk": "script:camp_chatter"}

Keyed — an ordinary interact record:

{
 "trigger": {"kind": "interact", "map": "map:Meadowbrook", "object": "camp_1"},
 "commands": [{"cmd": "run_script", "script": "script:camp_chatter"}]
}

Use inline for camp anywhere. Interact records are keyed by map and object, so a keyed record only reaches your handle on the maps you wrote records for — which is the designated-sites problem again. The inline dialogue travels with the spawn, so it works on any map the player stops on.

Use a keyed record when you want this specific place to be different. Where both exist, the keyed record wins — specific beats generic.

A talk script gets the full hatch surface run_script gets, so combine it with get_mon_field and per-individual fields and each body round your fire says something different depending on who it is, who else is in the party, and where you pitched:

let kind = get_mon_field(0, "type1")
if kind == "FIRE" {
  say("It sits closer to the flames than the others.")
  return true
}
say("It watches the sparks go up.")
return true

A talk script that errors or returns nothing rolls back its writes and falls through to text if you gave one — a half-written script is a visible no-op, not a silent success.

It never sticks around

Spawned objects are transient by construction:

  • A map change clears every one of them. The object list is rebuilt from the map's own, always.
  • Nothing is written to your save file.
  • Nothing enters the link identity, so a spawn can't desync a trade or a battle.

Which means a campfire re-places every time the player camps. Nothing persists except whatever flag you set yourself — and that is the behavior you want anyway.

The guard rails

A cap of 8 per map. Past it, spawn_object refuses loudly and stops the event. That second half is the point: a record looping over something unbounded halts instead of logging ninety errors and half-building a scene.

Re-spawning a handle replaces it. If the player camps twice on the same map without leaving, your record just runs again — the second spawn_object for camp_1 replaces the first rather than refusing. And despawn_object on something that isn't there is a quiet no-op, so "despawn everything, then spawn everything" is a safe opening move.

The map's own objects are off limits, both ways. You can't take over a map object's key with a handle, and despawn_object can only ever undo a spawn — there is no way to permanently delete an NPC the map shipped with.

Sprites come from your existing roster. No new asset path. An unknown sprite name refuses at --validate and at load, naming it.

What refuses when

The handle, the facing and the sprite are all authored literals, so all three refuse at load, naming the record — not mid-scene. --validate also collects every handle any record spawns, so a record addressing what it spawned is never reported as a dangling reference, while a despawn_object naming a handle nothing creates still is.

What this isn't

It isn't a follower system. An overworld companion that walks behind you everywhere is a much bigger feature — it needs walk cycles rather than idle poses, and it still wouldn't answer "put six of them round a fire". If followers ever ship, this stays useful; the campfire would just gain bodies that walked in rather than appeared.

Pairs with Overlay Projects and Set-Piece Battles & Narration. Gate: --mapobjtest.

Clone this wiki locally