Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Created August 2, 2026 13:43
Show Gist options
  • Select an option

  • Save adammyhre/73082cf86dba7d136d9ee4d72009472b to your computer and use it in GitHub Desktop.

Select an option

Save adammyhre/73082cf86dba7d136d9ee4d72009472b to your computer and use it in GitHub Desktop.
Simple Boids in Unity
using System.Collections.Generic;
using UnityEngine;
using UnityUtils;
[DisallowMultipleComponent]
public class BoidAgent : MonoBehaviour {
#region Fields
[Header("Movement")]
[SerializeField, Min(0.1f)] float maxSpeed = 7f;
[SerializeField, Min(0.1f)] float accelerationForce = 18f;
[SerializeField, Min(0.1f)] float rotationSharpness = 10f;
[Header("Behavior Distances")]
[SerializeField, Min(0.1f)] float separationDistance = 1.5f;
[SerializeField, Min(0.1f)] float alignmentDistance = 3.5f;
[SerializeField, Min(0.1f)] float cohesionDistance = 4.5f;
[Header("Behavior Weights")]
[SerializeField, Min(0f)] float separationWeight = 1.6f;
[SerializeField, Min(0f)] float alignmentWeight = 1f;
[SerializeField, Min(0f)] float cohesionWeight = 1.2f;
[SerializeField, Min(0f)] float boundsWeight = 2.5f;
[SerializeField, Min(0f)] float obstacleWeight = 3f;
[Header("Neighbor Query")]
[SerializeField] LayerMask neighborMask = ~0;
[SerializeField, Min(8)] int maxNeighborColliders = 128;
[Header("Obstacle Avoidance")]
[SerializeField] bool avoidObstacles = true;
[SerializeField] LayerMask obstacleMask = ~0;
[SerializeField, Min(0.1f)] float obstacleProbeRadius = 0.45f;
[SerializeField, Min(0.5f)] float obstacleLookAhead = 3.5f;
[SerializeField, Min(0.1f)] float floorClearance = 0.8f;
[Header("Vertical Constraint")]
[SerializeField] bool constrainY;
[SerializeField] float constrainedY = 4f;
Rigidbody rb;
readonly List<BoidAgent> neighbors = new(32);
Collider[] neighborHits;
Vector3 boundsCenter;
float boundsRadius = 25f;
bool useBounds = true;
float separationDistanceSqr;
float alignmentDistanceSqr;
float cohesionDistanceSqr;
float neighborScanRadius;
#endregion
public Vector3 Velocity => rb ? rb.linearVelocity : Vector3.zero;
protected void Awake() {
rb = gameObject.GetOrAdd<Rigidbody>();
rb.useGravity = false;
rb.constraints = RigidbodyConstraints.FreezeRotation;
rb.interpolation = RigidbodyInterpolation.Interpolate;
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
rb.linearDamping = 0.2f;
var bufferSize = Mathf.Max(8, maxNeighborColliders);
neighborHits = new Collider[bufferSize];
separationDistanceSqr = separationDistance * separationDistance;
alignmentDistanceSqr = alignmentDistance * alignmentDistance;
cohesionDistanceSqr = cohesionDistance * cohesionDistance;
neighborScanRadius = Mathf.Max(separationDistance, Mathf.Max(alignmentDistance, cohesionDistance));
}
protected void Start() {
if (rb.linearVelocity.sqrMagnitude < 0.01f) rb.linearVelocity = Random.onUnitSphere * maxSpeed;
if (!constrainY) return;
var position = transform.position;
position.y = constrainedY;
transform.position = position;
}
public void ConfigureBounds(Vector3 center, float radius, bool enabled = true) {
boundsCenter = center;
boundsRadius = Mathf.Max(0.5f, radius);
useBounds = enabled;
}
public void ConfigureSpeed(float speed) => maxSpeed = Mathf.Max(0.1f, speed);
public void ConfigureVerticalConstraint(bool enabled, float yValue) {
constrainY = enabled;
constrainedY = yValue;
}
void FindNeighbors() {
neighbors.Clear();
var hitCount = Physics.OverlapSphereNonAlloc(
transform.position,
neighborScanRadius,
neighborHits,
neighborMask,
QueryTriggerInteraction.Collide
);
for (var i = 0; i < hitCount; i++) {
var hit = neighborHits[i];
if (!hit) continue;
var other = hit.attachedRigidbody ? hit.attachedRigidbody.GetComponent<BoidAgent>() : hit.GetComponent<BoidAgent>();
if (!other || other == this) continue;
neighbors.Add(other);
}
}
Vector3 ComputeSeparation() {
var force = Vector3.zero;
var count = 0;
var position = transform.position;
for (var i = 0; i < neighbors.Count; i++) {
var toOther = position - neighbors[i].transform.position;
var sqrDistance = toOther.sqrMagnitude;
if (sqrDistance > separationDistanceSqr || sqrDistance < 0.0001f) continue;
force += toOther / sqrDistance;
count++;
}
return count > 0 ? force / count : Vector3.zero;
}
Vector3 ComputeAlignment() {
var averageVelocity = Vector3.zero;
var count = 0;
var position = transform.position;
for (var i = 0; i < neighbors.Count; i++) {
var offset = neighbors[i].transform.position - position;
if (offset.sqrMagnitude > alignmentDistanceSqr) continue;
averageVelocity += neighbors[i].Velocity;
count++;
}
return count > 0 ? (averageVelocity / count).normalized : transform.forward;
}
Vector3 ComputeCohesion() {
var center = Vector3.zero;
var count = 0;
var position = transform.position;
for (var i = 0; i < neighbors.Count; i++) {
var otherPosition = neighbors[i].transform.position;
if ((otherPosition - position).sqrMagnitude > cohesionDistanceSqr) continue;
center += otherPosition;
count++;
}
return count > 0 ? (center / count - position).normalized : Vector3.zero;
}
Vector3 ComputeBoundsSteer() {
if (!useBounds) return Vector3.zero;
var offset = transform.position - boundsCenter;
var distance = offset.magnitude;
var innerRadius = boundsRadius * 0.85f;
if (distance <= innerRadius) return Vector3.zero;
var strength = Mathf.InverseLerp(innerRadius, boundsRadius, distance);
return (boundsCenter - transform.position).normalized * strength;
}
Vector3 ComputeObstacleAvoidance() {
if (!avoidObstacles) return Vector3.zero;
var velocity = rb.linearVelocity;
if (velocity.sqrMagnitude < 0.0001f) return Vector3.zero;
var direction = velocity.normalized;
var position = transform.position;
var avoidance = Vector3.zero;
if (Physics.SphereCast(position, obstacleProbeRadius, direction, out var forwardHit, obstacleLookAhead, obstacleMask, QueryTriggerInteraction.Ignore)) {
var awayFromHit = Vector3.Reflect(direction, forwardHit.normal).normalized;
avoidance += awayFromHit;
}
if (Physics.Raycast(position, Vector3.down, out var floorHit, floorClearance, obstacleMask, QueryTriggerInteraction.Ignore)) {
var floorStrength = Mathf.InverseLerp(floorClearance, 0f, floorHit.distance);
avoidance += Vector3.up * floorStrength;
}
return avoidance.normalized;
}
protected void FixedUpdate() {
FindNeighbors();
// This one block is the boids thesis: blend local influences and let emergence do the rest.
var steering =
ComputeSeparation() * separationWeight +
ComputeAlignment() * alignmentWeight +
ComputeCohesion() * cohesionWeight+
ComputeBoundsSteer() * boundsWeight+
ComputeObstacleAvoidance() * obstacleWeight;
if (steering.sqrMagnitude < 0.0001f) steering = transform.forward;
var acceleration = steering.normalized * accelerationForce;
var nextVelocity = rb.linearVelocity + acceleration * Time.fixedDeltaTime;
if (constrainY) nextVelocity.y = 0f;
rb.linearVelocity = Vector3.ClampMagnitude(nextVelocity, maxSpeed);
if (rb.linearVelocity.sqrMagnitude > 0.01f) {
var lookDirection = Vector3.ProjectOnPlane(rb.linearVelocity, Vector3.up);
var targetRotation = Quaternion.LookRotation(lookDirection.normalized, Vector3.up);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSharpness * Time.fixedDeltaTime);
}
}
}
using System.Collections.Generic;
using UnityEngine;
using UnityUtils;
public class BoidSpawner : MonoBehaviour {
#region Fields
[Header("Prefab")]
[SerializeField] GameObject boidPrefab;
[Header("Spawn")]
[SerializeField, Min(1)] int boidCount = 60;
[SerializeField] Vector3 spawnExtents = new(14f, 6f, 14f);
[SerializeField, Min(0.1f)] float boidScale = 0.5f;
[Header("Movement")]
[SerializeField, Min(0.1f)] float baseSpeed = 7f;
[SerializeField, Min(0f)] float speedVariance = 1.5f;
[SerializeField, Min(0.5f)] float flockBoundsRadius = 30f;
[Header("Vertical Constraint")]
[SerializeField] bool constrainY;
[SerializeField] float constrainedY = 4f;
readonly List<BoidAgent> spawnedBoids = new();
Transform runtimeRoot;
#endregion
protected void Start() => SpawnFlock();
[ContextMenu("Spawn Flock")]
public void SpawnFlock() {
ClearFlock();
EnsureRuntimeRoot();
for (var i = 0; i < boidCount; i++) {
var boid = CreateBoid(i);
spawnedBoids.Add(boid);
}
}
[ContextMenu("Clear Flock")]
public void ClearFlock() {
for (var i = spawnedBoids.Count - 1; i >= 0; i--) {
var boid = spawnedBoids[i];
if (boid) DestroyImmediate(boid.gameObject);
}
spawnedBoids.Clear();
if (!runtimeRoot) return;
DestroyImmediate(runtimeRoot.gameObject);
runtimeRoot = null;
}
void EnsureRuntimeRoot() {
if (runtimeRoot) return;
var root = new GameObject("BoidsRuntime");
root.transform.SetParent(transform, false);
runtimeRoot = root.transform;
}
BoidAgent CreateBoid(int index) {
var spawnPosition = transform.position + GetRandomSpawnOffset();
var boidObject = Instantiate(boidPrefab, spawnPosition, Quaternion.identity, runtimeRoot);
boidObject.name = $"Boid_{index:000}";
boidObject.transform.localScale = Vector3.one * boidScale;
var capsuleCollider = boidObject.GetOrAdd<CapsuleCollider>();
capsuleCollider.isTrigger = false;
var body = boidObject.GetOrAdd<Rigidbody>();
body.useGravity = false;
body.constraints = RigidbodyConstraints.FreezeRotation;
body.interpolation = RigidbodyInterpolation.Interpolate;
body.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
var boid = boidObject.GetOrAdd<BoidAgent>();
boid.ConfigureBounds(transform.position, flockBoundsRadius, true);
boid.ConfigureSpeed(baseSpeed + Random.Range(-speedVariance, speedVariance));
boid.ConfigureVerticalConstraint(constrainY, constrainedY);
return boid;
}
Vector3 GetRandomSpawnOffset() => new(
Random.Range(-spawnExtents.x, spawnExtents.x),
constrainY ? 0f : Random.Range(-spawnExtents.y, spawnExtents.y),
Random.Range(-spawnExtents.z, spawnExtents.z)
);
}
@dstew99

dstew99 commented Aug 2, 2026

Copy link
Copy Markdown

Thanks for doing this! Great help.

@intentor

Copy link
Copy Markdown

Very helpful and insightful! Thank you! :-D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment