diff --git a/benches/benches/bevy_ecs/observers/dynamic.rs b/benches/benches/bevy_ecs/observers/dynamic.rs
new file mode 100644
index 0000000000000..8b0be622420c3
--- /dev/null
+++ b/benches/benches/bevy_ecs/observers/dynamic.rs
@@ -0,0 +1,51 @@
+use bevy_ecs::{
+    event::Event,
+    observer::{DynamicEvent, EmitDynamicTrigger, EventSet, Observer, Trigger},
+    world::{Command, World},
+};
+use criterion::{black_box, Criterion};
+
+pub fn observe_dynamic(criterion: &mut Criterion) {
+    let mut group = criterion.benchmark_group("observe_dynamic");
+    group.warm_up_time(std::time::Duration::from_millis(500));
+    group.measurement_time(std::time::Duration::from_secs(4));
+
+    group.bench_function("1", |bencher| {
+        let mut world = World::new();
+        let event_id_1 = world.init_component::<TestEvent<1>>();
+        world.spawn(Observer::new(empty_listener_set::<DynamicEvent>).with_event(event_id_1));
+        bencher.iter(|| {
+            for _ in 0..10000 {
+                unsafe {
+                    EmitDynamicTrigger::new_with_id(event_id_1, TestEvent::<1>, ())
+                        .apply(&mut world)
+                };
+            }
+        });
+    });
+    group.bench_function("2", |bencher| {
+        let mut world = World::new();
+        let event_id_1 = world.init_component::<TestEvent<1>>();
+        let event_id_2 = world.init_component::<TestEvent<2>>();
+        world.spawn(
+            Observer::new(empty_listener_set::<DynamicEvent>)
+                .with_event(event_id_1)
+                .with_event(event_id_2),
+        );
+        bencher.iter(|| {
+            for _ in 0..10000 {
+                unsafe {
+                    EmitDynamicTrigger::new_with_id(event_id_2, TestEvent::<2>, ())
+                        .apply(&mut world)
+                };
+            }
+        });
+    });
+}
+
+#[derive(Event)]
+struct TestEvent<const N: usize>;
+
+fn empty_listener_set<Set: EventSet>(trigger: Trigger<Set>) {
+    black_box(trigger);
+}
diff --git a/benches/benches/bevy_ecs/observers/mod.rs b/benches/benches/bevy_ecs/observers/mod.rs
index 0b8c3f24869ce..ceab56c99e1fb 100644
--- a/benches/benches/bevy_ecs/observers/mod.rs
+++ b/benches/benches/bevy_ecs/observers/mod.rs
@@ -1,8 +1,21 @@
 use criterion::criterion_group;
 
+mod dynamic;
+mod multievent;
 mod propagation;
+mod semidynamic;
 mod simple;
+use dynamic::*;
+use multievent::*;
 use propagation::*;
+use semidynamic::*;
 use simple::*;
 
