07 — Feeding Frenzy¶
A multi-system simulation with job-based processing. Fish hunt meals, grow when they eat, and shrink from starvation.
Source: com.trecs.core/Samples~/Tutorials/07_FeedingFrenzy/
What it does¶
Fish swim toward meals. On contact a fish consumes the meal, grows, and looks for the next one. Fish shrink from starvation — too small and they die. Up/down arrows adjust population.
Schema¶
Components¶
[Unwrap] public partial struct SimPosition : IEntityComponent { public float3 Value; }
[Unwrap] public partial struct SimRotation : IEntityComponent { public quaternion Value; }
[Unwrap] public partial struct TargetMeal : IEntityComponent { public EntityHandle Value; }
[Unwrap] public partial struct ApproachingFish : IEntityComponent { public EntityHandle Value; }
[Unwrap] public partial struct DestinationPosition : IEntityComponent { public float3 Value; }
[Unwrap] public partial struct MealNutrition : IEntityComponent { public float Value; }
Plus Velocity and Speed defined locally, and Position, Rotation, UniformScale, ColorComponent from Common.
Templates with partitions¶
public partial class FishEntity
: ITemplate,
IExtends<CommonTemplates.IndirectRenderable>,
ITagged<FrenzyTags.Fish>,
IPartitionedBy<FrenzyTags.NotEating, FrenzyTags.Eating>
{
// Position/Rotation are render-only — VisualSmoothingSystem chases
// SimPosition/SimRotation each variable frame.
[VariableUpdateOnly]
Position Position;
[VariableUpdateOnly]
Rotation Rotation = new(quaternion.identity);
SimPosition SimPosition = default;
SimRotation SimRotation = new() { Value = quaternion.identity };
Velocity Velocity = default;
Speed Speed;
TargetMeal TargetMeal = default;
DestinationPosition DestinationPosition = default;
}
Fish have two partitions: NotEating (idle, bobbing) and Eating (moving toward a meal). IndirectRenderable is the GPU-instanced base (no per-entity GameObject); [VariableUpdateOnly] keeps the fixed phase from touching the smoothed display values — see Accessor Roles.
Key systems¶
LookingForMealSystem¶
Pairs idle fish with available meals via nested aspect queries. Sets velocity toward the target and moves both fish and meal into the Eating partition.
ConsumingMealSystem ([WrapAsJob])¶
Checks whether eating fish have reached their meal. On contact: removes the meal, grows the fish, moves the fish back to NotEating.
Shows accessing a different entity type inside a [WrapAsJob] method — [FromWorld] on mealFactory lets the job look up meal data by EntityHandle:
[ForEachEntity(typeof(FrenzyTags.Fish), typeof(FrenzyTags.Eating))]
[WrapAsJob]
static void Execute(
in ConsumingFish fish,
in NativeWorldAccessor world,
[FromWorld(typeof(FrenzyTags.Meal))]
in MealNutritionView.NativeFactory mealFactory
)
{
if (math.lengthsq(fish.DestinationPosition - fish.SimPosition) >= EatDistanceSqr)
return;
var meal = mealFactory.Create(fish.TargetMeal.ToIndex(world));
fish.UniformScale = fish.UniformScale + 0.05f * meal.MealNutrition;
meal.ApproachingFish = EntityHandle.Null;
meal.Remove(world);
fish.TargetMeal = EntityHandle.Null;
fish.SetTag<FrenzyTags.NotEating>(world);
}
MovementSystem ([WrapAsJob])¶
Moves eating fish toward their destination position.
IdleBobSystem ([WrapAsJob])¶
Applies sinusoidal bobbing to idle fish. The per-iteration EntityHandle gives each fish a stable phase offset so they bob out of sync — Id survives recycling, unlike a buffer index:
[ForEachEntity(typeof(FrenzyTags.Fish), typeof(FrenzyTags.NotEating))]
[WrapAsJob]
static void Execute(in Fish fish, EntityHandle handle, in NativeWorldAccessor world)
{
float phaseOffset = handle.Id * GoldenRatio;
float y = 0.3f * fish.UniformScale * math.sin(3f * world.ElapsedTime + phaseOffset);
var pos = fish.SimPosition;
pos.y = y;
fish.SimPosition = pos;
}
StarvationSystem ([WrapAsJob])¶
Shrinks all fish over time, removes those too small, and colors them by current size.
Shows [PassThroughArgument] for passing configuration into a job, and entity removal from a parallel job via NativeWorldAccessor:
[ForEachEntity(typeof(FrenzyTags.Fish))]
[WrapAsJob]
static void ExecuteImpl(
ref UniformScale scale,
ref ColorComponent color,
EntityHandle handle,
in NativeWorldAccessor world,
[PassThroughArgument] Settings settings
)
{
scale.Value -= settings.ShrinkRate * world.DeltaTime;
if (scale.Value <= settings.MinScale)
{
handle.Remove(world);
return;
}
// Color indicates starvation: cyan (healthy) → red-orange (starving)
float healthRaw = (scale.Value - settings.MinScale) / (settings.HealthyScale - settings.MinScale);
float health = math.saturate(healthRaw / settings.HealthyColorThreshold);
color.Value = Color.Lerp(settings.StarvingColor, settings.HealthyColor, health);
}
VisualSmoothingSystem ([ExecuteIn(SystemPhase.Presentation)])¶
Each visual frame, lerps Position/Rotation toward SimPosition/SimRotation. Movement stays smooth at the display rate even though the simulation runs at a lower fixed timestep:
[ExecuteIn(SystemPhase.Presentation)]
public partial class VisualSmoothingSystem : ISystem
{
[ForEachEntity(typeof(FrenzyTags.Fish))]
[WrapAsJob]
static void Execute(in Fish fish, in NativeWorldAccessor world)
{
float t = math.saturate(world.DeltaTime * ChaseSpeed);
fish.Position = math.lerp(fish.Position, fish.SimPosition, t);
fish.Rotation = math.slerp(fish.Rotation, fish.SimRotation, t);
}
partial struct Fish : IAspect, IRead<SimPosition, SimRotation>, IWrite<Position, Rotation> { }
}
RemoveCleanupHandler¶
Bidirectional cleanup — removing a fish also removes its target meal, and vice versa, preventing orphans:
public partial class RemoveCleanupHandler : IDisposable
{
readonly DisposeCollection _disposables = new(); // sample helper — supply your own IDisposable container
public RemoveCleanupHandler(World world)
{
World = world.CreateAccessor(AccessorRole.Fixed);
World.Events.EntitiesWithTags<FrenzyTags.Fish>()
.OnRemoved(OnFishRemoved)
.AddTo(_disposables);
World.Events.EntitiesWithTags<FrenzyTags.Meal>()
.OnRemoved(OnMealRemoved)
.AddTo(_disposables);
}
WorldAccessor World { get; }
[ForEachEntity]
void OnFishRemoved(in TargetMeal targetMeal)
{
if (targetMeal.Value.Exists(World))
targetMeal.Value.Remove(World);
}
[ForEachEntity]
void OnMealRemoved(in ApproachingFish fish)
{
if (fish.Value.Exists(World))
fish.Value.Remove(World);
}
public void Dispose() => _disposables.Dispose();
}
Architecture pattern: SimPosition vs Position¶
The simulation writes to SimPosition (the "true" position at fixed rate). A variable-update system lerps Position toward SimPosition each display frame:
Fixed Update: SimPosition jumps to new position
Variable Update: Position = lerp(Position, SimPosition, smoothFactor)
Rendering: Reads Position for smooth visual movement
An alternative to the formal interpolation system. The longer interpolation interval lets fish rotate smoothly to new directions.
Concepts introduced¶
- Native aspect factories —
MealNutritionView.NativeFactorylets a job look up another entity's components by handle inside Burst. See Advanced Job Features and Aspects. - Multi-system simulation with many interacting systems.
- Entity population management — dynamically adjusting fish/meal counts via
[WrapAsJob]andMaxFishChangePerFramethrottling. - Bidirectional references with cleanup handlers — see Entity Events.
- Partition transitions between NotEating and Eating — see Partitions.
- Generic tags —
NotEatingandEatingrepresent dynamic states reused across templates. - Visual smoothing — separating simulation position (
SimPosition, fixed) from render position (Position, variable). For the formal alternative, see Interpolation and Sample 09. [VariableUpdateOnly]components — see Accessor Roles.