Search Unity

Question Architectural Advice / Feedback for multiuse UI Components

Discussion in 'Scripting' started by sasuchi, Apr 20, 2021.

  1. sasuchi

    sasuchi

    Joined:
    Jun 18, 2019
    Posts:
    13
    Hi guys,

    I need an architectural advice / help with the implementation of "reuseable" / "multiuse" UI Components. First of what do I mean with reuseable / multiuse:
    I want UI components to be useable in several places/ui compositions. For example the player inventory, which is shown at the equipment screen or at a npc shop screen. Obviously, when the player transfers an item from his inventory different behaviour is needed, in the equipment screen we want to equip the transfered item (Player->Equip(item)) and in the shop we want to sell the item (Player->SellItem(NPC,item)).

    My current Implementation looks like this:

    Per UIComponent a seperate script (e.g. UIPlayerInventory, UICharacterEquipment, UIReadiedItem) with neccessary references (to the player/equipment/etc). Furthermore, Components that contain items/have slots implement the interface IItemSlotHolder.

    Abstract class UIItemSlot that extends explicit ItemSlot Classes (UIInventoryItemSlot, UIEquipmentItemSlot, UIReadiedItemSlot). Those concrete Implementations are attached to corresponding prefabs. The ItemSlot has a reference to its parent ui component as a IItemSlotHolder.

    A class for an Item (ItemStack), also attached to a prefab. OnEnabling/Moving the item prefab, it saves a reference to its parent slot as a GameObject.

    InventoryLogic.png

    Now for the interaction between those components, i made the item drag'n'droppable, meaning the class UIItemStack implements the interfaces IPointerDownHandler, IDragHandler, IBeginDragHandler, IEndDragHandler and the UIItemSlot implements the interface OnDropHandler with virtual (probably could/should be abstract) OnDrop implementation, where the extended classes (UIInventoryItemSlot/...) have their specific OnDrop implementation (e.g. Player->Equip(itemRecieved), Player->Unequip(itemReceived)).

    Now the (rising) problem I see with this implementation:
    There is a sender, the one where the ItemStack is dragged from, and a receiver, where the ItemStack is dropped on a ItemSlot.
    The receiver knows explicitly what his UIComponent is, e.g. UIInventoryItemSlot's UIComponent is the UIPlayerInventory. And the receiver can also get the type of the senders UIComponent (the eventData holds the ItemStack, which has a reference to its originating slot//concrete class for UIItemSlot, which has a reference to the parent IItemSlotHolder), however how would I implement different behaviour based on the senders type.

    Of course i could go the route of if/switch statements based on the senders type, but having watched the jason weimann videos about the solid principle multiple times, i understood this could cause poor maintainability. For each new Composition where a UI is needed, i need to add a new branch to that statement.

    A different solution i thought of was to have a seperate class, as a kind of processor, that determines the correct behaviour... this would prevent the need to edit every existing ui component if a new one is created. however its not truly prevented but rather shifted to a seperate class...so this seems to be a suboptimal solution aswell...

    Any advices on archtecture are appreciated...
    I hope I could explain my problem clearly, as I am not a native speaker, if there are any questions or need of concrete code let me know.

    Thanks everyone in advance.
     
    Last edited: Apr 20, 2021
  2. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    I don't think you've created anything generic or multi-use at all, if I'm being honest. Every element on your UI is highly specialized and specific to the system using it. I personally don't like the idea of the UI having so much knowledge of what it's representing and how to handle it.

    I would only have your UIItemSlot and that's it. Every time an item is placed in a slot, broadcast an event. Every time an item is removed from the slot, broadcast an event. Put a layer between that, like InventoryWindow, which re-broadcasts the event for every item slot it's managing. Then your actual game systems only need to worry about listening for specific events from specific windows.
     
  3. sasuchi

    sasuchi

    Joined:
    Jun 18, 2019
    Posts:
    13
    Hi GroZZleR,

    thanks for the input.

    I agree, i dont want the UI to have knowledge about what it should represents, it should only trigger the corresponding action caused by the usage. In that regard my system is probably as you mentioned poorly designed.

    I am not sure if I fully understood you suggestion here, so ill try to rephrase it and point out my uncertainty.
    -The Item fires an Event when it gets dragged (something like RemoveItemFromSlotX)
    -The ItemSlot fires an Event when something gets dropped on (something like AddItemToSlotY)
    -InventoryWindow is class, which represents a window like PlayerInventory/Equipment, that manages an array of ItemSlots and listens to the events (RemoveItemFromSlotX/AddItemToSlotY). The underlying system, listens to forwarded events from the InventoryWindow and does processes the logic behind.
    E.g.
    1. I begin dragging an Item from an ItemSlot in the InventoryWindow (PlayerInventory) -> ItemSlot in InventoryWindow (PlayerInventory) fires the event RemoveItemFromSlot(x) -> InventoryWindow (PlayerInventory) fires the event that the underlying gamelogic process removing the item from the slot x.
    2. I drop the dragged item on an ItemSlot in the InventoryWindow (Equipment) -> ItemSlot in InventoryWindow (Equipment) fires the event AddItemToSlot(y) -> InventoryWindow (Equipment) fires the event that the underlying gamelogic process adding the item to the slot y.

    As mentioned before, i probably did not fully understand your suggestion, however if my rephrasing is correct, how could i ensure process safety ? The process of removing an item from inventory A is decoupled from adding it to inventory B.
    As an example what if the player trys to unequip an item into an full inventory, on the equipment side removing is no problem, on the player inventory side now i face the problem i dont know the origin. Or would i pass the origin of the transaction within the event, that i could reverse the process?

    sorry im a bit lost :D
     
  4. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    Sure, you've mostly got it. I'll demonstrate with regular C# events but you can also use UnityEvents and then rig things up in the inspector.

    Code (csharp):
    1.  
    2.  
    3. class UI_ItemSlot : MonoBehaviour, all the callback events
    4. {
    5.    // call these events when an item is dragged in or out of the slots successfully
    6.    // you already have that logic with dragging and I don't know it off-hand
    7.    // you invoke events like functions: itemAdded(item);
    8.    public event Action<Item> itemAdded;
    9.    public event Action<Item> itemRemoved;
    10. }
    11.  
    12. class UI_EquipmentWindow : MonoBehaviour
    13. {
    14.    // create all your slots
    15.    UI_ItemSlot headSlot;
    16.    UI_ItemSlot leftHandSlot;
    17.    // etc etc
    18.  
    19.    // now define the rebroadcast messages
    20.    public event Action<Item> helmetEquipped;
    21.    public Event Action<Item> helmetUnequipped;
    22.    // etc etc
    23.  
    24.    void Start()
    25.    {
    26.        headSlot.itemAdded += OnHelmetEquipped;
    27.        headSlot.itemRemoved += OnHelmetUnequipped;
    28.    }
    29.  
    30.    void OnHelmetEquipped(Item item)
    31.    {
    32.        if(helmetEquipped != null)
    33.            helmetEquipped(item);
    34.    }
    35.  
    36.    void OnHelmetUnequipped(Item item)
    37.    {
    38.        if(helmetUnequipped != null)
    39.            helmetUnequipped(item);
    40.    }
    41. }
    42.  
    43. // I don't know the specifics of this part of your game, you may not want to register
    44. // directly to the UI events in the Player itself, but you get the idea
    45. class Player : MonoBehaviour
    46. {
    47.    void Start()
    48.    {
    49.        UI_EquipmentWindow equipmentWindow = // find it
    50.        equipmentWindow.helmetEquipped += OnHelmetEquipped;
    51.        equipmentWindow.helmetUnequipped += OnHelmetUnequipped;
    52.    }
    53.  
    54.    void OnHelmetEquipped(Item item)
    55.    {
    56.        // gain stats
    57.        // change visuals
    58.    }
    59.  
    60.    void OnHelmetUnequipped(Item item)
    61.    {
    62.        // lose stats
    63.        // change visuals
    64.    }  
    65. }
    66.  
    UnityEvent docs if you want to use them instead: https://docs.unity3d.com/ScriptReference/Events.UnityEvent.html