Companion files:
CLAUDE.mdwraps this file for Claude Code — editAGENTS.md, notCLAUDE.md.docs/README.md(and linked pages) is the user-facing documentation set.
- Package:
com.gamelovers.uiservice - Unity: 6000.0+
- Dependencies (see
package.json)com.unity.addressables(2.6.0)com.cysharp.unitask(2.5.10)
This package provides a centralized UI management service that coordinates presenter load/open/close/unload, supports layering, UI sets, and multi-instance presenters, and integrates with Addressables + UniTask.
For user-facing docs, treat docs/README.md (and linked pages) as the primary documentation set. This file is for contributors/agents working on the package itself.
- Service core:
Runtime/UiService.cs(UiService : IUiServiceInit)- Owns configs, loaded presenter instances, visible list, and UI set configs.
- Creates a
DontDestroyOnLoadparent GameObject named"Ui"and attachesUiServiceMonoComponentfor resolution/orientation tracking. - Tracks presenters as instances:
Dictionary<Type, IList<UiInstance>>where eachUiInstancestores(Type, Address, UiPresenter). - Editor support:
UiService.CurrentServiceis an internal static reference used by editor windows to access the active service in play mode.
- Public API surface:
Runtime/IUiService.cs- Exposes lifecycle operations (load/open/close/unload) and readonly views:
VisiblePresenters : IReadOnlyList<UiInstanceId>UiSets : IReadOnlyDictionary<int, UiSetConfig>GetLoadedPresenters() : List<UiInstance>
- Note: multi-instance overloads (explicit
instanceAddress) exist onUiService(concrete type), not onIUiService.
- Exposes lifecycle operations (load/open/close/unload) and readonly views:
- Configuration:
Runtime/UiConfigs.cs(ScriptableObject)- Stores UI configs as
UiConfigs.UiConfigSerializable(address + layer + type name) and UI sets asUiSetConfigSerializablecontainingUiSetEntryitems. - Use the specialized subclasses (each has
CreateAssetMenu):AddressablesUiConfigs(default),ResourcesUiConfigs,PrefabRegistryUiConfigs(embedded prefab registry). - Note:
UiConfigsisabstractto prevent accidental direct usage—always use one of the specialized subclasses.
- Stores UI configs as
- UI Sets:
Runtime/UiSetConfig.csUiSetEntrystores:- presenter type as
AssemblyQualifiedNamestring - optional
InstanceAddress(empty string means default instance)
- presenter type as
UiSetConfigis the runtime shape:SetId+UiInstanceId[].
- Presenter pattern:
Runtime/UiPresenter.cs- Lifecycle hooks:
OnInitialized,OnOpened,OnClosed,OnOpenTransitionCompleted,OnCloseTransitionCompleted. - Typed presenters:
UiPresenter<T>with aDataproperty that triggersOnSetData()on assignment (works duringOpenUiAsync(..., initialData, ...)or later updates). - Presenter features are discovered via
GetComponents(_features)at init time and are notified in the open/close lifecycle. - Transition tasks:
OpenTransitionTaskandCloseTransitionTaskare publicUniTaskproperties that complete when all transition features finish. - Visibility control:
UiPresenteris the single point of responsibility forSetActive(false)on close; it waits for allITransitionFeaturetasks before hiding.
- Lifecycle hooks:
- Composable features:
Runtime/Features/*PresenterFeatureBaseallows attaching components to a presenter prefab to hook lifecycle.ITransitionFeatureinterface for features that provide open/close transition delays (presenter awaits these).- Built-in transition features:
TimeDelayFeature,AnimationDelayFeature. - UI Toolkit support:
UiToolkitPresenterFeature(viaUIDocument) providesAddVisualTreeAttachedListener(callback)for safe element queries. Callback is invoked on each open because UI Toolkit recreates elements when the presenter is deactivated/reactivated.
- Helper views:
Runtime/Views/*(GameLovers.UiService.Views)SafeAreaHelperView: adjusts anchors/size based on safe area (notches).NonDrawingView: raycast target without rendering (extendsGraphic).AdjustScreenSizeFitterView: layout fitter that clamps between min/flexible size.InteractableTextView: TMP link click handling.
- Asset loading:
Runtime/Loaders/IUiAssetLoader.csIUiAssetLoaderabstraction with multiple implementations underRuntime/Loaders/:AddressablesUiAssetLoader(default): usesAddressables.InstantiateAsyncandAddressables.ReleaseInstance.PrefabRegistryUiAssetLoader: uses direct prefab references (useful for samples/testing). Can be initialized with aPrefabRegistryUiConfigsin its constructor.ResourcesUiAssetLoader: usesResources.Load.
- Supports optional synchronous instantiation via
UiConfig.LoadSynchronously(in Addressables loader).
- Docs (user-facing):
docs/docs/README.md— documentation entry point.
- Runtime:
Runtime/- Entry points:
IUiService.cs,UiService.cs,UiPresenter.cs,UiConfigs.cs,UiSetConfig.cs,UiInstanceId.cs. - Integrations / extension points (start here when behavior differs from expectations):
Loaders/*— how presenter prefabs are instantiated / released.- If UI fails to load/unload, start at
Loaders/IUiAssetLoader.csand the active loader (AddressablesUiAssetLoader,ResourcesUiAssetLoader,PrefabRegistryUiAssetLoader). - Loader choice is typically driven by which
UiConfigssubclass you use (AddressablesUiConfigs/ResourcesUiConfigs/PrefabRegistryUiConfigs).
- If UI fails to load/unload, start at
Features/*— presenter composition (components attached to presenter prefabs).- Lifecycle hooks live in
PresenterFeatureBase; features are discovered during presenter initialization. - Transition timing issues (UI not showing/hiding when expected) usually involve
ITransitionFeatureimplementations (egTimeDelayFeature,AnimationDelayFeature). - UI Toolkit presenters rely on
UiToolkitPresenterFeature; avoid queryingUIDocument.rootVisualElementduringOnInitialized()—useAddVisualTreeAttachedListener(...).
- Lifecycle hooks live in
Views/*— optional helper components used by presenter prefabs (safe area, raycasts, layout fitters, TMP link clicks).- If interaction/layout is off but service bookkeeping looks correct, look here before changing
UiService.
- If interaction/layout is off but service bookkeeping looks correct, look here before changing
- Entry points:
- Editor:
Editor/(assembly:Editor/GameLovers.UiService.Editor.asmdef)- Config editors:
UiConfigsEditorBase.cs,*UiConfigsEditor.cs,DefaultUiConfigsEditor.cs. - Debugging:
UiPresenterManagerWindow.cs,UiPresenterEditor.cs.
- Config editors:
- Samples:
Samples~/- Demonstrates basic flows, data presenters, delay features, UI Toolkit integration.
- Tests:
Tests/Tests/EditMode/*— unit tests (configs, sets, loaders, core service behavior). Owned byGameLovers.UiService.Tests.asmdefwhich is editor-only (includePlatforms: ["Editor"]).Tests/PlayMode/*— integration/performance/smoke tests and unit tests that require PlayMode (e.g.DontDestroyOnLoad). Owned byGameLovers.UiService.Tests.PlayMode.asmdef(runtime-compatible).Tests/Helpers/*— shared test fixtures consumed by both EditMode and PlayMode. Owned byGameLovers.UiService.Tests.Helpers.asmdef(runtime-compatible, gated bydefineConstraints: ["UNITY_INCLUDE_TESTS"]). MonoBehaviour-derived test presenters (e.g.,TestUiPresenter,TestDataUiPresenter) MUST live here, not underTests/EditMode/. Placing a MonoBehaviour in the editor-only EditMode asmdef makes Unity rejectAddComponent<T>()calls (silentnullreturn + warning:Can't add script behaviour '<name>' because it is an editor script), which causes tests that create prefabs viaTestHelpers.CreateTestPresenterPrefab<T>to run without ever attaching the presenter component.- Performance test pattern (
Measure.Method): the body runsWarmupCount + MeasurementCounttimes. Stateful operations againstUiService(Load/Unload/Open/Close) MUST use.SetUp()and/or.CleanUp()to reset per-iteration state — otherwise iterations 2+ hit the cache and logThe Ui <X> was already loaded/<X> is already open, and the benchmark measures a no-op cache hit instead of the real operation. Correct shapes:- Measuring Load:
body = Load; CleanUp = Unload; - Measuring Unload:
SetUp = Load; body = Unload; - See
Tests/PlayMode/Performance/PerformanceTests.cs(Perf_LoadUi_SinglePresenter,Perf_UnloadUi_SinglePresenter) for the pattern.
- Measuring Load:
LogAssert.Expectscope — asserts, does not silence:UnityEngine.TestTools.LogAssert.Expect(LogType.Warning, regex)ensures a matching warning appears during the test (test fails if it doesn't) and prevents the warning from failing the test run for being "unexpected". It does NOT suppress the warning fromEditor.logor the Unity Console — the log line is still emitted. Use it to pin expected-warning contracts (service behavior under test), not to reduce console noise. For the latter, restructure the test (see Performance test pattern above) or change the runtime log site — notLogAssert.
- Instance address normalization
UiInstanceIdnormalizesnull/""tostring.Empty.- Prefer
string.Emptyas the default/singleton instance identifier.
- Ambiguous “default instance” calls
UiServiceuses an internalResolveInstanceAddress(type)when an API is called without an explicitinstanceAddress.- If multiple instances exist, it logs a warning and selects the first instance. For multi-instance usage, prefer calling
UiServiceoverloads that includeinstanceAddress.
- Presenter self-close + destroy with multi-instance
UiPresenter.Close(destroy: true)now correctly uses the presenter's storedInstanceAddressto unload the correct instance.- This works seamlessly for both singleton and multi-instance presenters.
- Layering
UiServiceenforces sorting by settingCanvas.sortingOrderorUIDocument.sortingOrderto the config layer when adding/loading.- Loaded presenters are instantiated under the
"Ui"root directly (no per-layer container GameObjects).
- UI Sets store types, not addresses
- UI sets are serialized as
UiSetEntry(type name + instance address). The default editor populatesInstanceAddresswith the addressable address for uniqueness.
- UI sets are serialized as
LoadSynchronouslypersistenceUiConfig.LoadSynchronouslyexists and is respected byAddressablesUiAssetLoader.- However:
UiConfigs.UiConfigSerializablecurrently does not serializeLoadSynchronously, so configs loaded from aUiConfigsasset will produceLoadSynchronously = falseinUiConfigs.Configs.
- Static events
UiService.OnResolutionChanged/UiService.OnOrientationChangedare staticUnityEvents raised byUiServiceMonoComponent.- The service does not clear listeners; consumers must unsubscribe appropriately.
- Disposal
UiService.Dispose()closes all visible UI, attempts to unload all loaded instances, clears collections, and destroys the"Ui"root GameObject.
- Editor debugging tools
- Some editor windows toggle
presenter.gameObject.SetActive(...)directly for convenience; this may not reflect inIUiService.VisiblePresenterssince it bypassesUiServicebookkeeping.
- Some editor windows toggle
- UI Toolkit visual tree timing and element recreation
UIDocument.rootVisualElementmay not be ready whenOnInitialized()is called on a presenter.- UI Toolkit recreates visual elements when the presenter GameObject is deactivated/reactivated (close/reopen cycle),
AddVisualTreeAttachedListener(callback)invokes on each open to handle element recreation.
- UI Toolkit test
PanelSettingscreation — silence the theme warning- A runtime-created
PanelSettings(no theme asset configured) makes Unity logNo Theme Style Sheet set to PanelSettings , UI will not render properlytwice per instantiation: once when assigned toUIDocument.panelSettings, and again when the hosting GameObject first goesSetActive(true). - Fix in test helpers: assign the theme before handing the panel to the document.
var panel = ScriptableObject.CreateInstance<PanelSettings>(); panel.themeStyleSheet = ScriptableObject.CreateInstance<ThemeStyleSheet>(); // empty theme is enough document.panelSettings = panel;
- See
Tests/PlayMode/Helpers/TestUiToolkitPresenter.csandTestMultiFeatureToolkitPresenter.csfor the pattern.
- A runtime-created
- C#: C# 9.0 syntax; no global
usings; keep explicit namespaces. - Assemblies
- Runtime code should avoid
UnityEditorreferences; editor-only tooling belongs underEditor/andGameLovers.UiService.Editor.asmdef. - If you must add editor-only code near runtime types, guard it with
#if UNITY_EDITORand keep it minimal.
- Runtime code should avoid
- Async
- Use
UniTask; threadCancellationTokenthrough async APIs where available.
- Use
- Memory / allocations
- Avoid per-frame allocations; keep API properties allocation-free (see
UiServiceread-only wrappers forVisiblePresentersandUiSets).
- Avoid per-frame allocations; keep API properties allocation-free (see
When you need third-party source/docs, prefer the locally-cached UPM packages:
- Addressables:
Library/PackageCache/com.unity.addressables@*/ - UniTask:
Library/PackageCache/com.cysharp.unitask@*/
- Add a new presenter
- Create a prefab with a component deriving
UiPresenter(orUiPresenter<T>). - Ensure it has a
CanvasorUIDocumentif you want layer sorting to apply. - Mark the prefab Addressable and set its address.
- Add/update the entry in
UiConfigs(menu:Tools/GameLovers/UI Configs/Select UI Configs).
- Create a prefab with a component deriving
- Add / update UI sets
- The default
UiConfigsinspector usesDefaultUiSetId(out-of-the-box). - To customize set ids, create your own enum and your own
[CustomEditor(typeof(UiConfigs))] : UiConfigsEditor<TEnum>.
- The default
- Add multi-instance flows
- Use
UiInstanceId(default =string.Empty) when you need to track instances externally. - Presenters know their own instance address via the internal
InstanceAddressproperty;Close(destroy: true)unloads the correct instance.
- Use
- Add a presenter feature
- Extend
PresenterFeatureBaseand attach it to the presenter prefab. - Features are discovered via
GetComponentsat init time and notified during open/close. - For features with transitions (animations, delays): implement
ITransitionFeatureso the presenter can await yourOpenTransitionTask/CloseTransitionTask.
- Extend
- Change loading strategy
- Prefer using one of the built-in loaders (
AddressablesUiAssetLoader,PrefabRegistryUiAssetLoader,ResourcesUiAssetLoader) or extendingIUiAssetLoaderfor custom needs.
- Prefer using one of the built-in loaders (
- Update docs/samples
- User-facing docs live in
docs/and should be updated when behavior/API changes. - If you add a new core capability, consider adding/adjusting a sample under
Samples~/.
- User-facing docs live in
Update this file when:
- Public API changes (
IUiService,IUiServiceInit, presenter lifecycle, config formats) - Core runtime systems/features are introduced/removed (features, views, multi-instance)
- Editor tooling changes how configs or sets are generated/serialized