How to make Syndicate-style civilian AI without horrible slowdown?

I’m currently using Unity standard to make a game with wandering civilians who can be infected by the player (red) and will then identify civilians on the blue team and shoot at them.

[Syndicate civilian-Persuadertron interaction example here][1]

I’m using the navmesh to move the unaffected civilian/humans to randomly selected positions in an array of transforms - and so far, so smooth.

The problem is when different populations of newly infected humans start to interact with each other. To identify an enemy, the enemy has to be within an infected human’s vision sphere (collision sphere trigger), has to be within a field of view angle and there has to be an uninterrupted line of raycast between them (and their infection state has to be non-clean and not the infection state of the infected human) - at which point, they can fire at each other!

But while running a level with 9 or so civilians wandering about (and occasionally entering infection zones), my wimpy laptop starts stuttering, movement slows to a crawl, etc.

I’m using a lot of physics, I’m using a lot of hit.getComponent, so I imagine that this is where my code is getting bogged down. But I’m trying to accomplish nothing more than Syndicate from 1993 with far fewer characters (and indeed, Satellite Reign is doing similar) … so where do I find the performance boosts to boost my performance?

Vision code follows…

using UnityEngine;
using System.Collections;

public class HumanVisionAttack : MonoBehaviour {

	public 	Color 			debugRayColor 			= Color.green;
	[SerializeField]
	private	float 			fieldOfViewAngle 		= 110f;		// Number of degrees, centred on forward, for the enemy see.
	private	float			halfFieldOfViewAngle;
	private const float		eyeHeight				= 1.60f;
	[SerializeField]
	private	LayerMask		humanLayerMask; 					// So vision raycast only detects humans, set in inspector for your pleasure!

	private HumanInfection	myInfection;

	private	bool			hasTarget				= false;

	// Use this for initialization
	void Start () {
	
		myInfection				= GetComponent<HumanInfection>();
		halfFieldOfViewAngle	= fieldOfViewAngle * 0.5f;

	}
	
	// Update is called once per frame
	void Update () {
	
	}

	void OnTriggerStay (Collider other){
		
		if(other.CompareTag("human") && myInfection.infectionState != HumanInfection.InfectionState.Clean){
			
			RaycastHit 	hit;
			Vector3 	humanToTarget 	= other.transform.position - transform.position;
			Vector3 	eyePos	 		= new Vector3 (transform.position.x, transform.position.y + eyeHeight, transform.position.z);
			
			// Create a vector from the enemy to the player and store the angle between it and forward.
			float enemyRelativeAngle	= Vector3.Angle(humanToTarget, transform.forward);
			
			// If the angle between forward and where the player is, is less than half the angle of view...
			if(IsWithinFieldOfView(enemyRelativeAngle))	{
				if(Physics.Raycast(eyePos, humanToTarget, out hit, humanLayerMask) && hit.transform.CompareTag("human")){
					HumanInfection.InfectionState otherInfection = hit.transform.GetComponent<HumanInfection>().infectionState;
					if(otherInfection != myInfection.infectionState && otherInfection != HumanInfection.InfectionState.Clean){ 
						// Debug.DrawLine(eyePos, hit.point, debugRayColor);
// Shooting stuff goes here
					} 
				}
			}
		}
	}


	bool IsWithinFieldOfView (float enemyRelativeAngle) {
		return enemyRelativeAngle < halfFieldOfViewAngle;
	}
}

Here’s hoping somebody out there has some solutions or suggestions to speed this sucker up!

–Rev

EDIT: Hope this doesn’t count as necro’ing an old thread (10 days old?), but I’ve spent some time smacking together a solution to this problem, taking advice from the gents below. I hope it’s useful for somebody else.

HumanVision’s InspectNearbyHumans method is now triggered by a Coroutine which sweeps through an object pool of humans every .25 seconds, selecting 3-4 humans to test. The HumanVision script now reads as:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class HumanVision : MonoBehaviour {

	public 	Color 			debugRayColor 			= Color.green;
	[SerializeField]
	private	float 			fieldOfViewAngle 		= 110f;		// Number of degrees, centred on forward, for the enemy viewcone.
	private	float			halfFieldOfViewAngle;
	private const float		eyeHeight				= 1.60f;
	[SerializeField]
	private	LayerMask		humanLayerMask; 					// So vision raycast only detects humans, set in inspector for your pleasure!

	private HumanInfection	myInfection;
	public	List<HumanInfection>	nearbyHumans	= new List<HumanInfection>();

	private	HumanAttack		humanAttack;

	void Awake () {
		myInfection				= GetComponent<HumanInfection>();
		humanAttack				= GetComponent<HumanAttack>();
		halfFieldOfViewAngle	= fieldOfViewAngle * 0.5f;
	}

	void OnTriggerEnter (Collider other){
		if(other.CompareTag("human")){
			nearbyHumans.Add(other.GetComponent<HumanInfection>());
		}
	}

	void OnTriggerExit (Collider other){
		if(other.CompareTag("human")){
			nearbyHumans.Remove (other.GetComponent<HumanInfection>()); // Really unsure if this is removing the correct corresponding HumanInfection
		}
	}

	public void InspectNearbyHumans() {
		// Debug.Log ("Running TestVision");
		if(humanAttack.armed == HumanAttack.Armed.unarmed){
			return;
		}else {
			for(int i = 0; i < nearbyHumans.Count; i++){
					
				RaycastHit 	hit;
				Vector3 	humanToTarget 	= nearbyHumans*.transform.position - transform.position;*
  •  		Vector3 	eyePos	 		= new Vector3 (transform.position.x, transform.position.y + eyeHeight, transform.position.z);*
    
  •  		// Create a vector from the enemy to the player and store the angle between it and forward.*
    
  •  		float enemyRelativeAngle	= Vector3.Angle(humanToTarget, transform.forward);*
    
  •  		// If the angle between forward and where the player is, is less than half the angle of view...*
    
  •  		if(IsWithinFieldOfView(enemyRelativeAngle))	{*
    
  •  			if(Physics.Raycast(eyePos, humanToTarget, out hit, humanLayerMask) && hit.transform.CompareTag("human")){*
    
  •  				HumanInfection.InfectionState otherInfection = hit.transform.GetComponent<HumanInfection>().infectionState;*
    
  •  				if(otherInfection != myInfection.infectionState && otherInfection != HumanInfection.InfectionState.Clean){*
    
  •  					humanAttack.Shoot (eyePos, hit.point, hit.transform.GetComponent<HumanHealth>());*
    
  •  				}* 
    
  •  			}*
    
  •  		}*
    
  •  	}*
    
  •  }*
    
  • }*

_ /* Stick an else in here which if THIS human is clean and the human in view is NOT && is !UNARMED,_
_ * then setdirection to transform.position - other.transform.position normalised * run distance (evac zone? Cower state?)_
_ */_

Okay, that code is formatted like crazy, you should really look into that. But to answer your question:

Using collision for the detection is probably a really bad idea. With OnTriggerStay, your code runs every frame for every thing the vision “cone” is intersecting with, which will give a huge overhead of unnecessary steps. Both collider cones and to some degree overlap spheres gives off a big extra cost where they pick up all of the geometry in your scene.

It’s not intuitively obvious, but it’s often a lot faster to just have a list of all of the things you’re supposed to care about, and then loop through that list every so often, checking distances between everything you need to check. Simply add all of your infected to one list, and all of the enemies to another. Then, you find the distances between all of the pairs from the two lists, and if an infected and an enemy is close enough, do the angle check, then do the raycast. The raycast is the most expensive part of this, and you can kill it if you have simple geometry, but it’ll probably not be necessary.

Now, if you have a CS background, you’ll notice that this is an n^2 algorithm, and that sounds bad. But, trust me, the OnTriggerStay is probably a lot more expensive than some Vector3.SqrDistance calls (oh yeah, use SqrDistance for this stuff, it’s faster than Distance). In addition, you don’t have to to the distance checks every frame - if you put them in a coroutine that runs every .5 seconds or so, that’ll still give you almost the same behavior, but for a lot cheaper.

If you’re still getting slowdowns, you can optimize further by only updating some of the distances every time you run the checks. That’ll make your guys not move in lockstep either, which might actually improve the “realism” of their behavior. Don’t do any optimization before you know that it’s actually necessary.

You are doing everything multiple times every frame. That’s crazy. Only process what you need to. Start with killing OnTriggerStay. This is a bad function and should almost never be used.

Here is a rough step through of how I would code it.

  • Use OnTriggerEnter to add each enemy to a List. This means you only ever need to detect an enemy once. (If its possible that enemies might escape then take them off the list with OnTriggerExit.)
  • Store the appropriate components you need in the list, instead of the GameObject. This way you only call GetComponent once.
  • Sort the list so you can see which one is closest.
  • Check if the first item on the list is in field of view. If it is then check if that one is unobstructed, if not bail out early.
  • As soon as you find a valid target then stop checking the rest of your targets. (Assuming your zombie can only fire in one direction at a time).
  • Take some action (like shooting, chasing, following ect).
  • Don’t start checking targets again until you have finished with the action on the first one.

There are two general principles to remember here

  1. Don’t do processing you don’t need to use
  2. Start with the cheapest conditional, and work your way up