Progress Bar Architecture

Progress Bar family — one shared animation core (ProgressBarData + ProgressBarController), several shapes on top. This page documents that core; siblings: Overview · Linear · Radial · RPG Demo.

This page documents the internal architecture of the Progress Bar system. Use this guide when you need to:

  • Create custom progress bar visuals (not rectangular or radial)
  • Implement custom animation behaviors
  • Understand how the animation system works
  • Build progress bars for non-UI Toolkit systems

Overview

The Progress Bar system uses a separation of concerns architecture:

The controller depends on IProgressBarAnimator (scheduling) and IProgressBarNodeUpdater<TElement> (rendering). IProgressBarVisualUpdater is separate—it styles the rectangular ProgressBar's chrome (background, icon, overlay, title) and is not part of the animation pipeline.

Core Classes

ProgressBarData

Pure data container holding all state and configuration. UI-agnostic—can be used with any rendering system. No visual references or animation scheduling; visuals subscribe to its events.

csharp
public class ProgressBarData { // Animation settings public bool InstantPositive { get; set; } public bool InstantNegative { get; set; } public long UpdateFrequency { get; set; } public float Step { get; set; } public long FirstDelay { get; set; } public long SecondDelay { get; set; } public bool UseTimeScale { get; set; } // Scale animation by Time.timeScale // Progress segments public ProgressBarNode[] Segments { get; set; } public ProgressBarNode Indicator { get; set; } // Indicator appearance public bool IndicatorAutoColor { get; set; } public Color IndicatorColorPositive { get; set; } public Color IndicatorColorNegative { get; set; } // Events visuals subscribe to public Action<int, float> OnSegmentCurrentValueChanged; public Action<float> OnIndicatorCurrentValueChanged; public Action OnSegmentsChanged; public Action<int, bool> OnSegmentAnimationComplete; public Action<bool> OnIndicatorAnimationComplete; // Segment management public void SetSegments(ProgressBarNode[] segments); public void RebuildSegments(int count); }

ProgressBarNode

Represents a single progress segment with current/target values and color.

csharp
public class ProgressBarNode { public float CurrentValue { get; set; } // 0-1, setter fires OnCurrentValueChanged public float TargetValue { get; set; } // 0-1, value animating toward public Color Color { get; set; } // Fired when CurrentValue changes public Action<float> OnCurrentValueChanged; // Silent setter (no event) — used for initialization public void SetCurrentValueSilent(float value); }

ProgressBarController<TElement>

Generic animation orchestrator that works with any UI element type. Handles:

