diff --git a/inventory-property-drawers/Scripts/AmmoPropertyDrawer.cs b/inventory-property-drawers/Scripts/AmmoPropertyDrawer.cs index 8d9e13e..e921257 100644 --- a/inventory-property-drawers/Scripts/AmmoPropertyDrawer.cs +++ b/inventory-property-drawers/Scripts/AmmoPropertyDrawer.cs @@ -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; @@ -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; } -} \ No newline at end of file +} diff --git a/inventory-property-drawers/Scripts/GunPropertyDrawer.cs b/inventory-property-drawers/Scripts/GunPropertyDrawer.cs new file mode 100644 index 0000000..58cc88f --- /dev/null +++ b/inventory-property-drawers/Scripts/GunPropertyDrawer.cs @@ -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( + "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), + } + }; +} diff --git a/inventory-property-drawers/Scripts/HealthPack.cs b/inventory-property-drawers/Scripts/HealthPack.cs index 351a8e4..b02107a 100644 --- a/inventory-property-drawers/Scripts/HealthPack.cs +++ b/inventory-property-drawers/Scripts/HealthPack.cs @@ -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 }; } diff --git a/inventory-property-drawers/Scripts/Inventory.cs b/inventory-property-drawers/Scripts/Inventory.cs index 4894701..b69eba4 100644 --- a/inventory-property-drawers/Scripts/Inventory.cs +++ b/inventory-property-drawers/Scripts/Inventory.cs @@ -7,6 +7,15 @@ public partial class Inventory List m_Items = new List(); Dictionary m_ItemDictionary = new Dictionary(); + [UxmlAttribute] + public string description; + + [UxmlAttribute] + public int maxSlots = 10; + + [UxmlAttribute] + public float maxWeight = 50; + [UxmlAttribute] int nextItemId = 1; diff --git a/inventory-property-drawers/Scripts/InventoryPropertyDrawer.cs b/inventory-property-drawers/Scripts/InventoryPropertyDrawer.cs index 49f35a5..01dc67a 100644 --- a/inventory-property-drawers/Scripts/InventoryPropertyDrawer.cs +++ b/inventory-property-drawers/Scripts/InventoryPropertyDrawer.cs @@ -1,29 +1,43 @@ -// 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( + "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, @@ -31,85 +45,80 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) 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(); + GenericMenu menu = new GenericMenu(); + TypeCache.TypeCollection items = TypeCache.GetTypesDerivedFrom(); foreach (var item in items) { if (item.IsAbstract) @@ -117,14 +126,11 @@ void OnAddItem(BaseListView baseListView, Button button) 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); } -} \ No newline at end of file +} diff --git a/inventory-property-drawers/Scripts/SwordPropertyDrawer.cs b/inventory-property-drawers/Scripts/SwordPropertyDrawer.cs new file mode 100644 index 0000000..81ae734 --- /dev/null +++ b/inventory-property-drawers/Scripts/SwordPropertyDrawer.cs @@ -0,0 +1,56 @@ +// SwordPropertyDrawer showcases the C# approach to UxmlAttributeField and UxmlAttributeFieldDecorator. +// It overrides CreateChildPropertiesGUI to prepend a type label, then delegates to CreateChildPropertyGUI +// for each property. CreateChildPropertyGUI customizes slashDamage while letting the base class handle +// all other properties (name, weight) with the default UxmlAttributeField. +using Unity.UIToolkit.Editor; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +[CustomPropertyDrawer(typeof(Sword.UxmlSerializedData))] +public class SwordPropertyDrawer : UxmlSerializedDataPropertyDrawer +{ + protected override void CreateChildPropertiesGUI(VisualElement container, SerializedProperty property) + { + container.Add(ItemTypeLabel("Sword")); + base.CreateChildPropertiesGUI(container, property); + } + + protected override void CreateChildPropertyGUI(VisualElement container, SerializedProperty property, + SerializedProperty childProperty) + { + if (childProperty.name == "slashDamage") + { + // Pattern 3 & 4 in C#: UxmlAttributeFieldDecorator wrapping an explicit Slider. + // This uses the same control type as the Slider in InventoryDrawer.uxml, but created + // in code rather than UXML. + UxmlAttributeFieldDecorator decorator = new UxmlAttributeFieldDecorator(); + Slider slider = new Slider(childProperty.displayName, 1, 100) + { + showInputField = true, + bindingPath = childProperty.propertyPath + }; + slider.AddToClassList(Slider.alignedFieldUssClassName); + decorator.Add(slider); + container.Add(decorator); + } + else + { + // Pattern 3: let the base class create a UxmlAttributeField for name and weight. + base.CreateChildPropertyGUI(container, property, childProperty); + } + } + + 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), + } + }; +} diff --git a/inventory-property-drawers/UI/GunDrawer.uxml b/inventory-property-drawers/UI/GunDrawer.uxml new file mode 100644 index 0000000..a3f5405 --- /dev/null +++ b/inventory-property-drawers/UI/GunDrawer.uxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/inventory-property-drawers/UI/InventoryDrawer.uxml b/inventory-property-drawers/UI/InventoryDrawer.uxml new file mode 100644 index 0000000..57ef1d8 --- /dev/null +++ b/inventory-property-drawers/UI/InventoryDrawer.uxml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/inventory-property-drawers/UI/Sniper.uxml b/inventory-property-drawers/UI/Sniper.uxml index e51f57a..5aa537d 100644 --- a/inventory-property-drawers/UI/Sniper.uxml +++ b/inventory-property-drawers/UI/Sniper.uxml @@ -1,9 +1,9 @@ - + - +