Skip to content

Commit

Permalink
Add opt-out entity spawning
Browse files Browse the repository at this point in the history
  • Loading branch information
MrGVSV committed Apr 30, 2023
1 parent faca07d commit 84cbb77
Show file tree
Hide file tree
Showing 12 changed files with 140 additions and 37 deletions.
1 change: 0 additions & 1 deletion assets/examples/basic_schematic/Player.prototype.ron
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
schematics: {
"basic_schematic::Playable": (),
"basic_schematic::Alignment": Good,
"basic_schematic::PlayerHealth": (100),
// This schematic is provided by `bevy_proto` and resolves
// to a Bevy `SpriteBundle`:
"bevy_proto::custom::SpriteBundle": (
Expand Down
9 changes: 9 additions & 0 deletions assets/examples/basic_schematic/PlayerConfig.prototype.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
(
name: "PlayerConfig",
// Since this prototype only contains a resource,
// we can mark it as not needing an entity to be spawned.
entity: false,
schematics: {
"basic_schematic::MaxPlayers": (1),
}
)
40 changes: 35 additions & 5 deletions bevy_proto_backend/src/proto/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,29 @@ impl<'w, 's, T: Prototypical> ProtoCommands<'w, 's, T> {
ProtoEntityCommands::new(self.commands.spawn_empty().id(), self)
}

/// Apply the prototype with the given [ID] to the world.
///
/// This should only be called on prototypes that do not [require an entity].
/// To spawn this prototype as a new entity, use [`spawn`] instead.
///
/// [ID]: Prototypical::id
/// [require an entity]: Prototypical::require_entity
/// [`spawn`]: Self::spawn
pub fn apply<I: Into<T::Id>>(&mut self, id: I) {
self.add(ProtoInsertCommand::<T>::new(id.into(), None));
}

/// Remove the prototype with the given [ID] from the world.
///
/// This should only be called on prototypes that do not [require an entity].
/// To remove this prototype from an entity, use [`ProtoEntityCommands::remove`] instead.
///
/// [ID]: Prototypical::id
/// [require an entity]: Prototypical::require_entity
pub fn remove<I: Into<T::Id>>(&mut self, id: I) {
self.add(ProtoInsertCommand::<T>::new(id.into(), None));
}

/// Get the [`ProtoEntityCommands`] for the given entity.
///
/// This internally calls [`Commands::entity`].
Expand Down Expand Up @@ -117,7 +140,7 @@ impl<'w, 's, 'a, T: Prototypical> ProtoEntityCommands<'w, 's, 'a, T> {
pub fn insert<I: Into<T::Id>>(&mut self, id: I) -> &mut Self {
let id = id.into();
self.proto_commands
.add(ProtoInsertCommand::<T>::new(id, self.entity));
.add(ProtoInsertCommand::<T>::new(id, Some(self.entity)));
self
}

Expand All @@ -127,7 +150,7 @@ impl<'w, 's, 'a, T: Prototypical> ProtoEntityCommands<'w, 's, 'a, T> {
pub fn remove<I: Into<T::Id>>(&mut self, id: I) -> &mut Self {
let id = id.into();
self.proto_commands
.add(ProtoRemoveCommand::<T>::new(id, self.entity));
.add(ProtoRemoveCommand::<T>::new(id, Some(self.entity)));
self
}

Expand All @@ -151,7 +174,7 @@ pub struct ProtoInsertCommand<T: Prototypical> {
}

impl<T: Prototypical> ProtoInsertCommand<T> {
pub fn new(id: T::Id, entity: Entity) -> Self {
pub fn new(id: T::Id, entity: Option<Entity>) -> Self {
Self {
data: ProtoCommandData { id, entity },
}
Expand All @@ -178,7 +201,7 @@ pub struct ProtoRemoveCommand<T: Prototypical> {
}

impl<T: Prototypical> ProtoRemoveCommand<T> {
pub fn new(id: T::Id, entity: Entity) -> Self {
pub fn new(id: T::Id, entity: Option<Entity>) -> Self {
Self {
data: ProtoCommandData { id, entity },
}
Expand All @@ -198,7 +221,7 @@ impl<T: Prototypical> Command for ProtoRemoveCommand<T> {

struct ProtoCommandData<T: Prototypical> {
id: T::Id,
entity: Entity,
entity: Option<Entity>,
}

impl<T: Prototypical> ProtoCommandData<T> {
Expand Down Expand Up @@ -290,6 +313,13 @@ impl<T: Prototypical> ProtoCommandData<T> {
let handle = prototypes.get_handle(*handle_id);
let proto = prototypes.get(&handle).unwrap();

if proto.requires_entity() && !context.entity().is_some() {
panic!(
"could not apply command for prototype {:?}: requires entity",
proto.id()
);
}

on_before_prototype(config, proto, context);

for (_, schematic) in proto.schematics().iter() {
Expand Down
5 changes: 5 additions & 0 deletions bevy_proto_backend/src/proto/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ pub enum ProtoError {
path: AssetPath<'static>,
existing: AssetPath<'static>,
},
/// Indicates that an operation that requires an entity was attempted on a prototype that doesn't require one.
///
/// This includes attempting to register children on an entity-less prototype.
#[error("expected prototype with ID {id:?} to require an entity")]
RequiresEntity { id: String },
}
6 changes: 6 additions & 0 deletions bevy_proto_backend/src/proto/prototypical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ pub trait Prototypical: Asset + Sized {
fn id(&self) -> &Self::Id;
/// The path to this prototype's asset file.
fn path(&self) -> &ProtoPath;
/// Whether or not this prototype requires an entity to be spawned.
///
/// Defaults to `true`.
fn requires_entity(&self) -> bool {
true
}
/// An immutable reference to the collection of [`Schematics`] contained in this prototype.
fn schematics(&self) -> &Schematics;
/// A mutable reference to the collection of [`Schematics`] contained in this prototype.
Expand Down
6 changes: 4 additions & 2 deletions bevy_proto_backend/src/schematics/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ impl<'a, 'b> SchematicContext<'a, 'b> {

/// Returns a reference to the entity this schematic is being applied to, if any.
pub fn entity(&self) -> Option<EntityRef> {
Some(self.world.entity(self.tree.entity()))
self.tree.entity().map(|entity| self.world.entity(entity))
}

/// Returns a mutable reference to the entity this schematic is being applied to, if any.
pub fn entity_mut(&mut self) -> Option<EntityMut> {
Some(self.world.entity_mut(self.tree.entity()))
self.tree
.entity()
.map(|entity| self.world.entity_mut(entity))
}

/// Find an entity in the tree using the given [`EntityAccess`].
Expand Down
8 changes: 7 additions & 1 deletion bevy_proto_backend/src/tree/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ impl<'a, T: Prototypical> ProtoTreeBuilder<'a, T> {
return Ok(Some(tree));
}

let mut tree = ProtoTree::new(handle, merge_key, prototype.id());
let mut tree = ProtoTree::new(handle, merge_key, prototype);

if let Some(children) = prototype.children() {
self.recurse_children(children, &mut tree, checker)?;
Expand All @@ -67,6 +67,12 @@ impl<'a, T: Prototypical> ProtoTreeBuilder<'a, T> {
self.recurse_templates(templates, &mut tree, checker)?;
}

if !tree.requires_entity() && !tree.children().is_empty() {
return Err(ProtoError::RequiresEntity {
id: prototype.id().to_string(),
});
}

self.registry.insert_tree(handle_id, tree);
Ok(self.registry.get_tree(handle_id).cloned())
}
Expand Down
34 changes: 17 additions & 17 deletions bevy_proto_backend/src/tree/entity_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub struct EntityTree<'a> {
impl<'a> EntityTree<'a> {
pub(crate) fn new<T: Prototypical>(
tree: &'a ProtoTree<T>,
root: Entity,
root: Option<Entity>,
world: &mut World,
) -> Self {
let mut nodes = vec![EntityTreeNode {
Expand All @@ -64,11 +64,15 @@ impl<'a> EntityTree<'a> {
let child_index = entity_children.insert(index, child.id_str());
parents.insert(index, parent_index);

let entity = Self::init_entity(
ProtoInstance::new(child.handle(), child_index),
Some(parent_entity),
world,
);
let entity = if child.requires_entity() {
Some(Self::init_entity(
ProtoInstance::new(child.handle(), child_index),
parent_entity,
world,
))
} else {
None
};

nodes.push(EntityTreeNode {
id: child.id_str(),
Expand All @@ -91,14 +95,14 @@ impl<'a> EntityTree<'a> {
}
}

/// Get the current entity being processed.
pub fn entity(&self) -> Entity {
/// Get the current entity being processed, if any.
pub fn entity(&self) -> Option<Entity> {
self.current().entity
}

/// Find an entity in the tree using the given [`EntityAccess`].
pub fn find_entity(&self, access: &EntityAccess) -> Option<Entity> {
self.get(access).map(EntityTreeNode::entity)
self.get(access).and_then(EntityTreeNode::entity)
}

pub(crate) fn get(&self, access: &EntityAccess) -> Option<&EntityTreeNode<'a>> {
Expand Down Expand Up @@ -250,11 +254,7 @@ impl<'a> Debug for EntityTree<'a> {
smart_write!(f, depth - 1, "EntityTree {{")?;
smart_write!(f, depth, "id: {:?}, ", node.id)?;

if f.alternate() {
smart_write!(f, depth, "entity: {:#?}, ", node.entity)?;
} else {
smart_write!(f, depth, "entity: {:?}, ", node.entity)?;
}
smart_write!(f, depth, "entity: {:?}, ", node.entity)?;

match tree.children.get(&node.index) {
None => {
Expand Down Expand Up @@ -293,7 +293,7 @@ impl<'a> Debug for EntityTree<'a> {
pub(crate) struct EntityTreeNode<'a> {
id: &'a str,
index: usize,
entity: Entity,
entity: Option<Entity>,
prototypes: &'a IndexSet<HandleId>,
}

Expand All @@ -303,8 +303,8 @@ impl<'a> EntityTreeNode<'a> {
self.id
}

/// The corresponding [`Entity`].
pub fn entity(&self) -> Entity {
/// The corresponding [`Entity`], if any.
pub fn entity(&self) -> Option<Entity> {
self.entity
}

Expand Down
22 changes: 17 additions & 5 deletions bevy_proto_backend/src/tree/proto_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub(crate) struct ProtoTree<T: Prototypical> {
id_str: String,
/// The asset handle ID of this prototype.
handle: HandleId,
/// Whether or not this tree requires an entity to be spawned
requires_entity: bool,
/// The set of template prototypes, in their reverse-application order.
///
/// The first entry in the set should be this prototype itself.
Expand All @@ -44,10 +46,11 @@ pub(crate) struct ProtoTree<T: Prototypical> {
}

impl<T: Prototypical> ProtoTree<T> {
pub fn new(handle: Handle<T>, merge_key: Option<MergeKey<T>>, id: &T::Id) -> Self {
pub fn new(handle: Handle<T>, merge_key: Option<MergeKey<T>>, prototype: &T) -> Self {
Self {
id: id.clone(),
id_str: id.to_string(),
id: prototype.id().clone(),
id_str: prototype.id().to_string(),
requires_entity: prototype.requires_entity(),
handle: handle.id(),
prototypes: IndexSet::from([handle.id()]),
merge_key,
Expand All @@ -64,6 +67,10 @@ impl<T: Prototypical> ProtoTree<T> {
self.handle
}

pub fn requires_entity(&self) -> bool {
self.requires_entity
}

/// Append the given tree as a new child of this one.
pub fn append_child(&mut self, tree: Self) {
if let Some(merge_key) = tree.merge_key.as_ref() {
Expand All @@ -86,7 +93,10 @@ impl<T: Prototypical> ProtoTree<T> {
self.prototypes.insert(prototype);
}

// 2. Merge children
// 2. Update entity requirement
self.requires_entity |= tree.requires_entity;

// 3. Merge children
for child in tree.children {
self.append_child(child);
}
Expand All @@ -103,7 +113,7 @@ impl<T: Prototypical> ProtoTree<T> {
}

/// Converts this tree to a corresponding [`EntityTree`], using the given root [`Entity`].
pub fn to_entity_tree(&self, root: Entity, world: &mut World) -> EntityTree<'_> {
pub fn to_entity_tree(&self, root: Option<Entity>, world: &mut World) -> EntityTree<'_> {
EntityTree::new(self, root, world)
}
}
Expand All @@ -114,6 +124,7 @@ impl<T: Prototypical> Clone for ProtoTree<T> {
id: self.id.clone(),
id_str: self.id_str.clone(),
handle: self.handle,
requires_entity: self.requires_entity,
prototypes: self.prototypes.clone(),
merge_key: self.merge_key.clone(),
children: self.children.clone(),
Expand All @@ -127,6 +138,7 @@ impl<T: Prototypical> Debug for ProtoTree<T> {
f.debug_struct(&format!("ProtoTree<{}>", std::any::type_name::<T>()))
.field("id", &self.id)
.field("handle", &self.handle)
.field("requires_entity", &self.requires_entity)
.field("prototypes", &self.prototypes)
.field("merge_key", &self.merge_key)
.field("children", &self.children)
Expand Down
29 changes: 24 additions & 5 deletions examples/basic_schematic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ fn main() {
.register_type::<Alignment>()
// If you didn't use `#[reflect(Schematic)]`,
// you can still register it manually:
.register_type::<PlayerHealth>()
.register_type_data::<PlayerHealth, ReflectSchematic>()
.register_type::<MaxPlayers>()
.register_type_data::<MaxPlayers, ReflectSchematic>()
// =============== //
.add_plugin(ProtoPlugin::default())
.add_startup_systems((setup, load))
.add_systems((
spawn.run_if(prototype_ready("Player").and_then(run_once())),
spawn.run_if(
prototype_ready("Player")
.and_then(prototype_ready("PlayerConfig"))
.and_then(run_once()),
),
inspect,
))
.run();
Expand Down Expand Up @@ -52,29 +56,44 @@ enum Alignment {
/// The derive macro also has basic support for Bevy resources.
///
/// This can be done by specifying the "kind".
/// It's also a good idea to set `entity: false` in the prototype file.
///
/// Note that when a schematic is applied, it will replace the current instance
/// of the resource in the world.
#[derive(Resource, Schematic, Reflect, FromReflect)]
#[schematic(kind = "resource")]
struct PlayerHealth(u16);
struct MaxPlayers(u8);

fn load(mut prototypes: PrototypesMut) {
prototypes.load("examples/basic_schematic/Player.prototype.ron");
prototypes.load("examples/basic_schematic/PlayerConfig.prototype.ron");
}

fn spawn(mut commands: ProtoCommands) {
commands.spawn("Player");
// Since this schematic just defines a resource and doesn't need an entity,
// we can use `apply` instead of `spawn`.
commands.apply("PlayerConfig");
}

// This relies on the `auto_name` feature to be useful
fn inspect(query: Query<(DebugName, &Alignment), Added<Playable>>) {
fn inspect(
query: Query<(DebugName, &Alignment), Added<Playable>>,
max_players: Option<Res<MaxPlayers>>,
) {
for (name, alignment) in &query {
println!("===============");
println!("Spawned Player:");
println!(" ID: {name:?}");
println!(" Alignment: {alignment:?}");
println!("===============");
match &max_players {
Some(max_players) if max_players.is_added() => {
println!("Max. Players: {}", max_players.0);
println!("===============");
}
_ => {}
}
}
}

Expand Down
Loading

0 comments on commit 84cbb77

Please sign in to comment.