A Tower Defense game in Unity, part 1

If you’re in a hurry, you can find the complete source code on GitHub: https://github.com/dgkanatsios/TowerDefense and the second part of the tutorial is posted here: https://dgkanatsios.com/2014/09/06/a-tower-defense-game-in-unity-part-2-3/. In part 1, we will see how the game levels are saved/loaded from XML files and a mechanism we use to optimize our game, called object pooling. In part 2, we will visit the core tower defense game mechanics. You can also test the game in WebGL here.

Unless you’ve been living in a cave in the recent years, you surely must have played a Tower Defense style game. Be it Plants vs Zombies, Kingdom Rush, geoDefense, Jelly Defense or any other, I’m sure you’ve spent much quality time setting up your defenses, killing enemies and advancing stages. So, since it’s one of the most common game types you can find on the application stores out there, I decided to try my luck and create one from scratch, using Unity3D and present it in this tutorial. Since it’s a bit bigger compared to my previous efforts, it will be split in two parts. In this first post, we’ll describe the game, the level editor, the respective XML creation and parsing and the object pool used in the game to increase performance. Check the screenshot below to get a taste of how the game looks like, running on the Unity editor.

image_1B244292.png

Scenario and gameplay are both pretty basic, actually. Badgers are attacking user’s bunny house and she has her Chuck Norris trained bunnies to protect it, by shooting arrows. In order to be able to create more protector bunnies, user needs carrot money. If user kills all the badgers after the predetermined number of rounds, she wins. If enough badgers get into the bunny house, user loses! Badgers follow a path in order to approach the house upon which protector bunnies cannot be placed.

Let’s dive a bit deeper into the game mechanics and gameplay.

Bunny House: Initial life is 10, each badger that arrives If 10 badgers arrive at the house, game is over.

Path: The path that the badgers will walk on in order to arrive at the house. Specific waypoints will designate their direction.

Badger: Our enemy. It has a speed property and a health component, which is decreased when it is hit by an arrow. It follows waypoints (non-visible gameobjects) that are placed on the path pieces.

– Bunny: Our defense. It can shoot arrows at a constant fire rate. It starts its activity by looking for an enemy at a close vicinity. If it finds one, it starts shooting. If the enemy dies or leaves the vicinity, it searches for another enemy. Has a standard carrot cost to create.

Carrot: Falling randomly from the top of the screen. User needs to tap/click on them in order to increase money, to create more bunnies.

BunnyGenerator: (yeah, I could have come up with a better name) It’s the bunny on the lower left part of the screen. User needs to drag it in order to create new bunnies. On areas that a new bunny cannot be created (such as on the path), it will highlight them as red. Upon a new bunny creation, a standard amount of money will be taken of the user’s account.

Level Generator:  All game levels are to be stored in an XML file that uses a standard format. The Unity developer has the option to use a custom made Unity editor that saves the file from the scene editor to an XML file.

We also have a couple of “roles” that will be mentioned in this tutorial

Unity developer: The one that will use our custom editor to create new levels for the game

User/gamer: The end user that will enjoy our game!

Level Generator

The XML file structure

As we mentioned, all game levels are stored in an XML file. When we design something like this, we first need to determine the stuff that will be placed into it. Take a look at the Level1.xml file contents, located in Resources folder (in order to be able to pull its into during runtime).

1234

From the above picture, you can see that we store the initial money of the user, two variables to modify the spawn time for the carrots, the location of the bunny house (thereafter “Tower”), X and Y location for the PathPieces (the path that our enemies will walk on), X and Y location for the waypoints (that our enemies will follow in order to get to the bunny house) and a minor piece of information for each game Round. Specifically, PathPieces contain information about the level path sprites and Waypoints are to be used for empty GameObjects creation that will “guide” the enemies to the Tower. The carrot spawn time will be randomly chosen each time and it will be clamped between the minimum and maximum values we set. Before we dive into the code, let’s mention that game code was written in Visual Studio (check the free Community edition here). For debugging purposes, don’t forget to check the Visual Studio tools for Unity here.

XML Parsing

All this information is mapped to the LevelStuffFromXML class

namespace Assets.Scripts
{
    /// <summary>
    /// Simple class to hold all our level details
    /// </summary>
    public class LevelStuffFromXML
    {
        public float MinCarrotSpawnTime;
        public float MaxCarrotSpawnTime;
        public int InitialMoney;
        public List<Round> Rounds;
        public List<Vector2> Paths;
        public List<Vector2> Waypoints;
        public Vector2 Tower;
        public LevelStuffFromXML()
        {
            Paths = new List<Vector2>();
            Waypoints = new List<Vector2>();
            Rounds = new List<Round>();
        }

    }

    /// <summary>
    /// Some basic information about each game round
    /// </summary>
    public class Round
    {
        public int NoOfEnemies { get; set; }
    }


    
}

We use the ReadXMLFile method to obtain information from the XML file at runtime. We could use serialization for the class but we instead chose LINQ to XML because 1. it rulezzz 2. its syntax is gorgeous! and 3. it allows for greater control of how your object stuff/properties are converted to XML and vice versa. Consequently, we use the XDocument API to parse the Level1.xml file in the resources folder and create a LevelStuffFromXML instance, to be used in our game. Code used is pretty straightforward and self-explainable but, nevertheless, we use a top-down approach starting from the root XML element and traversing the tree to the bottom, fetching specific attributes and xml elements along the way. Should be pretty clear for you, even if you’re a beginner!

      public static LevelStuffFromXML ReadXMLFile()
        {
            LevelStuffFromXML ls = new LevelStuffFromXML();
            //we're directly loading the level1 file, change if appropriate
            TextAsset ta = Resources.Load("Level1") as TextAsset;
            //LINQ to XML rulez!
            XDocument xdoc = XDocument.Parse(ta.text);
            XElement el = xdoc.Element("Elements");
            var paths = el.Element("PathPieces").Elements("Path");

            foreach (var item in paths)
            {
                ls.Paths.Add(new Vector2(float.Parse(item.Attribute("X").Value), float.Parse(item.Attribute("Y").Value)));
            }

            var waypoints = el.Element("Waypoints").Elements("Waypoint");
            foreach (var item in waypoints)
            {
                ls.Waypoints.Add(new Vector2(float.Parse(item.Attribute("X").Value), float.Parse(item.Attribute("Y").Value)));
            }

            var rounds = el.Element("Rounds").Elements("Round");
            foreach (var item in rounds)
            {
                ls.Rounds.Add(new Round()
                {
                    NoOfEnemies = int.Parse(item.Attribute("NoOfEnemies").Value),
                });
            }

            XElement tower = el.Element("Tower");
            ls.Tower = new Vector2(float.Parse(tower.Attribute("X").Value), float.Parse(tower.Attribute("Y").Value));

            XElement otherStuff = el.Element("OtherStuff");
            ls.InitialMoney = int.Parse(otherStuff.Attribute("InitialMoney").Value);
            ls.MinCarrotSpawnTime = float.Parse(otherStuff.Attribute("MinCarrotSpawnTime").Value);
            ls.MaxCarrotSpawnTime = float.Parse(otherStuff.Attribute("MaxCarrotSpawnTime").Value);

            return ls;
        }

One may ask, why save on an XML file and not save each level into its own scene? Well, this could also be done. However, we opted to use this method for the following reasons

  • XML is a human readable standard, can be altered without opening the Unity editor
  • Our game could possibly download new levels just by adding additional XML files to a server
  • Users/gamers could create new levels via a custom (non-Unity editor) and upload them on a community site, where users could rate and download
  • And, of course, pure educational reasons (that includes this tutorial’s own existence!)

Editor Window

So, you might ask, does one have to write the XML file by herself? Well, one could do that! However, we wanted to provide a convenient way for the Unity developer to create the XML file, so we created a small custom Unity editor window. If you open the project on Unity, you will see a new menu item titled “Custom editor” with a single command named “Export level”.

image_11D8EEE7.png

If you click on that, you will be presented with the below window

image_6F17B729

By clicking on the “Add new round” button, you can create a new round that has specific number of enemies, based on the value of the below slider. Try it! After a few rounds’ addition, result can be like this

image_3A0DB8AA.png

That’s the way to create the rounds for our level. Of course, you can delete a round, if you did a mistake. Moreover, you can also modify values for initial money, the carrot spawn times and the desired filename for the exported level. If you try and export the level in an empty scene, you’ll be presented with the below error message.

