Inventory System · Part 2 of 2 in the series
Part 2: An Inventory Bar With UI Toolkit
Show the inventory from part 1 in a bar at the bottom of the screen with UI Toolkit: UXML for the layout, USS for the look and one generic C# class that keeps both inventories up to date.

Quest objectives
- Lay out a bar in UXML and style it with USS
- Show any inventory with one generic C# class
- Update only the slots that changed, through the inventory’s events
- Connect the UI with a Panel Renderer, or a UI Document on Unity 6.3 LTS
In part 1 you built the inventory. Now the player gets to see it: a bar at the bottom of the screen, collectibles on the left, buildables on the right, every slot with the item's icon and how many there are.
The bar is made with UI Toolkit, from three files: a UXML file for the layout, a USS file for the look and a C# script that fills the bar and keeps it up to date.
The layout
Create a folder Assets/UI, and in it a UXML file named InventoryUi.uxml (right-click, Create > UI Toolkit). Replace its content with this:
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<Style src="InventoryUi.uss" />
<ui:VisualElement name="Inventory" class="inventory">
<ui:VisualElement name="CollectibleItems" class="inventory-row">
<!-- A placeholder, so the layout shows in the UI Builder -->
<ui:VisualElement class="slot">
<ui:Label text="99" class="slot-amount" />
</ui:VisualElement>
</ui:VisualElement>
<ui:VisualElement name="BuildableItems" class="inventory-row">
<ui:VisualElement class="slot">
<ui:Label text="1" class="slot-amount" />
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>The bar is one element with two rows, CollectibleItems and BuildableItems, which the script will look up by name. Each row holds one placeholder slot, so the layout shows in the UI Builder. The script removes it when it fills the row.
The stylesheet is referenced by a path relative to the UXML file, so it only has to be in the same folder.
The look
Create the stylesheet InventoryUi.uss next to it (Create > UI Toolkit > StyleSheet):
.inventory {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 56px;
flex-direction: row;
}
.inventory-row {
flex-grow: 1;
flex-basis: 0;
flex-direction: row;
align-items: center;
padding: 0 4px;
background-color: rgba(0, 0, 0, 0.75);
}
#CollectibleItems {
margin-right: 4px;
}
.slot {
width: 48px;
height: 48px;
margin-right: 4px;
-unity-background-scale-mode: scale-to-fit;
}
.slot-amount {
position: absolute;
right: 2px;
bottom: 0;
margin: 0;
padding: 0;
font-size: 12px;
color: white;
-unity-font-style: bold;
-unity-text-outline-width: 1px;
-unity-text-outline-color: black;
}The bar sits at the bottom of the screen, across its full width. flex-grow: 1 with flex-basis: 0 gives both rows the same width, whatever is in them. A slot is a square of 48 pixels with the item's icon as its background, scaled to fit, and the amount sits in its bottom right corner, white with a black outline, so it can be read on any icon.
A bar for any inventory
The two rows show different inventories, but they do exactly the same thing with them. So one generic class does it, for any inventory from part 1. Create a folder Assets/Scripts/UI, and in it InventoryBar.cs:
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
// Shows one inventory as a row of slots, each with the item's icon and amount,
// and keeps the row up to date while the inventory changes.
public class InventoryBar<TInventoryItem, TItem, TItemType> : IDisposable
where TInventoryItem : class, IInventoryItem<TItem, TItemType>, new() {
readonly VisualElement row;
readonly Inventory<TInventoryItem, TItem, TItemType> inventory;
// The amount label of every slot, by item type
readonly Dictionary<TItemType, Label> amounts = new Dictionary<TItemType, Label>();
public InventoryBar(VisualElement row, Inventory<TInventoryItem, TItem, TItemType> inventory) {
this.row = row;
this.inventory = inventory;
// Removes the placeholder from the UXML
row.Clear();
foreach (TInventoryItem stack in inventory.items) {
Refresh(stack);
}
inventory.ItemDeposited += Refresh;
inventory.ItemWithdrawn += Refresh;
}
// Stops following the inventory, for when the UI goes away
public void Dispose() {
inventory.ItemDeposited -= Refresh;
inventory.ItemWithdrawn -= Refresh;
}
void Refresh(TInventoryItem changed) {
TItemType type = changed.Type;
int amount = inventory.GetAmount(type);
bool shown = amounts.TryGetValue(type, out Label label);
if (amount == 0) {
if (shown) {
label.parent.RemoveFromHierarchy();
amounts.Remove(type);
}
return;
}
if (!shown) {
label = AddSlot(changed.Icon);
amounts.Add(type, label);
}
label.text = amount.ToString();
}
Label AddSlot(Sprite icon) {
VisualElement slot = new VisualElement();
slot.AddToClassList("slot");
slot.style.backgroundImage = new StyleBackground(icon);
Label amount = new Label();
amount.AddToClassList("slot-amount");
slot.Add(amount);
row.Add(slot);
return amount;
}
}It has the same type parameters and constraints as Inventory, so it takes any inventory, and it uses nothing but what IInventoryItem offers: the type, the amount and the icon.
The constructor empties the row, adds a slot for every stack that is in the inventory already and subscribes to the inventory's two events.
amountsremembers the amount label of every slot by item type, so a change finds its slot without searching the row.Refreshhandles every change the same way, whether something went in or came out: it asks the inventory for the new total of that type. At zero the slot goes. If there is no slot yet, one is added. Then the label gets the new number. Only the slot that changed is touched.Disposeunsubscribes from the events. The inventory belongs to the player and can outlive the UI, and without it the inventory would go on calling a bar whose elements are gone.
Connecting it
A component connects the bars to the screen and to the player. Create InventoryUi.cs in the same folder:
using System;
using UnityEngine;
using UnityEngine.UIElements;
// Shows the player's inventories in a bar at the bottom of the screen.
[RequireComponent(typeof(PanelRenderer))]
public class InventoryUi : MonoBehaviour {
[SerializeField] PlayerController player;
PanelRenderer panelRenderer;
IDisposable collectibleBar;
IDisposable buildableBar;
void OnEnable() {
panelRenderer = GetComponent<PanelRenderer>();
// Called right away if the UI is loaded already, and again whenever it reloads
panelRenderer.RegisterUIReloadCallback(OnUIReload);
}
void OnDisable() {
panelRenderer.UnregisterUIReloadCallback(OnUIReload);
Unbind();
}
void OnUIReload(PanelRenderer panel, VisualElement root, int version) {
Bind(root);
}
void Bind(VisualElement root) {
// A reload brings new elements, so the old bars go first
Unbind();
collectibleBar = new InventoryBar<CollectibleInventoryItem, CollectibleItem, CollectibleItemType>(
root.Q<VisualElement>("CollectibleItems"),
player.collectibles
);
buildableBar = new InventoryBar<BuildableInventoryItem, BuildableItem, BuildableItemType>(
root.Q<VisualElement>("BuildableItems"),
player.buildables
);
}
void Unbind() {
collectibleBar?.Dispose();
buildableBar?.Dispose();
collectibleBar = null;
buildableBar = null;
}
}The Panel Renderer loads the UXML itself, and the script must not look for elements before that has happened. RegisterUIReloadCallback takes care of the timing: Unity calls OnUIReload right away if the UI is loaded already, and again whenever it reloads. Every call brings the root element, and Bind creates the two bars for it, after Unbind has disposed of the old ones. OnDisable does the same, so a disabled UI doesn't follow the inventories anymore.
The script gets the player as a field in the Inspector rather than through a singleton. Unity loads every object of a scene, with its fields, before it calls the first Awake or OnEnable, so the player's inventories are there in time. A static Instance may not be, because the order in which different objects wake up is not fixed.
Setting up the scene
Choose GameObject > UI Toolkit > Panel Renderer. Unity creates a GameObject with a Panel Renderer and, in a folder
UI Toolkit, a Panel Settings asset with a default theme, which the Panel Renderer already uses.Drag
InventoryUi.uxmlinto the Source Asset field of the Panel Renderer.Add the
InventoryUicomponent to the same GameObject and drag the player into its Player field.
Press Play and pick up a few resources: their slots appear in the left row and count up. Craft a storage with Craft Storage from part 1, and the wood and stone count down while the storage appears on the right.
On Unity 6.3 LTS
Without a Panel Renderer, choose GameObject > UI Toolkit > UI Document instead, which creates the Panel Settings as well, and drag the UXML into its Source Asset. A UI Document builds its elements when it is enabled, which is why Unity's documentation looks them up in OnEnable. The controller then looks like this, with Bind and Unbind unchanged:
using System;
using UnityEngine;
using UnityEngine.UIElements;
// Unity 6.3 LTS and older: the same controller with a UI Document
[RequireComponent(typeof(UIDocument))]
public class InventoryUi : MonoBehaviour {
[SerializeField] PlayerController player;
IDisposable collectibleBar;
IDisposable buildableBar;
void OnEnable() {
// The UI Document builds its elements when it is enabled
Bind(GetComponent<UIDocument>().rootVisualElement);
}
void OnDisable() {
Unbind();
}
// Bind() and Unbind() as above
}The project now has this structure, with the files from part 1 where they were:
Assets/
├── Objects/
│ └── ...
├── UI/
│ ├── InventoryUi.uss
│ └── InventoryUi.uxml
├── UI Toolkit/
│ └── ...
└── Scripts/
├── Inventory/
│ └── ...
├── Items/
│ └── ...
├── UI/
│ ├── InventoryBar.cs
│ └── InventoryUi.cs
└── PlayerController.csFrom here the bar can grow with your game: the item's name as a tooltip, a click on a buildable to place its prefab, a short flash on the slot that just changed. Slots are created in AddSlot and updated in Refresh, so each of these goes into one place.