DES311 #5: Predators

Progress continues, this week I’ve implemented the last feature on the sprint 2 backlog, predators. Furthermore I’ve been making tweaks to the flock behaviour that has lead to some interesting behaviours and have put in a spawning system that controls the number of BOIDs on screen.

Predators

How predators work is quite simple when compared to the BOIDs. After a set cooldown period a predator is spawned in a random position outside the cameras view. The fist thing the predator does is create a list of all the BOIDs in the scene:

        boids = GameObject.Find("BOID Spawner");

        foreach (Transform child in boids.transform)
        {
            prey.Add(child);
        }

This was actually the first time I had used the for each statement, which on reflection is crazy because its very handy. Next the predator averages the position of each BOID and creates a vector that intersects that position. I’ve written similar code several times already in this project, so I just copy pasted it from the cohesion script. This sends the predator after the flock and when it collides with a BOID that BOID is destroyed. A bool value keeps track of if the predator is hungry. If it has collided with a BOID its hunger is satisfied and it stops following the flock, leaving the scene to be destroyed when out of view:

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.tag == "BOID")
        {
            hungry = false;
            Destroy(collision.gameObject);
        }
    }

After the the predator is destroyed the predator spawner starts its cool down timer. When its up another predator will be spawned.

Since BOIDs are now being destroyed I’ve had to put in a spawner that adds new BOIDs to the scene when they are destroyed by a predator. I experimented with using a similar script to the predator spawner that brings new BOIDs in at random positions off camera. However this lead to a problem. This method meant that if a predator destroys multiple BOIDs then new ones appear from multiple random directions, this leads to quite a lot of chaos as the BOIDs attempt to reform a single flock again. The solution was to have new BOIDs spawned in batches from a single random position that changes periodically. This also has the added benefit of leaving the scene empty for a while if a lot of BOIDs have been destroyed. It gives the simulation the feel of an Attenborough documentary as you watch a few remaining survivors of an attack attempt to regroup and reform their flock.

The introduction of predators that could destroy BOIDs caused a lot of problems due to the BOIDs creating lists of nearby flock mates to generate their behaviours. As soon as null objects started appearing in those lists I was getting a lot of errors and crazy bugs. This was easily remedied by adding a reverse counter that goes through lists removing null objects (it goes in reverse as to not interfere with the index when objects are removed):

        //removes destroyed BOIDS
        for (int i = nearbyBodies.Count - 1; i > -1; i--)
        {
            if(nearbyBodies[i] == null)
            {
                nearbyBodies.RemoveAt(i);
            }
        }

It seems strange to admit but at this point I did get a little sad, preparing those little white triangles to deal with the death of their nearby friends. But deep down I knew this was a good thing. If I’m getting sad about the death of little triangles it means their movement and behaviours are life like enough to trick my brain into forming attachments… after all isn’t that the essence of game development?

Weird Behaviours

I thought I would take some time to talk about some patterns and behaviours I’ve been noticing. The first is how the BOID flocks tend to always turn left, leading to anticlockwise motion. At first I thought this was because the separation script that generates vectors to avoid collisions checks the left side first. So I added a function that randomises the direction that is checked first:

float directionFlip = Mathf.Sign(Random.Range(-1, 1));

direction = Vector3.Normalize(new Vector3(X, Y, -Z * directionFlip))

It’s not very clean code and has a slight bias to the right side, but that doesn’t matter, because the BOIDs still swirl in an anticlockwise motion. So far I haven’t found any other reasons in the code this may be the case. Its super weird.

Another interesting behaviour that I first thought was a bug was the seemingly random snap changes in direction. Randomly in unison the flock will flip 180 degrees and go the other way. This I don’t think is a bug, since this behaviour can be observed in real-world schools of fish. Some further research is needed but what I think is happening is the BOIDs at the front are slowing down to try and reach the centre of the school. In doing so they gradually change the average velocity of the flock, and when enough of them do this and the average velocity reach’s nearly zero a chain reaction starts as each BOID follows their neighbour whilst avoiding stopping. What looks like a complex decision made by some hive mind is really each BOID reacting to a situation in a similar way. For now I’m classing this as an emergent behaviour.

