R&D · UE5 · Simulation · Spatial Partitioning

Boids & Flocking

Boids and Flocking

A full C++ implementation of Craig Reynolds' Boids flocking algorithm in Unreal Engine 5, built around a custom Octree spatial partitioner (FBoidSpatialNode) that cuts neighbour queries from O(n²) to O(log n). Scales to 15–18k agents at 60 FPS via a data/ISM mode alongside the full Actor-based mode.

UE5 C++ Custom Octree O(log n) Queries ISM / Actor Modes Influence Volumes
What I Built
  • Custom Octree — O(n²) → O(log n) (FBoidSpatialNode) — rebuilt every tick; sphere-intersection traversal eliminates brute-force O(n²) comparisons, sustaining 15–18k agents at 60 FPS.
  • UFlockSubsystem — 1 query API — world subsystem owns the Octree and all boid data; exposes 1 unified GetNeighbors(center, radius) API as the single lookup point for all boids.
  • 2 simulation modes — Actor-based (full skeletal mesh fidelity) and Data/ISM-based (massive scale); 2 modes switchable at runtime with no architectural change.
  • Influence volumes — 2 radii, 3 modes (ABoidRing) — 2 radii (inner/outer) with 3 behavioral modes (pass-through, swirl, avoidance) plus optional launch boost.
  • 3 per-rule runtime toggles — separation, alignment, cohesion each independently switchable; 3 boolean flags plus an external force injection API for gameplay interactions.

Flocking — Octree Neighbour Query

Each boid queries UFlockSubsystem::GetNeighbors() instead of iterating all agents. The three steering rules each clamp their result to MaxAlignForce; all three are independently togglable at runtime.

Boid.cpp — Flock() + Separation / Cohesion / Align
void ABoid::Flock()
{
    if (!FlockSubsystem) return;

    if (bPendingLaunch)
    {
        const float Speed = PendingLaunchVelocity.Size();
        if (Speed > 0.f)
        {
            MaxAlignForce = FMath::Max(MaxAlignForce, Speed);
            BoidVelocity  = PendingLaunchVelocity;
        }
        ExternalForce = BoidAcceleration = FVector::ZeroVector;
        bPendingLaunch = false;
        return;
    }

    BoidAcceleration = ExternalForce;
    ExternalForce    = FVector::ZeroVector;

    // O(log n) Octree query — only boids within PerceptionRadius
    TArray<ABoid*> Neighbors;
    FlockSubsystem->GetNeighbors(this, PerceptionRadius, Neighbors);

    if (!bDisableSeparation) BoidAcceleration += Separation(Neighbors);
    if (!bDisableAlign)      BoidAcceleration += Align    (Neighbors);
    if (!bDisableCohesion)   BoidAcceleration += Cohesion (Neighbors);
}

// Separation — steer away from crowded neighbours (inverse-distance weight)
FVector ABoid::Separation(const TArray<ABoid*>& Boids)
{
    FVector Force = FVector::ZeroVector;
    for (ABoid* Other : Boids)
    {
        FVector Diff = GetActorLocation() - Other->GetActorLocation();
        float   Dist = Diff.Size();
        if (Dist > 0.f && Dist < SeparationRadius)
            Force += Diff.GetSafeNormal() / Dist;
    }
    return Force.GetClampedToMaxSize(MaxAlignForce);
}

// Cohesion — steer toward average neighbour position
FVector ABoid::Cohesion(const TArray<ABoid*>& Boids)
{
    if (Boids.IsEmpty()) return FVector::ZeroVector;
    FVector Center = FVector::ZeroVector;
    for (ABoid* B : Boids) Center += B->GetActorLocation();
    Center /= Boids.Num();
    return (Center - GetActorLocation()).GetClampedToMaxSize(MaxAlignForce);
}

// Alignment — match average neighbour velocity
FVector ABoid::Align(const TArray<ABoid*>& Boids)
{
    if (Boids.IsEmpty()) return FVector::ZeroVector;
    FVector AvgVel = FVector::ZeroVector;
    for (ABoid* B : Boids) AvgVel += B->GetBoidVelocity();
    AvgVel /= Boids.Num();
    return AvgVel.GetClampedToMaxSize(MaxAlignForce);
}
Engine Unreal Engine 5 Language C++ Spatial structure Custom Octree — O(log n) queries Scale 15–18k agents · 60 FPS Render modes Actor-based · Data/ISM Category R&D · AI · Simulation Source github.com/khaled71612000 ↗
Connect