Unity Turret Tutorial

I am currently creating a series of videos to show how to make an automated turret in unity. It is going to be spread across a few videos, but the first one is up now.

Part 1 - Tracking

TrackingSystem.cs

using UnityEngine;
using System.Collections;

public class TrackingSystem : MonoBehaviour {
    public float speed = 3.0f;

    GameObject m_target = null;
    Vector3 m_lastKnownPosition = Vector3.zero;
    Quaternion m_lookAtRotation;

    // Update is called once per frame
    void Update () {
        if(m_target){
            if(m_lastKnownPosition != m_target.transform.position){
                m_lastKnownPosition = m_target.transform.position;
                m_lookAtRotation = Quaternion.LookRotation(m_lastKnownPosition - transform.position);
            }

            if(transform.rotation != m_lookAtRotation){
                transform.rotation = Quaternion.RotateTowards(transform.rotation, m_lookAtRotation, speed * Time.deltaTime);
            }
        }
    }

    public void SetTarget(GameObject target){
        m_target = target;
    }
}

EnemyAi.cs

using UnityEngine;
using System.Collections;

public class EnemyAi : MonoBehaviour {
    public Transform pointA;
    public Transform pointB;
    public float speed;

    // Update is called once per frame
    void Update () {
        transform.position = Vector3.Lerp(pointA.position, pointB.position, Mathf.Pow(Mathf.Sin(Time.time * speed), 2));
    }
}

Part 2 - Projectiles & Shooting

A -

B -

BaseProjectile.cs

using UnityEngine;
using System.Collections;

public abstract class BaseProjectile : MonoBehaviour {
    public float speed = 5.0f;

    public abstract void FireProjectile(GameObject launcher, GameObject target, int damage, float attackSpeed);
}

NormalProjectile.cs

using UnityEngine;
using System.Collections;

public class NormalProjectile : BaseProjectile {
using UnityEngine;
using System.Collections;

public class NormalProjectile : BaseProjectile {
    Vector3 m_direction;
    bool m_fired;
    GameObject m_launcher;
    GameObject m_target;
    int m_damage;

    // Update is called once per frame
    void Update () {
        if(m_fired){
            transform.position += m_direction * (speed * Time.deltaTime);
        }
    }

    public override void FireProjectile(GameObject launcher, GameObject target, int damage, float attackSpeed){
        if(launcher && target){
            m_direction = (target.transform.position - launcher.transform.position).normalized;
            m_fired = true;
            m_launcher = launcher;
            m_target = target;
            m_damage = damage;

            Destroy(gameObject, 10.0f);
        }
    }

    void OnCollisionEnter(Collision other)
    {
        if(other.gameObject == m_target)
        {
            DamageData dmgData = new DamageData();
            dmgData.damage = m_damage;

            MessageHandler msgHandler = m_target.GetComponent<MessageHandler>();

            if(msgHandler)
            {
                msgHandler.GiveMessage(MessageType.DAMAGED, m_launcher, dmgData);
            }
        }

        if(other.gameObject.GetComponent<BaseProjectile>() == null)
            Destroy(gameObject);
    }
}

TrackingProjectile.cs

using UnityEngine;
using System.Collections;

public class TrackingProjectile : BaseProjectile {
    GameObject m_target;
    GameObject m_launcher;
    int m_damage;

    Vector3 m_lastKnownPosition;

    // Update is called once per frame
    void Update () {
        if(m_target){
            m_lastKnownPosition = m_target.transform.position;
        }
        else
        {
            if(transform.position == m_lastKnownPosition)
            {
                Destroy(gameObject);
            }
        }

        transform.position = Vector3.MoveTowards(transform.position, m_lastKnownPosition, speed * Time.deltaTime);
    }
 
    public override void FireProjectile(GameObject launcher, GameObject target, int damage, float attackSpeed){
        if(target){
            m_target = target;
            m_lastKnownPosition = target.transform.position;
            m_launcher = launcher;
            m_damage = damage;
        }
    }

