Realm Rush (prototype)
Status
Completed
Project Type
Personal
Software
Unity
Language(s)
C#
Role(s)
Gameplay programming
Realm Rush is a prototype of a 3D tower defense game. The goal is to stop enemies from making their way to your base by spawning up to 5 reusable towers. Those towers will fire at the enemies when in range.

NOTE: I was more focused on getting the pathfinding algorithm right than the UI for this game.
When creating the floor of the level you need the placement of the cubes to be accurate (stay on the grid). So we need to be able to snap the cubes onto specific points in the world. Unity does already have a way to snap the cubes onto the grid but you can only do that by holding down keys while moving them. I created a script that will snap the cubes to the grid while dragging them without needing to press any keys. It made the creation of the floor faster and accurate.

”animated”

Here is the code from my CubeEditor script which is attached to the cubes (known as Waypoints in this game) in the world. The most important part of this script is the [ExecuteInEditMode] on line 2. It lets this script run before you even press the play button. I thought that was pretty cool because now it can run while in Edit Mode. So my code that snaps the cubes to the grid will always run without me having to anything.

My SnapToGrid() method does the grid snapping in just 2 lines. On line 19, I am getting the value of the gridSize int variable, which is set to 10 in my Waypoint script. This means that 10f in the game's world is equal to 1 on the grid in the game. So if a cube is positioned at these coordinates(10,0,20), then we get (1,0,2) on the grid. Also, since we are in a 3D world, we will use the z coordinate as the y coordinate in a coordinate pair. This is because the y in the 3D world moves the cube up/down so that cubes would be on top/below each other instead of next to each other like on a 2D axis of just x and y. So, (1,0,2) is just (1,2), where x = 1 and y = 2.

The UpdateLabel() method just takes the cube's text mesh and labels it according to it's grid position. This is so you can see the coordinate of each cube in the world instead of having to click on each one to see it's coordinates. It also names the actual cube object by it's grid position. Of course, by the time you are reading this, I have already hidden the labels when preparing the game to be deployed, but you can the labels in the gif above.



I used the Breadth First Search algorithm to calculate the path that the enemies traverse. First we add the starting waypoint(cube) to the queue. While the queue has items in it, we check to see if we found the goal. If not then, we remove the waypoint we are currently on from the queue. Then we search in all specified directions for new waypoints and add them to the queue. Then, we mark the waypoint we took off the queue as 'explored'. We keep repeating those steps until we get to the end or the target waypoint.

”animated”

On line 62, we add the starting waypoint (assigned in Unity's Inspector) to the queue.

We go into a while loop which keeps executing until we find the end waypoint. On line 65, we remove the waypoint at the beginning of the queue (which would be the starting waypoint on the first execution).

Next we use the HaltIfEndFound() method to check if the current waypoint is the end waypoint. If it is we set isRunning to false to stop the while loop after it finsihes the current execution.

On line 69 we use the ExploreNeighbors() method to iterate 4 times (1 for each direction: up, down, left, right) find any waypoints and then add them to the queue. The code for this is below.

After that we mark the current waypoint as explored on line 71 and keep executing the while loop until we find the end waypoint.




Here is the ExploreNeighbors() method.

line 84 just helps us skip this method if the end waypoint was already found.

On line 86 we are iterating through an array of Vector2Ints that contain the Vector2Int representations of the directions up,dpwn,left and right.

On line 87 we get the getting the neighbors coordinates by adding the current Vector2Int direction to the waypoint we are currently searching from the queue. So if I was on the waypoint (2,2) and I searched up (which is (1,0)), I'd be at (3,2).

On line 89 I search the dictionary of waypoints to see if the neighbor coordinates match 1 of the coordinates in the dictionary. If it does, we use the QueueNewNeighbors() method to add thos neighbor waypoint to the queue (by passing in it's coordinates.) See below for the code.



Here is the QueueNewNeighbors() method:

On line 98, we get the waypoint from the neighborCoordinates we passed in. We use that to get the waypoint from the dictionary.

On line 100 we check to see if this waypoint we got has already been explored so we don't bother adding it to the queue again. Otherwise in the else statement on line 102, we add this waypoint to the queue on line 104.

Then on line 106, we mark this waypoint with the waypoint we used to get to it. This is important because after we calculate the path, we can add all the waypoints on the enemy's path (starting from the end since the start waypoint won't have been explored from another waypoint) to a list, reverse that list and have the enemy move according to the order of waypoints in that list.

Lines 108-115 is a solution to a problem I was having with the end waypoint not being found in the dictionary, even though the coordinates matched. This is because I assigned the end waypoint via Unity's Inspector, so it treated it as a different instance of the end waypoint with the same coordinates. So instead, I just compared the coordinates.


The player can place up to 5 towers on the cubes with yellow lines on them, "friendly cubes". I wanted the player to be able to move towers and still have a maximum of 5 towers in the world. I could have just destroyed a tower and then instantiated a new one, but I learned that can lead to memory fragmentation. If I destroy a tower it leaves a hole in memory and if I instantiate something that uses up more memory than what was there before, it will use up more memory and may affect the speed at which the game runs. So I implemented a ring buffer which takes the "eldest" tower (the tower that was placed at the beginning of the list) and just changes it's location to a new valid location that the player chooses.

”animated”

Here is the code from my TowerFactory script that is responsible for the ring buffer of towers. Note on line 49, before putting the old tower back to the top of the queue it is set to the new location (on line 46), which makes it the most recently placed tower.


Each tower uses a Particle System to fire bullets at the enemies.

”animated”

The snippet below is from my Tower script. My Shoot() method on line 82 accesses the Particle System's Emission Module (line 84), and then set it active or inactive depending what bool is passed in.

My FireAtEnemy() method looks at the distance between the enemy and tower (line 72) and uses the if statement on line 74 to determine if the tower should shoot that enemy or not. If the enemy is within a specified range (determined by the attackRange float in this script but not shown here) then we pass in true into the Shoot() method to fire at the enemy, otherwise we pass in false and the tower will not fire at the enemy.