Section 2 - Basic Behaviours
4 - Setting up the teamwork blackboard/framework
In this tutorial we will go over setting up a simple script which will convey crucial combat information between predetermined AIs. This means we get our AIs to plan and execute tactics together. Not only will this allow for team tactics, but it will help save processing power and space! If we were to put some of these checks in our AI script, every single AI would have to perform that check, but now we can do certain checks once. Or we can move every check from an AI into this script This is where I personally feel things start to get fun, when working with AI.
In this tutorial we will only cover the basics of this script, and set up a line of sight check. We will come back to this later, when new checks and tactics are needed!
Notice how I often use the word blackboard? This is an AI term. A blackboard simply refers to a set of variables which is equally accessible by multiple AIs. What we are making now is a blackboard script.
NOTE: This system is best suited for having different squads of AI in a small open world, or for spawning enemies in waves. If youâre trying to do something else, you might want to create a dynamic blackboard, or use a toolbar/singleton system.
Scripting - Refrencing
First of all, make a new C# script. This will be the squad system script, so name it appropriately.
The squad script will need a refrence to all its AIs, and it would be best if the AI also know which squad they are a part of. Doing this is simple, and will require only a list of the agents in this squad. Remember to add the System.Collections.Generic; directive!
In this tutorial we are only making a single kind of AI, and therefor I will only refrence the AI script we are making in this list. However, do note that it is easily possible to create a fully dynamic version using interfaces. See the previous tutorial (2.3) for more information!
List
using System.Collections.Generic;
public class Squad : MonoBehaviour {
public List<myAIscript> agents = new List<myAIscript>();
}
Thats it. Only downside being we have to manually assign the AIs. See the suggestion list for improvement.
Now our AI needs to know what squad they are in. Open up the main AI script and add a refrence to the squad script. Make it private or internal or add a [HideInInspector] attribute to it, so it isnât visible in the inspector.
squad
But why hide it in the inspector? How do we change it now? Weâll do this in the squad script, in the Awake() function. Foreach ai in the list we made, we will change its squad script refrence to this squad script. (This is a good place to allow for multiple kinds of ai using an interface and linq)
Foreach
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class Squad : MonoBehaviour {
public List<myAIscript> agents = new List<myAIscript>();
void Awake() {
foreach(myAIscript ai in agents) {
ai.gameObject.SetActive(true); //Optional
ai.sq = this;
}
}
}
Scripting - Line of Sight Coroutine
To help our AI make some crucial combat decisions, we need as much information as possible to be easily available for it. Some information is AI specific, like location and distance to the player etc. But Some info is the same for everyone. An example of this is the players Line of Sight. Naturally our AI would want to avoid the players Line of Sight when taking cover, or moving between cover. This is where the A* pathfinding becomes very handy. We can consider every node as a potential coverspot, as opposed to a navmesh where we need to generate lines and fustrums and everything gets very complicated! Note that the follow code is specific to the A* project by Aron Granberg. All the features should be available in the free version!
First of all, start by adding the A* project pathfinding directive, before declaring all necessary variables. A refrence to the players transform. A layermask which will include any solid object which might block the players view, and exclude any see-through/transparent layers. An array of GridNodes which will contain all the nodes on the A* grid. And finally a float which will dictate how frequently the line of sight check will fire.
Variables
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Pathfinding;
public class Squad : MonoBehaviour {
public Transform player;
public List<myAIscript> agents = new List<myAIscript>();
public LayerMask l;
public float freq = .5f;
internal GridNode[] nodes;
void Awake() {
foreach(myAIscript ai in agents) {
ai.gameObject.SetActive(true); //Good idea
ai.sq = this;
}
}
}
Now we need to create a coroutine, and start it at the end of the Awake() function. The first thing we write inside this is a WaitForEndOfFrame command. This is because of what we do next. Assigning the GridNode[ ] nodes we declared earlier. This is a single line of the A* project.
nodes = AstarPath.active.astarData.gridGraph.nodes;
Now we need to set up a loop. In a coroutine we can use goto, or while(true) to acheive this effect. Inside the loop we will have another set of variables. This is the position of the player and the forward direction of the player. After that we will make yet another loop. This will be a foreach loop which will iterate through each GridNode in the nodes array [foreach(GridNode g in nodes)]. After this foreach has finished we will restart the while(true) loop with a new WaitForSeconds(freq).
The rest of the code will be done inside the foreach loop. First of all we want to ignore any node which cannot be walked on. In the A* project we can check if a GridNode is .Walkable to see if it blocked or not. Check if this GridNode is walkable, and if not, continue; the foreach loop.
Loops
IEnumerator LoS() {
yield return new WaitForEndOfFrame();
nodes = AstarPath.active.astarData.gridGraph.nodes;
while(true) {
Vector3 pos = player.position;
Vector3 fw = player.forward*0.1f;
foreach(GridNode g in nodes) {
if(!g.Walkable)
continue;
}
yield return new WaitForSeconds(freq);
}
}
Now we need some node specific local variables. First of all we need to convert the Vector2 position of the node to a normal Vector3 variable. Then we need the distance from the player to the node, and the angle between the players forward direction and the node, relative to the player.
Vector3 p = new Vector3(((Vector3)g.position).x, fw.y, ((Vector3)g.position).z);
float d = Vector3.Distance(pos, p);
float a = Vector3.Angle(fw, p-pos);
Theory - Cover scoring system base
Now comes the actual line of sight check. Since we arenât using the A* grid tags, we can utulize these. (If you need to use tags for something else, you can save these line of sight tags as an array of float in the squad script.) Not only will we check if a node can provide cover by using a line of sight check, but we will check if the AI have to crouch, or they can stand in this spot. I struggled for a long time to make an efficient solution to this, and I settled for a binary type system. We need two numbers. The first number from the right will be 0, 1 or 2, and will tell if there is no cover, waist-high cover, or full cover when standing.

