This document covers the fundamental concepts of the UI Service: presenters, layers, sets, features, and configuration.
- Editor Windows
- Service Interfaces
- UI Presenter
- Presenter Features
- UI Layers
- UI Sets
- Multi-Instance Support
- UI Configuration
The package includes unified tools for development and debugging.
Menu: Tools → UI Service → Presenter Manager
Manage active and loaded UI presenters in real-time during play mode:
- View all loaded presenters and their current status (Open/Closed)
- Quick Open/Close/Unload actions per instance
- Bulk operations: Close All, Unload All
- Status indicators: 🟢 Open, 🔴 Closed
Menu: Tools → UI Service → Select Ui Configs
Select any UiConfigs asset to see the enhanced inspector:
- Visual layer hierarchy
- Color-coded layers
- Drag & drop reordering
- Statistics panel
- UI set management
The UI Service exposes two interfaces for different use cases:
| Interface | Purpose | Key Methods |
|---|---|---|
IUiService |
Consuming the service | OpenUiAsync, CloseUi, LoadUiAsync, UnloadUi, IsVisible, GetUi |
IUiServiceInit |
Initializing the service | Inherits IUiService + Init(UiConfigs), Dispose() |
Use IUiServiceInit when:
- You create and own the
UiServiceinstance - You need to call
Init(UiConfigs)to initialize - You need to call
Dispose()to clean up
Use IUiService when:
- You receive the service via dependency injection
- You only need to open/close/query UI
- You don't manage the service lifecycle
using UnityEngine;
using GameLovers.UiService;
public class GameInitializer : MonoBehaviour
{
[SerializeField] private UiConfigs _uiConfigs;
// ✅ Use IUiServiceInit - you need Init() and Dispose()
private IUiServiceInit _uiService;
void Start()
{
_uiService = new UiService();
_uiService.Init(_uiConfigs); // Available on IUiServiceInit
}
void OnDestroy()
{
_uiService?.Dispose(); // Available on IUiServiceInit
}
}// ❌ WRONG - IUiService does NOT have Init()
private IUiService _uiService;
void Start()
{
_uiService = new UiService();
_uiService.Init(_uiConfigs); // CS1061: 'IUiService' does not contain 'Init'
}// Service locator or DI container registers with IUiServiceInit
public class ServiceLocator
{
private IUiServiceInit _uiService;
public void Initialize(UiConfigs configs)
{
_uiService = new UiService();
_uiService.Init(configs);
}
// Consumers get IUiService (no access to Init/Dispose)
public IUiService GetUiService() => _uiService;
public void Shutdown()
{
_uiService?.Dispose();
}
}
// Consumer class only needs IUiService
public class ShopController
{
private readonly IUiService _uiService;
public ShopController(IUiService uiService)
{
_uiService = uiService;
}
public async void OpenShop()
{
await _uiService.OpenUiAsync<ShopPresenter>();
}
}The UiPresenter is the base class for all UI elements in the system. It provides lifecycle management and service integration.
| Method | When Called | Use For |
|---|---|---|
OnInitialized() |
Once, when first loaded | Setup, event subscriptions |
OnOpened() |
Every time UI is shown | Animations, data refresh |
OnClosed() |
When UI is hidden | Cleanup, save state |
OnOpenTransitionCompleted() |
After all transition features finish opening | Post-transition logic, enable interactions |
OnCloseTransitionCompleted() |
After all transition features finish closing | Post-transition cleanup |
Note:
OnOpenTransitionCompleted()andOnCloseTransitionCompleted()are always called, even for presenters without transition features. This provides a consistent lifecycle for all presenters.
Presenters expose public UniTask properties for external awaiting:
// Wait for a presenter to fully open (including transitions)
await presenter.OpenTransitionTask;
// Wait for a presenter to fully close (including transitions)
await presenter.CloseTransitionTask;public class BasicPopup : UiPresenter
{
protected override void OnInitialized()
{
// Called once when the presenter is first loaded
// Set up UI elements, subscribe to events
}
protected override void OnOpened()
{
// Called every time the UI is shown
// Start animations, refresh data
}
protected override void OnClosed()
{
// Called when the UI is hidden
// Stop animations, save state
}
}For UI that needs initialization data, use UiPresenter<T>:
public struct QuestData
{
public int QuestId;
public string Title;
public string Description;
}
public class QuestPresenter : UiPresenter<QuestData>
{
[SerializeField] private Text _titleText;
[SerializeField] private Text _descriptionText;
protected override void OnSetData()
{
// Called automatically whenever Data is assigned
_titleText.text = Data.Title;
_descriptionText.text = Data.Description;
}
}
// Usage - initial data on open
var questData = new QuestData { QuestId = 1, Title = "Dragon Slayer", Description = "..." };
await _uiService.OpenUiAsync<QuestPresenter, QuestData>(questData);The Data property on UiPresenter<T> has a public setter that automatically triggers OnSetData() whenever assigned. This enables updating UI data at any time without closing and reopening:
// Get the presenter and update its data directly
var questPresenter = _uiService.GetUi<QuestPresenter>();
// This will automatically call OnSetData()
questPresenter.Data = new QuestData
{
QuestId = 2,
Title = "Updated Quest",
Description = "New description"
};Note: Setting
Dataalways callsOnSetData(), whether called during initial open viaOpenUiAsyncor when updating data afterwards. This provides a consistent lifecycle for data-driven presenters.
Presenters can close themselves:
public class ConfirmPopup : UiPresenter
{
public void OnConfirmClicked()
{
// Close but keep in memory
Close(destroy: false);
}
public void OnCancelClicked()
{
// Close and unload from memory
// Works correctly even for multi-instance presenters
Close(destroy: true);
}
}The UI Service uses a feature-based composition system to extend presenter behavior without inheritance complexity.
Features that implement ITransitionFeature provide open/close transition delays. The presenter automatically awaits all transition features before:
- Calling
OnOpenTransitionCompleted()(after open) - Hiding the GameObject and calling
OnCloseTransitionCompleted()(after close)
This ensures visibility is controlled in a single place (UiPresenter) and transitions are properly coordinated.
Adds time-based delays to UI opening and closing. Implements ITransitionFeature:
[RequireComponent(typeof(TimeDelayFeature))]
public class DelayedPopup : UiPresenter
{
[SerializeField] private TimeDelayFeature _delayFeature;
protected override void OnOpened()
{
base.OnOpened();
Debug.Log($"Opening with {_delayFeature.OpenDelayInSeconds}s delay...");
}
protected override void OnOpenTransitionCompleted()
{
Debug.Log("Opening delay completed - UI is ready!");
}
protected override void OnCloseTransitionCompleted()
{
Debug.Log("Closing delay completed!");
}
}Inspector Configuration:
Open Delay In Seconds- Time to wait after opening (default: 0.5s)Close Delay In Seconds- Time to wait before closing (default: 0.3s)
Synchronizes UI lifecycle with animation clips. Implements ITransitionFeature:
[RequireComponent(typeof(AnimationDelayFeature))]
public class AnimatedPopup : UiPresenter
{
[SerializeField] private AnimationDelayFeature _animationFeature;
protected override void OnOpenTransitionCompleted()
{
Debug.Log("Intro animation completed - UI is ready!");
}
protected override void OnCloseTransitionCompleted()
{
Debug.Log("Outro animation completed!");
}
}Inspector Configuration:
Animation Component- Auto-detected or manually assignedIntro Animation Clip- Plays when openingOutro Animation Clip- Plays when closing
Provides UI Toolkit (UI Elements) integration with safe visual tree handling.
⚠️ Important: UI Toolkit recreates visual elements when the presenter GameObject is deactivated/reactivated. The callback registered viaAddVisualTreeAttachedListeneris invoked on each open to handle this.
Properties:
Document- The attachedUIDocumentRoot- The rootVisualElement(may be null before panel attachment)
Methods:
AddVisualTreeAttachedListener(callback)- Registers a callback invoked when the visual tree is ready. Invokes on each open to handle element recreation.RemoveVisualTreeAttachedListener(callback)- Removes a previously registered callback.
Recommended Pattern:
Since elements may be recreated on each open, always unregister from old elements before querying and registering on new ones:
[RequireComponent(typeof(UiToolkitPresenterFeature))]
public class UIToolkitMenu : UiPresenter
{
[SerializeField] private UiToolkitPresenterFeature _toolkitFeature;
private Button _playButton;
protected override void OnInitialized()
{
_toolkitFeature.AddVisualTreeAttachedListener(SetupUI);
}
private void SetupUI(VisualElement root)
{
// 1. Unregister from old elements (may be stale after close/reopen)
_playButton?.UnregisterCallback<ClickEvent>(OnPlayClicked);
// 2. Query fresh elements
_playButton = root.Q<Button>("play-button");
// 3. Register on current elements
_playButton?.RegisterCallback<ClickEvent>(OnPlayClicked);
}
private void OnPlayClicked(ClickEvent evt)
{
// Handle click
}
private void OnDestroy()
{
_playButton?.UnregisterCallback<ClickEvent>(OnPlayClicked);
}
}Features can be combined freely:
[RequireComponent(typeof(TimeDelayFeature))]
[RequireComponent(typeof(UiToolkitPresenterFeature))]
public class DelayedUiToolkitPresenter : UiPresenter
{
[SerializeField] private UiToolkitPresenterFeature _toolkitFeature;
protected override void OnInitialized()
{
base.OnInitialized();
// Disable UI until transition completes
_toolkitFeature.AddVisualTreeAttachedListener(root => root.SetEnabled(false));
}
protected override void OnOpenTransitionCompleted()
{
// Enable UI after delay completes
_toolkitFeature.Root?.SetEnabled(true);
}
}Extend PresenterFeatureBase for basic lifecycle hooks. For transition features, also implement ITransitionFeature:
using Cysharp.Threading.Tasks;
using UnityEngine;
[RequireComponent(typeof(CanvasGroup))]
public class FadeFeature : PresenterFeatureBase, ITransitionFeature
{
[SerializeField] private CanvasGroup _canvasGroup;
[SerializeField] private float _fadeDuration = 0.3f;
private UniTaskCompletionSource _openTransitionCompletion;
private UniTaskCompletionSource _closeTransitionCompletion;
// ITransitionFeature implementation
public UniTask OpenTransitionTask => _openTransitionCompletion?.Task ?? UniTask.CompletedTask;
public UniTask CloseTransitionTask => _closeTransitionCompletion?.Task ?? UniTask.CompletedTask;
public override void OnPresenterOpening()
{
_canvasGroup.alpha = 0f;
}
public override void OnPresenterOpened()
{
FadeInAsync().Forget();
}
public override void OnPresenterClosing()
{
FadeOutAsync().Forget();
}
private async UniTask FadeInAsync()
{
_openTransitionCompletion = new UniTaskCompletionSource();
float elapsed = 0f;
while (elapsed < _fadeDuration)
{
_canvasGroup.alpha = elapsed / _fadeDuration;
elapsed += Time.deltaTime;
await UniTask.Yield();
}
_canvasGroup.alpha = 1f;
_openTransitionCompletion.TrySetResult();
}
private async UniTask FadeOutAsync()
{
_closeTransitionCompletion = new UniTaskCompletionSource();
float elapsed = 0f;
while (elapsed < _fadeDuration)
{
_canvasGroup.alpha = 1f - (elapsed / _fadeDuration);
elapsed += Time.deltaTime;
await UniTask.Yield();
}
_canvasGroup.alpha = 0f;
_closeTransitionCompletion.TrySetResult();
}
}Available Lifecycle Hooks:
OnPresenterInitialized(UiPresenter presenter)OnPresenterOpening()OnPresenterOpened()OnPresenterClosing()OnPresenterClosed()
Creating Transition Features:
- Implement
ITransitionFeaturefor features that need the presenter to wait - Expose
OpenTransitionTaskandCloseTransitionTaskasUniTaskproperties - Use
UniTaskCompletionSourceto signal when your transition completes - The presenter will await all
ITransitionFeaturetasks before completing its lifecycle
UI elements are organized into layers, where higher layer numbers appear on top (closer to camera).
// Recommended layer structure:
// Layer 0: Background UI (skyboxes, parallax)
// Layer 1: Game HUD (health bars, minimap)
// Layer 2: Menus (main menu, settings)
// Layer 3: Popups (confirmations, rewards)
// Layer 4: System messages (errors, loading)
// Layer 5: Debug overlays// Close all UI in a specific layer
_uiService.CloseAllUi(layer: 2);
// Layers are configured per-presenter in UiConfigs- Each presenter is assigned a layer in
UiConfigs Canvas.sortingOrder(uGUI) orUIDocument.sortingOrder(UI Toolkit) is set automatically- Higher layers render on top of lower layers
Group related UI elements for batch operations.
Sets are defined in your UiConfigs asset. Each presenter can optionally belong to a set by its Set ID.
Set 0: Core UI (always loaded)
Set 1: Main Menu (logo, menu buttons, background)
Set 2: Gameplay (HUD, minimap, chat)
Set 3: Shop (shop window, inventory, currency)
// Load all UI in a set (returns array of tasks)
var loadTasks = _uiService.LoadUiSetAsync(setId: 1);
await UniTask.WhenAll(loadTasks);
// Close all UI in a set
_uiService.CloseAllUiSet(setId: 1);
// Unload set from memory
_uiService.UnloadUiSet(setId: 1);
// Remove set and get removed presenters
var removed = _uiService.RemoveUiSet(setId: 2);
foreach (var presenter in removed)
{
Destroy(presenter.gameObject);
}| Set ID Range | Purpose |
|---|---|
| 0 | Core/Persistent UI (always loaded) |
| 1-10 | Scene-specific UI |
| 11-20 | Feature-specific UI (shop, inventory) |
By default, each UI presenter type is a singleton. The UiInstanceId system enables multiple instances of the same type.
- Multiple tooltip windows
- Stacked notification popups
- Multiple player info panels (multiplayer)
- Pooled UI elements
// Default/singleton instance
var defaultId = UiInstanceId.Default(typeof(TooltipPresenter));
// Named instances
var itemTooltipId = UiInstanceId.Named(typeof(TooltipPresenter), "item");
var skillTooltipId = UiInstanceId.Named(typeof(TooltipPresenter), "skill");
// Check if default
if (instanceId.IsDefault)
{
Debug.Log("This is the singleton instance");
}// Get all loaded presenters
List<UiInstance> loaded = _uiService.GetLoadedPresenters();
foreach (var instance in loaded)
{
Debug.Log($"Type: {instance.Type.Name}");
Debug.Log($"Address: {instance.Address}"); // Empty for default
Debug.Log($"Presenter: {instance.Presenter.name}");
}
// Check visible presenters
IReadOnlyList<UiInstanceId> visible = _uiService.VisiblePresenters;| Struct | Purpose | Contains |
|---|---|---|
UiInstanceId |
Identifier for referencing | PresenterType, InstanceAddress |
UiInstance |
Full data about loaded instance | Type, Address, Presenter |
The UiConfigs ScriptableObject stores all UI configuration.
- Right-click in Project View
- Navigate to
Create→ScriptableObjects→Configs→UiConfigs
| Property | Description |
|---|---|
| Type | The presenter class type |
| Addressable Address | Addressable key to the UI prefab |
| Layer | Depth layer (higher = closer to camera) |
| Load Synchronously | Block main thread during load (use sparingly) |
| UI Set ID | Optional grouping for batch operations |
// Add configuration at runtime
var config = new UiConfig(typeof(DynamicPopup), "UI/DynamicPopup", layer: 3);
_uiService.AddUiConfig(config);
// Add UI set at runtime
var setConfig = new UiSetConfig(setId: 5, new[] { typeof(ShopUI), typeof(InventoryUI) });
_uiService.AddUiSet(setConfig);
// Add instantiated UI
var dynamicUi = Instantiate(uiPrefab);
_uiService.AddUi(dynamicUi, layer: 3, openAfter: true);