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.

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