Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

UI - game communication architecture

Discussion in 'Scripting' started by alexverdumiralles, May 26, 2018.

  1. alexverdumiralles

    alexverdumiralles

    Joined:
    May 26, 2018
    Posts:
    9
    Which are the best approaches when it comes to the architecture for user interface-game? I'm having hard times to find a good solution regarding to communication between the game and the UI.

    Right now I have a UIManager that has references to all the needed elements in the UI, a GameController class has a reference to the UIManager, and the game entities has a reference to the GameController and the GameController has a reference to the game entities. So when the player has received damage, the GameController is notified and it tells the UIManager to display the new health value. In the other direction, when the player presses the "Heal!" button, the button has a reference to the UIManager, then it notifies the GameController and the GameController triggers the action in the player.

    This approach is working for me but my games end up with a GameController that is kind of a God-like class that has a reference to almost everything and all the game logic needs to go through it.

    Given the use case presented before, which would be the best game architecture to get a simple but extensible solution?
     
  2. It is really matter of preference and subjective perception of maintainability. I strongly prefer this architecture for communication between systems and using for coupling. So far it serves its role.
     
    Circool, Doug_B and alexverdumiralles like this.
  3. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    In the examples you described, heath and the UI to represent it are common. If your health class/method is called when taking damage or healing, the UI portion can be notified by an event.
    If other things happen as a result, too, then they can be added to the event, also.
     
  4. alexverdumiralles

    alexverdumiralles

    Joined:
    May 26, 2018
    Posts:
    9
    So you suggest firing an event when user presses the "Heal" button and to have the player suscribed to that event, and to fire an event in the player when it is hurt and to have the UI suscribed to that even. Am I right?

    I don't think this would be the best approach. Let me explain myself:
    -Code logic is hard to understand and to trace back when execution jumps from one place to another because of events and callbacks.
    -There aren't any abstraction layer or separation between the UI elements and the gameObjects were actions are triggered. I would like to have a less coupled system.

    Did I understand you correct about your solution?
     
  5. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    If you prefer the logic is more in one place, that's up to you.
    The second part of your first sentence was what I was saying, however for the first part I was thinking that the heal button heals the health, the health script fires an event which the UI is subscribed to.
     
  6. alexverdumiralles

    alexverdumiralles

    Joined:
    May 26, 2018
    Posts:
    9
    Thanks for sharing the video! The guy shows some really nice techniques that I didn't ever think about!

    The part about converting attributes into ScriptableObjects solves mostly part of my problem.
    But I'm not sure about the events part. As I said in my previous message, I think that code that abuses on events and callback makes pretty hard to trace the code logic when reading the code. And so that it makes hard to understand the code.

    Is that the approach that you use when you want to communicate to your game entities events that are fired in the UI?
     
  7. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Are you referring to normal events or Unity's events, when you say it's difficult to understand the logic?

    For the latter, I partially agree. It can be quite annoying to get an overview, or when something needs to be linked again due to lost references and such. It is also difficult to spot where methods are used, since none of the current IDEs tracks the methods that have been linked via inspector.
    I personally avoid Unity Events even though they can help a lot to prevent coupling and reduce programmatic initialization / subscription etc.

    For the former, it strongly depends how you use events. Even though different entities might be interested in an event, you should probably prevent many of them from direct subscription, as you said, it tends to couple a lot of systems together.

    Let's take the UI example.
    In my opinion, actions raised by UI controls should only be forwarded to some sort of controller or communicator that sits in between the UI and other parts of the application.
    I usually use the controller as some kind of middle-man and it serves as implementor for application logic and control flow. I tend to see this as the single point where multiple systems come together, yet only the controller knows how they work together - so it knows the application logic and uses the available systems.

    That is, if I had a UI control for healing my character, neither my inventory, nor my character would directly subscribe to it. The reason is that first of all, state needs to be evaluated - can I actually heal myself in the current situation, do I have enough potions to heal myself, which is the preferred order of execution for the actions that have to take place?

    Neither of these checks and management decisions belong into the UI, neither of these checks belong into the inventory and strictly speaking, neither of those belong directly into a character class, as that would imply it knows the envinronment and all the state of the game.

    Instead, the action is forwarded to that controlling component. It knows the systems, the other components and can perform various queries and checks befoe it removes a healing potion from the inventory, applies healing effects to the character and triggers various updates.
    As mentioned earlier, it knows how the systems have to work together - it's the implementor of (more or less) application specific logic, while the others can stay highly re-usable.

    That's the theory though. It requires more seperation and leads to more source code... but in the end, it'll help to maintain the application and it also simplifies introduction of news systems, because the controller is often the only one who's concerned about the new stuff.
     
    Last edited: May 26, 2018
  8. Doug_B

    Doug_B

    Joined:
    Jun 4, 2017
    Posts:
    1,596
    I have to say, I have watched that video before and it is fascinating.

    However, I do have some thoughts about it. For example, let's take this point in his demo where he shows the interaction between a HP variable and a UI HP monitor. He then shows the code for the monitor at this point in the video. Notice here, that there is a public variable that has to be set in the editor to link this health UI to the health variable.

    Now of course, this isn't at all surprising- the value has to come *somehow* from *somewhere*. So, to a certain extent, whether you use this Scriptable Object approach or an event driven one, as suggested by methos5k, you are dealing with plugging parts of the system correctly together. That 'plugging things together' then becomes the tricky part to get right.

    My point is, the choice of SO or events may be as much personal preference as anything else and relies on the developer handling them, and the overall system architecture, appropriately.

    That is exactly the idea the chap is discussing at this point in the above video (the separation of components). He has obviously chosen to use SO, but I guess yours could just as easily be a standard mono behavior, right?
     
  9. Well, it matters of habit and preferences. I believe the key is the proper naming convention. If you see an event fired and it has some weird name which has nothing to do with the intention in the current context, you will be derailed.
    But if you see the same event fired with a proper name you will know that on the other end something will accept this event and act accordingly.

    The event-based architecture is great because you need to build it once. If you change either side of this relationship without modifying the contract between them you will know that your change won't affect the other side.
    If you worry about the decoupling than you do something wrong.

    Yes, you won't be able to follow the path without looking for the place where you subscribe for the given event, but it is just minor inconvenience on the long run IMHO.

    The key is to not to try to understand the whole code at once, the event call is a substitute for a block of code which is a machine which takes the arguments and do what it was designed for. If that's not happening, you take a look inside the box and check if the given arguments are correct or not.
    This is one of the corner stone of the TDD. You test your machines separately, in isolation with all of the possible inputs and check if it do/spit out the correct things.

    But you have to design it and your tests correctly. Once.
     
  10. Yepp, it's true. The big difference in my book is that the events in general are creating garbage all the time.
    While these 'looks like decoupling but in reality it's not'-solutions are great because they give you the feeling of decoupling, but at the end it's just you don't do it in the code, you do it in the editor.
    It's always easier to set up something in the editor than to do it in the code and if you forget something you can see it as soon as you check it out while in the code you have to remember that you forgot to insert another line of code which wires up some event.
    Not to mention that if you provide the proper building blocks, non-coders can wire up a system almost entirely alone without coder's help.
     
  11. Doug_B

    Doug_B

    Joined:
    Jun 4, 2017
    Posts:
    1,596
    I agree, this part is definitely true if you are part of a team and providing other game writers with tools rather than writing the entire game yourself as a developer.

    Must admit, I'm not sure about this statement, unless I am misunderstanding you? :)

    Say I have a script that contains a reference to another object. That other object has to be given in the Editor when I add it to a GameObject. But if I forget to do that, I won't know until the point in runtime when it is used and I just get a "null reference" exception? Or is there a way in the Editor to say "show me all references that I have not set"?
     
  12. Well, it's not perfect solution, but it can be used: Although it is not documented, ScriptableObjects have the OnValidate callback on them. Which means if you create a ScriptableObject instance in the editor and you change something (it is unavoidable at this point), the OnValidate mehtod will run so you can write basic checks and you can throw Debug messages or whatever you like. Unfortunately it does not work if you just create the SO instance and leave it alone.

    Or you can always create a general propertydrawer for it and just colorize the background where you have null.
    What is important, that it is more likely 'tool programming' than 'game programming' :) But that's the point IMHO.
     
  13. Doug_B

    Doug_B

    Joined:
    Jun 4, 2017
    Posts:
    1,596
    Ah, ok, that's an interesting approach. But, of course, it does require you to be looking at that object in the inspector. :)

    It strikes me as odd that there does not seem to be a menu option in the Unity Editor that says "Go through all my scene objects and list out all editor visible references within them that are null". Or at least I cannot see one. Neither can I find something like this by Googling. Yet it would seem like such a basic thing to want to be able to do?
     
  14. Well, the 'go through all my scene objects' wouldn't work in case of ScriptableObjects, since they aren't scene objects. They aren't in the hierarchy.

    Usually when you create a new instance of a ScriptableObject you will see it in the Inspector and you will start to fill up the data. I don't think it's easy to miss something at the time unless you intentionally just create multiple objects and don't care about the data in them.
    But this is true even if you're creating a bunch of GameObjects and just leave them alone.

    Using ScriptableObjects is great because it is the same experience as using the hierarchy and GOs in general so it is familiar for everyone on the team.
     
  15. alexverdumiralles

    alexverdumiralles

    Joined:
    May 26, 2018
    Posts:
    9
    That's exactly the same approach I'm using. The downside that I see is that for a big game your GameController end up being a huge God-like class.

    Anyway you explained how you handle UI input, but how you do it for changes in the game that should be visualized in the UI? Say the player is shooted by an enemy and the UI player's health bar should shrink.
    Do the player notify the GameController and it changes the interface?
     
  16. Doug_B

    Doug_B

    Joined:
    Jun 4, 2017
    Posts:
    1,596
    My understanding of Suddoha's post was that he is using the separation of responsibilities approach that you mention, in which case presumably the data would flow as stated: player -> controller -> UI.

    Another issue to consider is "how" that data hops across. There are, I think, 3 main ways:
    1. The UI component has a method, let's say
      UpdateHealth(int newHealth)
      . Something (either the player directly or some other intermediary) calls that method setting the value.
    2. The Player has an event, let's say
      HealthChanged(int newHealth)
      . Now something needs to subscribe to that. Again, that may be the UI directly or another intermediary that would, in turn, let the player know (again using one of these 3 methods).
    3. The Scriptable Object approach. If you want to cut straight to it, it is here in the video.
    Each of these 3 alternatives will have their own merits, drawbacks and/or concerns.
     
  17. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    That part is indeed interesting, I gotta watch the full video.
    My first thought: it reminds me of data/property binding and honestly, I haven't seen this before on such a granular level ("per value").

    It may sound as if it was the exact same idea, but the approach in the video is a little bit different.

    Here are my thoughts:
    It has some great benefits, as you can quickly extend the systems and the value can be observed by literally anything that needs the value for its logic. It's also flexible with regards to exchanging the systems that need to observe the value, as you only have to link it to the new system.

    However, the idea is pretty much a variant of using well-defined [tiny] interfaces in combination with programmatic dependency injection in its most trivial form (constructor and / or setter, or even automatically using DI containers). And - no disrespect for SOs, i admire and love them - interfaces are still way more powerful than SOs when it comes to flexibility and platform- / environment-independent modelling.

    Think of it as if you wrote a normal console application. Everything starts in Main. If you had a component that is - similarly to Main - responsible for setting up [parts of] your game, you'll have additional advantages to the one shown in the video:
    • The systems can accept interfaces, they're no longer bound to a concrete SO (base) class. There's no limitation as to WHO implements the interface, where it comes from and the etc... It can be an SO, it can be a player-class (which can internally access an SO, or a field of a primitive type, or a character-stats class that is used in favor of composition to build up the player - It simply doesn't matter, and you could even write a game that uses a different engine / SDK
    • You can write more passive components, i.e. they do not "do what they want" and can be part of your core systems, as they receive instructions instead of acting by themselves.. the latter tends to harm seperation of concerns, especially when you quickly add fields for hooking up certain other components that you need to access (which might only be required for a single game)
    • You won't lose a lot of linking if something messes up the setup, because the setup components do the heavy work for you... it won't change the way it works, it knows and references all the systems in a single place
    • the SO-approach can still be used, the linking is just done in a different place (in a better place IMO), for example in the player class where it belongs to (as an implementation detail)
    • You still have more control about how things work together
    Let's take a different example:
    Some kind of configurations for the application, let it be localized text, default settings, path settings etc.
    One developer might like to configure all that stuff in an SO. Someone else likes to retreive the information from a file. Yet another developer wants to pull the information from a cloud on demand.

    A component that requires the that configuration could simply have an exposed inspector field... let's link a SO and it's done. Suddenly, you want to support customization, the consumer's implementation does not satisfy the needs, as it only knows that SO (which is currently an implementation detail). Something has to be changed, and I've seen myself (and others) starting to mess around with the component that wants to simply "consume" the configuration.

    Let's take the localization example with SOs: all the components now need to react to that action. If they don't receive the new values from the update, they'll either need to locate it themselves or they have to query a huge-SO that knows all the translations and cultures ... not very memory efficient.
    Alternatively, the SOs could load the information on the fly to consume less memory, but wait... Why should any SO instance do that? Why should a type be able to re-populate itself? How does the type know where to get the currently selected information from? It's not it's concern if it serves as a data-providing type (think of PODs).

    Using the approach described earlier, we simply allow to accept an IXYZConfiguration. A setup-component is then responsible to get that configuration somehow from somewhere (e.g. for lolcation, a LocalizationManager or a LocalizationProxy), and the consuming components don't change at all... and they do neither care what concrete type or base type it is, because it's going to get an instane that satifies a contract (the interface).

    Disclaimer:

    I understand that most of the time, this is too much effort and all the ways of doing things in Unity are overwhelming and often easier. I usually find myself spending too much time thinking about architecture and I often end up realizing it's over-engineering.

    However, it's not that I want to finish something as quickly as possible. For my personal project (which turned into a framework), it doesn't matter when I finish it, or if I ever finish it. I'm still far away from having something I could actually use in a real project.
    Though I got other projects that I contribute to, that's where I'm simply doing my tasks just like anyone would with realistic and pragmatic code structures.

    The important take-away for me (of all the theory described above) is the intellectual challenge to find solutions, to see what's possible and how everything could be done. Doing this for many years clearly changes the way of thinking and how you start to appraoch specific problems. It's satisfying and I love spending my time with it.
     
    Last edited: May 27, 2018
    wickedwaring likes this.
  18. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Well, of course there wouldn't be just one controller / manager. You can write multiple ones for various subsystems that require setup and need to interact with each other.

    But the important thing is that it all comes together in certain places, without coupling them directly. The systems can report events that need to be processed. And they can be updated. It also reduces redundant and superfluous polling in Update methods, which again allows to turn some of the systems in non-Unity specific implementations when they're way more "passive".

    One of the options listed by @Doug_B . Or you do it like guy in the video, and set the dependencies programatically. Then you'd only have the controller as a dependency manager (which could even be replaced by advanced DI-Frameworks).

    An additional advantage of using a third component is that you can bring different modules and systems together, potentially being developed by different people. Neither of both can /should be changed to directly support the other.
     
    Last edited: May 27, 2018
  19. Well, SOs are practically script components without game objects saved as assets. Why components take care themselves? Because of separation of concerns and responsibilities exactly.

    If you have watched the video you saw the 'FloatReference' and the *Reference objects. In your case, you create a TranslationReference with a third option:
    - Constant
    - StringVariable (Or TranslationVariable if you will)
    - TranslationValue

    In that TranslationReference you just load up the proper translation from the translation system or whatever you're using.

    And voila, you don't have to think about it anymore, the designers can create a new TranslationReference for every translation they have and they can work without engineering help. Which saves a lot of time again. Also they can test things out without changing things all over the place (just change the value to constant and they can test what fits in the field practically live).

    --

    In my opinion, the greatest thing about this pattern or framework or whatever you want to call it, that this is usually design time coupling. You save these things with the asset.
     
  20. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Well yes, it's a great observer pattern, as mentioned earlier.

    However, if a user changes the language, updates have still to be made. The one way or the other.

    Also, it still involves a lot of linking that could potentially break. And you have to set up the connection between the Translation References and the translation system, or whoever is responsible for updating the localized text when the language is changed at runtime.

    Next, what's the solution to updatable elements? A state label, a dialog-system's labels/buttons for the message + answers? Are you going to issue these highly contextual updates to the translation system?
    Or are you going to set these directly? Who's then responsible to take care of using the correct translation?
    How would that look like?

    Personally, I prefer to tell my view/form to update itself with the information I provide. The one who issues the updates could then still use that pattern so it's no longer "baked" into the UI, it's no longer required for a UI implementation to rely on that pattern.

    I agree that it appears old-fashioned, but in my opinion it seperates the concerns a little better.
     
    Last edited: May 27, 2018
  21. Of course, it is not the concern of this specific piece of stuff.

    This pattern is very good because changing the code is expensive (in terms of developer's time), but playing around and building stuff out of these building blocks is cheaper (designer's and world builder's time).
    Mistakes could be made either way, it does not protect you against that. Or at least not completely.

    Again, it is not the concern of these piece of building blocks. If you want, you can build a similar piece, to track the UI change (the selection of a language), which modifies a SO's field, on the OnValidate you instruct the translation engine to load up the new language and drop the old one (or whatever) and fire an event to update the currently shown elements on the screen. Or something like that.

    I personally like to leave update to the piece of element which should update.

    Like I tell the label:
    "you should update yourself"

    - so the label reads the linked TranslationReference and whatever it finds in there writes in the text.text.
    - the TranslationReference checks if it is a const or a TranslationValue, if the later, it reads from the translation engine

    This way the update event should not carry information about whatever should go in the text at the end, that is loaded up later through these references.

    ---

    Now, obviously if you're (or I'm) a lone developer who does everything, having a system like this is not _that_ important (although it is convenient), since you jump to the code and just replace the read with a string to test things around.
    But working with non-coders can be time consuming, to have a system like this, which allows them to wire up things alone and at the same time allow them to play around with values (essentially testing the design) is very important and time-saving.
     
  22. Doug_B

    Doug_B

    Joined:
    Jun 4, 2017
    Posts:
    1,596
    I had exactly the same thought when I was watching it. :)

    But one concern I have about that (and if I recall correctly, the chap mentions this briefly at some point in that talk) - is this explosion of objects that a more complex project will end up having. The problem then becomes one of storage. Now you have to be very careful about your organisational structure for where you put all these myriad classes.

    It is certainly an interesting idea. I'd be keen to try it out in a small project - I think that's the best way to get a real feel for it.

    This seems to be a valid point and possibly a key driver for using an approach such as this. I guess my worry there would be when it comes to debugging their system creations. :)
     
  23. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    No changes in code are required if you move that pattern outside of the UI.
    The benefits of that pattern do still exist, designers can still work without additional coding effort and your views stay compatible to any pattern you use in the controlling class. The great benefit is that the UI knows everything it actually needs to know: it'll receive the data from a component that knows how to gather the information. One less concern for the UI, more flexibility, pattern-agnostic.

    OnValidate is one of the message triggered in the editor only.

    Another component for localization wouldn't be necessary. The UI could be updated via the observable when the language changes. But that's not what I meant.
    I was referring to other kinds of updates. Suppose you have a state label. You want to display that new state, a new information.
    A view that accepts displayable information is easier to update than trying to work around that with various kinds of additional components that you have to speak to, which then update a translation reference, which in turns needs to speak to the translation system, which then triggers the final update again.


    This contradicts to what you said earlier, seperation of concerns.
    According to your post, you would only instruct the view to update itself and only the view knows how to do that.
    The view is now a black-box that has to speak (indirectly) to other systems,in order to request information.

    In other words, it feels odd to tell UIs to update themselves without any further information.
    It would feel less odd if it solely listened to events triggered by the observed instance. Then again, there's still the question how you manage values that represent different information.
    You'll need to set the information somehow, somewhere.

    One option is now to add an overload to allow alternative values to be set... well, that's basically what this pattern attempted to avoid.
    Another option is to bind the references to other components as well. Now the pattern spreads out and becomes an implementation detail of all your components.

    I'm not saying it's bad pattern. It's a smart and neat system that fits nicely into Unity.
    However, I personally like to be independent from such strong and intrusive patterns that are specifc for one engine / dev environement.
     
    Last edited: May 27, 2018
    Lurking-Ninja likes this.
  24. mrgarcialuigi

    mrgarcialuigi

    Joined:
    Feb 11, 2015
    Posts:
    12
    @Suddoha I am thinking the architecture of my game very similar to the way you described, but I am also trying to incorporate some ideas from Ryan's presentation.

    The mid guy(controller), knows how to use subsystems and UI to compose a certain behaviour for the game.
    What is your opinion on separating this controller into two, the part that handles logic (talks to subsystems and data), and the part that dictates how presentation/UI is handled for that controller?
    Its not clear in my head yet if there is any advantage on separating or not.

    Another thing I was thinking is maybe those controllers instead of directly talking to UI, it maintains a presentation state, which is read by whoever that will actually describe how UI will behave.

    Thing is, these high level controllers have logic and presentation working together, and the two concepts might be so dependent that separation makes no sense.
     
  25. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,517
    mrgarcialuigi likes this.
  26. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    (Sorry for the late reply)

    The latter sounds like the presentation model approach.

    The idea is similar to what can be seen in some videos about Scriptable Objects and similar to what @Kurt-Dekker has implemented with his DataSacks package, except that these approaches break that model into many individual objects. So you don't have a "character stats presentation model", but each of the stats as their own object that you can compose and link individually to all the places you need.

    Both approaches keep the logic for presentation state out of the view, they're somehow linked but the view only knows that abstract model (similar to view models in MVVM, although you craft specific models for each view to limit access to the optimal minimum).
    Neither does the view need to talk to specific models or systems for data, nor does it need to attempt to figure out whether a functionality is available / an action can be taken, instead the view observes (either by polling or event subscriptions) the state of data and additional values (which depend on data + applications state) that is kept in these specific objects.

    You're right when you say that this could be an additional seperation.
    Suppose you write self-contained and highly decoupled domain models and modules. On their own, these parts can only supply the data they're aware about, but in the end you'll need that additional application layer that takes all of them into account in order to make the final decisions, as only the application specific logic knows what exists in its universe.
     
    Last edited: Apr 19, 2019
    mrgarcialuigi likes this.