Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Clever way to use UI; entagled in public functions

Discussion in 'Scripting' started by Arcatus, Jul 23, 2015.

  1. Arcatus

    Arcatus

    Joined:
    May 27, 2015
    Posts:
    45
    I have been dabbling in unity for a while now. And the more I learn the less I know.

    I can display, move, hide and in general manipulate unity 5's UI objects exactly the way I want.

    The "problem" is to populate the UI with information. There must be a better way than using "public static ChangeGUIValue(value) " for each UI element in a GUI controller and then calling the correct functions from all over the place.

    In my game there can be ~20 ships the player can move.

    Currently I have a ship constructor script that builds a ship from prefabs. Something like this:

    LargeFighter (attached scripts; movement, targeting)
    - Child cannon #1 (controlled with targeting script )
    - Child cannon #2 (controlled with targeting script )
    - Child Engine (providing values (trust,etc) to movement script )

    I have a clean nice interface but if I am using a public function I need to do this:

    When firing a weapon ... call a GUI function...
    When using fuel.. call a GUI function..
    When selecting... call (lots of) GUI functions etc,etc.

    If I go down this path it's going to get nasty as it creates lots of dependencies. Suddenly I need to check if a unit is selected every time it fires so I can update the "ammo" field in the GUI.

    Or should this go the other way around? Should the GUI "poll" the selected unit? Then the parent again has to poll each of it's children to get ammo, fuel, cargo, etc. Currently the targeting script has no concept of ammo; It just passes a game object to the turret: fire at this.

    So my scripts will turn into an entangled mess. Where do I go from here? Advice?
     
    Last edited: Jul 23, 2015
  2. A.Killingbeck

    A.Killingbeck

    Joined:
    Feb 21, 2014
    Posts:
    483
    Think MVC - Model View Controller.

    You are right, dependencies are not scalable. Unless you design your code perfectly from the start and never need to add anything(extremely unlikely).

    The Controller is the Users interface to drive the logic. I.e. Input
    The View is what the user actually sees. i.e. the UI
    The Model is how it behaves. i.e. The "logic" which is driven by the Input and drives the UI

    So, events are a good candidate for 'decoupling'. The concept is an object 'signals' that something has happened and interested parties can 'subscribe' to these signals and act accordingly.

    So your input class could just handle input, then send out events when they happen. i.e. MouseDown

    Your "logic" for example, a Weapon class, can subscribe to whichever input will trigger it. (MouseDown?). The logic can determine how fast you fire, how much ammo you have, how much damage it does etc. It will in turn send it's own events for when things happen that the user should know about. i.e. Shooting a weapon with information about how much ammo is left, what type of weapon it is. It sends them and forgets about them, therefore it's completely decoupled from anything else.

    The View would then 'subscribe' to events from your 'Model'. So for instance you could have a : WeaponGUI script which listens to the "Weapon" object logic, and can then display the information passed to it on screen. i.e. Type of weapon, ammo left or whatever else.

    Decoupling is good. Coupling on a global object = bad x2
     
  3. Arcatus

    Arcatus

    Joined:
    May 27, 2015
    Posts:
    45
    In this case each turret instance is very simple;
    If it's activated (by the parent targeting script) it's just a timer that on a set interval does some calculations and instantiates the bullet in an appropriate direction.

    So; If I understand the events right: Each time it fires the turret triggers the eventtype the GUI object listens to. the GUI object doesn't care witch object that created the event; it just catches the information. That would work I guess.

    To only have selected turrets generate events I still I need to couple the turret more tightly to the parent. I guess that's fine. Those are coupled anyway.

    And when selecting a unit; The select script on the parent isn't currently coupled to the children. But that is an easy connection to make. Then I need a "SendEventToGUI" function in all the children that is called when selecting the parent. It that a good method? Or is there a better way?
     
  4. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    Basically you set up a relay station. This relay station is just a list of subscribers for a specific event. The GUI scripts would subscribe to the relay stations in question (say, a station who's sole job it is to tell subscribers that a "fire cannon" event has occurred within the cannon script). When it comes time to fire the cannon, the cannon script sends a signal to the relay station and says "okay, I fired a cannon!" and then the relay station sends a signal out to all of the subscribers (through saved delegates). This way, the GUI isn't constantly checking if a cannon's been fired, and the cannon has no idea which (and how many) scripts care about it firing. Keep in mind that the delegates don't have to be parameterless- you can pass in a self-reference when activating the relay station quite easily so that "which" cannon fired is apparent.

    One thing to keep in mind is the size and function of the relay stations themselves. You can have a tiny single-use event subscription system on each of the objects individually, which only handle their own events, or you can make one larger event manager that's much more scalable (this becomes something like a "message" system or a post-office, rather than a simple relay). It really depends on how much effort you want to put into it now and how much flexibility you need your system to have.
     
  5. Arcatus

    Arcatus

    Joined:
    May 27, 2015
    Posts:
    45
    Cool,

    I look up some event tutorials familiarize myself with them. I'll post back here if/when there are some questions they don't answer.
     
  6. Arcatus

    Arcatus

    Joined:
    May 27, 2015
    Posts:
    45
    I am struggling a bit here:

    The problem is that the action I want to do : changing the displayed text on the GUI can (and will) have multiple sources.

    The way I now understand events is that the manager script that sets them up is also the broadcaster. This is the complete opposite of what I need: I need the event; for example "UpdateGUI_ammo" to be broadcasted from the turret script.

    From what I understand I need a delegate and event the the turret script. And in the hitpoint script. And at all other broadcasters. That is a mess.

    I could make the turret use static functions in the manager, but then I loop around and are to back to where I started. With entangled functions.

    Am I doing something completely wrong since events doesn't really do what I need?
     
  7. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    I think you are misinterpreting. The events should definitely be on the components themselves. This is the observer/observable pattern.

    Can you explain a bit more about your GUI?

    Can you select more than one ship?
    Does it show stats only for the currently selected ships?
     
  8. Arcatus

    Arcatus

    Joined:
    May 27, 2015
    Posts:
    45
    Yea, it's basically like most other top-down RTS games. (well, or will be ;) )

    So, yea, I assume I am misinterpreting. This is not an uncommon problem, so it's probably one of those things are are easy and obvious once I figure it out. I obviosly tried some googling, and I find allot of topics that cover selection, but not how to do the GUI.

    Having the events on the components is exactly what I was trying to avoid; If I make a component that behaves in a different way it is going to have a different script. (but might access the same GUI elements)
    That leaves me with two options;
    1) Attach a separate "component manager" that contains the delegate and event. Similar components will use this manager. The component would reference the manager than again manages the GUI manager. That is not ideal...
    2) Have a delegate and event in every component script. Then I need the GUI manager to register for all of these events and manage accordingly. If I add a different type of component I need to update the eventmanager. This is the definition of entanglement. The concept of a WeaponTurretEvent and WeaponSpinalEvent, etc etc is also very unappealing.

    At the moment is seems much simpler to add a function to the GUI manager:
    Code (CSharp):
    1.  
    2. public static void textToGUI (enum_GUIelement GUIelement, string textToGUI)
    3. {
    4. switch(GUIelement)
    5.  
    6. case enum_GUIelement.name
    7. nameGUIGO.text = textToGUI
    8.  
    9. ...etc, etc...
    10.  
    11. }
    12.  
    13.  
    I then have one function I can reuse, anywhere. And; most importantly: the GUI manager does not have a concept of the components. I only need to change the GUI manager if I change the GUI.

    I really struggle to see the advantage of how events reduces entanglement (in this case).
     
  9. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    You must have some way of determining which unit is selected already. You can tie your event into this.

    Who calls this method? The Components or some other manager?
     
  10. Arcatus

    Arcatus

    Joined:
    May 27, 2015
    Posts:
    45
    Yes, the components.

    If a shot is fired GUI needs to show less ammo, so the turret script attached to the turret component prefab needs to let the GUI know that it now has less ammo

    Similarly, if damage is taken the hitpoints script attached to the armor prefab needs to show it's current health.

    There might, offcource, be underlaying flaws with the entire system here; but the reason I am doing it like this is that I am reusing prefabs as children of the parent ship (model). (and got shot in the leg by the lack of nested prefabs) This method of constructing ship is also quite flexible.
     
  11. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    I'm confused. You don't want to put an event on each component but you are okay with each component needing to call the GUI methods?

    I think this is the wrong way to look at it. You want your GUI to know about the components they represent - because they represent them. You don't, however, want the components to know about the GUI because then they need to change if your GUI changes in a fundamental way .For example, say you wanted to start supporting a different language. Would you go through each component and add different strings? Or perhaps you want to start using a symbol based interface with no strings at all. You would have to rethink your component code again.

    It is better if the GUI depends on the components because it is their job to interpret the state of the components and translate it into a user readable format. However, you need your components to notify the GUI that something has changed. Here is the dilemma: you want the components to communicate with the GUI but you don't want your components to depend on the GUI. What you need is to break apart the communication from the dependency. This is exactly what events are for.

    The components can declare that they have an event available. Then any object can register to be notified of that event. The component doesn't need to know about the types of objects which are registered for the event, just that whatever is on the other end knows how to receive notification.

    I think you are doing things in a smart way. Modular ships sounds like a great way to go - you just need to sort out the GUI synchronization.
     
  12. Arcatus

    Arcatus

    Joined:
    May 27, 2015
    Posts:
    45
    That makes allot of sense.

    I will set up some tests and see how something like that will work out. I'll come back to this.
    Thanks :)
     
  13. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I've approached UI a different way. Not suggesting it's the best, it's developed specifically for my needs and application. But I'm putting it up here as food for thought.

    In a nutshell I use reflection from my UI to find the members of the class I am interested in observing. Then I poll for changes every frame. I also have a custom inspector set up that let's me iterate over all members of all components on a target GameObject.

    It's probably overkill, but it has a couple of distinct benefits. The UI and the component have no dependency on each other. Components require no setting up to be observable by the UI. Retargeting the UI is trivially easy via the inspector. I can use my regular UI for debugging.

    It has some disadvantages too. Polling every frame costs performance. Reflection is slower then direct access. And it took a fair chunk of coding and debugging for the initial set up. But I'm hardly pushing processor limits with my game, so the extra cost isn't that big of a deal.

    I've gone back and forwards over time with the best direction for UI dependency. Having the UI dependent on the components often nessecitates game logic in UI. Having each componet control it's own UI is nice, but requires UI logic on the components.
     
  14. Arcatus

    Arcatus

    Joined:
    May 27, 2015
    Posts:
    45
    And It works! Well; ofcource it would but it's nice to see a gui with unit name and remaining hitpoints.

    My thinking here was that I will have components that are, in some ways, similar so they display in the same way, but they have gamecode that is fundamentally different. I.e; a kinetic armor is managing damage in a completely different way than a regenerating organic armor, The GUI will show hitpoints in both cases. But; the more I think of it, the less of a problem this is. Thanks for pointing me in a new and better direction. That is exactly why I made this thread.

    This makes me think that I am missing something; Currently my compoents send the string I want to display. the GUI doesn't do any interpreting. One of my delegates looks like this:

    Code (CSharp):
    1.  
    2.   public delegate void GUISelectedUnit(string GUIUnitName);
    3.  
    so the function registered to the event only does this:

    Code (CSharp):
    1.  
    2.   public static void DisplayUnitName(string UnitName)
    3.    {
    4.      GUIUnitName.text = UnitName ;
    5.    }
    6.  
    Should I instead pass the gameobject so the GUI fetches whatever it needs? The "whatever" could be made quite complex; I could even detect instances of the components and then just grab the public variables I need and populate the GUI.

    I imagine this would be a bit like the concept outlined by

    but it would be on demand and not every frame.
    Still; lots of "GetComponentsInChildren" and if's and for's. Not sure I like that.

    The option is my current way of having the select script call a function in the component(s), telling the component to fire it's GUI event.

    Sorry for the long posts here guys, I know this is very brain-stormy, but this all feels a bit like inventing the wheel... and since there have been some great weels invented by many of you I am just making sure I don't build a square one. it will work for a while, but then fail spectacularly :)

     
  15. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    Precisely. This is what the GUI components should be doing. It is their "responsibility" to interpret the component into a user readable format.

    You have good foresight. This is where another design pattern can come in handy. The Abstract Factory pattern defines a generic set of components that can be created - concrete implementations of this factory then create different kinds of components. Here's how it might work:

    Code (csharp):
    1. interface GuiFactory
    2. {
    3.   public GUIElement CreateHitPointView(Ship ship);
    4.   public GUIElement CreateAmmoView(Ship ship);
    5. }
    6.  
    7. class TerranDestroyerGuiFactory : GuiFactory
    8. {
    9.  
    10.   public GUIElement CreateHitPointView(Ship ship)
    11.   {
    12.     return CreateHitPointView(ship as TerranDestroyer);
    13.   }
    14.  
    15.   public GUIElement CreateAmmoView(Ship ship)
    16.   {
    17.     return CreateAmmoView(ship as TerranDestroyer);
    18.   }
    19.  
    20.   public GUIElement CreateHitPointView(TerranDestroyer ship)
    21.   {
    22.     AssertShip(ship);
    23.  
    24.     var uiElement = UnityEngine.Object.Instantiate<GUIText>(TextPrefab);
    25.     uiElement.text = string.Format("Shields at {0}%.  Structural Integrity at {1}%", ship.Shields, ship.Health);
    26.     return uiElement;
    27.   }
    28.  
    29.   public GUIElement CreateAmmoView(TerranDestroyer ship)
    30.   {
    31.     AssertShip(ship);
    32.  
    33.     var uiElement = UnityEngine.Object.Instantiate<GUIText>(TextPrefab);
    34.     foreach (var turret in ship.Turrets)
    35.       uiElement.text = string.Format("{0} ammo: {1}", turret.Name, turret.Ammo);
    36.     return uiElement;
    37.   }
    38.  
    39.   private void AssertShip(TerranDestory ship)
    40.   {
    41.     if (ship == null)
    42.       throw new NotImplementedException("TerranDestoryGuiFactory doesn't know how to interpret this ship");
    43.   }
    44. }
    When a ship is clicked, something will need to decide what kind of Factory to build. It might even be the ship itself, though I would recommend building a registration system so that another class can identify what type of ship was clicked and create the corresponding factory. This factory will be passed to a GUI manager.

    The GuiFactory will be consumed by a GUI manager. The manager will call CreateHitPointView, CreateAmmoView, etc.. and place the returned objects on the screen in the correct places.

    There are a lot of gaps in my code because I'm not 100% sure how the GUI system works in Unity. I also didn't concern myself with caching, pooling or creating strings. Don't use this code in your game but just as a stepping stone to trying something new.
     
  16. Arcatus

    Arcatus

    Joined:
    May 27, 2015
    Posts:
    45
    As cool as that looks, that went above my head (but only slightly, I think) I'll do some googling :)

    Could you help me wrap my head around the purpose of this:

    Code (CSharp):
    1.   public GUIElement CreateHitPointView(Ship ship)
    2.   {
    3.     return CreateHitPointView(ship as TerranDestroyer);
    4.   }
     
  17. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    Right.. Since each GuiFactory will need to work on a ship, but not the same ship, they need to have something in common. In this case, I choose the word "Ship" to represent a base class that all types of ships would inherit from. Of course, you could just as easily forgo the base class since all classes implicitly inherit from object...

    So to your question..
    Code (CSharp):
    1.   public GUIElement CreateHitPointView(Ship ship)
    2.   {
    3.     return CreateHitPointView(ship as TerranDestroyer);
    4.   }
    When the GUI Manager receives a GuiFactory, it doesn't know what kind of factory it is and we want to make sure the same code can be used regardless what factory and what ship we happen to have. We need to make things quite generic: we know we will have a GuiFactory and we know we will have a Ship but that's it.

    So let's take the TerranDestroyer as an example. At runtime, we will have a TerranDestroyer and a TerranDestroyerGuiFactory. Unfortunately, we didn't know that at code time, so we just said "call a method named CreateHitPointView and pass in the ship". In order for the TerranDestroyerGuiFactory to correctly implement this pattern, it has to accept any kind of ship. When the TerranDestroyerGuiFactory receives this message call, it will try to decide which of the two methods named CreateHitPointView should be called. Since we don't know what type of ship we have, we end up in the generic method, the one above.

    In this generic method, I now cast the generic ship into a specific type - namely TerranDestroyer. I then call CreateHitPointView again but with a different kind of ship - the specific one. After this call I will always end up in the specific version of CreateHitPointView expecting a TerranDestroyer. If the cast was successful, then I will have a TerranDestroyer. If it was unsuccessful, then I end up with null, which is why I call Assert before doing anything.


    Now that I'm reviewing this, it doesn't do a great job of following Unity's component architecture. Unity likes all GameObjects to be fundamentally the same but with different components attached. I really like this pattern but it's easy to mix it up - like I just did. Here's another approach that I think will work better with Unity:

    Let's take all the different things that make up a ship which I was trying to stuff into a class hierarchy based on Ship and make them MonoBehaviours which we will simply drop onto GameObjects.

    Code (csharp):
    1. interface ShieldSystem
    2. {
    3.   void TakeDamage(int damage);
    4.   bool IsActive();
    5.   string Name { get; };
    6.   string Percent { get; };
    7. }
    8. class KineticShields : MonoBehaviour, ShieldSystem
    9. {
    10.   // Some public fields to show up in the editor
    11.   public readonly int maxShields;
    12.   public readonly string name;
    13.  
    14.   int power = maxShields;
    15.  
    16.   public void TakeDamage(int damage)
    17.   {
    18.     power -= damage;
    19.   }
    20.  
    21.   public bool IsActive()
    22.   {
    23.     return power > 0;
    24.   }
    25.  
    26.   public string Name
    27.   { get { return name; } }
    28.  
    29.   public string Percent
    30.   { get { return (power / maxShields).ToString(); } }
    31. }
    Now our GUI code can be quite a bit simpler because we don't need different factories for each type of ship, though we may still want to use that design pattern for different reasons - maybe a different GUI when multiple ships are selected.

    When a ship is selected, we should just pass the whole GameObject to the GuiFactory. Each method can grab the components it is interested in.

    Code (csharp):
    1. class GuiFactory
    2. {
    3.   GUIElement CreateHitPointView(GameObject ship)
    4.   {
    5.     var uiElement = UnityEngine.Object.Instantiate<GUIText>(TextPrefab);
    6.  
    7.     foreach (var shield in ship.GetComponentsInChildren<ShieldSystem>())
    8.       uiElement.text += string.Format("Shield System {0} at {1}%", shield.Name, shield.Percent);
    9.  
    10.     return uiElement;
    11.   }
    12. }
     
    Last edited: Jul 24, 2015
  18. Arcatus

    Arcatus

    Joined:
    May 27, 2015
    Posts:
    45
    Got it!

    This is all quite cool. Interfaces is also something I haven't used, I have seen them used elsewhere, but I didn't quite get it. Until now.

    the GetComponentsinChildren<Interface> will be a real door opener. I didn't know that was possible.

    I got some coding to do. :) If all the pieces fit together as I hope they will this will be quite dynamic and scalable. Thank you so much for your help!