- Home /
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
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[i].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?)
*/
bool IsWithinFieldOfView (float enemyRelativeAngle) {
return enemyRelativeAngle < halfFieldOfViewAngle;
}
}
The horrible, unfinished, janky, awful outcome of this is https://dl.dropboxusercontent.com/u/53322618/230115%2000.56/230115%2000.56.html Two player split-screen - pick a cat and get infecting humans with Toxoplasmosis!
As I'm going back to rewrite my function, I'm torn between using InvokeRepeating or Coroutines waitforseconds to reduce the amount of vision checks. Any suggestions?
$$anonymous$$eanwhile: dinner.
InvokeRepeating is generally a teeny bit faster, and also easier to write than coroutines. However it's more limited in function and flexibility. (You can't, for example, pass arguments to a function called by InvokeRepeating).
Coroutines are the nuts (coll. = "A good thing") when you get used to them - they're very powerful and flexible, but can be a bit tricky to get your head around.
Either one is miles better than trying to shove everything in Update() or OnTriggerStay() though :)
Coroutines, then. I need the practice. Thanks, tanoshimi.
Waaaaat?! Shoving everything into Update or OnTriggerStay is GRRREAT. You crazy. =P
Answer by Kiwasi · Jan 12, 2015 at 03:24 AM
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
Don't do processing you don't need to use
Start with the cheapest conditional, and work your way up
Cheers! Delighted to get this much thoughtful feedback.
I am pretty crazy, but you're right that doing everything multiple times per frame is unreasonable.
The note on removing OnTriggerStay and replacing it with OnTriggerEnter, OnTriggerExit and a Generic List sounds great. $$anonymous$$y only worry is whether this'll cause a lot of overhead with Generic Lists (one per human, 32 humans per scene) being constantly added to and removed from. Probably much, much less overhead than my previous physics system...
Will skip the GameObject, check.
Not sure if I understand the reasoning behind checking to see if the closest human is in the fov, and if not, bailing out early. Couldn't this leave a potential target dead center in front of the 'zombie', but ignored because there's a closer target, but outside of the fov?
Point taken on the early out via finding a valid target in the list. GREAT suggestion, will take on board. I recognise that without sorting the list for distance this could lead to slightly odd behavior, but I'm unsure if this is a good argument for the distance test. Will test.
Solid point on making 'zombie' unavailable for test if taking action - thanks!
Thanks for the advice on processing. I've been using Unity for a while, but I'm still a rank amateur - really dig when I can get wisdom from old hands!
Answer by Baste · Jan 12, 2015 at 12:56 AM
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.
Woot! Some excellent advice!
Code formatting an artifact of using the answers.unity3D Code Sample feature. Will look into trying to straighten the example above out later.
Agree with at lot of your points, Baste, but still unsure whether it makes sense to use SqrDistance/Distance over Triggers to check if target is in radius. $$anonymous$$y vision system is derived from an official Unity tutorial, with the reasoning for using PhysX over any distance system explained here.
Definitely will be using Generic Lists and iterating through 'em for my next version of this system, combining your suggestion with one from Bored $$anonymous$$ormon. $$anonymous$$any thanks!
CS!?!?! You flatter me. =) I'm self-taught and clumsily fumbling about.
GREAT idea about only updating some units per check. Will try to incorporate this.
"Don't do any optimization before you know that it's actually necessary." And here we are! ^-^
$$anonymous$$any thanks, update in the pipe.
On trigger boxes vs. checking distance, two points:
1: As the post said, make sure that you're not moving colliders without rigidbodies on them. Ever. Unity is optimized with the assumption that you never move those colliders, and when you do, the entire scene has to do some recalculations. They'll fix that in Unity 5 since it's causing so much problems (it's simply not intuitive that you have to add a rigidbody to things that are not going to use physics), but for now, just add a kinematic rigidbody to any detection if you haven't done so already.
2: It's true that using trigger boxes is faster than using distance checks if you're doing the distance checks every frame. The big downside with triggers is that you can't tell them to only do checks every now and again, so even if you don't need to detect collision on every frame, that's what you're going to get.
For something like a trigger zone that's supposed to react instantly as you walk into it, definitely use OnTriggerEnters. I'd also generally use triggers as they're easy to use, until you get to a point where you're noticing that your game is slowing down, and way too much collision detection is the likely culprit. That being said, my first answer should have been "do your vision cones have rigidbodies?", because if they don't have that, you're guaranteed to get a slow down.