openlang

Signals fire right now, not at the end of the frame

This one bit me in Godot and seems to bite everyone eventually. emit_signal() and signal.emit() call the connected functions right away, inside the emitting call, before the emitter's next line runs. It's a function call with extra steps, not a message that waits for the end of the frame.

So when a health_changed handler removes a node from the tree, or modifies the array the emitter is looping over, it does that in the middle of the emitter's logic. If you want the handler to run later, connect with CONNECT_DEFERRED and the call gets queued for idle time instead.

The other half is where per-frame logic lives. _process(delta) runs once per rendered frame with a varying delta. _physics_process(delta) runs at the fixed physics tick rate from the project settings. Moving a CharacterBody2D in _process mostly works, then behaves differently at another frame rate, because movement and the physics step aren't in lockstep anymore.

My current rule is collision-related movement in _physics_process, visuals in _process, and deferred connections whenever a handler might change the scene tree. Not sure that last one should be the default. Deferred everywhere makes the order of events harder to follow.

后续讨论 3

顺着彼此的依据读下去
  1. 固定链接

    await adds a second handoff on top of this one, if I'm reading the GDScript docs right. A function that hits await some_signal returns to its caller at that line, and the rest of it runs later, when the signal is emitted, inside that emit call. So who runs the rest of it? Whoever emits, wherever that happens to be in the frame.

  2. 固定链接

    That one's sneakier than the signal itself. A reload that does await $Timer.timeout continues from inside the timer's own emit, during whichever processing step the timer runs in. When I want a predictable spot I follow it with await get_tree().process_frame, which resumes just before nodes get their _process calls.

  3. 固定链接

    Not switching every connection to deferred after all. Only one thing in my scene actually broke. A pickup that turns off its own collision shape in body_entered kept hitting the flushing-queries error, and the error text already says what to do, use set_deferred(). So the pickup now does $Shape.set_deferred("disabled", true) and its connection stays direct.

    That's the version I'm keeping. Connections are direct by default, and only the one line that changes physics state in the middle of a callback gets deferred. Handlers still run in order with everything else, which is the part I didn't want to lose. The await handoff from earlier still applies.

同一个问题的其他角度

  1. Who owns a Vulkan buffer after submission?

    A C++ RAII wrapper around a Vulkan buffer records work that uses the buffer, submits it, and goes out of scope. Its destructo…

    回复 7最近活动 cppc
  2. A status region that reads Saved twice

    The screen reader said "Saved" twice after one save. The state behind it has a single transition into saved, and the reducer…

    回复 3最近活动 typescriptjavascript
  3. One health bar went red and took the rest with it

    Got bitten by shared resources in Godot UI this time. Every unit in my little prototype has a health bar scene, a ProgressBar…

    回复 10最近活动 gdscript