Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 44 additions & 13 deletions inventory-property-drawers/Scripts/AmmoPropertyDrawer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// Note that this example creates a PropertyDrawer for the Ammo type because it's not a UxmlObject.
// `AmmoPropertyDrawer` inherits from `PropertyDrawer` because `Ammo` is a plain `[Serializable]` struct,
// not a `UxmlObject`. There is no `UxmlSerializedDataPropertyView` to establish a relative binding
// context, so the UI is built in C# and binding paths use absolute `SerializedProperty.propertyPath`
// values. A `ProgressBar` provides visual feedback for the current ammo fill level.
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
Expand All @@ -9,30 +12,58 @@ public class AmmoPropertyDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var root = new VisualElement { style = { flexDirection = FlexDirection.Row } };
VisualElement root = new VisualElement();

var count = property.FindPropertyRelative("count");
var maxCount = property.FindPropertyRelative("maxCount");
SerializedProperty count = property.FindPropertyRelative("count");
SerializedProperty maxCount = property.FindPropertyRelative("maxCount");

var ammoField = new IntegerField("Ammo") { isDelayed = true, bindingPath = count.propertyPath };
ammoField.TrackPropertyValue(count, p =>
VisualElement row = new VisualElement { style = { flexDirection = FlexDirection.Row } };

IntegerField countField = new IntegerField("Ammo")
{
isDelayed = true,
bindingPath = count.propertyPath
};
countField.AddToClassList(IntegerField.alignedFieldUssClassName);
row.Add(countField);
row.Add(new Label("/") { style = { marginLeft = 2, marginRight = 2 } });

IntegerField maxCountField = new IntegerField
{
isDelayed = true,
bindingPath = maxCount.propertyPath,
style = { width = 50 }
};
row.Add(maxCountField);
root.Add(row);

ProgressBar ammoBar = new ProgressBar();
root.Add(ammoBar);

void UpdateBar()
{
ammoBar.highValue = Mathf.Max(maxCount.intValue, 1);
ammoBar.value = count.intValue;
ammoBar.title = $"{count.intValue}/{maxCount.intValue}";
}

countField.TrackPropertyValue(count, p =>
{
count.intValue = Mathf.Min(p.intValue, maxCount.intValue);
property.serializedObject.ApplyModifiedProperties();
UpdateBar();
});
root.Add(ammoField);
root.Add(new Label("/"));

var countField = new IntegerField { isDelayed = true, bindingPath = maxCount.propertyPath };
countField.TrackPropertyValue(maxCount, p =>
maxCountField.TrackPropertyValue(maxCount, p =>
{
count.intValue = Mathf.Min(p.intValue, count.intValue);
count.intValue = Mathf.Min(count.intValue, p.intValue);
property.serializedObject.ApplyModifiedProperties();
UpdateBar();
});
root.Add(countField);

root.Bind(property.serializedObject);
UpdateBar();

return root;
}
}
}
53 changes: 53 additions & 0 deletions inventory-property-drawers/Scripts/GunPropertyDrawer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// `GunPropertyDrawer` showcases loading a UXML template inside a `UxmlSerializedDataPropertyDrawer`.
// The template uses `UxmlAttributeField` (pattern 1) and `UxmlAttributeFieldDecorator` (pattern 2)
// with `binding-path`. Because this is a `UxmlSerializedDataPropertyDrawer`, the binding context
// set by `UxmlSerializedDataPropertyView` makes those relative `binding-path` values resolve correctly.
//
// The ammo field is rendered by calling `CreateChildPropertyGUI`, which creates a `UxmlAttributeField`.
// `UxmlAttributeField` internally uses a `PropertyField`, which invokes `AmmoPropertyDrawer` and
// preserves the override indicator bar alongside the ammo count/max row and `ProgressBar`.
using Unity.UIToolkit.Editor;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

