
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.
FBoidSpatialNode) — rebuilt every tick; sphere-intersection traversal eliminates brute-force O(n²) comparisons, sustaining 15–18k agents at 60 FPS.GetNeighbors(center, radius) API as the single lookup point for all boids.ABoidRing) — 2 radii (inner/outer) with 3 behavioral modes (pass-through, swirl, avoidance) plus optional launch boost.
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.
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);
}