Fish Tank

FISH TANK

What is this?

This is a virtual aquarium where different flocks of fishes can have their own behaviour. Each fish can avoid obstacles and flee from any danger elements. This small project has no art (except a gorgeous shark) since it was made by me solely and I wanted to concentrate on the functions rather than the visuals.

  • Engine: Unity
  • Language: C#
  • Platform: PC
  • Development Time: 2 days
  • Team Size: Solo

Crowd Simulation

I used Reynolds flocking concept to create the fishes behavior. The boids algorithm has tree rules: 

Separation

Avoid crowding another group member.

 

To avoid neighbors, each individual fish had to be aware of its neighbors and the turn them away.

Alignment

Align with the average heading of the group.

 

To do this I had to add all the headings of each fishes and then divide it by the amount of fishes in the group

Cohesion

Move the fishes towards the average position of the group.

 

I took all the individual position from all the flock object, added them together to then divide the number by the amount of fishes in the group.

Flock Manager

The flock manager takes care of what the behaviours a specific type of fish should have in its flock. To make it easy to change the behaviours, they are all values that can be changeable in the inspector of the game.

Flock

Each fish that is spawned by the flock manager has a scripted called flock. This script does the calculations of the flocking behaviours.

 



void ApplyRules()
    {
        GameObject[] fishObjects;
        fishObjects = _manager.fishes;
        
        Vector3 averageCenter = Vector3.zero;
        Vector3 averageAvoidance = Vector3.zero; 
        float averageSpeed = 0.0f;
        float averageNeighbourDistance;
        float groupSize = 0;
                
        for(int i = 0; i < fishObjects.Length; i++)
        {
            if(fishObjects[i] != gameObject)
            {
                averageNeighbourDistance = Vector3.Distance(fishObjects[i].transform.position, transform.position);
                if (averageNeighbourDistance <= _manager._neighbourDistance)
                {
                    averageCenter += fishObjects[i].transform.position;
                    groupSize++;

                    // Value = how close we are allowd to be to another fish (before we should avoid it)
                    if (averageNeighbourDistance < 1.0f) 
                        averageAvoidance += (transform.position - fishObjects[i].transform.position);

                    Flock anotherFlock = fishObjects[i].GetComponent< Flock>();
                    averageSpeed += anotherFlock._speed;
                }
            }
        }

        if (groupSize > 0)
        {
            // After we calculated the average of the center position, we'll add a vector to the goal position
            averageCenter = averageCenter / groupSize + (_manager.goalPosition - transform.position); 
            _speed = averageSpeed / groupSize; 
            
            Vector3 direction = (averageCenter + averageAvoidance) - transform.position; 
            
            if (direction != Vector3.zero)
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), _manager._rotationSpeed * Time.deltaTime);
        }
    }

    

Obstacle

Having a fish tank with no obstacle isn’t fun. I added a way for the fishes to avoid obstacles by first adding a bounding box around the aquarium, to avoid making the fishes escape. I then added the same principle to the obstacles.

 

To make a fish turn to avoid an obstacle in a nice way, I had to get a new direction for the fish. Instead of having it turn 90 degrees, I calculated the reflection of the direction the fish had towards the obstacle. By doing this I got a new vector I then could Slerp the fish towards.

 

We know that the angle of incidence A is equal to the reflections angle B. Therefor I can use this to calculate a new direction for our fishes. 



 void CheckForObstacle()
    {
        Bounds bounds = new Bounds(_manager.transform.position, _manager.swimLimit * 2);
        RaycastHit hit = new RaycastHit();
               
        if (!bounds.Contains(transform.position))
        {
            _turning = true;
            _direction = _manager.transform.position - transform.position;
        }
              
        else if (Physics.Raycast(transform.position, transform.forward * _manager._obstacleReaction * Time.deltaTime, out hit))
        {
            _turning = true;
            _direction = Vector3.Reflect(transform.forward, hit.normal);
        }
        else
            _turning = false;


        if (_turning)
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(_direction), _manager._rotationSpeed * Time.deltaTime);
        else
        {
            if (Random.Range(0, 100) < 10)
                _speed = Random.Range(_manager.minSpeed, _manager.maxSpeed);

            if (Random.Range(0, 100) < 20)
                ApplyRules();
        }
    }

    

Danger

I wanted to play around with some danger elements. If a shark would appear, I wanted to make the fishes panic and change direction in a faster motion then they do with a regular obstacle.

 

Instead of using reflection to make the fishes turn direction smoothly, I wanted to make them turn 90 degrees. To give them the panic effect I multiplied their speed each time they saw the shark.



    void CheckForDanger()
    {
        RaycastHit hit = new RaycastHit();

        if (Physics.Raycast(transform.position, transform.forward * _manager._dangerReaction * Time.deltaTime, out hit))
        {
            if (hit.transform.CompareTag("Shark"))
            {
                _seeDanger = true;
                _direction = transform.forward * -1;
                transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(_direction), _manager._rotationSpeed * 10 * Time.deltaTime);
            }
            else
                _seeDanger = false;
        }
        
        if (_seeDanger)
            _speed = Random.Range(_manager.minSpeed, _manager.maxSpeed) * 2;
        else
            _speed = Random.Range(_manager.minSpeed, _manager.maxSpeed);
    }

    

The Process

My goal was to create a type of AI behavior I never done before. There was a time I wanted to buy an aquarium with fishes, and I spend a bit too much time looking at the fishes swimming around during my free time. I got intrigued by the way they moved, and this inspired me to create this project.

Challenges

My main issue was performance. In the beginning I couldn’t have enough fishes spawning at the same time. As soon as I had more then 100 fishes the FPS dropped to 20. I did manage to fix it by optimizing the code. For instance, I used a for each-loop instead of a regular for-loop because I didn’t realize it’s an enumerator.

 

After fixing some code I checked the profiler and saw that I had a lot of debugging happening without my knowledge; and this was the second reason why my performance was slow. I realized I forgot to connect some values of my animator. It was a small mistake that made a huge difference. I wouldn’t have managed to find it so quickly if I hadn’t checked the profiler. After fixing that bug my program ran smoothly.

 

Another challenge I had was making the fishes avoid obstacles smoothly. I had trouble deciding how to create the detections without causing the performance to drop too much. My original plan was to create a bounding box on them to be able to detect obstacles easier. Doing that made my performance drop a lot! I then decided to only crate a ray from the direction of the fishes, since I wanted them to avoid the obstacles in front of them. This solution solved the performance issue and still looked good in run time.

 

The only thing this solution affected was that the fishes couldn’t always sport every obstacle in time. This meant they sometimes turned away from the obstacles too late and ended up going throw the objects corner a bit.