4 min read

Building flexible UI architecture for Unity development projects.

Problem

When creating Unity UI for games or applications, we often encounter a situation where there are many similar elements in terms of functionality but different in the types of data they visualize. I want to have a system (hierarchy) where it's easy to create new elements without copy-pasting and generating a lot of redundant code. Below, I've provided an example of elements that share the functionality of an infinite scroll list but display different data. What should I use as a solution?

Solution

Let's abstract the data which we need to populate infinite lists with. We have a base class responsible for handling the scrolling logic of our list. Its primary responsibility is to create the illusion that the user is scrolling through a vast stream of elements (the base class does not know what these elements are). However, during scrolling and element replacement, it should populate them with current data from the abstract data list. The data type should be easily replaceable, and it should still be possible to view/edit the data from the Unity editor. This way, the list script can be used to visualize various types of data: payment history, user activity history, match history, task list, achievements, etc.

PopulateData - Base Data Class.
// Enum for determine data type
public enum DataType {...}

// Base data class
[Serializable]
public abstract class PopulateData {
   // The abstract property that we need to define in each data class that inherits from PopulateData.  
   public abstract DataType DataType { get; }
}

 Thanks to the fact that the DataType property requires overriding in derived classes, we always know which data type we are working with, directly from the base PopulateData.

 For populating elements with data, the IPopulateElement interface is used, and all elements that need to be populated with data inherit from it.

Interface IPopulateElement
public interface IPopulateElement<T> where T : PopulateData {
   public void Populate(T data);
}

IPopulateElement serves as a common entry point for populating elements with data. It is designed as a generic interface to define the static data type that a specific element will display.

List base class – InfinityScrollList
public abstract class InfinityScrollList<T> : BaseUIElement, IBeginDragHandler, IDragHandler, IEndDragHandler where T : PopulateData {
     
     // Checking and controlling the number of elements in the scene 
     // Checking the availability of data for visualization 
     // Populating elements with data
     public virtual void Activate() {...}

     // Begin drag logic
     public void OnBeginDrag(PointerEventData eventData) {...}
     
     // Drag logic
     public void OnDrag(PointerEventData eventData) {...}

     // End drag logic
     public void OnEndDrag(PointerEventData eventData) {...}
     
     // Populating an element with data
     protected void PopulateChild(Transform child, int dataIndex) {
     	  // Better to add collection to not over use GetComponent-method
          child.GetComponent<IPopulateElement<T>>()?.Populate(dataList[dataIndex]);
     }
}

 During scrolling, elements are populated with current data. To be able to create different types of elements and populate them with different data types, we use the base abstract data class PopulateData.

Example and Usage

HistoryInfinityScrollList - a class for displaying balance changes

 Let's create a class that will display a list of player balance changes.

Conditions:

  • The class should inherit from the base class for an infinite list (InfiniteScrollList).
  • There should be validation and control of the number of elements in the scene.
  • Before populating the list, there should be a validation for the availability of data for visualization.
public class HistoryInfinityScrollList : InfinityScrollList<HistoryInfinityScrollList.HistoryData> {

   // Base Data class of HistoryInfinityScrollList
   public abstract class HistoryData : PopulateData {
       public override DataType DataType => DataType.HistoryData;
       public enum ElementType {
           ACTION,
           SESSION
       }

       // Abstract Enum Property for Defining Local Data Type
       public abstract ElementType Type { get; set; }
       [field: SerializeField] public DateTime DateTime { get; set; }
   }

   // Class for Player Action Data that Caused Balance Change
   public class ActionData : HistoryData {
       // Overriding Local Data Type
       [field: SerializeField] public override ElementType Type { get; set; } = ElementType.ACTION;
       #region DataFields
           ...
       #endregion
   }
   
   // Class for Session Start Data
   public class SessionData : HistoryData {
       // Redefining Local Data Type
       [field: SerializeField] public override ElementType Type { get; set; } = ElementType.SESSION;
   } 
}
HistoryElement - a class that will be interacted by HistoryInfinityScrollList

 Since we need to support two different types of data, let's create HistoryElement as a layer that will determine which final element to display and populate with data.

ActionHistoryElement and GameNightElement - final elements

 These classes simply populate the necessary UI components (Image, TextMeshProUGUI, etc.) with the required data.

public class ActionHistoryElement : MonoBehaviour, IPopulateElement<HistoryInfinityScrollList.ActionData> {
    // Populating Element with Data
    public void Populate(HistoryInfinityScrollList.ActionData data) {...}
}
public class GameNightElement : MonoBehaviour, IPopulateElement<SessionData> {
   // Populating Element with Data
   public void Populate(SessionData historyData) {...}
}
 Hierarchy in the Scene

 To control the switching and populating of ActionHistoryElement and GameNightElement, they are child objects of HistoryElement. The hierarchy of all other elements that will inherit from InfinityScrollList will be similar, with the difference being in populating the base element that will be filled with data.

Conclusions

 We now have the ability to create UI infinite lists populated with the necessary data types, and we can also expand the base functionality as needed. Here is what we achieved:

  • Understanding for creating similar UI elements in the future.
  • Ability to prevent chaos in the project.
  • Shared hierarchy in infinite lists.