bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
35.54k stars 3.52k forks source link

Schematics - An abstraction layer for stable scene representation and exposing ECS configuration in the editor #3877

Open SamPruden opened 2 years ago

SamPruden commented 2 years ago

Problem

Scene representation and the ECS runtime have different and conflicting data representation requirements.

ECS data should be:

Scene representation data should be:

Proposed Solution

This isn't a detailed technical proposal as I'm not yet experienced enough with Bevy's internals to go there, but I'd like to propose that Bevy's architecture adopt this distinction. Inspiration and proof of concept is provided by Unity's DOTS, although Bevy can do better.

Schematics

These were called Authors in the first version of this proposal.

I propose a new first class feature which I will call "schematics", with the following properties:

I can e.g. rewrite my physics engine to use completely different components, but the Rigidbody schematic will remain untouched, and scenes that feature it will continue to work seamlessly! I think this is very important for allowing designers and engineers to iterate on development side-by-side. If these data representations are tied too closely together, changes will be blocked by breaking compatibility with composed assets. More trivially, I can change some component field from f32 to f16.

Unity provides inspiration here. They have a Data Oriented Technology Stack (Unity DOTS) in development which builds ECS into the engine. They have a similar separation between editor data and ECS data, with a live conversion and synchronisation mechanism (Unity LiveLink). They use their classic GameObjects and Monobehaviours as schematics to convert into ECS data and it's all rather messy and clumsy, but it validates the core idea of having this abstraction layer. Bevy has the opportunity do to this right, instead of building on legacy jank.

Joachim Ante, Unity's CTO, said on the forum:

The concept of runtime data and editing data being the same is simply a bad idea. It took me 14 years to figure that out (Sorry it took so long...)

He's a voice of experience to listen to, and a reason to take this idea seriously.

Open questions

Two way synchronisation

In the editor, can a change to an entity reflect back to the schematic?

Should schematics be dev only, or shipped in production?

One option would be to have schematics be used only in the editor, and compiled into a more compact binary format for production. This would likely be optimal for loading times, as in the best case scenario data can be loaded into ECS via optimal mem copy. Unity does this. A disadvantage is that if we change the ECS layout, we need to recompile and reship that asset data. There's a middle ground, where assets are shipped in schematic format, but the compiled form is cached on disk for fast loading.

This could even be taken to the extreme, and used to automatically implement game saving in a way that's reasonably robust across updates which may change data layouts. It would require full serialisation of ECS data back into schematics. I have a hunch that this would end up being impractical and cumbersome, but it's worth thinking about. This is probably a bad idea.

Schematics as crates?

In a healthy ecosystem of Bevy assets, would it make sense to have schematic only crates? They could provide a standard interface to certain units of functionality, that could be implemented independently by other crates. For example, perhaps a standardised Rigidybody schematic could be used that would allow physics engines to be swapped out, each interpreting the schematic data as they see fit.

Pros

Cons


If this is better served by a discussion than an issue, then feel free to convert it.

alice-i-cecile commented 2 years ago

This is a great write-up, and I like keeping it as an issue. Eventually, this will need to be an RFC, but I think we're too far out for that to be feasible: there's still a lot of related work that needs to be fleshed out.

alice-i-cecile commented 2 years ago

I propose a new first class feature which I will call "authors", with the following properties:

IMO this badly needs a new name: it will deeply confuse users, as "authors" in the sense of "users, particularly on the art side" is a natural interpretation of that term.

So, what to call them?

alice-i-cecile commented 2 years ago

As for the idea itself: I like this quite a bit. Certainly, I like it much, much more than overloading bundles to incorporate these use cases.

I think it does a good job solving the stable serialization problem, and decoupling the "designer / artist representation" from the "programmer representation". There will be some serious technical and UX hurdles to overcome, but I think they're worth it.

SamPruden commented 2 years ago

Agreed on the need for a name change! I stole that from Unity, which calls theirs "Authoring components", but that's clumsy.

I think definitely not "prefab". If Bevy has prefabs, they would be composed of objects, which are in turn composed of authors - or whatever we call them.

Blueprint is actually Unreal not Unity, not that that's relevant. :)

I think I like "schematic"!

Another option might be "widget", or "editor". If there's a one-to-one correspondence between "schematics" and editor widgets, maybe they're just the same thing with one name.

I've updated the proposal to use "schematic" instead of "author". It's a simple find/replace, so hopefully I didn't break anything.

SamPruden commented 2 years ago

I should mention the existence of the Unity patent. I haven't read it and I'm not qualified to interpret it, but I understand that it gets close to this area and touches on their GameObject to ECS conversion system. Hopefully it's not a blocker. It would be rather evil if it were, as I think this is a fairly straightforward and obvious design direction that falls out of basic engineering principles.

SamPruden commented 2 years ago

If for some reason one wants to be lazy and not deal with the boilerplate of schematics, a simple component could perhaps be its own schematic by simply throwing a #[derive(Schematic)] on it. It would then show up in the editor, and conversion would be done by .clone() or something.

This would forgo most of the benefits and generally be against Best Practice, but maybe people would want this to lower friction during rapid prototyping. It might be a good idea to make sure this works, even if we maybe lint against it.

alice-i-cecile commented 2 years ago

If for some reason one wants to be lazy and not deal with the boilerplate of schematics, a simple component could perhaps be its own schematic by simply throwing a #[derive(Schematic)] on it. It would then show up in the editor, and conversion would be done by .clone() or something.

This would forgo most of the benefits and generally be against Best Practice, but maybe people would want this to lower friction during rapid prototyping. It might be a good idea to make sure this works, even if we maybe lint against it.

Doing bad things should be hard. They can manually impl Schematic if they really want. We may want a helper method between Schematic and Bundle though?

sixfold-origami commented 2 years ago

In general, I like this a lot!

Conversion from schematics to ECS happens live in the editor - schematic and ECS scenes are kept in sync

I might be misinterpreting a typo here, but IMO this should happen in the code, not the editor (although the editor should provide a visual for what the schematic "breaks down" into.)

Can also be used directly from code - if you're composing an entity in code, you may sometimes choose to do this using schematics for interface stability

This is arguably an implementation detail, but IMO this shouldn't be enforced. Not all schematics will make sense to use a psuedo-bundles, especially if they contain entities. I think the better design here is to allow and encourage that schematics impl the Bundle trait. This way they can easily hook into the existing Bundle functionality without adding additional complexity to the user.

Re: two-way synchronization

