You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I've been working on a simulation project that heavily utilizes shipyard (great project!) and have come to a point where I struggle a little bit with code organization in quickly evolving complicated state machines that access ECS storages. For splitting up the code into more easily readable bits, I previously simply used functions, like so:
This can work fine, but it introduces some annoying problems. First off there's a lot of particularly annoying (to me) boilerplate with the loop and placing the &mut and & borrows correctly. Secondly, and most importantly, it requires manually passing each borrow to the various helper functions, introducing more boilerplate again. It also becomes more difficult when the system gets big, and you suddenly realize that flirp, frobnicate, and florp all need to suddenly access 2-3 new storages, and you need to go over it again. Even more annoying, if for example frobnicate calls frobnicate_flarp, which calls frobnicate_larp and suddenly you need to add new arguments to all previous functions in the chain when frobnicate_larp needs to access a new storage, this creates a lot of noise.
Granted, this isn't an enormous issue, but it annoys me enough personally that I wanted a solution, and this is the pattern I suggest (pseudocode):
#[derive(Borrow,BorrowInfo)]pubstructStateMachineView<'a>{ctrl:ViewMut<'a,Ctrl>,thing:ViewMut<'a,Thing>,rigid_set:UniqueViewMut<'a,RigidBodySet>,b:View<'a,PhysicsBody>,thingy_two:View<'a,ThingyTwo>,}structStateMachine<'a>{ctrl:&'a mutCtrl,thing:&'a mutThing,rigid_set:&'a mutRigidBodySet,thingy_two:&'a ThingyTwo,b:&'a PhysicsBody,}pubtraitEntityView{fnupdate_entity(&mutself);}impl<'a>EntityViewforStateMachine<'a>{// Entry point for stepping the state machinefnupdate_entity(&mutself){matchself.ctrl.state{Ctrl::State1 => {self.florp();self.flirp();}Ctrl::State2 => {self.frobnicate();self.flirp();self.thingy_two.take_damage(1);}}}}// Helper methods that all have access to the storage for this entity, to// organize code betterimpl<'a>StateMachine<'a>{fnflorb(&mutself){// florbing ...}fnflirp(&mutself){// flirping ...}fnfrobnicate(&mutself){// frobnicating}}pubtraitMagicView<T:EntityView>{fnupdate_view(view:Self);}impl<'a>MagicView<StateMachine<'a>>forStateMachineView<'a>{fnupdate_view(state_machine_view:StateMachineView){letStateMachineView{mut ctrl,mut thing,mut rigid_set, thingy_two, b } = state_machine_view;for(ctrl, thing, thingy_two, b)in(&mut ctrl,&mut thing,&thingy_two,&b).iter(){StateMachine{ ctrl, thing, thingy_two, b,rigid_set:&mut rigid_set }.update_entity();}}}
I am thinking of writing a proc-macro that automates this to something like
#[shipyard::magic_view(StateMachine)]#[derive(Borrow,BorrowInfo)]pubstructStateMachineView<'a>{ctrl:ViewMut<'a,Ctrl>,thing:ViewMut<'a,Thing>,rigid_set:UniqueViewMut<'a,RigidBodySet>,b:View<'a,PhysicsBody>,thingy_two:View<'a,ThingyTwo>,}impl<'a>EntityViewforStateMachine<'a>{// Entry point for stepping the state machinefnupdate_entity(&mutself){matchself.ctrl.state{Ctrl::State1 => {self.florp();self.flirp();}Ctrl::State2 => {self.frobnicate();self.flirp();self.thingy_two.take_damage(1);}}}}// Helper methods that all have access to the storage for this entity, to// organize code betterimpl<'a>StateMachine<'a>{fnflorb(&mutself){// florbing ...}fnflirp(&mutself){// flirping ...}fnfrobnicate(&mutself){// frobnicating}}
Again, this issue is perfectly solvable without resorting to such a pattern, and a new proc-macro, but I feel like this pattern allows for much faster and easier iteration on systems, especially when you know that they can get quite large and will change a lot.
Let me know if this is something you're interested in having in shipyard, and I'll keep that in mind when creating the proc-macro so that I can create a pull request against master later.
Do also let me know if you are aware of a better and easier pattern that accomplishes the same goal, which would save me some work :)
Unresolved issues
Naming bikeshedding
Common processing that happens before the loop, sometimes you want to do something like this:
@leudz could respond to this more eloquently, but I have an idea that might work or it might be an antipattern.
For what you are doing, I would consider simply making each helper system function a separate system fn that has an initial guard check to check if Ctrl is the right state.
So, now, all your enum values effectively act as a tag-like/state-like component (e.g. in Minecraft you could have a 'Following { None, Player(PlayerId) }')
Alternatively, though, perhaps the variants of this enum are promoted to individual components, so the actual system definition filters to the right entities automatically.
I think the approach to implementing methods into a borrow structure looks convenient, but I wonder if there are negative implications to not keeping all the systems flattened more.
I've been working on a simulation project that heavily utilizes shipyard (great project!) and have come to a point where I struggle a little bit with code organization in quickly evolving complicated state machines that access ECS storages. For splitting up the code into more easily readable bits, I previously simply used functions, like so:
This can work fine, but it introduces some annoying problems. First off there's a lot of particularly annoying (to me) boilerplate with the loop and placing the
&mut
and&
borrows correctly. Secondly, and most importantly, it requires manually passing each borrow to the various helper functions, introducing more boilerplate again. It also becomes more difficult when the system gets big, and you suddenly realize thatflirp
,frobnicate
, andflorp
all need to suddenly access 2-3 new storages, and you need to go over it again. Even more annoying, if for examplefrobnicate
callsfrobnicate_flarp
, which callsfrobnicate_larp
and suddenly you need to add new arguments to all previous functions in the chain whenfrobnicate_larp
needs to access a new storage, this creates a lot of noise.Granted, this isn't an enormous issue, but it annoys me enough personally that I wanted a solution, and this is the pattern I suggest (pseudocode):
I am thinking of writing a proc-macro that automates this to something like
Again, this issue is perfectly solvable without resorting to such a pattern, and a new proc-macro, but I feel like this pattern allows for much faster and easier iteration on systems, especially when you know that they can get quite large and will change a lot.
Let me know if this is something you're interested in having in
shipyard
, and I'll keep that in mind when creating the proc-macro so that I can create a pull request against master later.Do also let me know if you are aware of a better and easier pattern that accomplishes the same goal, which would save me some work :)
Unresolved issues
UniqueStorage
The text was updated successfully, but these errors were encountered: