Go to Top

An introduction to coding Artificial Intelligence a subject that can be as frustrating as it is enthralling.

Space Shooter AI

The link for the project file is working as normal again.

In this part of the tutorial, we develop the basic of a simple space shooter.We start with a 2D environment to learn our AI. Our game will use the basic right scrolling gameplay, meaning our ship goes to the right while the enemies come from right to left. Our point is to try and give some life to our enemy ships.

We will be using trigonometry principles, basic of OOP (inheritance, polymorphism,...) as well as some vector algebra. If you are familiar with those, that will not be a problem, if you are not, those are quickly explained in this article. It is up to you to go and look for more information as we cannot fit them all in here...

The project can be downloaded here. You can either use the components from the project or create your own components as you read on. Make sure you give different names or delete the original ones as it would create conflict. Also, keep in mind to remove previous scripts when adding a new one. We will consider you are using the provided components though.

Here is what you are about to see:

Creating our ship

First we will create our own ship. This is not AI since we create code for the human-player to use. The AI part comes right after.  So if you downloaded the project, you should have a SpaceShip object. It already has everything it needs and most information you need to know are written as comments on the script.

Now we need to create the Non-Player-Character (NPC) or in our case, Non-Player-Ship. Our first task is to instantiate a random amount of ships at random location. In the SpawnObject folder is the script below, it it already added to the Spawn object of the Hierarchy. You can simply tick it on.

using UnityEngine;
using System.Collections;

public class CreateRandom : MonoBehaviour {
    public GameObject ship;
    Transform _transform; 
    void Start () {
         _transform= GetComponent<Transform>();
         InvokeRepeating("CreateRandomShip",0.01f,2.0f);
    }

    void CreateRandomShip(){
        Vector3 vec= new Vector3(_transform.position.x,0,Random.Range (-1f,-9f));
        Instantiate(ship,vec,transform.rotation);
    }
}

All magic lie in the Vector3 vec =new Vector3(22,0,Random.Range(-1f,-9f)); Now what does that do? It creates a vector3 with x = 22 which is just outside our screen so that our enemies do not pop up in the middle, y = 0 because we are in 2D and y is not used here. Finally, we use a random number clamped within a range to define where the enemy gets created. This is the command that is responsible for the messy appearance of our ships.

Most of the values given in the tutorial will have to be redefined for your game depending on the original position of your plane.

Our project has a Spawn object which is a simple empty game object. Attach the above script to it. The CreateRandomShip is called with InvokeRepeating each 2 second.

Moving our ship

Now for the second part, creating the ships is fine but they also need to move. That is where OOP kicks in. We are about to declare a class that will stand for the basic behavior of our ships. Should we want to get the ships to do something more specific, we just declare a new class that inherits from our base class (also called parent class). The derived class (also called child class) will specify what behaviors should be modified and what behaviors should be used as such.

Below is our base class Enemy. We will get back to it later on to add some more basic actions. Add this to the Enemy prefab in the Project Panel.

Make sure to follow when we say to add to the object in the Hierarchy or the prefab in the Project Panel
using UnityEngine;
using System.Collections;

public class Enemy : MonoBehaviour {

	protected Vector3 velocity;
	public Transform _transform;
	public virtual void Start () {
                _transform = GetComponent<Transform>();
                velocity = new Vector3(Random.Range(0.5f,1.5f),0,0);
	}
        public virtual void Update(){
              _transform.Translate ( -velocity.x*Time.deltaTime,0,velocity.z*Time.deltaTime,Space.World);
        }

       void OnTriggerEnter(Collider other){
		if(other.gameObject.tag == "EnemyDeath")Destroy(gameObject);
	}
}

This will stand for our base class. Any enemy class will inherit from this one so any member we want to be universal to all enemy should be added here. That is why most variables are declared protected or public so far.

Notice that most functions are declared virtual as we will probably override them later in our derived class. Note also that the Trigger function is not virtual. We want all ships to be destroyed  when getting out of the screen (there are trigger boxes on each side of the plane in our project). This is a common behavior for all of our ships. We won't have to rewrite that later on.

velocity =new Vector3(Random.Range(0.5f,3.0f),0,0); again we assign a random value for the velocity so that all ships won't move at the same speed.

We now have a simple first level. You can use the same principle with rocks and we get ship and asteroids.

RandomShip

Our ships are moving but they do not do much more, so we want them to shoot.

You could also use InvokeRepeating to modify the z velocity of our ships. Then, they would not always go straight left but a little up and sometimes down:

public virtual void Start(){
    InvokeRepeating("ChangeZ",2.0f,1.0f);
}
public virtual void Update(){
     _transform.Translate( -velocity.x*Time.deltaTime,0,
         velocity.z*Time.deltaTime,Space.World);
}

void ChangeZ(){
    velocity.z = Random.Range(-1.0f,1.0f);
}

As you can see that creates quite of a mess but that could also be what you are looking for.

Shooting ship

Since our ships have two cannons, we are about to give them the ability to "reload" before shooting again. Well, in fact we just alternate the shooting between the left and right cannons. The ship has two empty game objects as children in front of the cannons that we will use for our shooting. The below code is added to our base class:

Transform _primaryShoot;
Transform _secondaryShoot;
bool side = true; 
public GameObject proj; 

protected void Shoot(){
    if(_primaryShoot){
        if(_secondaryShoot){
            if(side)
                Instantiate (proj,_primaryShoot.position,Quaternion.identity);
            else
                Instantiate (proj,_secondaryShoot.position,Quaternion.identity);
             side = !side;
        }else
             Instantiate (proj,_primaryShoot.position,Quaternion.identity);
   }
}
void OnCollisionEnter(Collision other){
      if(other.gameObject.tag == "Ship"){
           Destroy (gameObject);
           other.gameObject.SendMessage("DecreaseHealth",15);
      }
}

Take a quick look at the Shoot function. The basic here is that enemies might have one, two weapons or none. We could think that if there is no weapon why trying to shoot, you are never free from mistake...You could actually add an else statement at the bottom that would be printing out a warning like "You are shooting but this object has no weapon". Without the first if check, you would crash your game.

Now we need to add our first basic enemy class. In the EnemyScript folder, find the BasicEnemy and add it to the Enemy prefab. You can actually deactivate or remove the Enemy component. Since BasicEnemy is a child of Enemy, Unity will automatically add all thepubic and protected members of the parent class:

using UnityEngine;
using System.Collections;

public class BasicEnemy : Enemy {

	public override void Start () {
		base.Start();
		_primaryShoot = transform.Find ("SpawnLeft").transform;
		_secondaryShoot = transform.Find ("SpawnRight").transform;	
               InvokeRepeating("Shoot",Random.Range(2.0f,4.0f),Random.Range(2.0f,4.0f));
	}

	public override void Update () {
		base.Update ();
	}
}

We override the Start function but as some members are assigned in the parent Start function we call it with base.Start(); Next, we find our two weapons and start invoking the Shoot function. You might be wondering why I do not call base.Shoot(), Shoot is protected in my base class so it is inherited. You use base when you want to reach a member in the parent class that is overridden in the derived class.

Et voilà. They are ready to shoot, you just need to drag and drop the ProjectileEnemy prefab. You will find tit in the Projectile folder under ProjectileEnemy (it is just a colorful sphere). I also added a collision function in case the player hits the ship. Keep in mind that dying is also a kind of AI.

If you are using the projectile from the project it already has the script attached with this code:

using UnityEngine;
using System.Collections;

public class ProjEnemy : MonoBehaviour {

	Transform _transform;
	void Start () {
		_transform = GetComponent<Transform>();
	}

	void Update () {
		_transform.Translate ( speed*Time.deltaTime,0,0,Space.World); 
	}
	void OnCollisionEnter(Collision other){
		if(other.gameObject.tag == "Ship"){
			other.gameObject.GetComponent<ShipControl>().DecreaseHealth(10);
			Destroy(gameObject);
		}
	}
	void OnTriggerEnter(Collider other){
		if(other.gameObject.tag == "EnemyDeath")Destroy(gameObject);
	}
}

Nothing fancy here again. The collision function sends a message to the ship for decreasing the health. Note that if you would create later on some more advanced projectile, you could also use inheritance.

Giving some fancy pattern with trigonometry

Let’s consider a second type of really basic AI movement. We are now about to enter the realm of oscillations and waves. What you are about to see here is simply the basic equation for the principle of the universe. Yeah I know that sounds a little pushy but I did not do anything about it.

So using simple function like cosine or sine, we can easily get some nice shapes.

Whether you use sin or cos does not really matter somehow. You will get the same shape but one will start at 0 as sin(0) = 0 and the other starts at 1 as cos(0) = 1. Again, I did not decide that, the universe did.
One nice feature of sin and cos is that the result is lerping and clamped between -1 and 1. We can get a variable that will constantly go from 1 to -1 and back to 1 and so on.

So we have our base class and we are about to declare a new class that will inherit from our base class. First off, you can remove the script Enemy from our EnemyShip object and create a new one.

Everything that is not private in the Enemy class is accessible in EnemyShip, if it is not private it is either public or protected.The word private was mentioned, the other choices are public and protected (there is also internal but let's leave it aside for the moment). Public is accessible by any object anywhere (See the GetComponent tutorial), private is only seen within the file/class it is declared,a child class does not inherit private members from the base class. Protected is similar to private but the child will inherit the protected members. 

Let's declare a new class and create a pattern for the movement of our ship.The code below will simply get the enemy ship from right to left across the screen in an oscillating pattern:

using UnityEngine;
using System.Collections;

public class ShipOsc : Enemy {

    float amplitude = 2;
    float omega = 0.5f;
    float index;
    float zPos;

    public override void Start(){
        base.Start ();
        _transform.position = new Vector3(22f,0f,-5f);    
        _primaryShoot = transform.Find ("SpawnLeft").transform;
        _secondaryShoot = transform.Find ("SpawnRight").transform;
        zPos=_transform.position.z;
        InvokeRepeating("Shoot",Random.Range(2.0f,4.0f),Random.Range(2.0f,4.0f));
    }

    public override void Update(){
        index += Time.deltaTime;
        float zPos=amplitude*(Mathf.Cos(omega*index))*Time.deltaTime;
        _transform.Translate ( -velocity.x*Time.deltaTime,0,zPos,Space.World);
    }
}
Here is the basic cos/sin function:  Amplitude*Mathf.Cos(omega*Time.time+phase);

A quick explanation on the equation, Amplitude will affect the size of the wave, omega affects the frequency of the wave, so how fast from one peak to another, time is required to modify the value. The phase stands for phase shift. The value in radians will shift the starting point of the wave of an equivalent value. We will see an  example later on.

This class of enemies also has the ability to shoot.

The picture below shows that the position is depending on the time, if all ships have the same time given, they all have the same position. The index starts incrementing when the ship is created so all ships will have a different value which solves our issue.

Sin

With that simple code you gave a little bit of life to your enemy.

Et voilà (again)!!! Our ships seems to be following each other in a nice pattern .

OscShip
OscShip2

See on the first picture, the value given to omega is greater than in the second picture. As a result, the ship wave is shrunk. If you give a smaller value to amplitude then the wave will cover a smaller distance from top to bottom and the other way around. Just play with amplitude and omega to get a first grasp on oscillations and waves. It is not only useful for game patterns but it also happen to rule a lot of physics laws.

You could think of using this principle for your projectiles as well. Same would apply and they would use this oscillating pattern. You got the tool to do so, now it is up to you to do it.

Spinning around

We continue to modify our trigonometric functions and this time we are about to spin around a point. What we want to achieve is a set of ships placed in circle around a point and spinning around as the point moves towards our player ship. Mmm, not sure you quite get my point but you will see this is no magic, just math.

We need a new script that will get our spawn object to move along the x-axis. No big deal:

using UnityEngine;
using System.Collections;

public class CirclePattern : MonoBehaviour {

	void Update () {
		_transform.Translate ( -Time.deltaTime,0,0,Space.World);
	}
}

Attach this to the spawn object and it will move, alone...

We now need to declare a function that will instantiate and position our ships around that Spawn object. Fact is we also want to be able to reuse this function with the less possible problem. Say we want to place 10 ships (come on say it), but next we only want 5 ships so that it breaks a little bit the pattern. Again, we are bout to dive into our trigonometric laws and principles.

We now need to think in terms of the Unit Circle. It is a simple circle of radius 1 which contains the corresponding value of cos and sin in radians all around it.

Unit_circle_angles_color.svg
We need to find a way that each  ship will get a unique coordinate on the Unit Circle. That is if we have 4 ships we want them on each main coordinate: we need to get 0, PI/2, PI and finally 3PI/2. If we get 8 ships, we want the main coordinates and the red coordinates. Hence, our ships are evenly positioned around our spawn object.

using UnityEngine;
using System.Collections;

public class CirclePattern : MonoBehaviour {

	public GameObject ship;
	public int amount; //Give a value in the Inspector for how many ships
	void Start () {
		for(int i = 0 ;i< amount ;i++){
			//Create object and parent it to the Spawn object
                        GameObject obj = (GameObject) Instantiate (ship, transform.position, transform.rotation);	
			obj.transform.parent = transform;

                        PlaceShip (obj,i,amount);

                        // Getting the angle between the ship and the Spawn object 
                        // and use it as phase shift
			CircleMove sc = obj.GetComponent<CircleMove>();
			float angle = Vector3.Angle(obj.transform.localPosition, transform.forward);
    		        Vector3 cross = Vector3.Cross(obj.transform.localPosition, transform.forward);
    		        if (cross.y < 0) angle = -angle;
			     sc.phase = angle * Mathf.Deg2Rad;
		}
	}

	void Update () {
		_transform.Translate ( -Time.deltaTime,0,0,Space.World);
	}

        // Using the index of the newly created object from the loop
        // Divide it by the amount of object and multiply by 2PI (full circle)
	void PlaceShip(GameObject obj, int i, int amount){
		float u = ((float)i / (float)(amount)) * (Mathf.PI*2);
		float x = (float)Mathf.Cos(u);
                float z = (float)Mathf.Sin(u);

		obj.transform.localPosition = new Vector3(x, 0,z)*2;
	}
}

Ok, please do not be scared (you may not be actually). The script is commented for information. Note that we need to add a script to our ship called CircleMove with a member called phase. You will also get some errors as some variables are required in another script that is not yet declared.

PlaceShip function is simply the birthday cake principle. How many people we have and divide the cake in equal share. Same here.

The ship uses the following script:

using UnityEngine;
using System.Collections;

public class CircleMove : Enemy {

    public float phase;
    public override void Start () {
        base.Start();
        _primaryShoot = transform.Find ("SpawnLeft").transform;
        _secondaryShoot = transform.Find ("SpawnRight").transform;    
        InvokeRepeating("Shoot",Random.Range(2.0f,4.0f),Random.Range(2.0f,4.0f));
    }

    public override void Update () {
        transform.localPosition= new Vector3(Mathf.Cos (Time.time+phase ),0,Mathf.Sin (Time.time+phase));
    }
}

Thanks to our inheritance, we only deal with movement but our ships are still shooting. The ships are moved in localPosition because we want them to rotate around this Spawn object which somehow is now our origin point. Thanks to the phase variable, the ships are evenly placed around the center.  Try and change the amount of ships and you will see that as long as you do not give a too large value, it looks nice.

Now try and remove the two + phase in the Update of our ships, they are all in the same spot. Remove only one, they are aligned.

You can also try to declare a second phase variable and use it like this:

	public float phase;
	float phaseA;

	public override void Start () {
		phaseA =phase +30f;
	}

	public override void Update () {
		transform.localPosition= new Vector3(Mathf.Cos (Time.time+phaseA ),0,Mathf.Sin (Time.time+phase));
	}

Shrink and grow

Let's continue to play with the same principle. Now we want the ship group to spin around the center but also to come and go.

	public override void Update () {
		transform.localPosition= new Vector3((Mathf.Cos (Time.time)+2)*Mathf.Cos (Time.time + phase),0,
			(Mathf.Cos (Time.time)+2)*Mathf.Sin (Time.time+phase));
	}

Remember when I said that cos and sin are lerping and clamped between 1 and -1, I am using this feature here. I am constantly increasing then decreasing the distance between the center and each ship. The +2 is used ot give a minimum distance with the center, so yo udo not get all the center joining in the middle (again remove them to see what it does, trying is knowing). That was not too hard you see.

Here again try the phaseA variable.

Elliptic pattern

Our last modification on the trigonometric functions, we will create an ellipse or somehow a similar shape. Let's think a second what is an ellipse. First what is a circle? It is a geometric shape that has every single point equidistant from the center. An ellipse is then a circle based shape that has some point closer to the center than other.

So we want to get some point closer and other further away, but in the meanwhile the changing of the distance cannot abrupt of that would cause the ships to suddenly move a large distance. Remember again that cos and sin are lerping and clamped, and in the previous example we were using that feature and apply the same cos function to our amplitude for both x and z values. Well, we can simply change that alittle so that one grows differently to the other.

	public override void Update () {
		transform.localPosition= new Vector3((Mathf.Cos (Time.time+phase)+2)*Mathf.Cos (Time.time + phase),0,
			(Mathf.Cos (Time.time)+2)*Mathf.Sin (Time.time+phase));
	}

I added one variable and that is it, you will see the ships moving apart of the center when they get to a certain position. Note that this is not a perfect ellipse as the equation for ellipse is different, I just meant en elliptic pattern.

For a real ellipse, this is actually more simple :) you simply need to give different amplitude to your cos and sin functions :

	void Update () {
		transform.localPosition= new Vector3(2*Mathf.Cos (Time.time + phase),0,
			3*Mathf.Sin (Time.time+phase));
	}