Yes, this should probably be borne out in the editor, at least visually. This means we need a bijective mapping between schematics and their internals. This is likely more work, but probably worth it.

Re: schematics being dev-only

This is probably a bad idea.

Yeah. Unfortunately, I think I'm with you on this one, even though it does sound really cool. Regardless, I think this can be considered separately, probably in its own RFC down the line. (In the meanwhile, I think we can/should act under the assumption that schematics are dev only.)

Re: schematics as crates

I like this idea a lot. It hits very similar notes to, for example, the log crate, which exists purely to "logging facade". My only hesitation is that different teams/internal architectures may want different schematic divisions, but I think we should try to support this regardless.

IceSentry commented 2 years ago

This idea sounds interesting, but I don't personally know of any popular game engines that actually exposes an editor that let's you play with the ECS directly. I feel like this is an unexplored space and we might be missing some cool concepts if we don't at least try to have an ECS first editor.

I get the part that unity isn't great because it merges runtime and editing time data, but I feel the issue is also based around the fact that unity doesn't really have a data first model. At least not currently. It's still a good concern to have and making it easier for artist to use is a good idea. I just don't want to do this at the expanse of making tooling for people wanting to work with the ECS more directly.

SamPruden commented 2 years ago

Conversion from schematics to ECS happens live in the editor - schematic and ECS scenes are kept in sync

I might be misinterpreting a typo here, but IMO this should happen in the code, not the editor (although the editor should provide a visual for what the schematic "breaks down" into.)

Assuming that the editor provides a live preview of the game, that preview will be powered by the ECS. This would basically be the actual game, but not running the gameplay systems.

When we change data on the schematics, this needs to update in realtime in that preview. There needs to be a live ECS world converted from the schematic world. The editor would be responsible for doing incremental updates to keep this in sync by detecting changes to the schematics and reinvoking the conversion for only those relevant entities.

This is roughly how Unity does it.

Can also be used directly from code - if you're composing an entity in code, you may sometimes choose to do this using schematics for interface stability

This is arguably an implementation detail, but IMO this shouldn't be enforced. Not all schematics will make sense to use a psuedo-bundles, especially if they contain entities. I think the better design here is to allow and encourage that schematics impl the Bundle trait. This way they can easily hook into the existing Bundle functionality without adding additional complexity to the user.

I see these as having different purposes. A bundle represents specific a collection of ECS components and is tied to the ECS layout. A schematic is more like a declarative factory, where the same schematic may later map to different components after a refactoring. I'm excited about Bevy but have very little experience with it, so I'm not confident in my understanding of the role of bundles and can easily be persuaded on this point.

Re: two-way synchronization

Yes, this should probably be borne out in the editor, at least visually. This means we need a bijective mapping between schematics and their internals. This is likely more work, but probably worth it.

Agreed, although I think that full bijection is probably impossible because it's not guaranteed to be one-to-one, so fields would need to be either bijected, or enter a desynchronised state.

Re: schematics as crates

I like this idea a lot. It hits very similar notes to, for example, the log crate, which exists purely to "logging facade". My only hesitation is that different teams/internal architectures may want different schematic divisions, but I think we should try to support this regardless.

The open question here is how many different implementations would be comfortable working from the same stable schematic interface, or whether they would all have slightly different requirements that makes this impractical.

SamPruden commented 2 years ago

This idea sounds interesting, but I don't personally know of any popular game engines that actually exposes an editor that let's you play with the ECS directly. I feel like this is an unexplored space and we might be missing some cool concepts if we don't at least try to have an ECS first editor.

My take on this would be that a good editor design would still expose the ECS in a mostly readonly form. As we edit schematics, we get a live visualisation of what they're being converted into, as well as visualisations of which queries they feature in and all of that. ECS data could even be edited directly in the editor for debugging purposes.

Any well designed schematic will give intuitive and complete control over the ECS data anyway.

My intent is not to hide the ECS, just to provide a safe, mediated, and well designed way of interacting with it.

james7132 commented 2 years ago

Having dealt with this in Unity's ECS implementation, I would like to say that this introduces quite a few interesting and unintuitive edge cases. Trying to explain to a game designer that two distinct authoring components X and Y cannot be used together because they share a underlying component is horrible UX. For programmers, this is sort of workable since the current Bundles has this issue, but the code and bundle definitions are directly visible to them from the API.

SamPruden commented 2 years ago

Having dealt with this in Unity's ECS implementation, I would like to say that this introduces quite a few interesting and unintuitive edge cases. Trying to explain to a game designer that two distinct authoring components X and Y cannot be used together because they share a underlying component is horrible UX. For programmers, this is sort of workable since the current Bundles has this issue, but the code and bundle definitions are directly visible to them from the API.

Agreed. Unity's current version of this is clunky, but I think those are solvable problems. The editor should be able to detect things like conflicts and give clear explanations.

Probably the best that can be done here is best practices for schematic design that don't lead to conflicts.

There can be relations like a schematic requiring a component to be present without setting its value, or setting a marker component in ways that automatically resolve conflicts. This is a little tricky and will require some design work, but I'd say it's doable.

Can you give some examples of times when you've had multiple Unity author components that conflict like that?

sixfold-origami commented 2 years ago

Conversion from schematics to ECS happens live in the editor - schematic and ECS scenes are kept in sync

I might be misinterpreting a typo here, but IMO this should happen in the code, not the editor (although the editor should provide a visual for what the schematic "breaks down" into.)

Assuming that the editor provides a live preview of the game, that preview will be powered by the ECS. This would basically be the actual game, but not running the gameplay systems.

When we change data on the schematics, this needs to update in realtime in that preview. There needs to be a live ECS world converted from the schematic world. The editor would be responsible for doing incremental updates to keep this in sync by detecting changes to the schematics and reinvoking the conversion for only those relevant entities.

This is roughly how Unity does it.

Ah, that makes sense. I thought you were saying that the mapping layer would be defined by the editor. This is a lot more clear to me now. Thanks!

Can also be used directly from code - if you're composing an entity in code, you may sometimes choose to do this using schematics for interface stability

This is arguably an implementation detail, but IMO this shouldn't be enforced. Not all schematics will make sense to use a psuedo-bundles, especially if they contain entities. I think the better design here is to allow and encourage that schematics impl the Bundle trait. This way they can easily hook into the existing Bundle functionality without adding additional complexity to the user.

I see these as having different purposes. A bundle represents specific a collection of ECS components and is tied to the ECS layout. A schematic is more like a declarative factory, where the same schematic may later map to different components after a refactoring. I'm excited about Bevy but have very little experience with it, so I'm not confident in my understanding of the role of bundles and can easily be persuaded on this point.

