45 Core Utilities – System

18 min read

Reflection and type lookup with caching, interface-typed component queries Unity does not offer, thread-safety primitives with main-thread marshalling, platform capability checks, and Resources loading that names the asset it could not find.

Summary

Between your game and the runtime underneath it sit six classes, each covering a different kind of friction: finding types by name, querying components by interface, moving work back to the main thread, asking what the current platform can do, and loading from Resources without the silent null Unity hands back.

Two of them solve problems Unity leaves open. InterfaceUtil finds components by interface, which GetComponent<T> cannot do usefully across a scene, so a system that wants “every object implementing IDamageable” has somewhere to ask. ResourceUtil wraps Resources.Load with variants that distinguish “missing and that is fine” from “missing and that is a bug”, so the second case names the path in the log rather than passing you a null that looks like the first.

TypeUtil is the reflection layer, and it caches. Type lookup by name, attribute reads, finding every type derived from a base or carrying an attribute: all the operations a plugin system, a save migrator or an editor tool needs, with the results cached so the second call is cheap. Reflection is expensive enough that doing it uncached in a loop is a real cost.

ThreadUtil is the thread-safety toolkit: atomics, volatile reads and writes, spin waits, and the main-thread marshalling that matters because almost all of Unity’s API is main-thread only. PlatformUtil answers what you are running on and what it can do. AssemblyUtil is two properties reporting whether a domain reload is in progress, which is the thing that invalidates every cached reflection result.

Use the system utilities for:

  • Plugin and mod discovery: finding every type deriving from a base or carrying an attribute.
  • Interface-based systems where the concrete component type is not known: damageable, interactable, saveable.
  • Getting results from a background thread or an async continuation back onto the main thread safely.
  • Branching on platform capability rather than platform name: touchscreen, gyroscope, compute shaders.
  • Loading from Resources where a missing asset should be reported at the load rather than surfacing as a null three systems later.

Features

  • Cached type lookup. GetType and TryGetType resolve a type from a string across loaded assemblies, with the result cached.
  • Attribute reading. GetAttribute, GetAttributes and HasAttribute on any MemberInfo, with an index for repeated attributes.
  • Type discovery. GetDerivedTypes<T> and GetTypesWithAttribute<T> scan the loaded assemblies once and cache the result, so a plugin registry can build itself at startup with a single scan.
  • Interface-typed component queries. GetComponents<T> and FindComponentsOfType<T> where T is an interface, covering the case Unity’s own API does not.
  • Main-thread marshalling. RunOnMainThread runs immediately when already on the main thread, and otherwise posts to Unity’s synchronization context.
  • Atomics and volatile access. Increment, Decrement, Add, Exchange and CompareExchange, plus typed VolatileRead and VolatileWrite for bool, int, long, float, double and reference types.
  • Spin and yield helpers. SpinWait, SpinUntil with a timeout, and YieldPeriodically for a long loop that should not starve other threads.
  • Platform identity and capability. Family checks like IsMobile and IsConsole, specific platforms down to IsPS5 and IsXboxSeries, and capability checks such as HasTouchscreen and SupportsComputeShaders.
  • Device information. Processor count, system and graphics memory, maximum texture size, device and OS names, for scaling quality settings to the hardware.
  • Resource loading with intent. Load warns when an asset is missing, LoadOptional stays silent, TryLoad gives you the branch. All three still return null on a miss; what differs is whether it is reported. Plus async and Awaitable variants and prefab instantiation in one call.
  • Domain reload detection. AssemblyUtil.IsReloading and ReloadCount, for editor code that must not touch cached state mid-reload.

The six classes

The six classes split by which part of the runtime each one talks to.

ClassCoversNotes
TypeUtilReflection, type lookup, attributesCaches type lookups and derived-type scans, since scanning every loaded assembly is expensive and the answer does not change during a session. ClearCache exists for editor code that survives a recompile. IsSubclassOfRawGeneric handles the open-generic case that IsSubclassOf gets wrong, and IsNullable plus GetUnderlyingTypeOrSelf cover nullable value types when reflecting over serialized fields.
InterfaceUtilComponent queries by interfaceThe gap-filler. Unity’s GetComponent<T> does work with interfaces on a single object, but there is no good scene-wide equivalent, and this provides one. Also caches, with its own ClearCache. GetInterfaceTypes reports which interfaces a type implements, for a registry that dispatches by contract.
ThreadUtilThreading and main-thread marshallingIsMainThread and RunOnMainThread are the ones you will use. The atomics wrap Interlocked and the volatile helpers wrap Volatile, both with the typed overloads that avoid casting. SpinUntil takes a timeout so a wait cannot hang forever.
PlatformUtilWhat you are running on, and what it can doAll properties, no methods. Splits into family checks, specific platforms, capability checks and device information. Prefer the capability checks to the platform checks wherever one exists, since a desktop with a touchscreen exists and a phone without a gyroscope does too.
ResourceUtilLoading from ResourcesThe variants encode intent rather than changing the outcome. All of them return null when the asset is absent; Load also logs a warning naming the path, LoadOptional says nothing, and TryLoad reports the result as a bool. Choosing the right one is about whether a miss should show up in the log, not about whether you still need a null check. LoadAsync returns Unity’s request object and LoadAsyncAwaitable returns an Awaitable for async code. The InstantiatePrefab overloads load and instantiate in one call, with parent, position and rotation forms.
AssemblyUtilDomain reload stateTwo properties. IsReloading is true while the editor is recompiling and swapping the domain, which is exactly when cached reflection results and static state are unreliable. ReloadCount lets a tool notice that a reload happened since it last looked.

Recipes

Nothing below depends on anything else, so treat this as a lookup: find your problem, then read that one recipe.

Discover every plugin type at startup

Problem. Your game supports content modules that register themselves, and you do not want a hand-maintained list of them. Anything deriving from a base class, or carrying your attribute, should be found automatically.

Solution. GetDerivedTypes and GetTypesWithAttribute scan the loaded assemblies and cache the result, so the startup cost is paid once.

/* Scans every loaded assembly for concrete types deriving from the base. The result
   is cached, so a second call during the same session is a dictionary read. */
var abilityTypes = TypeUtil.GetDerivedTypes<AbilityBase>();

foreach (var type in abilityTypes)
{
    /* Attribute reads are part of the same class, so metadata and discovery use one
       API rather than two. */
    if (!TypeUtil.HasAttribute<RegisterAbilityAttribute>(type))
    {
        continue;
    }

    var meta = TypeUtil.GetAttribute<RegisterAbilityAttribute>(type);
    _abilityRegistry.Register(meta.Id, type);
}

/* The attribute-first form when the attribute is the marker rather than the base. */
var commandTypes = TypeUtil.GetTypesWithAttribute<ConsoleCommandAttribute>();

/* Type from a string, for a save file that stored a type name. TryGetType is the safe
   form for anything that came from data. */
if (TypeUtil.TryGetType(save.AbilityTypeName, out var restoredType))
{
    Restore(restoredType);
}

Find everything in the scene that can take damage

Problem. Your explosion needs every object implementing IDamageable in a radius. The concrete types are unrelated, so there is no component type to search for.

Solution. InterfaceUtil queries by interface across the scene.

/* Scene-wide query by interface. Unity has no built-in way to ask this question,
   so this class exists to answer it. */
var damageables = InterfaceUtil.FindComponentsOfType<IDamageable>();

foreach (var target in damageables)
{
    var component = target as Component;

    if (component == null)
    {
        continue;
    }

    if (blast.Contains(component.transform.position))
    {
        target.ApplyDamage(damage);
    }
}

/* The single-object forms, for when you have the GameObject already. */
if (InterfaceUtil.TryGetComponent<IInteractable>(hitObject, out var interactable))
{
    interactable.Interact(player);
}

/* And the reflection-side question: which of my contracts does this type implement?
   Useful for a registry that dispatches an object to every system that wants it. */
var contracts = InterfaceUtil.GetInterfaceTypes<ISaveable>();

Get a background result back onto the main thread

Problem. You loaded save data on a worker thread, or awaited a network call, and now you need to update the UI. Almost every Unity API call throws off the main thread.

Solution. RunOnMainThread runs the action immediately if you are already on the main thread, and otherwise posts it to Unity’s synchronization context to run on the next frame.

private async void LoadProfileAsync()
{
    /* Work off the main thread: parsing, decompression, a network round trip. */
    var profile = await Task.Run(() => ParseProfile(bytes));

    /* Back to the main thread to touch Unity objects. If this is already the main
       thread the action runs inline, so the call is safe either way. */
    ThreadUtil.RunOnMainThread(() =>
    {
        _nameLabel.text = profile.Name;
        _levelLabel.text = profile.Level.ToString();
    });
}

/* IsMainThread when you want to branch rather than marshal: assert in debug code, or
   take a faster path that skips the marshalling entirely. */
if (!ThreadUtil.IsMainThread)
{
    Log.Warning("Texture upload attempted off the main thread.", LogCategory.Renderer);
    return;
}

Count work across threads without a lock

Problem. Several worker threads are processing chunks and you need an accurate count of how many finished, plus a flag the main thread can read to know they are done.

Solution. The atomics and volatile helpers, which wrap Interlocked and Volatile with typed overloads.

private int _chunksCompleted;
private bool _generationFinished;

/* Called from worker threads. Increment is atomic, so no lock and no lost updates. */
private void OnChunkDone()
{
    var done = ThreadUtil.Increment(ref _chunksCompleted);

    if (done == _totalChunks)
    {
        /* A volatile write makes the flag visible to other threads promptly, rather
           than sitting in a register or a store buffer. */
        ThreadUtil.VolatileWrite(ref _generationFinished, true);
    }
}

/* Read from the main thread. The volatile read is the other half of the pair: without
   it the compiler is free to hoist the check out of a loop and spin forever. */
private void Update()
{
    if (ThreadUtil.VolatileRead(ref _generationFinished))
    {
        BuildMeshes();
    }
}

/* CompareExchange for claim-once semantics: only the first caller gets the slot. */
if (ThreadUtil.CompareExchange(ref _ownerId, myId, 0) == 0)
{
    /* We claimed it. */
}

Wait for a worker without hanging the game

Problem. You need to block briefly until a background job publishes its result, and you want a guarantee that a bug in the worker cannot freeze the game forever.

Solution. SpinUntil takes a timeout and reports whether the condition was met.

/* Returns false on timeout instead of waiting forever, so a stuck worker becomes a
   recoverable error rather than a hang. */
if (!ThreadUtil.SpinUntil(() => ThreadUtil.VolatileRead(ref _resultReady), 2000))
{
    Log.Error("Chunk worker did not publish a result within 2 seconds.", LogCategory.Core);
    return;
}

/* YieldPeriodically inside a long worker loop, so it does not starve other threads.
   The counter is passed by ref and the helper decides when to yield. */
var iterations = 0;

for (var i = 0; i < voxelCount; i++)
{
    ProcessVoxel(i);
    ThreadUtil.YieldPeriodically(ref iterations);
}

Scale quality to the hardware rather than to the platform name

Problem. You want lower settings on weak devices. Branching on “is this mobile” gets a flagship phone the same settings as a five-year-old budget one.

Solution. PlatformUtil reports capability and device information as well as platform identity, and the capability checks are the ones that age well.

/* Capability, not platform. A desktop with a touchscreen exists, and so does a phone
   without a gyroscope, so ask about the feature you actually need. */
if (PlatformUtil.HasTouchscreen)
{
    EnableTouchControls();
}

if (!PlatformUtil.SupportsComputeShaders)
{
    /* Fall back to the fragment shader path rather than failing to render. */
    _oceanRenderer.UseComputePath = false;
}

/* Device information for scaling rather than switching. Memory and core count say
   more about what a device can handle than its family does. */
var tier = PlatformUtil.SystemMemoryMB >= 6000 && PlatformUtil.ProcessorCount >= 6
    ? QualityTier.High
    : QualityTier.Low;

/* Texture budget from what the GPU will actually accept. */
_streamingBudget = Mathf.Min(PlatformUtil.MaxTextureSize, 2048);

/* Platform identity when the difference really is the platform: store integration,
   input glyphs, a certification requirement. */
if (PlatformUtil.IsPS5 || PlatformUtil.IsXboxSeries)
{
    EnableConsolePresenceIntegration();
}

/* And the build-type check, for diagnostics that ship disabled. */
if (!PlatformUtil.IsReleaseBuild)
{
    EnableDeveloperOverlay();
}

Load a required asset and know immediately when it is missing

Problem. A missing prefab returns null from Resources.Load, and you discover it three systems later as a NullReferenceException with no clue what was actually absent.

Solution. The ResourceUtil variants encode whether the asset is required, so the failure surfaces at the load.

/* Load logs a warning naming the path when the asset is missing, so the failure
   shows up where it happened rather than as a null reference three systems later.
   It still returns null, so a required asset is worth checking as well. */
var bulletPrefab = ResourceUtil.Load<GameObject>("Prefabs/Bullet");

/* LoadOptional for something genuinely optional: a localisation override, a themed
   skin that not every build ships. A null here is expected, not a bug. */
var seasonalSkin = ResourceUtil.LoadOptional<Material>("Skins/Halloween");

/* TryLoad when the branch reads better than a null check. */
if (ResourceUtil.TryLoad<AudioClip>("Audio/Stinger", out var stinger))
{
    audioSource.PlayOneShot(stinger);
}

/* Exists to check without loading, for validation passes and editor tooling. */
if (!ResourceUtil.Exists<GameObject>("Prefabs/Boss"))
{
    Log.Error("Boss prefab is missing from Resources.", LogCategory.Asset);
}

/* Load and instantiate in one call, with the parenting and placement forms you
   would otherwise write around it. */
var muzzleFlash = ResourceUtil.InstantiatePrefab("FX/MuzzleFlash", muzzle.position,
    muzzle.rotation, effectsRoot);

/* The typed form returns the component rather than the GameObject. */
var spawnedTurret = ResourceUtil.InstantiatePrefab<Turret>("Prefabs/Turret", baseTransform);

Load without stalling the frame

Problem. Loading a large asset on demand causes a visible hitch.

Solution. Two async shapes, depending on whether your code is coroutine-based or async.

/* Coroutine style: Unity's ResourceRequest, yielded from an IEnumerator. */
private IEnumerator PreloadBoss()
{
    var request = ResourceUtil.LoadAsync<GameObject>("Prefabs/Boss");
    yield return request;

    _bossPrefab = request.asset as GameObject;
}

/* Async style: an Awaitable, for code already using async/await. */
private async Awaitable PreloadBossAsync()
{
    _bossPrefab = await ResourceUtil.LoadAsyncAwaitable<GameObject>("Prefabs/Boss");
}

Keep editor tooling honest across a recompile

Problem. Your editor window caches reflection results, and after a script recompile those cached Type objects belong to a domain that no longer exists.

Solution. AssemblyUtil reports reload state, and both caching classes expose a clear.

/* True while the editor is swapping the domain, which is when cached reflection
   results and static state are not to be trusted. */
if (AssemblyUtil.IsReloading)
{
    return;
}

/* ReloadCount lets a tool notice a reload happened since it last looked, without
   subscribing to the editor's own callbacks. */
if (_seenReloadCount != AssemblyUtil.ReloadCount)
{
    _seenReloadCount = AssemblyUtil.ReloadCount;

    TypeUtil.ClearCache();
    InterfaceUtil.ClearCache();
    RebuildInspectorModel();
}

API Sketch

All six live in Scylla.Core.Util.

/* ---------------------------------------------------------------- */
/* TypeUtil: reflection, cached                                     */
/* ---------------------------------------------------------------- */
Type      GetType(string typeName);
bool      TryGetType(string typeName, out Type type);
T         GetAttribute<T>(MemberInfo member, int index = 0);
Attribute GetAttribute(MemberInfo member, Type attributeType, int index = 0);
T[]       GetAttributes<T>(MemberInfo member);
bool      HasAttribute<T>(MemberInfo member);
bool      IsSubclassOfRawGeneric(this Type toCheck, Type baseType);
Type[]    GetDerivedTypes<T>();
Type[]    GetDerivedTypes(Type baseType);
Type[]    GetTypesWithAttribute<T>();
bool      IsNullable(this Type type);
Type      GetUnderlyingTypeOrSelf(this Type type);
void      ClearCache();

/* ---------------------------------------------------------------- */
/* InterfaceUtil: component queries by interface                    */
/* ---------------------------------------------------------------- */
T[]    GetComponents<T>();
T[]    GetComponents<T>(bool includeInactive);
T[]    FindComponentsOfType<T>();
T[]    FindComponentsOfType<T>(bool includeInactive);
T[]    GetComponents<T>(GameObject gameObject);
T      GetComponent<T>(GameObject gameObject);
bool   TryGetComponent<T>(GameObject gameObject, out T component);
bool   HasComponent<T>(GameObject gameObject);
Type[] GetInterfaceTypes<T>();
Type[] GetInterfaceTypes(Type interfaceType);
void   ClearCache();

/* ---------------------------------------------------------------- */
/* ThreadUtil: threading and main-thread marshalling                */
/* ---------------------------------------------------------------- */
bool IsMainThread { get; }
void RunOnMainThread(Action action);
void ProcessMainThreadQueue();      // only needed on the fallback path; see the callout

int  Increment(ref int location);
int  Decrement(ref int location);
int  Add(ref int location, int value);
int  Exchange(ref int location, int value);
int  CompareExchange(ref int location, int value, int comparand);

bool   VolatileRead(ref bool location);      void VolatileWrite(ref bool location, bool value);
int    VolatileRead(ref int location);       void VolatileWrite(ref int location, int value);
long   VolatileRead(ref long location);      void VolatileWrite(ref long location, long value);
float  VolatileRead(ref float location);     void VolatileWrite(ref float location, float value);
double VolatileRead(ref double location);    void VolatileWrite(ref double location, double value);
T      VolatileRead<T>(ref T location);

void MemoryBarrier();
void Yield();
void Sleep(int milliseconds);
void SpinWait(int iterations);
bool SpinUntil(Func<bool> condition, int timeoutMilliseconds = -1);
void YieldPeriodically(ref int iterationCount, int yieldFrequency = 256);

/* ---------------------------------------------------------------- */
/* PlatformUtil: all properties                                     */
/* ---------------------------------------------------------------- */
/* Families */      bool IsDesktop, IsMobile, IsConsole, IsWeb, IsWSA;
/* Platforms */     bool IsWindows, IsOSX, IsLinux, IsIOS, IsAndroid, IsWebGL, IsTVOS;
/*           */     bool IsPlayStation, IsPS5, IsXbox, IsXboxSeries, IsSwitch;
/* Build */         bool IsReleaseBuild, IsDualInputEnabled;
/* Capabilities */  bool HasTouchscreen, SupportsGyroscope, SupportsAccelerometer;
/*              */  bool SupportsVibration, SupportsLocationService, SupportsComputeShaders;
/* Device */        string DeviceName, DeviceModel, OperatingSystem, ProcessorType;
/*        */        int ProcessorCount, SystemMemoryMB, GraphicsMemoryMB, MaxTextureSize;

/* ---------------------------------------------------------------- */
/* ResourceUtil: Resources loading with intent                      */
/* ---------------------------------------------------------------- */
T    Load<T>(string path);              // warns on a miss, still returns null
T    LoadOptional<T>(string path);      // silent on a miss, returns null
bool TryLoad<T>(string path, out T resource);
T[]  LoadAll<T>(string path);
T    GetBuiltin<T>(string path);
bool Exists(string path);
bool Exists<T>(string path);

ResourceRequest   LoadAsync<T>(string path);
async Awaitable<T> LoadAsyncAwaitable<T>(string path);

GameObject InstantiatePrefab(string path);
GameObject InstantiatePrefab(string path, Transform parent);
GameObject InstantiatePrefab(string path, string name, Transform parent);
GameObject InstantiatePrefab(string path, Vector3 position, Quaternion rotation, Transform parent = null);
T          InstantiatePrefab<T>(string path);
T          InstantiatePrefab<T>(string path, Transform parent);
T          InstantiatePrefab<T>(string path, Vector3 position, Quaternion rotation, Transform parent = null);

/* ---------------------------------------------------------------- */
/* AssemblyUtil: domain reload state                                */
/* ---------------------------------------------------------------- */
bool IsReloading { get; }
int  ReloadCount { get; }

See the API reference for full signatures, remarks, and exception contracts on every member.

Best Practices

  • Do type discovery once, at startup. GetDerivedTypes caches, but the first call still scans every loaded assembly. Build your registry during initialization rather than lazily on first use in gameplay.
  • Prefer a self-registering list to a repeated scene-wide interface query. FindComponentsOfType walks the scene; objects adding themselves in OnEnable and removing themselves in OnDisable costs nothing per query.
  • Use IsSubclassOfRawGeneric whenever the base is an open generic. IsSubclassOf returns false for AbilityBase<> against a closed subclass, and that false looks like a missing type rather than a wrong question.
  • Wrap Unity API calls from async continuations in RunOnMainThread. An await can resume anywhere, and most of Unity’s API throws off the main thread. The call is cheap when you are already on it.
  • Pair every volatile write with a volatile read. Writing volatile and reading normally gives you no guarantee at all; the compiler can still hoist the read out of a loop.
  • Give SpinUntil a timeout. The default is to wait forever, which turns a worker bug into a hung game rather than a logged error.
  • Ask about capability, not platform, wherever both exist. A desktop with a touchscreen and a phone without a gyroscope both exist, and the capability check is the one that stays correct as hardware changes.
  • Scale on device information rather than switching on platform family. Memory and core count separate a flagship phone from a budget one; IsMobile does not.
  • Pick the ResourceUtil variant that matches your intent. All three return null on a miss, so the choice is about whether it is reported: Load warns, LoadOptional does not, TryLoad hands you a bool. Using LoadOptional for a required asset is how a missing file becomes a silent null.
  • Clear the reflection caches on domain reload in editor tooling. TypeUtil and InterfaceUtil both cache, and cached Type objects from a previous domain are not valid after a recompile.
  • Check AssemblyUtil.IsReloading before touching static state in editor code. A reload in progress is exactly when caches and statics are being torn down.
  • Prefer Resources for small, always-needed assets only. Everything in a Resources folder is included in the build and loaded into the catalog. For anything large or conditional, Addressables is the better tool.

Pitfalls