Skip to content

12 — Feeding Frenzy Benchmark

A performance benchmark that implements the same gameplay using three different ECS architectures and nine iteration styles. Useful for understanding performance trade-offs.

Source: Samples/12_FeedingFrenzyBenchmark/

What It Does

The same fish-eating-meals simulation as 07 — Feeding Frenzy, but implemented three different ways. You can switch approaches and iteration styles at runtime to compare performance with up to 200,000+ entities.

Three Approaches

Branching

Eating/NotEating tracked via component values. Systems iterate all entities and branch on value:

// All fish iterated, check if eating
if (fish.TargetMeal.IsNull)
{
    // Idle behavior
}
else
{
    // Eating behavior
}

Trade-off: Simple code, but iterates entities that don't need processing.

Sets

Uses IEntitySet for membership tracking. Systems filter by set:

[ForEachEntity(Tag = typeof(FrenzyTags.Fish), Set = typeof(FrenzySets.NotEating))]
void IdleBob(in Fish fish) { ... }

Trade-off: Only visits relevant entities. No group changes when membership changes. Sparse iteration.

Partitions

Uses IHasPartition for group separation:

[ForEachEntity(Tags = new[] { typeof(FrenzyTags.Fish), typeof(FrenzyTags.NotEating) })]
void IdleBob(in Fish fish) { ... }

Trade-off: Dense, cache-friendly iteration. But partition changes copy component data between groups.

Nine Iteration Styles

Each approach is tested with multiple iteration patterns:

Style Description
ForEachMethodAspect [ForEachEntity] with aspect parameter
ForEachMethodComponents [ForEachEntity] with component ref parameters
AspectQuery Manual Aspect.Query(World).WithTags<T>() loop
QueryGroupSlices World.Query().GroupSlices() with direct buffer access
RawComponentBuffersJob Manual Burst job with NativeComponentBufferRead/Write
ForEachMethodAspectJob [ForEachEntity] aspect with [WrapAsJob]
ForEachMethodComponentsJob [ForEachEntity] components with [WrapAsJob]
WrapAsJobAspect Static [WrapAsJob] method with aspect
WrapAsJobComponents Static [WrapAsJob] method with components

Runtime Controls

Key Action
F1 / F2 / F3 Switch to Branching / Sets / Partitions
Tab / Shift+Tab Cycle iteration style
Up / Down Adjust entity count preset

The display shows real-time performance stats: simulation Hz, FPS, entity count, and memory usage.

Fish Count Presets

Logarithmically spaced from 5,000 to 1,000,000 entities for testing at different scales.

Key Implementation Details

Population Management

Fish and meal counts smoothly lerp toward the desired preset. The spawning system prioritizes removing idle entities first to minimize disruption.

GPU Rendering

At high entity counts, the RendererSystem uses GPU-instanced indirect rendering. A Burst job marshals Position, Rotation, Scale, and Color from ECS components to GPU instance buffers each frame.

Bidirectional Entity References

Fish ↔ Meal pairing uses EntityHandle cross-references with cleanup handlers to prevent dangling references when either entity is removed.

What to Learn from This Sample

  • Partitions are fastest for dense iteration — entities are contiguous in memory
  • Sets avoid group explosion — no combinatorial blowup with multiple dimensions
  • Branching is simplest but slowest — iterates everything, branches per entity
  • Jobs provide the biggest speedup — Burst + parallel iteration scales with core count
  • Source generation produces efficient code[ForEachEntity] and [WrapAsJob] generate tight loops comparable to hand-written jobs