From 6e923667784b6211a626548579491259262007c1 Mon Sep 17 00:00:00 2001 From: MDeiml Date: Mon, 17 Oct 2022 16:15:28 +0200 Subject: [PATCH 01/16] Add schematics RFC --- rfcs/64-schematics.md | 181 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 rfcs/64-schematics.md diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md new file mode 100644 index 00000000..84058fcd --- /dev/null +++ b/rfcs/64-schematics.md @@ -0,0 +1,181 @@ +# Feature Name: `schematics` + +## Summary + +The goal of this RFC is to facilitate and standardize two-way conversion and synchronization between ECS worlds. +The approach described here aims to be more user friendly than just handcoded synchronization systems, but more flexible than a prefab system. +This design is buitl on top of the [many worlds RFC][many_worlds]. + +## Motivation + +When devolping an editor for bevy, the information visible should not necessarily be in the same format that most runtime systems operate on. +The runtime representation is often optimized for efficiency and use in those systems, while the information displayed in the editor should be optimized for +easy understanding and stability agains changes. +We propose to address this by adding another default world to the ones described in the [many worlds RFC][many_worlds] that we call the "schematic world", +and which contains the information used in the editor and its scene file formats. + +The main problem that arises from this approach is the synchronisation between the main and schematic worlds, so the RFC focuses on this aspect. +Similar problems, where data needs to kept synchronized between worlds, might also appear in other areas. +One example for this is the "render world", for which the extract phase could be adapted to use schematics. +Another example could be a "server world" in a multiplayer game, that only contains information which is shared with other clients and the server. +Finally the approach chosen here would help in implementing a "prefab" system. + +## User-facing explanation + +### The `CoreWorld::Schematic` + +### The `SchematicQuery` + +The main interface added by this RFC is the `SchematicQuery`, which you will usually only use in systems that run on `CoreWorld::Schematic`. +We call such systems "schematics". +A `SchematicQuery` works almost the same as a normal `Query`, but when iterating over components will additionaly return `SchematicCommands` for the component queried. +This can be used to insert and modify components on the corresponding entity on `CoreWorld::Main` as well as spawn new entities there. +The entities spawned in this way can not be modified by `SchematicCommands` from other systems, but will be remembered for this system similar to `Local` system parameters. + +A typical schematic will look like this: + +```rust +#[derive(Component)] +struct SchematicA(u32, Entity); + +#[derive(Component)] +struct MainA(u32); + +#[derive(Component)] +struct MainAChild(Entity); + +/// Translates a `SchematicA` to a `MainA` as well as a child entity that has a `MainAChild`. +/// The system can contain any other parameter besides the schematic query +fn schematic_for_a(query: SchematicQuery>, mut some_resource: ResMut) { + for (a, commands) in query { + some_resource.do_something_with(a); + // You can modify components + commands.insert_or_update(MainA(a.0)); + // And spawn other entities. + // This will only spawn an entity on the first execution. It will remember the entity + // by the label "child" so `child_commands` will operate on the same entity the next + // time this system is run. + commands.spawn_or_select_child("child", |child_commands| { + // Entity references within the schematic world need to be mapped in this way. + // (This might not be needed depending on the implementation of many worlds) + let entity = child_commands.map_entity(a.1); + child_commands.insert_or_update(MainAChild(entity)); + }); + } +} +``` + +The `SchematicQuery` will automatically only iterate over components that were changed since the system last ran. +Other than a normal query, the first generic argument can only be a component, so tuples or `Entity` are not allowed as the first argument. + +The main methods of `SchematicCommands` are: +* `insert_or_update(bundle: B)` +* `spawn_or_select_child(label: L, F)` +* `spawn_or_select(label: L, F)` +* `despawn_if_exists(label: L)` +* `remove_if_exists()` + +### The `default_schematic` system + +Since with many components you want to just copy the same component from the schematic to the main world, a `default_schematic` is provided. +The implementation of this is just: + +```rust +fn default_schematic(query: SchematicQuery) { + for (a, commands) in query { + commands.insert_or_update(a.clone()); + } +} +``` + +### Adding schematics to your app + +`Schematic`s can be added to schedules like any other system +```rust +schedule.add_system(schematic_for_a); +``` + +Usually you will want to syncronize between the app's `SchematicWorld` and `CoreWorld::Main`. +In this case you can use + +```rust +app.add_schematic(default_schematic::); +``` + +## Implementation strategy + +* `SchematicQuery` is basically `(Query)>, Local>, AppCommands)` where `SchematicData` is something like + ```rust + struct SchematicData { + corresponding_entities: HashMap + } + ``` +* `SchematicCommands` is something like + ```rust + struct SchematicCommands<'a> { + app_commands: &'a mut AppCommands, + schematic_data: &'a mut SchematicData, + current_label: Option, + } + ``` + +## Drawbacks + +* Adds another data model that programmers need to think about. +* The design may not be very performant and use a lot of extra memory +* Need to add additional code to pretty much any component, even if it is just `app.add_schematic(default_schematic::)` +* It is not possible to check invariants, e.g. synchronization should be idempotent, schmatics shouldn't touch same component, ... + +## Rationale and alternatives + +This design +* Integrates well into ECS architecture +* Is neutral with regard to usage +* Can use existing code to achive parallel execution +* Can live alongside synchronization systems that don't use `SchematicQuery` + +The problem could also be solved by "prefabs", i.e. scenes that might expose certain fields of the components they contain. +But this would be a lot more restrictive the "schematics" described here. +What might make more sense is to implement prefabs atop schematics. + +This design does not allow the schematic world to behave sensibly for use in the editor by itself. +It would need something like [archetype invariants](https://github.com/bevyengine/bevy/issues/1481) additionally. + +## Prior art + +*TODO: Compare with Unity's system of converting game objects to ECS components* + +See https://github.com/bevyengine/bevy/issues/3877 + +Discuss prior art, both the good and the bad, in relation to this proposal. +This can include: + +- Does this feature exist in other libraries and what experiences have their community had? +- Papers: Are there any published papers or great posts that discuss this? + +This section is intended to encourage you as an author to think about the lessons from other tools and provide readers of your RFC with a fuller picture. + +Note that while precedent set by other engines is some motivation, it does not on its own motivate an RFC. + +## Unresolved questions + +- What parts of the design do you expect to resolve through the RFC process before this gets merged? +- What parts of the design do you expect to resolve through the implementation of this feature before the feature PR is merged? +- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? + +## \[Optional\] Future possibilities + +Think about what the natural extension and evolution of your proposal would +be and how it would affect Bevy as a whole in a holistic way. +Try to use this section as a tool to more fully consider other possible +interactions with the engine in your proposal. + +This is also a good place to "dump ideas", if they are out of scope for the +RFC you are writing but otherwise related. + +Note that having something written down in the future-possibilities section +is not a reason to accept the current or a future RFC; such notes should be +in the section on motivation or rationale in this or subsequent RFCs. +If a feature or change has no direct value on its own, expand your RFC to include the first valuable feature that would build on it. + +[many_worlds]: https://github.com/bevyengine/rfcs/pull/43 From 94be9b415c87f8e172761e1145423570c573ca85 Mon Sep 17 00:00:00 2001 From: MDeiml Date: Thu, 20 Oct 2022 17:41:40 +0200 Subject: [PATCH 02/16] Implement feedback and add unresolved questions --- rfcs/64-schematics.md | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index 84058fcd..715ef40c 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -10,9 +10,11 @@ This design is buitl on top of the [many worlds RFC][many_worlds]. When devolping an editor for bevy, the information visible should not necessarily be in the same format that most runtime systems operate on. The runtime representation is often optimized for efficiency and use in those systems, while the information displayed in the editor should be optimized for -easy understanding and stability agains changes. -We propose to address this by adding another default world to the ones described in the [many worlds RFC][many_worlds] that we call the "schematic world", -and which contains the information used in the editor and its scene file formats. +easy understanding. +We propose to address this by adding another default world to the ones described in the [many worlds RFC][many_worlds] that we call the "schematic world". +A big emphasis on the design is also for the schematic representation to be able to be stable against even major changes to the runime representation. +This not only enables us to use the schematic world for a stable scene format inside one project, but it also helps with separating internal implentation changes in both official +and unofficial plugins from what users, especially non-technical users. The main problem that arises from this approach is the synchronisation between the main and schematic worlds, so the RFC focuses on this aspect. Similar problems, where data needs to kept synchronized between worlds, might also appear in other areas. @@ -50,16 +52,16 @@ fn schematic_for_a(query: SchematicQuery>, mut some_resource: Re for (a, commands) in query { some_resource.do_something_with(a); // You can modify components - commands.insert_or_update(MainA(a.0)); + commands.require(MainA(a.0)); // And spawn other entities. // This will only spawn an entity on the first execution. It will remember the entity // by the label "child" so `child_commands` will operate on the same entity the next // time this system is run. - commands.spawn_or_select_child("child", |child_commands| { + commands.require_child("child", |child_commands| { // Entity references within the schematic world need to be mapped in this way. // (This might not be needed depending on the implementation of many worlds) let entity = child_commands.map_entity(a.1); - child_commands.insert_or_update(MainAChild(entity)); + child_commands.require_component(MainAChild(entity)); }); } } @@ -67,13 +69,15 @@ fn schematic_for_a(query: SchematicQuery>, mut some_resource: Re The `SchematicQuery` will automatically only iterate over components that were changed since the system last ran. Other than a normal query, the first generic argument can only be a component, so tuples or `Entity` are not allowed as the first argument. +It provides read-only access to that component. +Systems that mutate data in the schematic world should usually be separate from schematics. The main methods of `SchematicCommands` are: -* `insert_or_update(bundle: B)` -* `spawn_or_select_child(label: L, F)` -* `spawn_or_select(label: L, F)` -* `despawn_if_exists(label: L)` -* `remove_if_exists()` +* `require(bundle: B)` +* `require_child(label: L, F)` +* `require_entity(label: L, F)` +* `require_despawned(label: L)` +* `require_deleted()` ### The `default_schematic` system @@ -159,9 +163,11 @@ Note that while precedent set by other engines is some motivation, it does not o ## Unresolved questions -- What parts of the design do you expect to resolve through the RFC process before this gets merged? -- What parts of the design do you expect to resolve through the implementation of this feature before the feature PR is merged? -- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? +* Conversion from the runtime to schematic world +* Conflict resolution in the schematic world vs during conversion +* "Function style" vs "system" schematics +* Handle removal of components in the schematic world +* Checking that every component is read during conversion and similar things ## \[Optional\] Future possibilities From db90adbc8fcf38b36141b65240e5378c19e802db Mon Sep 17 00:00:00 2001 From: MDeiml Date: Thu, 20 Oct 2022 17:45:56 +0200 Subject: [PATCH 03/16] Add `CoreWorld::Schematic` section --- rfcs/64-schematics.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index 715ef40c..faae1cc1 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -26,6 +26,11 @@ Finally the approach chosen here would help in implementing a "prefab" system. ### The `CoreWorld::Schematic` +We propose adding another world to the `CoreWorld` enum, namely `CoreWorld::Schematic`. +This world is meant to be an representation of the runtime world to be used in the editor and for scene formats. +As such it should group components from the runtime world, if they can only show up together, and avoid duplicate data such as `Transform` and `GlobalTransform`. +The purpose of this RFC is to facilitate the conversion between the runtime world and the schematic world. + ### The `SchematicQuery` The main interface added by this RFC is the `SchematicQuery`, which you will usually only use in systems that run on `CoreWorld::Schematic`. From 8a7d026ec2adc6eeb9a9a48d89e8795f9c2ca3eb Mon Sep 17 00:00:00 2001 From: MDeiml Date: Fri, 21 Oct 2022 09:43:13 +0200 Subject: [PATCH 04/16] Add draft for inference section --- rfcs/64-schematics.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index faae1cc1..efaa9dd7 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -111,6 +111,46 @@ In this case you can use app.add_schematic(default_schematic::); ``` +### Schematic inference + +You can also optionally write conversions from the runtime world to the schematic world. + +```rust +fn inference_for_a( + query: InferenceQuery<(&MainA, &Children), SchematicA>, + child_query: InferenceQuery<(Entity, &MainAChild)>, +) { + for ((a, children), inference_commands) in query { + let result = children + .iter_many(children) + .filter_map(|result| result.ok()) + .next(); + let (child, a_child) = match result { + Some(value) => value, + None => continue, + }; + let entity = inference_commands.map_entity(a_child.0); + inference_commands.infer(SchematicA(a.0, entity)); + // This is important so that the schematic world knows about this relation + inference_commands.set_entity("child", child); + } +} +``` + +For more efficient tracking you should also add update systems + +```rust +fn update_for_a( + schematic_a: &mut SchematicA, + main_a: &MainA, + main_a_child: SchematicUpdate<&MainAChild, "child">, +) { + schematic_a.0 = main_a.0; + // TODO: Introduce entity mapping here + schematic_a.1 = main_a_child.1; +} +``` + ## Implementation strategy * `SchematicQuery` is basically `(Query)>, Local>, AppCommands)` where `SchematicData` is something like From a724e6aed58c3aec44d775216b3ecec49efd1f5f Mon Sep 17 00:00:00 2001 From: MDeiml Date: Fri, 21 Oct 2022 09:53:20 +0200 Subject: [PATCH 05/16] Add more explanation to inference section --- rfcs/64-schematics.md | 44 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index efaa9dd7..0a59bc53 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -144,13 +144,55 @@ fn update_for_a( schematic_a: &mut SchematicA, main_a: &MainA, main_a_child: SchematicUpdate<&MainAChild, "child">, -) { +) -> bool { schematic_a.0 = main_a.0; // TODO: Introduce entity mapping here schematic_a.1 = main_a_child.1; + // return true, as this schematic still exists + true +} +``` + +Example for "enum schematic" + +```rust +#[derive(Component)] +enum AnimationState { + Idle, + Walking, + Jumping, +} + +#[derive(Component)] +struct Idle; + +#[derive(Component)] +struct Walking; + +#[derive(Component)] +struct Jumping; + +fn update_animation_state( + animation_state: &mut AnimationState, + idle: &Option, + walking: &Option, + jumping: &Option +) -> bool { + match (idle, walking, jumping) { + (Some(_), _, _) => *animation_state = AnimationState::Idle, + (_, Some(_), _) => *animation_state = AnimationState::Walking, + (_, _, Some(_)) => *animation_state = AnimationState::Jumping, + _ => return false, + } + true } ``` +* If for a schematic there is only an update system but no inference, then no new instances of this schematic can be found during runtime. +* If for a schematic there is only an inference system but no update, then the schematic gets replaced by a new one every time the schmatic world is updated. +* If for a schematic there is both, then inference works as expected (schematics are updated if they still exist, new ones are created). +* If for a schematic there is neither, then no inference is done (schematics are not updated, no new ones are created). + ## Implementation strategy * `SchematicQuery` is basically `(Query)>, Local>, AppCommands)` where `SchematicData` is something like From 91a6b3719b33e3f15ed95e5c9f2fcba6395c9f99 Mon Sep 17 00:00:00 2001 From: MDeiml Date: Fri, 21 Oct 2022 12:37:57 +0200 Subject: [PATCH 06/16] Suggest api structure --- rfcs/64-schematics.md | 114 ++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 33 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index 0a59bc53..5263c34e 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -22,6 +22,10 @@ One example for this is the "render world", for which the extract phase could be Another example could be a "server world" in a multiplayer game, that only contains information which is shared with other clients and the server. Finally the approach chosen here would help in implementing a "prefab" system. +We optionally suggest the "collect-resolve-apply" approach to solve the problem of keeping and resolving invariants between components like "every `ParticleSystem` needs a `Transform`". +This could also be solved using archetype invariants in the schematic world. +Some work on archetype invariants is already in progress (see https://github.com/bevyengine/bevy/pull/5121). + ## User-facing explanation ### The `CoreWorld::Schematic` @@ -29,17 +33,53 @@ Finally the approach chosen here would help in implementing a "prefab" system. We propose adding another world to the `CoreWorld` enum, namely `CoreWorld::Schematic`. This world is meant to be an representation of the runtime world to be used in the editor and for scene formats. As such it should group components from the runtime world, if they can only show up together, and avoid duplicate data such as `Transform` and `GlobalTransform`. -The purpose of this RFC is to facilitate the conversion between the runtime world and the schematic world. +The purpose of this RFC is to facilitate the synchronization between the runtime world and the schematic world. + +For this you need to define 3 algorithms for every schematic: +* Conversion from the schematic world to the runtime world +* Conversion from the runtime world to the schematic world +* Update the schematic world with changed data from the runtime world + +Usually you will not need to write these algorithms yourself - you can use `#[derive(Schematic)]` in most cases. + +### Deriving `Schematic` + +The derived implementation contains conversions both direction as well as updates. + +```rust +#[derive(Component, Schematic)] +struct MeshRenderer { + mesh: Handle, + material: MaterialPath, + #[schematic_ignore] + thumbnail: Handle, +} -### The `SchematicQuery` +#[derive(Component)] +struct Walking { + speed: f32, +} -The main interface added by this RFC is the `SchematicQuery`, which you will usually only use in systems that run on `CoreWorld::Schematic`. -We call such systems "schematics". +#[derive(Component)] +struct Jumping; + +#[derive(Component, Schematic)] +enum AnimationState { + #[schematic_marker(Walking)] + Walking(Walking) + #[schematic_marker(Jumping)] + Jumping, +} +``` + +### Conversion from schematic to runtime world + +Conversions from schematic to runtime world are just normal systems that run on the schematic world and have a `SchematicQuery` parameter. A `SchematicQuery` works almost the same as a normal `Query`, but when iterating over components will additionaly return `SchematicCommands` for the component queried. This can be used to insert and modify components on the corresponding entity on `CoreWorld::Main` as well as spawn new entities there. The entities spawned in this way can not be modified by `SchematicCommands` from other systems, but will be remembered for this system similar to `Local` system parameters. -A typical schematic will look like this: +A typical conversion will look like this: ```rust #[derive(Component)] @@ -84,34 +124,7 @@ The main methods of `SchematicCommands` are: * `require_despawned(label: L)` * `require_deleted()` -### The `default_schematic` system - -Since with many components you want to just copy the same component from the schematic to the main world, a `default_schematic` is provided. -The implementation of this is just: - -```rust -fn default_schematic(query: SchematicQuery) { - for (a, commands) in query { - commands.insert_or_update(a.clone()); - } -} -``` - -### Adding schematics to your app - -`Schematic`s can be added to schedules like any other system -```rust -schedule.add_system(schematic_for_a); -``` - -Usually you will want to syncronize between the app's `SchematicWorld` and `CoreWorld::Main`. -In this case you can use - -```rust -app.add_schematic(default_schematic::); -``` - -### Schematic inference +### Conversion from runtime to schematic world You can also optionally write conversions from the runtime world to the schematic world. @@ -137,6 +150,8 @@ fn inference_for_a( } ``` +### Update schematic components + For more efficient tracking you should also add update systems ```rust @@ -193,6 +208,39 @@ fn update_animation_state( * If for a schematic there is both, then inference works as expected (schematics are updated if they still exist, new ones are created). * If for a schematic there is neither, then no inference is done (schematics are not updated, no new ones are created). +### Putting the algorithms together + +```rust +trait Schematic { + type InterpretParams; + type InferParams; + + fn interpret() -> Option>; + fn infer() -> Option>>; + fn updates() -> Vec>; +} + +trait SchematicUpdate { + type Component: Component; + type Params: SchematicUpdateParams; + + fn update(component: &mut Component, params: Params) -> bool; +} +``` + +### Adding schematics to your app + +`Schematic`s can be added to the app using +```rust +app.add_schematic::(); +``` + +Since with many components you want to just copy the same component from the schematic to the main world, a `DefaultSchematic` is provided. + +### \[Optional\] Collect-Resolve-Apply + +@SamPruden you can add your stuff here + ## Implementation strategy * `SchematicQuery` is basically `(Query)>, Local>, AppCommands)` where `SchematicData` is something like From 845a3d00805e1921252a84aef7314a7750e6bc51 Mon Sep 17 00:00:00 2001 From: MDeiml Date: Fri, 21 Oct 2022 13:30:07 +0200 Subject: [PATCH 07/16] Go more into detail --- rfcs/64-schematics.md | 78 +++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index 5263c34e..74402a62 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -4,15 +4,17 @@ The goal of this RFC is to facilitate and standardize two-way conversion and synchronization between ECS worlds. The approach described here aims to be more user friendly than just handcoded synchronization systems, but more flexible than a prefab system. -This design is buitl on top of the [many worlds RFC][many_worlds]. +This design is built on top of the [many worlds RFC][many_worlds], but the implementation can be easily adapted to work with sub apps instead. ## Motivation -When devolping an editor for bevy, the information visible should not necessarily be in the same format that most runtime systems operate on. -The runtime representation is often optimized for efficiency and use in those systems, while the information displayed in the editor should be optimized for -easy understanding. +When devolping an editor for bevy, the information visible should not necessarily be in the same format that the runtime operates on. +The runtime representation is often optimized for efficiency and use in logic systems, while the information displayed in the editor should be optimized for +easy understanding and stability. We propose to address this by adding another default world to the ones described in the [many worlds RFC][many_worlds] that we call the "schematic world". -A big emphasis on the design is also for the schematic representation to be able to be stable against even major changes to the runime representation. +The entities and components in this world are the ones shown to users in the editor. + +A big emphasis on the design is also for the schematic representation to be able to be stable against major changes to the runime representation. This not only enables us to use the schematic world for a stable scene format inside one project, but it also helps with separating internal implentation changes in both official and unofficial plugins from what users, especially non-technical users. @@ -25,54 +27,86 @@ Finally the approach chosen here would help in implementing a "prefab" system. We optionally suggest the "collect-resolve-apply" approach to solve the problem of keeping and resolving invariants between components like "every `ParticleSystem` needs a `Transform`". This could also be solved using archetype invariants in the schematic world. Some work on archetype invariants is already in progress (see https://github.com/bevyengine/bevy/pull/5121). +The design does not depend on either of these approaches though. ## User-facing explanation ### The `CoreWorld::Schematic` We propose adding another world to the `CoreWorld` enum, namely `CoreWorld::Schematic`. -This world is meant to be an representation of the runtime world to be used in the editor and for scene formats. +This world is meant to be an representation of the runtime world which is to be used in the editor and for scene formats. As such it should group components from the runtime world, if they can only show up together, and avoid duplicate data such as `Transform` and `GlobalTransform`. The purpose of this RFC is to facilitate the synchronization between the runtime world and the schematic world. -For this you need to define 3 algorithms for every schematic: +For this up to 3 algorithms can be defined for every schematic component: * Conversion from the schematic world to the runtime world -* Conversion from the runtime world to the schematic world -* Update the schematic world with changed data from the runtime world +* Conversion from the runtime world to the schematic world (optional) +* Update the schematic world with changed data from the runtime world (optional) Usually you will not need to write these algorithms yourself - you can use `#[derive(Schematic)]` in most cases. ### Deriving `Schematic` -The derived implementation contains conversions both direction as well as updates. +You can add `#[derive(Schematic)]` to any struct or enum that is a component. + +If you derive `Schematic` for a struct, every field needs to implement `Component` and `Clone`. +Otherwise it either needs to implement `Default` and be tagged it with `#[schematic_ignore]`, or you need to specify another `Component`, such that the field has `From` and `Into` instances for this component. +In the latter case you need to tag the field with `#[schematic_into(OtherComponent)]` ```rust #[derive(Component, Schematic)] struct MeshRenderer { mesh: Handle, - material: MaterialPath, + material: Handle, + #[schematic_into(Name)] + name: String, #[schematic_ignore] thumbnail: Handle, } +``` + +If you derive `Schematic` for an enum +* Every unit variant nees to be tagged with `#[schematic_marker(MarkerComponent)]`. The given component needs to implement `Default`. +* Every tuple variant can only have one field which needs to implement `Component` and `Clone` or be tagged with `#[schematic_into(OtherComponent)]`. +* For struct variants the same rules as for structs apply. Struct variants need to have at least one field that is not marked with `#[schematic_ignore]`. +Alternatively every variant can also be tagged with `#[schematic_ignore]` +```rust #[derive(Component)] struct Walking { speed: f32, } -#[derive(Component)] +impl From for Walking { + // ... +} + +#[derive(Component, Default)] struct Jumping; +#[derive(Component, Clone)] +struct Attacking { + damage: f32, + weapon_type: WeaponType, +} + #[derive(Component, Schematic)] enum AnimationState { - #[schematic_marker(Walking)] - Walking(Walking) + Walking { + #[schematic_into(Walking)] + speed: f32 + } #[schematic_marker(Jumping)] Jumping, + Attacking(Attacking), } ``` -### Conversion from schematic to runtime world +The derived implementation contains conversions both direction as well as updates. + +### Writing `Schematic` from hand + +#### Conversion from schematic to runtime world Conversions from schematic to runtime world are just normal systems that run on the schematic world and have a `SchematicQuery` parameter. A `SchematicQuery` works almost the same as a normal `Query`, but when iterating over components will additionaly return `SchematicCommands` for the component queried. @@ -124,7 +158,7 @@ The main methods of `SchematicCommands` are: * `require_despawned(label: L)` * `require_deleted()` -### Conversion from runtime to schematic world +#### Conversion from runtime to schematic world You can also optionally write conversions from the runtime world to the schematic world. @@ -150,7 +184,7 @@ fn inference_for_a( } ``` -### Update schematic components +#### Update schematic components For more efficient tracking you should also add update systems @@ -208,7 +242,7 @@ fn update_animation_state( * If for a schematic there is both, then inference works as expected (schematics are updated if they still exist, new ones are created). * If for a schematic there is neither, then no inference is done (schematics are not updated, no new ones are created). -### Putting the algorithms together +#### Putting the algorithms together ```rust trait Schematic { @@ -228,6 +262,10 @@ trait SchematicUpdate { } ``` +Notice that an implementation of `Schematic` is not tied to any component. +The trait can also be implemented for any unit struct unrelated to any components similar to `Plugin`. +As such you can write your own implementations for existing schematic components. + ### Adding schematics to your app `Schematic`s can be added to the app using @@ -237,6 +275,10 @@ app.add_schematic::(); Since with many components you want to just copy the same component from the schematic to the main world, a `DefaultSchematic` is provided. +```rust +app.add_schematic::>(); +``` + ### \[Optional\] Collect-Resolve-Apply @SamPruden you can add your stuff here From eaea1d157f479c7e09e2d6dac252ed0ffe9032a9 Mon Sep 17 00:00:00 2001 From: MDeiml Date: Fri, 21 Oct 2022 16:00:35 +0200 Subject: [PATCH 08/16] Add schematic combinators --- rfcs/64-schematics.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index 74402a62..a3d9672d 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -104,7 +104,17 @@ enum AnimationState { The derived implementation contains conversions both direction as well as updates. -### Writing `Schematic` from hand +### Schematic combinators + +Every schematic that implements `SimpleSchematic` has the following functions which can be used to create new schematics: +* `map(into: F, from: G) -> impl SimpleSchematic where F: Fn(&Self::Component) -> C, G: Fn(C) -> Self::Component` +* `add() -> impl SimpleSchematic` +* `alternative() -> impl SimpleSchematic>` +* `filter bool>() -> impl SimpleSchematic` + +(`SimpleSchematic` is a schematic that only handles one schematic component. All derived schematics are also simple schematics) + +### Writing `Schematic` manually #### Conversion from schematic to runtime world From 5d03a2680556069afb958876b17807432f166f42 Mon Sep 17 00:00:00 2001 From: MDeiml Date: Sat, 22 Oct 2022 22:27:41 +0200 Subject: [PATCH 09/16] Update everything and restrict schematic on type --- rfcs/64-schematics.md | 168 ++++++++++++++++++++---------------------- 1 file changed, 78 insertions(+), 90 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index a3d9672d..cd4d0a4b 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -8,15 +8,15 @@ This design is built on top of the [many worlds RFC][many_worlds], but the imple ## Motivation -When devolping an editor for bevy, the information visible should not necessarily be in the same format that the runtime operates on. +When eventually devolping an editor for bevy, the information visible should not necessarily be in the same format that the runtime operates on. The runtime representation is often optimized for efficiency and use in logic systems, while the information displayed in the editor should be optimized for easy understanding and stability. We propose to address this by adding another default world to the ones described in the [many worlds RFC][many_worlds] that we call the "schematic world". The entities and components in this world are the ones shown to users in the editor. -A big emphasis on the design is also for the schematic representation to be able to be stable against major changes to the runime representation. +A big emphasis on the design is also for the schematic representation to be able to be stable against major changes to the runtime representation. This not only enables us to use the schematic world for a stable scene format inside one project, but it also helps with separating internal implentation changes in both official -and unofficial plugins from what users, especially non-technical users. +and unofficial plugins from what users, especially non-technical users, see in the editor. The main problem that arises from this approach is the synchronisation between the main and schematic worlds, so the RFC focuses on this aspect. Similar problems, where data needs to kept synchronized between worlds, might also appear in other areas. @@ -24,37 +24,44 @@ One example for this is the "render world", for which the extract phase could be Another example could be a "server world" in a multiplayer game, that only contains information which is shared with other clients and the server. Finally the approach chosen here would help in implementing a "prefab" system. -We optionally suggest the "collect-resolve-apply" approach to solve the problem of keeping and resolving invariants between components like "every `ParticleSystem` needs a `Transform`". -This could also be solved using archetype invariants in the schematic world. -Some work on archetype invariants is already in progress (see https://github.com/bevyengine/bevy/pull/5121). -The design does not depend on either of these approaches though. - ## User-facing explanation ### The `CoreWorld::Schematic` We propose adding another world to the `CoreWorld` enum, namely `CoreWorld::Schematic`. -This world is meant to be an representation of the runtime world which is to be used in the editor and for scene formats. +This world is meant to be an representation of the runtime world which is to be used in the editor and for serialization formats. As such it should group components from the runtime world, if they can only show up together, and avoid duplicate data such as `Transform` and `GlobalTransform`. The purpose of this RFC is to facilitate the synchronization between the runtime world and the schematic world. -For this up to 3 algorithms can be defined for every schematic component: -* Conversion from the schematic world to the runtime world -* Conversion from the runtime world to the schematic world (optional) -* Update the schematic world with changed data from the runtime world (optional) +### Schematics + +To synchronize between the main and schematic worlds, for every component in the schematic world you need to add a `Schematic` to your app. +A `Schematic` is just a collection of systems used for this synchronization process. -Usually you will not need to write these algorithms yourself - you can use `#[derive(Schematic)]` in most cases. +* A system to convert components in the schematic world to components in the runtime world +* A system to convert components in the runtime world to components in the schematic world +* A system to update a schematic component given the data of its related components in the runtime world -### Deriving `Schematic` +Only the first of those 3 systems is mandatory. +The other 2 can be left out, but conversion from the runtime world might not be possible or not efficient respectively. -You can add `#[derive(Schematic)]` to any struct or enum that is a component. +* If for a schematic there is only an update system but no inference, then no new instances of this schematic can be found during runtime. +* If for a schematic there is only an inference system but no update, then the schematic gets replaced by a new one every time the schmatic world is updated. +* If for a schematic there is both, then inference works as expected (schematics are updated if they still exist, new ones are created). +* If for a schematic there is neither, then no inference is done (schematics are not updated, no new ones are created). -If you derive `Schematic` for a struct, every field needs to implement `Component` and `Clone`. +Usually though, you will not need to write these algorithms yourself - you can use `#[derive(DefaultSchematic)]` in most cases. + +### Deriving `DefaultSchematic` + +You can add `#[derive(DefaultSchematic)]` to any struct or enum that is a component. + +If you derive `DefaultSchematic` for a struct, every untagged field needs to implement `Component` and `Clone`. Otherwise it either needs to implement `Default` and be tagged it with `#[schematic_ignore]`, or you need to specify another `Component`, such that the field has `From` and `Into` instances for this component. In the latter case you need to tag the field with `#[schematic_into(OtherComponent)]` ```rust -#[derive(Component, Schematic)] +#[derive(Component, DefaultSchematic)] struct MeshRenderer { mesh: Handle, material: Handle, @@ -65,7 +72,7 @@ struct MeshRenderer { } ``` -If you derive `Schematic` for an enum +If you derive `DefaultSchematic` for an enum * Every unit variant nees to be tagged with `#[schematic_marker(MarkerComponent)]`. The given component needs to implement `Default`. * Every tuple variant can only have one field which needs to implement `Component` and `Clone` or be tagged with `#[schematic_into(OtherComponent)]`. * For struct variants the same rules as for structs apply. Struct variants need to have at least one field that is not marked with `#[schematic_ignore]`. @@ -90,7 +97,7 @@ struct Attacking { weapon_type: WeaponType, } -#[derive(Component, Schematic)] +#[derive(Component, DefaultSchematic)] enum AnimationState { Walking { #[schematic_into(Walking)] @@ -102,21 +109,44 @@ enum AnimationState { } ``` +The `DefaultSchematic` trait has only one function +```rust +fn default_schematic() -> Schematic +``` The derived implementation contains conversions both direction as well as updates. ### Schematic combinators -Every schematic that implements `SimpleSchematic` has the following functions which can be used to create new schematics: -* `map(into: F, from: G) -> impl SimpleSchematic where F: Fn(&Self::Component) -> C, G: Fn(C) -> Self::Component` -* `add() -> impl SimpleSchematic` -* `alternative() -> impl SimpleSchematic>` -* `filter bool>() -> impl SimpleSchematic` +`Schematic` has the following functions which can be used to create new schematics: +* `map(self, into: F, from: G) -> Schematic where F: Fn(&A) -> C, G: Fn(&C) -> A` +* `zip(self, other: Schematic) -> Schematic<(A, C)>` +* `alternative(self, other: Schematic) -> Schematic>` +* `filter bool>(self, f: F) -> Schematic` -(`SimpleSchematic` is a schematic that only handles one schematic component. All derived schematics are also simple schematics) +### Creating `Schematic` manually -### Writing `Schematic` manually +`Schematic` can be constructed manually using the `new` function +```rust +fn new(system: S) -> Schematic +where + S: IntoSchematicConversion +``` + +You can add conversion in the other direction by using the `add_inference` function +```rust +fn add_inference(self, system: S) -> Schematic +where + S: IntoSchematicInference +``` -#### Conversion from schematic to runtime world +Finally you can add updates by using the `add_update` function +```rust +fn add_udpate(self, system: S) -> Schematic +where + S: IntoSchematicUpdate +``` + +#### Writing schematic conversion systems Conversions from schematic to runtime world are just normal systems that run on the schematic world and have a `SchematicQuery` parameter. A `SchematicQuery` works almost the same as a normal `Query`, but when iterating over components will additionaly return `SchematicCommands` for the component queried. @@ -168,9 +198,7 @@ The main methods of `SchematicCommands` are: * `require_despawned(label: L)` * `require_deleted()` -#### Conversion from runtime to schematic world - -You can also optionally write conversions from the runtime world to the schematic world. +#### Writing inference systems ```rust fn inference_for_a( @@ -194,14 +222,12 @@ fn inference_for_a( } ``` -#### Update schematic components - -For more efficient tracking you should also add update systems +#### Writing update systems ```rust fn update_for_a( schematic_a: &mut SchematicA, - main_a: &MainA, + main_a: SchematicUpdate<&MainA>, main_a_child: SchematicUpdate<&MainAChild, "child">, ) -> bool { schematic_a.0 = main_a.0; @@ -247,52 +273,19 @@ fn update_animation_state( } ``` -* If for a schematic there is only an update system but no inference, then no new instances of this schematic can be found during runtime. -* If for a schematic there is only an inference system but no update, then the schematic gets replaced by a new one every time the schmatic world is updated. -* If for a schematic there is both, then inference works as expected (schematics are updated if they still exist, new ones are created). -* If for a schematic there is neither, then no inference is done (schematics are not updated, no new ones are created). - -#### Putting the algorithms together - -```rust -trait Schematic { - type InterpretParams; - type InferParams; - - fn interpret() -> Option>; - fn infer() -> Option>>; - fn updates() -> Vec>; -} - -trait SchematicUpdate { - type Component: Component; - type Params: SchematicUpdateParams; - - fn update(component: &mut Component, params: Params) -> bool; -} -``` - -Notice that an implementation of `Schematic` is not tied to any component. -The trait can also be implemented for any unit struct unrelated to any components similar to `Plugin`. -As such you can write your own implementations for existing schematic components. - ### Adding schematics to your app `Schematic`s can be added to the app using ```rust -app.add_schematic::(); +app.add_schematic(AnimationState::default_schematic()); ``` -Since with many components you want to just copy the same component from the schematic to the main world, a `DefaultSchematic` is provided. +Since with many components you want to just copy the same component from the schematic to the main world, a `CloneSchematic` is provided. ```rust -app.add_schematic::>(); +app.add_schematic(CloneSchematic::::default()); ``` -### \[Optional\] Collect-Resolve-Apply - -@SamPruden you can add your stuff here - ## Implementation strategy * `SchematicQuery` is basically `(Query)>, Local>, AppCommands)` where `SchematicData` is something like @@ -309,12 +302,21 @@ app.add_schematic::>(); current_label: Option, } ``` +* `Schematic` is + ```rust + struct Schematic { + conversion: Box>, + inference: Option>>, + update: Option>>, + } + ``` +* `SchematicConversion`, `SchematicInference` and `SchematicUpdate` are basically just `System` with a restriction on the parameters ## Drawbacks * Adds another data model that programmers need to think about. * The design may not be very performant and use a lot of extra memory -* Need to add additional code to pretty much any component, even if it is just `app.add_schematic(default_schematic::)` +* Need to add additional code to pretty much any component, even if it is just `app.add_schematic(CopySchematic::::default())` * It is not possible to check invariants, e.g. synchronization should be idempotent, schmatics shouldn't touch same component, ... ## Rationale and alternatives @@ -323,7 +325,7 @@ This design * Integrates well into ECS architecture * Is neutral with regard to usage * Can use existing code to achive parallel execution -* Can live alongside synchronization systems that don't use `SchematicQuery` +* Can live alongside synchronization systems that are not built using `Schematic` The problem could also be solved by "prefabs", i.e. scenes that might expose certain fields of the components they contain. But this would be a lot more restrictive the "schematics" described here. @@ -350,25 +352,11 @@ Note that while precedent set by other engines is some motivation, it does not o ## Unresolved questions -* Conversion from the runtime to schematic world -* Conflict resolution in the schematic world vs during conversion -* "Function style" vs "system" schematics -* Handle removal of components in the schematic world * Checking that every component is read during conversion and similar things -## \[Optional\] Future possibilities - -Think about what the natural extension and evolution of your proposal would -be and how it would affect Bevy as a whole in a holistic way. -Try to use this section as a tool to more fully consider other possible -interactions with the engine in your proposal. - -This is also a good place to "dump ideas", if they are out of scope for the -RFC you are writing but otherwise related. +## Future possibilities -Note that having something written down in the future-possibilities section -is not a reason to accept the current or a future RFC; such notes should be -in the section on motivation or rationale in this or subsequent RFCs. -If a feature or change has no direct value on its own, expand your RFC to include the first valuable feature that would build on it. +*TODO: Write how this would fit into an editor UI* +*TODO: Write how it's ok that not every schematic can be converted back* [many_worlds]: https://github.com/bevyengine/rfcs/pull/43 From 97ffb8b1163f276a4a8b07e8e9d9782fb0bc700f Mon Sep 17 00:00:00 2001 From: MDeiml Date: Sat, 22 Oct 2022 22:35:57 +0200 Subject: [PATCH 10/16] Add UntypedSchemtic --- rfcs/64-schematics.md | 62 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index cd4d0a4b..b208394f 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -123,6 +123,19 @@ The derived implementation contains conversions both direction as well as update * `alternative(self, other: Schematic) -> Schematic>` * `filter bool>(self, f: F) -> Schematic` +### Adding schematics to your app + +`Schematic`s can be added to the app using +```rust +app.add_schematic(AnimationState::default_schematic()); +``` + +Since with many components you want to just copy the same component from the schematic to the main world, a `CloneSchematic` is provided. + +```rust +app.add_schematic(CloneSchematic::::default()); +``` + ### Creating `Schematic` manually `Schematic` can be constructed manually using the `new` function @@ -132,16 +145,16 @@ where S: IntoSchematicConversion ``` -You can add conversion in the other direction by using the `add_inference` function +You can add conversion in the other direction by using the `set_inference` function ```rust -fn add_inference(self, system: S) -> Schematic +fn set_inference(self, system: S) -> Schematic where S: IntoSchematicInference ``` -Finally you can add updates by using the `add_update` function +Finally you can add updates by using the `set_update` function ```rust -fn add_udpate(self, system: S) -> Schematic +fn set_udpate(self, system: S) -> Schematic where S: IntoSchematicUpdate ``` @@ -273,17 +286,33 @@ fn update_animation_state( } ``` -### Adding schematics to your app +### `UntypedSchematic` -`Schematic`s can be added to the app using +A `UntypedSchematic` is the same as a `Schematic` just without the type restriction. +Usually it is safer to use the `Schematic` interface, but you might want to handle multiple schematic components within the same system. +Not having an associated type means, that the `map`, `zip`, `alternative` and `filter` methods are not available for `UntypedSchematic`. + +`UntypedSchematic` can be constructed manually using the `new` function ```rust -app.add_schematic(AnimationState::default_schematic()); +fn new(system: S) -> UntypedSchematic +where + S: IntoSchematicConversion ``` -Since with many components you want to just copy the same component from the schematic to the main world, a `CloneSchematic` is provided. +You can add conversion in the other direction by using the `set_inference` function. +This will not replace any systems added previously by this method. +```rust +fn add_inference(self, system: S) -> UntypedSchematic +where + S: IntoSchematicInference +``` +Finally you can add updates by using the `add_update` function. +This will not replace any systems added previously by this method. ```rust -app.add_schematic(CloneSchematic::::default()); +fn add_udpate(self, system: S) -> UntypedSchematic +where + S: IntoSchematicUpdate ``` ## Implementation strategy @@ -305,9 +334,18 @@ app.add_schematic(CloneSchematic::::default()); * `Schematic` is ```rust struct Schematic { - conversion: Box>, - inference: Option>>, - update: Option>>, + marker: PhantomMarker, + conversion: Box>, + inference: Option>>, + update: Option>>, + } + ``` +* `UntypedSchematic` is + ```rust + struct UntypedSchematic { + conversion: Vec>, + inference: Vec>, + update: Vec>, } ``` * `SchematicConversion`, `SchematicInference` and `SchematicUpdate` are basically just `System` with a restriction on the parameters From 54a9a94532a1bd3f61a12fdbbb20419e2a5fa747 Mon Sep 17 00:00:00 2001 From: MDeiml Date: Sun, 23 Oct 2022 12:08:06 +0200 Subject: [PATCH 11/16] Remove updates --- rfcs/64-schematics.md | 84 +++---------------------------------------- 1 file changed, 4 insertions(+), 80 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index b208394f..75c0b052 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -40,16 +40,8 @@ A `Schematic` is just a collection of systems used for this synchronization proc * A system to convert components in the schematic world to components in the runtime world * A system to convert components in the runtime world to components in the schematic world -* A system to update a schematic component given the data of its related components in the runtime world - -Only the first of those 3 systems is mandatory. -The other 2 can be left out, but conversion from the runtime world might not be possible or not efficient respectively. - -* If for a schematic there is only an update system but no inference, then no new instances of this schematic can be found during runtime. -* If for a schematic there is only an inference system but no update, then the schematic gets replaced by a new one every time the schmatic world is updated. -* If for a schematic there is both, then inference works as expected (schematics are updated if they still exist, new ones are created). -* If for a schematic there is neither, then no inference is done (schematics are not updated, no new ones are created). +Only the first those two is mandatory. Usually though, you will not need to write these algorithms yourself - you can use `#[derive(DefaultSchematic)]` in most cases. ### Deriving `DefaultSchematic` @@ -113,7 +105,7 @@ The `DefaultSchematic` trait has only one function ```rust fn default_schematic() -> Schematic ``` -The derived implementation contains conversions both direction as well as updates. +The derived implementation contains conversions both directions. ### Schematic combinators @@ -152,13 +144,6 @@ where S: IntoSchematicInference ``` -Finally you can add updates by using the `set_update` function -```rust -fn set_udpate(self, system: S) -> Schematic -where - S: IntoSchematicUpdate -``` - #### Writing schematic conversion systems Conversions from schematic to runtime world are just normal systems that run on the schematic world and have a `SchematicQuery` parameter. @@ -235,57 +220,6 @@ fn inference_for_a( } ``` -#### Writing update systems - -```rust -fn update_for_a( - schematic_a: &mut SchematicA, - main_a: SchematicUpdate<&MainA>, - main_a_child: SchematicUpdate<&MainAChild, "child">, -) -> bool { - schematic_a.0 = main_a.0; - // TODO: Introduce entity mapping here - schematic_a.1 = main_a_child.1; - // return true, as this schematic still exists - true -} -``` - -Example for "enum schematic" - -```rust -#[derive(Component)] -enum AnimationState { - Idle, - Walking, - Jumping, -} - -#[derive(Component)] -struct Idle; - -#[derive(Component)] -struct Walking; - -#[derive(Component)] -struct Jumping; - -fn update_animation_state( - animation_state: &mut AnimationState, - idle: &Option, - walking: &Option, - jumping: &Option -) -> bool { - match (idle, walking, jumping) { - (Some(_), _, _) => *animation_state = AnimationState::Idle, - (_, Some(_), _) => *animation_state = AnimationState::Walking, - (_, _, Some(_)) => *animation_state = AnimationState::Jumping, - _ => return false, - } - true -} -``` - ### `UntypedSchematic` A `UntypedSchematic` is the same as a `Schematic` just without the type restriction. @@ -299,7 +233,7 @@ where S: IntoSchematicConversion ``` -You can add conversion in the other direction by using the `set_inference` function. +You can add conversion in the other direction by using the `add_inference` function. This will not replace any systems added previously by this method. ```rust fn add_inference(self, system: S) -> UntypedSchematic @@ -307,14 +241,6 @@ where S: IntoSchematicInference ``` -Finally you can add updates by using the `add_update` function. -This will not replace any systems added previously by this method. -```rust -fn add_udpate(self, system: S) -> UntypedSchematic -where - S: IntoSchematicUpdate -``` - ## Implementation strategy * `SchematicQuery` is basically `(Query)>, Local>, AppCommands)` where `SchematicData` is something like @@ -337,7 +263,6 @@ where marker: PhantomMarker, conversion: Box>, inference: Option>>, - update: Option>>, } ``` * `UntypedSchematic` is @@ -345,10 +270,9 @@ where struct UntypedSchematic { conversion: Vec>, inference: Vec>, - update: Vec>, } ``` -* `SchematicConversion`, `SchematicInference` and `SchematicUpdate` are basically just `System` with a restriction on the parameters +* `SchematicConversion` and `SchematicInference` are basically just `System` with a restriction on the parameters ## Drawbacks From 761df0938edb6d7c5d995e54584e30a4694eb8e5 Mon Sep 17 00:00:00 2001 From: MDeiml Date: Sun, 23 Oct 2022 12:35:29 +0200 Subject: [PATCH 12/16] Redefine inference --- rfcs/64-schematics.md | 74 ++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index 75c0b052..687d6c52 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -46,14 +46,14 @@ Usually though, you will not need to write these algorithms yourself - you can u ### Deriving `DefaultSchematic` -You can add `#[derive(DefaultSchematic)]` to any struct or enum that is a component. +You can add `#[derive(DefaultSchematic)]` to any struct or enum that is a component and that implements `Default`. If you derive `DefaultSchematic` for a struct, every untagged field needs to implement `Component` and `Clone`. Otherwise it either needs to implement `Default` and be tagged it with `#[schematic_ignore]`, or you need to specify another `Component`, such that the field has `From` and `Into` instances for this component. In the latter case you need to tag the field with `#[schematic_into(OtherComponent)]` ```rust -#[derive(Component, DefaultSchematic)] +#[derive(Component, Default, DefaultSchematic)] struct MeshRenderer { mesh: Handle, material: Handle, @@ -89,7 +89,7 @@ struct Attacking { weapon_type: WeaponType, } -#[derive(Component, DefaultSchematic)] +#[derive(Component, Default, DefaultSchematic)] enum AnimationState { Walking { #[schematic_into(Walking)] @@ -130,28 +130,21 @@ app.add_schematic(CloneSchematic::::default()); ### Creating `Schematic` manually -`Schematic` can be constructed manually using the `new` function ```rust fn new(system: S) -> Schematic where S: IntoSchematicConversion -``` -You can add conversion in the other direction by using the `set_inference` function -```rust -fn set_inference(self, system: S) -> Schematic +fn add_inference(self, system: S) -> Schematic where S: IntoSchematicInference -``` -#### Writing schematic conversion systems - -Conversions from schematic to runtime world are just normal systems that run on the schematic world and have a `SchematicQuery` parameter. -A `SchematicQuery` works almost the same as a normal `Query`, but when iterating over components will additionaly return `SchematicCommands` for the component queried. -This can be used to insert and modify components on the corresponding entity on `CoreWorld::Main` as well as spawn new entities there. -The entities spawned in this way can not be modified by `SchematicCommands` from other systems, but will be remembered for this system similar to `Local` system parameters. +fn add_inference_for_entity(self, entity_label: impl SchematicLabel, system: S) -> Schematic +where + S: IntoSchematicInference +``` -A typical conversion will look like this: +#### Example 1 ```rust #[derive(Component)] @@ -165,7 +158,7 @@ struct MainAChild(Entity); /// Translates a `SchematicA` to a `MainA` as well as a child entity that has a `MainAChild`. /// The system can contain any other parameter besides the schematic query -fn schematic_for_a(query: SchematicQuery>, mut some_resource: ResMut) { +fn schematic_a(query: SchematicQuery>, mut some_resource: ResMut) { for (a, commands) in query { some_resource.do_something_with(a); // You can modify components @@ -182,6 +175,34 @@ fn schematic_for_a(query: SchematicQuery>, mut some_resource: Re }); } } + +fn inference_for_main_a( + query: InferenceQuery<&MainA, SchematicA>, +) { + for (a, inference_commands) in query { + inference_commands.infer(|schematic_a| { + schematic_a.0 = a.0; + }); + } +} + +fn inference_for_main_a_child( + query: InferenceQuery<&MainAChild, SchematicA>, + child_query: Query<&Children>, +) { + for (a_child, inference_commands) in query.iter_find(|entity| child_query.get(entity)) { + let entity = inference_commands.map_entity(a_child.0); + inference_commands.infer(|schematic_a| { + schematic_a.1 = entity; + }); + } +} + +fn build_schematic() -> Schematic { + Schematic::new(schematic_a) + .add_inference(inference_for_main_a) + .add_inference_for_entity("child", inference_for_main_a_child) +} ``` The `SchematicQuery` will automatically only iterate over components that were changed since the system last ran. @@ -199,25 +220,6 @@ The main methods of `SchematicCommands` are: #### Writing inference systems ```rust -fn inference_for_a( - query: InferenceQuery<(&MainA, &Children), SchematicA>, - child_query: InferenceQuery<(Entity, &MainAChild)>, -) { - for ((a, children), inference_commands) in query { - let result = children - .iter_many(children) - .filter_map(|result| result.ok()) - .next(); - let (child, a_child) = match result { - Some(value) => value, - None => continue, - }; - let entity = inference_commands.map_entity(a_child.0); - inference_commands.infer(SchematicA(a.0, entity)); - // This is important so that the schematic world knows about this relation - inference_commands.set_entity("child", child); - } -} ``` ### `UntypedSchematic` From 0ec0789c082d06ed3ee124ab1206735917887a60 Mon Sep 17 00:00:00 2001 From: MDeiml Date: Sun, 23 Oct 2022 12:41:50 +0200 Subject: [PATCH 13/16] Remove typing from schematic and combinators --- rfcs/64-schematics.md | 63 ++++++++----------------------------------- 1 file changed, 11 insertions(+), 52 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index 687d6c52..066a452a 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -103,17 +103,10 @@ enum AnimationState { The `DefaultSchematic` trait has only one function ```rust -fn default_schematic() -> Schematic +fn default_schematic() -> Schematic ``` -The derived implementation contains conversions both directions. - -### Schematic combinators -`Schematic` has the following functions which can be used to create new schematics: -* `map(self, into: F, from: G) -> Schematic where F: Fn(&A) -> C, G: Fn(&C) -> A` -* `zip(self, other: Schematic) -> Schematic<(A, C)>` -* `alternative(self, other: Schematic) -> Schematic>` -* `filter bool>(self, f: F) -> Schematic` +The derived implementation contains conversions both directions. ### Adding schematics to your app @@ -131,17 +124,17 @@ app.add_schematic(CloneSchematic::::default()); ### Creating `Schematic` manually ```rust -fn new(system: S) -> Schematic +fn new(system: S) -> Schematic where S: IntoSchematicConversion -fn add_inference(self, system: S) -> Schematic +fn add_inference(self, system: S) -> Schematic where - S: IntoSchematicInference + S: IntoSchematicInference -fn add_inference_for_entity(self, entity_label: impl SchematicLabel, system: S) -> Schematic +fn add_inference_for_entity(self, entity_label: impl SchematicLabel, system: S) -> Schematic where - S: IntoSchematicInference + S: IntoSchematicInference ``` #### Example 1 @@ -198,7 +191,7 @@ fn inference_for_main_a_child( } } -fn build_schematic() -> Schematic { +fn build_schematic() -> Schematic { Schematic::new(schematic_a) .add_inference(inference_for_main_a) .add_inference_for_entity("child", inference_for_main_a_child) @@ -217,32 +210,6 @@ The main methods of `SchematicCommands` are: * `require_despawned(label: L)` * `require_deleted()` -#### Writing inference systems - -```rust -``` - -### `UntypedSchematic` - -A `UntypedSchematic` is the same as a `Schematic` just without the type restriction. -Usually it is safer to use the `Schematic` interface, but you might want to handle multiple schematic components within the same system. -Not having an associated type means, that the `map`, `zip`, `alternative` and `filter` methods are not available for `UntypedSchematic`. - -`UntypedSchematic` can be constructed manually using the `new` function -```rust -fn new(system: S) -> UntypedSchematic -where - S: IntoSchematicConversion -``` - -You can add conversion in the other direction by using the `add_inference` function. -This will not replace any systems added previously by this method. -```rust -fn add_inference(self, system: S) -> UntypedSchematic -where - S: IntoSchematicInference -``` - ## Implementation strategy * `SchematicQuery` is basically `(Query)>, Local>, AppCommands)` where `SchematicData` is something like @@ -261,17 +228,9 @@ where ``` * `Schematic` is ```rust - struct Schematic { - marker: PhantomMarker, - conversion: Box>, - inference: Option>>, - } - ``` -* `UntypedSchematic` is - ```rust - struct UntypedSchematic { - conversion: Vec>, - inference: Vec>, + struct Schematic { + conversion: Box, + inference: Option>, } ``` * `SchematicConversion` and `SchematicInference` are basically just `System` with a restriction on the parameters From 3060f2fdab3a5f8851c38d56a27ddd520bc2f22d Mon Sep 17 00:00:00 2001 From: MDeiml Date: Sun, 23 Oct 2022 14:13:55 +0200 Subject: [PATCH 14/16] Add examples --- rfcs/64-schematics.md | 125 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 119 insertions(+), 6 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index 066a452a..43478dec 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -137,7 +137,118 @@ where S: IntoSchematicInference ``` -#### Example 1 +#### Example for struct + +```rust +#[derive(Component, Default)] +struct MeshRenderer { + mesh: Handle, + material: Handle, + name: String, + thumbnail: Handle, +} + +fn mesh_renderer_schematic(query: SchematicQuery) { + for (mesh_renderer, commands) in query { + commands.require(mesh_renderer.mesh.clone()); + commands.require(mesh_renderer.material.clone()); + commands.require(mesh_renderer.name.into()); + } +} + +fn infer_mesh_renderer( + mesh_query: InferenceQuery, MeshRenderer>, + material_query: InferenceQuery, MeshRenderer>, + name_query: InferenceQuery, +) { + for (mesh, commands) in mesh_query { + commands.infer(|mesh_renderer| mesh_renderer.mesh = mesh.clone()); + } + for (material, commands) in material_query { + commands.infer(|mesh_renderer| mesh_renderer.material = material.clone()); + } + for (name, commands) in name_query { + commands.infer(|mesh_renderer| mesh_renderer.name = name.into()); + } +} + +impl DefaultSchematic for AnimationState { + fn default_schematic() -> Schematic { + Schematic::new(animation_state_schematic) + .add_inference(infer_animation_state) + } +} +``` + +#### Example for enum + +```rust +#[derive(Component)] +struct Walking { + speed: f32, +} + +#[derive(Component)] +struct Jumping; + +#[derive(Component, Clone)] +struct Attacking { + damage: f32, + weapon_type: WeaponType, +} + +#[derive(Component, Default)] +enum AnimationState { + Walking { + speed: f32 + } + Jumping, + Attacking(Attacking), +} + +fn animation_state_schematic(query: SchematicQuery) { + for (animation_state, commands) in query { + match animation_state { + AnimationState::Walking { speed } => commands.require(Walking { speed }), + AnimationState::Jumping => commands.require(Jumping), + AnimationState::Attacking(attacking) => commands.require(attacking.clone()), + } + } +} + +fn infer_animation_state( + query: InferenceQuery<(Option<&Walking>, Option<&Jumping>, Option<&Attacking>), AnimationState>, +) { + for ((walking, jumping, attacking), commands) in query { + match (walking, jumping, attacking) { + (Some(Walking { speed }), _, _) => { + commands.infer(|animation_state| { + *animation_state = AnimationState::Walking { speed }; + }); + }, + (_, Some(Jumping), _) => { + commands.infer(|animation_state| { + *animation_state = AnimationState::Jumping; + }); + }, + (_, _, Some(attacking)) => { + commands.infer(|animation_state| { + *animation_state = AnimationState::Attacking(attacking.clone()); + }); + }, + } + } +} + +impl DefaultSchematic for AnimationState { + fn default_schematic() -> Schematic { + Schematic::new(animation_state_schematic) + .add_inference(infer_animation_state) + } +} +``` + +#### Example with related entities ```rust #[derive(Component)] @@ -151,7 +262,7 @@ struct MainAChild(Entity); /// Translates a `SchematicA` to a `MainA` as well as a child entity that has a `MainAChild`. /// The system can contain any other parameter besides the schematic query -fn schematic_a(query: SchematicQuery>, mut some_resource: ResMut) { +fn schematic_a(query: SchematicQuery>, mut some_resource: ResMut) { for (a, commands) in query { some_resource.do_something_with(a); // You can modify components @@ -191,10 +302,12 @@ fn inference_for_main_a_child( } } -fn build_schematic() -> Schematic { - Schematic::new(schematic_a) - .add_inference(inference_for_main_a) - .add_inference_for_entity("child", inference_for_main_a_child) +impl DefaultSchematic for SchematicA { + fn default_schematic() -> Schematic { + Schematic::new(schematic_a) + .add_inference(inference_for_main_a) + .add_inference_for_entity("child", inference_for_main_a_child) + } } ``` From 753b8ad8f391e766f82478a21fe4814e724a31df Mon Sep 17 00:00:00 2001 From: MDeiml Date: Sun, 23 Oct 2022 14:38:32 +0200 Subject: [PATCH 15/16] Fix struct example --- rfcs/64-schematics.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index 43478dec..ab30bd74 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -156,26 +156,36 @@ fn mesh_renderer_schematic(query: SchematicQuery) { } } -fn infer_mesh_renderer( - mesh_query: InferenceQuery, MeshRenderer>, - material_query: InferenceQuery, MeshRenderer>, - name_query: InferenceQuery, +fn infer_mesh( + query: InferenceQuery, MeshRenderer>, ) { - for (mesh, commands) in mesh_query { + for (mesh, commands) in query { commands.infer(|mesh_renderer| mesh_renderer.mesh = mesh.clone()); } - for (material, commands) in material_query { +} + +fn infer_material( + query: InferenceQuery, MeshRenderer>, +) { + for (material, commands) in query { commands.infer(|mesh_renderer| mesh_renderer.material = material.clone()); } - for (name, commands) in name_query { +} + +fn infer_name( + query: InferenceQuery, MeshRenderer>, +) { + for (name, commands) in query { commands.infer(|mesh_renderer| mesh_renderer.name = name.into()); } } -impl DefaultSchematic for AnimationState { +impl DefaultSchematic for MeshRenderer { fn default_schematic() -> Schematic { - Schematic::new(animation_state_schematic) - .add_inference(infer_animation_state) + Schematic::new(mesh_renderer_schematic) + .add_inference(infer_mesh) + .add_inference(infer_material) + .add_inference(infer_name) } } ``` From 0882fdfb3cc173404fd2994a759337b7c97f73b1 Mon Sep 17 00:00:00 2001 From: MDeiml Date: Sun, 23 Oct 2022 15:10:09 +0200 Subject: [PATCH 16/16] Add discovery --- rfcs/64-schematics.md | 93 +++++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/rfcs/64-schematics.md b/rfcs/64-schematics.md index ab30bd74..306bcfa8 100644 --- a/rfcs/64-schematics.md +++ b/rfcs/64-schematics.md @@ -135,6 +135,10 @@ where fn add_inference_for_entity(self, entity_label: impl SchematicLabel, system: S) -> Schematic where S: IntoSchematicInference + +fn add_discovery(self, system: S) -> Schematic +where + S: IntoSchematicDiscovery ``` #### Example for struct @@ -157,7 +161,7 @@ fn mesh_renderer_schematic(query: SchematicQuery) { } fn infer_mesh( - query: InferenceQuery, MeshRenderer>, + query: InferenceQuery<&Handle, MeshRenderer>, ) { for (mesh, commands) in query { commands.infer(|mesh_renderer| mesh_renderer.mesh = mesh.clone()); @@ -165,7 +169,7 @@ fn infer_mesh( } fn infer_material( - query: InferenceQuery, MeshRenderer>, + query: InferenceQuery<&Handle, MeshRenderer>, ) { for (material, commands) in query { commands.infer(|mesh_renderer| mesh_renderer.material = material.clone()); @@ -173,19 +177,29 @@ fn infer_material( } fn infer_name( - query: InferenceQuery, MeshRenderer>, + query: InferenceQuery<&Handle, MeshRenderer>, ) { for (name, commands) in query { commands.infer(|mesh_renderer| mesh_renderer.name = name.into()); } } +fn discover_mesh_renderer( + query: Query>, With>, With)>, + commands: DiscoveryCommands, +) { + for entity in query { + comands.discover(entity); + } +} + impl DefaultSchematic for MeshRenderer { fn default_schematic() -> Schematic { Schematic::new(mesh_renderer_schematic) .add_inference(infer_mesh) .add_inference(infer_material) .add_inference(infer_name) + .add_discovery(discover_mesh_renderer) } } ``` @@ -226,34 +240,46 @@ fn animation_state_schematic(query: SchematicQuery) { } } -fn infer_animation_state( - query: InferenceQuery<(Option<&Walking>, Option<&Jumping>, Option<&Attacking>), AnimationState>, +fn infer_walking( + query: InferenceQuery<&Walking, AnimationState>, ) { - for ((walking, jumping, attacking), commands) in query { - match (walking, jumping, attacking) { - (Some(Walking { speed }), _, _) => { - commands.infer(|animation_state| { - *animation_state = AnimationState::Walking { speed }; - }); - }, - (_, Some(Jumping), _) => { - commands.infer(|animation_state| { - *animation_state = AnimationState::Jumping; - }); - }, - (_, _, Some(attacking)) => { - commands.infer(|animation_state| { - *animation_state = AnimationState::Attacking(attacking.clone()); - }); - }, - } + for (walking, commands) in query { + commands.infer(|animation_state| *animation_state = AnimationState::Walking { speed }); + } +} + +fn infer_jumping( + query: InferenceQuery<(), AnimationState, With>, +) { + for (_, commands) in query { + commands.infer(|animation_state| *animation_state = AnimationState::Jumping); + } +} + +fn infer_attacking( + query: InferenceQuery<&Attacking, AnimationState>, +) { + for (attacking, commands) in query { + commands.infer(|animation_state| *animation_state = AnimationState::Attacking(attacking.clone())); + } +} + +fn discover_animation_state( + query: Query, With, With)>, + commands: DiscoveryCommands, +) { + for entity in query { + commands.discover(entity); } } impl DefaultSchematic for AnimationState { fn default_schematic() -> Schematic { Schematic::new(animation_state_schematic) - .add_inference(infer_animation_state) + .add_inference(infer_walking) + .add_inference(infer_jumping) + .add_inference(infer_attacking) + .add_discovery(discover_animation_state) } } ``` @@ -302,9 +328,8 @@ fn inference_for_main_a( fn inference_for_main_a_child( query: InferenceQuery<&MainAChild, SchematicA>, - child_query: Query<&Children>, ) { - for (a_child, inference_commands) in query.iter_find(|entity| child_query.get(entity)) { + for (a_child, inference_commands) in query { let entity = inference_commands.map_entity(a_child.0); inference_commands.infer(|schematic_a| { schematic_a.1 = entity; @@ -312,11 +337,27 @@ fn inference_for_main_a_child( } } +fn discover_schematic_a( + query: Query<(Entity, &Children), With>, + child_query: Query>, + commands: DiscoveryCommands, +) { + for (entity, children) in query { + let child = child_query.get_many(children).filter_map(|result| result.ok()).next(); + if Some(child) = child { + commands + .discover(entity) + .with_entity("child", child); + } + } +} + impl DefaultSchematic for SchematicA { fn default_schematic() -> Schematic { Schematic::new(schematic_a) .add_inference(inference_for_main_a) .add_inference_for_entity("child", inference_for_main_a_child) + .add_discovery(discover_schematic_a) } } ```