bevy icon indicating copy to clipboard operation
bevy copied to clipboard

Implement world.trigger_event remote method

Open Lancelotbronner opened this issue 2 months ago • 11 comments

Objective

Tools using bevy_remote will be able to identify (via the schema) and trigger events.

  • [x] Document bevy_ecs/src/reflect/event.rs

Solution

I've added a method world.trigger_event, added Event to the schema's reflection metadata and ReflectEvent to allow this.

Testing

I have copied the (tested) code from my game but have NOT tested this branch yet. I am new to Rust/Cargo and need to go to sleep now, I'll figure this out and test it tomorrow.


Showcase

Here's what I needed to add to my game in order to allow my editor to access and trigger an event:

#[derive(Event, Reflect)]
#[reflect(Event)]
pub struct AssignToRoute {
    pub vehicle: Entity,
    pub route: Entity,
    pub origin: Entity,
}

Here's a screenshot of my editor using this feature:

Screenshot 2025-11-10 at 1 40 42 AM

Lancelotbronner avatar Nov 10 '25 06:11 Lancelotbronner

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

github-actions[bot] avatar Nov 10 '25 06:11 github-actions[bot]

gave it a brief look. What happens if you use an EntityEvent?

Otherwise generally seems reasonable and follows the pattern for other such types.

ChristopherBiscardi avatar Nov 10 '25 07:11 ChristopherBiscardi

gave it a brief look. What happens if you use an EntityEvent?

Otherwise generally seems reasonable and follows the pattern for other such types.

#[derive(EntityEvent, Reflect)]
#[reflect(Event)]
pub struct Explode(pub Entity);

commands.spawn(Name::new("WontExplode".to_string()));
commands.spawn(Name::new("WillExplode".to_string())).observe(
  |event: On<Explode>, mut commands: Commands| {
    println!("Boom!");
    commands.entity(event.event_target()).despawn();
  },
);

Sending the event for both entities only explodes "WillExplode", so I guess EntityEvent also works!

Lancelotbronner avatar Nov 10 '25 14:11 Lancelotbronner

I'm trying to pass CI locally but I keep getting error: associated function new is never used on the following:

impl ReflectEventFns {
    /// Get the default set of [`ReflectEventFns`] for a specific event type using its
    /// [`FromType`] implementation.
    ///
    /// This is useful if you want to start with the default implementation before overriding some
    /// of the functions to create a custom implementation.
    pub fn new<'a, T: Event + FromReflect + TypePath>() -> Self
    where
        T::Trigger<'a>: Default,
    {
        <ReflectEvent as FromType<T>>::from_type().0
    }
}

It's right but doesn't warn on the equivalent methods for components, resources, bundles, etc. and they have no usage either according to my IDE.

Anything I'm missing? I don't want to allow(dead_code) either since they don't have it but I can't figure out where they're supposed to be used.

Lancelotbronner avatar Nov 10 '25 15:11 Lancelotbronner

It's probably because ReflectEventFns is not being exported to the bevy_ecs level.

hymm avatar Nov 10 '25 17:11 hymm

Very sensible idea. Do we have pre-existing tests for this module? If so, we should be testing this too. If not, well, that can wait for follow-up.

alice-i-cecile avatar Nov 10 '25 23:11 alice-i-cecile

Added a test for bevy_remote, we sorta didn't but now we have at least one testing the happy path.

I'm also trying to write a test for bevy_ecs but I can't figure out how to satisfy the borrow checker. Can we consider the bevy_remote one an integration test for both?

Here's what I had for bevy_ecs so far:

#[derive(EntityEvent, Reflect)]
#[reflect(Event)]
struct Explode(pub Entity);

#[derive(Component)]
struct DespawnOnExplode;

#[test]
fn trigger_event() {
    let mut world = World::new();

    let type_registry = AppTypeRegistry::default();
    {
        let mut registry = type_registry.write();
        registry.register::<Explode>();
        registry.register_type_data::<Explode, ReflectEvent>();
    }
    world.insert_resource(type_registry);

    let mut system_state: SystemState<Commands> = SystemState::new(&mut world);
    let mut commands = system_state.get_mut(&mut world);

    let entity = commands
        .spawn_empty()
        .observe(|event: On<Explode>, mut commands: Commands| {
            commands.entity(event.0).despawn()
        })
        .id();
    let entity2 = commands.spawn(DespawnOnExplode).id();

    commands.add_observer(
        |event: On<Explode>,
         entities: Query<Entity, With<DespawnOnExplode>>,
         mut commands: Commands| {
            for entity in entities.iter() {
                commands.entity(entity).despawn()
            }
        },
    );

    let mut reflect_event = DynamicTupleStruct::default();
    reflect_event.insert(entity.to_bits());
    {
        let registry = *world.resource::<AppTypeRegistry>().write();
        let event =
            from_reflect_with_fallback::<Explode>(&reflect_event, &mut world, &registry);
        world.trigger(event);
    }

    assert!(world.get_entity(entity).is_err());
    assert!(world.get_entity(entity2).is_err());
}

Lancelotbronner avatar Nov 11 '25 10:11 Lancelotbronner

So my local CI runs keep flagging unrelated code so I can't verify locally and now the actions CI will fail if I'm missing a use for documentation and fails if I add it because I'm not using it in code...

Also why are there like 3 different actions (build, check-doc, ci) that all build separately? Isn't that a bit wasteful?

Lancelotbronner avatar Nov 14 '25 16:11 Lancelotbronner

So my local CI runs keep flagging unrelated code so I can't verify locally and now the actions CI will fail if I'm missing a use for documentation and fails if I add it because I'm not using it in code...

You can add the link in-line, using e.g. [Schedule](bevy::ecs::prelude::Schedule) :)

Also why are there like 3 different actions (build, check-doc, ci) that all build separately? Isn't that a bit wasteful?

In some ways! Generally our CI is limited by cache size and length of the longest job, not the number of runners. Some of the duplicated builds use slightly different settings as well. Not saying that it's perfect, but just giving you a bit of background.

alice-i-cecile avatar Nov 16 '25 21:11 alice-i-cecile

Thanks! I might be able to get it to pass CI now! Still wondering what I did wrong for local CI to fail on latest main so I'm going off the remote CI's feedback

Lancelotbronner avatar Nov 16 '25 22:11 Lancelotbronner

Thanks! I might be able to get it to pass CI now! Still wondering what I did wrong for local CI to fail on latest main so I'm going off the remote CI's feedback

Yep, that's totally fine. I sometimes do this myself when it's acting up or I'm feeling lazy.

alice-i-cecile avatar Nov 16 '25 23:11 alice-i-cecile