image_4B80AA33.png

Dear friends, every user input is evil!!! You should always protect user input stuff in apps *and* games for bad input. In our case, we do not have a tower object, so the export script would create an well-formed xml file for the game (syntactically correct) but it would not be proper, since we absolutely need a tower object for each level (logically incorrect). Let’s now see the code in depth.

Code

First of all, the script is located on folder called Editor. This is a folder recognized by Unity (via name convention) to contain editor related code.

image_68F58BF0.png

The corresponding class is not a MonoBehavior, like the ones we’ve been building so far. It’s a special type of class, called EditorWindow, with a static method ShowWindow and a corresponding C# attribute, to designate the menu item to be created for this purpose. We also have some variables to help with our editor window creation.

public class LevelExport : EditorWindow
{
    [MenuItem("Custom Editor/Export Level")]
    public static void ShowWindow()
    {
        EditorWindow.GetWindow(typeof(LevelExport));
    }

    Vector2 scrollPosition = Vector2.zero;
    int noOfEnemies;
    int initialMoney;
    int MinCarrotSpawnTime, MaxCarrotSpawnTime;
    string filename = "LevelX.xml";
    int waypointsCount;
    int pathsCount;

The OnGUI method helps with the appearance of the necessary GUI elements.

    void OnGUI()
    {
        scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
        EditorGUILayout.LabelField("Total Rounds created:" + rounds.Count);
        for (int i = 0; i < rounds.Count; i++)
        {
            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.LabelField("Round " + (i + 1));
            EditorGUILayout.LabelField("Number of Enemies " + rounds[i].NoOfEnemies);
            if (GUILayout.Button("Delete"))
            {
                rounds.RemoveAt(i);
            }
            EditorGUILayout.EndHorizontal();
        }
        EditorGUILayout.EndScrollView();

        EditorGUILayout.LabelField("Add a new round", EditorStyles.boldLabel);
        noOfEnemies = EditorGUILayout.IntSlider("Number of enemies", noOfEnemies, 1, 20);

        if (GUILayout.Button("Add new round"))
        {
            rounds.Add(new Round() { NoOfEnemies = noOfEnemies });
        }
        initialMoney = EditorGUILayout.IntSlider("Initial Money", initialMoney, 200, 400);
        MinCarrotSpawnTime = EditorGUILayout.IntSlider("MinCarrotSpawnTime", MinCarrotSpawnTime, 1, 10);
        MaxCarrotSpawnTime = EditorGUILayout.IntSlider("MaxCarrotSpawnTime", MaxCarrotSpawnTime, 1, 10);
        filename = EditorGUILayout.TextField("Filename:", filename);
        EditorGUILayout.LabelField("Export Level", EditorStyles.boldLabel);
        if (GUILayout.Button("Export"))
        {
            Export();
        }
    }

We use a scrollview (via the EditorGUILayout.BeginScrollView and EditorGUILayout.EndScrollView methods) to host the number of the rounds that the Unity developer will create (since she may create a lot of rounds). For each round, we display a label (mentioning the round number), the number of enemies for this round and a button to delete the round. We then continue by displaying an IntSlider that will used to designate how many enemies we want, plus a button to add a new round with the required number of enemies. We finish up the creation of our GUI by creating some other IntSliders that accept the initial money amount and the carrot spawn time (minimum and maximum). Finally, we have a button that will call the Export method that will save our new level, if the Unity developer has created it properly.

