How does Bevy "scope" its systems based on the type of the arguments?
Asked Answered
U

1

8

Bevy, a new Rust game engine and ECS, has a feature where it "scopes" its systems based on the type of the arguments. From its docs:

The parameters we pass in to a "system function" define what entities the system runs on. In this case, greet_people will run on all entities with the Person and Name component.

It looks like this:

struct Person;
struct Name(String);

fn greet_people(person: &Person, name: &Name) {
    println!("hello {}", name.0);
}

How is Bevy able to make this happen? I thought I read somewhere that Rust didn't support reflection in this way.

Uchida answered 20/8, 2020 at 1:50 Comment(0)
A
16

Bevy defines a set of traits (IntoSystem, which is implemented for all functions that implement SystemParamFunction/ExclusiveSystemParamFunction) that are implemented for all functions that can be used as systems. Those traits are then exported by the Bevy prelude. One of the limitations of this is that you can only convert functions up to a certain number of arguments into systems, and the arguments must be in a particular order ([command?], [resources...], [queries/components...]).

You can do this yourself by implementing traits in a similar manner:

trait MyTrait<Args> {
    fn invoke(&mut self, args: Args);
}

impl<F> MyTrait<()> for F
where
    F: FnMut() -> (),
{
    fn invoke(&mut self, (): ()) {
        (self)()
    }
}

// More impls for functions up to N arity...
// Usually a helper macro is used for this

fn test() {
    println!("Hello, world!");
}

fn main() {
    test.invoke(());
}

Playground

Accompaniment answered 20/8, 2020 at 2:14 Comment(3)
The way Bevy ECS does it is the correct way to go. It is the framework's job to handle boilerplate code such as systems requirement analyses. When the signature of a system function already demonstrates its requirements, there is no concrete need for the user to tag a macro or register something manually.Meacham
Is this considered hacky? I was quite confused by this when learning the lib.Unguent
@Unguent It's not too uncommon these days to find libraries doing something similar to this. For example, axum (web server lib) uses this approach for handlers. I don't think it's hacky at all, but I guess it depends on how you define "hacky". For what it's worth, functions implementing traits isn't unique to this either - functions and closures can implement Send, Sync, and Clone depending on what they capture, and those traits are often relied upon when passing around callbacks, like you might find with tower or even std::thread::spawn.Accompaniment

© 2022 - 2024 — McMap. All rights reserved.