Bala TD – tower defense

My tower defense game is available for android in the Google Play store at play.google.com/store/apps/details?id=com.balacraft.balapiano

It’s web version is available here on my site at balacraft.com/balatd

Project source is at github: github.com/balazon/BalaTD

1

It was developed in Unity, which is an industry leading game engine.

There are many tutorials, great documentation, and a community on the forums that are all contributing to get you started with unity, to get you up to speed on best practices, and to provide a helping hand when in need.

 
Prototype
For a game as complex as this, a prototype is fundamental. I’ll walk you through how to create a basic tower defense prototype.

Let’s start with a new project and an empty scene (by default we only have a camera, and a directional light). Change the intensity of the light to 0.5

Now create a terrain object (right click on empty space in the hierarchy, 3D Object / Terrain), and set some of its parameters:
terrain_settings
Set a new material for it (eg. mobile/diffuse, nature/diffuse).
Now paint a simple map with a path in it:
on the terrain component, click paint height, choose a brush , then choose one of these
– set height to 5 and paint the hills with it
– set the height to 5, click flatten, then paint the path with 0 height
terrain_paint
Adjust the camera so that it looks on the terrain from up top at an angle.
My terrain looks like this:
terrain_shape
To set up the navmesh, click on the Navigation tab in the editor, select Bake, adjust the parameters (I reduced the max slope, and the step height), then click Bake:
navigation
You can see the navmesh in the Scene view when the navigation tab is open:
navmesh

Next we’ll create a unit:
– create an empty GameObject called Unit
– add a Sphere as a child object with the Sphere Collider removed
– add a Nav Mesh Agent component
– add a script component called Unit

public class Unit : MonoBehaviour
{
	protected Transform goal;
	protected NavMeshAgent agent;

	void Awake()
	{
		agent = GetComponent<NavMeshAgent>();
		goal = GameObject.FindGameObjectWithTag("Goal").transform;
	}

	// Use this for initialization
	void Start ()
	{
		agent.destination = goal.position;
	}
	
	// Update is called once per frame
	void Update ()
	{
	
	}
}

– drag your Unit GameObject to the Assets\Prefabs folder
– move your Unit GameObject to the start of the path on the terrain
– create an empty GameObject called Goal, move it to the end of the path, and add the tag “Goal” so that the Unit’s script finds it inside the Awake function (optionally, add a child object so that the goal is visible)

Now press the play button and if you did everything correctly, the sphere will go through the path you’ve drawn on your terrain.
navmeshagent

Next, we’ll create a tower:
– create an empty GameObject, add a cylinder child (right click on it, 3D Object / Cylinder)
– add a sphere collider component, set its radius to something bigger (tower uses this sphere to detect units that are close), and mark its checkbox “Is Trigger”
– add a script component called Tower

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

public class Tower : MonoBehaviour
{
	public GameObject bulletPrefab;
	public float attackRate = 1.0f;
	public Vector3 bulletSpawnPoint = new Vector3(0, 2.5f, 0);
	public int sampleCount = 10;
	public float bulletInitialSpeedAngle = 20.0f;
	public float bulletInitialSpeedMagnitude = 2.0f;

	protected IList<Unit> unitsClose;
	protected Unit target;
	protected float cooldownRemaining = 0.0f;
	protected IList<Unit> removables;

	void Awake()
	{
		unitsClose = new List<Unit>(20);
		removables = new List<Unit>();
	}
	
	// Update is called once per frame
	void Update ()
	{ 
		if(cooldownRemaining > 0.0f)
		{
			cooldownRemaining -= Time.deltaTime;
		}
		else
		{
			RemoveInvalidTargets();

			Fire();
		}
	}

	void RemoveInvalidTargets()
	{
		removables.Clear();
		for (int i = 0; i < unitsClose.Count; i++)
		{
			if (unitsClose[i] == null || unitsClose[i].Dead)
			{
				removables.Add(unitsClose[i]);
			}
		}
		for (int i = 0; i < removables.Count; i++)
		{
			unitsClose.Remove(removables[i]);
		}
	}

	Unit FindTarget()
	{
		if(target != null && unitsClose.Contains(target))
		{
			return target;
		}

		float minDistSq = -1.0f;
		Unit minUnit = null;

		int count = unitsClose.Count;
		int loopCount = count < sampleCount ? count : sampleCount;
		for(int i = 0; i < loopCount; i++)
		{
			var index = (count < sampleCount) ? i : Random.Range(0, count);
			var unit = unitsClose[index];

			float distSq = (unit.transform.position - transform.position).sqrMagnitude;
			if (minUnit == null || distSq < minDistSq)
			{
				minUnit = unit;
				minDistSq = distSq;
			}
		}
		return minUnit;
	}

