From cb7b36122a7488c31eeed41916cf1c6e9be8e060 Mon Sep 17 00:00:00 2001 From: Christian Hughes Date: Sat, 2 May 2026 15:43:19 -0500 Subject: [PATCH] `SystemId` scene templating --- crates/bevy_ecs/src/system/system_registry.rs | 129 +++++++++++++++++- examples/ecs/callbacks.rs | 21 ++- 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 819f78c8c9c44..e33775d2e1f37 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -4,13 +4,16 @@ use crate::{ change_detection::Mut, entity::Entity, error::BevyError, + prelude::{FromTemplate, Template}, system::{ input::SystemInput, BoxedSystem, IntoSystem, RunSystemError, SystemParamValidationError, }, + template::TemplateContext, world::World, }; use alloc::boxed::Box; use bevy_ecs_macros::{Component, Resource}; +use bevy_platform::sync::{Arc, Mutex}; use bevy_utils::prelude::DebugName; use core::{any::TypeId, marker::PhantomData}; use thiserror::Error; @@ -158,6 +161,105 @@ impl core::fmt::Debug for SystemId { } } +impl FromTemplate for SystemId { + type Template = SystemIdTemplate; +} + +/// A [`Template`] that produces a [`SystemId`]. +pub enum SystemIdTemplate { + /// Creates a [`SystemId`] by cloning the given [`SystemId`] value. + Id(SystemId), + /// Creates a [`SystemId`] by registering the given system value using + /// [`World::register_system`]. This will cache the resulting [`SystemId`] + /// on the template and reuse it for future template builds. + /// + /// This should generally be constructed using [`SystemIdTemplate::value`] or + /// [`system_value`]. + Value(SystemIdValue), +} + +/// Stores an [`Arc>>`]. +pub struct SystemIdValue( + Arc>>, +); + +impl Clone for SystemIdValue { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} + +enum SystemIdOrValue { + Id(SystemId), + Value(Option>), +} + +impl SystemIdTemplate { + /// This will create a new [`SystemIdTemplate`] for the given `system` value. + /// This makes it possible to define systems "inline" in templates / scenes + /// that produce a [`SystemId`]. + pub fn value(system: impl IntoSystem) -> Self { + Self::Value(SystemIdValue(Arc::new(Mutex::new(SystemIdOrValue::Value( + Some(Box::new(IntoSystem::into_system(system))), + ))))) + } +} + +impl Template for SystemIdTemplate { + type Output = SystemId; + + fn build_template( + &self, + context: &mut TemplateContext, + ) -> crate::prelude::Result { + match self { + Self::Id(id) => Ok(*id), + Self::Value(value) => { + let mut value_or_id = value.0.lock().unwrap(); + match &mut *value_or_id { + SystemIdOrValue::Id(id) => Ok(*id), + SystemIdOrValue::Value(system) => { + let system = system.take().unwrap_or_else(|| unreachable!()); + let id = context + .entity + .world_scope(|world| world.register_boxed_system(system)); + *value_or_id = SystemIdOrValue::Id(id); + Ok(id) + } + } + } + } + } + + fn clone_template(&self) -> Self { + match self { + Self::Id(id) => Self::Id(*id), + Self::Value(value) => Self::Value(value.clone()), + } + } +} + +impl Default for SystemIdTemplate { + fn default() -> Self { + Self::Id(SystemId::from_entity(Entity::PLACEHOLDER)) + } +} + +impl From> for SystemIdTemplate { + fn from(id: SystemId) -> Self { + Self::Id(id) + } +} + +/// This will create a new [`SystemIdTemplate`] for the given `system` value. +/// This makes it possible to define systems "inline" in templates / scenes that +/// produce a [`SystemId`]. +pub fn system_value( + system: impl IntoSystem, +) -> SystemIdTemplate { + SystemIdTemplate::value(system) +} + /// A cached [`SystemId`] distinguished by the unique function type of its system. /// /// This resource is inserted by [`World::register_system_cached`]. @@ -604,7 +706,7 @@ mod tests { use crate::{ prelude::*, - system::{RegisteredSystemError, SystemId}, + system::{system_value, RegisteredSystemError, SystemId, SystemIdTemplate}, }; #[derive(Resource, Default, PartialEq, Debug)] @@ -1074,4 +1176,29 @@ mod tests { Err(err) => panic!("Failed with wrong error. `{:?}`", err), } } + + #[test] + fn system_id_template() { + fn my_system() {} + + let mut world = World::new(); + + { + let my_system_id = world.register_system(my_system); + let system_id = world + .spawn_empty() + .build_template(&SystemIdTemplate::Id(my_system_id)) + .unwrap(); + assert_eq!(system_id, my_system_id); + } + + { + let template = system_value(my_system); + + let a = world.spawn_empty().build_template(&template).unwrap(); + let b = world.spawn_empty().build_template(&template).unwrap(); + + assert_eq!(a, b); + } + } } diff --git a/examples/ecs/callbacks.rs b/examples/ecs/callbacks.rs index cf1586bf6619e..5ecd03ff6c5cd 100644 --- a/examples/ecs/callbacks.rs +++ b/examples/ecs/callbacks.rs @@ -4,16 +4,25 @@ //! This pattern trades some performance for flexibility and works well for things like cutscenes, scripted events, //! or one-off UI-driven interactions that don't need to run every frame. -use bevy::{ecs::system::SystemId, prelude::*}; +use bevy::{ + ecs::system::{system_value, SystemId}, + prelude::*, + scene::ScenePlugin, +}; fn main() { let mut app = App::new(); + + // These two plugins are only needed when spawning via a scene! + // Spawning via `Commands::spawn` works without them. + app.add_plugins((AssetPlugin::default(), ScenePlugin)); + app.add_systems(Startup, setup_callbacks); app.add_systems(Update, run_callbacks); app.run(); } -#[derive(Component)] +#[derive(Component, FromTemplate)] // `FromTemplate` is only needed when spawning via a scene! struct Callback { system_id: SystemId<(), ()>, } @@ -42,6 +51,14 @@ fn setup_callbacks(mut commands: Commands) { commands.spawn(trivial_callback); commands.spawn(ordinary_system_callback); commands.spawn(exclusive_callback); + + commands.spawn_scene(bsn! { + Callback { + system_id: system_value(|| { + println!("This is a callback spawned via a scene."); + }) + } + }); } // In many cases, you might want to use an observer to detect when a callback should run,