[CustomPropertyDrawer(typeof(Gun.UxmlSerializedData))]
public class GunPropertyDrawer : UxmlSerializedDataPropertyDrawer
{
// Cached to avoid a disk lookup on every drawer instantiation.
// Note: the path below must match the location of the UI folder in your project.
// If you move or rename the inventory-property-drawers folder, update this path accordingly.
static VisualTreeAsset s_Template;

protected override void CreateChildPropertiesGUI(VisualElement container, SerializedProperty property)
{
container.Add(ItemTypeLabel("Gun"));

// Pattern 1 & 2: load a UXML template that uses UxmlAttributeField and
// UxmlAttributeFieldDecorator with binding-path for name, weight, damage, and fireRate.
if (s_Template == null)
s_Template = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
"Assets/ui-toolkit-manual-code-examples/inventory-property-drawers/UI/GunDrawer.uxml");
if (s_Template != null)
container.Add(s_Template.Instantiate());

// Render the ammo property via CreateChildPropertyGUI. The base implementation creates a
// UxmlAttributeField, which wraps a PropertyField that delegates to AmmoPropertyDrawer.
SerializedProperty ammoProperty = property.FindPropertyRelative("ammo");
if (ammoProperty != null)
CreateChildPropertyGUI(container, property, ammoProperty);
}

static Label ItemTypeLabel(string typeName) => new Label(typeName)
{
style =
{
unityFontStyleAndWeight = FontStyle.Bold,
paddingLeft = 2,
paddingBottom = 2,
marginBottom = 2,
borderBottomWidth = 1,
borderBottomColor = new Color(0.5f, 0.5f, 0.5f, 0.3f),
}
};
}
3 changes: 3 additions & 0 deletions inventory-property-drawers/Scripts/HealthPack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public partial class Gun : Item
[UxmlAttribute]
public float damage;

[UxmlAttribute]
public float fireRate = 1;

[UxmlAttribute]
public Ammo ammo = new Ammo { count = 10, maxCount = 10 };
}
Expand Down
9 changes: 9 additions & 0 deletions inventory-property-drawers/Scripts/Inventory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ public partial class Inventory
List<Item> m_Items = new List<Item>();
Dictionary<int, Item> m_ItemDictionary = new Dictionary<int, Item>();

[UxmlAttribute]
public string description;

[UxmlAttribute]
public int maxSlots = 10;

[UxmlAttribute]
public float maxWeight = 50;

[UxmlAttribute]
int nextItemId = 1;

Expand Down
154 changes: 80 additions & 74 deletions inventory-property-drawers/Scripts/InventoryPropertyDrawer.cs
Original file line number Diff line number Diff line change
@@ -1,130 +1,136 @@
// 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.
// This drawer showcases four ways to create inspector fields for `UxmlSerializedData` properties:
//
// 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.
// 1. `UxmlAttributeField` in UXML – `binding-path` resolves relative to the `UxmlSerializedData`
// property because `UxmlSerializedDataPropertyView` sets up
// the binding context.
// 2. `UxmlAttributeFieldDecorator` in UXML – wraps an explicit field type in UXML while keeping
// the override indicator bar and context menu.
// 3. `UxmlAttributeField` in C# – creates a field programmatically from a `SerializedProperty`.
// 4. `UxmlAttributeFieldDecorator` in C# – wraps any `IBindable` element in code.
using Unity.UIToolkit.Editor;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEngine;
using UnityEditor.UIElements;

