Using the UI Toolkit with the RPG/Third Person merge project?

I am following along with:
Integrating the Third Person Course with the RPG Course
and I am finding it very helpful. The part about blend tree nesting is amazing.
My question is; I would like to switch to the UI Toolkit. I am still traumatized by the thought of nested UI prefabs. When would the best time be to make the switch? Should I wait until the end of the merge? Should I do it now as part of the set up?

Also, could someone out there do me a huge favor and create a few bullet points to guide the way to converting “old” UI to the new system? I am not looking for a complete tutorial or step-by-step instructions. I want to do it myself. Think of it a supersized challenge slide from one of the courses. I have the Unity UI Toolkit course and it is a good introduction. I’m just looking for a gentle shove in the right direction.

I would say “You can’t combine UI Toolkit with either the RPG project or the Third Person without going crazy”, but that would be a lie.

There are some gatchas. It took me about a week or so to work out a decent dragging and dropping setup. While you’d think that would have been the easy part, I had to completely discard our Inventory System’s Drag handling scripts and start from scratch.

So here’s a snapshot my heirarchy in SpellBorn Hunter, my magnum opus project.

The disabled objects are the old UI, which are still in the Core prefab, but largely disabled. In there place is a handful of UI documents sitting under an otherwise empty GameObject. Each individual document underneath represents a different uxml based UI.


A couple quick head start scripts:

UIElementsDriver.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

[RequireComponent(typeof(UIDocument))]
[RequireComponent(typeof(PanelEventHandler))]
[RequireComponent(typeof(PanelRaycaster))]
public abstract class UIElementsDriver : MonoBehaviour
{
    [SerializeField] private AudioClip onOpenSound = null;
    [SerializeField] private AudioClip onCloseSound = null;
    protected UIDocument Document => GetComponent<UIDocument>();
    protected VisualElement root => Document.rootVisualElement;
    protected static int SortingOrder = 1;
    public int DocumentSortingOrder => Mathf.CeilToInt(Document.sortingOrder);
    public static event System.Action<UIElementsDriver> OnElementEnabled;
    public static event System.Action<UIElementsDriver> OnElementDisabled;
    protected abstract void OnShow();
    
    public void Show(bool callOnElementEnabled=true)
    {
        SortingOrder++;
        Document.sortingOrder = SortingOrder;
        root.Show();
        OnShow();
        if(callOnElementEnabled) OnElementEnabled(this);
        if (onOpenSound)
        {
            GetComponent<AudioSource>().PlayOneShot(onOpenSound);
        }
    }

    public void Hide(bool callOnElementEnabled=true)
    {
        root.Hide();
        if(callOnElementEnabled) OnElementDisabled?.Invoke(this);
        if (onCloseSound)
        {
            GetComponent<AudioSource>().PlayOneShot(onCloseSound);
        }
    }

    protected void PushWindowForward(PointerDownEvent evt)
    {
        SortingOrder++;
        Document.sortingOrder = SortingOrder;
    }
    
    public virtual void Toggle()
    {
        if (root.style.display == DisplayStyle.None)
        {
            Show();
        }
        else
        {
            Hide();
        }
    }
    
    public bool isHidden => root.style.display == DisplayStyle.None;

}

All of my UI Documents inherit fromt this class that handles some of the tedium, sets up the documents, provides common methods, etc.

InventoryUIElement.cs
using GameDevTV.Inventories;
using TkrainDesigns.UIToolkit;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;

public class InventoryItemVisualElement : VisualElement, IDraggableUIElement<InventoryItemVisualElement, InventoryItem>
{

    public new class UxmlFactory : UxmlFactory<InventoryItemVisualElement, UxmlTraits>
    {

    }

    protected int imageSize = 100;
    protected Image image;
    private InventoryItem currentItem;
    private int currentSlot;
    private Inventory inventory;
    private VisualElement Ghost;
    protected UIElementsTooltipManager tooltipManager;

    public InventoryItemVisualElement()
    {
        style.height = imageSize;
        style.width = imageSize;
        image = new Image();
        Add(image);
        AddToClassList("inventoryContainer");
        RegisterCallback<PointerDownEvent>(OnPointerDown);
        RegisterCallback<PointerMoveEvent>(OnPointerMove);
        RegisterCallback<PointerUpEvent>(OnPointerUp);
        RegisterCallback<PointerOverEvent>((evt) =>
        {
            ShowTooltip();
        });
        RegisterCallback<PointerOutEvent>((evt) =>
        {
            HideTooltip();
        });
    }

