Blackboard AI architecture

When creating bloom, I wanted to experiment with the idea of an overarching AI that would be responsible for macro level plays, such as if it should attack or be on the defensive. I read through many different possible implementations, and one source that was of great help was the gameaipro series, it’s free online and has many great articles written by different developers. Coincidentally, the editor, Steve Rabin, teaches at my school, and has my favorite class.

I settled on using a blackboard architecture, where information will be periodically gathered, into the “blackboard”, and a decision will be made that determines the AI actions for the next few minutes. The AI class, had a list of wants and goals.

	
std::string goalNames[]{ "ExpandBase","IncreaseUnitStrength","AttackPlayer","HarrassPlayer","StockpileResource" };
std::string wantNames[]{ "buildRecruitment","buildUnit","buildGenerator","sendPatrol","readyWave","attackPlayer","pokePlayer"};

The AI operates on a score based system. Goals have a priority of 0 - 1, where 1 is the highest.

Every AIupdate , all goals will have an updated value. This could change based on various factors, like when the player is weak, the AI goal of attacking the player will have a higher priority.

Each wants all have a value, and the highest value will be the one executed. Each goal has a modifier that impacts each want, and during the update, we will calculate which want has the highest value.

The highest valued action will be done first, and in my implementation, it will do the next one or two in a loop until the next AIupdate.

Here’s an example, on update, the AI will update its values, in my case I take in the strength of the player, the strength of the AI, and various other factors like number of resources or if the player has attacked recently. These factors will all modify the priorities of each goal.

	

	if (skynetUnitStrength > playerStrength * 1.5f)
	{ 
		//dont have to make more units
		getGoal("IncreaseUnitStrength")->priority *= 0.2f;

		//should focus on attacking instead of just harrass
		getGoal("AttackPlayer")->priority *= 1.2f;
		getGoal("HarrassPlayer")->priority *=0.8f;
	}
	else if (skynetUnitStrength > playerStrength)
	{

		//have more units but not overwhelming, put pressure
		getGoal("HarrassPlayer")->priority *= 1.2f;
		getGoal("AttackPlayer")->priority *= 0.8f;
	}
	//dont @ this if, i just want to make it look clear
	else if ( skynetUnitStrength < playerStrength)
	{
		//need more units to make up for strength
		getGoal("IncreaseUnitStrength")->priority *= 1.2f;

		//dont want to lose
		getGoal("AttackPlayer")->priority *= 0.4f;
		getGoal("HarrassPlayer")->priority *= 0.4f;
		//SPEND
		getGoal("StockpileResource")->priority *= 0.4f;

			
	}
	//this is a random number for now, can be changed in the future
	//just checking to see if i have extra money
	if (resources > 100 || skynetNumberOfGenerator > 3)
	{
		//can spend money on something
		getGoal("ExpandBase")->priority *= 1.2f;
		getGoal("IncreaseUnitStrength")->priority *= 1.2f;
		//stop stockpiling
		getGoal("StockpileResource")->priority *= 0.2f;
		
	}

Then later, these goals are multiplied with the score impact and then the action list is determined. Actions like building more units would be low in the action list if the goal for expanding is low, or if other goals have a higher priority.

	
	for (const auto& i : wants)
	{
		std::cout << *i;
		std::cout << "action value: " << (*i).changeGoalValues(goals) << std::endl << std::endl;
		if ((*i).changeGoalValues(goals) >= highestAction)
		{
			highestName = (*i).name;
			highestAction = (*i).changeGoalValues(goals);
		}
	}


float SkynetAi::Want::changeGoalValues(std::vector theGoals)
{
	float score = 0;
	for (int i = 0; i < numOfGoals; ++i)
	{
		score += theGoals[i].priority * goalImpact[i];
	}
	return score;
}

I skimmed over many of the details, but this was just to give a high level overview of what it looks like. It was pretty fun to do, although I’m sure that I could have done it an easier way. I thought that this was quite robust, being able to handle different situations without having to hardcode it in, and adding a weighted random could give the AI some unpredictability. Although the numbers were easily tweaked, one big issue was that it was hard to visualize what the AI might do in different situations, I had to add some debug functionality to go through the process and see if what the AI is doing is logical. This was kind of time consuming and would make bugs in the logic really hard to solve.

Previous
Previous

Indirect Instanced draw calls