-
Notifications
You must be signed in to change notification settings - Fork 23
Description
This issue is to discuss an ergonomics issue that I've faced both in Bones ECS and in Bevy ECS, and to explore whether or not there's a good solution.
The scenario is simple:
I have a custom system parameter named CollisionWorld that contains helper methods and logic around detecting collisions and manipulating the physics world.
As a part of that CollisionWorld parameter, I borrow the Transform components mutably. This allows me to, as a part of collision world methods like translate move entities, and also to get the entities positions, as is necessary for collision detection.
This causes an ergonomics problem when any the user wants to include both a CollisionWorld system param, and a CompMut<Transforms> system param, because that is a conflict: you have two mutable borrows of Transform components.
The current solution in my WIP branch in jumpy is to make the transforms: CompMut<Transform> field of CollisionWorld public. This allows you to access a borrow of the transforms, but it's not a perfect solution, and users are not going to expect that it's impossible to borrow both their own transforms argument, and the CollisionWorld.
Also, considering the situation where the CollisionWorld and another system parameter needs to borrow CompMut<Transform>, there is no good workaround.
This is partially just a limitation of the way borrowing works. The issue is "solved" in Bevy using ParamSets which usually feels un-ergonomic, but again, there's only so much we can do in Rust, where we must make our borrowing intentions clear at compile time. We can't have two mutable references to the Transform components at the same time. The only way around this is to delay the actual borrowing of the components, requiring an extra lock()/borrow() step.
Maybe we make a Defer system parameter, that wraps around other system parameters, deferring the borrow, and requiring an extra borrow() call to do runtime borrow checking later in the function.
In that case you would be allowed to have transforms: Defer<CompMut<Transform>> and collision_world: Defer<CollisionWorld> in your system params, but you would have to .borrow() them before you could use them, and you wouldn't be able to .borrow() them at the same time without a panic at runtime.
Finally, another alternative, is to have CollisionWorld use it's own component to represent the entity positions, and this component must be synchronized somehow with the Transform component. This is also a foot-gun because it's easy to forget to synchronize the values, and that there is in fact a separate CollisionWorld version of the entity position.