  • Animation timing and scheduling
  • Coordinating progress vs indicator animations
  • Determining which bar moves first based on positive/negative changes
csharp
public class ProgressBarController<TElement> : IDisposable { public ProgressBarController( ProgressBarData data, IProgressBarAnimator animator, IProgressBarNodeUpdater<TElement> nodeUpdater, Func<int, TElement> getSegmentElement, Func<TElement> getIndicatorElement ); // Invoked when IndicatorAutoColor needs a style swap (UI-specific) public Action<bool> OnIndicatorStyleChanged; public void PlayProgress(); public void PlayIndicator(); public void InstantAllSegments(); public void SetIndicatorColor(bool positive); public void StopAll(); public void Dispose(); }

Instead of holding element references, the controller resolves visual elements through the getSegmentElement / getIndicatorElement callbacks—so the view can rebuild its hierarchy without re-wiring the controller.

Interfaces

IProgressBarAnimator

Drives animations: implementations step ProgressBarNode.CurrentValue toward TargetValue over time, which fires data events that visuals react to. The default implementation is UIToolkitProgressBarAnimator, which routes all ticking through Luna's single global LunaTicker dispatcher instead of allocating per-element VisualElement.schedule tasks.

csharp
public interface IProgressBarAnimator { void AnimateSegment(int index, long delay, Action<float> onUpdate, Action onComplete); void AnimateIndicator(long delay, Action<float> onUpdate, Action onComplete); void StopSegment(int index); void StopIndicator(); void StopAll(); }

IProgressBarNodeUpdater<TElement>

Applies data changes to the actual UI element. Stateless—receives full context with each call. Implement this to create custom rendering (shader-based, transform-based, etc.).

csharp
public interface IProgressBarNodeUpdater<TElement> { void SetValue(ProgressBarNodeType nodeType, int index, TElement element, float value); void UpdateValue(ProgressBarNodeType nodeType, int index, TElement element, float value); void UpdatePosition(ProgressBarNodeType nodeType, int index, TElement element, float position); void SetColor(ProgressBarNodeType nodeType, int index, TElement element, Color color); void OnAnimationStart(ProgressBarNodeType nodeType, int index, TElement element, bool positive); void OnAnimationComplete(ProgressBarNodeType nodeType, int index, TElement element, bool positive); } // Non-generic alias for UI Toolkit public interface IProgressBarNodeUpdater : IProgressBarNodeUpdater<VisualElement> { }

ProgressBarNodeType distinguishes Progress segments from the Indicator. The default implementation, DefaultProgressBarNodeUpdater, animates the element's width style.

IProgressBarVisualUpdater

Styles the rectangular ProgressBar's non-animated chrome—background, icon, overlay image, and title. Not used by the animation pipeline; swap it via ProgressBar.SetVisualUpdater().

csharp
public interface IProgressBarVisualUpdater { void SetBackgroundColor(VisualElement background, Color color, string ussClass); void SetIconColor(VisualElement icon, Color backgroundColor, Color tintColor, string ussClass); void SetIconVisibility(VisualElement icon, bool visible); void SetIconSize(VisualElement icon, int size); void SetOverlayTint(VisualElement container, Color color); void SetOverlayImage(VisualElement container, StyleBackground image); void SetOverlayImageSize(VisualElement container, int width); void SetOverflow(VisualElement container, VisualElement parent, bool visible); void SetTitleText(Label label, string text); }

Creating Custom Progress Bars

Step 1: Create Your Visual Element

csharp
public class MyCustomProgressBar : VisualElement { private ProgressBarData _data; public MyCustomProgressBar() { _data = new ProgressBarData { Segments = new[] { new ProgressBarNode() }, Indicator = new ProgressBarNode() }; } }

Step 2: Implement Node Updater

csharp
public class MyCustomNodeUpdater : IProgressBarNodeUpdater<VisualElement> { public void SetValue(ProgressBarNodeType nodeType, int index, VisualElement element, float value) { // Set value instantly (no animation) } public void UpdateValue(ProgressBarNodeType nodeType, int index, VisualElement element, float value) { // Your custom rendering logic per animation step // e.g., update shader parameters, move sprites, etc. } public void UpdatePosition(ProgressBarNodeType nodeType, int index, VisualElement element, float position) { } public void SetColor(ProgressBarNodeType nodeType, int index, VisualElement element, Color color) { } public void OnAnimationStart(ProgressBarNodeType nodeType, int index, VisualElement element, bool positive) { } public void OnAnimationComplete(ProgressBarNodeType nodeType, int index, VisualElement element, bool positive) { } }

Step 3: Wire Up Controller

In the constructor, forward ProgressBarData's value-changed events to your node updater and register the attach/detach callbacks. Then create the animator and controller after the element attaches to a panel (the animator needs a host element for LunaTicker scheduling), and dispose on detach:

csharp
public class MyCustomProgressBar : VisualElement { private UIToolkitProgressBarAnimator _animator; private ProgressBarController<VisualElement> _controller; private MyCustomNodeUpdater _nodeUpdater; // Your element accessors — return the fill/indicator child elements your // updater renders into. This minimal example just returns the bar itself. private VisualElement GetSegmentElement(int index) => this; private VisualElement GetIndicatorElement() => this; public MyCustomProgressBar() { // ... _data construction from Step 1 ... // Forward data events to the node updater — this is what makes // UpdateValue run on every animation tick. _nodeUpdater = new MyCustomNodeUpdater(); _data.OnSegmentCurrentValueChanged += (index, value) => _nodeUpdater.UpdateValue(ProgressBarNodeType.Progress, index, GetSegmentElement(index), value); _data.OnIndicatorCurrentValueChanged += value => _nodeUpdater.UpdateValue(ProgressBarNodeType.Indicator, 0, GetIndicatorElement(), value); RegisterCallback<AttachToPanelEvent>(OnAttach); RegisterCallback<DetachFromPanelEvent>(OnDetach); } private void OnAttach(AttachToPanelEvent evt) { _animator = new UIToolkitProgressBarAnimator(this, _data); _controller = new ProgressBarController<VisualElement>( _data, _animator, _nodeUpdater, index => GetSegmentElement(index), () => GetIndicatorElement() ); } private void OnDetach(DetachFromPanelEvent evt) { _controller?.Dispose(); _controller = null; _animator = null; } public void PlayProgress() => _controller?.PlayProgress(); public void PlayIndicator() => _controller?.PlayIndicator(); }

The controller never calls UpdateValue itself—per-tick rendering flows through ProgressBarData's value-changed events, which your element must forward to the node updater (this is exactly what the built-in ProgressBar does in its constructor). Subscribing once in the constructor is safe because _data is owned by the element and shares its lifetime.

For non-UI Toolkit systems, implement IProgressBarAnimator yourself (e.g., driven by MonoBehaviour.Update) and use any TElement type—the controller and data layer have no UI Toolkit dependency.

Animation Flow

When PlayProgress() / PlayIndicator() are called:

Both animations are scheduled immediately—sequencing comes purely from the FirstDelay / SecondDelay values, not from completion chaining. Each tick moves CurrentValue by a dynamic step (larger when far from the target), scaled by Time.timeScale when UseTimeScale is enabled.

Built-in Implementations

ProgressBar (Rectangular)

