Migration Guide: v0.10 to v0.11
This guide covers breaking changes and new behavior when upgrading from godot-bevy 0.10.x to 0.11.0.
Table of Contents
Breaking changes:
- Typed signals now use observers
- Collisions API redesigned
- Main-thread access is now explicit via GodotAccess
- GodotNodeHandle is ID-only; GodotNodeId removed
- Legacy untyped signals removed
Breaking: Typed signals now use observers
The typed signals API has been redesigned to use Bevy's observer pattern instead of the Message/MessageReader pattern. This provides more reactive, push-based event handling where signal handlers fire immediately when signals are received.
Type changes
Signal types now derive Event instead of Message:
Before:
#[derive(Message, Debug, Clone)]
struct StartGameRequested;
After:
#[derive(Event, Debug, Clone)]
struct StartGameRequested;
SystemParam renamed
The TypedGodotSignals<T> SystemParam has been renamed to GodotSignals<T>:
Before:
fn connect_button(signals: TypedGodotSignals<StartGameRequested>) {
// ...
}
After:
fn connect_button(signals: GodotSignals<StartGameRequested>) {
// ...
}
Method renamed
The connect_map method has been renamed to connect:
Before:
signals.connect_map(*handle, "pressed", None, |_args, _node, _ent| {
Some(StartGameRequested)
});
After:
signals.connect(*handle, "pressed", None, |_args, _node, _ent| {
Some(StartGameRequested)
});
Handling signals with observers
Replace MessageReader<T> systems with observers using On<T>:
Before:
fn on_start_game(mut reader: MessageReader<StartGameRequested>) {
for _ in reader.read() {
// Start the game
}
}
app.add_systems(Update, on_start_game);
After:
fn on_start_game(
_trigger: On<StartGameRequested>,
mut next_state: ResMut<NextState<GameState>>,
) {
next_state.set(GameState::Playing);
}
app.add_observer(on_start_game);
Note: Observers are registered with app.add_observer(), not app.add_systems().
Deferred connections renamed
The TypedDeferredSignalConnections<T> component has been renamed to DeferredSignalConnections<T>:
Before:
commands.spawn((
MyArea,
TypedDeferredSignalConnections::<BodyEntered>::with_connection(
"body_entered",
|_a, _node, e| Some(BodyEntered(e.unwrap())),
),
));
After:
commands.spawn((
MyArea,
DeferredSignalConnections::<BodyEntered>::with_connection(
"body_entered",
|_a, _node, e| Some(BodyEntered { entity: e.unwrap() }),
),
));
Plugin renamed
The GodotTypedSignalsPlugin<T> has been renamed to GodotSignalsPlugin<T>:
Before:
app.add_plugins(GodotTypedSignalsPlugin::<ButtonPressed>::default());
After:
app.add_plugins(GodotSignalsPlugin::<ButtonPressed>::default());
Summary of changes
| Old API | New API |
|---|---|
#[derive(Message)] | #[derive(Event)] |
GodotTypedSignalsPlugin<T> | GodotSignalsPlugin<T> |
TypedGodotSignals<T> | GodotSignals<T> |
connect_map() | connect() |
connect_map_immediate() | connect_immediate() |
MessageReader<T> | On<T> (observer) |
app.add_systems(Update, handler) | app.add_observer(handler) |
TypedDeferredSignalConnections<T> | DeferredSignalConnections<T> |
Breaking: Collisions API redesigned
The collision API has been redesigned, providing a cleaner separation between querying current collision state and reacting to collision events.
Collisions component removed
The per-entity Collisions component has been replaced with a Collisions system parameter
for querying global collision state.
Before:
fn check_player_death(
player: Query<(&Player, &Collisions)>,
) {
for (player, collisions) in player.iter() {
for &other in collisions.colliding() {
// handle collision
}
for &other in collisions.recent_collisions() {
// handle new collision this frame
}
}
}
After:
fn check_player_death(
player: Query<(Entity, &Player)>,
collisions: Collisions,
) {
for (entity, player) in player.iter() {
// Query current collisions
for &other in collisions.colliding_with(entity) {
// handle collision
}
// Check specific pair
if collisions.contains(entity, enemy) {
// player is touching enemy
}
}
}
New collision events for change detection
To detect when collisions start or end, use the new CollisionStarted and CollisionEnded
events instead of querying per-frame.
As messages:
fn handle_hits(mut started: MessageReader<CollisionStarted>) {
for event in started.read() {
println!("{:?} hit {:?}", event.entity1, event.entity2);
}
}
As observers:
app.add_observer(|trigger: Trigger<CollisionStarted>| {
let event = trigger.event();
println!("{:?} hit {:?}", event.entity1, event.entity2);
});
CollisionMessage removed
The internal CollisionMessage type has been replaced with the public CollisionStarted
and CollisionEnded event types.
Summary of changes
| Old API | New API |
|---|---|
Query<&Collisions> | Collisions system param |
collisions.colliding() | collisions.colliding_with(entity) |
collisions.recent_collisions() | MessageReader<CollisionStarted> |
CollisionMessage | CollisionStarted / CollisionEnded |
Breaking: Main-thread access is now explicit via GodotAccess
The #[main_thread_system] macro has been removed. Systems that call Godot APIs must include
GodotAccess in their parameters. GodotAccess is a NonSend SystemParam that pins the system
to the main thread.
Before:
#[main_thread_system]
fn update_ui(mut q: Query<&mut GodotNodeHandle>) {
for mut handle in &mut q {
if let Some(mut label) = handle.try_get::<Label>() {
label.set_text("Hi");
}
}
}
After:
fn update_ui(q: Query<&GodotNodeHandle>, mut godot: GodotAccess) {
for handle in &q {
if let Some(mut label) = godot.try_get::<Label>(*handle) {
label.set_text("Hi");
}
}
}
Notes:
SceneTreeRefis also aNonSendSystemParam. If a system already takesSceneTreeRefand does not call Godot APIs, you do not need an extraGodotAccessparameter.- If you only have an
InstanceId, useGodotAccess::try_get_instance_idorget_instance_id.
Breaking: GodotNodeHandle is ID-only; GodotNodeId removed
GodotNodeId has been removed. GodotNodeHandle is now a lightweight, copyable wrapper around
an InstanceId.
Before:
struct QuitRequested { source: GodotNodeId }
After:
struct QuitRequested { source: GodotNodeHandle }
Constructors:
Before:
let handle = GodotNodeHandle::from_id(node_id);
After:
let handle = GodotNodeHandle::from_instance_id(node_id);
// or: let handle: GodotNodeHandle = node_id.into();
If you were using GodotNodeHandle::get or try_get directly, switch to
GodotAccess::get or GodotAccess::try_get instead.
Breaking: Legacy untyped signals removed
The untyped signal API has been removed:
GodotSignalsPluginGodotSignalmessageconnect_godot_signal
Use typed signals instead (GodotTypedSignalsPlugin::<T> + GodotSignals<T>).
Migration Checklist
-
Replace
#[derive(Message)]with#[derive(Event)]for signal types. -
Replace
GodotTypedSignalsPlugin<T>withGodotSignalsPlugin<T>. -
Replace
TypedGodotSignals<T>withGodotSignals<T>. -
Replace
connect_map()withconnect(). -
Replace
MessageReader<T>signal handlers with observers usingOn<T>. -
Replace
app.add_systems(Update, handler)withapp.add_observer(handler)for signal handlers. -
Replace
TypedDeferredSignalConnections<T>withDeferredSignalConnections<T>. -
Replace
Query<&Collisions>withCollisionssystem param. -
Replace
collisions.colliding()withcollisions.colliding_with(entity). -
Replace
collisions.recent_collisions()withMessageReader<CollisionStarted>or observer. -
Replace
#[main_thread_system]withGodotAccessparameters where you call Godot APIs. -
Replace
GodotNodeIdwithGodotNodeHandle. -
Replace
handle.getorhandle.try_getwithgodot.getorgodot.try_get. - Replace legacy untyped signal APIs with typed signals.