    void Export()
    {
        // Create a new output file stream
        doc = new XDocument();
        doc.Add(new XElement("Elements"));
        XElement elements = doc.Element("Elements");


        XElement pathPiecesXML = new XElement("PathPieces");
        var paths = GameObject.FindGameObjectsWithTag("Path");
       
        foreach (var item in paths)
        {
            XElement path = new XElement("Path");
            XAttribute attrX = new XAttribute("X", item.transform.position.x);
            XAttribute attrY = new XAttribute("Y", item.transform.position.y);
            path.Add(attrX, attrY);
            pathPiecesXML.Add(path);
        }
        pathsCount = paths.Length;
        elements.Add(pathPiecesXML);

        XElement waypointsXML = new XElement("Waypoints");
        var waypoints = GameObject.FindGameObjectsWithTag("Waypoint");
        if (!WaypointsAreValid(waypoints))
        {
            return;
        }
        //order by user selected order
        waypoints = waypoints.OrderBy(x => x.GetComponent<OrderedWaypointForEditor>().Order).ToArray();
        foreach (var item in waypoints)
        {
            XElement waypoint = new XElement("Waypoint");
            XAttribute attrX = new XAttribute("X", item.transform.position.x);
            XAttribute attrY = new XAttribute("Y", item.transform.position.y);
            waypoint.Add(attrX, attrY);
            waypointsXML.Add(waypoint);
        }
        waypointsCount = waypoints.Length;
        elements.Add(waypointsXML);

At the first part of the Export method, we create a new XDocument and add the root Element (called “Elements”). We begin by adding the XElements for the Paths (with their corresponding X and Y attributes). We validate the Waypoints via the WaypointsAreValid method and then we add them, in a similar to paths way.

        XElement roundsXML = new XElement("Rounds");
        foreach (var item in rounds)
        {
            XElement round = new XElement("Round");
            XAttribute NoOfEnemies = new XAttribute("NoOfEnemies", item.NoOfEnemies);
            round.Add(NoOfEnemies);
            roundsXML.Add(round);
        }
        elements.Add(roundsXML);

        XElement towerXML = new XElement("Tower");
        var tower = GameObject.FindGameObjectWithTag("Tower");
        if(tower == null)
        {
            ShowErrorForNull("Tower");
            return;
        }
        XAttribute towerX = new XAttribute("X", tower.transform.position.x);
        XAttribute towerY = new XAttribute("Y", tower.transform.position.y);
        towerXML.Add(towerX, towerY);
        elements.Add(towerXML);

        XElement otherStuffXML = new XElement("OtherStuff");
        otherStuffXML.Add(new XAttribute("InitialMoney", initialMoney));
        otherStuffXML.Add(new XAttribute("MinCarrotSpawnTime", MinCarrotSpawnTime));
        otherStuffXML.Add(new XAttribute("MaxCarrotSpawnTime", MaxCarrotSpawnTime));
        elements.Add(otherStuffXML);


        if (!InputIsValid())
            return;



        if (EditorUtility.DisplayDialog("Save confirmation",
            "Are you sure you want to save level " + filename +"?", "OK", "Cancel"))
        {
            doc.Save("Assets/" + filename);
            EditorUtility.DisplayDialog("Saved", filename + " saved!", "OK");
        }
        else
        {
            EditorUtility.DisplayDialog("NOT Saved", filename + " not saved!", "OK");
        }
    }

At the second part of the Export method, we add all the rounds the user has created, along with their corresponding number of enemies. We also add the tower, the initial money and the carrot spawn time. If a gameobject tagged as “Tower” cannot be found in our scene, we display an error message and exit the method. Moreover, before saving, we use some validation logic (shown below) and we display a final confirmation for the user. If the Unity developer opts to save her work, we save the file into the Assets folder.

    private bool WaypointsAreValid(GameObject[] waypoints)
    {
        //first check whether whey all have a OrderedWaypoint component
        if (!waypoints.All(x => x.GetComponent<OrderedWaypointForEditor>() != null))
        {
            EditorUtility.DisplayDialog("Error", "All waypoints must have an ordered waypoint component", "OK");
            return false;
        }
        //check if all Order fields on the orderwaypoint components are different

        if (waypoints.Count() != waypoints.Select(x=>x.GetComponent<OrderedWaypointForEditor>().Order).Distinct().Count())
        {
            EditorUtility.DisplayDialog("Error", "All waypoints must have a different order", "OK");
            return false;
        }
        return true;
    }

Regarding the waypoints, when the Unity developer places them on the scene, she must a) tag them as “Waypoint” and b) add a OrderedWaypointForEditor component. These components have an Order field, which represents the order in which our enemies will walk towards our waypoints. The waypoint with the lowest order will be our entry point whereas the waypoint with the largest order must be located at the bunny house. The WayPointsAreValid method checks all waypoints to determine whether a) all of them have a OrderedWaypointForEditor component and b) all their order fields are different. If both of these conditions are satisfied, the method returns true. Otherwise, it informs the user of the error and returns false.

    private void ShowErrorForNull(string gameObjectName)
    {
        EditorUtility.DisplayDialog("Error", "Cannot find gameobject " + gameObjectName, "OK");
    }

    private bool InputIsValid()
    {
        if (MinCarrotSpawnTime > MaxCarrotSpawnTime)
        {
            EditorUtility.DisplayDialog("Error", "MinCarrotSpawnTime must be less or equal "
            + " to MaxCarrotSpawnTime", "OK");
            return false;
        }

        if (rounds.Count == 0)
        {
            EditorUtility.DisplayDialog("Error", "You cannot have 0 rounds", "OK");
            return false;
        }

        if (waypointsCount == 0)
        {
            EditorUtility.DisplayDialog("Error", "You cannot have 0 waypoints", "OK");
            return false;
        }

        if (pathsCount == 0)
        {
            EditorUtility.DisplayDialog("Error", "You cannot have 0 paths", "OK");
            return false;
        }

        return true;
    }

