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.