I have a number of units (mostly self-propelled) that take orders from some kind of supervisor system. Depending on order a unit executes specific chain of steps, with the majority of them containing high level movement tasks (i.e. “engage target”, “land”, “dock”, “withdraw” etc). Since all of them need same data (locations/rotations calc, collision evasion if necessary, passing low-level orders to unit’s propulsion system etc.) - I keep them all in one system with many methods. Problem is that this system is growing I wonder how I could split it effectively. I tried to do it by having different systems for different unit types - but that was not that good idea, since many behaviors (say, collision evasion) simply duplicate, plus adding a new type of unit means I have to write the whole system for it again. What else I can think of… applying unit state component for every concrete step and process every state in its own system? But there’d be a lot to copy-paste this way too, since I’d need the same functionality (like, “evade collisions while docking” etc)…
Or, having big system with plenty of EntitySets and even more methods is just OK?
I mean, “probably” not ok? I feel like I would need some more concrete information to be sure.
You are right that splitting on steps or behaviors is way better than splitting on unit type, though. Whether you did it (or are thinking of doing it) in the best way or not is hard to say.
Well, I dunno actually. Refactoring fathers teach us that it’s better to have more classes with fewer shorter methods, but they never mentioned ES though. I had to offload one class that had ~2k lines already, and I see my current system rapidly coming to that level too, which kinda makes me wonder. Plus, it makes the system kinda dominating on every other since it has to take into account many things (like, for landing it needs to find a free landing pad, so it needs landing pads set, for unloading cargo it needs to find a proper storage, so it needs storage set etc). That’s just an impression, not real trouble now, so I wonder if it can be OK, speaking in general.
I’ll definitely try to. You don’t know, how many posts here just never appeared, cause I found the problem while was composing the description. It’s like “if you want to understand something, try to explain it to the other”
Just not to spawn another topic, I’m splitting the system now using state components, and the flow looks now like
in core system ( called from update() ):
private void process(EntityId eId, ...) {
...
setting eId state component - say, Cmp2 - for system2...
then system 2 does all the work it’s dedicated to, then it sets next state component, say, Cmp3, and removes Cmp2, and then back in core system I do the check
Hmm and it is always null. I mean when I’m setting Cmp2 - it goes smooth, the other system works, but when it sets Cmp3, core system doesn’t see it. EntityData's got exactly same way in both systems, and update loops look similar. How could that be, if I always refer via EntityId so EntitySet's probably shouldn’t matter here?
Some things to consider… and if I haven’t already added this to my wiki then I probably should.
Most of your systems (probably 90% or more) will be of two varieties:
for a set of objects possessing certain components, we process them every frame to produce new components. A simple physics system(s) is an example of this. Position, Velocity in new Position out.
for a set of objects possessing certain components, we want to process them when they join this set and when they leave this set.
This last one is where Zay-ES really shines because it’s inherent in its data model that this information is available. I kind of think it’s one of its unique traits… and it’s no coincidence that probably the majority of systems are the second case. Maybe a 60%/30% split.
The model system in something like Asteroid Panic or the Sim-eth-es example is a good example of this type of system. When an entity joins, we create the spatial and display it. When the entity leaves, we get rid of its corresponding spatial.
The closer you get to the user, the more likely it is that you’ll be of type (2). The back end will be where most of the type (1) systems live.
Either way, if you find your system does not fall into one of those patterns then it’s either that rare 5-10% or you are on a strange path and might want to get a sanity check from the community.
You were right. It was actually set, but then reset by other pre-check…
This is a debug one, I’m just trying not to break too many things at once, so keeping some old functionality as is and do it step by step.
Thanks for the notes, yeah, I’m working on back end now (that’s where the gameplay is, right?), so most of them are of type (1). I did very tiny changes to your original model state (spatials → nodes, couple of methods for world calc, sort of that) so far…
Not sure if it is kind of a bad pattern, but for example my tiles I manage in one system (like this entity id to model managing @pspeed is doing in asteroid panic). But my control system or my effect system also need tiles. To avoid having a big system I just made this tile hashmap accessible to other system. Downside is the other systems should be in the same thread and maybe the systems need some ordering. Upside all my systems are less than 300 lines most often.
For me this works pretty fine as you can get the app stats easily by classname
As I said not sure if that is actually a bad pattern.
It’s fine that your views collaborate. The tight coupling is worth the benefit, in my opinion.
If it makes you feel better, you could always centralize the spatial creation into some kind of “cache” that all of the systems share so that it’s not like some systems talking to one other system. I do this in Mythruna.
It’s not about thread safety… it’s about process ordering. You’d want the model state to have created the model before the other states try to do something with it.
Else you go with a central cache and track usage… then anyone can access it from any order and you don’t have a bunch systems depending on some other system. It’s a trade off though because that way is more complicated.
I have a one more brand new noob question: Filters. I’ve never seen them used anywhere in examples, and the purpose I assumed was delegitimized at the end of this thread. So how they’re supposed to be used?
Filters are for grabbing all entities that have some component where that component also has a particular field value (or field values). For example, grabbing all of the items in some container (InContainer.parent = someEntityId) or for grabbing all of the objects in some zone (Position.zone = 12).
Okay, I’ve split the code a little bit better (at least most systems within 250 lines, and no one more than 450. Now back to that note about weird null/non-null check that might indicate design problem. If I have, say, two movement systems that are performing different movements (they’re well isolated by their own state components, so one never gives movement orders to entities that are served by the other one at the same time). Now I have collision avoidance task to be solved in both. Collision avoidance system itself exist and isolated by selecting those entities with Obstacle component. It drives avoidance maneuver, removes Obstacle component and standing by until new Obstacle component will be set by collision detection system to that entity. Now, I want other two systems (regardless of which is working with the entity now) to do their work. Since they’re also setting movement orders I don’t want them to conflict with collision avoidance (different kind of trash behavior happens when they conflict, and yeah, it looks pretty fancy in ES, but still). Easiest straightforward way would be to check for Obstacle component. If it is not here, then we’re on our own and can operate. If there’s one, we place continue; and everything is still smooth. Repeating collision avoidance code wouldn’t be any better, obviously. But, it does involve that same check for null/non-null. So I’m curious, what else could I do?
Refactor so that your movement is handled by one system. Without knowing the details of why you have all of the same concern spread over multiple systems it’s tough for me to offer more advice. But that’s what it sounds like.
In other words, you’ve split systems vertically when maybe they should have been split horizontally or something.
Well they were split by unit types, now they’re split by behaviors, but few behaviors imply movements that are split into simple steps and called in according sequential order… but on any of those simple steps an obstacle may appear… combining all the movement back into one system keeping it at reasonable size… probably I need to make my “simple steps” more generic and reduce their number to as few as possible