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.