    public static VisualElement Circle(string circleSelector, string textSelector, float size = 20)
    {
        VisualElement result = new VisualElement();
        result.style.height = size;
        result.style.width = size;
        Label label = new Label();
        result.AddToClassList(circleSelector);
        label.AddToClassList(textSelector);
        label.RemoveFromClassList(".unity-label");
        label.RemoveFromClassList(".unity-text-element");
        label.text = "99";
        result.Add(label);
        return result;
    }

    public virtual void ShowTooltip()
    {
        if (item == null || tooltipManager == null) return;
        if (UIElementsDragHandler<InventoryItemVisualElement, InventoryItem>.IsDragging) return;
        tooltipManager.ShowTooltip(this, item.GetIcon(), item.GetDisplayName(), item.GetDescription());
    }

    public virtual void HideTooltip()
    {
        if (tooltipManager != null)
        {
            tooltipManager.HideTooltip();
        }
    }

    public void SetGhost(VisualElement element)
    {
       
        Ghost = element;
        //Debug.Log($"Ghost = {Ghost.name}");
    }

    public void SetToolTipManager(UIElementsTooltipManager manager)
    {
        tooltipManager = manager;
    }
    
    public int ImageSize
    {
        get => imageSize;
        set
        {
            imageSize = value;
            style.height = imageSize;
            style.width = imageSize;
        }
    }

    public Sprite background
    {
        get => style.backgroundImage.value.sprite;
        set
        {
            var setter = style.backgroundImage.value;
            setter.sprite = value;
            style.backgroundImage = setter;
        }
    }

    public Sprite sprite
    {
        get => image.sprite;
        set => image.sprite = value;
    }

    public InventoryItem item
    {
        get => currentItem;
        set
        {
            currentItem = value;
            sprite = currentItem ? currentItem.GetIcon() : GetDefaultIcon();
        }
    }

    protected virtual Sprite GetDefaultIcon()
    {
        return null;
    }
    
    public virtual void RemoveItem()
    {
        
    }

    public virtual void Refresh()
    {
        if (item) sprite = item.GetIcon();
    }
    


    public Rect GetBoundingRect()
    {
        return worldBound;
    }

    public virtual Sprite GetSprite()
    {
        return sprite;
    }

    public InventoryItem GetItem()
    {
        return item;
    }

    public virtual int MaxAcceptable(InventoryItem otherItem)
    {
        return 0;
    }

    public virtual int QuantityAvailable()
    {
        return 0;
    }

    public virtual void AddItem(InventoryItem sourceItem, int amount)
    {
    }

    public virtual void HandleDoubleClick()
    {
        
    }
    
    private bool IsDragging;
    private bool ReadyToDrag;

    private double lastClicked;

    static private IEventHandler currentTarget;
    
    void OnPointerDown(PointerDownEvent evt)
    {
        
        if (evt.button != 0 || item == null) return;
        ReadyToDrag = true;
        if (currentTarget!=this)
        {
            currentTarget = evt.currentTarget;
            PrepareToClearCurrentTarget();
        }
        else
        {
            HandleDoubleClick();
        }
        // if (Time.timeAsDouble - lastClicked < 0.25)
        // {
        //     HandleDoubleClick();
        // }
        // lastClicked = Time.timeAsDouble;
    }

    async void PrepareToClearCurrentTarget()
    {
        await Task.Delay(250);
        ClearCurrentTarget();
    }
    
    void ClearCurrentTarget()
    {
        if (currentTarget == this) currentTarget = null;
    }
    
    void OnPointerUp(PointerUpEvent evt)
    {
        IsDragging = false;
        ReadyToDrag = false;
    }
    
    private void OnPointerMove(PointerMoveEvent evt)
    {
        if(!IsDragging && ReadyToDrag)
        {
            BeginDrag(evt);
        }
    }

    private void BeginDrag(PointerMoveEvent evt)
    {
        Debug.Log("BeginDrag()");
        sprite = null;
        HideTooltip();
        IsDragging = true;
        ReadyToDrag = false;
        UIElementsDragHandler<InventoryItemVisualElement, InventoryItem>.BeginDragging(this, Ghost, evt.position);
    }
}

This script forms the base of all of the Inventory Item UI type classes, and forms the basis of the dragging system.

Of course, there’s a LOT more to it than that. An Inventory UIElements tutorial is on the list of TODOs, but will actually be a separate tutorial from the RPG/ThirdPerson merge tutorial as it isn’t really dependent on the merge at all (it was actually written before I ported the Third Person into Spellborn Hunter.

1 Like

Challenge Accepted!!

Thanks Brian. This gives me a starting direction. I will definitely try this as a fork of the main project. Something to work on when I want to create something substantial with out a lot of hand holding. Two years into this journey and I am still amazed at how much goes into a game.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms