UTools is a lightweight Unity toolkit for dependency injection, UI/object lookup, message dispatching, and common runtime helpers.
| Module | What it is for |
|---|---|
UDI |
Scene/global dependency injection, lifecycle callbacks, async startup gates |
UComponent |
Pointer enter/click/drag listener with Inspector callbacks and global message publishing |
UFind |
Attribute-based component, child, children-list, and Resources binding |
UMessage |
Typed publish/subscribe messages with disposable subscriptions |
UUtils |
String, time, file, GameObject, UI, texture, and mesh helpers |
| Editor helpers | Small Inspector attributes such as buttons, conditional fields, and component toggles |
Open Window > Package Manager, choose Add package from git URL..., then use:
https://github.com/roymina/UTools.git?path=/Assets/UTools
This repository is still a normal Unity project. Assets/UTools is the package root exposed to UPM.
TextMeshPro is declared through com.unity.textmeshpro. If sample UI assets miss TMP resources, run:
Window > TextMeshPro > Import TMP Essential Resources
If you prefer manual import, use the packaged release artifact in Releases/.
- Install the package.
- Add exactly one
UDIContextto a scene bootstrap GameObject. - Add a
MonoInstallerto the same bootstrap object, or assign installers into theUDIContextlists. - Register services in
InstallBindings. - Use
[Inject]on scene components and useUGameObjectFactoryfor injected runtime prefabs.
using UnityEngine;
using UTools;
public interface IGameClock
{
float Time { get; }
}
public sealed class UnityGameClock : IGameClock
{
public float Time => UnityEngine.Time.time;
}
public sealed class GameInstaller : MonoInstaller
{
public override void InstallBindings(UDIContainer container)
{
container.Bind<IGameClock>()
.To<UnityGameClock>()
.AsSingle();
}
}
public sealed class ClockLabel : MonoBehaviour
{
[Inject] private IGameClock _clock;
private void Start()
{
Debug.Log($"Injected time: {_clock.Time}");
}
}UDI is the dependency injection module. Use it to prepare services, inject scene objects, inject runtime prefabs, and run lifecycle interfaces from a single context.
GlobalInstaller: prepares global fallback services for the whole game.MonoInstaller: prepares scene-specific services for the current scene.ScriptableObjectInstaller: prepares reusable asset-based config/services for the current scene.
The important rule is not "does this class implement a lifecycle interface?" but "has this object entered a UDIContainer?"
| Object kind | Needs a scene GameObject? |
How it joins UDI | What UDI guarantees |
|---|---|---|---|
Pure C# service, for example InventoryService : IInitializable |
No | Bind or resolve it through the container | Injection, [PostInjection], lifecycle callbacks |
MonoBehaviour consumer, for example HealthPanel : MonoBehaviour |
Yes | Live under a scene injected by UDIContext, or be created through UGameObjectFactory, .FromGameObject(...), or container-driven creation |
Injection, [PostInjection], lifecycle callbacks if it implements lifecycle interfaces |
MonoInstaller |
Yes | Add it to the bootstrap object or assign it into UDIContext.Installers |
InstallBindings is executed during context startup |
ScriptableObjectInstaller |
No, asset only | Assign the asset into UDIContext.Scriptable Object Installers |
InstallBindings is executed for that scene context |
GlobalInstaller |
No, asset only | Put exactly one asset under Resources |
Global container bootstraps before scenes load |
Notes:
- Implementing
IInitializable,IAsyncInitializable, orITickabledoes not make an object discoverable by itself. - Injection is guaranteed only after the object is created, resolved, or explicitly injected by a
UDIContainer. MonoInstallerconfigures bindings; it is not the same thing as a business service that happens to use lifecycle interfaces.
Use UDIContext as the scene DI entry point.
- Create a dedicated
BootstrapGameObject. - Add
UDIContext. - Add
MonoInstallercomponents to the same GameObject, or assign them toUDIContext>Installers. - Assign
ScriptableObjectInstallerassets toUDIContext>Scriptable Object Installers. - If any binding uses
.RequiredForContextStart(), assignAsync Wait Root.
Notes:
- Keep exactly one
UDIContextin a scene. Multiple contexts cause initialization to fail. - Keep
UDIContextand startup installers on a dedicated bootstrap object. Gameplay consumers can live anywhere in the scene. - A
MonoInstalleris executed only when it is on the same GameObject asUDIContextor explicitly assigned into theInstallerslist. - A standalone
MonoInstallercan auto-create a context when none exists, but explicit setup is safer and clearer. UDIContext.IsReadytells whether startup completed;ReadyTaskcan be awaited by code that must wait manually.- If initialization throws,
InitializationExceptionis set and the context stays not ready.
Use MonoInstaller when bindings need scene references such as transforms, prefabs, cameras, or serialized components.
using UnityEngine;
using UTools;
public sealed class GameplayInstaller : MonoInstaller
{
[SerializeField] private Transform spawnRoot;
[SerializeField] private EnemyView enemyPrefab;
public override void InstallBindings(UDIContainer container)
{
container.Bind<Transform>()
.FromInstance(spawnRoot)
.AsSingle();
container.Bind<EnemyService>()
.ToSelf()
.AsSingle()
.NonLazy();
container.Bind<IFactory<EnemyView>>()
.FromInstance(new PrefabFactory<EnemyView>(enemyPrefab, container, spawnRoot))
.AsSingle();
}
}Notes:
- Put
.NonLazy()at the end of the binding chain because it creates/finalizes the binding immediately. - Prefer
.FromInstance(...)for serialized scene references. - Use
.FromGameObject(host)forMonoBehaviourbindings when the component should be found or added on a specific object. FromGameObjectstill reuses an existing instance of thatMonoBehaviourtype if Unity finds one first.
Use ScriptableObjectInstaller when the same binding set should be reused by multiple scenes.
using UnityEngine;
using UTools;
[CreateAssetMenu(menuName = "Game/Installers/Audio Installer")]
public sealed class AudioInstaller : ScriptableObjectInstaller
{
[SerializeField] private AudioSettings settings;
public override void InstallBindings(UDIContainer container)
{
container.Bind<AudioSettings>()
.FromInstance(settings)
.AsSingle();
container.Bind<AudioService>()
.ToSelf()
.AsSingle();
}
}Usage:
- Create the asset from Unity's
Createmenu. - Assign it to
UDIContext>Scriptable Object Installers. - Only assets assigned in that list are executed for the current scene.
- Keep per-scene object references in
MonoInstaller; keep reusable config/data inScriptableObjectInstaller. ScriptableObjectInstalleris not a scene component. Do not attach it to aGameObject.
Use GlobalInstaller for fallback services shared by scenes.
using UnityEngine;
using UTools;
[CreateAssetMenu(menuName = "UTools/Global Installer")]
public sealed class GameGlobalInstaller : GlobalInstaller
{
public override void InstallBindings(UDIContainer container)
{
container.Bind<IClock>()
.To<UnityClock>()
.AsSingle()
.AsGlobal();
}
}Usage:
- Create exactly one
GlobalInstallerasset. - Put the asset under a
Resourcesfolder, for exampleAssets/Resources/GameGlobalInstaller.asset. - Use
.AsGlobal()only inside aGlobalInstaller. - Scenes without
UDIContextcan still receive global services. - Scenes with one
UDIContextuse local bindings first, then fall back to global bindings. - Do not create multiple
GlobalInstallerassets underResources; the global runtime supports only one. GlobalInstalleris also an asset, not a scene component. It is loaded automatically fromResources.
| API | Use it for |
|---|---|
Bind<T>() |
Register T as both contract and concrete type |
Bind<TContract, TImplementation>() |
Register an interface/base type to an implementation |
.To<T>() |
Set the concrete implementation |
.ToSelf() |
Use the contract type itself as the concrete type |
.AsSingle() |
Reuse one instance in the current container |
.AsTransient() |
Create a new instance for each resolve |
.InScope(BindingScope.Scoped) |
Cache one instance in the current context/container |
.FromInstance(instance) |
Use an existing instance, usually a serialized reference |
.FromGameObject(gameObject) |
Resolve or add a MonoBehaviour on a target GameObject |
.NonLazy() |
Create the instance during context startup |
.RequiredForContextStart() |
Wait for this binding before the async startup gate opens |
.AsGlobal() |
Mark a global binding; only valid in GlobalInstaller |
Notes:
- Constructor injection is not supported.
- If a concrete non-abstract class is resolved without an explicit binding, UDI can auto-create it with a parameterless constructor.
- Prefer explicit bindings for services you rely on, especially interfaces and abstract types.
- Circular dependencies throw an error instead of resolving partially.
Use [Inject] on fields, writable properties, or methods. Private members are supported.
using UnityEngine;
using UTools;
public sealed class PlayerPresenter : MonoBehaviour
{
[Inject] private IGameClock _clock;
[Inject]
public Transform SpawnRoot { get; private set; }
private Camera _camera;
[Inject]
private void Construct(Camera camera)
{
_camera = camera;
}
[PostInjection]
private void AfterInject()
{
Debug.Log($"Clock={_clock.Time}, Root={SpawnRoot.name}, Camera={_camera.name}");
}
}Notes:
- Injection order is fields, properties, methods, then
[PostInjection]. [Inject]methods can have parameters; each parameter is resolved from the container.[PostInjection]runs after dependencies are assigned and can also receive resolved parameters.- Scene objects are injected by
UDIContext; runtime prefabs should be created withUGameObjectFactoryorPrefabFactory<T>. - UDI runs very early by default. Avoid setting consumer scripts to execute earlier than
UDIContext. - Implementing a lifecycle interface does not replace injection; it only adds callbacks after the object has already entered UDI management.
Implement lifecycle interfaces on services or injected objects when the context should manage them.
| Interface | When it runs |
|---|---|
IInitializable.Initialize() |
After context injection is complete |
IAsyncInitializable.InitializeAsync(...) |
Only awaited when the binding is marked .RequiredForContextStart() |
ITickable.Tick() |
Every Update while not paused |
IFixedTickable.FixedTick() |
Every FixedUpdate while not paused |
ILateTickable.LateTick() |
Every LateUpdate while not paused |
IUDisposable.Dispose() |
When the LifecycleManager is destroyed |
IPausable.Pause() / Resume() |
When LifecycleManager.Pause() / Resume() is called |
Notes:
LifecycleManageris added automatically to theUDIContextobject if missing.- Non-lazy and resolved instances are tracked once.
IAsyncInitializableis not automatically awaited unless its binding is required for context start.- Pure C# services can implement these interfaces without any
GameObject. MonoBehaviourclasses participate only after the scene context injects them or a factory/container creates them through UDI.
Use this when a scene subtree must not wake until required async services are ready.
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UTools;
public sealed class RemoteConfigService : IAsyncInitializable
{
public async Task InitializeAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(3000, cancellationToken);
}
}
public sealed class GameInstaller : MonoInstaller
{
public override void InstallBindings(UDIContainer container)
{
container.Bind<RemoteConfigService>()
.ToSelf()
.AsSingle()
.RequiredForContextStart();
}
}After this binding is registered:
- Assign
Async Wait RootonUDIContext. - On scene start,
Async Wait Rootis disabled if it was active. - Required async services run in binding registration order.
- After they finish, UDI injects the
Async Wait Rootsubtree, then restores its previous active state. - Objects outside
Async Wait Rootare injected immediately and keep running.
Notes:
- Put gameplay nodes that must wait under
Async Wait Root. - Do not put the
UDIContextobject insideAsync Wait Root. Async Wait Rootmust belong to the same scene as the context.- If a required async binding exists but
Async Wait Rootis not assigned, initialization fails. - If
Async Wait Rootwas inactive before startup, it stays inactive after async initialization, but it is still injected. - If async initialization fails or is cancelled, the context stays not ready and the wait root is not restored.
Use UGameObjectFactory instead of Object.Instantiate for prefabs that contain [Inject] consumers.
using UnityEngine;
using UTools;
public sealed class EnemySpawner : MonoBehaviour
{
[SerializeField] private GameObject enemyPrefab;
[SerializeField] private Transform spawnRoot;
public GameObject Spawn(Vector3 position)
{
return UGameObjectFactory.InstantiateWithDependency(
enemyPrefab,
position,
Quaternion.identity,
spawnRoot);
}
}Notes:
- Prefer overloads that pass a
Transform parentwhen the instance should use the nearest parent context. - The factory injects all
MonoBehaviourcomponents in the created GameObject subtree. - If no context exists, the factory falls back to a global/default container and logs a warning.
Object.Instantiate(...)by itself does not run UDI injection.
Use this rule of thumb:
MonoInstaller: scene bootstrap configuration. Best when bindings need scene references such as cameras, spawn roots, prefabs, or existing components.ScriptableObjectInstaller: reusable asset configuration. Best when the same binding set or data asset should be shared by multiple scenes.GlobalInstaller: cross-scene fallback registration. Best when a service should exist even in scenes withoutUDIContext.- Normal
MonoBehaviour: gameplay/UI logic that consumes injected services. - Pure C# service: domain or runtime logic that does not need Unity callbacks directly, but can still use UDI lifecycle interfaces.
Common mistakes:
- Do not put gameplay logic inside an installer just because an installer can run early.
- Do not expect a random
MonoBehaviourcreated withnewor plainInstantiateto be injected automatically. - Do not put more than one
GlobalInstallerasset underResources.
PointerEventListener wraps Unity's pointer interfaces into one component:
IPointerClickHandlerIPointerEnterHandlerIPointerExitHandlerIPointerDownHandlerIPointerUpHandlerIBeginDragHandlerIDragHandlerIEndDragHandler
Add PointerEventListener when you want a single component that can:
- react through plain runtime delegates such as
onClickoronEnter - expose UnityEvents in the Inspector
- publish pointer messages globally through
UMessageCenter
using UnityEngine;
using UTools;
public sealed class PointerExample : MonoBehaviour
{
[SerializeField] private PointerEventListener listener;
private IMessageSubscription _subscription;
private void Awake()
{
listener.onEnter += () => Debug.Log("Pointer entered");
listener.onClickWithData += data => Debug.Log($"Clicked at {data.position}");
listener.onDragWithData += (_, delta) => Debug.Log($"Drag delta: {delta}");
}
private void OnEnable()
{
_subscription = UMessageCenter.Instance.Subscribe<PointerEventMessage>(
OnPointerMessage,
replayPending: false);
}
private void OnDisable()
{
_subscription?.Dispose();
_subscription = null;
}
private void OnPointerMessage(PointerEventMessage message)
{
if (message.Target != listener.gameObject)
{
return;
}
Debug.Log($"{message.EventType} from {message.Target.name}, 3D={message.Is3DObject}, delta={message.Delta}");
}
}Usage notes:
- The component supports click, enter, exit, down, up, begin-drag, drag, and end-drag.
publishGloballyis enabled by default. Turn it off when you want only local callbacks/Inspector events.- For every event there are three hook styles:
- runtime delegates such as
onClick,onClickWithData,onDragWithData - Inspector UnityEvents such as
onClickEvent,onClickEventWithData,onDragEventWithData - global
PointerEventMessagepublish throughUMessageCenter
- runtime delegates such as
PointerEventMessageincludesEventType,Target,EventData,Is3DObject, andDelta.OnPointerClickignores events while Unity reportseventData.dragging, so a drag does not also emit a click.Is3DObjectistruewhen the target is not aRectTransform.
Scene setup requirements:
- Always have an
EventSystemin the scene. - For UI objects:
- place the listener on a UI object under a
Canvas - the canvas needs a
GraphicRaycaster - the target graphic must allow raycasts
- place the listener on a UI object under a
- For 3D objects:
- add a collider to the target object
- use a camera with
PhysicsRaycaster
UFind reduces repetitive GetComponent, transform.Find, and Resources.Load code.
- Inherit from
UBehaviour. - Add
[Comp],[Child],[Children], or[Resource]to fields. - If you override
Awake, callbase.Awake()before using bound fields.
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UTools;
public sealed class InventoryPanel : UBehaviour
{
[Comp] private Canvas _canvas;
[Comp] private Button _closeButton;
[Child] private TextMeshProUGUI Title;
[Child("Content/Buttons/ConfirmButton")] private Button _confirmButton;
[Child("Content/Icon")] private Image _icon;
[Children("Content/Buttons")] private List<Button> _buttons;
[Children(parentName = "Content/Buttons", includeDescendants = true, includeInactive = false)]
private List<GameObject> _activeButtonNodes;
[Resource("Icons/Inventory")] private Sprite _inventorySprite;
protected override void Awake()
{
base.Awake();
Title.text = "Inventory";
_icon.sprite = _inventorySprite;
}
}- Finds a component on the same GameObject.
- Works on fields only.
- Does not overwrite a field that already has a value.
- Logs a warning if the component is missing.
- With no argument, uses the field name as the child name.
- With a string argument, uses that name or path.
- If the field type is
GameObject, assigns the child object. - If the field type is a component, finds the child first, then gets that component from the child.
- Simple names search descendants case-insensitively.
- Path lookup uses
Transform.Findrelative to the current transform. - If multiple descendants share a simple name, use a path such as
Root/Panel/Button.
- Finds a parent child object, then collects its children into a list.
- Supports
List<GameObject>andList<TComponent>. - With no
parentName, uses the field name as the parent name. includeDescendants = falseby default, so only direct children are collected.includeInactive = trueby default.- Component lists keep only child objects that contain the requested component.
- Errors are reported per field; one failed binding does not stop other fields.
- Loads an asset with
Resources.Load(path, fieldType). - With no path, uses the field name as the resource path.
- Do not include the file extension in the path.
- Keep resources under any Unity
Resourcesfolder.
UMessage is a typed message center.
using UnityEngine;
using UTools;
public sealed class ScoreChangedMessage
{
public int Value;
}
public sealed class ScorePublisher : MonoBehaviour
{
public void ReportScore(int score)
{
UMessageCenter.Instance.Publish(new ScoreChangedMessage { Value = score });
}
}
public sealed class ScoreListener : MonoBehaviour
{
private IMessageSubscription _subscription;
private void OnEnable()
{
_subscription = UMessageCenter.Instance.Subscribe<ScoreChangedMessage>(OnScoreChanged);
}
private void OnDisable()
{
_subscription?.Dispose();
_subscription = null;
}
private void OnScoreChanged(ScoreChangedMessage message)
{
Debug.Log($"Score updated: {message.Value}");
}
}Usage notes:
- Use
Publish<T>(message)to send messages. - Use
Subscribe<T>(handler)to listen and keep the returnedIMessageSubscription. - Dispose subscriptions in
OnDisableorOnDestroy. - If a message is published before any subscriber exists, it is queued and replayed to the first later subscriber.
- Use
Subscribe<T>(handler, replayPending: false)when you do not want old messages. UMessageCenter.Instance.Clear()removes all subscribers and pending messages; it is mainly useful for tests or full resets.- Exceptions thrown by handlers are logged and do not stop other handlers.
- Destroyed
UnityEngine.Objectsubscriber targets are cleaned up automatically.
IMessageSubscription subscription = UMessageCenter.Instance.Subscribe<ScoreChangedMessage>(
message => Debug.Log(message.Value),
replayPending: false);UUtils is a collection of small runtime helpers. Import UTools and call them as extension/static methods.
using System;
using UnityEngine;
using UTools;
public sealed class UtilityExample : MonoBehaviour
{
private void Start()
{
bool validName = "player_one".CheckUserName();
string shortName = "VeryLongDisplayName".TrimLength(10);
string timer = 95.ToTimeString();
string chineseTimer = TimeSpan.FromSeconds(3661).ToHhMmSsString(useChinese: true);
Debug.Log($"{validName}, {shortName}, {timer}, {chineseTimer}");
}
}Common methods:
IsNullOrEmpty(),IsNotNullOrEmpty()CheckUserName(),CheckStrChinese(),IsIPAddress()TrimLength(maxLength),ToBase64String()ToTimeString(),ToHhMmSsString(),TryCalculateTimeSpan(...)
using UTools;
UFileUtilities.WriteToPersistentDataPath("{\"volume\":0.8}", "settings/user.json");
string json = UFileUtilities.ReadFromPersistentDataPath("settings/user.json");Notes:
- Files are read from and written to
Application.persistentDataPath. - Parent folders are created automatically.
ReadFromPersistentDataPathcreates an empty file by default when it does not exist.
using UnityEngine;
using UTools;
public sealed class UiHelperExample : MonoBehaviour
{
[SerializeField] private GameObject panelRoot;
[SerializeField] private RectTransform popup;
public void ShowInventory()
{
GameObject closeButton = panelRoot.FindChild("CloseButton");
panelRoot.ToggleAllChildren(true);
popup.ToggleAsCanvasGroup(true, useTween: false);
closeButton?.ToggleBlink(true);
}
}Common methods:
FindChild(...),GetAllDescendants(),GetDirectChildren()ShowOnlyDescendantNamed(...),HideDescendantNamed(...)ToggleAllChildren(show)toggles descendants, not the root itselfEnsureComponent<T>(),HasComponent<T>(),SetLayerRecursively(...)ToggleAsCanvasGroup(...),ToggleAsCanvasGroupAuto(...),TweenColor(...),MoveOutOfScreen(...)
Notes:
- Most helpers are null-safe and return
null, empty collections, orfalsewhen input is invalid. - UI tween helpers use a hidden persistent coroutine runner if no runner is provided.
- Pass your own
MonoBehaviourrunner when you want coroutine lifetime to follow a specific object.
Common methods:
Texture2D.ToSprite()Sprite.ToTexture2D()Texture.ToTexture2D()Texture2D.ToBase64()DecodeBase64Image(...)CloneMesh(...),CombineMesh(...),GenerateQuadMesh(...),GeneratePolygonMesh(...),GeneratePlane(...)
Notes:
- Texture conversion creates runtime objects; destroy them when they are no longer needed.
- Mesh helpers create runtime GameObjects/meshes; manage their lifetime like other generated Unity objects.
Creates an Inspector button for a parameterless method.
using UnityEngine;
using UTools;
public sealed class SpawnDebugTool : MonoBehaviour
{
[Button("Spawn Test Enemy")]
private void SpawnTestEnemy()
{
Debug.Log("Spawned");
}
}Notes:
- Methods must have no parameters.
- Public and private instance methods are supported.
- With multi-object selection, the button invokes the method on every selected target.
Shows a serialized field based on another serialized field.
using UnityEngine;
using UTools;
public sealed class DamageConfig : MonoBehaviour
{
[SerializeField] private bool useCritical;
[SerializeField, ShowIf(nameof(useCritical))]
private float criticalMultiplier = 2f;
[SerializeField, ShowIf(nameof(useCritical), inverse: false)]
private float normalMultiplier = 1f;
}Notes:
- By default, the field is shown when the condition is truthy.
inverse: falseshows the field when the condition is falsey.- Supported condition types:
bool,int,float,string, and object reference. - Missing or unsupported condition fields are shown and log an error.
Adds or removes required components from the same GameObject through a boolean Inspector toggle.
using UnityEngine;
using UTools;
public sealed class PhysicsToggle : MonoBehaviour
{
[SerializeField, AutoComponent(typeof(Rigidbody), typeof(Collider))]
private bool usePhysics;
}Notes:
- The attribute is intended for
boolfields. - Toggling on adds missing components in the editor.
- Toggling off removes those components in the editor.
- It does not add or remove components while the game is playing.
Unity Test Framework entry points are included:
Assets/UTools/Tests/EditModeAssets/UTools/Tests/PlayMode
Assets/UTools/Scripts: runtime and editor sourceAssets/UTools/Example: example scenes and scripts used inside this repoAssets/UTools/Tests: EditMode and PlayMode testsAssets/UTools/Documentation~: package documentationAssets/UTools/Samples~: package sample placeholder content