Bundles are mostly used to add a group of components together to an entity. They're often used when multiple components are required together, or when a plugin wants to put many components on certain entities (AKAIK at least). The idea of the schematic being a factory is interesting. Do you imagine that, when used in this way, multiple schematics would be composed or combined together? If not, then I can see the argument that they should be used as primitives. If, however, they are to be composed with other components, then I think hooking them into the Bundle trait provides a cleaner API. This distinction might be best left for the RFC though.

I think the refactoring point is moot here- if, during a refactor, the schematic mapping changes, then the Bundle implementation would also need to change in turn. This is more work, but I think that tradeoff is worth it.

Re: two-way synchronization Yes, this should probably be borne out in the editor, at least visually. This means we need a bijective mapping between schematics and their internals. This is likely more work, but probably worth it.

Agreed, although I think that full bijection is probably impossible because it's not guaranteed to be one-to-one, so fields would need to be either bijected, or enter a desynchronised state.

Hm. Could you give an example where the fields would become desynchronized?

Re: schematics as crates I like this idea a lot. It hits very similar notes to, for example, the log crate, which exists purely to "logging facade". My only hesitation is that different teams/internal architectures may want different schematic divisions, but I think we should try to support this regardless.

The open question here is how many different implementations would be comfortable working from the same stable schematic interface, or whether they would all have slightly different requirements that makes this impractical.

This is an excellent point. I'm not entire sure what the right answer is. I think it's entirely reasonable that, in some cases, the backing implementation of a schematic is a black box. For example, using different collision detection engines for realtime games vs scientific simulation. The main point of divergence at the schematic level would come from, I think, configuration parameters. If one physics library has additional options/features, then those would need to be exposed with either a different schematic, or a second schematic working in tandem.

Unfortunately, I don't know enough about different implementation strategies to confidently say how common either of those cases will be.

SamPruden commented 2 years ago

Bundles are mostly used to add a group of components together to an entity. They're often used when multiple components are required together, or when a plugin wants to put many components on certain entities (AKAIK at least). The idea of the schematic being a factory is interesting. Do you imagine that, when used in this way, multiple schematics would be composed or combined together? If not, then I can see the argument that they should be used as primitives. If, however, they are to be composed with other components, then I think hooking them into the Bundle trait provides a cleaner API. This distinction might be best left for the RFC though.

I think the refactoring point is moot here- if, during a refactor, the schematic mapping changes, then the Bundle implementation would also need to change in turn. This is more work, but I think that tradeoff is worth it.

Perhaps it makes sense to think of schematics like parameterised bundles. That isn't a perfect analogue, because schematics can also add additional entities, which bundles can't do.

In the editor, we would add a Mesh Renderer schematic to associate a mesh with an entity, and setup all of the appropriate components in the ECS. Here's Unity's Mesh Renderer component, as an approximate example of what that might look like.

image

In the ECS, this maps to different components depending on what settings are used. For example, the receive shadows bool on the schematic probably conditionally adds a ShadowReceiver marker component. When I'm setting up this schematic in the editor, I don't have to know or care about that marker component, I just care about that bool/checkbox. Maybe during development it's decided that there should be a NotShadowReceiver marker component instead, and the schematic conversion logic can be updated for this, but the schematic's data doesn't have to change, and remains compatible with work already done in the editor.

A schematic takes in some declarative description of the functionality being added, and dynamically sets up the ECS data to achieve that goal, for example conditionally adding marker components according to a bool value.

People could simply have a fn setup_mesh(mesh: Handle<Mesh>, [...], receive_shadows: bool) that does this in their code, but that would be duplicating functionality from the schematic, so we might as well allow the schematic to be used from code in this same way.

Hm. Could you give an example where the fields would become desynchronized?

Well schematics can be converted into ECS data by arbitrary conversion logic, so that process will only be bijective if the user writes a bijective conversion. We can't guarantee that, so we need to have a fallback.

I think most conversions would be bijective, but anything that does a many-to-one mapping wouldn't be. Maybe a schematic is setup to allow a high/medium/low enum config, but in the end medium and low get mapped to the exact same ECS data. Now it's impossible to reconstruct the schematic from the ECS.

It's even possible that one of the components added by a schematic gets dynamically removed during gameplay. In this case, there's nothing to sync back, but the editor still has to do something.

There's probably some clever design work to do here, but I think that the obvious and general approach is to just sync back when we can and when it's helpful, and gracefully fail when we can't.

This is an excellent point. I'm not entire sure what the right answer is. I think it's entirely reasonable that, in some cases, the backing implementation of a schematic is a black box. For example, using different collision detection engines for realtime games vs scientific simulation. The main point of divergence at the schematic level would come from, I think, configuration parameters. If one physics library has additional options/features, then those would need to be exposed with either a different schematic, or a second schematic working in tandem.

Unfortunately, I don't know enough about different implementation strategies to confidently say how common either of those cases will be.

Agreed. I think supporting schematic crates should be fairly trivial - it's just a struct - but whether they will prove valuable is an open question.

therocode commented 2 years ago

Just a thought as I'm reading this very promising discussion: If schematics are about a stable API for scenes/designers, maybe the schematic data format and system could automatically include version awareness?

If I do need to break compatibility in the API of a certain schematic, maybe that should automatically lead to a version bump for that schematic so that the editor/code can detect when an older schematic is loaded. This could lead to an error instead of undefined behaviour, and perhaps a mechanism for writing migrations on a per-schematic basis could be provided.

Maybe I'm getting ahead of myself here but these situations will definitely arise so maybe worth thinking about.

alice-i-cecile commented 2 years ago

If schematics are about a stable API for scenes/designers, maybe the schematic data format and system could automatically include version awareness?

I like this a lot. IMO embedding a semver'd version number is a critical part of the design here.

SamPruden commented 2 years ago

Agreed on the version point, although relying on devs to semver each schematic might be a little brittle. This could be at least somewhat automated by also tracking descriptions of the structs, but I haven't put much thought into that beyond assuming that it's a good idea.

Although the aim is to have schematics be as stable as possible, we should also make them as robust to changes as possible. For example, if a field were removed from a schematic, that probably shouldn't break assets built with it. That data can simply be ignored. When there's an incompatibility that can't be resolved automatically, we should make sure that we can give good errors. The serialisation format needs to have enough information to do that.