As I mentioned at the beginning, the purpose is to show you what can be done to give life to your object. You would want to do a space shooter and use them as they are shown here, but you could also use this in a horror movie. You could have an empty game object with an AudioSource attached that emits a "Booooooo" sound. Using an elliptic pattern around the main player, the object seems to be spinning around it and going back and forth from the player. With a little more tweaking variables you can create a confusing audio effect.

Hide and seek (first FSM)

Here we are about to introduce a new word, Finite-State-Machine (FSM). What is behind this fancy word? Simply a set of action depending on other actions or environment situations. That means your NPC is performing an action regarding its analysis of the surrounding and its own state. There is an advanced tutorial for this topic here but mark my word, it is advanced.

For our little ship,  we will create a simple FSM where is can hide behind a rock for protection and continue its journey.  Before jumping into coding it is always recommended to take the time to think your script. So what is going to happen there:

  1. The ship moves leftwards until he gets shot
  2. The ship is shot and run for cover if cover is not behind
  3. The ship waits a little while and then gets back to its journey
  4. The ship reaches the other end or gets killed on the way

The graph below shows the FSM states and decision tree.

FSMShip

This will get more sense now that we have a look at our script. You need to add a public int health; to our Enemy.cs base class.

Now the big part:

using UnityEngine;
using System.Collections;

