Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proto-RFC: Composable Systems and Components #1951

Closed
anchpop opened this issue Apr 17, 2021 · 8 comments
Closed

Proto-RFC: Composable Systems and Components #1951

anchpop opened this issue Apr 17, 2021 · 8 comments
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged

Comments

@anchpop
Copy link
Contributor

anchpop commented Apr 17, 2021

What problem does this solve or what need does it fill?

Some restrictions on Bevy's ECS (as well as every other ECS I've ever used) lead to certain systems and components becoming bloated and unpleasant to write, and in some cases unmaintainable. Lifting these restrictions would not make the ECS harder to understand or affect existing users. Let me give some examples of where these restrictions require worse code:

  1. Forcing the choice between bloated systems and unnecessary components. Imagine you have several types of agents walking around in your game. (Maybe this is dwarf fortress and you have miners, painters, plumbers, etc.). The game stores which type each agent is in a component called AgentType. You'd like to add a button for each agent-type that allows the user to select all the agents of that type. (Maybe this opens a further UI that allows the user to tell them to perform some action or take the day off or something, but that's not relevant). We'll assume each agent internally has a unique identifier component called an AgentID. You want each button to have this behavior:

    1. When the button is hovered, slightly enlarge all the agents of that type, so the user can see which ones they'll be selecting.

    2. When the button is clicked, add the AgentIDs of all the agents of that type to a vector inside a resource that represents the currently selected agents. (Call the resource type SelectedAgents.)

    3. When the button isn't hovered, return the agents to their original size

    Currently, you have two choices for how to go about implementing this.

    1. Bloated system: You have a system that iterates over every button and checks its interaction state. Each button obviously needs to know which AgentType it applies to, so let's say that's stored in a component on the button. The system also takes a queryset containing two queries, Query<(&AgentType, &mut Transform)> and Query<(&AgentType, &AgentID)>, as well as ResMut<SelectedAgents>. For each button, if it's hovered we iterate over the first query and enlarge the matching agents, if it's clicked we iterate over the second query and add the AgentID of matching agents to the SelectedAgents resource, and if it's neither we iterate over the first query and return all agents to their original size.

      This system is huge, containing the logic for all 3 button states. You could factor out the logic into separate functions (possibly scoped inside the system), but no matter what this system has to take the queryset containing two queries and the resource.

    2. Unnecessary components: Your system is the same as the previous one, except instead of doing the highlighting/selection logic inside the button-system, the button system only needs a Query<(Entity, &AgentType)> so it can iterate over the agents and add marker components to agents of the appropriate type. Separate systems then are in charge of enlarging/ensmallening agents based on which of the marker components they have.

      This works too, but it spreads the code specifying the button's behavior into multiple functions which all really only make sense in terms of each other. It also makes possible some minor defects where something isn't selected until the frame after you hover over the button because the system that adds the marker component is running after the system that consumes the component. Also, it just feels weird - you iterate over all the entities so you can add a component to them, then you iterate over all the entities with the component so you can do something else to them. Why not just do it while you're iterating?

  2. Making declarative components impossible: This one is a lot simpler haha. Imagine you want to have a DeclarativeButton component, which stores three functions, one for each interaction state. A system iterates over all the DeclarativeButton components and executes the appropriate function given the button's interaction state. This could work today if the only thing your functions did was println or something, but if you wanted these functions to actually be systems, that's just not possible right now.

    And declarative buttons in this style would be really nice, e.g. when you want to have 10 different buttons, each one doing something completely different (one opening the print menu, one saving the project, one opening a help page in a web browser, etc.).

    The alternative is having like 10 different marker components that each mark exactly one entity - kind of lame. There's an argument to be made that this is an unholy inter-mingling of components and systems, and it certainly wouldn't be right for every occasion, but in many cases I think it would add considerable clarity.

What solution would you like?

I suggest: adding one-shot systems, with the option when calling them to specify their In parameter (if they have one). Once we have that, constructing systems from closures would be an amazing-but-not-strictly-necessary addition. Here's how that changes the examples:

  1. The dwarf fortress clone: Now, you have one system that iterates over all the buttons. The function that specifies the system only needs to take a Commands and the query needed to grab each button's Interaction and associated AgentType. At the top of the function, you have three nested systems unhighlight, highlight, and select (just written as regular functions as usual, only nested). (The systems also take an In<AgentType> that tells them which agent type they're going to apply to.) Then, you just iterate over each button and call e.g. commands.run_system_with_input(highlight_all.system(), agent_type).

    This lets you group the related code to make it easier to understand what the function is doing, avoid polluting the global namespace, avoid unecessary components, and makes the code more modular because a change in the what query (for example) the highlight_all system needs doesn't need to affect the system that's just in charge of iterating over the buttons.

    commands.run_system_with_input need not execute the system immediately. If it did, work would have to be done to make sure systems invoked this way can't query for mutable references to values also referenced by the system that called them.

  2. The declarative buttons: This one is pretty obvious I think, basically the DeclarativeButton component also has three fields on_click on_hover on_leave or something, each storing a system that takes an In<Entity> which will be passed the entity of the button that got clicked, as well as any other queries or resources it needs to do its job. (I'm not sure what the types of those fields would actually be. impl System<In = Entity>s?)

I have an ulterior motive here: these abilities would drastically simplify my project of porting data joins and react-spring to Bevy. They're both incredibly useful abstractions I often find myself reaching for. With these capabilities, we could make some things that are currently difficult or time-consuming in almost all ECS systems incredibly ergonomic and composable in Bevy.

@anchpop anchpop added C-Feature A new feature, making something new possible S-Needs-Triage This issue needs to be labelled labels Apr 17, 2021
@TheRawMeatball
Copy link
Member

commands.run_system_with_input(highlight_all.system(), agent_type)

This is also something I've considered, and should be pretty simple to enable with a single impl:

impl<F: FnOnce(&mut World)> Command for F {}

Then, this functionality could be trivially built on top of this: you have these systems in a resource, and inside the closure get the resource using a resource scope and run it using &mut World. Oneshot systems are a very interesting concept. You're more than welcome to open a simple PR to lead the way for a full RFC :)

@hymm
Copy link
Contributor

hymm commented Apr 17, 2021

I'm rather new to ecs, so this might be off base, but I feel like this sort of thing is against the ecs pattern. That instead of operating on groups of components (data), you're now acting on individual entities.

I feel like a better solution to this problem is a more explicit way of expressing data piping. Where you have a sequence of systems acting on related sets of data. We currently do things like branching with marker components, but it does make everything pretty loosely coupled and hard to reason about. I'm not sure what a more explicit api would look like.

@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events and removed S-Needs-Triage This issue needs to be labelled labels Apr 17, 2021
@alice-i-cecile
Copy link
Member

Relevant to @TheRawMeatball's point above, I've toyed with the idea of one shot systems as commands before, and ways we can unify the two concepts. I think there's a lot of promise there.

@hymm
Copy link
Contributor

hymm commented Apr 17, 2021

adding to my last comment, like for your button example. I really want a way to express three new systems that each work on a disjoint subset of the original query. Maybe something like query piping?

@alice-i-cecile
Copy link
Member

alice-i-cecile commented Apr 17, 2021

On your original point: I've seen this pattern pop up a lot with UI. It would be interesting to have ways to allow systems to operate more generically over types, without forcing you to manually add a separate system for each new type you want to operate on. The systems in my proposed add_style method in the Styling RFC is a great clear example of this pattern and the boilerplate it requires.

This aims to capture the idea of "multiple related systems running in disjoint ways" that @hymm wants, but stated in a tangible ECS and Rust-compatible way.

@hymm
Copy link
Contributor

hymm commented Apr 17, 2021

after reading @alice-i-cecile's system-driven-ui rfc, I now agree that closure commands could be a nice solution for things like ui where you have a single event acting on one entity. It'll be important to have good documentation about this as I can see people reaching for this pattern because it is "closer" to other programming styles, but it is more appropriate to use a system instead.

@alice-i-cecile alice-i-cecile added the S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged label Dec 12, 2021
@Testare
Copy link
Contributor

Testare commented Jan 30, 2024

Resolved by #10380?

@alice-i-cecile
Copy link
Member

Yeah, the bulk of this appears to be resolved!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged
Projects
None yet
Development

No branches or pull requests

5 participants