The InputIsValid method checks the various variables for invalid input and returns false if it is not valid. The ShowErrorForNull method will display an error dialog if the user has forgotten to add a specific gameobject.

That’s all for our EditorWindow! So, you could ask, how does one make a new level?

Making a new level for the Tower Defense game

Let’s see the steps to create a new level for our game!

1. Create a new empty scene, set the Camera to 0,0,-10, Orthographic projection and 0.3 and 1000 for the Clipping planes. Make sure it’s tagged as “MainCamera”.

2. Drag a Tower prefab (located in the Prefabs folder) to the scene

3. Drag some paths to, well, make a path! Useful keys are Ctrl-D (for gameObject duplication) and V key for vertex snapping (to make gameObjects align to each other). You should now have something like this

image_34A3A7C1

4. Create empty gameobjects to represent our waypoints. For better results, create one outside of the path (the lowest one on the below screenshot), one at the first part of the path, one at each corner and one at the bunny house. Add the OrderedWaypointForEditor script to all of them and modify their Order field respectively. Do not forget to tag them as Waypoints!!!

image_0F39B453.png

5. Open the Custom Editor –> Export Level menu item

6. Add some rounds, edit whatever you want and add Level2.xml as the filename

image_2A749E05.png

7. Click Export and agree, go to the Assets folder and right click –> refresh. Level2 file will appear. Drag it to the Resources folder.

image_328E5B0B.png

8. Go to the Utilities.cs file (located in the Scripts folder), find the ReadXMLFile method and change the “Level1” to “Level2” (yeah, I should have parameterized that somehow).

9. Go to your new scene and delete everything, apart from the camera. Yeah, you won’t regret it!

10. Drag the RootGO prefab onto the scene. Place it at 0,0,0 if placed elsewhere.

11. Ready??? Press the editor’s “Play” button and your new level will appear! Congrats, you should feel great and powerful!

image_5CFD1CD9.png

Object pooling

What’s that?

Another thing worth mentioning is the use of object pooling for some of the game objects on this game. If you are new to this topic, we’d encourage you to view this video tutorial from Unity. In short, object pooling helps from a performance perspective. In other words, lots of times you want to add objects to your game that are short-lived. This would imply the following flow

– Create the object (either via “new GameObject” or “Instantiate”)

– object lives its lifetime

Destroy() gets called on the object, when it is no longer needed

However, Instantiate has a performance hit and Destroy does not immediately remove the object from memory (this depends on the Garbage Collector lifecycle). Object pooling, on the other hand, works like this

– Calculate how many objects will be needed (e.g. we suspect that our game will not have more than 10 arrows “alive” at the same time)

– Instantiate those objects and set them as inactive

Each time the game needs an object, it fetches the first inactive from the list

– Object is set to active and lives its lifetime

– When the object is no longer needed, it is set to inactive

That’s a pretty basic explanation of object pooling, onto the source code now!

Source code for the ObjectPooler class

public class ObjectPooler : MonoBehaviour
{
    //[optional] set a parent for the new gameobject
    public Transform Parent;
    //[optional] prefab to instantiate our pool with
    public GameObject PooledObject;
    private List<GameObject> PooledObjects;
    public int PoolLength = 10;

    private Type[] componentsToAdd;

    public void Initialize()
    {
        PooledObjects = new List<GameObject>();
        for (int i = 0; i < PoolLength; i++)
        {
            CreateObjectInPool();
        }
    }

