Tag: UI

Refactoring UI Elements

Refactoring UI Elements

I started this post with just the reference pic above almost 6 weeks ago, so I’ve been trying to remember exactly what I was trying to tell myself here. Fortunately for me I did annotate the code so I’ve managed to backtrack and figure things out.

As with many posts during a period of review and testing this is again about extensibility, reusability and ease of use.

The issue was with the size of the UI prefab – it had become a bit of a goliath with many interdependent elements (objects) being controlled via the centralised UI controller class. This meant that the the UI was a pain to instantiate in a scene as the references were difficult to ‘find’ and were so interwoven with one another that any bug tracking or recycling/repurposing was getting to be a bit of a chore. What I’m always after is rapid prototyping and that means trying to make things that are for the most part drag and drop.

I’ve read so many times across the Unity fora about the importance of keeping things as simple and independent as possible – I think I’ve written a lot about it too – but as ever the best teacher is failure and thats exactly what my giant prefab brought to the fore.

So as of this last iteration of the UI, all child elements are separated out into their own classes and are saved as individual prefabs. This has simplified matters and allowed the whole thing to be come more extensible and flexible in terms of functionality:

Overview of the UI elements

This new arrangement of prefabs can now be used in a modular way and new elements should be able to be introduced with a minimum of fuss as everything is pretty much separate, with each element of the UI existing as its own class containing its own functions that can be referenced and called from anywhere or anything else.

For example, the prefab element called BroadcastMessages contains:

  • its own class
  • a text (TMP) object
  • a background object
  • an animator component

The class itself becomes very simple to manage as its functionality applies only to its children. This functionality is accessed as a function that displays (and animates) incoming text addressed to that function from elsewhere by calling:

BraodCastMessages.IncomingBroadcastMessages(message);

public class BroadCastMessages : MonoBehaviour
{
    [Header("BROADCAST MESSAGES")]
    TMP_Text broadcastMessages_Text;
    Animator animator_broadcastMessages;

    void Start()
    {
        //GAME BROADCAST MESSAGE (LARGE CENTRAL)
        broadcastMessages_Text = GetComponentInChildren<TMP_Text>();
        animator_broadcastMessages = GetComponentInChildren<Animator>();
        broadcastMessages_Text.enabled = false;
    }

    //public method for receiving messages:
    public void IncomingBroadcastMessages(string message)
    {
        broadcastMessages_Text.text = message;

        if (broadcastMessages_Text.text != null)
        {
            broadcastMessages_Text.enabled = true;
            animator_broadcastMessages.SetBool("broadcastMessage_FadeUp", true);
        }

        if (broadcastMessages_Text.text == null)
        {
            animator_broadcastMessages.SetBool("broadcastMessage_FadeUp", false);
            broadcastMessages_Text.enabled = false;
        }
    }
}

The upshot of this is that the UI is now a system of prefab elements responsible (in the main) for their own part of the display using no more that 30-40 lines of code:

Outline of UI structure using individual prefabs with individual classes

The only complication/extension in the display I am currently using is the ScreenToWorldPoint method (outlined in this post) that displays a line linking information boxes to specific objects identified and passed from the raycast function in PlayerInteraction(). This display method can be seen in the screen grab at the top of this post.

The information (text) is passed from the raycast hit object to either the RHS or LHS messages class (depending on the objects’ tag).

The information is then displayed using the ScreenToWorldPoint method which I have now simplified by moving into the class UI_Manager – the same function is called by both the RHS and LHS Messages classes so this saves duplication:

public class UI_Manager : MonoBehaviour
{
   ........

    public void FadeUpLineRenderer(LineRenderer lineRenderer, Transform rayHitTransform,
        TMP_Text text)
    {
        //Fade up and link text to the object in world space
    }

    public void FadeDownLineRenderer(LineRenderer lineRenderer, TMP_Text text)
    {
        if (alpha_lineRenderer > 0)
        {
            //Fade down
    }
}

So this class (UI_manager) gets called by either LHS of RHS messages which provide references to a lineRederer, a Transform and the text. The LHS/RHS messages are accessed themselves using a unique function.

The snippet below is from the LHS_messages class. Its is a slightly more complex version of the Broadcast Messages class but basic functionality is the same – messages are passed to it using the call:

LHS_Messages.IncomingMessages(message)

Then the appropriate elements are passed to the function in UI_Manager:

UI_Manager.FadeUpLineRenderer

or

UI_Manager.FadeDownLineRenderer

public class LHS_Messages : MonoBehaviour
{
..................

//Incoming messages public method
    public void IncomingMessages(string message)
    {
        LHS_messageText.text = message;
    }

    void Update()
    {
        //If no raycast hits turn off the displays
        if (playerInteraction.rayCastHitObject == null)
        {
            {
                //fade down UI window
                LHS_messageAnimator.SetBool("LHS_Panel_FadeUp", false);

                //set the rayHitTransform to null
                rayHitTransform = null;

                //Fade Down LineRenderer:
                uI_Manager.FadeDownLineRenderer(lineRenderer, LHS_messageText);
            }
        }

        //if raycast hit display if tag is "Interactable"
        if (playerInteraction.rayCastHitObject != null
            && playerInteraction.rayCastHitObject.CompareTag("Interactable"))
        {
            //turn on this element
            LHS_messageText.enabled = true;
            lineRenderer.enabled = true;

            //Fade up via Animator
            LHS_messageAnimator.SetBool("LHS_Panel_FadeUp", true);

            // set up the generic Transform component:
            rayHitTransform = playerInteraction.rayCastHitObject;

            //ADD THE LINE
            uI_Manager.FadeUpLineRenderer(lineRenderer, rayHitTransform,
                LHS_messageText);
        }
    }
}

Summing up what this approach and refactoring has helped achieve is a more extensible, easier to track/debug and generally much more flexible series of prefabs/classes that can be adapted, reused, repurposed and rewritten as circumstances require.

For example the UI_Manager class is now nothing more complex than an extra function holder that can be logically extended to include and execute any repeated functions that occur within the UI as a whole – I do believe I’m not far away form understanding Properties here so no doubt at some point down the line I’ll have an Aha! moment and write a new post referencing and ridiculing my clumsiness in this one….

That plus perhaps a more important point for me – by trying to employ a more sensible, simplified approach to writing the code in the first place I’m able to come back after a complete absence of some 6 weeks and take only 20 minutes or so to see what I’m doing.

Knowing what I’m trying to do is mostly a good thing…..leaving easy to read comments is definitely a good thing.

Player: UI Info Overlay

Player: UI Info Overlay

Solution to displaying interactable object information on `UI based on https://forum.unity.com/threads/get-ui-placed-right-over-gameobjects-head.489464/

    //send the ray from the **CAMERA** (not the player)
    rayDirection = camera.transform.TransformDirection(Vector3.forward);
    //point of origin is LOOK - from the camera
    ray_pointOfOrigin = camera.transform.position;
    if (Physics.Raycast(ray_pointOfOrigin, rayDirection,
        out interactableObjectHit, scanRange))
    {
        //Change the color based on object in range
        //Check if the hit obj has Interactable Tag
        if (interactableObjectHit.transform.CompareTag("Interactable"))
        {
            crosshair.color = Color.red;
            //Get the transfrom of the hit object
            rayCastHitObject_transform = interactableObjectHit.transform;
            //Get the name of the hit object
            rayCastHitObject_name = interactableObjectHit.transform.name;
            if (interactableObjectHit.transform == null)
            {
                rayCastHitObject_name = null;
            }
        }
    }
    else
    {
        crosshair.color = Color.white;
        //change the name to null so we can differentiate a
        //no hit event in the UI controller
        rayCastHitObject_name = null;
    }
/*
RAYCAST INFO    
Running in update to ensure we are updating what we're looking at.
    We are receiveing raycast info from playerinteraction script that gives
    us the name of the object we've hit (we are looking at) using:
    rayCastHitInfo = hit.transform.name;
    We can use this info in the UI to display the name and info about
    the object we're currently looking at.
    *********************************************************************/
    if (playerInteraction.rayCastHitObject_name != null)
    {
        //Turn on the components we need in UI
        canvasGroup_interactableObjUI.enabled = true;
        panel_interactableObjUI.enabled = true;
        text_interactableObjUI.enabled = true;
        lineRenderer_hitObjectToUI.enabled = true;
        //add the line
        lineRenderer_hitObjectToUI.startWidth = 0.001f;
        lineRenderer_hitObjectToUI.endWidth = 0.01f;
        //Set up a new V3 to hold the position of the UI element:
        Vector3 UI_element_transform = new Vector3(
            //x and y will attach to pivot points of the UI object set in the Inspector
            text_interactableObjUI.rectTransform.position.x,
            text_interactableObjUI.rectTransform.position.y,
            //Add 1.0f on z else we won't see the line
            1f
            );
        //No. of points on the line (start/end = 2)
        lineRenderer_hitObjectToUI.positionCount = 2;
        //start at position of the UI obj (rayInfo) and change it to World position
        lineRenderer_hitObjectToUI.SetPosition(0, cam.ScreenToWorldPoint(UI_element_transform));
        //second point is position of the object hit by the raycast in playerInteraction
        lineRenderer_hitObjectToUI.SetPosition(1, playerInteraction.rayCastHitObject_transform.position);
        /*********************************************************************
        Add the text
        -this needs to pull in pre stored text like a databasethat depends on
        the name of the hit object - playerInteraction.rayCastHitObject_name
        *********************************************************************/