public class ProtectScript :Enemy {

	Transform cover;
	Transform [] wayout = new Transform[2];
	public int way;
	enum State{Forward,Hiding,Moving};
        State Current;
	public bool hit;
	public override void Start () {
		base.Start ();
		health = 3;
		cover = GameObject.Find ("Cover").GetComponent<Transform>();
		GameObject[] obj = GameObject.FindGameObjectsWithTag("Wayout");
		for(int i = 0;i<obj.Length;i++)wayout[i] = obj[i].GetComponent<Transform>();
		way = -1;
		hit =false;
                Current = State.Forward
                _primaryShoot = _transform.Find ("SpawnLeft").GetComponent<Transform>();
		_secondaryShoot = _transform.Find ("SpawnRight").GetComponent<Transform>();
		InvokeRepeating("Shoot",0.01f,Random.Range(2.0f,4.0f));
	}

	public override void Update () {
		switch(Current){
			case State.Forward:
				_transform.Translate ( -velocity.x*Time.deltaTime,0,velocity.z*Time.deltaTime,Space.World);
				break;
			case State.Hiding: 
				ToTarget (_cover);
				if(Vector3.Distance (cover.position,_transform.position) <= 0.5f &&way<0 ) {		
					StartCoroutine(ChangeSwitch(Random.Range (1.0f,3.0f)));	
					way = Random.Range (0,2);
				}
                                break;
			case State.Moving:
				ToTarget (_wayout[way]);
				if(Vector3.Distance (wayout[way].position,_transform.position) < 0.2f) current = State.Forward;	
				break;
		}
	}

	IEnumerator ChangeSwitch(float time) {
		yield return new WaitForSeconds(time);
		Current = State.Moving;
	}

	public override void OnCollisionEnter(Collision other){
		if(other.gameObject.tag == "ProjectileShip"){
			Destroy(other.gameObject);
			health-=1;
			if(health<=0)Destroy(gameObject);
			if(Current ==State.Forward &&_transform.position.x >cover.position.x )
                             Current =State.Hiding;
		}
	}
	void ToTarget(Transform tr){
		Vector3 direction = tr.position - _transform.position;
		direction.Normalize();
		_transform.Translate ( direction.x*Time.deltaTime,0,direction.z*Time.deltaTime,Space.World);
	}
}

You need to add the RockPrefab and position it properly on the level. Make sure the cover and the two way outs are at y = 0.

we use an enum to define which state the ship is on. The good point of enumeration is that you cannot assign a wrong value. Using a switch with a integer case, for some reasons, you may end up with the wrong value. This can't be happening with enum as only the values declared can be used.

In the Update, we use a switch with the current state.   If we are shot first time, we change the state from forward to hiding and the ship goes towards the cover. It will wait there a random amount of time until a wayout is chosen and the state is changed to moving. The ship is now going out of cover. When reaching the wayout, it moves back to state forward. As we have passed the cover point, shooting the ship again will not affect its behavior anymore.if(Current ==State.Forward &&_transform.position.x > cover.position.x )current =State.Hiding; This will ensure that once the position is smaller than the cover position (we move leftwards and x is decreased) the state cannot be changed anymore.

ToTarget is called to get the new vector to move our ship.

Run the game, if you do not do anything, the ship will make its way to the left. If you shoot it early enough, its direction will change and the various states will be used. Shoot it again after cover, it won't affect it anymore.

In the FSM tutorial here (same as above), you will see how to modify the code so that only a few lines are used inside the Update().

Kamikaze ship

The next behavior is fairly simple, we want the ship to suicide itself into our own. But we will add a little more thing. We do not want the ship to go back and forth to hit us, only if we are ahead of it.

As we want to reuse the ToTarget function from the previous paragraph, we will move it into another file to be able to call it anywhere we need it.

The project has a GameManager script which already include our extension methods:

	public static void Target(this Transform _transform, Transform target){
		Vector3 direction = target.position - _transform.position;
		direction.Normalize();
		_transform.Translate ( direction.x*Time.deltaTime,0,direction.z*Time.deltaTime,Space.World);
	}

We create a new script that we attach to the enemy ship :

using UnityEngine;
using System.Collections;

public class Banzai : Enemy {

	Transform target;
	public override void Start () {
		base.Start ();
		_primaryShoot = transform.Find ("SpawnLeft").GetComponent<Transform>();
		_secondaryShoot = transform.Find ("SpawnRight").GetComponent<Transform>();
		target = GameObject.FindGameObjectWithTag("Ship").GetComponent<Transform>();
	}

	public override void Update () {
		_transform.Target(_target);
	}
}

If we run the game, the ship will constantly run after us. As we said we only want that as long as our ship is ahead of our enemy.

	public override void Update () {
		if(_transform.position.x > _target.position.x)
			_transform.Target(_target);
		else 
			base.Update();
	}

So if we are on the left of the enemy we use our Target method, otherwise we call the basic behavior from our base class.

You can easily apply the exact same behavior to the enemy projectile, so that the ship and the projectile are directed at the player. You can also use it only on the projectile with the ship using any of the previous behaviors. Even more!! What if you try to create an oscillating projectile!!

Keep in mind that you could try to convert the algorithms we used previously into extension methods just like Target so that you can easily call them on any object.

Smarter ship that can see (in front of itself)

Finally, in our last part (are still here anyway?...) we will get back to our very first algorithm which was creating ships at random positions. We will also remove our rock. Remember, each ship is given a different velocity. As a result, some will go faster than others (waoh, took me a while to figure that one out). That means that some of them will get on top of others and guess what, I do not want that to happen. So we will give some eyes to our ships so that they can check in front of them if something is there and modify their direction depending on that.

The behavior we need to recreate is Line of Sight.The picture below shows the basic principle. The blue arrow is the transform.forward, we just need to see if something is the area of 45 degrees (or else) of this vector within a range defined by the green circle.

To achieve this principle, there are different ways. The one I chose uses the physics engine provided by Unity. So I will add a sphere collider to my enemy prefab and set its radius to 3. You will get a warning that a box collider is already attached, just click Add. Make this collider a trigger ticking IsTrigger.

Now I just create a simple script:

using UnityEngine;
using System.Collections;

public class SeeFront : Enemy {

	float angle =45;
	public override void Start () {
		base.Start ();
	}

	public override void Update () {
		base.Update ();
	}

	void OnTriggerEnter(Collider other){
		if(other.gameObject.tag == "EnemyShip"){
			if(Vector3.Angle(other.gameObject.transform.position - _transform.position, transform.forward) <= angle){
				SeeFront sc = other.GetComponent<SeeFront>();
				velocity = sc.velocity;
			}
		}
	}
}

All I am doing is regulating the traffic. In the case one ship collides with another one from behind, it will check if there is an angle less than 45 degrees, and if so it will get the velocity of the other ship and apply it to itself.  Note that I have not given them the ability to shoot but you should know how to do that by now.

This could also be used in a  traffic simulation game like SimCity or GTA.

Conclusion

We have reached the end of this tutorial. Hopefully, you have learnt some tips you will use in your own game.  We went through some basic trigonometry with our Unit Circle and how to use its features via sin and cos. We created some fancy elliptical shapes and circles. We saw some basic vector algebra such as finding the angle between two vectors or the distance between two points. We also went through some basic OOP feature like inheritance, override and virtual keywords, polymorphism and others.

Now it is up to you to mix the various features you just saw and create NPCs that accumulate many behaviors. Keep in mind that AI gets quickly expensive on the CPU. You may want to ease a little with InvokeRepeting so that calculations are only done every x second.

All in all, I hope it was useful and more is coming up.

, , , , , , ,

8 Responses to "Basic of AI – Space shooter"

  • Chris
    December 7, 2012 - 1:33 am Reply

    Hi,

    Just starting this tutorial, looking great.

    My first question has to so with the ProjectileDeath trigger. I always thought that the object that was the trigger would be the one to have the OnTriggerEnter() function in its script, and then do the tag check for colliders entering it. Here, you have the Ship Projectiles with the OnTriggerEnter() function in its script that checks for the tag of the ProjectileDeath trigger.

    I want to ask if this is the standard practice in games, or whether its a one-off in this instance. I can see a benefit of doing it this way, in that the check for colliders in only done while the projectile is ‘alive’, rather than constantly if the OnTriggerEnter() was on the trigger itself. So saves on processing time in this way.

    Also, if this is the case, then couldn’t the ship projectiles just be triggers and the ProjectileDeath box just be a collider, as well as the rocks, etc? It would then check the tag of these and then destroy itself on them entering. Would this not be the same thing?

    Thanks and apologies for all the questions, but its got me thinking :D

  • fafase
    December 7, 2012 - 8:14 am Reply

    Hey Chris,

    Well, the logic would say “Attach the trigger function to the object that enters/moves”. In our case the projectile. My tip is, use it the way that makes it easier.

    Let’s think of the projectile case, the trigger is on the projectile. The projectile checks what is hit (1) and destroy itself (2). If I do it the other way. The box checks (1), destroy the reference (2) but a reference needs to first dereference the object (3). See I save one cycle.

    Programming is philosophy, not religion. You can make it many ways and all are right in some ways.
    You can go from London to Paris by train (yes you can) or plane, one is faster, one is cheaper, programming is the same.

    Fafase

  • Terry Morgan
    January 12, 2013 - 7:09 pm Reply

    can’t get the file ‘disconnected from server’ Do you have dropbox over there?

    • fafase
      January 13, 2013 - 9:19 am Reply

      Hi Terry, If you are talking about the project file at the beginning of the article, I’m sorry to say that the problem probably comes from your side as I can download it with no issue.

      Fafase

  • Terry Morgan
    January 13, 2013 - 2:55 pm Reply

    I got it, and the astar one also, thanks

  • GlorySimba
    April 12, 2013 - 3:27 am Reply

    Thanks a lot for your brilliant toturial!

  • PTTC
    April 12, 2013 - 9:55 pm Reply

    Just found this site landing on this page, looks like an excellent learning resource for c# in Unity!!! Can’t wait to go through your tutorials starting tomorrow morning

  • Shooter AI Tutorial | circle7labs
    August 25, 2013 - 1:03 am Reply

    […] else interested in this kind of stuff, Unity Gems has a pretty nice tutorial (with project files) HERE.  I’m particularly interested in the “look front” script, which acts as a […]

Leave a Reply