bevy_proto
bevy_proto copied to clipboard
Reflection Update
What is this?
This is an update to the crate that replaces typetag
with Bevy's reflection system. It also contains major changes to how prototypes are loaded, spawned, written, and stored.
Why?
One of the main reasons for this is because typetag
has been deprecated and its repository has been archived. I believe the reason for this is because it was essentially a hack and not something truly supported. It also apparently had issues with WASM (from what i've been told).
This means we need an alternative for use in the long run. luckily, Bevy has the bevy_reflect
crate which provides us with some basic reflection support. And since this is meant for the Bevy engine anyways, it makes sense to use what they already have.
What's New?
The two major changes are the switch to bevy_reflect
and updates to prototypes— all other changes are mostly byproducts of one of these.
Below is a list of some of the biggest changes (not all of them, but some of the more noteworthy ones).
Change to Reflection
-
ProtoComponent
has new methods and supertrait bounds:pub trait ProtoComponent: Reflect + Send + Sync + 'static { // Required: fn apply(&self, entity: &mut EntityMut); fn as_reflect(&self) -> &dyn Reflect; // Default: fn name(&self) -> &'static str { std::any::type_name::<Self>() } fn preload_assets(&mut self, preloader: &mut AssetPreloader) {} }
Current Version
pub trait ProtoComponent: Send + Sync + 'static { // Required: fn insert_self(&self, commands: &mut ProtoCommands, asset_server: &Res<AssetServer>); // Default: fn prepare(&self, world: &mut World, prototype: &dyn Prototypical, data: &mut ProtoData) {} }
-
Deriving/Implementing
ProtoComponent
requires new traits:#[derive(Reflect, FromReflect, Component, ProtoComponent, Clone)] #[reflect(ProtoComponent)] struct Health { max: u16, }
Current Version
#[derive(Clone, Serialize, Deserialize, ProtoComponent, Component)] struct Health { max: u16, }
-
Prototype files now use Bevy's Reflect format (this also means a new serializer/deserializer):
# assets/prototype/alice.prototype.yaml --- name: "Alice" template: NPC components: - type: "templates::Named" tuple_struct: - type: "alloc::string::String" value: "Alice Allison"
Current Version
# assets/prototype/alice.yaml --- name: "Alice" template: NPC components: - type: Named value: "Alice Allison"
Prototypes should now end in
.prototype.EXTENSION
. We support the basic.EXTENSION
but it's not preferred since Bevy uses the extension to select the correct asset loader
Change to Prototypes
-
Prototypes are now assets. Custom prototypical types will need to also be a Bevy asset in order for the type to be used by this crate.
-
This means prototypes are now loaded via the
AssetServer
:fn load_prototype(asset_server: Res<AssetServer>) { let handle: Handle<Prototype> = asset_server.load("prototypes/alice.prototype.yaml"); // ... }
-
Prototype templates are loaded as dependencies of the prototype itself and they are now relative paths to other template files rather than names. If given no extension, a template will be expanded with the extension of the current file (i.e.
foo
is read internally as./foo.prototype.yaml
assuming the prototype file ends with.prototype.yaml
) -
ProtoPlugin
is now generic and comes with different config options (check out the config example). -
Protopical
trait has both new and changed methods:pub trait Prototypical: 'static + Send + Sync { // Required: fn name(&self) -> &str; fn dependencies(&self) -> &DependencyMap; fn dependencies_mut(&mut self) -> &mut DependencyMap; fn components(&self) -> Iter<'_, Box<dyn ProtoComponent>>; fn components_mut(&mut self) -> IterMut<'_, Box<dyn ProtoComponent>>; // Default: fn templates(&self) -> Option<&TemplateList> { None } fn templates_mut(&mut self) -> Option<&mut TemplateList> { None } fn spawn<'a, 'p, 'w, 's>( &'p self, commands: &'a mut Commands<'w, 's>, ) -> EntityCommands<'w, 's, 'a> where Self: Asset + Sized { /* ... */ } fn insert<'a, 'p, 'w, 's>( &'p self, entity: Entity, commands: &'a mut Commands<'w, 's>, ) -> EntityCommands<'w, 's, 'a> where Self: Asset + Sized { /* ... */ } }
Current Version
pub trait Prototypical: 'static + Send + Sync { // Required: fn name(&self) -> &str; fn iter_components(&self) -> Iter<'_, Box<dyn ProtoComponent>>; fn create_commands<'w, 's, 'a, 'p>( &'p self, entity: EntityCommands<'w, 's, 'a>, data: &'p Res<ProtoData>, ) -> ProtoCommands<'w, 's, 'a, 'p>; // Default: fn templates(&self) -> &[String] { &[] } fn templates_rev(&self) -> Rev<Iter<'_, String>> { self.templates().iter().rev() } fn spawn<'w, 's, 'a, 'p>( &'p self, commands: &'a mut Commands<'w, 's>, data: &Res<ProtoData>, asset_server: &Res<AssetServer>, ) -> EntityCommands<'w, 's, 'a> { /* ... */ } fn insert<'w, 's, 'a, 'p>( &'p self, entity: EntityCommands<'w, 's, 'a>, data: &Res<ProtoData>, asset_server: &Res<AssetServer>, ) -> EntityCommands<'w, 's, 'a> { /* ... */ } }
-
This means prototypes now only need
Commands
to be spawned -
Added
ProtoManager
system param to help manage these protoypical assetsfn spawn_adventurer(mut commands: Commands, manager: ProtoManager) { if let Some(proto) = manager.get("Alice") { proto.spawn(&mut commands); } }
Possible Improvements and Future Work
File Format
The biggest improvement will come when https://github.com/bevyengine/bevy/pull/4042 is merged as it will allow us to significantly improve the prototype format. In my tests, I managed to change something like:
---
name: "Urist"
components:
- type: "templates::Named"
tuple_struct:
- type: "alloc::string::String"
value: "Urist McTemplate"
- type: "templates::Occupation"
tuple_struct:
- type: "templates::OccupationType"
value: Miner
- type: "templates::Health"
struct:
max:
type: u16
value: 30
into something like:
---
name: "Urist"
components:
- type: "templates::Named"
value: ["Urist McTemplate"]
- type: "templates::Occupation"
value: Miner
- type: "templates::Health"
value:
max: 30
which is much cleaner and easier to work with.
Pruning & Caching
A big issue with the crate is performance. It's fine for small-medium sized batches, but takes 4–6 times longer than it does via Rust code (in debug mode at least). We can possibly mitigate this by doing two things as soon as a prototype is loaded:
- Pruning - We sometimes have cases where templates may be redundantly inherited by a sibling template. Ideally, we'd catch these issues when the asset is first loaded and prune any of these redundancies away.
- Caching - It might also be good to allow a prototype to enable caching, wherein we only recurse through the templates once (when finished loading) in order to track all the components/bundles inserted and maintain a copy of them. Then, we can iterate through and apply the components/bundles in this cache instead of recursing through all the templates again.
Additional Details
I plan on keeping a branch that contains the old typetag
-based system. This will allow people to still contribute to and use that version if they're okay with the risks/deprecation.
Temporarily downgraded to Bevy 0.6 so this could be tested in non-bevy-main
projects.
This should be reverted before merging.
I updated this branch to be compatible with bevy 0.10 https://github.com/aprilwade/bevy_proto/commit/eb88fdd34a45b6c0f07593bc77e16ab633c3180f . I can open a PR to the reflection branch if that would be useful.
I updated this branch to be compatible with bevy 0.10 https://github.com/aprilwade/bevy_proto/commit/eb88fdd34a45b6c0f07593bc77e16ab633c3180f . I can open a PR to the reflection branch if that would be useful.
@aprilwade I appreciate it! Unfortunately, I’m actually planning on closing this branch and putting up a new one very soon (it's similar in a lot of ways but also very different).
I probably should have closed this one in the meantime, sorry about that 😕
Closed as I’m currently working on a rewrite of this rewrite. I’m hoping to release something before the next Bevy Jam but we'll see how that goes 😅