This is the first installment of a tutorial series about creating a simple tower defense game. It covers the creation of the game board, pathfinding, and placement of destinations and walls.

This tutorial is made with Unity 2018.3.0f2.

It is a chained assignment. In this case, it means that we assign a reference to the instantiated tile to both the array element and the local variable. It does the exact same thing as the below code.

We'll need to access the tiles later, so keep track of them with an array. We don't need to use a list because we won't change the board size once initialized.

Then it can instantiate them in Initialize with a double loop over the two dimensions of the grid. Although the size is expressed in X and Y, we place the tiles in the XZ plane, just like the board itself. As the board is centered on the origin, we have to subtract the relevant size minus one, divided by two, from the tile position's components. Note that this must be a floating-point division, otherwise it won't work for even sizes.

Note that the tiles themselves don't really need to be game objects. They're purely for keeping track of the board state. We could've used the same approach as for behavior in the Object Management series. But game objects work just fine for the early stages of simple or prototype games. We might change this in the future.

You can open prefab-editing mode by double-clicking the prefab asset or by selecting the prefab and clicking the Open Prefab button in its inspector. You can exit the prefab-editing mode via the arrow button at the top left of its hierarchy header.

Construct a tile object and turn it into a prefab. The tiles will be aligned with the ground, so move the arrow up a bit to prevent depth issues during rendering. Also, scale the arrow down a bit so there's some space between adjacent arrows. An Y offset of 0.001 and a uniform scale of 0.8 will do.

We'll use a game object to represent each tile in the game. Each will have its own quad with the arrow material, just like the board has its ground quad. We'll also give tiles a custom GameTile component, with a reference to their arrow.

That makes it possible for the arrow to be shadowed when using Unity's default render pipeline.

Put the arrow texture in your project and enable its Alpha As Transparency option. Then create a material for the arrow, which can be a default material set to cutout mode, with the arrow as its main texture.

The board is made up of square tiles. Enemies will be able to walk from tile to tile, across edges but not diagonally. Movement will always be toward the nearest destination. Let's visualize the movement direction per tile, with an arrow. Here is an arrow texture for that.

Via the dropdown menu accessible via the gear button at the top right corner of the component.

Now we get a properly-sized board after entering play mode. While playing, position the camera so the entire board is clearly visible, copy its transformation component, exit play mode, and paste the component values. For an 11×11 board at the origin, you get a nice top-down view by placing the camera at (0,10,0) and rotating it 90° around the X axis. We'll keep the camera at this fixed orientation, but might make it move in the future.

OnValidate is the only place in our code where we should ever assign values to component configuration fields.

If it exists, the Unity editor invokes it on components after they might have changed. This includes when they're added to a game object, after a scene is loaded, after a recompilation, after an edit via the inspector, after an undo/redo, and after a component reset.

Board sizes can only be positive, and it doesn't make much sense to have a board with only a single tile. So let's enforce 2×2 as the minimum. We can do that by adding an OnValidate method that enforces the minimum.

Next, create a Game component that is in charge of the entire game. At this point that means initializing the board. We'll simply make the size configurable via its inspector and have it initialize the board when it awakens. Let's use 11×11 as the default size.

Although the game plays in 2D, we're going to render it in 3D, with 3D enemies and a camera that could be moved around at some point. The XZ plane is more convenient for that and matches the default skybox orientation used for environmental lighting.

Create the board object in a new scene and give it the required quad child, with a material that makes it look like ground. As we're creating a simple prototype-like game, a uniform green material will do. Also rotate the quad 90° around its X axis so it lies flat in the XZ plane.

The idea is that everything that we configure via the Unity editor is exposed via serialized private fields. These fields should only be changed via an inspector. Unfortunately, the Unity editor will always show a compiler warning complaining that the value is never assigned to. We can suppress this warning by explicitly assigning a default value to the field. We could assign null , but I make it explicit that we just use the default value, which doesn't represent a valid ground reference, so use default instead.

Besides that, we'll visualize the board with a single quad that represents the ground. We won't make the board object a quad itself, instead we'll give it a quad child object. When initializing, we make the ground's XY scale equal to the board's size. So each tile is one square unit.

The game board is the most important part of the game, so we'll create it first in this tutorial. It's going to be a game object with a custom GameBoard component that can be initialized with a 2D size, for which we can use a Vector2Int value. The board should work with any size, but we'll decide which to use somewhere else, so we'll provide a public Initialize method for that.