    void OnCollisionEnter(Collision other)
    {
        if(other.gameObject == m_target)
        {
            DamageData dmgData = new DamageData();
            dmgData.damage = m_damage;
         
            MessageHandler msgHandler = m_target.GetComponent<MessageHandler>();
         
            if(msgHandler)
            {
                msgHandler.GiveMessage(MessageType.DAMAGED, m_launcher, dmgData);
            }
        }
     
        if(other.gameObject.GetComponent<BaseProjectile>() == null)
            Destroy(gameObject);
    }
}

BeamProjectile.cs

using UnityEngine;
using System.Collections;

public class BeamProjectile : BaseProjectile {
    public float beamLength = 10.0f;
    GameObject m_launcher;
    GameObject m_target;
    int m_damage;
    float m_attackSpeed;
    float m_attackTimer;

    // Update is called once per frame
    void Update () {
        m_attackTimer += Time.deltaTime;

        if(m_launcher){
            GetComponent<LineRenderer>().SetPosition(0, m_launcher.transform.position);
            GetComponent<LineRenderer>().SetPosition(1, m_launcher.transform.position + (m_launcher.transform.forward * beamLength));
            RaycastHit hit;

            if(Physics.Raycast(m_launcher.transform.position, m_launcher.transform.forward, out hit, beamLength))
            {
                if(hit.transform.gameObject == m_target)
                {
                    if(m_attackTimer >= m_attackSpeed)
                    {
                        DamageData dmgData = new DamageData();
                        dmgData.damage = m_damage;
                     
                        MessageHandler msgHandler = m_target.GetComponent<MessageHandler>();
                     
                        if(msgHandler)
                        {
                            msgHandler.GiveMessage(MessageType.DAMAGED, m_launcher, dmgData);
                        }

                        m_attackTimer = 0.0f;
                    }
                }
            }
        }
    }
 
    public override void FireProjectile(GameObject launcher, GameObject target, int damage, float attackSpeed){
        if(launcher){
            m_launcher = launcher;
            m_target = target;
            m_damage = damage;
            m_attackSpeed = attackSpeed;
            m_attackTimer = 0.0f;
        }
    }
}

ShootingSystem.cs

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

public class ShootingSystem : MonoBehaviour {
    public float fireRate;
    public int damage;
    public float fieldOfView;
    public bool beam;
    public GameObject projectile;
    public List<GameObject> projectileSpawns;

    List<GameObject> m_lastProjectiles = new List<GameObject>();
    float m_fireTimer = 0.0f;
    GameObject m_target;

    // Update is called once per frame
    void Update () {
        if(!m_target)
        {
            if(beam)
                RemoveLastProjectiles();

            return;
        }

        if(beam && m_lastProjectiles.Count <= 0){
            float angle = Quaternion.Angle(transform.rotation, Quaternion.LookRotation(m_target.transform.position - transform.position));
         
            if(angle < fieldOfView){
                SpawnProjectiles();
            }
        }else if(beam && m_lastProjectiles.Count > 0){
            float angle = Quaternion.Angle(transform.rotation, Quaternion.LookRotation(m_target.transform.position - transform.position));

            if(angle > fieldOfView){
                RemoveLastProjectiles();
            }
        }else{
            m_fireTimer += Time.deltaTime;

            if(m_fireTimer >= fireRate){
                float angle = Quaternion.Angle(transform.rotation, Quaternion.LookRotation(m_target.transform.position - transform.position));
             
                if(angle < fieldOfView){
                    SpawnProjectiles();
                 
                    m_fireTimer = 0.0f;
                }
            }
        }
    }

    void SpawnProjectiles(){
        if(!projectile){
            return;
        }

        m_lastProjectiles.Clear();

        for(int i = 0; i < projectileSpawns.Count; i++){
            if(projectileSpawns[i]){
                GameObject proj = Instantiate(projectile, projectileSpawns[i].transform.position, Quaternion.Euler(projectileSpawns[i].transform.forward)) as GameObject;
                proj.GetComponent<BaseProjectile>().FireProjectile(projectileSpawns[i], m_target, damage, fireRate);

                m_lastProjectiles.Add(proj);
            }
        }
    }
 
    public void SetTarget(GameObject target){
        m_target = target;
    }

