Megabite #10 - Creating My First AI System

Over this holiday break, I was able to do something exciting - create a working AI for a top-down game I've been working on.  While it isn't perfect, I'll share with you the steps and the code that I used in order to get the functionality that I wanted.

First, allow me to explain how this game works because AI is going to be very specific to the type of game you want to create for yourself, and there won't really ever be any plug-and-play solution to paste into your title.  My newest game, being top-down, ignores the Y plane.  the Z plane makes my little spaceships go up/down, and the X plane is left/right.  Because of this simplicity, I was better able to grasp what needed to be done.  The ships are all individual rigidbodies that will be able to bounce from walls, rotate, strafe, fire bullets at players, and determine their own path.  (an example of the AI in action can be found here).

Before we can do something such as having the enemy turn attention to a player, I wanted first to focus on making sure the ships can do basic pathfinding on their own.  Each of them has a Constant Force attribute that propels them forward indefinitely, but how do we determine if we're coming up to a wall?  In this example, we're going to use a couple of awesome Unity features: Physics.Raycast and Debug.DrawRay.

First, Debug.DrawRay is a feature for you, not the player.  What it allows you to do is see a line drawn in the "scene" window while the game is running.  In the example here, you can see the green line that the ship shoots forward.  This is a cool feature that will show you how your other scripts are working together if you set it up correctly.  In the Update() section of a javascript file attached to this ship, you will see:

Debug.DrawRay(transform.position, transform.TransformDirection(Vector3.forward) * reactionDist, Color.green);

It's a fairly simple piece of script.  We want to send a line from the position of the ship forward for a distance that we've set as a variable called reactionDist (25), and make the color green.  We want to do this because the actual physics detector, Physics.Raycast, will be invisible to us.  On that note, let's add the raycast now and I'll explain what happens there.

if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), reactionDist)) {
front = true;
} else {
front = false;
}

So now we're sending an invisible line forward.  When it hits a collider, we have a boolean variable that comes back true, but otherwise it stays false.  Also, because we've used a variable for reactionDist, we can customize this quite easily in the inspector.  We can actually set up as many of these types of lines as we want (at the expense of processing power, so don't go absolutely nuts :P ), and have a multitude of reaction input to go off of.  For my purpose, I know the ships will always be moving forward, so all I wanted was the "front" direction, two 45 degree angles forward, and the two sides.  It came out something like this:

I also made the front/side lines a bit longer than the 45 degree ones which "rounded out" my feelers.  The code for all of this is here:

//Determine colliders within reactionDist
if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), reactionDist + ftReacDist)) {
front = true;
} else {
front = false;
}

if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3(1,0,1)), reactionDist)) {
right = true;
} else {
right = false;
}

if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3(-1,0,1)), reactionDist)) {
left = true;
} else {
left = false;
}

if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3(-1,0,0)), reactionDist + ftReacDist)) {
farLeft = true;
} else {
farLeft = false;
}

if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3(1,0,0)), reactionDist + ftReacDist)) {
farRight = true;
} else {
farRight = false;
}

//Draw rays
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3.forward) * (reactionDist + ftReacDist), Color.green);
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3(1,0,1)) * reactionDist, Color.green);
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3(-1,0,1)) * reactionDist, Color.green);
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3(-1,0,0)) * (reactionDist + ftReacDist), Color.yellow);
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3(1,0,0)) * (reactionDist + ftReacDist), Color.yellow);

Again, I put all of this into the Update() section of the script so that I knew it would execute every frame.  I made the side lines yellow because I knew the strafe code would be a bit different than the code needed to just turn the ship, but other than cosmetically it didn't change anything at this point.  On the next page, I'll show you how to use the information gathered here and actually perform the movement calculations.
Here is the script that I used to determine movement at this point:

//rotation conditions
if ((front == true) && (right == false) && (left == false)) {  //head-on without corners
   if (Time.time > lastRand) {
	lastRand = Time.time + 2;
	ranDir = Random.Range(0,2);
   }
	if (ranDir == 1) {
		transform.Rotate(transform.TransformDirection(Vector3.up) * rotSpeed * Time.deltaTime, Space.World);
	}
	if (ranDir == 0) {
		transform.Rotate(transform.TransformDirection(Vector3.down) * rotSpeed * Time.deltaTime, Space.World);
	}
}
if ((front == true) && (right == true) && (left == false)) {  //head-on & right side
	transform.Rotate(transform.TransformDirection(Vector3.down) * rotSpeed * Time.deltaTime);
}
if ((front == true) && (right == false) && (left == true)) {  //head-on & left side
	transform.Rotate(transform.TransformDirection(Vector3.up) * rotSpeed * Time.deltaTime);
}
if ((front == false) && (right == true) && (left == false)) {  //right side proximity
	transform.Rotate(transform.TransformDirection(Vector3.down) * rotSpeed * Time.deltaTime);
}
if ((front == false) && (right == false) && (left == true)) {  //left side proximity
	transform.Rotate(transform.TransformDirection(Vector3.up) * rotSpeed * Time.deltaTime);
}
if ((front == true) && (right == true) && (left == true)) {  //wall-stuck
	//transform.Rotate(transform.TransformDirection(Vector3.down) * rotSpeed * Time.deltaTime);
	constantForce.relativeForce.z = -150;
} else {
	constantForce.relativeForce.z = baseSpeed;
	}

if ((farRight == true) && (farLeft == false)) {  //far-right
	constantForce.relativeForce.x = -strafeSpeed;
} else {
	constantForce.relativeForce.x = 0;
}

if ((farRight == false) && (farLeft == true)) {  //far-left
	constantForce.relativeForce.x = strafeSpeed;
} else {
	constantForce.relativeForce.x = 0;
}

This code, at first glance, might seem extremely complicated.  In truth, I simply did it a single section at a time and watched how the calculations affected my baddies.  To explain how it works, the ship is constantly checking the variables we created before.  If the "front" line hits a wall, based on the code here we have it pick a random direction to determine if it will go left or right.  Obviously it doesn't know the lay of the land, and it might be getting itself into a pickle, but that's where other sections come in.  There is code there for being stuck against a wall, code for being too close to a wall (which keeps the ships from dragging themselves against a wall while moving forward).

Another thing I should point out is that the way we called Physics.Raycast makes it so that any collider we hit returns true.  This includes the colliders of other ships, be it Players or baddies.  While it's possible to put players or enemies into separate layers and ignore them or focus on them, to keep them together worked very well for my purpose and further helped to randomize the ship movement.

In a previous article, we've already discussed how to make a bullet projectile, so I won't go in-depth on that here.  Suffice it to say that we need a rigid body bullet to attach into the inspector for the next section because we want our ships to be able to actually attack a player.  The only necessity is that our player is given the tag of "Player" for the next section of code.  Also, I created an empty, invisible gameobject and attached it to my baddie to be used as a bullet spawn point called "firefrom".  The transform of that object is also attached in the Unity inspector.

	//select single-player, judge distance
	var player = GameObject.FindGameObjectWithTag("Player");
	if (player) {
	playerDist = Vector3.Distance(transform.position, player.transform.position);
	//target player
	if ((playerDist < 100) && (Physics.Raycast(transform.position, player.transform.position, 100))) {
	target = player.transform;

	} else {
	target = null;
	}

	//shoot at player
	if ((playerDist < 100) && (Physics.Raycast(transform.position, player.transform.position, 100)) && (health > fireAtEnergy) && (Time.time > lastfire)) {
	lastfire = (Time.time + 2);
	var fired = Instantiate (bullet, firefrom.transform.position, Quaternion.identity);
	fired.transform.parent = transform;
	}
	}

What this section of code does is detect if there are any players.  If there are, the ship will determine how far away they are.  I set a minimum of 100 units into the script, so nothing happens otherwise if they are too far away.  However, if they are within that 100 units, the baddie will declare them as "target" and fire.  I also set the ability to fire for only once every 2 seconds to prevent the ships from firing a bullet every single frame.  You'll notice that the bullet is also attached to the baddie as a child, so be sure to throw a transform.parent = null into your bullet script after the bullet is fired - this was the easiest way to be sure the bullet was given the same rotation and vector as the ship it was fired from before it flew off.

While this is all well and good, it doesn't work well unless our ship actually aims at the player.  Based on the above code, the ship could be simply flying by and our baddie would just fire willy-nilly.  So lastly, we want to slowly turn toward a player if they are close enough.

if (target) {
  // Look at and dampen the rotation
     var rotation = Quaternion.LookRotation(target.position - transform.position);
     transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.deltaTime * damping);
  }

Now we're cooking.  If a player comes within that 100 units from above, this code goes to work and starts slowly turning our ship toward them.  Because of all of the pieces of code working together, our ships won't act completely stupid and get stuck against walls or anything in the process.  At the same time, they are able to aim and actually cause panic in a player.  By altering the "damping" variable, you can slow down or increase the speed at which a baddie aims at a player.

Honestly, I had to "dumb-down" the AI by altering the variables to my liking.  My original code was just mean, and it was nearly impossible to elude the enemies once they were onto you.  They began to swarm and work together, and it was an interesting experience.  While I know this code is not universal, I hope it helped in the making of your own game.  My current project has been a lot of fun to work on, and I'm really looking forward to the opportunity to show it off soon enough.

If you are struggling in trying to make an AI yourself, drop me a comment here and I'll see if theres any help I can provide.  Im far from mastery of Unity, but practice every day has me seeing things that had escaped me before.   until next week, happy programming!

Here's a link to the complete javascript file.  From it, I hope you are able to alter it for your own needs.