Another area where automated compatibility checking is key would be cached conversion. If we assume that we would like to avoid running the full conversion process every time an asset is loaded, then we would want to cache it into a compact representation for fast loading into the ECS. That cache would need to be automatically invalidated when the conversion logic changed. Detecting that automatically sounds... maybe hard.

Serialisation should probably be in a fairly simple plaintext format rather than any type of packed binary situation. Firstly because being less sensitive to precise layout is a good thing, and secondly because it plays much more nicely with version control.

I'm getting way ahead of myself here, but I can even imagine a future where the editor is version control aware, encourages and helps designers to use a version control workflow, and can provide a nice interface over the git history of a level design. Like IDE features, but for level editing. I don't know if this is a good idea, but it sounds cool in my head. I think compatibility with this possibility would be good to keep in mind when designing schematics.

I realise this is suddenly becoming a very large feature that touches all areas of editor design. Oops.

alice-i-cecile commented 2 years ago

@SamPruden Can you update the title of this issue to include "Schematics" so it's easier to find / more clear what it's referring to? :)

SamPruden commented 2 years ago

@SamPruden Can you update the title of this issue to include "Schematics" so it's easier to find / more clear what it's referring to? :)

Done. That good?

AlphaModder commented 2 years ago

I was thinking about how we might do schematic conversion and deconversion API-wise, and had to write down this snippet to get it out of my mind:

Code snippet ```rust pub trait Schematic { fn build<'b, 'e>(&'b self, EntityBuilder<'b, 'e>) -> Option Fn(&'u mut Self, SchematicEntities<'u, 'e>) + 'e>>; } type Invariant<'e> = PhantomData<&'e mut &'e ()>; #[derive(Copy, Clone)] struct SchematicEntity<'e>(Entity, Invariant<'e>); struct EntityBuilder<'b, 'e>(EntityMut<'b>, Invariant<'e>); impl<'b, 'e> EntityBuilder<'b, 'e> { fn insert(&mut self, value: T) where T: Component -> &mut Self { self.0.insert(value); self } fn insert_bundle(&mut self, value: T) where T: Bundle -> &mut Self { self.0.insert_bundle(value); self } // ... etc. fn id(&self) -> SchematicEntity<'e> { SchematicEntity(self.0.id(), Invariant) } fn secondary_entity<'a>(&'a mut self) -> EntityBuilder<'a, 'e> { EntityBuilder(self.0.world().spawn(), Invariant) } } pub struct SchematicEntities<'u, 'e> { primary_id: SchematicEntity<'e>, // for convenience, not strictly necessary world: &'u World, } // can't expose EntityRef directly since it has `EntityRef::world` (and, to a lesser extent, `EntityRef::get_mut_unchecked`) pub struct SchematicEntityRef<'a>(EntityRef<'a>); impl<'a> SchematicEntityRef<'a>(EntityRef<'a>) { fn get(&self) -> Option<&'a T>; // uh, is this sound? i copied it from EntityRef... // etc... } impl SchematicEntities<'u, 'e> { fn primary(&self) -> Option> { self.secondary(self.primary) } fn secondary(&self, entity: SchematicEntity<'e>) -> Option { world.get_entity(entity.0).map(|e| SchematicEntityRef(e, Invariant)) } } ```

The basic idea here is that calling build on a schematic allows it to instantiate some number of entities, and it can optionally return a function used to update its settings from those entities later. The main trick is using the invariant/generative lifetime 'e to ensure statically that the returned update closure can only read entities that belong to it. If the lifetimes are seen as too confusing, this could be made a runtime check (or simply ignored), but I wanted to demonstrate that it is possible. To be clear, the parameter 'e only exists to prevent smuggling SchematicEntitys outside of the function or the closure it returns. Its actual value when calling Schematic::build is irrelevant, and internally we would just pass 'static so we can actually store the closure returned.

One extension that could be made to this API is for the schematic to be able to reserve entity ids ahead of time (i.e. stored in its settings) and spawn secondary entities with those IDs. That way, one schematic could create components which reference the not-yet-created entities of another schematic by ID if desired. Such AOT IDs would just be Entity (or maybe another type convertible to EntityId with an EntityBuilder?), but importantly not SchematicEntity, since they do not belong to schematics they were not created by.

SamPruden commented 2 years ago

The idea of returning an optional update function is interesting! I can see that that could be very nice, but it does leave some challenges:

I think we should look in a little more detail at when the update functionality would be used. I see two scenarios.

  1. We're playing the game in the editor, and we want to see the state reflected back into the schematic UI just to see what's happening. This is for temporary display purposes, but doesn't actually update the saved scene representation, and resets when we stop playing the game. We can potentially still edit this to update the entities during gameplay - for example, edit the transform to move an entity.
  2. We let the game run and the entities update, and we actually want to save this back into the scene. Like 1, but permanent.

I'm not 100% sure that the second scenario is even necessary. It's hard to do for arbitrary gameplay, because the systems may have changed things in arbitrary ways. Do we want this? Is it useful?

However, looking only at the first scenario, it raises the question of whether updating back to the schematic is actually the right way to go. Perhaps what we want instead is to display that same information in the editor by a different mechanism. The schematic update function could return a completely separate set of fields that represent the state during gameplay. This avoids the bijection problem, as it only has the fields that it wants to display. This would in turn need a way to apply its changes back to the entities for live editing... This may be neater, but might also be getting into Too Much Boilerplate territory. I've got some vague API ideas around some type of Live Schematic for this purpose, but I'll wait until I'm less sleepy to see if they make sense.

AlphaModder commented 2 years ago

@SamPruden Actually, I had a different use-case in mind for the above mechanism. My thoughts were more on the idea of providing some sort of hybrid editing, where it might be possible for schematics and direct adjustment to ECS entities to coexist in some fashion. That said, the more I think about this the more it seems like a questionable proposition :P

As for the points you made, yeah, I don't think we could promise much more than updating on a best-effort basis, which is perhaps not very useful for nontrivial schematics. I do like the idea of maybe just displaying the state differently at runtime, but I wonder if we could get away with it being a strict subset of the in-editor state. If a schematic provides a set of 'editor fields,' we might just give it the option of hiding or making readonly those fields that can't be reliably displayed or changed at runtime. Or perhaps we could allow the fields to be changed, but for those which cannot be readily updated, display a warning stating that the schematic needs to be fully regenerated in order to apply changes (which might destroy some changes in the world that the schematic cannot accommodate).

SamPruden commented 2 years ago

There's a scenario which we haven't given proper consideration yet: How do we do nice editor display for entities spawned at runtime, not from schematics?