	void Fire()
	{
		target = FindTarget();
		if(target == null)
		{
			return;
		}

		var go = Instantiate(bulletPrefab, transform.position + bulletSpawnPoint, Quaternion.identity) as GameObject;
		Bullet b = go.GetComponent<Bullet>();
		b.init(target, bulletInitialSpeedAngle, bulletInitialSpeedMagnitude);

		cooldownRemaining = 1.0f / attackRate;
	}


	void OnTriggerEnter(Collider other)
	{
		var unit = other.GetComponent<Unit>();
		if (unit == null)
		{
			return;
		}

		unitsClose.Add(unit);
	}

	void OnTriggerExit(Collider other)
	{
		var unit = other.GetComponent<Unit>();
		if(unit == null)
		{
			return;	
		}

		unitsClose.Remove(unit);
	}
}

– drag your tower GameObject to the Prefabs folder

We also need a bullet:
– create an empty GameObject, add a sphere child (right click on it, 3D Object / Sphere), remove the collider component from it
– add a script component called Bullet:

using UnityEngine;
using System.Collections;

public class Bullet : MonoBehaviour
{
	public float baseDamage = 1.0f;
	public float Speed = 20.0f;
	public float Acceleration = 100.0f;
	// for estimating future position ahead by Tau time
	public float Tau = 0.5f;
	public float ReachingDistanceSquared = 1.0f;

	protected Unit target;
	protected Vector3 velocity;
	protected Vector3 lastTargetPosition;
	
	// Update is called once per frame
	void Update ()
	{ 
		Vector3 targetPos = target ? target.transform.position : lastTargetPosition;
		lastTargetPosition = targetPos;

		Vector3 toTarget = targetPos - transform.position;

		if (toTarget.sqrMagnitude < ReachingDistanceSquared)
		{
			ReachedTarget();
			return;
		}

		if (target && !target.Dead)
		{
			var targetAgent = target.GetComponent<NavMeshAgent>();
			toTarget += targetAgent.desiredVelocity * Tau;
		}

		Vector3 dir = toTarget.normalized;
		Vector3 force = dir * Speed - velocity;
		force = Vector3.ClampMagnitude(force, Acceleration);

		velocity += force * Time.deltaTime;
		transform.position += velocity * Time.deltaTime;
	}

	public void init(Unit target, float angle, float magn)
	{
		this.target = target;
		lastTargetPosition = target.transform.position;

		setInitialVelocity(angle, magn);
	}

	void setInitialVelocity(float angle, float magn)
	{
		Vector3 toTarget = (target.transform.position - transform.position);
		toTarget.y = 0;
		toTarget.Normalize();

		Vector3 rotAxis = new Vector3(-toTarget.z, 0, toTarget.x);
		toTarget = Quaternion.AngleAxis(angle, rotAxis) * toTarget;
		velocity = toTarget * magn;
	}

	void ReachedTarget()
	{
		if (target && !target.Dead)
		{
			target.TakeDamage(baseDamage);
		}

		Destroy(gameObject);
	}
}

Checking if target is reached is done manually in code with the boolean expression: (toTarget.sqrMagnitude < ReachingDistanceSquared)
You could use a trigger collider, but the problem is, when another bullet kills your bullet’s target, the bullet won’t be destroyed when reaching the target position, which is why I used the simple boolean expression above to check if target is reached.
SetInitialVelocity just makes sets the bullet’s initial velocity at an angle and magnitude, but it might not be necessary. The important tweakable values are Speed, Acceleration, and Tau. The bullet’s movement is essentially a Pursue steering behavior (check my previous post: steering behaviors), we use the agent’s desired velocity and tau to estimate a future position, which is then seeked by applying a force whose direction is the difference between desired velocity (dir * Speed), and current velocity. Other methods could be used for projetile movement, like shooting off the bullet with the simple initial velocity, using only gravity, and not correcting its velocity while it moves. But I wanted to create a bullet that always finds its target.

– drag your bullet GameObject to the Prefabs folder
– drag your bullet prefab from the Prefabs folder to the tower prefab’s “Bullet Prefab” variable in the editor

Unit has to be modified:
– add a Rigidbody component with “Is Kinematic” on (because its movement is not controlled by physics, but instead by the Nav Mesh Agent component)
– add a Sphere collider
– modify its script

using UnityEngine;
using System.Collections;

public class Unit : MonoBehaviour
{
	public float maxHealth = 4.0f;
	public float Health { get; protected set; }

	protected Transform goal;
	protected NavMeshAgent agent;

	public bool Dead { get; protected set; }

