Now that we know how to implement sensory-level algorithms, it's time to see how they could be taken into account in order to develop higher-level techniques for creating agent awareness.
This recipe is based on the work of Brook Miles and its team at Klei Entertainment for the game, Mark of the Ninja. The mechanism moves around the notion of having interest sources that can be seen or heard by the agents, and a sensory manager handling them.
As a lot of things move around the idea of interests, we'll need two data structures for defining an interest's sense and priority, and a data type for the interest itself.
This is the data structure for sense:
public enum InterestSense { SOUND, SIGHT };
This is the data structure for priority:
public enum InterestPriority { LOWEST = 0, BROKEN = 1, MISSING = 2, SUSPECT = 4, SMOKE = 4, BOX = 5, DISTRACTIONFLARE = 10, TERROR = 20 };
The following is the interest data type:
using UnityEngine; using System.Collections; public struct Interest { public InterestSense sense; public InterestPriority priority; public Vector3 position; }
Before developing the necessary classes for implementing this idea, it's important to note that sensory-level functions are left blank in order to keep the recipe flexible and open to our custom implementations. These implementations could be developed using some of the previously learned recipes.
This is a long recipe where we'll implement two extensive classes. It is advised to carefully read the following steps:
using UnityEngine; using System.Collections; using System.Collections.Generic; public class AgentAwared : MonoBehaviour { protected Interest interest; protected bool isUpdated = false; }
public bool IsRelevant(Interest i) { int oldValue = (int)interest.priority; int newValue = (int)i.priority; if (newValue <= oldValue) return false; return true; }
public void Notice(Interest i) { StopCoroutine(Investigate()); interest = i; StartCoroutine(Investigate()); }
public virtual IEnumerator Investigate() { // TODO // develop your implementation yield break; }
public virtual IEnumerator Lead() { // TODO // develop your implementation yield break; }
using UnityEngine; using System.Collections; using System.Collections.Generic; public class InterestSource : MonoBehaviour { public InterestSense sense; public float radius; public InterestPriority priority; public bool isActive; }
public Interest interest { get { Interest i; i.position = transform.position; i.priority = priority; i.sense = sense; return i; } }
protected bool IsAffectedSight(AgentAwared agent) { // TODO // your sight check implementation return false; }
protected bool IsAffectedSound(AgentAwared agent) { // TODO // your sound check implementation return false; }
public virtual List<AgentAwared> GetAffected(AgentAwared[] agentList) { List<AgentAwared> affected; affected = new List<AgentAwared>(); Vector3 interPos = transform.position; Vector3 agentPos; float distance; // next steps }
foreach (AgentAwared agent in agentList) { // next steps } return affected;
agentPos = agent.transform.position; distance = Vector3.Distance(interPos, agentPos); if (distance > radius) continue;
bool isAffected = false; switch (sense) { case InterestSense.SIGHT: isAffected = IsAffectedSight(agent); break; case InterestSense.SOUND: isAffected = IsAffectedSound(agent); break; }
if (!isAffected) continue; affected.Add(agent);
using UnityEngine; using System.Collections; using System.Collections.Generic; public class SensoryManager : MonoBehaviour { public List<AgentAwared> agents; public List<InterestSource> sources; }
Awake
function:public void Awake() { agents = new List<AgentAwared>(); sources = new List<InterestSource>(); }
public List<AgentAwared> GetScouts(AgentAwared[] agents, int leader = -1) { // next steps }
if (agents.Length == 0) return new List<AgentAwared>(0); if (agents.Length == 1) return new List<AgentAwared>(agents);
List<AgentAwared> agentList; agentList = new List<AgentAwared>(agents); if (leader > -1) agentList.RemoveAt(leader);
List<AgentAwared> scouts;
scouts = new List<AgentAwared>();
float numAgents = (float)agents.Length;
int numScouts = (int)Mathf.Log(numAgents, 2f);
while (numScouts != 0) { int numA = agentList.Count; int r = Random.Range(0, numA); AgentAwared a = agentList[r]; scouts.Add(a); agentList.RemoveAt(r); numScouts--; }
return scouts;
public void UpdateLoop() { List<AgentAwared> affected; AgentAwared leader; List<AgentAwared> scouts; foreach (InterestSource source in sources) { // next steps } }
if (!source.isActive) continue; source.isActive = false;
affected = source.GetAffected(agents.ToArray()); if (affected.Count == 0) continue;
int l = Random.Range(0, affected.Count); leader = affected[l]; scouts = GetScouts(affected.ToArray(), l);
if (leader.Equals(scouts[0])) StartCoroutine(leader.Lead());
foreach (AgentAwared a in scouts) { Interest i = source.interest; if (a.IsRelevant(i)) a.Notice(i); }
There is a list of interest sources that could get the attention of a number of agents in the world. Those lists are kept in a manager that handles the global update for every source, taking into account only the active ones.
An interest source receives the list of agents in the world and retrieves only the affected agents after a two-step process. First, it sets aside all the agents that are outside its action radius and then only takes into account those agents that can be reached with a finer (and more expensive) sensory-level mechanism.
The manager handles the affected agents, sets up a leader and scouts, and finally
It is worth mentioning that the SensoryManager
class works as a hub to store and organize the list of agents and the list of interest sources, so it ought to be a singleton. Its duplication could bring undesired complexity or behavior.
An agent's interest is automatically changed by the sensory manager using the priority values. Still, it can be reset when needed, using the public function Notice
.
There is room for improvement still, depending on our game. The scout lists can overlap with each other, and it's up to us and our game to handle this scenario the best way we can. However, the system we built takes advantage of the priority values to make decisions.