I'll give a brief summary of the requirements first, using the example of a trivial Transform schematic:

  1. During editing, the transform's fields are displayed nicely and can be edited arbitrarily
  2. The object can also be positioned in the editor using drag handles of some kind - this updates the schematic
  3. Whilst playing the game in the editor, we can still see and manually update the live values for the transform
  4. Dynamically spawned entities which have the transform components, but might not have a transform schematic, also display nicely whilst the game is running in the editor

There are two properties which would be nice to achieve, but which seem to conflict:

  1. The mechanism/UX is the same during normal editing (1), and during editing whilst the game is playing in the editor (3)
  2. The mechanism/UX is the same for objects composed in the editor using schematics (1, 3), as for dynamically spawned entities (4)

Property 1 relies on schematics for both scenarios. Property 2 requires that the behaviour be the same without a schematic being present. This is awkward.

I see two broad approaches here:

I think what I've identified here is a significant hole in my original version of the proposal, but I'm keen to hear feedback on this. I think the query approach is interesting, but I would need to do some more thinking to work out what it actually looks like.

AlphaModder commented 2 years ago

@SamPruden To be honest, I am not a fan of either option presented. Although I'm not opposed to making schematics available at runtime, effectively forcing them to be used by all gameplay code for projects using the editor seems like too much of a blow to Bevy's strength in code-first workflows to me. And I think inferring schematics from entities would range from tricky to near-impossible depending on how complex schematics we intend to support, for essentially the reasons you listed. I would also add that trying to automatically detect schematics which span multiple entities would be an incredibly difficult problem. Instead, I would propose a third option: don't hide the ECS representations when we don't need to.

I agree that schematics should be the serializable source of truth, but the editor could still provide the ability to view an object in terms of entities and components, in addition to a collection of schematics. This would naturally imply that both components and schematics can provide inspectors and widgets. Then something like a Transform widget would simply be provided by the Transform component, rather than the schematic that generates it. Of course, there are some challenges to this approach:

Below is a much more detailed explanation of how this approach could work at the API level:

Details A schematic is a struct or perhaps a dynamic piece of data containing all information necessary to provide a logical group of functionality. It is serializable, cloneable, and has a sane default value. It provides a `build` method, which creates the ECS components and entities corresponding to the schematic: ```rust pub trait Schematic { fn build<'b, 'e>(&'b self, root: EntityBuilder<'b, 'e>) -> Result, BuildError>; // other methods... } ``` The first responsibility of `build` is to instantiate the schematic by adding components and entities to the world through the provided `EntityBuilder`, whose API is shown below. Each entity created this way is indelibly associated with the schematic, and a handle of type `SchematicEntity` is returned for later use. Additionally, when a component is inserted using `EntityBuilder`, the schematic may use a flag to prevent the user from editing this component directly, in which case its fields will show up as read-only in the ECS inspector. This allows the schematic to prevent the user (though not gameplay systems) from making ECS-level changes that it would not be able to accommodate. The widgets of such a component would also be read-only, and could be hidden entirely by another similar flag. ```rust #[derive(Copy, Clone)] pub struct SchematicEntity<'e>(Entity, PhantomData<&'e mut &'e ()>); bitflags! { pub struct EditorComponentFlags: u32 { const NONE = 0b00000000; const PREVENT_EDITING = 0b00000001; const HIDE_WIDGETS = 0b00000010; } } impl EntityBuilder<'b, 'e> { fn insert(&mut self, value: T, flags: EditorComponentFlags) -> &mut Self; fn require(&mut self, flags: EditorComponentFlags) -> Result<(), BuildError>; fn id(&self) -> SchematicEntity<'e>; fn secondary<'a>(&'a mut self) -> EntityBuilder<'a, 'e>; } ``` The second responsibility of `Schematic::build` is to return a `LiveSchematic`, which consists of two *fallible* databinding callbacks for syncing data from the schematic to the ECS and vice versa. Importantly, these callbacks are able to capture the `SchematicEntity`s obtained from the `EntityBuilder`, so that they can easily reference the entities that belong to the schematic. `LiveSchematic` could be defined like this: ```rust pub struct LiveSchematic<'e, S> { on_schematic_changed: Box Fn(&'c S, SchematicEntities<'c, 'e, Mut>) -> Result<(), NeedsRebuild>>; on_components_changed: Box Fn(&'c mut S, SchematicEntities<'c, 'e, Immut>) -> Result<(), OutOfSync>>; } impl<'u, 'e, Access> SchematicEntities<'u, 'e, Access> { fn root(&self) -> Option>; fn entity(&self, id: SchematicEntity<'e>) -> Option>; } impl<'a, Access> SchematicEntityRef<'a, Access> { fn get(&self) -> Option<&T>; fn changed(&self) -> bool; // using reliable change detection, determines whether the value of T has changed. } impl<'a> SchematicEntityRef<'a, Mut> { // we will need to be careful with changeticks to avoid causing `on_schematic_changed` to recursively call `on_components_changed`. fn get_mut(&mut self) -> Option>; } ``` When a field of the schematic is changed, we call `on_schematic_changed`. If it returns `Ok`, all is well, but if it returns `Err(NeedsRebuild)`, the behavior depends on the editor's mode. In 'edit mode,' we just immediately rebuild the schematic by deleting all entities associated with it and calling `build` again. In 'play mode,' we mark the schematic as out-of-sync and display a warning in the UI, but do not rebuild the schematic unless the user presses a button in its UI marked as doing so. As for `on_components_changed`, if `EntityBuilder` keeps track of every component added to an entity by the schematic, we can use changeticks to reliably detect when a component of interest has changed. and call `on_components_changed`. If it returns `Ok`, no problems there, but if it returns `OutOfSync`, we place a warning on the schematic and give the user the option to rebuild it. Ideally, this kind of `OutOfSync` warning would rarely occur during edit mode (when no gameplay systems run,) since the `Schematic` can simply use `EditorComponentFlags::PREVENT_EDITING` to prevent the user from modifying components it is unable to sync. After all that, perhaps an example would help. Here's my idea of how the `Schematic` trait could be implemented: ```rust pub struct MySchematic { non_bijective_data: Field, // &NonBijectiveData: Into component2: Field, // the `Field` wrapper provides change detection for inspector controls, maybe some other functionality. It derefs to `T`. } impl Schematic for MySchematic { fn build<'b, 'e>(&'b self, root: EntityBuilder<'b, 'e>) -> Result, BuildError> { // this schematic only makes sense for entities which have a `Transform`, but doesn't manage that transform itself. let transform = root.require::()?; // this component can't be properly updated, so we'll pass `PREVENT_EDITING` so the user can't change it from the editor // of course, gameplay systems might still change it, which could lead to an `OutOfSync` later. this is unavoidable root.insert::(self.non_bijective_data.into(), EditorComponentFlags::PREVENT_EDITING); // maybe the `Component2` needs to go on a secondary entity for some reason let secondary: SchematicEntity<'e> = root.secondary() // this time, we can easily sync changes to the component, so don't prevent user from editing it .insert::(self.component2.clone(), EditorComponentFlags::NONE) .id(); Ok(LiveSchematic { on_components_changed: Box::new(|this, entities| { // this data is bijective, if it changes on the entity we can just copy it back into the schematic if(entities.root().changed::()) { this.component2 = entities.root().get::().ok_or(OutOfSync)?; } // the conversion from NonBijectiveData to Component1 is irreversible. If component1 changes, we are out of sync. if(entities.root().changed::()) { return Err(OutOfSync) } Ok(()) }), on_schematic_changed: Box::new(|this, entities| { if this.component2.changed() { *entities.entity(secondary).ok_or(NeedsRebuild)?.get_mut::() = this.component2.clone(); } if this.non_bijective_data.changed() { return Err(NeedsRebuild) } Ok(()) }), }) } } ```
colepoirier commented 2 years ago