What’s Next

With the conclusion of this sprint I have completed all the main mechanic features I wanted for the simulation. What I plan for sprint 3 is to create some sprites and animations to make the simulation more interesting to look at, I’d also like to build a UI that lets the user change parameters of the simulation.

DES311 #4: Goal Seeking

This week I’ve made some improvements to the cohesion behaviour and implemented goal seeking.

Cohesion Improvements

Last week I spoke about how the cohesion behaviour wasn’t causing the swirling patterns I was looking for. I hypothesized this was because BOIDs were packed together too tightly and not able to jostle for central position. I attempted to improve this by having the separation behaviour use a collider to find nearby BOIDs rather than a single ray directly in front of it. This didn’t do the trick. Because separation looks for clear paths at increasing angles from the forward direction. This meant if a BOID entered the collider from the side the behaviour would try and find a clear path directly in front, which it would since the nearby BOID is at its side. This meant the separation was constantly maxing out the pilot module with a vector of straight ahead, meaning lower priority behaviours wouldn’t have any control.

I reverted the build back since this method seems like a dead end for now. Instead I made a change to the cohesion behaviour so it doesn’t take into account BOIDs that are too close:

                //objects too close don't get included, leads to natural separation
                if (Vector2.Distance(transform.position, nearbyBodies[i].transform.position) > targetSeperation)
                {
                    sumX += nearbyBodies[i].transform.position.x;
                    sumY += nearbyBodies[i].transform.position.y;
                }

With this emergent separation between BOIDs they now have room to jostle for central position, leading to the patterns I was looking for:

Goal Seeking

Next on my list of goals for this sprint was to add a goal seeking behaviour. This is the first behaviour that lets the user interact with the the flock.

How it works is when a static bool is read as true by the seeking behaviour in each BOID the position of the mouse (stored as a static vector) is used to calculate a new direction vector which is sent to the pilot module. The static bool is made true when the left mouse button is clicked. When the left mouse button is released a circle is instantiated at the mouse position and the BOIDs will move to that circle. The circle is destroyed when a BOID touches it.

When sprites are added this action can be made to look like the user is holding a piece of food above a fish tank, which they drop in when the mouse is released.

What’s Next

This concludes the first week of sprint two. For the second week I am aiming to implement predators and predator avoidance. These will be triangles who will move across the screen and the BOID flock will scatter to avoid.

DES311 #3: Coming Together

This week I have managed to implement the cohesion behaviour in my BOID simulation as well as completing a project timeline. I’ll discuss both in this post.

The Timeline

The project will be broken up into 4 fortnightly sprints whose tasks will be taken from a project backlog. This backlog will change over the course of the project based on research, feedback and lessons learnt from implementation, but I estimate the tasks for each sprint will look like this:

Sprint 1: 1st March – 14th March

  • Implement basic functions of:
    • Obstacle Avoidance
    • Follow nearby flock mates
    • Flock cohesion  

Sprint 2: 15th March – 28th March

  • Implement advanced features of:
    • Goal seeking
    • Predator avoidance

Sprint 3: 29th March – 11th April

  • Basic sprites
  • Basic animations
  • Basic sounds that provide feedback
  • A simple UI

Sprint 4: 19th April – 2nd May

  • Tweaking based on previous sprints and polish

Tasks and progress will be tracked on a public Trello board.

Cohesion

The cohesion behaviour makes BOIDs group and move closer together whilst also jostling for a central position in their local group. This is the behaviour that leads to the beautiful dynamic swirling seen in real world swarms.

In the simulation I currently have BOIDs keeping a list of any nearby flock mates inside a trigger collider around them. In the alignment behaviour the velocity of surrounding flock mates is averaged and sent to the pilot module. In the cohesion behaviour the position of surrounding BOIDs is instead averaged. A vector is then drawn between the BOID’s current position and the average position. This vector is then normalised, multiplied by the magnitude of the max velocity and then sent to the pilot module.

