04 — Predator Prey¶
Cross-entity references via EntityHandle, with cleanup handlers to prevent dangling references.
Source: com.trecs.core/Samples~/Tutorials/04_PredatorPrey/
What it does¶
Predators (red) chase the nearest prey (cyan). On contact, the prey is removed and a new one spawns. Predators re-target the nearest available prey.
Schema¶
Components¶
[Unwrap]
public partial struct ChosenPrey : IEntityComponent
{
public EntityHandle Value; // Handle to the prey being chased
}
[Unwrap]
public partial struct ApproachingPredator : IEntityComponent
{
public EntityHandle Value; // Handle to the predator chasing this prey
}
Plus Speed and MoveDirection from this sample, and Position from Common/.
Tags & templates¶
Template inheritance shares common movement components. Movable is abstract — it exists only as an IExtends base, so registering it directly would trip TRECS039. Concrete templates extend it and set their own PrefabId:
public abstract partial class Movable
: ITemplate,
IExtends<CommonTemplates.RenderableGameObject>,
ITagged<SampleTags.Movable>
{
Position Position = default;
MoveDirection MoveDirection = default;
Speed Speed;
}
public partial class PredatorEntity
: ITemplate,
ITagged<SampleTags.Predator>,
IExtends<Movable>
{
ChosenPrey ChosenPrey = default;
PrefabId PrefabId = new(PredatorPreyPrefabs.Predator);
}
public partial class PreyEntity
: ITemplate,
ITagged<SampleTags.Prey>,
IExtends<Movable>
{
ApproachingPredator ApproachingPredator = default;
PrefabId PrefabId = new(PredatorPreyPrefabs.Prey);
}
Systems¶
PredatorChoosePreySystem¶
Finds the nearest unassigned prey for each predator using nested aspect queries:
foreach (var predator in Predator.Query(World).WithTags<SampleTags.Predator>())
{
if (!predator.ChosenPrey.IsNull) continue; // Already has a target
float nearestDistSq = float.MaxValue;
Prey chosenPrey = default;
bool found = false;
foreach (var prey in Prey.Query(World).WithTags<SampleTags.Prey>())
{
if (!prey.ApproachingPredator.IsNull) continue; // Already claimed
float distSq = math.distancesq(predator.Position, prey.Position);
if (distSq < nearestDistSq)
{
nearestDistSq = distSq;
chosenPrey = prey;
found = true;
}
}
if (found)
{
// Link predator ↔ prey via aspects
chosenPrey.ApproachingPredator = predator.Handle(World);
predator.ChosenPrey = chosenPrey.Handle(World);
}
}
partial struct Predator : IAspect, IRead<Position>, IWrite<ChosenPrey> { }
partial struct Prey : IAspect, IRead<Position>, IWrite<ApproachingPredator> { }
PredatorChaseSystem¶
Steers predators toward their target prey and removes prey on contact.
PreyRespawnSystem¶
Maintains the prey population by spawning replacements.
GameObject cleanup¶
When prey are removed, the shared RenderableGameObjectManager (from Common/) automatically tears down the companion GameObject. It subscribes once to OnAdded / OnRemoved for every entity carrying PrefabId + GameObjectId, so concrete samples don't write their own cleanup observer — see Sample 15 — Reactive Events for the same OnRemoved pattern in user code, and Entity Events for the API. Events are preferred over inline destruction in the lifetime system because:
- Consistency — entity removal is deferred, so the entity still exists until submission. Without an event, later systems could read stale data on an about-to-be-removed entity.
- Centralized cleanup — if entities can be removed from multiple places (caught, starved, despawned), the same handler runs regardless of source.
Concepts introduced¶
EntityHandle— stable cross-entity references that survive structural changes. See Entities.- Template inheritance via
IExtends<T>to share common components. See Templates. - Nested aspect queries — iterate one template while querying another. See Queries & Iteration.
- Entity events (
OnRemoved) for cross-reference cleanup. See Entity Events. - Bidirectional linking — predator points to prey and prey points back.