06 — Wiring It Up With C#
Open Scripts/HUDController.cs and Scripts/HealthScoreSystem.cs side by side. Two scripts, two jobs:
- HealthScoreSystem — the model. Health and score numbers, clamping rules, C# events. It contains zero UI code and would work in a dedicated server build.
- HUDController — the view. It never changes game state directly; it forwards clicks to the model and repaints when the model announces a change.
This separation (a lightweight MVC) is what Unity's own samples like Dragon Crashers use, and it's the habit worth grading for.
Getting into the UI
VisualElement root = GetComponent<UIDocument>().rootVisualElement;
Everything starts at the UIDocument's root element. Do this in OnEnable — the visual tree is rebuilt when a UIDocument is disabled/enabled, so OnEnable/OnDisable is the correct lifecycle pair (not Start).
Finding elements: UQuery
Button damageButton = root.Q<Button>("damage-button"); // by name
Label anyLabel = root.Q<Label>(); // first Label
var allButtons = root.Query<Button>().ToList(); // all of them
Q is short for Query. Cache the results in fields once; never call Q every frame.
Reacting to the UI
// Buttons: the clicked event
damageButton.clicked += () => stats.TakeDamage(10);
// Every value-holding control (Slider, Toggle, TextField, Dropdown...):
healthSlider.RegisterValueChangedCallback(evt =>
stats.SetHealth(Mathf.RoundToInt(evt.newValue)));
// Lower-level pointer events power the hover-description feature:
button.RegisterCallback<PointerEnterEvent>(_ => ShowDescription(...));
One pattern — RegisterValueChangedCallback — covers sliders, toggles, dropdowns, and text fields alike. Learn it once, use it everywhere.
Updating the UI
scoreLabel.text = $"Score: {newScore}"; // text
healthBar.value = current; // progress bar fill
healthBar.title = $"{current} / {max}"; // text on the bar
healthSlider.SetValueWithoutNotify(current); // sync a control WITHOUT re-firing its callback
lockedButton.SetEnabled(false); // triggers :disabled styling
SetValueWithoutNotify matters: when the model changes health, we move the debug slider to match — but if that fired the slider's own callback, we'd loop forever.
Driving USS from code
healthBar.AddToClassList("health-low"); // bar turns red via USS
scoreLabel.AddToClassList("score-pop"); // transition animates the pop
scoreLabel.schedule.Execute(() =>
scoreLabel.RemoveFromClassList("score-pop")).StartingIn(150);
C# decides when; USS decides what it looks like. schedule.Execute is UI Toolkit's built-in timer — handy for delays without coroutines.
Always unsubscribe
OnDisable mirrors every += with a -=. Skipping this causes ghost callbacks and memory leaks — the kit shows the full pattern.
Exercises
- Add a Reset button (UXML + USS + a
clickedhandler that sets health to max and score to 0). - Make the description panel show the player's name when the TextField changes (already wired — read how, then extend it to validate empty names).
- Harder: give
HealthScoreSystemaDiedevent when health hits 0, and show a "You Died" overlay by toggling a USS class that changesdisplay: none→flex.