quot;{gold:N0} gold"); // int -> string goldLabel.SetBinding("text", binding); ``` For converters shared across the project, create a `ConverterGroup`, add converters, register it with `ConverterGroups.RegisterConverterGroup(...)`, and reference it by id (`source-to-ui-converters` in UXML). Check the converter pages in the manual (links at the end) for the exact registration API in your version. > [!tip] Binding limits > Bindings set *properties*. They cannot toggle a USS class. For "red when low", either (a) bind a bool/enum to a custom control that sets classes in its property setter (see 4.6), or (b) subscribe to the source event and call `EnableInClassList("bar--low", ratio < 0.25f)`. ## 4.6 Custom control: `ResourceBar` A reusable bar with a value, a max, a label, a class-driven low state, and bindable properties. It animates by scaling an inner element (`scale` does not dirty layout, unlike `width`). ### 4.6a Rules for `[UxmlElement]` controls (Unity 6) - Class must be `partial` (Unity's source generator adds the UXML factory) and have a public parameterless constructor. - `[UxmlElement]` on the class; `[UxmlAttribute]` on each property that should be an attribute in UXML. Attribute names are the property names converted to kebab-case (`MaxValue` -> `max-value`): check the Builder's Attributes list. - For bindings add `[CreateProperty]`, a `static readonly BindingId` per property, and call `NotifyPropertyChanged(...)` in the setter **only when the value changed**. - Use a namespace and reference it in UXML with an `xmlns` alias. - Styling comes from USS classes you document (`resource-bar`, ...), not from loading assets in the constructor. ### 4.6b `ResourceBar.cs` (`Assets/UI/Controls/`) ```csharp using Unity.Properties; using UnityEngine; using UnityEngine.UIElements; namespace Game.UI { [UxmlElement] public partial class ResourceBar : VisualElement { public const string UssClassName = "resource-bar"; public const string FillUssClassName = "resource-bar__fill"; public const string LabelUssClassName = "resource-bar__label"; public const string LowUssClassName = "resource-bar--low"; static readonly BindingId ValueProperty = nameof(Value); static readonly BindingId MaxValueProperty = nameof(MaxValue); static readonly BindingId LabelProperty = nameof(Label); readonly VisualElement fill; readonly Label labelElement; float value; float maxValue = 100f; string label = string.Empty; [UxmlAttribute, CreateProperty] public float Value { get => value; set { float clamped = Mathf.Clamp(value, 0f, maxValue); if (Mathf.Approximately(this.value, clamped)) return; this.value = clamped; Refresh(); NotifyPropertyChanged(ValueProperty); } } [UxmlAttribute, CreateProperty] public float MaxValue { get => maxValue; set { float v = Mathf.Max(1f, value); if (Mathf.Approximately(maxValue, v)) return; maxValue = v; this.value = Mathf.Min(this.value, maxValue); Refresh(); NotifyPropertyChanged(MaxValueProperty); } } [UxmlAttribute, CreateProperty] public string Label { get => label; set { if (label == value) return; label = value; Refresh(); NotifyPropertyChanged(LabelProperty); } } public ResourceBar() { AddToClassList(UssClassName); pickingMode = PickingMode.Ignore; fill = new VisualElement { name = "fill", pickingMode = PickingMode.Ignore }; fill.AddToClassList(FillUssClassName); Add(fill); labelElement = new Label { name = "label", pickingMode = PickingMode.Ignore }; labelElement.AddToClassList(LabelUssClassName); Add(labelElement); Refresh(); } void Refresh() { float ratio = maxValue <= 0f ? 0f : value / maxValue; // scale (not width): no layout pass. USS sets transform-origin: left center. fill.style.scale = new Scale(new Vector3(ratio, 1f, 1f)); labelElement.text = string.IsNullOrEmpty(label) ? quot;{Mathf.CeilToInt(value)} / {Mathf.CeilToInt(maxValue)}" : quot;{label} {Mathf.CeilToInt(value)} / {Mathf.CeilToInt(maxValue)}"; EnableInClassList(LowUssClassName, ratio < 0.25f); } } } ``` Points to notice: - Inside setters we use `this.value` for the field and `value` for the implicit setter argument. If that reads badly, rename the fields. - `NotifyPropertyChanged` is inherited from `VisualElement`; it raises the signal binding listens for (`INotifyBindablePropertyChanged`). - `Refresh()` runs in the constructor so the control renders sensibly in UI Builder with default values. - Guard with `Mathf.Approximately`/equality: otherwise setting a bound property to the same value every frame would notify every frame. ### 4.6c USS for the control (add to `Hud.uss` or its own file) ```css .resource-bar { height: 22px; background-color: rgba(255, 255, 255, 0.08); border-radius: 4px; overflow: hidden; justify-content: center; } .resource-bar__fill { position: absolute; left: 0; top: 0; bottom: 0; right: 0; /* full size; the scale shrinks it */ background-color: var(--color-accent); transform-origin: left center; transition-property: scale, background-color; transition-duration: 0.15s; transition-timing-function: ease-out; } .resource-bar__label { -unity-text-align: middle-center; font-size: 12px; margin: 0; } .resource-bar--low .resource-bar__fill { background-color: var(--color-health); } ``` ### 4.6d Use it in the HUD In `Hud.uxml`, add an `xmlns` alias to the root tag and replace the `ProgressBar` and hp label: ```xml <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:game="Game.UI" editor-extension-mode="False"> ... <game:ResourceBar name="hp-bar" label="HP" /> ... </ui:UXML> ``` In `HudController.OnUIReload`, replace the two `hpBar` binding lines and the `hp-text` line: ```csharp var hpBar = Require<Game.UI.ResourceBar>(root, "hp-bar"); hpBar.SetBinding(nameof(Game.UI.ResourceBar.MaxValue), ToTarget(nameof(PlayerStats.MaxHealth))); hpBar.SetBinding(nameof(Game.UI.ResourceBar.Value), ToTarget(nameof(PlayerStats.Health))); ``` Binding order matters slightly: if `Value` bound first while `MaxValue` were still the default 100, a health of 250 would be clamped to 100. That is why `MaxValue` is bound first above. Then play: damage the player and watch the fill shrink with a transition and flip red under 25 percent. > [!note] UI Builder refresh > After a script change, the new control shows up in the Builder Library under *Project > Custom Controls (C#)*. If the XML shows "unknown element", wait for compilation to finish and reimport the UXML. ## 4.7 Diagnosing binding problems - **Console messages**: a failed binding logs a warning/error naming the element, property and path. Misspelled property names are the usual cause. - **Not updating**: the source did not notify (4.3), the property is missing `[CreateProperty]`, or you changed the field instead of the property. - **Updates once, never again**: the binding mode is `ToTargetOnce`. - **Debugger**: the UI Toolkit Debugger shows the applied bindings on a selected element in recent Unity 6 versions (check the Bindings section of its inspector). - **Polling cost**: a source that does not notify and a binding with `EveryUpdate` both re-evaluate each frame. Profile before you scale up. ## 4.8 Further reading - Runtime binding overview: https://docs.unity3d.com/Manual/UIE-runtime-binding.html - Define a data source: https://docs.unity3d.com/6000.2/Documentation/Manual/UIE-runtime-binding-define-data-source.html - Binding types and C# examples: https://docs.unity3d.com/Manual/UIE-runtime-binding-types.html - Custom controls: https://docs.unity3d.com/6000.3/Documentation/Manual/UIE-create-custom-controls.html - Customize UXML attributes: https://docs.unity3d.com/Manual/ui-systems/custom-control-customize-uxml-attributes.html Next: [[5.Diegetic UI Approaches]].