  • Uses child VisualElements for segments
  • Animates width style property
  • Optional vertical line marker at the progress edge (VerticalLine)
  • Optional gap lines dividing the bar into cells (GapLineCount)
  • Includes overlay image support

RadialProgressBar (Circular)

  • Uses MeshGenerationContext for custom arc rendering
  • Draws arcs as stroked Painter2D paths on layered elements (track, fill, indicator)
  • Supports multi-segment cell mode (Overwatch-style)
  • Each segment rendered as consecutive arc

File Structure

ProgressBar/ ├── Core/ │ ├── ProgressBarData.cs # Pure data container │ ├── ProgressBarNode.cs # Single segment data │ └── ProgressBarController.cs # Animation orchestration ├── Interfaces/ │ ├── IProgressBarAnimator.cs # Animation scheduling │ ├── IProgressBarNodeUpdater.cs # Value/visual rendering strategy │ └── IProgressBarVisualUpdater.cs # Chrome styling (background, icon, overlay, title) └── UIToolkit/ ├── ProgressBar.cs # Rectangular implementation (partial: core + events) ├── ProgressBar.Settings.cs # Rectangular implementation (partial: UXML attributes, styling) ├── ProgressBar.GapLines.cs # Rectangular implementation (partial: gap line feature) ├── RadialProgressBar.cs # Circular implementation (partial: core + events) ├── RadialProgressBar.Settings.cs # Circular implementation (partial: UXML attributes, styling) ├── RadialProgressBar.Rendering.cs # Circular implementation (partial: Painter2D arc painting) ├── ProgressBarExtensions.cs # Convenience extension methods ├── UIToolkitProgressBarAnimator.cs # Default animator (LunaTicker-driven) ├── DefaultProgressBarNodeUpdater.cs # Width-based rendering ├── RadialProgressBarNodeUpdater.cs # Arc repaint rendering └── DefaultProgressBarVisualUpdater.cs # Default chrome styling

Both ProgressBar and RadialProgressBar are split into partial classes: the core file holds the element hierarchy and controller wiring, .Settings.cs holds UXML attributes and styling, and the third partial holds the feature-specific code (gap lines / arc rendering).

Best Practices

Do

  • ✅ Use ProgressBarData for all state management
  • ✅ Implement IDisposable pattern in controllers
  • ✅ Set Instant* to true during initialization, then false
  • ✅ Call PlayProgress() and PlayIndicator() after changing values

Don't

  • ❌ Modify CurrentValue directly—use TargetValue
  • ❌ Forget to call RebuildSegments() after changing segment count
  • ❌ Issue manual PlayProgress() / PlayIndicator() calls (or change targets) while a PlayMultiPass() sequence is active—multi-pass sequencing is driven by OnSegmentAnimationComplete events, so an outside completion advances its phase machine unexpectedly. Call CancelMultiPass() first.

Re-targeting outside a multi-pass is interruption-safe by design: each Play* call stops the running tick subscription, re-syncs the visual to the current data value, and animates toward the new target—you can call PlayProgress() on every value change without waiting for completion. This is the flip side of the scheduling note above: plain Play* calls never chain on completion (only FirstDelay/SecondDelay order them), which is exactly why they are free to interrupt each other—and why PlayMultiPass, which does chain on completion events, must not be mixed with manual calls.

See also

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL