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:
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.
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.
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);
}Represents a single progress segment with current/target values and color.
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);
}Generic animation orchestrator that works with any UI element type. Handles:
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.
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.
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();
}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.).
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.
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().
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);
}public class MyCustomProgressBar : VisualElement
{
private ProgressBarData _data;
public MyCustomProgressBar()
{
_data = new ProgressBarData
{
Segments = new[] { new ProgressBarNode() },
Indicator = new ProgressBarNode()
};
}
}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) { }
}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:
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.
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.
width style propertyVerticalLine)GapLineCount)MeshGenerationContext for custom arc renderingPainter2D paths on layered elements (track, fill, indicator)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 stylingBoth 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).
ProgressBarData for all state managementIDisposable pattern in controllersInstant* to true during initialization, then falsePlayProgress() and PlayIndicator() after changing valuesCurrentValue directly—use TargetValueRebuildSegments() after changing segment countPlayProgress() / 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.
IProgressBarNodeUpdater in practice (shader-driven orb)Settings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction