Skip to content
apk edited this page Feb 2, 2026 · 2 revisions

[Track] generates fast, allocation-free access to all currently active instances of T.

Usage API:

  • T.Instances (generated static property, returns enumerable, best used with foreach)
  • Optional, aligned arrays for jobs/unmanaged work:
    • T.TransformAccessArray (for job system transform access)
    • T.InstanceIDs (array of Unity Instance IDs)
    • T.Unmanaged.*Array (NativeArray<TData> with your custom data) via IUnmanagedData<TData>
  • Optional ID lookup via IFindByID<TId> (T.FindByID(...), T.TryFindByID(...))
  • Optional instance indexing via IInstanceIndex (useful for aligned-arrays usage)

Related pages:

Quick start

  1. Mark the type with [Track]
  2. Make it partial
  3. Iterate Instances with foreach:
[Track]
public sealed partial class Enemy : MonoBehaviour
{
    public int Health = 10;

    public void Hurt(int damage)
        => Health -= damage;
}

public sealed class DamageAllEnemies : MonoBehaviour
{
    void Update()
    {
        foreach (var enemy in Enemy.Instances)
            enemy.Hurt(1);
    }
}

Notes:

  • For MonoBehaviour, active means the component is enabled (registered on enable, unregistered on disable).
  • For ScriptableObject, active means it is loaded and has executed OnEnable (similar idea, different lifecycle).

Generated API

For a tracked class T, the source generator emits:

  • Static property: public static TrackedInstances<T> Instances { get; }
  • Registration hooks:
    • OnEnableINTERNAL() registers
    • OnDisableINTERNAL() unregisters
    • These are generated as protected new methods to avoid conflicts with your own OnEnable/OnDisable.
  • Optional extra members depending on what you enable (see below).

Generic API: Find.Instances<T>()

Find.Instances<T>() is the same underlying tracking, but works well in generic code and for tracked interfaces:

[Track]
public interface IDamageable
{
    void Hurt(int damage);
}

[Track]
public sealed partial class Enemy : MonoBehaviour, IDamageable
{
    public void Hurt(int damage) { }
}

[Track]
public sealed partial class Crate : MonoBehaviour, IDamageable
{
    public void Hurt(int damage) { }
}

public static class Example
{
    public static void HurtAllDamageables(int damage)
    {
        foreach (var d in Find.Instances<IDamageable>())
            d.Hurt(damage);
    }
}

Notes:

  • Marking an interface with [Track] does not generate members on the interface.
  • Instead, tracked classes that implement that interface will also register into the interface tracked list.

Configuration

There are several settings configurable in the TrackAttribute constructor:

[Track(
    instanceIdArray: false,
    transformAccessArray: false,
    transformInitialCapacity: 64,
    transformDesiredJobCount: -1,
    cacheEnabledState: false,
    manual: false
)]

manual

When manual: true, the generator does not auto-register in enable/disable callbacks. Instead it generates:

  • RegisterInstance()
  • UnregisterInstance()

You call them from your own lifecycle methods:

[Track(manual: true)]
public sealed partial class TrackedManually : MonoBehaviour
{
    void OnEnable()  => RegisterInstance();
    void OnDisable() => UnregisterInstance();
}

Rules for manual mode:

  • Always register/unregister symmetrically (exactly once each). Don't let destroyed instances stay in the registry.
  • If you also enable transforms/unmanaged data/ID lookup, those are updated by the same calls.

cacheEnabledState

When cacheEnabledState: true is set, the generator emits public new bool enabled for MonoBehaviour types.

Why it exists:

  • Reading Behaviour.enabled can be an expensive extern call.
  • The generated property caches enabled state in a field and keeps it updated during registration/unregistration.

Behavior notes:

  • Setting component.enabled = false still updates the real Behaviour.enabled (setter forwards to base.enabled).
  • In edit mode, the generated getter falls back to base.enabled for accuracy.

instanceIdArray

When instanceIdArray: true is set, the generator emits:

public static NativeArray<int> InstanceIDs { get; }

This is aligned with Instances (and with TransformAccessArray if enabled).

Use case: map tracked objects to IDs you can safely pass through unmanaged contexts to identify objects and/or later resolve them on the main thread (e.g., using Resources.InstanceIDToObjectList)

transformAccessArray, transformInitialCapacity, transformDesiredJobCount

When transformAccessArray: true is used, the generator emits:

public static TransformAccessArray TransformAccessArray { get; }

This is aligned with Instances and is updated with swap-back removal.

Use case: parallel work on tracked object transforms using the job system.

Read more: TransformAccessArray

Additional interfaces: IFindByID<TId> and IUnmanagedData<TData>

Implementing these interfaces opts into additional features.

IFindByID<TId> gives T.FindByID(id) / T.TryFindByID(id, out var instance) (fast dictionary lookup).

  • TId is the key type you want to use (e.g., int).
  • Use cases include anything requiring keyed instance lookup: item database, serialization, etc.
  • Read more: IFindByID

IUnmanagedData<TData> is a powerful feature that attaches an unmanaged NativeArray<TData> aligned with instances.

  • TData is the type of data you want to store. You'll usually want to define a struct to hold your data.
  • Use case: Burst + job system integration.
  • Read more: IUnmanagedData<TData>

Important behavior

1) Order is not stable (swap-back removal)

Unregister uses a swap-back removal for speed:

  • When an instance leaves tracking, the last element swaps into its slot.
  • This makes unregister fast, but it means indices and order can change over time.

Practical consequence:

  • Treat the tracked set like an unordered bag.
  • Never assume Instances[i] refers to the same object across frames (or even during a frame when you are unsure that instances aren't being created/enabled/disabled/destroyed).

2) Do not enable/disable tracked instances while enumerating

The instance list is reordered when instances are enabled/disabled. This will break enumeration logic.

If you must modify enabled state while iterating, take a snapshot using WithCopy:

foreach (var enemy in Enemy.Instances.WithCopy)
    enemy.enabled = false;

In DEBUG builds, the enumerator detects changes during enumeration and logs an error.

3) IInstanceIndex is the aligned-arrays key

If you use any parallel-arrays feature (unmanaged data, transforms, instance IDs), it is useful to implement IInstanceIndex:

  • Each instance gets an InstanceIndex that matches its slot in:
    • T.Instances
    • T.TransformAccessArray (if enabled)
    • T.InstanceIDs (if enabled)
    • T.Unmanaged.*Array (if enabled)

Read more: IInstanceIndex.

4) Edit mode behavior

In edit mode (for editor tooling compatibility), tracking uses a slower refresh path that rebuilds lists using Unity find APIs and caches for one editor update.

This is intentionally editor-only; play mode uses fast-path registration.

Clone this wiki locally