The result looks like this…

Did you spot them? Some BOIDs are breaking the speed limit somehow. I spent the best part of this week trying to find out a reason and solution. My current hypothesis is that it has something to do with the edge warp. The edge warp is what teleports objects to the opposite side of the screen when they go outside the bounds of the camera, like in the game Asteroids. I think when a group of BOIDs approachs the edge the ones at the back are calculating the average position to beyond the edge, and then when they teleport to the opposite side of the map some value somewhere breaks and the excessive speed is it compensating. Honestly I have no idea.

But in true programming form I don’t need a reason so long as I have some sort of solution. For some reason adding a script that adds a small forward force to each BOID if its velocity is zero solves the problem. I think it pushes the BOID off the edge line before they have a chance to go weird.

        //if BOID gets stuck add forward force to keep it moving
        else if (rb2d.velocity == new Vector2(0, 0))
        {
            rb2d.AddForce(transform.up * speed);
        }

And hey, it works, look at this…

Separate groups of BOIDs are now attracted to each other. Already this motion is causing more interesting patterns to develop. However as you can see they aren’t really jostling for a central position. This is because I have a capsule collider on each BOID so they can’t get on top of each other. This is necessary because without it they all bunch on top of each other and look like a single BOID. To create better jostling I think they need to be able to go on top of each other briefly whilst attempting to maintain distance apart. To do this I will need to improve the separation mechanic so it isn’t just looking at a single ray in front but moves the BOID to avoid flock mates next to it too.

What’s Next?

I am happy with the outcome of this fortnight sprint and think I have quite a respectable little simulation. The improved separation behaviour is high on my list of priorates for the next sprint though. Starting on the 15th of March sprint 2 will aim to implement this improvement as well as the goal seeking and predator avoidance behaviours. But for now I’m celebrating the end of this sprint with a German Donor kebab.

Aki: Spirit Walker

In this presentation I talk about how the team I’m part of put together a ten-minute game demo in under three weeks.

In Aki: Spirit Walker You play as Aki, a dog who was once lost in the woods and now guides lost souls to rest in the spirit world. You are sent to the land of the living once more, to help guide a soul who is lost in the woods much like you once were. In this short ten-minute demo you travel with the soul you were sent to find – along the way discovering objects that trigger memories of your past life. A life before you were Aki, the good boy who helps the lost find peace.

DES311 #2: Chilling with the BOIDS

This week I have implemented two parts of the BOID’s behaviour: Obstacle avoidance (separation) and the tendency to following nearby flock mates (alignment). I’ve also started the pilot module which makes the overall decisions on which way to go.

Quick side note. The plan for this project is to create a simulation of a school of fish, but for now I will be referring to the objects in the simulation as BOIDS (meaning bird like object) since that’s their common name in simulations of this nature. I’ll probably start calling them fish when I get round to making some sprites and artwork.

Separation

BOIDS with separation

Currently each BOID sends out a ray cast directly in front of it. If that ray cast returns with a BOID in front of it starts sending out ray casts at increasing angles until it finds a clear path. The vector of this clear path is sent to the pilot module and used in navigation.

At present the script that sends the rays out at increasing angles is functional, but I’m not overly happy with it. It feels a little over complicated and clumsy and could be made more efficient.

