-
Notifications
You must be signed in to change notification settings - Fork 10
[Track]
[Track] generates fast, allocation-free access to all currently active instances of T.
Usage API:
-
T.Instances(generated static property, returns enumerable, best used withforeach) - 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) viaIUnmanagedData<TData>
-
- Optional ID lookup via
IFindByID<TId>(T.FindByID(...),T.TryFindByID(...)) - Optional instance indexing via
IInstanceIndex(useful for aligned-arrays usage)
Related pages:
- Mark the type with
[Track] - Make it
partial - Iterate
Instanceswithforeach:
[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 executedOnEnable(similar idea, different lifecycle).
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 newmethods to avoid conflicts with your ownOnEnable/OnDisable.
-
- Optional extra members depending on what you enable (see below).
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.
There are several settings configurable in the TrackAttribute constructor:
[Track(
instanceIdArray: false,
transformAccessArray: false,
transformInitialCapacity: 64,
transformDesiredJobCount: -1,
cacheEnabledState: false,
manual: false
)]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.
When cacheEnabledState: true is set, the generator emits public new bool enabled for MonoBehaviour types.
Why it exists:
- Reading
Behaviour.enabledcan 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 = falsestill updates the realBehaviour.enabled(setter forwards tobase.enabled). - In edit mode, the generated getter falls back to
base.enabledfor accuracy.
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)
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
Implementing these interfaces opts into additional features.
IFindByID<TId> gives T.FindByID(id) / T.TryFindByID(id, out var instance) (fast dictionary lookup).
-
TIdis 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.
-
TDatais the type of data you want to store. You'll usually want to define astructto hold your data. - Use case: Burst + job system integration.
- Read more:
IUnmanagedData<TData>
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).
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.
If you use any parallel-arrays feature (unmanaged data, transforms, instance IDs), it is useful to implement IInstanceIndex:
- Each instance gets an
InstanceIndexthat matches its slot in:T.Instances-
T.TransformAccessArray(if enabled) -
T.InstanceIDs(if enabled) -
T.Unmanaged.*Array(if enabled)
Read more: IInstanceIndex.
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.