What if we steal Unreal Engine 5’s term and call them Blueprints instead of Schematics?

alice-i-cecile commented 2 years ago

What if we steal Unreal Engine 5’s term and call them Blueprints instead of Schematics?

These are much closer to "visual scripting", so I felt that the term was confusing.

colepoirier commented 2 years ago

What if we steal Unreal Engine 5’s term and call them Blueprints instead of Schematics?

These are much closer to "visual scripting", so I felt that the term was confusing.

Apologies, I’m not following. Can you elaborate further?

bjorn3 commented 2 years ago

https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/Blueprints/

The Blueprint Visual Scripting system in Unreal Engine is a complete gameplay scripting system based on the concept of using a node-based interface to create gameplay elements from within Unreal Editor.

colepoirier commented 2 years ago

Ah thanks Bjorn! That makes sense now, and schematics is definitely a better term for this feature in light of this popular use of the term blueprints.

B-Reif commented 2 years ago

Some of the usages described here sound like very leaky abstractions to me.

Specifically, if multiple schematics have colliding components, then designers must reason about the actual components anyway. In that case, schematics make things worse, because both designers and developers have to learn about and maintain this mapping between schematic and ECS.

We can have serialized representations of components that correspond directly to components. One good example of this in practice is the Noita modding API. This has serializable entities written in xml:

<Entity tags="prop">

  <VelocityComponent />

  <SimplePhysicsComponent />

  <SpriteComponent
    z_index="1"
    image_file="data/props_gfx/banner.xml"
    offset_x="0"
    offset_y="0"
  ></SpriteComponent>
</Entity>

To support cases where the ECS is refactored, we can help developers migrate existing serialized assets. This is the most common way to support refactoring the shape of serialized data when working with a SQL database, for instance. Migrations provide a direct mapping between versions without the need to maintain a separate mapping layer.

therocode commented 2 years ago

I feel the big benefit you're missing with having another layer over pure ECS is the previous mentioned point that the extra layer would have the purpose of providing a better workflow for designers. The underlying ECS is highly technical out of necessity (they are what implements the game under the hood with all the dirty details involved) whereas Schematics would live in the design universe so to speak.

This distinction isn't even anything new. Many studios and engine tooling work that way where f.ex. more senior technical designers are part of the process of building this higher abstraction layer with a more in-depth understanding than the typical designer. The end result is stuff that is easy to use and makes sense for the typical designer who doesn't need to be technical.

As for conflicting components, that's something that will pop up and it'd be beneficial if the Schematics system helped with this. Perhaps policies could be encoded on how components are overridden (or not) when there's a collision. Managing and working this out would probably also be the kind of work that would fall on technical designers and the like.

I think this "issue" is warranted because the complexity of components/attributes of entities IS a complex issue regardless of how you shape the system. The difference is going to be where this complexity is residing and what tools exist to manage it. For example, if there is no Schematic layer and only reusable "bundles" of components, you will STILL run into conflicts. The only case that wouldn't have conflicts is if designers have no tools at all and manually have to add every component to every single entity and this is not helpful - hence this complexity will need to be managable by tooling.

SamPruden commented 2 years ago

@B-Reif I have briefly considered migration as an option. My feeling is that it's too brittle, and too much work to maintain. Whatever solution gets landed on should have minimal overhead to use, because otherwise we'll end up in a situation where people avoid doing refactoring that would benefit the project, because the risk or cost is too high.

Migration is tricky, because for the complexity of refactoring we want to handle, migration would need custom logic. It's just far too easy to let a subtle bug slip into that process and corrupt asset data. You also get into this tricky state where your migration logic has to reason about two versions of the program simultaneously, including migrating from old versions of structs that may not even exist in the source anymore, so it can't be strongly typed.

The advantage of schematics as stable formats is that if you write buggy interpretation logic (sidenote: I like the terminology of "interpreting a schematic into ECS") it doesn't corrupt anything, you just fix the bug and restart.

As a loose goal, I would say that if updating a schematic to handle an ECS refactoring ever takes more than 5 minutes, we've probably made it too complicated for people.

B-Reif commented 2 years ago

As for conflicting components, that's something that will pop up and it'd be beneficial if the Schematics system helped with this. Perhaps policies could be encoded on how components are overridden (or not) when there's a collision. Managing and working this out would probably also be the kind of work that would fall on technical designers and the like.

When I describe this as a "leaky abstraction", this is what I mean. The underlying implementation "leaks" out, and the user is now dealing with the ECS anyway. In this case, the user must now reason about:

All of these elements introduce new layers of overhead to reason about. Components are already declarative sets of data. An arbitrary mapping from a different set of data can just as easily add complexity as "manage" it.

B-Reif commented 2 years ago

My take on conflict resolution: we expose an escape hatch, enabling users to fall back to specifying every component manually. Schematics help when they can, and get out of the way otherwise.

This would imply a couple of things:

This way developers are not required to maintain a set of conflict resolution policies, designers can resolve conflicts differently on a case-by-case basis, and schematics still help when conflicts are absent.

SamPruden commented 2 years ago