To do this, we first need to check every node twice, so below everything we have written so far inside our foreach(GridNode g in nodes), we write a foreach loop which counts down from 1 to 0. If we start from the top, and a node is covered in the standing stance position, we can assume that this spot also covers the crouching stance, and simply continue to the next node. Remember to reset the nodes tag value to 0 before starting this loop.
Scripting - Line of Sight
The line of sight check is simple. First we check if this node is inside the players field of view, and then we raycast from the models eyes middle position to the node, and change the nodes tag based on these two returns. Starting from the top and going downwards in our loop means we can easily avoid duplicating scores.
If the angle between the node and the players forward direction is less than the players cameras field of view, and this is the first node we check, set the tag to 10. Then if a linecast between the players eyes and the node returns true, we will add the current value of our for loop plus 1 to the tag and break out of the loop. (See the below code snippet if this doesnât make sense)
For Loop
g.Tag = 0; //Reset the nodes tag
for(int i = 1; i >= 0; i--) { //Do all this twice:
#region LoS
if(a < player.GetComponentInChildren<Camera>().fieldOfView && i == 1) { //Assumes both points are in sight if one is
g.Tag = 10; /*LoS | (11 - Crouching Cover in LoS) | (01 - Crouching Cover outside LoS)
| (12 - Standing Cover in LoS) | (02 - Standing Cover outside LoS)
| (10 - No Cover in LoS | (00 - No Cover outside LoS)
*/
}
#endregion
#region Cover
if(Physics.Linecast(pos, p+(Vector3.up*i)+(Vector3.up*.25f), l)) { //Raycast from player to node, decide height based on loop value
g.Tag += (uint)i+1; //Cover | (01 - cover crouching) | (02 - cover standing)
break; //NoCover | (00 - no cover) | (01 - no standing cover)
}
#endregion
}
Finished Squad Framework
Finished Squad Framework
Squad script
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Pathfinding;
public class Squad : MonoBehaviour {
public Transform player;
public List<myAIscript> agents = new List<myAIscript>();
public LayerMask l;
public float freq = .5f;
internal GridNode[] nodes;
void Awake() {
foreach(myAIscript ai in agents) {
ai.gameObject.SetActive(true); //Good idea
ai.sq = this;
}
StartCoroutine(LoS());
}
IEnumerator LoS() {
yield return new WaitForEndOfFrame();
nodes = AstarPath.active.astarData.gridGraph.nodes;
while(true) {
Vector3 pos = player.position;
Vector3 fw = player.forward*0.1f;
foreach(GridNode g in nodes) {
if(!g.Walkable)
continue;
Vector3 p = new Vector3(((Vector3)g.position).x, fw.y, ((Vector3)g.position).z); //Node position
float d = Vector3.Distance(pos, p); //Distance from player to node
float a = Vector3.Angle(fw, p-pos); //Angle between players forward direction and node relative to player
uint tagscore = 0;
for(int i = 1; i >= 0; i--) { //Do all this twice
#region Cover
if(Physics.Linecast(pos, p+(Vector3.up*i)+(Vector3.up*.25f), l)) { //Raycast from player to node, decide height based on loop value
tagscore += (uint)i+1; //Cover | (01 - cover crouching) | (02 - cover standing)
} //NoCover | (00 - no cover) | (01 - no standing cover)
#endregion
#region LoS
if(a < player.GetComponentInChildren<Camera>().fieldOfView && i == 1) { //Assumes both points are in sight if one is
tagscore += 10; /*LoS | (11 - Crouching Cover in LoS) | (01 - Crouching Cover outside LoS)
| (12 - Standing Cover in LoS) | (02 - Standing Cover outside LoS)
| (10 - No Cover in LoS | (00 - No Cover outside LoS)
*/
}
#endregion
}
g.Tag = tagscore;
}
yield return new WaitForSeconds(freq);
yield return null;
}
}
}
AI
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using OptionStacker;
using Pathfinding;
public class myAIscript : MonoBehaviour, IDamageable {
#region Variables
private ExtendedStacker stack = new ExtendedStacker();
internal Seeker seeker;
internal AIPath agent;
internal Squad sq;
private Transform target;
private float distance;
private Vector3 lastPrivateSpot;
private float spot;
private Vector3[] point;
private int indexer;
[Header("General")]
public Transform player;
public Transform head; //NOTE: AI's head
public LayerMask seeMask;
public float timeBeforeSpotted = 3;
public float health = 100;
[Header("Patrol")]
public GameObject patrolPath;
public float patrolSpd = 1;
[Header("Alert")]
public float alertSpeed = 0.75f;
private bool inSight;
#endregion
void Start() {
#region Get Agent
seeker = GetComponentInChildren<Seeker>();
agent = GetComponentInChildren<AIPath>();
target = new GameObject(gameObject.name + "'s pathfinding target").transform;
target.position = transform.position;
agent.target = target;
#endregion
#region Set up Patrol Path
List<Vector3> size = new List<Vector3>(patrolPath.GetComponentsInChildren<Transform>().Length-1);
foreach(Transform p in patrolPath.GetComponentsInChildren<Transform>()) {
if(p != patrolPath.transform) {
size.Add(p.position);
}
}
point = size.ToArray();
#endregion
#region Simple Option Stacker
stack.StartStack(this);
stack.PushState("Patrol");
#endregion
}
void Update() {
distance = Vector3.Distance(agent.GetFeetPosition(), target.position);
stack.Update();
}
#region Patrol
void OnPatrol() {
agent.speed = patrolSpd;
stack.PopState();
}
void Patrol() {
if(inSight || spot > 0) {
stack.PushState("Alert");
return;
}
if(agent.TargetReached && distance <= agent.endReachedDistance) {
indexer++;
if(indexer >= point.Length) indexer = 0;
target.position = point[indexer];
}
}
#endregion
#region Alert
public void OnAlert() {
agent.speed = alertSpeed;
stack.PopState();
}
public void Alert() {
if(inSight) {
spot+=1*Time.deltaTime;
} else {
spot-=0.5f*Time.deltaTime;
}
if(spot < timeBeforeSpotted && spot > 0) {
if(spot > 0.25f) {
target.position = lastPrivateSpot;
}
} else if(spot >= timeBeforeSpotted) {
spot = timeBeforeSpotted;
stack.PopState();
stack.PushState("Combat");
} else {
agent.speed = patrolSpd; //OnReturnPatrol()
spot = 0;
stack.PopState();
}
}
#endregion
#region Vision
void OnTriggerStay(Collider o) {
inSight = false;
Plane[] frustumPlanes = GeometryUtility.CalculateFrustumPlanes(head.GetComponent<Camera>());
if(!GeometryUtility.TestPlanesAABB(frustumPlanes, o.transform.FindChild("EthanBody").GetComponent<SkinnedMeshRenderer>().bounds)) { //I am using the Ethan model, which is part of the unity standard assets!
return;
}
Vector3 dir = player.position - head.position;
float d = Vector3.Distance(player.position, head.position);
if(Physics.Raycast(head.position, dir, d, seeMask)) {
return;
} else {
inSight = true;
}
}
#endregion
#region Damage
public void TakeDamage(float damage) {
health -= damage;
if(health > 0) {
stack.PushState("ReactToDamage");
} else {
Die();
}
}
IEnumerator ReactToDamage() {
Debug.Log("Ouch!");
yield return new WaitForSeconds(2); //Simulate reaction
stack.PopState();
}
void Die() {
DestroyImmediate(gameObject);
}
#endregion
}
Suggestion List:
Refrence multiple kinds of AI in the same list -
Assign every AI in the scene to this list if it is empty -
Make your AIs avoid any spot where any other AI has previously been damaged in -
Only take cover at edge nodes to ensure cover is being taken next to e.g. a wall -
Reserve coverspots, so no agents will ever try to take the same coverspot -
Tweak line of sight frequency/accuracy variables through an options menu ingame -
Fire the line of sight check every X seconds instead of X plus time-taken-to-complete seconds -