top of page

Learning Unity - Realm Rush

  • Writer: Jason Warrick
    Jason Warrick
  • Sep 19, 2022
  • 10 min read

Updated: Sep 20, 2022

Created by following and expanding upon a GameDev.tv tutorial

Realm Rush is a 3D voxel tower-defense game!


We started the project by creating a grid system, using Unity's built-in grid manager, with some cubes.

From there we created a prefab of one tile, including a new label that will always display on top of other game objects, and remade the grid, albeit smaller this time.

In order to make our grid system easier to use, we then added a script to the text that updates it and the prefab name to reflect its position on the grid in the editor. This was pretty simple, but I did learn a bit about updates that run in the editor, which is a very welcome tip!

As you can see, it's mostly just a matter of getting the position and the text, updating them in the Update() method, and doing the same with the prefab name. Pretty easy!

Now that we have a pretty solid grid system, we're going to start working on our enemies! This video shows the first version of the enemy pathing system. At this stage, we hand load what waypoints (empty scripts attached to each tile) we want the enemy to visit, and in what order. Then, on Start(), we iterate through that list and print them all out, mostly for testing purposes.

Here's the first version of the enemy movement script, which really only creates and iterates through a list. It's a start though!

With a few minor tweaks we now have a moving enemy!

We started by making the movement method a coroutine that way it can be delayed and recalled (Hence the IEnumerator return value now). Then, it was just a matter of setting the enemy position to that of the waypoint instead of a debug message to get the movement working!

The next two lessons were more about visuals and prefabs, so there's not too much to show aside from the new textures and path.

Keeping with the theme of the last lesson, we then updated the enemy mesh with the voxel one, and updated the enemy movement. The enemy now smoothly moves from one waypoint to the next with variable speed and the proper rotation.

Here's the code for that, which is actually pretty expanded from the previous iteration, mostly due to the lerping in the FollowPath() method. The method for that lerp was one I hadn't thought about before, but it's an interesting way of doing it, as it ties it to the time passed, rather than some completely arbitrary factor which can be hard to gauge.

Now that the enemy is pretty much finished (at least for now) we can move onto the tower placing, which starts with the mouse interaction. The video shows that the player can now click tiles to "select" them, and will get feedback if the tile is available (paths are not available to be clicked).

This was a very simple addition to the waypoint script, with a lot of opportunity for expansion. That isPlaceable boolean is what we will manipulate to determine if the tile is available to the player.

Things are starting to look game-y! You can now place one tower per tile, and the heads on the towers track the battering rams, which is cool!

As you could probably guess, the code is pretty simple. Placing the tower is just a matter of instantiating it in the Waypoint class, and then the tower finds the target (via its EnemyMover script) and makes the top half of the tower look at it. I know that FindObjectOfType isn't very efficient, and might work in strange ways with multiple targets, so I hope we change that in the future (if not, I probably will).

Now even more gamified, the turrets can shoot bolts at the cart, which will get destroyed once enough hit it! The bolts are just projectiles, so there's not too much to see there, but I'll show you the code for the enemy health.

This code bears a striking resemblance to that of the enemy health code from Argon Assault, which is because it basically is that code! I do want to make some adjustments to account for different damage levels as you upgrade/unlock new turrets, but that can come later.

In order to make the development experience easier, we added a toggle for the labels to turn them on and off, as well as make them adaptive. I'll skip the code for this one, as it's really just enabling and disabling the script for the toggle, and adjusting the text color when necessary.

This might not be entirely visible from this video, but the enemies are now finding the path on their own via code, rather than hand loading them into a list. Additionally, they now always start at the first waypoint and get destroyed after reaching the last.

This might be a lot, but it's the updated EnemyMover class. Now, the first thing an enemy does is find the path themselves. They do this by finding the tagged parent path object, and then loading all of its children into the path list. The enemy is then moved to the start, follows the path, and then destroys itself upon path completion! How morbid!