My take on conflicts, from intuition and from working with a similar system in Unity a little bit, is that they're a rare problem in the real world. Single components are rarely shared between two completely different units of functionality. Sometimes one unit of functionality depends on another being present, but that's handled easily enough by a RequiresOtherSchematic relationship, which is trivial for the user. Conflicts on marker components are also trivially resolved, because it's fine to add the same marker twice.

There's a danger that my intuition about this being rare is wrong.

@B-Reif To make this a little more concrete, can you propose an example of a couple of hypothetical schematics and in what way they would conflict?

B-Reif commented 2 years ago

A conflict could emerge when any two schematics specify values for the same ECS component.

For instance, suppose we implement a card game where cards have some component Cost(Option<u16>). Two different schematics specify the Cost of a card. Let's say we're describing a card entity, and we've added both FixedCostSchematic(u16) and ZeroCostSchematic, which set the card's Cost to Some(u16) and None respectively.

When the card is spawned, what should the value of its Cost component be? Should it be whichever schematic was applied "last", like bundles? Are we then saying that schematics are applied in a specific order? Or should it be a design-time error to apply both at once?

If it's a design-time error, how could I know why that would happen? And what should I do to fix it? The only way to answer these questions is by describing the underlying ECS implementation that we were trying to abstract away in the first place.

If it's not a design-time error, it's not intuitive which value would emerge for the component. Suppose I added ZeroCostSchematic at the top of the schematic ordering. I may not have any awareness that it's later being overridden by FixedCostComponent and I wouldn't understand why my change wasn't working.

Even this level of awareness assumes I understand that both of them are touching one component. We should expect users to write arbitrary schematics, like CommonCardSchematic { ... } or LegendaryCardSchematic { ... }, and in these cases designers may have no idea which components they're touching, what conflicts could arise, or how those conflicts are resolved.

SamPruden commented 2 years ago

Thanks for the example, @B-Reif!

To address the last point first, it would be a design time error. It's okay to expose the underlying ECS sometimes. We're not trying to pretend that it doesn't exist, we're just trying to abstract it when we can for ease of use. The editor will provide the ability to visualise the generated ECS data anyway, so it's not a problem to mention it in an error message occasionally. Even without reference to the conflicting components, "FixedCostSchematic cannot exist alongside ZeroCostSchematic." is an okay error.

If a designer finds themselves in a conflict like this that they can't trivially resolve themselves, they should talk to an engineer. My contention is that this will be a rare event.

To take your specific example, I would call that a badly designed schematic.

Card would probably be a single schematic. One of its fields would be cost: u16. That would interpret into whatever ECS data, perhaps only adding a Cost component if the cost field on the schematic is nonzero. Alternatively, if you didn't want Card to be a single schematic, CardCost might be the schematic, with the same behaviour.

So there wouldn't be a conflict in this example, because the unit of functionality assigned to the schematic is such that conflicts don't logically exist. The conflict only arises because you've chosen to design two conflicting schematics.

The more interesting scenario is when you have two schematics that should sometimes exist separately, but may logically be composed sometimes. The question is whether these will ever conflict on ECS data in real world scenarios. I think the answer might be no, because if two schematics are trying to set different values for the same component, then you actually have a logical problem. Their composition simply doesn't make sense. No amount of conflict resolution logic can get around the fact that they want the ECS to be in two different conflicting states.

The one logical case that I can see, is two schematics both adding the same component with the same exact value. There's no logical conflict between adding them simultaneously because they agree. I think we can find an automated way to resolve this. For marker components, it's trivial, and we may be able to extend that behaviour to all components that are equal in value.

B-Reif commented 2 years ago

My contention is that users defining arbitrary schematics will make mistakes and write conflicts, especially in large codebases. When writing or refactoring a schematic, we are asking developers to have awareness of all other existing schematics in the project (and their implementation details) to avoid writing a conflict. If every time I write a new schematic or refactor an existing one, I have to work around hundreds of existing schematics, I am either going to make mistakes sometimes or quit my job.

Regardless of how often conflicts may or may not happen, ideally we should offer an escape hatch such that a designer can just add components directly.

SamPruden commented 2 years ago

It's true that we need to tolerate engineers making poor schematic design decisions. That means that, despite being a "stable" format, schematics do need to be able to tolerate some changes throughout development. I've been assuming that schematic serialisation would use Serde, which already provides some tools to help with this.

My feeling is that conflicts will actually be very rare, though. Most components are only used within their own logical feature, so a conflict between two schematics from different features is unlikely. You don't need to be aware of all other schematics, just the ones dealing in the same sub feature.

If we allow designers to set components manually, that causes problems.

Firstly, it breaks the ECS-independent stable representation, because we now have some serialised data stored directly in component format. That kills one of the main reasons to have schematics in the first place. We could enable it as an escape hatch but strongly advise against it because it will cause problems down the line, but I don't think that's a good solution. Quoting @alice-i-cecile above, "Doing bad things should be hard."

Secondly, if SchematicA and SchematicB both want to set the value of ComponentC, and we "fix" that conflict by setting the value manually, I think our situation is now more conflicted, not less. Now we have three things competing to set that value, not two. It's technically resolvable, but it's even more of a mess. Neither of the schematics is necessarily getting what they want, now.

Furthermore, a schematic may logically rely on the value of a component being set as it requires. If we allow that to be overriden, we're breaking the assumptions that the schematics are making, which will end up causing problems.

Any time schematics do conflict, that means that you have two pieces of functionality that want conflicting things in the ECS. That's not just an inconvenience for a designer, it's a logical error that needs a logical solution, which means an engineer needs to look at it and determine the correct behaviour. You don't want designers hacking in values that seem to make things work - that way lies bugs.

B-Reif commented 2 years ago

Firstly, it breaks the ECS-independent stable representation, because we now have some serialised data stored directly in component format.

Schematics seem to consist of two concerns here: serialization and transformation. I don't necessarily think these things need to be coupled. If the component's API is 1:1 with the public API, you would only need serialization. Nothing prevents me from writing a schematic that just passes on the same value.

Secondly, if SchematicA and SchematicB both want to set the value of ComponentC, and we "fix" that conflict by setting the value manually, I think our situation is now more conflicted, not less. Now we have three things competing to set that value, not two. It's technically resolvable, but it's even more of a mess.

My thinking here is that you would essentially convert each schematic into a set of constituent components and then have only a component list, with manual values set on the conflicting component.

SamPruden commented 2 years ago

Schematics seem to consist of two concerns here: serialization and transformation. I don't necessarily think these things need to be coupled. If the component's API is 1:1 with the public API, you would only need serialization. Nothing prevents me from writing a schematic that just passes on the same value.

