bevy_proto icon indicating copy to clipboard operation
bevy_proto copied to clipboard

Reflection Update

Open MrGVSV opened this issue 2 years ago • 1 comments

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 assets

    fn 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.

MrGVSV avatar Mar 30 '22 23:03 MrGVSV

Temporarily downgraded to Bevy 0.6 so this could be tested in non-bevy-main projects.

This should be reverted before merging.

MrGVSV avatar Apr 02 '22 22:04 MrGVSV

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 avatar Mar 25 '23 16:03 aprilwade

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 😕

MrGVSV avatar Mar 25 '23 17:03 MrGVSV

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 😅

MrGVSV avatar Mar 25 '23 17:03 MrGVSV