You can create custom property drawersA Unity feature that allows you to customize the look of certain controls in the Inspector window by using attributes on your scripts, or by controlling how a specific Serializable class should look More info
See in Glossary to customize the appearance and behavior of UXML attributes of a custom control in the InspectorA Unity window that displays information about the currently selected GameObject, asset or project settings, allowing you to inspect and edit the values. More info
See in Glossary panel of the UI Builder.
SerializedFields support custom property drawers, similar to ScriptableObject or MonoBehaviour. You can apply a custom property drawer to either the type or the field.
This example creates an inventory system which includes an Item
class, a variety of items, and an Inventory
class. The Inventory
class manages inventory items inside a visual elementA node of a visual tree that instantiates or derives from the C# VisualElement
class. You can style the look, define the behaviour, and display it on screen as part of the UI. More info
See in Glossary. The example uses custom property drawers to manage the inventory system.
You can find the completed files that this example creates in this GitHub repository.
This guide is for developers familiar with the Unity Editor, UI Toolkit, and C# scripting. Before you start, get familiar with the following:
CustomPropertyDrawer
UxmlObject
UxmlAttribute
UxmlSerializedDataCreator.CreateUxmlSerializedData
First, create an Item
class. This class is abstract and serves as a blueprint for all types of objects, encompassing their shared properties. Next, create a variety of items, including a heath pack and different types of weapons.
Create a project in Unity with any template.
In your Project window, create a folder named inventory-property-drawers
to store your files.
In the inventory-property-drawers
folder, create a subfolder named Scripts
to store your C# scriptsA piece of code that allows you to create your own Components, trigger game events, modify Component properties over time and respond to user input in any way you like. More info
See in Glossary.
In the Scripts
folder, create a C# script named Item.cs
with the following content:
using UnityEngine.UIElements;
using UnityEngine;
[UxmlObject]
public abstract partial class Item
{
[UxmlAttribute, HideInInspector]
public int id;
[UxmlAttribute]
public string name;
[UxmlAttribute]
public float weight;
}
In the Scripts
folder, create a C# script named HealthPack.cs
with the following content:
using System;
using UnityEngine;
using UnityEngine.UIElements;
[UxmlObject]
public partial class HealthPack : Item
{
[UxmlAttribute]
public float healAmount = 100;
public HealthPack()
{
name = "Health Pack";
}
}
[UxmlObject]
public partial class Sword : Item
{
[UxmlAttribute, Range(1, 100)]
public float slashDamage;
}
[Serializable]
public class Ammo
{
public int count;
public int maxCount;
}
[UxmlObject]
public partial class Gun : Item
{
[UxmlAttribute]
public float damage;
[UxmlAttribute]
public Ammo ammo = new Ammo { count = 10, maxCount = 10 };
}
This example uses a custom attribute named Ammo
, therefore, you must define an attribute converter for it. You need an Inventory
class to store all items. To use the inventory, create a Character
custom control to manage the inventory items.
In the Scripts
folder, create a C# script named AmmoConverter.cs
with the following content:
using UnityEditor.UIElements;
public class AmmoConverter : UxmlAttributeConverter<Ammo>
{
public override Ammo FromString(string value)
{
var ammo = new Ammo();
var values = value.Split('/');
if (values.Length == 2)
{
int.TryParse(values[0], out ammo.count);
int.TryParse(values[1], out ammo.maxCount);
}
return ammo;
}
public override string ToString(Ammo value)
{
return $"{value.count}/{value.maxCount}";
}
}
In the Scripts
folder, create a C# script named Inventory.cs
with the following content:
using System.Collections.Generic;
using UnityEngine.UIElements;
[UxmlObject]
public partial class Inventory
{
List<Item> m_Items = new List<Item>();
Dictionary<int, Item> m_ItemDictionary = new Dictionary<int, Item>();
[UxmlAttribute]
int nextItemId = 1;
[UxmlObjectReference("Items")]
public List<Item> items
{
get => m_Items;
set
{
m_Items = value;
m_ItemDictionary.Clear();
foreach (var item in m_Items)
{
m_ItemDictionary[item.id] = item;
}
}
}
public Item GetItem(int id) => m_ItemDictionary.TryGetValue(id, out var item) ? item : null;
}
When you add the Character
element to UI Builder, you can manage inventory items,but the ID value isn’t assigned automatically. To fix this, add a InventoryPropertyDrawer
to the Inventory
class. This allows you to manage the inventory items directly in the UI Builder. Note that when you add the custom property drawer, edit the UxmlSerializedData
not the Inventory
class directly.
To ensure the Ammo
class to clamp the count value to be less than maxCount
, create a AmmoPropertyDrawer
for the Ammo
class.
In the Scripts
folder, create a C# script named InventoryPropertyDrawer.cs
with the following content:
// When you add a UxmlObject to the inventory list, include an instance of UxmlSerializedData, not an Item.
// To simplify this process, this example uses `UxmlSerializedDataCreator.CreateUxmlSerializedData`,
// a utility method that creates a UxmlObject’s UxmlSerializedData with default values.
//
// In this approach, the assignment of an ID value is introduced. To manage this, the last used ID value is stored
// within the element as a hidden field labeled `nextItemId`. Additionally, buttons are incorporated to add preconfigured
// sets of items. For instance, a Soldier might receive a Rifle, Machete, and Performance Pack.
using UnityEditor;
using UnityEngine.UIElements;
using UnityEngine;
using UnityEditor.UIElements;
[CustomPropertyDrawer(typeof(Inventory.UxmlSerializedData))]
public class InventoryPropertyDrawer : PropertyDrawer
{
SerializedProperty m_InventoryProperty;
SerializedProperty m_ItemsProperty;
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
m_InventoryProperty = property;
var root = new VisualElement();
m_ItemsProperty = property.FindPropertyRelative("items");
var items = new ListView
{
showAddRemoveFooter = true,
showBorder = true,
showFoldoutHeader = false,
reorderable = true,
virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight,
reorderMode = ListViewReorderMode.Animated,
bindingPath = m_ItemsProperty.propertyPath,
overridingAddButtonBehavior = OnAddItem
};
root.Add(items);
var addSniperGear = new Button(() =>
{
AddGun("Rifle", 4.5f, 33, 30, 30);
AddSword("Knife", 0.5f, 7);
AddHealthPack();
m_InventoryProperty.serializedObject.ApplyModifiedProperties();
});
addSniperGear.text = "Add Sniper Gear";
var addWarriorGear = new Button(() =>
{
AddGun("Rifle", 4.5f, 33, 30, 30);
AddHealthPack();
AddSword("Machete", 1, 11);
m_InventoryProperty.serializedObject.ApplyModifiedProperties();
});
addWarriorGear.text = "Add Warrior Gear";
var addMedicGear = new Button(() =>
{
AddGun("Pistol", 1.5f, 10, 15, 15);
AddHealthPack();
AddHealthPack();
AddHealthPack();
m_InventoryProperty.serializedObject.ApplyModifiedProperties();
});
addMedicGear.text = "Add Medic Gear";
root.Add(addSniperGear);
root.Add(addWarriorGear);
root.Add(addMedicGear);
root.Bind(property.serializedObject);
return root;
}
void AddGun(string name, float weight, float damage, int ammo, int maxAmmo)
{
m_ItemsProperty.arraySize++;
var newItem = m_ItemsProperty.GetArrayElementAtIndex(m_ItemsProperty.arraySize - 1);
newItem.managedReferenceValue = UxmlSerializedDataCreator.CreateUxmlSerializedData(typeof(Gun));
newItem.FindPropertyRelative("id").intValue = NextItemId();
newItem.FindPropertyRelative("name").stringValue = name;
newItem.FindPropertyRelative("weight").floatValue = weight;
newItem.FindPropertyRelative("damage").floatValue = damage;
var ammoInstance = newItem.FindPropertyRelative("ammo");
ammoInstance.FindPropertyRelative("count").intValue = ammo;
ammoInstance.FindPropertyRelative("maxCount").intValue = maxAmmo;
}
void AddSword(string name, float weight, float damage)
{
m_ItemsProperty.arraySize++;
var newItem = m_ItemsProperty.GetArrayElementAtIndex(m_ItemsProperty.arraySize - 1);
newItem.managedReferenceValue = UxmlSerializedDataCreator.CreateUxmlSerializedData(typeof(Sword));
newItem.FindPropertyRelative("id").intValue = NextItemId();
newItem.FindPropertyRelative("name").stringValue = name;
newItem.FindPropertyRelative("weight").floatValue = weight;
newItem.FindPropertyRelative("slashDamage").floatValue = damage;
}
void AddHealthPack()
{
m_ItemsProperty.arraySize++;
var newItem = m_ItemsProperty.GetArrayElementAtIndex(m_ItemsProperty.arraySize - 1);
newItem.managedReferenceValue = UxmlSerializedDataCreator.CreateUxmlSerializedData(typeof(HealthPack));
newItem.FindPropertyRelative("id").intValue = NextItemId();
}
int NextItemId() => m_InventoryProperty.FindPropertyRelative("nextItemId").intValue++;
void OnAddItem(BaseListView baseListView, Button button)
{
var menu = new GenericMenu();
var items = TypeCache.GetTypesDerivedFrom<Item>();
foreach (var item in items)
{
if (item.IsAbstract)
continue;
menu.AddItem(new GUIContent(item.Name), false, () =>
{
m_ItemsProperty.arraySize++;
var newItem = m_ItemsProperty.GetArrayElementAtIndex(m_ItemsProperty.arraySize - 1);
newItem.managedReferenceValue = UxmlSerializedDataCreator.CreateUxmlSerializedData(item);
newItem.FindPropertyRelative("id").intValue = NextItemId();
m_InventoryProperty.serializedObject.ApplyModifiedProperties();
});
}
menu.DropDown(button.worldBound);
}
}
In the Scripts
folder, create a C# script named AmmoPropertyDrawer.cs
with the following content:
// Note that this example creates a PropertyDrawer for the Ammo type because it's not a UxmlObject.
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
[CustomPropertyDrawer(typeof(Ammo))]
public class AmmoPropertyDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var root = new VisualElement { style = { flexDirection = FlexDirection.Row } };
var count = property.FindPropertyRelative("count");
var maxCount = property.FindPropertyRelative("maxCount");
var ammoField = new IntegerField("Ammo") { isDelayed = true, bindingPath = count.propertyPath };
ammoField.TrackPropertyValue(count, p =>
{
count.intValue = Mathf.Min(p.intValue, maxCount.intValue);
property.serializedObject.ApplyModifiedProperties();
});
root.Add(ammoField);
root.Add(new Label("/"));
var countField = new IntegerField { isDelayed = true, bindingPath = maxCount.propertyPath };
countField.TrackPropertyValue(maxCount, p =>
{
count.intValue = Mathf.Min(p.intValue, count.intValue);
property.serializedObject.ApplyModifiedProperties();
});
root.Add(countField);
root.Bind(property.serializedObject);
return root;
}
}
To test the inventory system, in UI Builder, add a Character
element and add items to the inventory.
Character
element from the Library panel to the Hierarchy panel.Did you find this page useful? Please give it a rating:
Thanks for rating this page!
What kind of problem would you like to report?
Thanks for letting us know! This page has been marked for review based on your feedback.
If you have time, you can provide more information to help us fix the problem faster.
Provide more information
You've told us this page needs code samples. If you'd like to help us further, you could provide a code sample, or tell us about what kind of code sample you'd like to see:
You've told us there are code samples on this page which don't work. If you know how to fix it, or have something better we could use instead, please let us know:
You've told us there is information missing from this page. Please tell us more about what's missing:
You've told us there is incorrect information on this page. If you know what we should change to make it correct, please tell us:
You've told us this page has unclear or confusing information. Please tell us more about what you found unclear or confusing, or let us know how we could make it clearer:
You've told us there is a spelling or grammar error on this page. Please tell us what's wrong:
You've told us this page has a problem. Please tell us more about what's wrong:
Thank you for helping to make the Unity documentation better!
Your feedback has been submitted as a ticket for our documentation team to review.
We are not able to reply to every ticket submitted.