    void RemoveLastProjectiles()
    {
        while(m_lastProjectiles.Count > 0){
            Destroy(m_lastProjectiles[0]);
            m_lastProjectiles.RemoveAt(0);
        }
    }
}

Part 3 - TurretAi

RangeChecker.cs

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

public class RangeChecker : MonoBehaviour {
    public List<string> tags;

    List<GameObject> m_targets = new List<GameObject>();

    void OnTriggerEnter(Collider other)
    {
        bool invalid = true;

        for(int i = 0; i < tags.Count; i++)
        {
            if(other.CompareTag(tags[i]))
            {
                invalid = false;
                break;
            }
        }

        if(invalid)
            return;

        m_targets.Add(other.gameObject);
    }

    void OnTriggerExit(Collider other)
    {
        for(int i = 0; i < m_targets.Count; i++)
        {
            if(other.gameObject == m_targets[i])
            {
                m_targets.Remove(other.gameObject);
                return;
            }
        }
    }

    public List<GameObject> GetValidTargets()
    {
        return m_targets;
    }

    public bool InRange(GameObject go)
    {
        for(int i = 0; i < m_targets.Count; i++)
        {
            if(go == m_targets[i])
                return true;
        }

        return false;
    }
}

TurretAi.cs

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

public class TurretAi : MonoBehaviour {
    public enum AiStates{NEAREST, FURTHEST, WEAKEST, STRONGEST};

    public AiStates aiState = AiStates.NEAREST;

    TrackingSystem m_tracker;
    ShootingSystem m_shooter;
    RangeChecker   m_range;

    // Use this for initialization
    void Start () {
        m_tracker =  GetComponent<TrackingSystem>();
        m_shooter =  GetComponent<ShootingSystem>();
        m_range   =  GetComponent<RangeChecker>();
    }
 
    // Update is called once per frame
    void Update () {
        if(!m_tracker || !m_shooter || !m_range)
            return;

        switch(aiState)
        {
        case AiStates.NEAREST:
            TargetNearest();
            break;
        case AiStates.FURTHEST:
            TargetFurthest();
            break;
        case AiStates.WEAKEST:
            TargetWeakest();
            break;
        case AiStates.STRONGEST:
            TargetStrongest();
            break;
        }
    }

    void TargetNearest()
    {
        List<GameObject> validTargets = m_range.GetValidTargets();

        GameObject curTarget = null;
        float closestDist = 0.0f;

        for(int i = 0; i < validTargets.Count; i++)
        {
            float dist = Vector3.Distance(transform.position, validTargets[i].transform.position);

            if(!curTarget || dist < closestDist)
            {
                curTarget = validTargets[i];
                closestDist = dist;
            }
        }

        m_tracker.SetTarget(curTarget);
        m_shooter.SetTarget(curTarget);
    }

    void TargetFurthest()
    {
        List<GameObject> validTargets = m_range.GetValidTargets();
     
        GameObject curTarget = null;
        float furthestDist = 0.0f;
     
        for(int i = 0; i < validTargets.Count; i++)
        {
            float dist = Vector3.Distance(transform.position, validTargets[i].transform.position);
         
            if(!curTarget || dist > furthestDist)
            {
                curTarget = validTargets[i];
                furthestDist = dist;
            }
        }
     
        m_tracker.SetTarget(curTarget);
        m_shooter.SetTarget(curTarget);
    }

    void TargetWeakest()
    {
        List<GameObject> validTargets = m_range.GetValidTargets();

        GameObject curTarget = null;
        int lowestHealth = 0;

        for(int i = 0; i < validTargets.Count; i++)
        {
            int maxHp = validTargets[i].GetComponent<Health>().maxHealth;

            if(!curTarget || maxHp < lowestHealth)
            {
                lowestHealth = maxHp;
                curTarget = validTargets[i];
            }
        }

        m_tracker.SetTarget(curTarget);
        m_shooter.SetTarget(curTarget);
    }