	void Awake()
	{
		Dead = false;
		Health = maxHealth;

		agent = GetComponent<NavMeshAgent>();
		goal = GameObject.FindGameObjectWithTag("Goal").transform;
	}

	// Use this for initialization
	void Start ()
	{
		agent.destination = goal.position;
	}
	
	public void TakeDamage(float damage)
	{
		Health -= damage;
		if (Health <= 0.0f)
		{
			Health = 0.0f;
			Killed();
		}
	}

	void Killed()
	{
		if (Dead)
		{
			return;
		}
		Dead = true;

		agent.Stop();
		agent.obstacleAvoidanceType = ObstacleAvoidanceType.NoObstacleAvoidance;
		Destroy(gameObject);
	}
}

– apply the changes to your prefab

Now you can create instances from your prefabs in your scene, and watch how the units try to go through the path, while the towers shoot them down.
towerdef_proto

I admit this prototype might be a long way from a finished product, but it’s a proof of concept, and it’s often useful to make prototypes like these and experiment with them at the start to get a feel for your game, what’s good or bad about it, and how it can be improved.

For easier access, I put the prototype up on github here: github.com/balazon/TowerDefenseProtoUnity

 
User Interface
At last, here’s some insight about the finished game’s user interface.
Undoubtedly, the UI can be a huge spaghetti code if we’re not careful. I admit, my UI in the end is still somewhat hardcoded, and not very readable, but using a few tricks helped mitigate the problem.

For starters, create a folder for your GUI scripts, and put every GUI script here so that they don’t get mixed with the other scripts.

Create a GuiHandler script:
-store the GameObject references that have some UI panel/context to show (eg. main menu, high scores, game over screen, pause menu, HUD)
-function to hide all these panels – call this whenever a context change requires it, then show the panels you want
-public functions that handle what should happen when a specific button is clicked (like a NewGame function for when the user clicks the “New Game” button)

On the UI Button:
-use UI Button’s On Click() event and call the public function you defined in GuiHandler
-set the navigation properties so that the arrow keys will select the desired UI element (I used the explicit mode)

On the Canvas Scaler (this component should be somewhere in your root GUI GameObject next to a Canvas component)
-set “UI Scale Mode” to “Constant Physical Size” so that when you build for android, the size of your UI elements are only dependent on the phone/tablet’s physical size, and not its resolution (of course this could make them too small for an android TV, but a TV is a whole other story anyway)
-before you’re building to HTML5 or a desktop platform (PC/Mac) then change this mode temporarily to “Scale With Screen Size” and set reference resolution so that your UI looks big enough

Design your GUI scripts so that if you removed them, the project still compiles (Gameplay objects should know virtually nothing of the GUI : www.glenstevens.ca/unity3d-best-practices/#Class_Design). This sentence saved me a ton of headache. How then you might ask does a GameObject notify the GUI when it needs to change? The answer is Observer pattern. I used a rather simplistic version of it for my game, but I found it sufficient:

public class MyObservable
{
	public delegate void MyVoidDelegate();
	
	public MyVoidDelegate notify;
	
	public void notifyObservers()
	{
		if(notify != null)
		{
			notify();
		}
	}
}

I used instances of this class in Gameplay classes (favor composition over inheritance), and whenever something changed the state of the object, I notified any GUI object that might be listening for changes.

public class SomeGameplayClass : MonoBehavior
{
	public MyObservable observable {get; private set;}

	void Awake()
	{
		observable = new MyObservable();
	}

	void FunctionThatChangesState()
	{
		//... function body
		observable.notifyObservers();
	}
}
public class SomeGUIClass : MonoBehavior
{
	void Start()
	{
		SomeGameplayClass object = ...; //get reference somehow
		object.observable.notify += SomeGameplayClassChanged;
	}

	public void SomeGameplayClassChanged()
	{
		//... update GUI
	}
}

I said that gameplay objects shouldn’t know anything of the GUI, but the reverse direction is important too: the GUI can (and probably has to) depend on your gameplay classes. Of course you could create interfaces for all your gameplay classes so that the GUI needn’t know anything about your gameplay implementations, but you might end up with a lot of interfaces and that adds to the complexity of your design.

For simplicity my GUI objects retrieved the whole state of the gameplay object they were listening on, and refreshed everything, since my gameplay objects did not have many state variables. If they did, it would have been wiser to replace the void delegate type in MyObservable to a delegate type that takes an event parameter from which a GUI object could determine what parts (state) it had to query, and update.

That’s all I had in mind about my tower defense game, feel free to try it out, tweak values or modify it in any way you want (links are at the top).

For those of you yet unfamiliar with Unity, give it a go, you won’t be disappointed.

Leave a Reply

Your email address will not be published. Required fields are marked *