    public void Initialize(params Type[] componentsToAdd)
    {
        this.componentsToAdd = componentsToAdd;
        Initialize();
    }

Our ObjectPooler class begins by declaring some variables. Parent and PooledObject are optional, if you want to set a parent for the newly created object or instantiate it via a prefab, respectively. We also have a PoolLength variable and an array that contains any components we want our newly created gameobjects to have. The Initialize method is overloaded, in case you want the pooled objects to have any components. The PooledObject list contains all the created gameobjects.

    private void CreateObjectInPool()
    {
        //if we don't have a prefab set, instantiate a new gameobject
        //else instantiate the prefab
        GameObject go;
        if (PooledObject == null)
            go = new GameObject(this.name + " PooledObject");
        else
        {
            go = Instantiate(PooledObject) as GameObject;
        }

        //set the new object as inactive and add it to the list
        go.SetActive(false);
        PooledObjects.Add(go);

        //if we have components to add
        //add them
        if (componentsToAdd != null)
            foreach (Type itemType in componentsToAdd)
            {
                go.AddComponent(itemType);
            }

        //if we have set the parent, assign it as the new object's parent
        if (Parent != null)
            go.transform.parent = this.Parent;


    }

The CreateObjectInPool method creates a new gameobject (either instantiating the PooledObject prefab or calling GameObject’s constructor) and optionally adds any components (via AddComponent(Type type) method) and parent. Most importantly, it sets the gameobject as inactive and adds it to the list.

    public GameObject GetPooledObject()
    {
        for (int i = 0; i < PooledObjects.Count; i++)
        {
            if (!PooledObjects[i].activeInHierarchy)
            {
                return PooledObjects[i];
            }
        }
        int indexToReturn = PooledObjects.Count;
        //create more
        CreateObjectInPool(); 
        //will return the first one that we created
        return PooledObjects[indexToReturn];
    }

The GetPooledObject method is the method that will be called from external scripts. It tries to find a gameobject in the PooledObjects list that is inactive (i.e. ready for use). If it finds one, it returns it. Otherwise, it will double the list’s capacity, create gameobjects in it and will return the first one that we created.

That’s it for the ObjectPooler class, let’s now take a peek at the class that it is using it.

The ObjectPoolerManager class

This is the object that will hold references to our ObjectPoolers. For this tutorial’s purposes, we’ll use object poolers for the arrows shot by the bunnies and for our audio objects.

public class ObjectPoolerManager : MonoBehaviour {

    //we'll need pools for arrows and audio objects
    public ObjectPooler ArrowPooler;
    public ObjectPooler AudioPooler;

    public GameObject ArrowPrefab;


    //basic singleton implementation
    public static ObjectPoolerManager Instance {get;private set;}
    void Awake()
    {
        Instance = this;
    }

    void Start()
    {
        //just instantiate the pools
        if (ArrowPooler == null)
        {
            GameObject go = new GameObject("ArrowPooler");
            ArrowPooler = go.AddComponent<ObjectPooler>();
            ArrowPooler.PooledObject = ArrowPrefab;
            go.transform.parent = this.gameObject.transform;
            ArrowPooler.Initialize();
        }

        if (AudioPooler == null)
        {
            GameObject go = new GameObject("AudioPooler");
            AudioPooler = go.AddComponent<ObjectPooler>();
            go.transform.parent = this.gameObject.transform;
            AudioPooler.Initialize(typeof(AudioSource));
        }

        
    }

This class follows the singleton pattern, since we want only one instance of it to be available at any given time. We have two references for our poolers (Arrow and Audio) and, upon the time Start() is called, we initialize the poolers. Arrow is a prefab so we have a field for that, to be filled at the Unity editor. The AudioPooler is initialized with an AudioSource type (for an AudioSource component to be created) whereas the AudioPooler gets the Arrow prefab.

The second part of this tutorial can be found here: https://dgkanatsios.com/2014/09/06/a-tower-defense-game-in-unity-part-2-3/ and don’t forget that you can play the game in WebGL here

You can find the complete source code on GitHub: https://github.com/dgkanatsios/TowerDefense

3 thoughts on “A Tower Defense game in Unity, part 1

Leave a comment