    void TargetStrongest()
    {
        List<GameObject> validTargets = m_range.GetValidTargets();
     
        GameObject curTarget = null;
        int highestHealth = 0;
     
        for(int i = 0; i < validTargets.Count; i++)
        {
            int maxHp = validTargets[i].GetComponent<Health>().maxHealth;
         
            if(!curTarget || maxHp > highestHealth)
            {
                highestHealth = maxHp;
                curTarget = validTargets[i];
            }
        }
     
        m_tracker.SetTarget(curTarget);
        m_shooter.SetTarget(curTarget);
    }
}
5 Likes

Thank you, I hope you follow through with the Tut.
renny

Part 2 is out now

This is great, thanks for going the extra step to show how to implement different types of projectiles.

1 Like

Glad you like it. I will hopefully do the next part this weekend, been busy with work lately.

Part 3 is finally out.

I apologise for the long delay & the next part will hopefully be out very soon.

More good stuff, thanks for the time you put into this. I’ve learnt a lot.

Looking good, best of luck to you.

1 Like

Turn’s out I can’t have more than five videos per forum post, so I can’t have everything at the top sorry.

Part 4A - Messaging & Health

MessageHandler.cs

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

public abstract class MessageData{};
public enum MessageType{DAMAGED, HEALTHCHANGED, DIED};
public delegate void MessageDelegate(MessageType msgType, GameObject go, MessageData msgData);

public class MessageHandler : MonoBehaviour {
    public List<MessageType> messages;

    List<MessageDelegate> m_messageDelegates = new List<MessageDelegate>();

    public void RegisterDelegate(MessageDelegate msgDele)
    {
        m_messageDelegates.Add(msgDele);
    }

    public bool GiveMessage(MessageType msgType, GameObject go, MessageData msgData)
    {
        bool approved = false;

        for(int i = 0; i < messages.Count; i++)
        {
            if(messages[i] == msgType)
            {
                approved = true;
                break;
            }
        }

        if(!approved)
            return false;

        for(int i = 0; i < m_messageDelegates.Count; i++)
        {
            m_messageDelegates[i](msgType, go, msgData);
        }

        return true;
    }
}

public class DamageData : MessageData{
    public int damage;
}

public class DeathData : MessageData{
    public GameObject attacker;
    public GameObject attacked;
}

public class HealthData : MessageData{
    public int maxHealth;
    public int curHealth;
}

Health.cs

using UnityEngine;
using System.Collections;

public class Health : MonoBehaviour {
    public int maxHealth = 100;

    int m_curHealth;
    MessageHandler m_messageHandler;

    // Use this for initialization
    void Start () {
        m_curHealth = maxHealth;
        m_messageHandler = GetComponent<MessageHandler>();

        if(m_messageHandler)
        {
            m_messageHandler.RegisterDelegate(RecieveMessage);
        }
    }

    void RecieveMessage(MessageType msgType, GameObject go, MessageData msgData)
    {
        switch(msgType)
        {
        case MessageType.DAMAGED:
            DamageData dmgData = msgData as DamageData;

            if(dmgData != null)
            {
                DoDamage(dmgData.damage, go);
            }
            break;
        }
    }

    void DoDamage(int dmg, GameObject go)
    {
        m_curHealth -= dmg;

        if(m_curHealth <= 0)
        {
            m_curHealth = 0;

            if(m_messageHandler)
            {
                DeathData deathData = new DeathData();
                deathData.attacker = go;
                deathData.attacked = gameObject;

                m_messageHandler.GiveMessage(MessageType.DIED, gameObject, deathData);
            }
        }

        if(m_messageHandler)
        {
            HealthData hpData = new HealthData();

            hpData.maxHealth = maxHealth;
            hpData.curHealth = m_curHealth;

            m_messageHandler.GiveMessage(MessageType.HEALTHCHANGED, gameObject, hpData);
        }
    }
}

Part 4B - Health UI & Damage

HealthUi.cs

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class HealthUi : MonoBehaviour {
    public Slider slider;

    // Use this for initialization
    void Start () {
        MessageHandler msgHandler = GetComponent<MessageHandler>();

        if(msgHandler)
        {
            msgHandler.RegisterDelegate(RecieveMessage);
        }
    }


    void RecieveMessage(MessageType msgType, GameObject go, MessageData msgData)
    {
        switch(msgType)
        {
        case MessageType.HEALTHCHANGED:
            HealthData hpData = msgData as HealthData;
         
            if(hpData != null)
            {
                UpdateUi(hpData.maxHealth, hpData.curHealth);
            }
            break;
        }
    }

    void UpdateUi(int maxHealth, int curHealth)
    {
        slider.value = (1.0f/maxHealth) * curHealth;
    }
}

Part 4C - Death & Turret Ai Continued

Death.cs

using UnityEngine;
using System.Collections;

public class Death : MonoBehaviour {