-criterion_group!(observer_benches, event_propagation, observe_simple);
+criterion_group!(
+    observer_benches,
+    event_propagation,
+    observe_simple,
+    observe_multievent,
+    observe_dynamic,
+    observe_semidynamic
+);
diff --git a/benches/benches/bevy_ecs/observers/multievent.rs b/benches/benches/bevy_ecs/observers/multievent.rs
new file mode 100644
index 0000000000000..4002481a30110
--- /dev/null
+++ b/benches/benches/bevy_ecs/observers/multievent.rs
@@ -0,0 +1,105 @@
+use bevy_ecs::{
+    component::Component,
+    event::Event,
+    observer::{EventSet, Observer, Trigger},
+    world::World,
+};
+use criterion::{black_box, measurement::WallTime, BenchmarkGroup, Criterion};
+
+pub fn observe_multievent(criterion: &mut Criterion) {
+    let mut group = criterion.benchmark_group("observe_multievent");
+    group.warm_up_time(std::time::Duration::from_millis(500));
+    group.measurement_time(std::time::Duration::from_secs(4));
+
+    group.bench_function("trigger_single", |bencher| {
+        let mut world = World::new();
+        world.observe(empty_listener_set::<TestEvent<1>>);
+        bencher.iter(|| {
+            for _ in 0..10000 {
+                world.trigger(TestEvent::<1>);
+            }
+        });
+    });
+
+    bench_in_set::<1, (TestEvent<1>,)>(&mut group);
+    bench_in_set::<2, (TestEvent<1>, TestEvent<2>)>(&mut group);
+    bench_in_set::<4, (TestEvent<1>, TestEvent<2>, TestEvent<3>, TestEvent<4>)>(&mut group);
+    bench_in_set::<
+        8,
+        (
+            TestEvent<1>,
+            TestEvent<2>,
+            TestEvent<3>,
+            TestEvent<4>,
+            TestEvent<5>,
+            TestEvent<6>,
+            TestEvent<7>,
+            TestEvent<8>,
+        ),
+    >(&mut group);
+    bench_in_set::<
+        12,
+        (
+            TestEvent<1>,
+            TestEvent<2>,
+            TestEvent<3>,
+            TestEvent<4>,
+            TestEvent<5>,
+            TestEvent<6>,
+            TestEvent<7>,
+            TestEvent<8>,
+            TestEvent<9>,
+            TestEvent<10>,
+            TestEvent<11>,
+            TestEvent<12>,
+        ),
+    >(&mut group);
+    bench_in_set::<
+        15,
+        (
+            TestEvent<1>,
+            TestEvent<2>,
+            TestEvent<3>,
+            TestEvent<4>,
+            TestEvent<5>,
+            TestEvent<6>,
+            TestEvent<7>,
+            TestEvent<8>,
+            TestEvent<9>,
+            TestEvent<10>,
+            TestEvent<11>,
+            TestEvent<12>,
+            TestEvent<13>,
+            TestEvent<14>,
+            TestEvent<15>,
+        ),
+    >(&mut group);
+}
+
+fn bench_in_set<const LAST: usize, Set: EventSet>(group: &mut BenchmarkGroup<WallTime>) {
+    group.bench_function(format!("trigger_first/{LAST}"), |bencher| {
+        let mut world = World::new();
+        world.observe(empty_listener_set::<Set>);
+        bencher.iter(|| {
+            for _ in 0..10000 {
+                world.trigger(TestEvent::<1>);
+            }
+        });
+    });
+    group.bench_function(format!("trigger_last/{LAST}"), |bencher| {
+        let mut world = World::new();
+        world.observe(empty_listener_set::<Set>);
+        bencher.iter(|| {
+            for _ in 0..10000 {
+                world.trigger(TestEvent::<LAST>);
+            }
+        });
+    });
+}
+
+#[derive(Event)]
+struct TestEvent<const N: usize>;
+
+fn empty_listener_set<Set: EventSet>(trigger: Trigger<Set>) {
+    black_box(trigger);
+}
diff --git a/benches/benches/bevy_ecs/observers/semidynamic.rs b/benches/benches/bevy_ecs/observers/semidynamic.rs
new file mode 100644
index 0000000000000..a6895cf572e1b
--- /dev/null
+++ b/benches/benches/bevy_ecs/observers/semidynamic.rs
@@ -0,0 +1,183 @@
+use bevy_ecs::{
+    event::Event,
+    observer::{EmitDynamicTrigger, EventSet, Observer, SemiDynamicEvent, Trigger},
+    world::{Command, World},
+};
+use criterion::{black_box, Criterion};
+
+pub fn observe_semidynamic(criterion: &mut Criterion) {
+    let mut group = criterion.benchmark_group("observe_semidynamic");
+    group.warm_up_time(std::time::Duration::from_millis(500));
+    group.measurement_time(std::time::Duration::from_secs(4));
+
+    group.bench_function("static/1s-1d", |bencher| {
+        let mut world = World::new();
+        let event_id_1 = world.init_component::<Dynamic<1>>();
+        world.spawn(
+            Observer::new(empty_listener_set::<SemiDynamicEvent<Static<1>>>).with_event(event_id_1),
+        );
+
+        bencher.iter(|| {
+            for _ in 0..10000 {
+                world.trigger(Static::<1>);
+            }
+        });
+    });
+    group.bench_function("dynamic/1s-1d", |bencher| {
+        let mut world = World::new();
+        let event_id_1 = world.init_component::<Dynamic<1>>();
+        world.spawn(
+            Observer::new(empty_listener_set::<SemiDynamicEvent<Static<1>>>).with_event(event_id_1),
+        );
+
+        bencher.iter(|| {
+            for _ in 0..10000 {
+                unsafe {
+                    EmitDynamicTrigger::new_with_id(event_id_1, Dynamic::<1>, ()).apply(&mut world)
+                };
+            }
+        });
+    });
+
+    group.bench_function("static/15s-15d", |bencher| {
+        // Aint she perdy?
+        let mut world = World::new();
+        let event_id_1 = world.init_component::<Dynamic<1>>();
+        let event_id_2 = world.init_component::<Dynamic<2>>();
+        let event_id_3 = world.init_component::<Dynamic<3>>();
+        let event_id_4 = world.init_component::<Dynamic<4>>();
+        let event_id_5 = world.init_component::<Dynamic<5>>();
+        let event_id_6 = world.init_component::<Dynamic<6>>();
+        let event_id_7 = world.init_component::<Dynamic<7>>();
+        let event_id_8 = world.init_component::<Dynamic<8>>();
+        let event_id_9 = world.init_component::<Dynamic<9>>();
+        let event_id_10 = world.init_component::<Dynamic<10>>();
+        let event_id_11 = world.init_component::<Dynamic<11>>();
+        let event_id_12 = world.init_component::<Dynamic<12>>();
+        let event_id_13 = world.init_component::<Dynamic<13>>();
+        let event_id_14 = world.init_component::<Dynamic<14>>();
+        let event_id_15 = world.init_component::<Dynamic<15>>();
+        world.spawn(
+            Observer::new(
+                empty_listener_set::<
+                    SemiDynamicEvent<(
+                        Static<1>,
+                        Static<2>,
+                        Static<3>,
+                        Static<4>,
+                        Static<5>,
+                        Static<6>,
+                        Static<7>,
+                        Static<8>,
+                        Static<9>,
+                        Static<10>,
+                        Static<11>,
+                        Static<12>,
+                        Static<13>,
+                        Static<14>,
+                        Static<15>,
+                    )>,
+                >,
+            )
+            .with_event(event_id_1)
+            .with_event(event_id_2)
+            .with_event(event_id_3)
+            .with_event(event_id_4)
+            .with_event(event_id_5)
+            .with_event(event_id_6)
+            .with_event(event_id_7)
+            .with_event(event_id_8)
+            .with_event(event_id_9)
+            .with_event(event_id_10)
+            .with_event(event_id_11)
+            .with_event(event_id_12)
+            .with_event(event_id_13)
+            .with_event(event_id_14)
+            .with_event(event_id_15),
+        );
+
+        bencher.iter(|| {
+            for _ in 0..10000 {
+                world.trigger(Static::<14>);
+            }
+        });
+    });
+    group.bench_function("dynamic/15s-15d", |bencher| {
+        // Aint she perdy?
+        let mut world = World::new();
+        let event_id_1 = world.init_component::<Dynamic<1>>();
+        let event_id_2 = world.init_component::<Dynamic<2>>();
+        let event_id_3 = world.init_component::<Dynamic<3>>();
+        let event_id_4 = world.init_component::<Dynamic<4>>();
+        let event_id_5 = world.init_component::<Dynamic<5>>();
+        let event_id_6 = world.init_component::<Dynamic<6>>();
+        let event_id_7 = world.init_component::<Dynamic<7>>();
+        let event_id_8 = world.init_component::<Dynamic<8>>();
+        let event_id_9 = world.init_component::<Dynamic<9>>();
+        let event_id_10 = world.init_component::<Dynamic<10>>();
+        let event_id_11 = world.init_component::<Dynamic<11>>();
+        let event_id_12 = world.init_component::<Dynamic<12>>();
+        let event_id_13 = world.init_component::<Dynamic<13>>();
+        let event_id_14 = world.init_component::<Dynamic<14>>();
+        let event_id_15 = world.init_component::<Dynamic<15>>();
+        world.spawn(
+            Observer::new(
+                empty_listener_set::<
+                    SemiDynamicEvent<(
+                        Static<1>,
+                        Static<2>,
+                        Static<3>,
+                        Static<4>,
+                        Static<5>,
+                        Static<6>,
+                        Static<7>,
+                        Static<8>,
+                        Static<9>,
+                        Static<10>,
+                        Static<11>,
+                        Static<12>,
+                        Static<13>,
+                        Static<14>,
+                        Static<15>,
+                    )>,
+                >,
+            )
+            .with_event(event_id_1)
+            .with_event(event_id_2)
+            .with_event(event_id_3)
+            .with_event(event_id_4)
+            .with_event(event_id_5)
+            .with_event(event_id_6)
+            .with_event(event_id_7)
+            .with_event(event_id_8)
+            .with_event(event_id_9)
+            .with_event(event_id_10)
+            .with_event(event_id_11)
+            .with_event(event_id_12)
+            .with_event(event_id_13)
+            .with_event(event_id_14)
+            .with_event(event_id_15),
+        );
+
+        bencher.iter(|| {
+            for _ in 0..10000 {
+                unsafe {
+                    EmitDynamicTrigger::new_with_id(event_id_14, Dynamic::<14>, ())
+                        .apply(&mut world)
+                };
+            }
+        });
+    });
+}
+
+/// Static event type
+#[derive(Event)]
+struct Static<const N: usize>;
+
+/// Dynamic event type
+#[derive(Event)]
+struct Dynamic<const N: usize>;
+
+fn empty_listener_set<Set: EventSet>(trigger: Trigger<Set>) {
+    black_box(trigger);
+}
diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs
index 7188102837dea..2d03deff583a9 100644
--- a/crates/bevy_app/src/app.rs
+++ b/crates/bevy_app/src/app.rs
@@ -6,6 +6,7 @@ pub use bevy_derive::AppLabel;
 use bevy_ecs::{
     event::{event_update_system, EventCursor},
     intern::Interned,
+    observer::EventSet,
     prelude::*,
     schedule::{ScheduleBuildSettings, ScheduleLabel},
     system::{IntoObserverSystem, SystemId},
@@ -990,7 +991,7 @@ impl App {
     }
 
     /// Spawns an [`Observer`] entity, which will watch for and respond to the given event.
-    pub fn observe<E: Event, B: Bundle, M>(
+    pub fn observe<E: EventSet, B: Bundle, M>(
         &mut self,
         observer: impl IntoObserverSystem<E, B, M>,
     ) -> &mut Self {
diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs
index 600384d478438..c1be95e518b21 100644
--- a/crates/bevy_ecs/src/observer/mod.rs
+++ b/crates/bevy_ecs/src/observer/mod.rs
@@ -2,31 +2,32 @@
 
 mod entity_observer;
 mod runner;
+mod set;
 mod trigger_event;
 
 pub use runner::*;
+pub use set::*;
 pub use trigger_event::*;
 
 use crate::observer::entity_observer::ObservedBy;
 use crate::{archetype::ArchetypeFlags, system::IntoObserverSystem, world::*};
 use crate::{component::ComponentId, prelude::*, world::DeferredWorld};
-use bevy_ptr::Ptr;
 use bevy_utils::{EntityHashMap, HashMap};
 use std::marker::PhantomData;
 
 /// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the
 /// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also
 /// contains event propagation information. See [`Trigger::propagate`] for more information.
-pub struct Trigger<'w, E, B: Bundle = ()> {
-    event: &'w mut E,
+pub struct Trigger<'w, E: EventSet, B: Bundle = ()> {
+    event: E::Item<'w>,
     propagate: &'w mut bool,
     trigger: ObserverTrigger,
     _marker: PhantomData<B>,
 }
 
-impl<'w, E, B: Bundle> Trigger<'w, E, B> {
+impl<'w, E: EventSet, B: Bundle> Trigger<'w, E, B> {
     /// Creates a new trigger for the given event and observer information.
-    pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self {
+    pub fn new(event: E::Item<'w>, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self {
         Self {
             event,
             propagate,
@@ -41,18 +42,13 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> {
     }
 
     /// Returns a reference to the triggered event.
-    pub fn event(&self) -> &E {
-        self.event
+    pub fn event(&self) -> E::ReadOnlyItem<'_> {
+        E::shrink_readonly(&self.event)
     }
 
     /// Returns a mutable reference to the triggered event.
-    pub fn event_mut(&mut self) -> &mut E {
-        self.event
-    }
-
-    /// Returns a pointer to the triggered event.
-    pub fn event_ptr(&self) -> Ptr {
-        Ptr::from(&self.event)
+    pub fn event_mut(&mut self) -> E::Item<'_> {
+        E::shrink(&mut self.event)
     }
 
     /// Returns the entity that triggered the observer, could be [`Entity::PLACEHOLDER`].
@@ -304,7 +300,7 @@ impl Observers {
 
 impl World {
     /// Spawns a "global" [`Observer`] and returns its [`Entity`].
-    pub fn observe<E: Event, B: Bundle, M>(
+    pub fn observe<E: EventSet, B: Bundle, M>(
         &mut self,
         system: impl IntoObserverSystem<E, B, M>,
     ) -> EntityWorldMut {
@@ -433,7 +429,8 @@ mod tests {
 
     use crate as bevy_ecs;
     use crate::observer::{
-        EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState, OnReplace,
+        DynamicEvent, EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState, OnReplace,
+        Or2, SemiDynamicEvent,
     };
     use crate::prelude::*;
     use crate::traversal::Traversal;
@@ -601,16 +598,60 @@ mod tests {
     }
 
     #[test]
-    fn observer_multiple_events() {
+    fn observer_multiple_events_static() {
         let mut world = World::new();
         world.init_resource::<R>();
+        #[derive(Event)]
+        struct Foo(i32);
+        #[derive(Event)]
+        struct Bar(bool);
+        world.observe(|t: Trigger<(Foo, Bar)>, mut res: ResMut<R>| {
+            res.0 += 1;
+            match t.event() {
+                Or2::A(Foo(v)) => assert_eq!(5, *v),
+                Or2::B(Bar(v)) => assert!(*v),
+            }
+        });
+        // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
+        // and therefore does not automatically flush.
+        world.flush();
+        world.trigger(Foo(5));
+        world.trigger(Bar(true));
+        assert_eq!(2, world.resource::<R>().0);
+    }
+
+    #[test]
+    fn observer_multiple_events_dynamic() {
+        let mut world = World::new();
+        world.init_resource::<R>();
+        let on_add = world.init_component::<OnAdd>();
         let on_remove = world.init_component::<OnRemove>();
         world.spawn(
-            // SAFETY: OnAdd and OnRemove are both unit types, so this is safe
-            unsafe {
-                Observer::new(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.0 += 1)
-                    .with_event(on_remove)
-            },
+            Observer::new(|_: Trigger<DynamicEvent, A>, mut res: ResMut<R>| res.0 += 1)
+                .with_event(on_add)
+                .with_event(on_remove),
+        );
+
+        let entity = world.spawn(A).id();
+        world.despawn(entity);
+        assert_eq!(2, world.resource::<R>().0);
+    }
+
+    #[test]
+    fn observer_multiple_events_semidynamic() {
+        let mut world = World::new();
+        world.init_resource::<R>();
+        let on_remove = world.init_component::<OnRemove>();
+        world.spawn(
+            Observer::new(
+                |trigger: Trigger<SemiDynamicEvent<OnAdd>, A>, mut res: ResMut<R>| {
+                    match trigger.event() {
+                        Ok(_onadd) => res.assert_order(0),
+                        Err(_ptr) => res.assert_order(1),
+                    };
+                },
+            )
+            .with_event(on_remove),
         );
 
         let entity = world.spawn(A).id();
@@ -976,4 +1017,78 @@ mod tests {
         world.flush();
         assert_eq!(2, world.resource::<R>().0);
     }
+
+    #[test]
+    fn observer_propagating_multi_event_between() {
+        let mut world = World::new();
+        world.init_resource::<R>();
+        #[derive(Event)]
+        struct Foo;
+
+        let grandparent = world
+            .spawn_empty()
+            .observe(|_: Trigger<(EventPropagating,)>, mut res: ResMut<R>| res.0 += 1)
+            .id();
+        let parent = world
+            .spawn(Parent(grandparent))
+            .observe(|_: Trigger<(EventPropagating, Foo)>, mut res: ResMut<R>| res.0 += 1)
+            .id();
+        let child = world
+            .spawn(Parent(parent))
+            .observe(|_: Trigger<(EventPropagating,)>, mut res: ResMut<R>| res.0 += 1)
+            .id();
+
+        // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
+        // and therefore does not automatically flush.
+        world.flush();
+        world.trigger_targets(EventPropagating, child);
+        world.flush();
+        assert_eq!(3, world.resource::<R>().0);
+        world.trigger_targets(Foo, parent);
+        world.flush();
+        assert_eq!(4, world.resource::<R>().0);
+    }
+
+    #[test]
+    fn observer_propagating_multi_event_all() {
+        let mut world = World::new();
+        world.init_resource::<R>();
+        #[derive(Component)]
+        struct OtherPropagating;
+
+        impl Event for OtherPropagating {
+            type Traversal = Parent;
+
+            const AUTO_PROPAGATE: bool = true;
+        }
+
+        let grandparent = world
+            .spawn_empty()
+            .observe(
+                |_: Trigger<(EventPropagating, OtherPropagating)>, mut res: ResMut<R>| res.0 += 1,
+            )
+            .id();
+        let parent = world
+            .spawn(Parent(grandparent))
+            .observe(
+                |_: Trigger<(EventPropagating, OtherPropagating)>, mut res: ResMut<R>| res.0 += 1,
+            )
+            .id();
+        let child = world
+            .spawn(Parent(parent))
+            .observe(
+                |_: Trigger<(EventPropagating, OtherPropagating)>, mut res: ResMut<R>| res.0 += 1,
+            )
+            .id();
+
+        // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
+        // and therefore does not automatically flush.
+        world.flush();
+        world.trigger_targets(EventPropagating, child);
+        world.flush();
+        assert_eq!(3, world.resource::<R>().0);
+        world.trigger_targets(OtherPropagating, child);
+        world.flush();
+        assert_eq!(6, world.resource::<R>().0);
+    }
 }
diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs
index 05afa4f614f76..246b0529aa3db 100644
--- a/crates/bevy_ecs/src/observer/runner.rs
+++ b/crates/bevy_ecs/src/observer/runner.rs
@@ -1,6 +1,9 @@
 use crate::{
     component::{ComponentHooks, ComponentId, StorageType},
-    observer::{ObserverDescriptor, ObserverTrigger},
+    observer::{
+        DynamicEvent, EventSet, ObserverDescriptor, ObserverTrigger, SemiDynamicEvent,
+        StaticEventSet,
+    },
     prelude::*,
     query::DebugCheckedUnwrap,
     system::{IntoObserverSystem, ObserverSystem},
@@ -260,12 +263,12 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate:
 /// serves as the "source of truth" of the observer.
 ///
 /// [`SystemParam`]: crate::system::SystemParam
-pub struct Observer<T: 'static, B: Bundle> {
-    system: BoxedObserverSystem<T, B>,
+pub struct Observer<E: EventSet + 'static, B: Bundle> {
+    system: BoxedObserverSystem<E, B>,
     descriptor: ObserverDescriptor,
 }
 
-impl<E: Event, B: Bundle> Observer<E, B> {
+impl<E: EventSet, B: Bundle> Observer<E, B> {
     /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered
     /// for _any_ entity (or no entity).
     pub fn new<M>(system: impl IntoObserverSystem<E, B, M>) -> Self {
@@ -301,24 +304,54 @@ impl<E: Event, B: Bundle> Observer<E, B> {
     /// # Safety
     /// The type of the `event` [`ComponentId`] _must_ match the actual value
     /// of the event passed into the observer system.
-    pub unsafe fn with_event(mut self, event: ComponentId) -> Self {
+    pub unsafe fn with_event_unchecked(mut self, event: ComponentId) -> Self {
         self.descriptor.events.push(event);
         self
     }
 }
 
-impl<E: Event, B: Bundle> Component for Observer<E, B> {
+impl<B: Bundle> Observer<DynamicEvent, B> {
+    /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`]
+    /// is triggered.
+    ///
+    /// # Note
+    /// As opposed to [`Observer::with_event_unchecked`], this method is safe to use because no pointer casting is performed automatically.
+    /// That is left to the user to do manually.
+    pub fn with_event(self, event: ComponentId) -> Self {
+        // SAFETY: DynamicEvent itself does not perform any unsafe operations (like casting), so this is safe.
+        unsafe { self.with_event_unchecked(event) }
+    }
+}
+
+impl<Static: StaticEventSet, B: Bundle> Observer<SemiDynamicEvent<Static>, B> {
+    /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`]
+    /// is triggered.
+    ///
+    /// # Note
+    /// As opposed to [`Observer::with_event_unchecked`], this method is safe to use because no pointer casting is performed automatically
+    /// on event types outside its statically-known set of. That is left to the user to do manually.
+    pub fn with_event(self, event: ComponentId) -> Self {
+        // SAFETY: SemiDynamicEvent itself does not perform any unsafe operations (like casting)
+        // for event types outside its statically-known set, so this is safe.
+        unsafe { self.with_event_unchecked(event) }
+    }
+}
+
+impl<E: EventSet, B: Bundle> Component for Observer<E, B> {
     const STORAGE_TYPE: StorageType = StorageType::SparseSet;
     fn register_component_hooks(hooks: &mut ComponentHooks) {
         hooks.on_add(|mut world, entity, _| {
             world.commands().add(move |world: &mut World| {
-                let event_type = world.init_component::<E>();
+                let mut events = Vec::new();
+                E::init_components(world, |id| {
+                    events.push(id);
+                });
                 let mut components = Vec::new();
                 B::component_ids(&mut world.components, &mut world.storages, &mut |id| {
                     components.push(id);
                 });
                 let mut descriptor = ObserverDescriptor {
-                    events: vec![event_type],
+                    events,
                     components,
                     ..Default::default()
                 };
@@ -354,7 +387,7 @@ impl<E: Event, B: Bundle> Component for Observer<E, B> {
 /// Equivalent to [`BoxedSystem`](crate::system::BoxedSystem) for [`ObserverSystem`].
 pub type BoxedObserverSystem<E = (), B = ()> = Box<dyn ObserverSystem<E, B>>;
 
-fn observer_system_runner<E: Event, B: Bundle>(
+fn observer_system_runner<E: EventSet, B: Bundle>(
     mut world: DeferredWorld,
     observer_trigger: ObserverTrigger,
     ptr: PtrMut,
@@ -381,12 +414,26 @@ fn observer_system_runner<E: Event, B: Bundle>(
     }
     state.last_trigger_id = last_trigger;
 
-    let trigger: Trigger<E, B> = Trigger::new(
-        // SAFETY: Caller ensures `ptr` is castable to `&mut T`
-        unsafe { ptr.deref_mut() },
-        propagate,
-        observer_trigger,
-    );
+    // SAFETY: We have immutable access to the world from the passed in DeferredWorld
+    let world_ref = unsafe { world.world() };
+    // SAFETY: Observer was triggered with an event in the set of events it observes, so it must be convertible to E
+    // We choose to use unchecked_cast over the safe cast method to avoid the overhead of matching in the single event type case (which is the common case).
+    let Ok(event) = (unsafe { E::unchecked_cast(world_ref, &observer_trigger, ptr) }) else {
+        // This branch is only ever hit if the user called Observer::with_event_unchecked with a component ID not matching the event set E,
+        // EXCEPT when the event set is a singular event type, in which case the cast will always succeed.
+        // This is a user error and should be logged.
+        bevy_utils::tracing::error!(
+            "Observer was triggered with an event that does not match the event set. \
+             Did you call Observer::with_event_unchecked with the wrong ID? \
+             Observer: {:?} Event: {:?} Set: {:?}",
+            observer_trigger.observer,
+            observer_trigger.event_type,
+            std::any::type_name::<E>()
+        );
+        return;
+    };
+
+    let trigger: Trigger<E, B> = Trigger::new(event, propagate, observer_trigger);
     // SAFETY: the static lifetime is encapsulated in Trigger / cannot leak out.
     // Additionally, IntoObserverSystem is only implemented for functions starting
     // with for<'a> Trigger<'a>, meaning users cannot specify Trigger<'static> manually,
diff --git a/crates/bevy_ecs/src/observer/set.rs b/crates/bevy_ecs/src/observer/set.rs
new file mode 100644
index 0000000000000..932435d080f14
--- /dev/null
+++ b/crates/bevy_ecs/src/observer/set.rs
@@ -0,0 +1,387 @@
+use bevy_ptr::{Ptr, PtrMut};
+
+use crate::component::ComponentId;
+use crate::event::Event;
+use crate::observer::ObserverTrigger;
+use crate::world::World;
+
+/// A set of [`Event`]s that can trigger an observer.
+///
+/// The provided implementations of this trait are:
+///
+/// - All [`Event`]s.
+/// - Any tuple of [`Event`]s, up to 15 types. These can be nested.
+/// - [`DynamicEvent`], which matches any [`Event`]s dynamically added to the observer with [`Observer::with_event`] and does not reify the event data.
+/// - [`SemiDynamicEvent`], which will first try to match a statically-known set of [`Event`]s and reify the event data,
+///   and if no match is found, it will fall back to functioning as a [`DynamicEvent`].
+///
+/// # Example
+///
+/// TODO
+///
+/// # Safety
+///
+/// Implementor must ensure that:
+/// - [`EventSet::init_components`] must register a [`ComponentId`] for each [`Event`] type in the set.
+/// - [`EventSet::matches`] must return `true` if the triggered [`Event`]'s [`ComponentId`] matches a type in the set,
+///   or unambiguously always returns `true` or `false`.
+///
+/// [`Observer::with_event`]: crate::observer::Observer::with_event
+pub unsafe trait EventSet: 'static {
+    /// The item returned by this [`EventSet`] that will be passed to the observer system function.
+    /// Most of the time this will be a mutable reference to an [`Event`] type, a tuple of mutable references, or a [`PtrMut`].
+    type Item<'trigger>;
+    /// The read-only variant of the [`Item`](EventSet::Item).
+    type ReadOnlyItem<'trigger>: Copy;
+
+    /// Safely casts a pointer to the [`Item`](EventSet::Item) type by checking prior
+    /// whether the triggered [`Event`]'s [`ComponentId`] [`matches`](EventSet::matches) a type in this event set.
+    fn cast<'trigger>(
+        world: &World,
+        observer_trigger: &ObserverTrigger,
+        ptr: PtrMut<'trigger>,
+    ) -> Result<Self::Item<'trigger>, PtrMut<'trigger>> {
+        if Self::matches(world, observer_trigger) {
+            // SAFETY: We have checked that the event component id matches the event type
+            unsafe { Self::unchecked_cast(world, observer_trigger, ptr) }
+        } else {
+            Err(ptr)
+        }
+    }
+
+    /// Casts a pointer to the [`Item`](EventSet::Item) type
+    /// without checking if the [`Event`] type matches this event set.
+    ///
+    /// # Safety
+    ///
+    /// Caller must ensure that the [`Event`]'s [`ComponentId`] [`matches`](EventSet::matches)
+    /// this event set before calling this function.
+    unsafe fn unchecked_cast<'trigger>(
+        world: &World,
+        observer_trigger: &ObserverTrigger,
+        ptr: PtrMut<'trigger>,
+    ) -> Result<Self::Item<'trigger>, PtrMut<'trigger>>;
+
+    /// Checks if the [`Event`] type matches the observer trigger.
+    ///
+    /// # Safety
+    ///
+    /// Implementors must ensure that this function returns `true`
+    /// if the triggered [`Event`]'s [`ComponentId`] matches a type in the set,
+    /// or unambiguously always returns `true` or `false`.
+    fn matches(world: &World, observer_trigger: &ObserverTrigger) -> bool;
+
+    /// Initialize the components required by this event set.
+    fn init_components(world: &mut World, ids: impl FnMut(ComponentId));
+
+    /// Shrink the [`Item`](EventSet::Item) to a shorter lifetime.
+    fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short>;
+
+    /// Shrink the [`Item`](EventSet::Item) to a shorter lifetime [`ReadOnlyItem`](EventSet::ReadOnlyItem).
+    fn shrink_readonly<'long: 'short, 'short>(
+        item: &'short Self::Item<'long>,
+    ) -> Self::ReadOnlyItem<'short>;
+}
+
+/// An [`EventSet`] that matches a statically pre-defined set of event types.
+///
+/// This trait is required in order to prevent a footgun where a user might accidentally specify an `EventSet` similar to
+/// `(DynamicEvent, EventA, EventB)`, which would always match `DynamicEvent` and never `EventA` or `EventB`.
+/// Therefore, we prevent the introduction of `DynamicEvent` in a static `EventSet`,
+/// most notably any `EventSet` tuple made up of normal [`Event`] types.
+///
+/// If you need to support both dynamic and static event types in a single observer,
+/// you can use [`SemiDynamicEvent`] instead.
+///
+/// # Safety
+///
+/// Implementors must ensure that [`matches`](EventSet::matches)
+/// returns `true` if and only if the event component id matches the event type,
+/// and DOES NOT match any other event type.
+pub unsafe trait StaticEventSet: EventSet {}
+
+// SAFETY: The event type has a component id registered in `init_components`,
+// and `matches` checks that the event component id matches the event type.
+unsafe impl<E: Event> EventSet for E {
+    type Item<'trigger> = &'trigger mut E;
+    type ReadOnlyItem<'trigger> = &'trigger E;
+
+    unsafe fn unchecked_cast<'trigger>(
+        _world: &World,
+        _observer_trigger: &ObserverTrigger,
+        ptr: PtrMut<'trigger>,
+    ) -> Result<Self::Item<'trigger>, PtrMut<'trigger>> {
+        // SAFETY: Caller must ensure that the component id matches the event type
+        Ok(unsafe { ptr.deref_mut() })
+    }
+
+    fn matches(world: &World, observer_trigger: &ObserverTrigger) -> bool {
+        world
+            .component_id::<E>()
+            .is_some_and(|id| id == observer_trigger.event_type)
+    }
+
+    fn init_components(world: &mut World, mut ids: impl FnMut(ComponentId)) {
+        let id = world.init_component::<E>();
+        ids(id);
+    }
+
+    fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> {
+        item
+    }
+
+    fn shrink_readonly<'long: 'short, 'short>(
+        item: &'short Self::Item<'long>,
+    ) -> Self::ReadOnlyItem<'short> {
+        item
+    }
+}
+
+// SAFETY: The event type is a statically known type.
+unsafe impl<E: Event> StaticEventSet for E {}
+
+/// An [`EventSet`] that matches any event type and performs no casting. Instead, it returns the pointer as is.
+/// This is useful for observers that do not need to access the event data, or need to do so dynamically.
+///
+/// # Example
+///
+/// TODO
+pub struct DynamicEvent;
+
+// SAFETY: Performs no unsafe operations, returns the pointer as is.
+unsafe impl EventSet for DynamicEvent {
+    type Item<'trigger> = PtrMut<'trigger>;
+    type ReadOnlyItem<'trigger> = Ptr<'trigger>;
+
+    fn cast<'trigger>(
+        _world: &World,
+        _observer_trigger: &ObserverTrigger,
+        ptr: PtrMut<'trigger>,
+    ) -> Result<Self::Item<'trigger>, PtrMut<'trigger>> {
+        Ok(ptr)
+    }
+
+    unsafe fn unchecked_cast<'trigger>(
+        _world: &World,
+        _observer_trigger: &ObserverTrigger,
+        ptr: PtrMut<'trigger>,
+    ) -> Result<Self::Item<'trigger>, PtrMut<'trigger>> {
+        Ok(ptr)
+    }
+
+    fn matches(_world: &World, _observer_trigger: &ObserverTrigger) -> bool {
+        // We're treating this as a catch-all event set, so it always matches.
+        true
+    }
+
+    fn init_components(_world: &mut World, _ids: impl FnMut(ComponentId)) {}
+
+    fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> {
+        item.reborrow()
+    }
+
+    fn shrink_readonly<'long: 'short, 'short>(
+        item: &'short Self::Item<'long>,
+    ) -> Self::ReadOnlyItem<'short> {
+        item.as_ref()
+    }
+}
+
+/// An [`EventSet`] that either matches a statically pre-defined set of event types and casts the pointer to the event type,
+/// or returns the pointer as-is if the event type was not matched.
+/// Basically, it allows you to mix static and dynamic event types in a single observer.
+///
+/// `SemiDynamicEvent` accepts two type parameters:
+///
+/// - **Static**
+///   The static event set that will be matched and casted.
+///   Generally, this should be a tuple of static event types, like `(FooEvent, BarEvent)`.
+///   Must implement [`StaticEventSet`] trait, which means no [`DynamicEvent`] or [`SemiDynamicEvent`] nesting.
+///
+/// # Example
+///
+/// TODO
+pub struct SemiDynamicEvent<Static: StaticEventSet>(std::marker::PhantomData<Static>);
+
+// SAFETY: No unsafe operations are performed. The checked cast variant is used for the static event type.
+unsafe impl<Static: StaticEventSet> EventSet for SemiDynamicEvent<Static> {
+    type Item<'trigger> = Result<Static::Item<'trigger>, PtrMut<'trigger>>;
+    type ReadOnlyItem<'trigger> = Result<Static::ReadOnlyItem<'trigger>, Ptr<'trigger>>;
+
+    unsafe fn unchecked_cast<'trigger>(
+        world: &World,
+        observer_trigger: &ObserverTrigger,
+        ptr: PtrMut<'trigger>,
+    ) -> Result<Self::Item<'trigger>, PtrMut<'trigger>> {
+        match Static::cast(world, observer_trigger, ptr) {
+            Ok(item) => Ok(Ok(item)),
+            Err(ptr) => Ok(Err(ptr)),
+        }
+    }
+
+    fn matches(_world: &World, _observer_trigger: &ObserverTrigger) -> bool {
+        true
+    }
+
+    fn init_components(world: &mut World, mut ids: impl FnMut(ComponentId)) {
+        Static::init_components(world, &mut ids);
+    }
+
+    fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> {
+        match item {
+            Ok(item) => Ok(Static::shrink(item)),
+            Err(ptr) => Err(ptr.reborrow()),
+        }
+    }
+
+    fn shrink_readonly<'long: 'short, 'short>(
+        item: &'short Self::Item<'long>,
+    ) -> Self::ReadOnlyItem<'short> {
+        match item {
+            Ok(item) => Ok(Static::shrink_readonly(item)),
+            Err(ptr) => Err(ptr.as_ref()),
+        }
+    }
+}
+
+// SAFETY: Forwards to the inner event type, and inherits its safety properties.
+unsafe impl<A: StaticEventSet> EventSet for (A,) {
+    type Item<'trigger> = A::Item<'trigger>;
+    type ReadOnlyItem<'trigger> = A::ReadOnlyItem<'trigger>;
+
+    fn cast<'trigger>(
+        world: &World,
+        observer_trigger: &ObserverTrigger,
+        ptr: PtrMut<'trigger>,
+    ) -> Result<Self::Item<'trigger>, PtrMut<'trigger>> {
+        A::cast(world, observer_trigger, ptr)
+    }
+
+    unsafe fn unchecked_cast<'trigger>(
+        world: &World,
+        observer_trigger: &ObserverTrigger,
+        ptr: PtrMut<'trigger>,
+    ) -> Result<Self::Item<'trigger>, PtrMut<'trigger>> {
+        A::unchecked_cast(world, observer_trigger, ptr)
+    }
+
+    fn matches(world: &World, observer_trigger: &ObserverTrigger) -> bool {
+        A::matches(world, observer_trigger)
+    }
+
+    fn init_components(world: &mut World, ids: impl FnMut(ComponentId)) {
+        A::init_components(world, ids);
+    }
+
+    fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> {
+        A::shrink(item)
+    }
+
+    fn shrink_readonly<'long: 'short, 'short>(
+        item: &'short Self::Item<'long>,
+    ) -> Self::ReadOnlyItem<'short> {
+        A::shrink_readonly(item)
+    }
+}
+
+// SAFETY: The inner event set is a static event set.
+unsafe impl<A: StaticEventSet> StaticEventSet for (A,) {}
+
+macro_rules! impl_event_set {
+    ($Or:ident, $(($P:ident, $p:ident)),*) => {
+        /// An output type of an observer that observes multiple event types.
+        #[derive(Copy, Clone)]
+        pub enum $Or<$($P),*> {
+            $(
+                /// A possible event type.
+                $P($P),
+            )*
+        }
+
+        // SAFETY: All event types have a component id registered in `init_components`,
+        // and `unchecked_cast` calls `matches` before casting to one of the inner event sets.
+        unsafe impl<$($P: StaticEventSet),*> EventSet for ($($P,)*) {
+            type Item<'trigger> = $Or<$($P::Item<'trigger>),*>;
+            type ReadOnlyItem<'trigger> = $Or<$($P::ReadOnlyItem<'trigger>),*>;
+
+            fn cast<'trigger>(
+                world: &World,
+                observer_trigger: &ObserverTrigger,
+                ptr: PtrMut<'trigger>,
+            ) -> Result<Self::Item<'trigger>, PtrMut<'trigger>> {
+                // SAFETY: Each inner event set is checked in order for a match and then casted.
+                unsafe { Self::unchecked_cast(world, observer_trigger, ptr) }
+            }
+
+            unsafe fn unchecked_cast<'trigger>(
+                world: &World,
+                observer_trigger: &ObserverTrigger,
+                ptr: PtrMut<'trigger>,
+            ) -> Result<Self::Item<'trigger>, PtrMut<'trigger>> {
+                if false {
+                    unreachable!();
+                }
+                $(
+                    else if $P::matches(world, observer_trigger) {
+                        match $P::unchecked_cast(world, observer_trigger, ptr) {
+                            Ok($p) => return Ok($Or::$P($p)),
+                            Err(ptr) => return Err(ptr),
+                        }
+                    }
+                )*
+
+                Err(ptr)
+            }
+
+            fn matches(world: &World, observer_trigger: &ObserverTrigger) -> bool {
+                $(
+                    $P::matches(world, observer_trigger) ||
+                )*
+                false
+            }
+
+            fn init_components(world: &mut World, mut ids: impl FnMut(ComponentId)) {
+                $(
+                    $P::init_components(world, &mut ids);
+                )*
+            }
+
+            fn shrink<'long: 'short, 'short>(item: &'short mut Self::Item<'long>) -> Self::Item<'short> {
+                match item {
+                    $(
+                        $Or::$P($p) => $Or::$P($P::shrink($p)),
+                    )*
+                }
+            }
+
+            fn shrink_readonly<'long: 'short, 'short>(
+                item: &'short Self::Item<'long>,
+            ) -> Self::ReadOnlyItem<'short> {
+                match item {
+                    $(
+                        $Or::$P($p) => $Or::$P($P::shrink_readonly($p)),
+                    )*
+                }
+            }
+        }
+
+        // SAFETY: All inner event types are static event sets.
+        unsafe impl<$($P: StaticEventSet),*> StaticEventSet for ($($P,)*) {}
+    };
+}
+
+// We can't use `all_tuples` here because it doesn't support the extra `OrX` parameter required for each tuple impl.
+#[rustfmt::skip] impl_event_set!(Or2, (A, a), (B, b));
+#[rustfmt::skip] impl_event_set!(Or3, (A, a), (B, b), (C, c));
+#[rustfmt::skip] impl_event_set!(Or4, (A, a), (B, b), (C, c), (D, d));
+#[rustfmt::skip] impl_event_set!(Or5, (A, a), (B, b), (C, c), (D, d), (E, e));
+#[rustfmt::skip] impl_event_set!(Or6, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f));
+#[rustfmt::skip] impl_event_set!(Or7, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g));
+#[rustfmt::skip] impl_event_set!(Or8, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h));
+#[rustfmt::skip] impl_event_set!(Or9, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i));
+#[rustfmt::skip] impl_event_set!(Or10, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j));
+#[rustfmt::skip] impl_event_set!(Or11, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j), (K, k));
+#[rustfmt::skip] impl_event_set!(Or12, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j), (K, k), (L, l));
+#[rustfmt::skip] impl_event_set!(Or13, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j), (K, k), (L, l), (M, m));
+#[rustfmt::skip] impl_event_set!(Or14, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j), (K, k), (L, l), (M, m), (N, n));
+#[rustfmt::skip] impl_event_set!(Or15, (A, a), (B, b), (C, c), (D, d), (E, e), (F, f), (G, g), (H, h), (I, i), (J, j), (K, k), (L, l), (M, m), (N, n), (O, o));
diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs
index 875e0ae9de8c6..5a27030426ba0 100644
--- a/crates/bevy_ecs/src/system/commands/mod.rs
+++ b/crates/bevy_ecs/src/system/commands/mod.rs
@@ -9,7 +9,7 @@ use crate::{
     component::{ComponentId, ComponentInfo},
     entity::{Entities, Entity},
     event::Event,
-    observer::{Observer, TriggerEvent, TriggerTargets},
+    observer::{EventSet, Observer, TriggerEvent, TriggerTargets},
     system::{RunSystemWithInput, SystemId},
     world::{
         command_queue::RawCommandQueue, Command, CommandQueue, EntityWorldMut, FromWorld,
@@ -778,7 +778,7 @@ impl<'w, 's> Commands<'w, 's> {
     }
 
     /// Spawn an [`Observer`] and returns the [`EntityCommands`] associated with the entity that stores the observer.  
-    pub fn observe<E: Event, B: Bundle, M>(
+    pub fn observe<E: EventSet, B: Bundle, M>(
         &mut self,
         observer: impl IntoObserverSystem<E, B, M>,
     ) -> EntityCommands {
@@ -1211,7 +1211,7 @@ impl EntityCommands<'_> {
     }
 
     /// Creates an [`Observer`] listening for a trigger of type `T` that targets this entity.
-    pub fn observe<E: Event, B: Bundle, M>(
+    pub fn observe<E: EventSet, B: Bundle, M>(
         &mut self,
         system: impl IntoObserverSystem<E, B, M>,
     ) -> &mut Self {
@@ -1443,7 +1443,7 @@ fn log_components(entity: Entity, world: &mut World) {
     info!("Entity {entity}: {debug_infos:?}");
 }
 
-fn observe<E: Event, B: Bundle, M>(
+fn observe<E: EventSet, B: Bundle, M>(
     observer: impl IntoObserverSystem<E, B, M>,
 ) -> impl EntityCommand {
     move |entity, world: &mut World| {
diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs
index c5a04f25dd4eb..eae0d663ff2fc 100644
--- a/crates/bevy_ecs/src/system/observer_system.rs
+++ b/crates/bevy_ecs/src/system/observer_system.rs
@@ -1,6 +1,7 @@
 use bevy_utils::all_tuples;
 
 use crate::{
+    observer::EventSet,
     prelude::{Bundle, Trigger},
     system::{System, SystemParam, SystemParamFunction, SystemParamItem},
 };
@@ -10,13 +11,13 @@ use super::IntoSystem;
 /// Implemented for systems that have an [`Observer`] as the first argument.
 ///
 /// [`Observer`]: crate::observer::Observer
-pub trait ObserverSystem<E: 'static, B: Bundle, Out = ()>:
+pub trait ObserverSystem<E: EventSet + 'static, B: Bundle, Out = ()>:
     System<In = Trigger<'static, E, B>, Out = Out> + Send + 'static
 {
 }
 
 impl<
-        E: 'static,
+        E: EventSet + 'static,
         B: Bundle,
         Out,
         T: System<In = Trigger<'static, E, B>, Out = Out> + Send + 'static,
@@ -25,7 +26,9 @@ impl<
 }
 
 /// Implemented for systems that convert into [`ObserverSystem`].
-pub trait IntoObserverSystem<E: 'static, B: Bundle, M, Out = ()>: Send + 'static {
+pub trait IntoObserverSystem<E: EventSet + 'static, B: Bundle, M, Out = ()>:
+    Send + 'static
+{
     /// The type of [`System`] that this instance converts into.
     type System: ObserverSystem<E, B, Out>;
 
@@ -37,7 +40,7 @@ impl<
         S: IntoSystem<Trigger<'static, E, B>, Out, M> + Send + 'static,
         M,
         Out,
-        E: 'static,
+        E: EventSet + 'static,
         B: Bundle,
     > IntoObserverSystem<E, B, M, Out> for S
 where
@@ -53,7 +56,7 @@ where
 macro_rules! impl_system_function {
     ($($param: ident),*) => {
         #[allow(non_snake_case)]
-        impl<E: 'static, B: Bundle, Out, Func: Send + Sync + 'static, $($param: SystemParam),*> SystemParamFunction<fn(Trigger<E, B>, $($param,)*)> for Func
+        impl<E: EventSet + 'static, B: Bundle, Out, Func: Send + Sync + 'static, $($param: SystemParam),*> SystemParamFunction<fn(Trigger<E, B>, $($param,)*)> for Func
         where
         for <'a> &'a mut Func:
                 FnMut(Trigger<E, B>, $($param),*) -> Out +
@@ -65,7 +68,7 @@ macro_rules! impl_system_function {
             #[inline]
             fn run(&mut self, input: Trigger<'static, E, B>, param_value: SystemParamItem< ($($param,)*)>) -> Out {
                 #[allow(clippy::too_many_arguments)]
-                fn call_inner<E: 'static, B: Bundle, Out, $($param,)*>(
+                fn call_inner<E: EventSet + 'static, B: Bundle, Out, $($param,)*>(
                     mut f: impl FnMut(Trigger<'static, E, B>, $($param,)*) -> Out,
                     input: Trigger<'static, E, B>,
                     $($param: $param,)*
diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs
index 34dc767341ba6..bfa23456c6e2e 100644
--- a/crates/bevy_ecs/src/world/entity_ref.rs
+++ b/crates/bevy_ecs/src/world/entity_ref.rs
@@ -4,8 +4,7 @@ use crate::{
     change_detection::MutUntyped,
     component::{Component, ComponentId, ComponentTicks, Components, StorageType},
     entity::{Entities, Entity, EntityLocation},
-    event::Event,
-    observer::{Observer, Observers},
+    observer::{EventSet, Observer, Observers},
     query::Access,
     removal_detection::RemovedComponentEvents,
     storage::Storages,
@@ -1445,7 +1444,7 @@ impl<'w> EntityWorldMut<'w> {
 
     /// Creates an [`Observer`] listening for events of type `E` targeting this entity.
     /// In order to trigger the callback the entity must also match the query when the event is fired.
-    pub fn observe<E: Event, B: Bundle, M>(
+    pub fn observe<E: EventSet, B: Bundle, M>(
         &mut self,
         observer: impl IntoObserverSystem<E, B, M>,
     ) -> &mut Self {