Tower Defense Architecture In Unity: Dynamic Tower Targeting
Tower Defense games can trace their lineage all the way back to the 80’s with games like “Space Invaders” and “Missile Command” and even “Star Wars: The Empire Strikes Back”. The seed was sown with these games and later would blossom into the golden age of tower defense games with the emergence of Flash. “Bloons” was one of these games that shined through and has served as a major inspiration of mine and many others. Just last year, the latest entry in the franchise “Bloons TD 6” grossed around $56 million. It’s safe to say that the TD genera of games is here to stay.
Let’s have a crack at making one in Unity!
Today, we’re just going to focus on the targeting system of a basic tower. The Bloons games have a nice feature were you can set the targeting mode on the fly depending on the situation. First, Last, Strong, Weak. That’s the “dynamic” targeting that we’re going to implement.
First, let’s set up our prototype tower.
Add a plane and a capsule at the planes center. Then, we’ll add a flattened sphere as a child to the capsule to act as our visual enemy detection radius.
It’s not necessary, but for the DetectionRadius’ material, I just used a green with the transparency turned down.
Next, add a sphere collider, set to trigger, to the DetectionRadius and fit it to flattened sphere.
We’ll be able to easily upgrade the attack distance of the tower in the future by modifying the scale of the DetectionRadius object. The Sphere Collider will scale with it, which is what we want, because that does the detecting.
Ok, now it’s time to dig into some coding.
Make a script called EnemyDetection and add it to the DetectionRadius object.
The first thing we need to worry about is know when an enemy enters the radius and when the enemy leave the radius. We can easily do this with the unity events OnTriggerEnter() and OnTriggerExit() and the Tag system.
We’ll filter out all of the non- enemy trigger enters and exits with an “enemy” tag and then add that enemy to an in range targets list, that list will be on a different class, but we’ll get around to that shortly.
This is all this script is going to be responsible for. Detecting and then adding or removing from a list.
Now, create a script called BaseTower and put it on the “BaseTower” object. Next,create two public methods, AddTargetToInRangeList(Enemy target) and RemoveTargetFromInRangeList(Enemy target)
Jump back over to EnemyDetection and add a serialized BaseTower variable called baseTower. Then, in start, we’ll use a null coalescing statement to get the reference to the baseTower.
We can add the BaseTower reference manually, this is just a good fail safe in case we forget.
Now in our OnTrigger() Events, we’re going to access those methods we just added to the BaseTower script and pass in the detected enemy with GetComponent<Enemy>.
OK, that’s it for the EnemyDetection script. We now have a system that adds and removes enemies as they enter and leave the radius.
Next, we’re going to create a script called Enemy and then head back into the BaseTower script and serialize a list of type Enemy.
In the past, you had to initialize lists with = new list<Enemy>(), but in C#9 we can just use new(). This only works in Unity 2021+.
In our two functions, we’ll access this list and add or remove the target from the list.
Next, create a serialized Enemy variable called “currentTarget”.
Now create a function called “GetCurrentTarget”.
This function is going to house all of the logic for the dynamic targeting. We’ll create an Enum class with our four different targeting styles, first, last, strong, weak, and use those enums to determine how we should select our current target.
Let’s go ahead and make that Enum Class in a separate script.
Next, we’ll create two variables both of type TargetingStyle.
The currentTargetingStyle will be defaulted to the First style.
For the Strong and weak styles we’ll need a variable on the Enemy to sort them by to choose the strongest or the weakest.
Really quick, head over to the Enemy script, and create an auto property called BaseHealth
Now, back over to the BaseTower script in our GetCurrentTarget function we can do what we need to do. When GetCurrentTarget gets called, we’ll check our list to see if there are any available targets and if there are, then well use a switch expression, different than a switch statement, to filter through what we need.
There isn’t a major difference in using a switch expression over a switch statement, it’s just more concise in my opinion to use the expression.
We need to think about when to call this method.
When any enemy enters the radius, it needs to be called to make sure we are staying on the correct target.
We also need to call it whenever an enemy leaves because it could be our current target that left.
It also needs to be called anytime our targeting style is switched. There could be 5 enemies in range and if we switch from First to Last style, we’ll want to instantly update the target, not only when it leaves. That wouldn’t be very dynamic. Lastly, it’ll need to be called whenever our current target dies while in range so we can get a new target.
Add GetCurrentTarget calls to the Add/Remove methods.
That handles the first two cases. Now we need to handle changing targets on switching targeting styles. That’s where our previousTargetingStyle variable comes in handy.
in Start() in the BaseTower script, set previousTargetingStyle = currentTargetingStyle.
Now create a method called HandleTargetStyleSwitch().
and then in Update() check for when the previousTargetingStyle doesn’t equal the current targeting style and if it doesn’t, call HandleTargetStyleSwitch()
The switching of the Targeting style will eventually happen from UI that will change the currentTargetingStyle variable which will set off the Update conditional to call HandleTargetStyleSwitch() which will then call GetCurrentTarget.
OK, that’s the Third case for when to call the switch style method. Now we need to handle for when our current target dies.
Head to the Enemy script and create an Action called OnDeath.
Now well make a death routine to call that action when the enemy dies.
That’s as far as well get for the death routine in this article, but it will serve our purposes for now. Just laying the groundwork.
Now, we need to create a method to subscribe to the enemies onDeath event.
create a method called HandleTargetDeath().
This will only be called when our current target dies. Now, we need to subscribe and unsubscribe at strategic locations to make sure this method doesn’t get called when the enemy is out of our range. We only want to be subscribed to the enemies in our range.
In the GetCurrentTarget method, we can add a subscription and unsubscription to the current target’s OnDeath event.
Now anytime we call GetCurrentTarget, well subscribe and unsubscribe accordingly. We unsubscribe first if our current target isn’t null, to account for the first target we get, so that we only have one subscription active at a time and only on our current target.
We’re in the home stretch here. Now we just need some moving enemies to test this out.
Bake a navmesh on the plain and add an end point and a spawn point, childed to a game manager, just outside of the radius. Create a tag called “End” and change the tag of the EndPoint to that.
Create a capsule and name it enemy and prefab it. Add a kinematic checked rigid body to it, a NavMeshAgent component, and add the Enemy script to it. Create a tag called “Enemy” and set the tag to that.
This is a simple prototype movement solution, it will be more advanced later. Get the reference to the agent and set the destination to the endpoint by searching for the tag.
Next, Create a GameManager script and in Update() check for space bar input and instantiate the enemy at the spawn point.
Attach this script to the GameManager object and plug in the enemyPrefab and SpawnLocation variables.
One last touch to help us visualize the targeting, let’s head back to the BaseTower script and add this logic for a debug line to our current target.
This will always draw a line to our current target if we have one.
BAM! we’re done. Now let’s test it out! We’ll only look at First, and Last and then switching between the two. I’ll show the Weak and Strong and the death handling in the next article as I need to hook that up to UI to properly show the sorting and this article is already long enough. let’s check it out!
Targeting Style First.
Targeting Style Last.
Switching between the two on the fly.
There we have it. Dynamic targeting.
Thanks for reading.