True, and that would be a common case. However this breaks if an engineer wants to change that component later on. The stable format goal requires that the same schematic data can be interpreted into different component data. That means serialisation and transformation are necessarily coupled. A component's API may be 1:1 with the public/schematic API for now, but we want to support that fact changing in the future.

My thinking here is that you would essentially convert each schematic into a set of constituent components and then have only a component list, with manual values set on the conflicting component.

Schematics should be transactional, meaning they either gets applied wholly or not at all. A value may be set on ComponentA which is only valid if a particular value is also set on ComponentB. Setting them together is a bulk transactional unit. Allowing anything to override ComponentB would cause invalid state. Overriding is unsafe. A conflict is a logic error, and should be presented as such. It is only safe to resolve conflicts when both schematics agree.

SamPruden commented 2 years ago

Loose conflicts are resolvable. For example, a schematic might say .require_or_add_component(MyComponent { val: 1 }) if it cares about a component existing, but not its value.

It would even be possible to accept a range of valid component values, possibly added by another schematic. In this example, the schematic requires the existence of a MyComponent with nonzero value:

.require_or_add_component(MyComponent { val: 1 }).conforming_to(|my_component| my_component.val != 0);

This breaks down into multiple passes. First, all components directly added by all schematics are processed. Then, a second pass resolves any require_or_add calls. Finally, all conformance would be checked. Here, the order in which these are resolved matters, because two schematics that both do require_or_add may provide different defaults. But all orderings would be safe. This whole process is transactional.

If we want complexity, we could even do tricks like preferring to use defaults that have corresponding conforming_to because they're more specific.

Can anyone come up with any examples of possible conflicts which can't be dealt with by this type of design?

therocode commented 2 years ago

...The argument of Schematics being a leaky abstraction...

I think it's misconstrued to set the goal of Schematics to be a non-leaky abstraction. It'd not try to opaquely hide the underlying ECS. It'd be a higher level design tool to work with the ECS, where the goal is to simplify/empower workflows and not hide anything. That's why I'd also be all for the proposed ideas of being able to still show the resulting components in the editor in case it'd be useful to a more technical designer, or when they bring a coder to their desk to work out some issue.

I'd agree Schematics can never be a non-leaky abstraction but by no means does it have to. It's more a thing to introduce separation of concerns and a tool for re-using designer work, reduce complexity on the day-to-day workflow of designers, and provide tools for common things like migrating data files while the programmers move tech forward and such.

This is also a good reason to make Schematics data-driven/interpreted as much as possible becuase that means that they can largely be owned by the technical designers in a team and not necessarily need much time from programmers. The workflow I imagine here is something like:

  1. Programmers code and support the actual ECS components with a focus on technical stuff, implementing features, keeping performance high, etc.
  2. Techincal designers own the process of building schematics with support from programmers. These people know what their designers need and are closer to the gameplay team and will build foundational Schematics that can be directly used in level/entity designs. Here it might prove useful to show resulting ECS components as they create the building blocks.
  3. Content designers will use the schematics with support from technical designers in their day-to-day work. They won't need to talk to programmers too often and can think almost entirely in design-space terms. Their content won't need to be constantly manually updated based on what the programmers are doing.

Note that this [Programmer] <-> [Technical designer] <-> [Designer] structure is already in place in many studios and there's often in-house equivalents to the Schematics system to fill exactly this role. And yes, I've seen those systems deal with the complex issue of attribute collisions etc. It's not a new thing.

A conflict could emerge when any two schematics specify values for the same ECS component.

For instance, suppose we implement a card game where cards have some component Cost(Option<u16>). Two different schematics specify the Cost of a card. Let's say we're describing a card entity, and we've added both FixedCostSchematic(u16) and ZeroCostSchematic, which set the card's Cost to Some(u16) and None respectively.

When the card is spawned, what should the value of its Cost component be? Should it be whichever schematic was applied "last", like bundles? Are we then saying that schematics are applied in a specific order? Or should it be a design-time error to apply both at once?

What is your proposed alternative workflow? The only workflow I can see for designers where you wouldn't possibly have conflicts in resulting components is that they manually add components to every entity. You can't even have a simple "bundle of ECS components" or "inherit other multiple entities" because those common mechanisms for reuse also suffer from the risk of conflicts.

My reasoning is:

Maybe I'm missing some alternative?

therocode commented 2 years ago

I'd also like to add an emphasis that I think that it's absolutely critical that a system like this one is well thought through and powerful enough without being too hard to work with. If Bevy would be used by a sizeable team, there would possibly be people who'd spend the vast majority of their 8h working day working with this system.

I think it's easy to underestimate the impact of poor/frustrating tools just in terms of morale and mental health when it's used by someone on a daily basis.

SamPruden commented 2 years ago

Excellently stated @therocode, that's exactly my position but you communicated it far better than I managed to today.

B-Reif commented 2 years ago

As I wrote above, the alternative I proposed was to allow designers to fall back to configuring individual components whenever they don’t want to use a schematic, for conflicts or for any other reason.

I never suggested dropping the idea of schematics. I obviously don’t want tooling to be poor or frustrating.

I want the ability to sidestep this tooling if, for any reason, I find it to be worse or -more- frustrating than manual component specs.

therocode commented 2 years ago

@B-Reif Yeah that makes sense to me, and I'd be all for that as well. For simpler gamejam/solo-dev stuff it might be more than enough to work with individual components and I'd vote for enabling that too and I feel that is aligned with Bevy in general - never hide the lower-level APIs.

SamPruden commented 2 years ago

My concern with allowing individual components is that it breaks other things that we're relying on, chiefly the safety of the stable representation as well as the serialisation mechanism, but I think I may have a middleground proposal.

What if an attribute could be applied to a component to automatically generate a corresponding 1:1 schematic? You get that instant implementation that you want when rapid prototyping / jamming, but the engine still gets to work with everything being a schematic, and the safety of the conflict detection etc.

If you later wanted to change the component layout without breaking the schematic, you would just upgrade the schematic to a manual implementation compatible with the automatic one.

This doesn't address the conflict situation, but I still believe that there are better ways to address that.

B-Reif commented 2 years ago

What if an attribute could be applied to a component to automatically generate a corresponding 1:1 schematic? You get that instant implementation that you want when rapid prototyping / jamming, but the engine still gets to enforce the safety of the schematic.

I was just typing this same thought up myself 👍 In some cases the representation will be 1:1 anyway and devs could skip writing boilerplate schematics around basically the identity function. This also lets devs specify which components can/should be used this way.