-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Description
What problem does this solve or what need does it fill?
1. Reducing boilerplate
In specs SystemData
solves the problem of re-using sets of resources and components, eliminating the need to copy-paste common storage declarations among different systems, that work with the same components or resources.
2. Attaching implementation to a set of resources or archetypes
Apart from that, SystemData
enables attaching implementation to a set of resources. One may argue that attaching functionality to components or resources goes against the ECS paradigm, but I can personally see this useful for implementing helper methods, that could be re-used by many systems.
In the game that I was building on Amethyst I used this pattern to "extend" (more like to compose together) the engine's Time
resource with additional data, that I used for counting frames spent playing a level: https://github.com/amethyst/grumpy_visitors/blob/f12b85d0dac07c101b43e83711b89777dc69dd58/libs/core/src/ecs/system_data/time.rs
This allowed me to create a useful abstraction, that helped me to conveniently access both game time and engine time from any system.
3. Working around the limit of the number of elements one can pass to a system
I haven't been able to look into Bevy's implementation of passing resources/components into systems yet, but I guess there's a limit of the number of elements one can pass to a system. SystemData
can also be used for working this limitation around.
4. Enhances the UX of accessing components
Users would be able to access components like they would access a struct field, without the need to destructure a tuple to get meaningful identifiers.
for attacker in attackers.iter() {
let damage_dealt = attacker.damage * *attacker.damage_modifier;
}
// instead of
for (damage, damage_modifier) in attackers.iter() {
let damage_dealt = damage * *damage_modifier;
}
// or
for attacker in attackers.iter() {
let damage_dealt = attacker.0 * *attacker.1;
}
If a query is ever edited, the first loop is much easier to refactor. For instance, if we add a new component somewhere in the middle, a developer won't need to add a new variable to a destructuring pattern, also keeping in mind the order of components.
Describe the solution would you like?
I believe that we could implement two derive macros: one for resources, one for components. Ideally, I'd imagine using them would look something like this:
#[derive(SystemResources, Clone, Copy)]
pub struct MyResources {
resource_a: &ResourceA,
resource_b: &mut ResourceB,
}
#[derive(SystemComponents, Clone, Copy)]
pub struct MyArchetype {
component_a: &ComponentA,
component_b: &mut ComponentB,
}
fn for_each_system(my_resources: MyResources, my_archetype: MyArchetype) {
// ...
}
fn query_system(my_resources: MyResources, my_archetype_query: Query<MyArchetype>) {
for my_archetype in my_archetype_query.iter() {
// ...
}
}
Describe the alternative(s) you've considered?
1. Reducing boilerplate
Speaking of system declarations this can be solved with introducing type aliases for some common queries. Though this solution introduces more cognitive complexity because developers will have to keep the order of components in mind. Introducing new components to a query is more painful to refactor.
2. Attaching implementation to a set of resources or archetypes
Attaching implementation is likely possible with implementing custom traits for tuples of components/resources references, but the UX of this method seems to have obvious drawbacks, that are covered in other sections as well.
3. Working around the limit of the number of elements one can pass to a system
Introducing a feature flag that would enable implementations of necessary traits for larger tuples: (A, B, C, ..., Z)
.
4. Enhances the UX of accessing components
I don't think there are any, though one can dispute the importance of this. I guess this point can be perceived as one of the positive side-effects if we implement SystemData
in Bevy.
Additional context
A couple of weeks ago I made an attempt to implement this for Legion, which resulted in SystemResources
derive macro: amethyst/legion#211. This PR enables this pattern only for resources, but not components (hopefully, yet).