Boids inside Unity(Github)
Project description
This weekend I experimented with boids in Unity. By following these three simple rules:
- Cohesion: Attraction to the average position of their group.
- Separation: Repelling from agents that are too close.
- Alignment: Attraction to the average heading of their group.
The class Fish shows how easy this behavior is to implement.
using System.Linq;
using UnityEngine;
public class Fish : MonoBehaviour
{
[SerializeField] private float speed;
[SerializeField] private float targetMinDistance;
[SerializeField] private float maxTurnAroundSpeed;
[SerializeField] private float obstacleTurnSpeed;
[SerializeField] private float targetTurnSpeed;
[SerializeField] private float alignmentTurnSpeed;
[SerializeField] private float cohesionTurnSpeed;
[SerializeField] private float separationTurnSpeed;
[SerializeField] private float minimumDistance;
[SerializeField] private float holdDistance;
private GameObject _target;
private Segment _currentSegment;
private Fish _closestFish;
private void Start()
{
_target = GameObject.FindGameObjectWithTag("Target");
}
private void Update()
{
CheckSegment();
GetClosestFish();
CalculateDirection();
MoveFish();
}
private void MoveFish()
{
transform.position += transform.forward * (speed * Time.deltaTime);
}
private void CalculateDirection()
{
if ((_target.transform.position - transform.position).sqrMagnitude > targetMinDistance)
{
var targetRotation = Quaternion.LookRotation(_target.transform.position - transform.position, Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, targetTurnSpeed * Time.deltaTime);
}
else
{
var targetRotation = Quaternion.LookRotation(transform.position - _target.transform.position, Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, maxTurnAroundSpeed * Time.deltaTime);
}
if (_closestFish == null) return;
var alignmentRotation = _closestFish.transform.rotation;
transform.rotation = Quaternion.RotateTowards(transform.rotation, alignmentRotation, alignmentTurnSpeed * Time.deltaTime);
if ((_closestFish.transform.position - transform.position).sqrMagnitude > holdDistance)
{
var cohesionRotation = Quaternion.LookRotation(_closestFish.transform.position - transform.position, Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, cohesionRotation, cohesionTurnSpeed * Time.deltaTime);
}
if ((_closestFish.transform.position - transform.position).sqrMagnitude < minimumDistance)
{
var separationRotation = Quaternion.LookRotation(transform.position - _closestFish.transform.position, Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, separationRotation, separationTurnSpeed * Time.deltaTime);
}
}
private void GetClosestFish()
{
_closestFish = _currentSegment.GetClosestFish(this);
}
private void CheckSegment()
{
var newSegment = WorldSegmentation.segments.OrderBy(segment => (segment.transform.position - transform.position).sqrMagnitude).First();
if (newSegment == _currentSegment) return;
if (_currentSegment != null) _currentSegment.RemoveFish(this);
newSegment.AddFish(this);
_currentSegment = newSegment;
}
private void OnDrawGizmos()
{
if (_closestFish == null) return;
Gizmos.color = Color.blue;
Gizmos.DrawLine(transform.position, _closestFish.transform.position);
}
private void OnTriggerStay(Collider other)
{
if (other.CompareTag("Obstacle"))
{
var obstacleRotation = Quaternion.LookRotation(transform.position - other.transform.position, Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, obstacleRotation, obstacleTurnSpeed * Time.deltaTime);
}
if (other.CompareTag("Wall"))
{
var homeRotation = Quaternion.LookRotation(Vector3.zero - other.transform.position, Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, homeRotation, maxTurnAroundSpeed);
}
}
}
To optimize the implementation and make it possible to have more fishes I implemented a segmentation system where the world gets divided into segments. This makes it possible to request the closest fish in a segment instead of looping through all spawned fishes.
The WorldSegmentation divides the world into segments.
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(WorldGeneration))]
public class WorldSegmentation : MonoBehaviour
{
[SerializeField] private WorldGeneration worldGeneration;
[Range(0.001f,1f)] [SerializeField] private float segmentDivider;
[SerializeField] private GameObject segmentPrefab;
public static List segments = new List();
private void Start()
{
GenerateSegments();
}
private void GenerateSegments()
{
var size = segmentDivider * worldGeneration.GetSize();
for (var level = 0; level < worldGeneration.GetSize() / size; level++)
{
for (var col = 0; col < worldGeneration.GetSize() / size; col++)
{
for (var row = 0; row < worldGeneration.GetSize() / size; row++)
{
var segment = Instantiate(segmentPrefab, new Vector3(
-worldGeneration.GetSize() / 2 + size / 2 + size * row,
-worldGeneration.GetSize() / 2 + size / 2 + size * level,
-worldGeneration.GetSize() / 2 + size / 2 + size * col),
Quaternion.identity).GetComponent();
segment.SetSize(Vector3.one * size);
segments.Add(segment);
}
}
}
}
}
Here is the segment class where each fish can request the closest fish in that segment.
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class Segment : MonoBehaviour
{
private readonly List _fish = new List();
public void AddFish(Fish fish)
{
_fish.Add(fish);
}
public void RemoveFish(Fish fish)
{
_fish.Remove(fish);
}
public void SetSize(Vector3 size)
{
transform.localScale = size;
}
private void OnDrawGizmos()
{
Gizmos.color = _fish.Count > 0 ? new Color(0, 1, 0, 0.2f) : new Color(1, 0, 0, 0.01f);
Gizmos.DrawCube(transform.position, transform.localScale);
}
public Fish GetClosestFish(Fish searchingFish)
{
var newColl = _fish.Where(fish => fish != searchingFish);
return !newColl.Any() ? null : newColl.OrderBy(fish => (searchingFish.transform.position - fish.transform.position).sqrMagnitude).First();
}
}
I learned by implementing this that advanced behavior is not always advanced to implement and, in some cases, straightforward to deal with. This project was not complicated or time-consuming, and by that, It has opened up doors to other systems I earlier was afraid to jump on.