The enemies are now even more self sufficient, as they spawn on their own now! It's currently on an infinite loop, but that will soon change.

Speak of the devil, here's that infinite loop I was just talking about! It's massively inefficient at the moment, but that will soon change!

The next lesson was a SUPER important one, as it covered object pooling, which is something I've needed to learn for a while. Yes it's pretty simple, but if you aren't taught it you'll never learn it and your games will run like garbage! For those of you that don't know what object pooling is, it's basically just creating a batch of game objects at the very start and then reusing them as needed, which you can see being done in the hierarchy on the left!

For a more technical overview, here's the pooling code! There were some adjustments made to different enemy scripts to account for this, but those were pretty minor.

Now it's the tower's turn to get a little smarter! They now only look at and fire upon enemies that are within their tower range.

Here's the code for that functionality too! It looks like a lot but it really just does exactly what you think it does.

The next 3 lectures were all kind of one big one, so I'll go through them quickly. The lectures were all about creating a currency system for the game, and we started with the bank and the enemies. The bank does what a normal bank would do (without interest) and the enemies now reward gold for being killed, and steal gold when they make it to the end of the level.

Now the other half of the game needs currency-ification, in the form of the towers! They now cost money to be placed, and in fact can't be placed if you don't have sufficient gold.

The final step was adding a very, very basic UI to complement this new system, which comes in the form of a "Gold" label that updates on any change made to the player's current balance.

I'll start off the technicals with the new tower script, which not only handles the cost of the towers, but also now handles its own instantiation, which is pretty cool!

Onto the meatier code, this is the bank script! It's essentially an extended, slightly more complex group of getters and setters, but hey, if it ain't broke!

After the addition of the currency, we did a little bit of balancing with the enemy health. Now, every time that the enemy dies, their HP increases by one. As you could guess, this makes the game impossible past a certain point, but that's not a massive concern for an early prototype like this.


Just as a note here, the next lesson was about refactoring code, so it was just some very minor optimizations and organization, so I don't feel like it's super necessary to include here


This is the halfway point of the section, and from here the game will change pretty drastically in its structure. So, at this point we have an (almost) feature complete tower defense game in the traditional style! Pretty cool!

The first step in the overhaul of our game is to get enemy pathfinding working, which starts by creating a node class and a way to interact with it. This is shown here with an empty game object with a GridManager script in it, which has one serialized node in it.

P.S. The map looks a bit different, I had to rebuild it because I made the first set of water prefabs wrong!

Here's that Node class which, as you might've noticed, is a Pure C# class. I'm not entirely clear on what that means, I just know that it means we have to specify that the class can be serialized, and that it can't be attached to a game object.

Since this section is so code heavy, I'm going to start with the code and then show it in action. The only thing the GridManager class does at the moment is creating and populating a grid. It does this with a dictionary and a nested for loop that iterates through all possible coordinates in our grid. It then assigns the coordinates as a key, and creates a node for the value of the cell.

You can see the grid getting populated in the console here, which will tell you how many individual nodes there are. This corresponds with the dimensions seen in the inspector.

In order to make debugging the pathfinding more efficient, we set up the coordinate labeler to indicate visually the state of each node. The default and unwalkable colors are the same, but if a node has been explored by the pathfinding it'll be yellow, and if it's the path itself it'll become orange! There's nothing to show for this yet as the pathfinding isn't made yet, so let's hope it works!


If you look closely at the (2,1) node and it's neighbors, you can see that they're colored! The given node is the "path" for testing purposes, and all of its existing neighbors are "explored." This isn't exactly how it'll end up working, but it shows that the pathfinder script (soon to be revealed) can find and search nodes!

And the wait is over! Here's the pathfinder script, which is attached to the object pool so the enemies have easy access to it. Currently, it's just finding a node and searching all of its neighbors. If each neighbor exists, it's added to a list of neighbors and appropriately colored.

The pathfinder is starting to pathfind! The yellow coordinates are ones that have been visited, and you can see that the pathfinder stopped visiting nodes once it reached (3,2), which is the end coordinate.