Tower defense is a game genre where the goal is to destroy hordes of enemies before they reach their destination. This is done by building towers that attack those enemies. There are many variations of the genre. We'll create a game with a tiled board. Enemies will move across the board toward their destination, while the player places obstacles to hinder them.

Pathfinding

At this point each tile has an arrow, but they're all pointing in the positive Z direction, which we'll interpret as north. The next step is to figure out the correct direction per tile. We do that by finding the paths that the enemies will follow to get to their destination.

Tile Neighbors Paths go from tile to tile, in either north, east, south, or west direction. To make searching easy, have GameTile keep track of references to its four neighbors. GameTile north, east, south, west; The neighbor relationship is symmetrical. If a tile is the eastern neighbor of a second tile, then the second tile is the western neighbor of the first tile. Add a public static method to GameTile to establish that relationship between two tiles. public static void MakeEastWestNeighbors (GameTile east, GameTile west) { west.east = east; east.west = west; } Why make it a static method? We could make it an instance method with a single parameter as well, in which case we could invoke it as eastTile.MakeEastWestNeighbors(westTile) or something like that. But in cases where it isn't clear which of the tiles the method should be invoked on a static method is a good approach. Examples are the Distance and Dot methods of Vector3 . Once that relationship has been established it should never be changed. If that does happen we made a programming mistake. We can verify this by checking whether both references are null before assigning them and log an error if that's not the case. We can use the Debug.Assert method for that. public static void MakeEastWestNeighbors (GameTile east, GameTile west) { Debug.Assert( west.east == null && east.west == null, "Redefined neighbors!" ); west.east = east; east.west = west; } What does Debug.Assert do? If the first argument is false , it logs an assertion error, using the second argument message if provided. This invocation is only included in development builds, not in release builds. So it's a good way to add checks during development that won't affect the final release. Add a similar method to create a north-south neighbor relationship. public static void MakeNorthSouthNeighbors (GameTile north, GameTile south) { Debug.Assert( south.north == null && north.south == null, "Redefined neighbors!" ); south.north = north; north.south = south; } We can establish these relationships when the tiles are created in GameBoard.Initialize . If the X coordinate is larger than zero then we can make an east-west relationship between the current tile and the previous one. If the Y coordinate is larger than zero then we can make a north-south relationship between the current tile and the one from a row earlier. for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … if (x > 0) { GameTile.MakeEastWestNeighbors(tile, tiles[i - 1]); } if (y > 0) { GameTile.MakeNorthSouthNeighbors(tile, tiles[i - size.x]); } } } Note that the tiles on the edge of the board have fewer than four neighbors. Either one or two or their neighbor references remain null .

Distance and Direction We're not going to have all enemies search for a path all the time. We only have to do it once per tile. Then enemies can then query the tile they're in for where to go next. We store that information in GameTile by adding a reference to the next tile on the path. Besides that, we'll also store the distance to the destination, expressed as the amount of tiles that still have to be entered before reaching the destination. That's not useful information for enemies, but we'll use it when finding the shortest paths. GameTile north, east, south, west , nextOnPath ; int distance; Each time we decide to find the paths, we must initialize the path data. Before a path is found, there isn't a next tile yet and the distance can be considered infinite. We can represent that with the largest possible integer value, int.MaxValue . Add a public ClearPath method to reset GameTile to this state. public void ClearPath () { distance = int.MaxValue; nextOnPath = null; } It is only possible to find any paths if there is a destination. That means that a tile has to become the destination. Such a tile has a distance of zero and there is not next tile as the path ends here. Add a public method to turn the tile into a destination. public void BecomeDestination () { distance = 0; nextOnPath = null; } Eventually, all tiles should have a path, so their distance is no longer equal to int.MaxValue . Add a convenient getter property to check whether a tile currently has a path. public bool HasPath => distance != int.MaxValue; How does that property work? It's a shorthand to define a getter property that contains only a single expression. It does the exact the same thing as the below code. public bool HasPath { get { return distance != int.MaxValue; } } The => arrow operator can also be used for the getter and setter parts of a property separately, for method bodies, constructors, and in some other places.