//if an object in front is detected
        if (rays.Length > 1)
        {
            //sets the arc to be scanned and increment it is done in
            float resolution = 5f;
            float angle = 120f;
            float radius = 2f;

            float Y, X, Z, tempAngle, tempAngle2;
            Vector3 direction;
            Vector3 center = transform.position;

            RaycastHit2D[] dangerCheck;


            for (tempAngle = 0; tempAngle <= angle; tempAngle += angle / resolution)
            {

                Y = -Mathf.Cos(tempAngle * Mathf.Deg2Rad);

                for (tempAngle2 = 0; tempAngle2 <= 180; tempAngle2 += angle / resolution)
                {

                    X = Mathf.Cos(tempAngle2 * Mathf.Deg2Rad) * Mathf.Sqrt(1 - Mathf.Pow(Y, 2));
                    Z = Mathf.Sqrt(1 - Mathf.Pow(Y, 2) - Mathf.Pow(X, 2));

                    //checks angle on right side
                    direction = Vector3.Normalize(new Vector3(X, Y, Z));
                    direction = transform.rotation * -direction;

                    Debug.DrawRay(center, direction * (radius), Color.green, .5f);
                    dangerCheck = Physics2D.RaycastAll(center, direction, radius);

                        if(dangerCheck.Length < 2)
                    {
                        //if a clear path is found breaks loop
                        pilot.seperationVelocity = direction;
                        return;
                    }

                    //checks angle on left side
                    direction = Vector3.Normalize(new Vector3(X, Y, -Z));
                    direction = transform.rotation * -direction;

                    Debug.DrawRay(center, direction * (radius), Color.green, .5f);
                    dangerCheck = Physics2D.RaycastAll(center, direction, radius);

                    if (dangerCheck.Length < 2)
                    {
                        //if a clear path is found breaks loop
                        pilot.seperationVelocity = direction;
                        return;
                    }
                }

            }
        }

Alignment

BOIDS with separation and alignment

Next I added the alignment behaviour. Now as well as avoiding other BOIDS in front of them BOIDS will find calculate the average velocity of their surrounding flock mates, resulting in them following each other. Seriously though, look at them go. I’m amazed how quickly these little triangles are starting to seem “alive”.

The code I wrote for this behaviour was a lot easier to understand and a lot more efficient.

        //checks if a another BOID is nearby
        if(nearbyBodies.Count != 0)
        {
            float sumX = 0;
            float sumY = 0;

            //sums velocity components of each nearby body
            for(int i = 0; i < nearbyBodies.Count; i++)
            {
                sumX += nearbyBodies[i].velocity.x;
                sumY += nearbyBodies[i].velocity.y;
            }

            //averages velocity components
            float meanX = sumX / nearbyBodies.Count;
            float meanY = sumY / nearbyBodies.Count;

            targetVelocity = new Vector2(meanX, meanY);
        }

Pilot Module

So I’ve mentioned the pilot module a few times. What does it do? Well so far there are two behaviours (alignment and separation) giving the BOID different direction vectors to move in. The pilot module receives these vectors and makes a decision on which way to go. But how should it make this decision? A simple solution is to average the incoming vectors and tell the BOID to go that way. In quite a few online tutorials this is the solution, and for the most part it works, but there is a problem. If separation detects an obstacle directly ahead and says move left and alignment sees a bunch of other BOIDS going right, the average vector would be straight ahead, and the BOID would drive straight into the obstacle. And this problem will only get worse as more behaviours are added. A real Buridan’s ass situation.

So clearly priority needs to be given to one of the behaviours. Since avoiding crashes is more important than following the group, separation is priority one. But how should priority be set, should each incoming vector be given a weighting based on what behaviour its from? Say everything from separation is multiplied by 3 and everything from alignment by 2… and so on. Well this leads to the problem of how do you decide the weighting, it’s just an arbitrary number that you’d have to test and adjust until the right balance is found. Yeah-nah, that would get tedious real quick.

The solution I reached was to have a maximum vector magnitude that the pilot’s target vector can not exceed. So the separation vector comes in and it is added to the target vector. Next comes separation, if adding it to the target vector means the target doesn’t exceed the limit then its added. If it does exceed the limit then its reduced so it can fit. This would then continue through each behaviour in order of their importance parcelling out parts of the target vector until its all used up. This means if a collision is imminent then separation can take full control, and when the BOID is traveling along safely is can make decisions based on other behaviours.

What’s next…

Here’s my plan for the upcoming weeks:

  • Create a Gantt chart breaking down the project
  • Streamline the separation script
  • Experiment with using colliders rather than ray casts to detect impending collisions
  • Implement the cohesion behaviour