Frank Moricz


Web Portfolio and Art Gallery

Magic made with caffeine and a keyboard.

Megabite #15 - Creating a More Advanced AI

In episode #10, I covered my first attempt at creating an AI system on a 2d plane.  Now, just 5 episodes later, I'm happy to report that I've been extremely happy with the advancement through the weeks and have adapted the original code to something more universal and configurable.  Infact, the same code is used for any of my AI enemies in the development of Stellar.  It's set up so that customization takes place within the inspector, and ship models are basically plug and play - I import, size them, make em into a prefab, and just attach my script.  Easy peasy.

That said, such a script comes with many variables to maintain:

var reactionDist : float = 10;
var strafeSpeed : float = 25;
var ftReacDist : float = 10;
var rotSpeed : float = 100;
var baseSpeed : int = 25;
var fireAtEnergy : int = 75;
var explosion : Transform;
var bullet : Transform;
var firefrom : Transform;
var damping = 30.0;
var smooth = true;
var canBounce : boolean = true;
var maxHealth : int = 100;
var fireRate : float = 2;
var fireCost : int = 30;
var recharge : float = 0.1;
var targetTag : String = "Player";
var targetRange : int = 100;
var canMine : boolean = false;
var kamikaze : boolean = false;
var thisIsABoss : boolean = false;
var tripleShot : boolean = false;
var tripleSpread : float = 7;
var willMine : boolean = false;
var retribution : boolean = true;
private var target : Transform;
private var front : boolean = false;
private var left : boolean = false;
private var right : boolean = false;
private var farRight : boolean = false;
private var farLeft : boolean = false;
private var lastRand : float = 0;
private var ranDir : int = 0;
private var health : float = 100;
private var playerDist : float;
private var lastfire : float = 0;
private var player : GameObject;
private var playerArr : GameObject[];
private var lastCheck : float = 0;
private var lastPorted : float;

While it may seem complex, I basically replaced any of the numbers or specifics within the script with a variable.  In doing, every little thing can be changed until the movement, reaction time, attack ability, etc. all meet my expectations.

For this article, I'll cover some of the major functions and explain how they work without errors.  Nothing is ever "perfect", s this may not be the final product, but it works perfectly for what I need right now.

First, let's look at how an enemy selects a target:

function findPlayer() {
 if (Time.time > lastCheck) {
  lastCheck = Time.time + 3;
  if (targetTag == "Player") {
  player = GameObject.FindGameObjectWithTag(targetTag);
  } else {
  playerArr = GameObject.FindGameObjectsWithTag(targetTag);
  var rnd = Random.Range(0, (playerArr.Length));
  player = playerArr[rnd];
  }
 var enemies : Collider[] = Physics.OverlapSphere(transform.position, 100);
 for (each in enemies) {
  if (each.transform.tag == "decoy") {
  player = each.transform.gameObject;
  }
 }
 }
}

What I've done here is allowed myself to change what the primary target of an enemy is.  In my game, enemies may simply ignore the player and go after another tag-based object, such as a player's structures.  What we see above will target a player if the inspector has it labelled as such, but we can use any tag we choose otherwise.  At the bottom, you can see that each time the script is executed, an enemy will search within 100 units for a unit with the tag of "decoy" as well.  If this is true, the target first becomes the decoy game object.

This next chunk of code is found within the "Update" section, and is executed every frame:

var inft : RaycastHit;
 var didhit = Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), inft, 100, 1 << 8 | 1 << 1 | 1 << 2 | 1 << 4 | 1 << 0);
 var goodShot : boolean = false;
 if (didhit) {
 if (inft.transform.gameObject.tag == "Player" || inft.transform.gameObject.tag == "playerStruct" || inft.transform.gameObject.tag == "decoy") {
 goodShot = true;
 }
 if (canMine == true && inft.transform.gameObject.tag == "genBlock") {
 goodShot = true;
 }
 }
 if ((playerDist < targetRange) && goodShot == true && (health > fireAtEnergy) && (Time.time > lastfire)) {
 lastfire = (Time.time + fireRate);
 health = health - fireCost;
 var fired = Instantiate (bullet, firefrom.transform.position, Quaternion.identity);
 Physics.IgnoreCollision(fired.collider, collider);
 fired.transform.parent = transform;

Im a big fan of the way this one worked out.  In the previous code, an enemy would fire whenever a "Player" was within range - whether or not they were actually aimed at the player.  Of course, we could have done a transform.LookAt, but that would have been unnatural and would have interfered with the way the ship navigated the terrain.  In this instance, we are telling the ship only to fire if the line of sight actually comes from the front of the ship and lands on an object with the labels of "Player", "playerStruct", or "decoy".  In doing, we've also solved the problem about firing through walls to get at a target.

Within OnColisionEnter(), we have:

if (transform.tag == "decoy") {
  if (collision.transform.gameObject.tag == "enemybullet") {
  health = health - 75;
  }
 }

Decoy?  That's right - even though decoy is a target, the same AI code controls the movement of a decoy ship!  This saves us from having to create a whole separate script, and everything is controlled by whether or not the ship has a "decoy" tag on it.

Now, for all intents and purposes, the AI is functional and has no real issue with navigation.  When we play through, we just take a look at various what-if scenarios and go from there.  This is where the idea of "retribution" came from, and this simple little piece of code causes a ship that would normally only attack buildings to turn and attack a player if fired upon:

if (retribution) {
 if (GameObject.FindGameObjectWithTag("Player")) {
 player = GameObject.FindGameObjectWithTag("Player");
 target = player.transform;
 }
 }

Retribution is a toggled ability in the inspector as well, so I can choose (like everything else), if this is something that the enemy naturally does.  By building the script in this way, the AI has become much more robust than before, and each enemy type seems to have a personality of its own.

Building an AI with Unity has been a lot of fun, and I really can't wait until things for this title are more finalized.  When that happens, I'll see if it will be possible to release the AI code in its entirety for all to see.  For now, if anyone has questions, feel free to drop me a comment and I'll get back to you as soon as possible!