Assignment #2 - Flocking+

GPR 440
[ 2.19.21 ]

Made With Languages
Unity3D
Rider
C#

Overview

The goal of this project was to combine the obstacle avoidance I created in the last project with a flocking behavior. For this one, I focused on de-coupling my code to make a more modular approach to the flocking behavior, to help myself by making it more maintainable and to provide a better code foundation for future projects.

Creating the Overarching Behavior Class

As part of my refactoring, I created a "Steering Component" class that returns a normalized vector direction to a "Steering Behavior" class. I may have misnamed them, as 'component' implies that the steering component is attached to the game object, when it is actually a part of the behavior which is attached to the game object.

So, as I created my boids, I made a new class that inherits from SteerComponent and took the avoidance functionality and moved it into there. For avoidance specifically, I created a new overload for GetSteering() that doesn't require the nearby parameter because it does not need it.

public abstract class SteerComponent
{
    protected Transform _self;

    protected SteerComponent(Transform self)
    {
        _self = self;
    }
    
    public abstract Vector3 GetSteering(Transform[] nearby);
}
Arbitration

For flocking especially, arbitrating between different individual components is very important, and is not something I really considered when only doing avoidance. With flocking, however, you also have to consider: separation, cohesion and alignment behaviors as well. Because of that, I need the ability to balance the resulting targets from 4 primary steering components.

This screenshot is of the debug view. You can see the separation, cohesion, alignment, avoid, forward and the resulting final direction post-arbitration.


// === FlockSteeringBehavior === //
/*  Public Variables */
float separationWeight
float cohesionWeight
float alignmentWeight
float avoidWeight

/*  In UpdateSteering()  */
// Get the directions
Vec3 cohe = cohesion.GetSteering(nearby);
Vec3 algn = alignment.GetSteering(nearby);
Vec3 sepa = separation.GetSteering(nearby);
Vec3 wAvd = avoid.GetSteering(nearby);

// Find the new acceleration
Vec3 newAcc = cohe * cohesionWeight + 
              algn * alignmentWeight +
              sepa * separationWeight + 
              wAvd * avoidWeight;

// Make ||acceleration|| = maximum acceleration
newAcc = newAcc.norm * maxAcc

My approach to arbitration is a simple one: weights. I just take in values for how much you want one component to be valued in relation to another. By multiplying the output by the weight and then added together. Components with a higher weight will of course have a much greater impact on the accumulated acceleration. This vector's magnitude is then set to the maximum acceleration.

For weights to work more reliably, you have to have the outputs for the steering components have a magnitude of 1, otherwise editing the logic and/or parameters in one of the components can then change the final acceleration calculated in a way that circumvents weights. This is because if the new logic/parameters returns a vector twice as long or half as long, it would effectively change the weight of that component by the same factor.

So basically, unit directions and relative weights allow fine control over how the flocking works.

Flocking Components

Below I go over the the basic goal of the different flocking components and the basic algorithm (they're all pretty straightforward) for their new target direction.

Note: I don't include it in the pseudocode below but I do normalize the results when they're returned.

Cohesion

The goal of cohesion is to try to keep the flock of boids together in a group. The basic algorithm is:

targetDir = avgPos - boidPos

Basically, the cohesion behavior makes boid moves toward the average position of it's neighbors within its check distance (not including itself). This helps keep other boids within the check distance to have a more define flock.


Separation

The goal of separation is to try to keep the flock of boids from running into each other. This component also requires a "minimum separation" value that determines how close another boid needs to be before it tries to separate.

First, you have to get the vector between the boid and its neighbor and check if it is too close. If yes: perform separation logic. (diffAccum is what is eventually returned)

diff = boidPos - neighborPos
if (diff.sqrMagnitude < minSeparationSqr)
diffAccum += diff;

This works because the vectors TO the boid FROM the neighbors collects all of the directions the boid needs go in to move away from the neighbors that are too close. You can then take a step furthur to make the boid prioritize moving away from very close neighbors. To do this, I use the equation:

diff = diff.norm * (minSeparation - diff.mag)

This way, neighbors on the edge of the minimum distance have almost zero effect on which direction the boid evades when there is another neighbor that is fairly close.


Alignment

The goal of alignment is to try to keep the flock of boids facing the same direction. The algorithm is as follows:

targetDir = avgHeading

The average heading is the average forward direction or rotation of all of the boid's neighbors. I accounted for the rollover issue with rotations (where 0° = 360°) by converting rotations into vector directions.

Vec3 heading = Vec3(Sin(neighborRot),
Cos(neighborRot), 0f)
avgHeading += heading


My Flocking Values

The harder part of flocking is getting the weights to a point where they exhibit the behavior you want. Especially when you also want to consider a seek behavior (like moving towards a point the user clicked) and an avoidance behavior. My solution was to weight those very high relative to all others, and then in the way I handle their behavior I make sure to handle when they do not matter. For example, if there are no hits on the avoidance behavior, the component returns a vector of zeros so it does not impact the overall steering at all because there is nothing to avoid.

Another thing to note is that I have a "forward" weight. The forward is component is just the vector straight forward in the direction the boid is facing. This helps keep the boids moving in the forward direction and resemble more bird-like movement behavior.