Skip to content

Pitfalls

Each of these was found the hard way. They’re grouped by what you notice first. If your problem isn’t here, the Debugging recipes will help you narrow it down.

An event never runs

You see
A print as the first line of your event never appears.
Because
The engine finds events by name. A misspelled name, a function without event export, or a component that isn't on any entity the game loads is never called, and nothing warns you.
Do this

Spell the event exactly as the engine sends it (tick, late_tick, created, trigger_overlapped; see the Events reference), write it event export fn, and check the component is placed in a level that’s loaded.

Passing null to mean “me”

You see
A lookup like $entity.get_child_named(name, true, null) always comes back empty.
Because
Native descriptions call the entity “(Optional) … Omit to use the entity of the calling script.” You can't omit arguments, and null means “nothing”, not “me”.
Do this

Pass your own entity explicitly: $entity.get().

A placed prefab can't find its partner

You see
A search by name finds nothing, or finds the wrong copy, once the object is placed as a prefab.
Because
A prefab's entities become children of the entity that places it, and every instance shares the same internal names. A search from your own entity can't see siblings of the placement; a search of the whole world can't tell instances apart.
Do this

Name the partner by its placement name (unique per level), and search upwards from your entity: each parent’s children in turn (see Something nearby). Keep the placements inside a group: something placed at the very top of the level has no parent to search from.

$world.get_entity finds nothing

You see
$world.get_entity(name, …) returns null for an entity you can see in the level.
Because
Observed: it doesn't find entities that a level merely places, with or without an owner.
Do this

Search from your own entity with $entity.get_child_named, climbing parents if needed.

Reading another component's setting gives nothing

You see
other.some_setting is empty, and the log may say the script “does not have a mutable named …”.
Because
The dot reaches another component's let mut fields. Its constants, the settings chosen in the editor, can't be read that way.
Do this

Read mutables only. For a setting, look for a native or an event that reports it, or keep the value somewhere you can reach.

A move doesn't stick

You see
A player standing still is moved correctly, but one who walks or jumps onto the spot only jerks and stays put.
Because
The player's movement writes their position at the end of its own update, from what it worked out at the start. A position you set earlier in the frame, in tick or inside trigger_overlapped, is overwritten.
Do this

Record what should move in a field when the event happens, and move it in late_tick. See Moving things.

A player move only happens on the server

You see
Online, the camera jumps and the player ends up where they started.
Because
Each player's game simulates their own movement. A position written only on the server is recalculated away by the client a moment later.
Do this

Run the move on every machine: no $net.is_server() guard around it, and $net.set_replicated(true, false) in type_created, as the shipped knockback scripts do.

A value changes back by itself

You see
You set a field and it snaps back to an older value a moment later.
Because
It's a replicated field and you changed it on a client. The server's next update overwrites the client's copy.
Do this

Change replicated fields only on the server (if !$net.is_server() { return }), and react on clients in field_replicated.

A ball goes back where it was

You see
Writing a ball's transform position does nothing, or it jumps back.
Because
The ball's own script owns where it is, and overwrites the transform from its own state.
Do this

Ask the ball to move: $component.queue_event("set_position_and_rotation", ball, [position, rotation], true) on its ball_transform. Leave held balls alone.

Spawning an effect crashes the game

You see
The game closes with an access violation the moment the effect should play.
Because
The effect asset was read off an effect component with a dot. That field is a constant, so the read gave nothing, and spawning nothing crashes.
Do this

Don’t spawn effects from references you fished out of other components. Place an effect in the level with play_on_created off and send it play. See Effects and sound.

An effect replays out of nowhere

You see
A one-shot effect plays again whenever you look back at it, from any distance, with nobody near.
Because
Without kill_on_done, a finished one-shot effect still counts as playing, and it's drawn again whenever its entity comes back into view.
Do this

Turn on kill_on_done on the effect component.

An inherited script half works

You see
A script built on a shipped one fails in ways that seem unrelated: fields the parent should have filled are null.
Because
Redefining created (or type_created) replaced the parent's. Without super::created() the parent never set itself up.
Do this

Call super::created() (or super::type_created()) first in any event you redefine.

An effect won't change colour

You see
The log says Invalid vfx attribute Color and the effect keeps its colour.
Because
The effect was made with its colour baked in, not exposed as an attribute. Only attributes the effect declares can be set.
Do this

Use a different effect that declares a colour attribute, or colour the scene another way (a light, a material).

“takes 3 arguments but 2 were given”

You see
A native call the reference calls optional is rejected (V0303).
Because
Every native argument must be passed, optional or not.
Do this

Pass the value meaning none: "", null, 0 or false. See Calling natives.

x += 1 or list[0] doesn't parse

You see
The compiler stops at += , ++ or [ with “expected one of …” (V0101).
Because
The language has no compound assignment, increment, or index syntax.
Do this

Write x = x + 1, and use list.get(i) / list.set(i, value).

A field can't be typed as my enum

You see
“field … cannot be typed …” on let mut state: MyEnum.
Because
Enums name int values; a field's type must be one the engine stores.
Do this

Declare the field int and assign the enum’s options to it.