Search Unity

Composition architecture: combining responsibilities for the sake of code readability?

Discussion in 'Game Design' started by BIGTIMEMASTER, Aug 21, 2022.

  1. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    edit: condensing original quesiton:
    In composition design, we have a bunch of entities in the game world and each entities behavior is determined by its components.

    If something major changes in the game, for instance in a multiplayer game you go from Pre-Game state to In-Game state, how are you disseminating information to all the entities?

    Do they watch the game state or some sort of information broadcaster, and then the components determine how each entity reacts?

    Does this mix at all with a more top-down approach? E.G., a manager finds entities and directs them what to do?

    I can see either way working, but my preference is to go with top-down because then my code is generally more centralized, meaning that in the future when I forgot how something worked, I don't have to do as much hopping around to figure it out.

    Are there other methods for dissemination of information? Would you consider it bad design to not have consistency (i.e. mix top-down communication with each-entity-is-its-own-boss)? Or you consider that normal?
     
    Last edited: Aug 22, 2022
  2. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Yes. My general rule of thumb is that "every object is responsible for listening for all events it cares about, and responding to them appropriately".

    For any "large" object that's fine, but sometimes there are groups of small objects which all have to behave the same way. For instance, I don't want every single UI widget each listening for game state to know when the game has paused. So...
    Yes. My rule of thumb here is that in cases where manager objects do exist, then they are responsible for listening for any events which are likely to effect the system it is managing on behalf of its objects. So, for example, instead of every UI widget or every UI screen listening for a pause event, the UI manager does and then makes appropriate function calls to its screens / widgets. If you think through a few use cases this one hopefully becomes intuitive, because it's less work and fewer errors to implement "do this when the game is paused" in one place than in many.

    If something only effects one object, rather than the system, then this does not apply. For instance, my health bar likely listens for OnHealthChanged on the player object, because that only effects the health bar. Routing that through the UI manager is just making more work, it doesn't make anything clearer, etc.

    - - -

    The key to all of the above is having clear delineations between your systems. Otherwise it's really hard to tell the difference between "this impacts the X system" as compared to "this is just the Y object". There'll often be a bit of judgement call involved, and you don't have to be mathematically precise with this stuff, but if your relationships and responsibilities are spaghetti then you've no basis at all to make consistent decisions on.
     
  3. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    yeah the more i think on this, it seems like it may be best to just stick with this rule and if I go against, it would be for a case like you've described. Actually, communications with the different UI elements is exactly the scenario I was thinking about.

    Since UI elements are kind of like, nested references within references, I want to communicate with them in a generic way. So if I have an empty container that UI elements can go in, then I can just communicate with that container.

    It makes sense that something like a progress bar would just handle it's own situation. But perhaps the visibility of the progress bar is determined by the container (or manager), as that would be dependent on the game state/player state.

    So, even though its a tiny bit more verbosity, I think I will go ahead and set things up this way, so that UI is not directed by other states, but has it's own manager and listens for it's cues.

    In my current project I think it's not 100% needed, but it seems like a more "ready-for-anything" approach that may be good to be in a habit of coding that way, especially when i start a new project and I'm not sure how things might end up.
     
  4. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    @angrypenguin

    your idea is working a lot better. I am having the UI manager listen for game and player states, and then manage its own state itself. I think this actually simplifies things a lot, which is opposite of what I expected.

    I actually even further split UI into two different states - there is state for HUD and state for Menus.

    It is code spread out in more places; however, it makes it so that I never have to ask complicated questions about responsibility and who ought to direct who and all of that. There is just like two broadcasters and everybody else is a listener.

    I thought it might be annoying to track down the effects of a cause like game state changing, but it's not really. Just find references to the event dispatch and that's all (not sure if this is engine agnostic term or not, but it's basically an event that broadcast itself and other classes can subscribe to it.)

    So overall, much simpler, easier to manage code.
     
  5. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Yeah, I can almost treat things as if they're their own, smaller programs. Each event handler is an "entry point" which leads to a fairly straightforward execution flow relating to reasonably clear responsibilities.

    The first time I tried this I was surprised at how much simpler it made things in practice. Because you're right, it seems like having all of these different bits and pieces which have to communicate with each other would make things more complicated, but it does the opposite.

    I suspect that the reason for that is as follows:
    1. Once your program reaches a certain size, it doesn't fit in your active memory regardless of its architecture. "More simple" is moot if it still isn't "simple enough".
    2. The clear boundaries made by following the above approach means that we don't have to keep the whole program in our head. At any time we only need to care about one fairly small part of it.

    Whenever I'm debugging something that's been developed in this style I generally have two things to look at.

    First, "Is the correct execution path being hit, and passed the correct data?" This is super easy to answer. Put a breakpoint at the event handling function and see if it gets hit and what it's being passed. The neat thing is that this cleanly divides your problem space. If the answer is "no" then you know the issue is in the calling code and exactly where to investigate next. If the answer is "yes" then you know the problem is within the current system's boundaries, and proceed to:

    "Where is its behaviour deviating from expectations?" This is a broader question, but if the boundaries between our systems are clear then we've got a pretty focused area in which to look for it, and if our responsibilities are clear we've got pretty well defined things to look for.

    When I was less experienced my debugging often meant wracking my brain as I stepped through sprawling code structures which jumped from one part of a program to another. In fact, step debugging was often not useful, and instead I'd have to put logs all over the place and then look for patterns in log messages. These days, I generally enable the step debugger, trigger a known error case, and within two or three runs know where the issue is. Sometimes it doesn't even take a whole run.

    Anyway, that's all a roundabout way of saying that it's not about making the whole program simpler, it's about arranging the necessary complexity of (some) large programs into simple, highly manageable pieces. I could ramble far more, but I should do some game dev... ;)
     
    Last edited: Aug 23, 2022
    BIGTIMEMASTER likes this.
  6. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    No need to ramble :) (unless you want to, of course) I think I am understanding your thought process very clearly now. You have given very succinct way to describe some notions I've been developing.

    When I first learned about how you can wrap stuff into an object, and I started doing that with a state pattern to handle input, that helps me so much because it accomplished this idea of delineating "problem spaces." That's a great way to describe it.

    I actually simplified a bit though and went to just switching on some enumerators, because taking your advice here I felt that my "problem spaces" are already so clearly defined, the extra steps of jumping into objects is no longer needed.

    I agree also that the great thing here is that pretty much the only thing I need to remember is a super simple, general information flow (in my case, game state broadcast and player state listens, then player state broadcast and everybody else listens to that).

    Each manager (camera manager, UI manager, etc) just takes the player state and maps that to their own states. And there are a few little edge cases, things that happen without regard for any states - like I have a function in UI manager to directly request an alert message to be drawn on the HUD, but if 90% of the code is following a clear pattern, those edge cases stick out easily.

    Well, anyway, thanks a lot for sharing, it's been a huge help getting this code to be waaay more manageable.

    P.S., aren't you working in education, IIRC? If you ever have time/interest I think you might make some great book/article/youtube videos. There is a ton written out there about design patterns and such but I find almost all of it hardly understandable at all. Seems like a rare talent to be able to explain things in simple, easy-for-humans-to-understand way.
     
  7. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Yeah, I use simple enums a fair bit. If the overall state behaviour is small enough to reasonably fit in one object (or would be expected to fit in one object, e.g. "what is the state of this audio player?") then they're a great way to go. I lean towards objects when individual states have high internal complexity, or when I need states to be highly configurable.

    Yeah, I've thought about it on and off a bit, actually. If time were permitting (i.e. if I weren't neck deep in a project I really want to finish) it's likely something I'd just start as a passion project. As you can see, I love talking about it, and from what I can see there are some gaps which I may be able to fill quite well.
     
    BIGTIMEMASTER likes this.
  8. Kreshi

    Kreshi

    Joined:
    Jan 12, 2015
    Posts:
    445
    Using events is a great way to create clean communication interfaces between objects and it helps reducing dependencies. However it's not something you should abuse (coding a callback hell). I like to use events for separating systems from each other (like for example with UnityEvents) or when functions have to wait for something (like a download, using Actions or delegates to call a callback function when the download finishes).

    Regarding your HUD, having a UIHealthbar script on the HUD with a direct reference to the Players Health script and setting the healthSlider.value to health.health in the Update loop is probably absolutely sufficient in 80+% of cases. Creating events + registering + unregistering to them for such a simple task is overkill in the long term (depends on the game and it's complexity though). Having an event fired when the Player dies on the other hand is a good approach because multiple instances (Enemies) or other objects (UI, gamestate,..) depends on knowing when this happens. Polling the event with if(health.health <= 0) in too many scripts would be very annoying fast so in such cases it makes a lot of sense to use concrete events instead.

    In general it makes a lot of sense to go with simplicity over complexity. Keep it simple stupid (KISS) is a great way of approaching problem solving.
     
    BIGTIMEMASTER likes this.
  9. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    yeah unfortunately it'd have to be. Doesn't appear to be much money in education.

    there is definitely a dearth though. I'd be inclined to think "thats just the way things are and people ought to deal with it," but I've chanced upon a few videos/articles here and there and it is a major difference between what you can learn from a professional educator with passion and talent for the craft versus what most of the available resources are: beginners trying to figure things out themselves and sharing what they learned along the way.

    I think there is also an even smaller niche in a person who has done some tiny-team indie dev, in that they understand the sorts of problems we face and how they might be different to somebody with a smaller scope of responsibilities in a larger team environment.

    It's pretty easy to find an answer to virtually any technical question you can encounter but finding somebody who has some wisdom and experience to answer questions like, "how the hell do I manage all this work?" is extremely rare.

    i agree completely. There are some cases where a solution is so simple it's stupid to do anything more complex. But when I first started, I was doing every little thing in random ways and although the game ended up working, the code feels overly difficult to maintain, given how simple of a game it is. I sometimes wish I'd just wrap it up already because I want to move onto the next one but I think the time taken to "fix" the code is paying off. I'll be at a much more advantageous start for the next project now.

    Of course, in learning, I want to try every stupid idea so i don't get hung up and slow myself down too much. But now I got some better ideas how I can form the code-base into a coherent whole, which seems to solve most of the major problems I've faced.

    It seems that consistency is a huge part of simplicity for me. Like, even if the way I communicate between two classes is inefficient and involves too many steps, so long as it is the same principles for each situation-type, that's still okay because I've essentially built an assembly line - only got to grind out the steps in a checklist and profit.

    Of course, an efficient assembly line with the fewest moving pieces to get product made is better - but I just making a point that it seems like the more consistent my coding patterns have, the simpler life becomes.

    I actually wasn't familiar with the term callback, even though it's something I've used quite a bit, especially when I was just getting started. I looked it up and right away understood what you meant by "callback hell", lol. Another problem that I was focused recently on fixing is that if two classes are hard referenced, then that causes one to require the other to load at the same time. In my game it's not actually causing a problem for the target platform but I think its something worth being aware of anyway. Someday I might need to port a game to switch and it'd be better to be in habit of writing more efficient code before then.

    anyway, just rambling. In summary, what I learned here is "combining responsibilities might make some portions of the code more immediately legible, but it makes it much harder to reason about because it forces to you start asking questions about responsibility that never have a simple answer."
    So it's better than to keep that question of responsibility stupid simple, even though that means spreading code out.
     
    Last edited: Aug 24, 2022