Growing the Path If we have a tile with a path, we can let it grow the path toward one of its neighbors. Initially the only tile with a path is the destination, so we start at distance zero and increase it from there, going in the opposite direction that the enemies will move. So all direct neighbors of the destination will have distance 1, and all other neighbors of those tiles will have distance 2, and so on. Give GameTile a private method to grow the path to one of its neighbors, defined via a parameter. The neighbor's distance becomes one longer than its own, and the neighbor's path points toward this tile. The method should only be invoked on tiles that already have a path, so assert that. void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); neighbor.distance = distance + 1; neighbor.nextOnPath = this; } The idea is that we invoke this method once for each of the four neighbors of the tile. As some of those references will be null , check for that and abort if so. Also, if a neighbor already has a path then we have nothing to do and can abort as well. void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); if (neighbor == null || neighbor.HasPath) { return; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; } How GameTile keeps track of its neighbors is unknown to other code. That's why GrowPathTo is private. Instead, we'll add public methods that instruct a tile to grow its path in a specific direction, indirectly invoking GrowPathTo . But the code that takes care of searching the entire board must keep track of which tiles have been visited. So have it return the neighbor, or null if we aborted. GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null ; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor; } Now add the methods to grow the path in specific directions. public GameTile GrowPathNorth () => GrowPathTo(north); public GameTile GrowPathEast () => GrowPathTo(east); public GameTile GrowPathSouth () => GrowPathTo(south); public GameTile GrowPathWest () => GrowPathTo(west);

Breadth-First Search It is the responsibility of GameBoard to make sure all its tiles contain valid path data. We'll implement this by performing a breadth-first search. We start with a destination tile, then grow the path to its neighbors, then to the neighbors of those tiles, and so on. Each step the distance increases by one, and paths are never grown toward tiles that already have a path. This guarantees that all tiles end up pointing along the shortest path to the destination. What about A* pathfinding? A* is an evolution of breadth-first search. It is useful when you're searching for a single shortest path. But we need all shortest paths, so A* provides no benefit. See the Hex Map series for examples of both breadth-first search and A* applied to a hex grid, with animations. To perform the search, we have to keep track of the tiles that we've added to the path, but haven't grown the path out from yet. That collection of tiles is often known as the search frontier. It is important that tiles are processed in the same order that they're added to the frontier, so let's use a Queue . We'll end up searching more than once later, so define it as a field in GameBoard . using UnityEngine; using System.Collections.Generic; public class GameBoard : MonoBehaviour { … Queue<GameTile> searchFrontier = new Queue<GameTile>(); … } To always keep the board state valid, we should find the paths at the end of Initialize , but put the code in a separate FindPaths method. The first step is to clear the path of all tiles, then make one tile the destination and add it to the frontier. Let's just pick the fist tile. As tiles is an array we can use a foreach loop without worrying about memory pollution. If we switch to a list later, we should also replace the foreach loops with for loops. public void Initialize (Vector2Int size) { … FindPaths(); } void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); } The next step is to take the single tile out of the frontier and grow the path to its neighbors, adding them all to the frontier. First go north, then east, then south, and finally west. public void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); } Repeat this step as long as there are tiles in the frontier. while (searchFrontier.Count > 0) { GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); } Growing the path doesn't always yield a new tile. We could check whether we got null before enqueueing, but we can also delay the null check until after we dequeue. GameTile tile = searchFrontier.Dequeue(); if (tile != null) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); }

Showing the Paths We now end up with a board containing valid paths, but we don't see this yet. We have to adjust the arrows so they point along the path through their tile. We can do that by rotating them. As these rotations are always the same, add static Quaternion fields to GameTile , one per direction. static Quaternion northRotation = Quaternion.Euler(90f, 0f, 0f), eastRotation = Quaternion.Euler(90f, 90f, 0f), southRotation = Quaternion.Euler(90f, 180f, 0f), westRotation = Quaternion.Euler(90f, 270f, 0f); Add a public ShowPath method as well. If the distance is zero then the tile is a destination and it has nowhere to point to, so deactivate its arrow. Otherwise, active the arrow and set its rotation. The correct rotation can be found by comparing nextOnPath with its neighbors. public void ShowPath () { if (distance == 0) { arrow.gameObject.SetActive(false); return; } arrow.gameObject.SetActive(true); arrow.localRotation = nextOnPath == north ? northRotation : nextOnPath == east ? eastRotation : nextOnPath == south ? southRotation : westRotation; } Invoke this method on all tiles at the end of GameBoard.FindPaths . public void FindPaths () { … foreach (GameTile tile in tiles) { tile.ShowPath(); } } Found paths. Why not rotate the arrow directly in GrowPathTo ? To keep search logic and visualization separate. Later on, we'll make the visualization optional. If arrows aren't shown, then we don't need to rotate them every time we invoke FindPaths .