Entity Events¶
Entity events let a service react to structural changes — entities added, removed, or moved between partitions. Observer callbacks fire during submission, after the queued change has been applied.
Anatomy of a subscription¶
Build a subscription in three parts: pick the scope (which entities to watch), the event (add / remove / move), and a handler:
World.Events
.EntitiesWithTags<MyTag>() // 1. scope
.OnRemoved(OnEntityRemoved); // 2. event (3. handler)
Scopes¶
A scope picks which entities the subscription watches:
| Method | Matches |
|---|---|
EntitiesWithTags<...>() |
Entities whose tag set includes all of the given tags |
EntitiesWithComponents<T...>() |
Entities whose template declares all of the given components (up to 4 type parameters) |
EntitiesWithTagsAndComponents<T...>(TagSet) |
Entities matching both the given tags and the given components |
EntitiesInGroup(GroupIndex) |
Entities in one specific group |
AllEntities() |
Every entity |
Events¶
| Event | Trigger |
|---|---|
OnAdded |
Entities that match the scope after a structural change |
OnRemoved |
Entities removed from matching scope |
OnMoved |
Entities that changed partition (tag combination) |
Handlers¶
The recommended pattern is to use a [ForEachEntity] method as the event handler. In a system, subscribe in OnReady and dispose in OnShutdown:
public partial class FishDeathSystem : ISystem
{
IDisposable _onFishRemoved;
partial void OnReady()
{
_onFishRemoved = World.Events
.EntitiesWithTags<FrenzyTags.Fish>()
.OnRemoved(OnFishRemoved);
}
partial void OnShutdown() => _onFishRemoved?.Dispose();
[ForEachEntity]
void OnFishRemoved(in TargetMeal targetMeal)
{
if (targetMeal.Value.Exists(World))
targetMeal.Value.Remove(World);
}
public void Execute() { }
}
See OnReady and OnShutdown for system lifecycle details.
The same pattern works outside systems — any class that has access to a WorldAccessor can subscribe. Use a DisposeCollection when managing multiple subscriptions:
public partial class RemoveCleanupHandler : IDisposable
{
readonly DisposeCollection _disposables = new();
public RemoveCleanupHandler(World world)
{
World = world.CreateAccessor(AccessorRole.Fixed);
World.Events
.EntitiesWithTags<FrenzyTags.Fish>()
.OnRemoved(OnFishRemoved)
.AddTo(_disposables);
}
WorldAccessor World { get; }
[ForEachEntity]
void OnFishRemoved(in TargetMeal targetMeal)
{
if (targetMeal.Value.Exists(World))
targetMeal.Value.Remove(World);
}
public void Dispose() => _disposables.Dispose();
}
All [ForEachEntity] features are supported on event handlers, including aspects:
public partial class CleanupHandlers : IDisposable
{
readonly RenderableGameObjectManager _goManager;
readonly DisposeCollection _disposables = new();
public CleanupHandlers(World world, RenderableGameObjectManager goManager)
{
World = world.CreateAccessor(AccessorRole.Fixed);
_goManager = goManager;
World.Events
.EntitiesWithTags<SampleTags.Prey>()
.OnRemoved(OnPreyRemoved)
.AddTo(_disposables);
}
WorldAccessor World { get; }
[ForEachEntity]
void OnPreyRemoved(in Prey prey)
{
var go = _goManager.Resolve(prey.GameObjectId);
UnityEngine.Object.Destroy(go);
}
public void Dispose() => _disposables.Dispose();
partial struct Prey : IAspect, IRead<GameObjectId, ApproachingPredator> { }
}
Components read inside OnRemoved are still valid even though the callback fires after removal — removed entities are parked at the end of the backing array (past the active count), so the buffers haven't been cleared. Removed components are contiguous in memory, which is cache-friendly for cleanup.
Beyond [ForEachEntity] parameters, you can also read removed entity data dynamically via EntityIndex.Component<T>(). This is useful when the set of components you need isn't known at compile time:
World.Events
.EntitiesWithTags<MyTag>()
.OnRemoved((group, indices) =>
{
for (int i = indices.Start; i < indices.End; i++)
{
var entityIndex = new EntityIndex(i, group);
var health = entityIndex.Component<Health>(World).Read;
// ...
}
});
Note that EntityHandle.Exists() returns false for removed entities during the callback — it reflects liveness, not data accessibility. Guard cross-entity cleanup with Exists() as usual to avoid operating on already-removed entities.
Priorities¶
Call WithPriority(int) before OnAdded / OnRemoved / OnMoved to control firing order across observers on the same scope (higher = later; default 0):
Disposing subscriptions¶
Subscription objects implement IDisposable. Dispose to unregister the handler.
var sub = World.Events
.EntitiesWithTags<GameTags.Bullet>()
.OnRemoved(OnBulletRemoved);
// ...
sub.Dispose();
The DisposeCollection used in the examples above is a small helper defined in the samples — Trecs core doesn't ship it. A List<IDisposable> walked in Dispose() works just as well.
Cascading structural changes from callbacks¶
A callback can itself queue structural changes — e.g. an OnRemoved handler that removes a follower, or an OnAdded handler that spawns a child. Trecs keeps processing the queue until empty or until WorldSettings.MaxSubmissionIterations (default 10) is reached. Hitting the cap throws "possible circular submission detected" in debug/editor builds.
Frame events¶
Separate from the per-entity events, World.Events exposes lifecycle hooks for the simulation loop and for snapshot / recording loads. The trigger times below align with the per-frame phase diagram:
| Event | Fires when |
|---|---|
OnVariableUpdateStarted |
At the start of every World.Tick(), after VariableDeltaTime has been updated. |
OnFixedUpdateStarted |
At the start of each fixed-update step (zero or more times per Tick(), depending on catch-up). |
OnInputsApplied |
Inside each fixed step, after queued AddInput<T> values have been written onto their target entities (typically the global entity, but any entity is valid). |
OnSubmissionStarted |
Submission is about to run. Fires at the start of every Submit() call — at the end of each fixed step, at the end of World.LateTick(), and on any manual World.Submit(). |
OnSubmissionCompleted |
Submission finished — all queued structural changes applied. Only fires when at least one structural change was processed. |
OnFixedUpdateCompleted |
At the end of each fixed-update step. |
OnVariableUpdateCompleted |
At the end of every World.LateTick(), after the final submission for the frame. |
OnDeserializeStarted |
A snapshot or recording is about to load into the world. |
OnDeserializeCompleted |
A snapshot or recording has finished loading. |
OnFixedPauseChanged |
WorldAccessor.FixedIsPaused just toggled. Callback receives the new value (Action<bool>). |
OnShutdown |
During World.Dispose(), after RemoveAllEntities and system OnShutdown hooks have run but before infrastructure teardown. Use this to dispose event subscriptions from non-system code. |
Each takes an Action (or Action, int priority for ordering) and returns IDisposable. Dispose to unsubscribe.