-
Notifications
You must be signed in to change notification settings - Fork 136
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
System Definition Alternatives #31
Comments
With regards to the safety of random access in the for-each trait/macro:
|
I hacked together a quick |
Using the new #[system]
fn system(
resource1: &Resource1,
resource2: &mut Resource2,
mut query: PreparedQuery<(Write<Position>, Read<Velocity>)>,
world: &mut PreparedWorld) {
for (mut pos, vel) in query.iter(world) { /* ... */
} It should be possible to implement something like this in Legion as well. (The only caveat is it doesn't support query filters yet, since those are handled at runtime. I need to figure out a way to encode filters at the type level.) |
Iterating a little on the macro design: #[system]
fn update_positions(
#[res] time: &Time,
#[query] entities: (Write<Position>, Read<Velocity>),
world: &mut PreparedWorld)
{
for (mut pos, vel) in entities.iter(world) {
*pos += *vel * time.delta_seconds();
}
}
#[for_each]
fn update_positions(#[res] time: &Time, #[com] pos: &mut Position, #[com] vel: &Velocity) {
*pos += *vel * time.delta_seconds();
} Two macros, Both compile into a function which builds a system via the
So the default behaviour is to create a generator function with the same name as the input function. e.g: #[for_each]
fn update_positions(
#[res] time: &Time,
#[com] pos: &mut Position,
#[com] vel: &Velocity)
{
*pos += *vel * time.delta_seconds();
}
#[for_each(fn = "build_update_positions")]
fn update_positions(
#[res] time: &Time,
#[com] pos: &mut Position,
#[com] vel: &Velocity)
{
*pos += *vel * time.delta_seconds();
}
#[for_each(struct = UpdatePositions)]
fn update_positions(
#[res] time: &Time,
#[com] pos: &mut Position,
#[com] vel: &Velocity)
{
*pos += *vel * time.delta_seconds();
}
let mut scheduler = SystemScheduler::new();
scheduler.add_system(Stages::Update, update_positions()); // create system via function call
scheduler.add_system(Stages::Update, build_update_positions()); // create system via function call
scheduler.add_system(Stages::Update, UpdatePositions); // create system via .into() on struct
neither of these are strictly needed. We could make resources implicit if the type isnt a query, world or command buffer. Queries are always tuples and not references, but unlike all the other parameters, their type is re-written by the macro into the proper query type, so there is some magic there.
We could make one of these implicit, but I am not sure whether or not that is desirable or, if so, which one. Preferably there would be some consistency between |
I've experimented with alternative system definitions and I think I came up with a pretty good solution, but it obviously still needs refining. I shared code on https://github.com/chemicstry/legion_test but keep in mind it's a test bed mess. It is based on experimental legion and you need to implement Anyway, skipping the implementation details, system syntax looks very similar to specs. Here is an example:
Most of the syntax looks okay, but I couldn't figure out how to express filters only with a type system or rather the expression gets really long. Legion filters are constructed at run time and this does not work with |
Do you think something along the lines of the following is possible? make_system!(build_my_system, |(res_a: Read<TestResourceA>,),
(query_a: Query<Write<...>,...>::filter(...),),
_command_buffer: &mut CommandBuffer,
_world: &mut SubWorld| {
do the things;
}); |
I guess it would be possible, but hard to tell. I'm not that experienced with macros yet. Although it still has the same problem of filters needing to be defined at type level. Having a low-boilerplate system syntax is nice, but at the same time macros make it really hard to understand what is happening under the hood. I would prefer to have a pure rust way to define them and have such a simplified macro as an alternative. Also, the reason I'm trying to implement systems as structs is because some systems (rendering) in amethyst have to do resource initialization (solved by double wrapping in closures) and deallocation (unsolved). Having a trait with |
I had a little play in the rust playground here a few days ago looking into whether I could work around the lifetime issues that were blocking #134, and while what I had there is very much just hashing stuff out, it looks like it should be entirely possible to have something like: let system = System::for_each(|a: &A, b: &mut B, c: Option<&C>, d: Option<&mut D>| {
...
}); For a basic single-query loop for the query The syntax is still very limited though, as (most notably) it does not allow access to resources. That might perhaps look something like: let system = System::for_each_with_resources(
|cmd: &mut CommandBuffer,
world: &mut SubWorld,
(a, b, c, d): (&A, &mut B, Option<&C>, Option<&D>),
(time, renderer): (&Time, &mut Renderer)| {
...
}
); It might, for consistency, make sense to then also put the |
I really think a struct is needed here. Closures are just not enough to provide multiple hooks as per #164 while still having named variables shared across these. If a macro is needed to make it reasonable to define, I think that should be considered. It's better than having to hack around closure limitations. |
@kabergstrom I highly agree with you. I've been trying various alternatives to struct and nothing felt good enough. The main limitation with struct systems is query filtering. Currently legion queries are implemented to be setup via runtime expression and this makes typed filters really inconvenient to use. Ideally this should be fixed in the filter types, but macro is also possible without much trouble. Maybe @TomGillen can comment on this. I was also trying to merge |
I have been working on this over the weekend (very nearly done, just need to sort supporting systems with generic arguments), but for handling system state it looks a bit like this: /// declare a system with mutable counter state
#[system]
fn stateful(#[state] counter: &mut usize) {
*counter += 1;
println!("state: {}", counter);
}
fn build_schedule() -> Schedule {
Schedule::builder()
// initialize state when you construct the system
.add_system(stateful_system(5usize))
.build()
} Other examples of what you can do: #[system(for_each)]
fn update_positions(position: &mut Position, velocity: &Velocity, #[resource] time: &Time) {
*position += *velocity * time.seconds();
}
#[system(par_for_each)]
#[filter(maybe_changed::<Position>())]
fn transform(position: &Position, rotation: Option<&Quaternion>, transform: &mut mat4x4) {
*transform = Transform::calculate(position, rotation);
}
#[system]
#[read_component(Velocity)]
#[write_component(Position)]
fn custom(world: &mut SubWorld, #[resource] time: &Time) {
let mut query = <(Write<Position>, Read<Velocity>)>::query();
for (pos, vel) in query.iter_mut(world) {
*pos += *vel * time.seconds();
}
} The functions that are annotated with the You even get nicer compile errors in many cases, because we can emit custom errors with the proc macro. e.g.: Where that
|
Just got generic systems working: #[system(for_each)]
fn print_component<T: Component + Display>(component: &T) {
println!("{}", component);
}
fn build_schedule() -> Schedule {
Schedule::builder()
.add_system(print_component_system::<Position>())
.build()
} |
We currently provide the
SystemBuilder
for constructing systems. This API is needed because it is very difficult to manually express the type of a legion system, as it statically encodes an arbitrary number of queries and resource accesses, and queries statically encode into their type all of their data accesses (tag/component types and mutable/immutable) and filter expressions (which can be arbitrary boolean expressions).However, in many cases the full power of
SystemBuilder
is not needed; most systems only use one query, and do not request any complex filter logic on that query. There may be scope for providing a higher level simplified API for system construction which would still be sufficient to cover the most common use cases.System Traits
The first option may be to provide system traits which can be implemented on a unique struct for each system, similar to how specs/amethyst defines its systems:
There are a few limitations on
ForEachSystem
:unsafe
APIs would be usable.A
ParForEachSystem
alternative could be provided which usespar_for_each
internally, but it would require the additional restriction thatResources
must beReadOnly
.These system structs can be converted into a
System
via.into_system()
.Macro
Alternatively, we could use a procedural macro to define systems. I do not have experience writing procedural macros, so this is mostly speculative:
This would create an
UpdatePositions
system struct and implementIntoSystem
. It has many of the restrictions of theForEachSystem
trait, except:#[tag]
,#[resource]
and some filters such as#[on_changed]
.for_each
orpar_for_each
, depending on whether or not all resources are accessed immutably, although the syntax does not make the performance impact obvious.Entity
andCommandBuffer
would be optional.PreparedWorld
could be allowed, provided we check safety.The above macro would generate something like:
The text was updated successfully, but these errors were encountered: