-
Notifications
You must be signed in to change notification settings - Fork 43
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
Alternative low boilerplate system syntax #75
Comments
im realizing now that |
Hi! Thank you, that's very nice of you =) I tried to do something like this in the past and systems might be functions in the end (or at least they'll be accepted too). I think my attempt was with Using "actual types" is another topic. Currently you can use 6 "storage types" and 4 modifiers in order to explain what you want from the pub struct Unique<T: ?Sized>(T);
pub struct NonSend<T: ?Sized>(T);
pub struct NonSync<T: ?Sized>(T);
pub struct NonSendSync<T: ?Sized>(T); I could use The chapter in the guide probably doesn't have the right name and is misleading. Currently unique storages are stored in a Despite all that,
pub struct ThreadPool; Regarding This concept is exactly the same in an ECS, you have shared and exclusive borrow, to components this time. I concede that having Lastly I just want to point out that the macro would still be useful. Not as much as today but would still have a few advantages:
If systems become simpler, it might become an opt-in feature instead of opt-out. Regarding for loops you can use them but you have to add an for (position, mut health) in (&positions, &mut healths).iter().into_iter() {
// do thing
} This function will transform a |
Thanks for the in depth response! I really appreciate that you took the time to explain everything.
In general it sounds like we're on the same page in most regards. Thanks again for your time! |
I think @cart is actually explaining the issue I have with the macro much more elegant. I actually switched back to the first, explicit approach to define the systems, since using this DSL, that's changing the meaning of standard rust language, breaks my cognitive flow and the usability of my programming environment. |
I've been thinking about the storage abstraction because of shared components recently. I agree that a single My goal with Std's Iterator is fairly flexible but not enough sadly. While you can choose the behavior of all its methods you're stuck with their return type. conflicting implementations of trait `std::iter::Iterator` for type `&mut _`:
conflicting implementation in crate `core`:
- impl<I> std::iter::Iterator for &mut I
where I: std::iter::Iterator, I: ?Sized; Or this: conflicting implementations of trait `std::iter::IntoIterator`:
conflicting implementation in crate `core`:
- impl<I> std::iter::IntoIterator for I
where I: std::iter::Iterator; But I just realized: I don't have to do something so generic. So I'm currently finishing the last chapter of the guide. When this is done I'll try to get a POC functional system to work and I'll report back to this issue. |
Haha your response makes me very happy. That all sounds brilliant. |
Indeed. This sounds like a good plan. Happy to test it in the future. |
I got way too curious 😄 I continued to think about it and came to the conclusion that this issue will change the library way too much if it works so I had to test. This is the result: use shipyard::*;
fn add_entities((mut entities, mut usizes): (EntitiesViewMut, ViewMut<usize>)) -> [EntityId; 3] {
[
entities.add_entity(&mut usizes, 0),
entities.add_entity(&mut usizes, 1),
entities.add_entity(&mut usizes, 2),
]
}
#[test]
fn test() {
let world = World::new();
let [entity1, entity2, entity3] = world.run_functional(add_entities);
world.run_functional(|usizes: View<usize>| {
assert_eq!(usizes.get(entity1), Ok(&0));
assert_eq!(usizes.get(entity2), Ok(&1));
assert_eq!(usizes.get(entity3), Ok(&2));
});
}
So the question is: and now? The compiler's type inference isn't good enough to have the same code work seamlessly with references. Returning values from workloads came up and I'll also look into it. Since this change will change a lot of things it'll be released a lot sooner than what I said earlier. |
Really appreciate your careful thought on this, checking in with the community (small as it is right now), and ability to prioritize and pivot! Thanks @leudz ! |
That is excellent news. I also really appreciate the quick turnaround and your openness to community feedback. |
I tried to get workloads to work but I don't think it's possible without GAT.
This is from december 10th, as you can see this is the same function based approach you proposed. I tried a new workaround with async/await under the hood: fn main() {
let world = World::new();
world
.add_workload("Test")
.with_system(add_entity)
.with_system(add_entity)
.build();
world.run_default();
}
fn add_entity(mut entities: EntitiesViewMut) {
dbg!(entities.add_entity((), ()));
} It works great... once. Here's what I think are the two best workarounds: fn main() {
let world = World::new();
world
.add_workload("Do things")
.with_system(|world: &World| world.run(add_entity), add_entity)
.with_system(|world: &World| world.run(print), print)
.build();
world.run_default();
}
fn add_entity(mut entities: EntitiesViewMut) {
entities.add_entity((), ());
}
fn print() {
println!("Done");
} This is the same thing as what I did back in december but less error prone. Workaround 2: fn main() {
let world = World::new();
world
.add_workload(
"Do things",
(
add_entity_system,
print_system,
),
);
// with async closure (not stable yet)
world
.add_workload(
"Do things",
(
async |world: &World| { world.async_run(add_entity).await; },
async |world: &World| { world.async_run(print).await; },
),
);
world.run_default();
}
fn add_entity(mut entities: EntitiesViewMut) {
entities.add_entity((), ());
}
fn print() {
println!("Done");
}
async fn add_entity_system(world: &World) {
world.async_run(add_entity).await;
}
async fn print_system(world: &World) {
world.async_run(print).await;
} This workaround is very different from the previous one or what shipyard does today. I'll have to modify the library a lot to make it work somewhat efficiently. So except if a better approach comes up, we'll have to choose between workaround 1, workaround 2 or keep the current trait. My opinion: I don't think workaround 2 is worth it. I'm very new to async/await, it'll take me between weeks to months to get the library back to where it is today. Plus it's very misleading that it needs an async function when systems can't be async themselves. Once async systems are implemented it'll be totally different. I don't think workaround 1 is horrible, the repetition could be problematic during refactoring but since they are in the same function call and most likely the same line, I think it's ok. |
Workaround 1 seems totally reasonable to me |
Workaround 1 seems fine indeed. About those async closures: I had to implement a workaround for that, since I use closures to give my test in it's own isolated test database on the fly and handle setup / cleanup (also on panic using |
Agree w/ Workaround 1. I like the chaining here, feels a little more explicit about the order systems are run in when they can't be in parallel as opposed to the current "left-to-right" approach Feels a little funny having to repeat the function name but not a big deal at all, especially since workloads are really just specified once, and if all you're waiting for is a planned Rust feature to get rid of that then it's a great tradeoff! I assume |
The chaining works the same way as left to right currently. Systems will run top to bottom and the scheduler will try to run them in parallel as much as possible based on what the systems borrow. Yep it's a bit silly, the first function is the system that will be stored and run, the second one is there to give all information about what the system is borrowing. Yes |
Workaround 1 seems like the best option to me as well. Personally if I have to choose between system declaration clarity / ergonomics and scheduling clarity / ergonomics, i'll pick systems every time. |
Workaround 1 seems to be the big winner. |
The migration chapter is up. |
First: this is a really cool project and I've enjoyed playing with it so far :)
Current system declarations
Right now there are three ways to declare systems and each has its own tradeoffs:
Struct with System trait impl
Function with macro
Anonymous function
Proposed system declaration
Additionally I think the lifetime could be elided in this case:
View<Positition>
) instead of type references (&Position
). But I view this as a win because its honest about the types being used / has parity with other storages likeUnique<T>
.I personally think this the most ergonomic / straightforward solution (provided its actually possible).
It also looks like this couldn't build off of the existing Run trait because it relies on T::SystemData, which assumes types are declared as
(&Position, &mut Health)
instead of(View<Position, ViewMut<Health>)
. But implementing this seems doable because it is actually less abstraction over the types used to run the system.Does this seem doable? Is it desirable?
The text was updated successfully, but these errors were encountered: