Enemy Detection and Firing

Baer Bradford 11/23/2014

Learn how to detect enemies and shoot projectiles at them. A continuation of the tower defense tutorial series.

Learn how to make turrets detect enemies and shoot projectiles at them. A continuation of the tower defense tutorial series.

Setting Up Your Turret GameObject

Go ahead and get started by creating a Sprite in your hierarchy.

This should create a new GameObject with the Transform and Sprite Renderer components

Don't worry too much about changing the position; we're going to be setting that programmatically when the player tries to place a turret on the map. Do, however, add some cool art to the Sprite property of your renderer.

This is what we're using for our Earth Type Turrets. Maybe one day we'll hire an artist...

Attach a MonoBehavior script and call it Turret.cs.

Attach a Sphere Collider too. Make sure the Is Trigger property is checked. Don't worry about changing values for Center and Radius as we'll be changing these programmatically.

Your GameObject's components should look something like this.

Go ahead and create a prefab by dragging the turret from your scene to a directory in the project tab. Having our turrets as prefabs will give the benefit of being able to instantiate them from code. We can even create multiple prefabs which will allow for different turret types, each with their own stats, special abilities, and artwork.

Writing the Scripts

Open up the Turret.cs attached to your prefab. It should look something like this:

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

public class Turret : MonoBehaviour
{

    // Use this for initialization
    void Start () {

    }

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

    }
}

Take note I've added a couple using statements.

Let's add some fields and properties. Keep in mind the public fields will be accessible in Unity's inspector panel. damage, range, and rateOfFire are all meant to adjusted there on a 0-10 scale. The associated properties AttackDelay and DetectionRadius scale those values to based on the size of the map. DetectionRadius also takes responsibility for changing the size of the attached sphere collider.

// Configurable
public float accuracyError = 2.0f;
public int damage = 10;
public GameObject projectileType;
public int range = 5;
public int rateOfFire = 5;

// Constants
private const float MinAttackDelay = 0.1f;
private const float MaxAttackDelay = 2f;

// Internal
private List myTargets;
private float nextDamageEvent;
private ObjectManager objectManager;    
private static readonly object syncRoot = new object ();

// Properties
private float AttackDelay
{
    get 
    {
        int inverted = rateOfFire;
        if (rateOfFire == 0) 
        { 
            return float.MaxValue;
        }
        else if (rateOfFire < 5)
        {
            inverted = rateOfFire + 2 * (5 - rateOfFire);
        }
        else if (rateOfFire > 5) 
        {
            inverted = rateOfFire - 2 * (rateOfFire - 5);
        }

        return (((float)inverted - 1f) / (10f - 1f)) * (MaxAttackDelay - MinAttackDelay) + .1f;
    }
}

public float DetectionRadius
{ 
    get 
    {   
        float minRange = Mathf.Min(objectManager.Map.nodeSize.x, objectManager.Map.nodeSize.y) * 1.5f;
        float maxRange = minRange * 4f;

        float detectionRadius = (((float)range - 1f) / (10f - 1f)) * (maxRange - minRange) + minRange;
        detectionRadius = detectionRadius / transform.localScale.x;

        return detectionRadius;
    }
    set 
    {
        float minRange = Mathf.Min(objectManager.Map.nodeSize.x, objectManager.Map.nodeSize.y) * 1.5f;
        float maxRange = minRange * 4f;

        float detectionRadius = (((float)value - 1f) / (10f - 1f)) * (maxRange - minRange) + minRange;
        detectionRadius = detectionRadius / transform.localScale.x;

        SphereCollider collider = transform.GetComponent ();
        collider.radius = detectionRadius;
    }
}

Here we initialize some of those private fields. objectManager is just a singleton we are using to help maintain game state. Matt talks more in-depth about it in some of the earlier videos from the Tower Defense Tutorial Video Series.

// Runs when entity is Instantiated
void Awake()
{
    objectManager = ObjectManager.GetInstance();
    objectManager.AddEntity(this);
}

// Use this for initialization
void Start ()
{
    DetectionRadius = range;
    myTargets = new List();
}

These two methods track when enemies enter and exit the attached sphere collider. At any given time, myTargets should now reflect all enemies inside the sphere, the turret's detectable area.

void OnTriggerEnter (Collider other)
{
    if (other.gameObject.tag == "enemy") {  
        myTargets.Add (other.GetComponent());
    }
}

void OnTriggerExit (Collider other)
{
    lock (syncRoot) {
        if (other != null &&
            myTargets.Select (t => t!= null && t.gameObject).Contains(other.gameObject)) {
            myTargets.Remove (other.GetComponent());
        }
    }

}

Killing Dudes with Projectiles

In order to create a projectile, create a new GameObject starting with as a sphere. I ended up adding a Mesh Renderer and a Line Renderer to get it to look like a bullet. Attach a MonoBehavior script called Projectile.cs. Go ahead and make a prefab from this object the same way you did for turrets.

Nothing too crazy going on in this script. It requires a target enemy (and associated location) and just homes in on it until it "hits". We're not doing any collision detection here, but rather checking distance between the projectile and its target. Once the projectile reaches the target, it destroys itself and damages the enemy by subtracting from its health.

using UnityEngine;
using System.Collections;

public class Projectile : MonoBehaviour
{
    // Configurable
    public float range;
    public float speed;
    public EnemyBase target;
    public Vector3 targetPosition;

    public int Damage { get; set; }

    // Internal
    private float distance;

    // Runs when entity is Instantiated
    void Awake ()
    {
        distance = 0;
    }

    // Update is called once per frame
    void Update ()
    {
        Vector3 moveVector = new Vector3 (transform.position.x - targetPosition.x,
                                         transform.position.y - targetPosition.y,
                                         transform.position.z - targetPosition.z).normalized;

        // update the position
        transform.position = new Vector3 (transform.position.x - moveVector.x * speed * Time.deltaTime,
                                         transform.position.y - moveVector.y * speed * Time.deltaTime,
                                         transform.position.z - moveVector.z * speed * Time.deltaTime);

        distance += Time.deltaTime * speed;

        if (distance > range ||
            Vector3.Distance (transform.position, new Vector3 (targetPosition.x, targetPosition.y, targetPosition.z)) < 1) 
        {
            Destroy (gameObject);
            if (target != null) 
            {
                target.Damage (Damage);
            }
        }
    }
}

Now that projectiles are good to go, drag that new projectile prefab onto the projectileType field (in the inspector when you've got a turret selected). Next, you'll need the following two methods to make the turret "fire" projectiles. All we're doing is setting up a loop where the turret instantiates new projectiles targeted at a random enemy within range.

void Fire (EnemyBase myTarget)
{
    var targetPosition = myTarget.transform.position;
    var aimError = Random.Range (-accuracyError, accuracyError);
    var aimPoint = new Vector3 (targetPosition.x + aimError, targetPosition.y + aimError, targetPosition.z + aimError);
    nextDamageEvent = Time.time + AttackDelay;
    GameObject projectileObject = Instantiate (projectileType, transform.position, Quaternion.LookRotation (targetPosition)) as GameObject;
    Projectile projectile = projectileObject.GetComponent ();
    projectile.Damage = damage;
    projectile.target = myTarget;
    projectile.targetPosition = aimPoint;
}

// Update is called once per frame
void Update ()
{
    lock(syncRoot)
    {
        if (myTargets.Any())
        {
            EnemyBase myTarget = myTargets.ElementAt(Random.Range(0, myTargets.Count));


            if (myTarget != null) {
                if (Time.time >= nextDamageEvent)
                {
                    Fire(myTarget);
                }
            }
            else
            {
                nextDamageEvent = Time.time + AttackDelay;
                myTargets.Remove(myTarget);
            }              
        }
    }       
}

To fill in some of the gaps like placing turrets on the map and spawning enemies, I encourage you to go take a look at some of the earlier videos from the Tower Defense Tutorial Video Series.