So we have a working boids implementation in UnrealEngine, this is fine and all, but when we come to look at expanding the behaviours of our boids...we're going to start hitting a wall.
In the original implementation, all boids use the same hard-coded algorithms, and will always behave identically. I embarked on a refactoring of the code with the following aims:
- Allow boid behaviour algorithms to be added and removed dynamically at runtime.
- Allow boids to maintain their own state and therefore behave independent of other boids.
The core of this meant refactoring the boid behaviours out into their own class, BoidAlgo. Here's the header :
class BoidAlgo { public: BoidAlgo(int _algo_type, float _weight); virtual ~BoidAlgo(); // Some consts for readability static const int COHERENCE = 0; static const int ALIGNMENT = 1; static const int AVOIDANCE = 2; static const int BOUNDS = 3; // The boid algorithm declarations static FVector func_coherence(const ABoid& _currentBoid, const boid_config& _config, const float& _weight); static FVector func_alignment(const ABoid& _currentBoid, const boid_config& _config, const float& _weight); static FVector func_avoidance(const ABoid& _currentBoid, const boid_config& _config, const float& _weight); static FVector func_bounds(const ABoid& _currentBoid, const boid_config& _config, const float& _weight); bool setAlgo(int _algoEnum); // Function pointer used to define which algorithm is to be used in this instance FVector(*calc_func) (const ABoid& _currentBoid, const boid_config& _config, const float& _weight); // weighting to apply to this algorithm float weight; };
Each boid behaviour is now defined as a static function within BoidAlgo. Each instance of BoidAlgo only contain the weight to apply to the algorithm, and a function pointer *calc_func to the appropriate static function. When we create a BoidAlgo we pass the algo type and weight to the constructor and hey presto, one very lightweight algorithm class. I did initially go the usual inheritance route of having an abstract BoidAlgo class and concrete implementations of each algorithm, but decided that this approach is cleaner, less files, and just as easy to maintain.
The setup and configuration of the boids by the BoidManager now looks like this :
// Called when the game starts or when spawned void ABoidManager::BeginPlay() { Super::BeginPlay(); World = GetWorld(); FVector thisSpawnPos; // Populate the configuration struct with values from the editor // ready to pass to the boids config.allBoids = &boids; config.avoidBoidRange = AvoidBoidRange; config.avoidBoundsStrength = AvoidBoundsStrength; config.velocityMax = VelocityMax; config.cohesionWeight = CohesionWeight; config.avoidWeight = AvoidWeight; config.alignWeight = AlignWeight; config.spawnBoundsStart = SpawnBoundsStart; config.spawnBoundsEnd = SpawnBoundsEnd; config.interactionRange = InteractionRange; for (int32 i = 0; i < NumberOfBoids; i++) { // Pick a random point in the defined bounds to spawn thisSpawnPos.X = FMath::RandRange(SpawnBoundsStart.X, SpawnBoundsEnd.X); thisSpawnPos.Y = FMath::RandRange(SpawnBoundsStart.Y, SpawnBoundsEnd.Y); thisSpawnPos.Z = FMath::RandRange(SpawnBoundsStart.Z, SpawnBoundsEnd.Z); // Spawn the boid into the world, keep the reference ABoid* spawnedBoid = World->SpawnActor<ABoid>(ABoid::StaticClass(), thisSpawnPos, FRotator(0.0f)); // Stash the reference, key to this implementation! boids.Add(spawnedBoid); // Setup the vector of algorithms to give to this boid // Note we're giving each boid it's own instance so they can change independently TArray<BoidAlgo> _algos; BoidAlgo algo_coherence(BoidAlgo::COHERENCE, config.cohesionWeight); BoidAlgo algo_alignment(BoidAlgo::ALIGNMENT, config.alignWeight); BoidAlgo algo_avoidance(BoidAlgo::AVOIDANCE, config.avoidWeight); BoidAlgo algo_bounds(BoidAlgo::BOUNDS, 1.0f); _algos.Add(algo_coherence); _algos.Add(algo_alignment); _algos.Add(algo_avoidance); _algos.Add(algo_bounds); // Pass the configuration and starting algorithms to the boid spawnedBoid->setup(&config, _algos); } }
Pretty similar to before, but now we are creating a vector of BoidAlgo instances for each Boid. This might seem wasteful, but remember, each instance only contains a float and a function pointer.
Inside the Boid class, we now handle all the behaviours with just a few lines of code :
for (auto& _algo : algos) { newVelocity += _algo.calc_func((*this), (*config), _algo.weight); }
Just loop through the algos we have and apply the result to our new velocity.
Although the boids behave exactly as they did previously, we now have a much more robust codebase to start adding advanced behaviours. We can change the algo weightings on each boid individually, and even add and remove algorithms on the fly....perhaps we will be adding an algorithm for terrain avoidance or predator evasion? Now it's as easy as writing the static function in BoidAlgo and adding it to the Boid's list of algos. Sorted :)
As before, all the code is available here on github (note I've moved to a new repo with this refactoring).
{fcomment}