[CustomPropertyDrawer(typeof(Inventory.UxmlSerializedData))]
public class InventoryPropertyDrawer : PropertyDrawer
public class InventoryPropertyDrawer : UxmlSerializedDataPropertyDrawer
{
SerializedProperty m_InventoryProperty;
SerializedProperty m_ItemsProperty;
// Cached to avoid a disk lookup on every drawer instantiation.
// Note: the path below must match the location of the UI folder in your project.
// If you move or rename the inventory-property-drawers folder, update this path accordingly.
static VisualTreeAsset s_Template;

public override VisualElement CreatePropertyGUI(SerializedProperty property)
protected override void CreateChildPropertiesGUI(VisualElement container, SerializedProperty property)
{
m_InventoryProperty = property;
// Pattern 1 & 2: load a UXML template that uses UxmlAttributeField and
// UxmlAttributeFieldDecorator with binding-path to render maxSlots and maxWeight.
// The binding paths resolve relative to this UxmlSerializedData property automatically.
if (s_Template == null)
s_Template = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
"Assets/ui-toolkit-manual-code-examples/inventory-property-drawers/UI/InventoryDrawer.uxml");
if (s_Template != null)
container.Add(s_Template.Instantiate());

var root = new VisualElement();
// Pattern 3: create a UxmlAttributeField in C# for the description property.
container.Add(new UxmlAttributeField(property.FindPropertyRelative("description")));

m_ItemsProperty = property.FindPropertyRelative("items");
var items = new ListView
// Pattern 4: create a UxmlAttributeFieldDecorator in C# to wrap the items ListView.
SerializedProperty itemsProperty = property.FindPropertyRelative("items");
ListView items = new ListView
{
showAddRemoveFooter = true,
showBorder = true,
showFoldoutHeader = false,
reorderable = true,
virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight,
reorderMode = ListViewReorderMode.Animated,
bindingPath = m_ItemsProperty.propertyPath,
overridingAddButtonBehavior = OnAddItem
bindingPath = itemsProperty.propertyPath,
overridingAddButtonBehavior = (baseListView, button) => OnAddItem(property, baseListView, button)
};
root.Add(items);

var addSniperGear = new Button(() =>
UxmlAttributeFieldDecorator listViewDecorator = new UxmlAttributeFieldDecorator();
listViewDecorator.Add(items);
container.Add(listViewDecorator);

container.Add(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(property, "Rifle", 4.5f, 33, 2.5f, 30, 30);
AddSword(property, "Knife", 0.5f, 7);
AddHealthPack(property);
property.serializedObject.ApplyModifiedProperties();
}) { text = "Add Sniper Gear" });

container.Add(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(property, "Rifle", 4.5f, 33, 2.5f, 30, 30);
AddHealthPack(property);
AddSword(property, "Machete", 1, 11);
property.serializedObject.ApplyModifiedProperties();
}) { text = "Add Warrior Gear" });

container.Add(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;
AddGun(property, "Pistol", 1.5f, 10, 1f, 15, 15);
AddHealthPack(property);
AddHealthPack(property);
AddHealthPack(property);
property.serializedObject.ApplyModifiedProperties();
}) { text = "Add Medic Gear" });
}

// Appends a new item of the given type to the items array and assigns its ID.
// Returns the SerializedProperty for the new element so callers can set type-specific fields.
SerializedProperty AppendItem(SerializedProperty property, System.Type itemType)
{
SerializedProperty itemsProperty = property.FindPropertyRelative("items");
itemsProperty.arraySize++;
SerializedProperty newItem = itemsProperty.GetArrayElementAtIndex(itemsProperty.arraySize - 1);
newItem.managedReferenceValue = UxmlSerializedDataCreator.CreateUxmlSerializedData(itemType);
newItem.FindPropertyRelative("id").intValue = NextItemId(property);
return newItem;
}

void AddGun(string name, float weight, float damage, int ammo, int maxAmmo)
void AddGun(SerializedProperty property, string name, float weight, float damage, float fireRate, 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();
SerializedProperty newItem = AppendItem(property, typeof(Gun));
newItem.FindPropertyRelative("name").stringValue = name;
newItem.FindPropertyRelative("weight").floatValue = weight;
newItem.FindPropertyRelative("damage").floatValue = damage;
newItem.FindPropertyRelative("fireRate").floatValue = fireRate;
var ammoInstance = newItem.FindPropertyRelative("ammo");
ammoInstance.FindPropertyRelative("count").intValue = ammo;
ammoInstance.FindPropertyRelative("maxCount").intValue = maxAmmo;
}

void AddSword(string name, float weight, float damage)
void AddSword(SerializedProperty property, 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();
SerializedProperty newItem = AppendItem(property, typeof(Sword));
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();
}
void AddHealthPack(SerializedProperty property) => AppendItem(property, typeof(HealthPack));

int NextItemId() => m_InventoryProperty.FindPropertyRelative("nextItemId").intValue++;
int NextItemId(SerializedProperty property) => property.FindPropertyRelative("nextItemId").intValue++;

void OnAddItem(BaseListView baseListView, Button button)
void OnAddItem(SerializedProperty property, BaseListView baseListView, Button button)
{
var menu = new GenericMenu();
var items = TypeCache.GetTypesDerivedFrom<Item>();
GenericMenu menu = new GenericMenu();
TypeCache.TypeCollection 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();
AppendItem(property, item);
property.serializedObject.ApplyModifiedProperties();
});
}

menu.DropDown(button.worldBound);
}
}
}
Loading