This is the updated pathfinder script, now including a BFS method! The general algorithm is that the search starts at an indicated node, explores all its neighbors, then moves onto each neighbor and explores its neighbors, until the destination is found.

As exemplified by this extensive demo, the pathfinding is working! It properly labels and finds the end node, and can work in any direction.

Here's the newer, bigger, and badder pathfinding code, that now includes a method that builds out a path. That's really the only major change and it was just a matter of looping back from the end node and seeing which node is connected to which. Not too bad!

In exciting news, the pathfinding now avoids blocked obstacles! We are steadily on our way to some dynamic pathfinding!

My apologies for the overwhelming screenshot, it just saves a lot of time for documentation. Anyways, the new waypoint (now tile) script on the left now labels each waypoint as blocked or not, using the BlockNode() method found in the GridManager class on the right! Those nodes are then labeled as "not walkable" and not factored into the pathfinding!

We're getting very close to a fully dynamic pathfinder, as it can now move around placed towers! Additionally for balance purposes, the player cannot place a tower if it will block the path to the end coordinate.

There were a few changes and additions to get this working, but the real moneymaker is this WillBlockPath() method. It essentially checks if there is a valid path if the given coordinate were to be blocked, then returns a boolean indicating whether it will be or not. This is then accessed by the tile script whenever the player attempts to place a tower.

As you can see, the enemies now follow the found path, which also changes dynamically and updates to the enemies! You would not believe the amount of frustration this (it was actually 2 sections but you get the idea) section caused me, and entirely due to my own mistake. My issue was that the path would be found perfectly and properly sent to the enemies, but they just wouldn't go anywhere. After a good hour of attempted bugfixing, I FINALLY realized that, when I converted from coordinates to positions, I was just setting the new position to itself, which was 0. After that, everything worked like a charm!

Since the sections were pretty long and covered a lot of different scripts, here's a video overview of the new changes. Essentially, the path that is given to the enemy is now found in the pathfinder, and when there's a change in the path, a message is broadcasted to the enemies, which then gives them a new path!

There are two changes in this video, one being very visible, and the other not so much! The more apparent change is that towers now take a second before they are functional, as a sort of balancing feature (plus it feels kinda cool). Additionally, the first enemy in a wave will no longer run into or through objects, which you probably didn't know anyways.

The new tower script isn't too different, aside from a new coroutine that handles the "construction" of the towers. Essentially, it just disables all of the components of the tower, then loops back through them one at a time and reenables them. I terms of pathfinding changes, it was a simple matter of starting the endCoordinate at the next node in the path, rather than the starting node.

Moving into polish territory, the game now has some more engaging and dramatic lighting! One thing I wanted to add was a light on the turrets, both to make them more visible as well as make the battlefield change visually a bit more over the course of the round.

The last bit of polish was just to add post-processing, which I'm very happy with! It's one of those things that I don't think of while I'm making it, but once I add it, I think "oh yeah, that's what a game looks like!"


Here's a quick little playthrough of the game in it's finished state! I didn't think I was going to like the pathfinding enemies but they're actually super fun, and I think the game is pretty solid! It's obviously not feature complete, and I don't intend on updating it after this, but as it stands I'm happy with it!


What I Learned

  • Object pooling

  • Pure C# classes

  • Dynamic enemy pathfinding

  • Broadcasting messages

Since so much of this tutorial was pathfinding-centric, it seems like there isn't that much. However, there is so much in pathfinding that it's pretty hard to summarize, so I definitely appreciate what I got out of this section.


Here's the link to the game page!


Ok, ok, I know I said I'm done, but I can't stand to have a game that doesn't have a pause or quit on my itch (ignore the past two tutorial games), so I added those, along with a restart button. Quit is Esc, pause is P, and restart is R, enjoy!

Comments


©2025 by Jason Warrick. Proudly created with Wix.com

bottom of page