    // Use this for initialization
    void Start () {
        MessageHandler msgHandler = GetComponent<MessageHandler>();
     
        if(msgHandler)
        {
            msgHandler.RegisterDelegate(RecieveMessage);
        }
    }


    void RecieveMessage(MessageType msgType, GameObject go, MessageData msgData)
    {
        switch(msgType)
        {
        case MessageType.DIED:
            DeathData dthData = msgData as DeathData;

            if(dthData != null)
            {
                Die();
            }
            break;
        }
    }

    void Die()
    {
        Destroy(gameObject);
    }
}
2 Likes

For the next part of the tutorials I am going to be focusing on the enemies to help with turret testing. While I am setting this up I was wondering what people would like to see next with the turret.

so far I’ve got the following things I would like to incorporate:

  • Player controlled turret
  • Smart tracking (Calculates where the target will be next)
  • Aoe turret
  • Special projectiles e.g. slow targets, do damage over time after hitting a target, bouncing damage etc.

I will probably think of a few more as I record more, but right now these are the things I want to add.

If you have anything that you are struggling with or would just like to see incorporated just leave a reply & Ill see what I can do.

Thanks :slight_smile:

1 Like

Hi, thanks for this great series!

Currently I have problems with Death.cs, if Die() is executed, I instantly get this error:

MissingReferenceException: The object of type 'GameObject' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
TurretAi.TargetNearest () (at Assets/01-Scripts/Towers/TurretAi.cs:57)
TurretAi.Update () (at Assets/01-Scripts/Towers/TurretAi.cs:34)

Someone can help?

Sorry for the late reply, been really busy with work lately.

This bug is my fault, I pre-code everything then strip it away & record myself redoing it. I forgot to remove & redo the fix in my video.

In the RangeChecker class you need to just change your GetValidTargets() to remove null targets:

public List<GameObject> GetValidTargets()
    {
        for(int i = 0; i < m_targets.Count; i++)
        {
            if(m_targets[i] == null)
            {
                m_targets.RemoveAt(i);
                i--;
            }
        }

        return m_targets;
    }

This should fix the null reference exception, any more issues just ask & I’ll help asap (Hopefully faster than this :P)

Why am i getting this error?
Assets/Scripts/NormalProjectile.cs(7,14): error CS0101: The namespace global::' already contains a definition for NormalProjectile’

This is generally caused by having two classes named NormalProjectile.

Ohhhhhhhh i found the duplicate thanks!

Ok i Found another error But with TurretAi.cs This time Here is the error And sorry about all this im a noob at coding xD

Assets/Scripts/TurretAi.cs(65,19): error CS0122: `TrackingSystem.SetTarget(UnityEngine.GameObject)’ is inaccessible due to its protection level

What does this mean?

You missed public from the function declaration. You probably have it like this: Void SetTarget, but it needs to be like this: Public Void SetTarget

All is good so far but i need to edit the X Axis on the Turret to keep it form looking up when the Object gets closer.

First of all create a public variable to use as the minimum distance on the tracking script.
Next if we have a target do a distance check each frame to see if its less than the minimum range.
Since you want to limit the pitch(x axis) you can just clamp the y value of the result of the last known position and the current position, 1 would be directly up, -1 would be down.
Once youve clamped the y value pass it to the rotate towards function.

Its been a long time, but finally carrying this on.

Part 5 - Pathing

PathGenerator.cs

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

public struct PathNode
{
    public Transform transform;
    public float dist;
    public float minDist;
    public float maxDist;
}

public class PathGenerator : MonoBehaviour {
    public bool looped;
    List<PathNode> m_nodes = new List<PathNode>();
    float m_totalDist;

    // Use this for initialization
    void Start () {
        FindNodes(transform);
        CalculatePath();
    }
   
    void FindNodes(Transform parent)
    {
        foreach(Transform child in parent)
        {
            if(child.childCount == 0)
            {
                PathNode newNode = new PathNode();
                newNode.transform = child;
                m_nodes.Add(newNode);
            }

            FindNodes(child);
        }
    }

    void CalculatePath()
    {
        float totalDist = 0.0f;

        for(int i = 0; i < m_nodes.Count; i++)
        {
            PathNode curNode = m_nodes[i];
            PathNode nextNode;

            if(looped && i+1 == m_nodes.Count)
                nextNode = m_nodes[0];
            else if(i+1 != m_nodes.Count)
                nextNode = m_nodes[i+1];
            else
                nextNode = new PathNode();

            float dist = 0.0f;

            if(nextNode.transform)
                dist = Vector3.Distance(curNode.transform.position, nextNode.transform.position);

            curNode.dist = dist;
            curNode.minDist = totalDist;
            totalDist += dist;
            curNode.maxDist = totalDist;

            m_nodes[i] = curNode;
        }

        m_totalDist = totalDist;
    }

    public PathNode GetPointOnPath(float progress, ref Vector3 pos)
    {
        if(looped)
            progress = progress - Mathf.Floor(progress);
        else if(progress > 1.0f)
            progress = 1.0f;
        else if(progress < 0.0f)
            progress = 0.0f;

        float curDist = m_totalDist * progress;
        int id = (int)Mathf.Floor(m_nodes.Count/2.0f);

        while(true)
        {
            if(m_nodes[id].minDist > curDist)
            {
                id--;
            }
            else if(m_nodes[id].maxDist < curDist)
            {
                id++;
            }
            else if(m_nodes[id].minDist <= curDist && m_nodes[id].maxDist >= curDist)
            {
                float lerpProg = m_nodes[id].dist == 0.0f ? 0.0f : (curDist - m_nodes[id].minDist) / m_nodes[id].dist;
                pos = Vector3.Lerp(m_nodes[id].transform.position, m_nodes[id+1 >= m_nodes.Count ? 0 : id+1].transform.position, lerpProg);
                return m_nodes[id];
            }
        }
    }

    public float GetTotalDistance()
    {
        return m_totalDist;
    }
}

ProgressAlongPathSpeed.cs

using UnityEngine;
using System.Collections;

public class ProgressAlongPathSpeed : MonoBehaviour {
    public PathGenerator path = null;
    public float speed = 2.0f;

    float m_progress = 0.0f;

    // Update is called once per frame
    void Update () {
        m_progress += ((1.0f / path.GetTotalDistance()) * speed) * Time.deltaTime;

        Vector3 pos = new Vector3();
        path.GetPointOnPath(m_progress, ref pos);
        transform.position = pos;
    }
}

ProgressAlongPathTime.cs

using UnityEngine;
using System.Collections;

public class ProgressAlongPathTime : MonoBehaviour {
    public PathGenerator path = null;
    public float time = 10.0f;
   
    float m_progress = 0.0f;
   
    // Update is called once per frame
    void Update () {
        m_progress += Time.deltaTime / time;
       
        Vector3 pos = new Vector3();
        path.GetPointOnPath(m_progress, ref pos);
        transform.position = pos;
    }
}

RotateToMovement.cs

using UnityEngine;
using System.Collections;

public class RotateToMovement : MonoBehaviour {
    public float turnSpeed = 3.0f;
    Vector3 m_lastLoc;

    // Use this for initialization
    void Start () {
        m_lastLoc = transform.position;
    }
   
    // Update is called once per frame
    void Update () {
        if(m_lastLoc != transform.position)
        {
            Vector3 dir = transform.position - m_lastLoc;
            dir.Normalize();
            Quaternion lookRot = Quaternion.LookRotation(dir);
            transform.rotation = Quaternion.RotateTowards(transform.rotation, lookRot, turnSpeed * Time.deltaTime);
        }

        m_lastLoc = transform.position;
    }
}