Unity 2D Pixel Art Game Tutorial
Key Takeaways
This video tutorial demonstrates how to build a complete 2D Pixel Art Tower Defense game in Unity from scratch, covering topics such as data-driven game development, enemy AI, and object pooling. The tutorial uses tools like Unity, Scriptable Objects, and Prefabs to create a modular and reusable game architecture.
Full Transcript
Learn how to build a complete 2D pixel art tower defense game in Unity from scratch. This step-by-step tutorial guides you from a blank project to a fully playable game with a main menu, multiple tower types, enemy waves, and three unique levels. You'll learn how to create a modular data-driven system using scriptable objects to manage towers, enemies, and waves, plus how to design forest, lava, and jungle levels with Unity tile maps. You'll also learn how to export and publish builds for Windows, Android, and web. Frank D'vorak developed this course. Do you want to make your own pixel art games in Unity, but don't know where to start? In this tutorial, I'll show you how to create a complete miniame with a main menu and three unique levels. We will have multiple tower types, enemy types, and different environments to keep it interesting. I will show you how to structure everything in a simple expandable way using the datadriven approach with Unity's scriptable objects. We'll go step by step to make sure everyone understands and I will provide full source code after each step so no one gets lost along the way. I create a new blank project with universal 2D render pipeline. I'm using Unity 6, but you can use any Unity version you want. It doesn't matter. We will first build the entire game using placeholder art assets and then we will add pixel art at the end, but you can use the same codebase with any graphic style you want. I'll also show you how to create hundreds of different pixel art characters using this powerful tool and how to animate and use them inside your own games. Let's jump in and make a complete fully playable miniame with three increasingly difficult levels plus an endless mode and learn how to export the playable windows mobile and web build. In assets scenes, I rename my scene to game. Inside the assets folder, I create a new folder I call art. You can download all art assets in the resources section below. I'll start with this simple spreadsheet where I have one frame representing an enemy and another frame representing a point along a path. With the image asset selected inside the inspector tab, if you're using pixel art like this image, you want to make sure it's not blurry. So, we set filter mode to point no filter and compression to none. I click apply. I open the sprite editor. and slice grid by cell size. These frames are 100 * 100 pixels. I click slice, then apply. Now I can use this little arrow to expand the sprite sheet and see individual frames. Here in the hierarchy window, I see all the game objects that make up my game scene. I can rightclick and create an empty game object. I call it path. Every game object in Unity has a transform component which defines the object's position, rotation, and scale. We can use these tools here. For example, I can select the move tool to move the object around the scene. And when I do that, its position values change. I can also rightclick and reset transform to center the object in the middle at coordinates 0. So this is the game scene. This white rectangle represents the visible game area. If I open the game tab, this is what the game will actually look like when played. In my art folder, I can expand my spreadsheet. If I drag and drop this frame into the scene, Unity will automatically create a game object for it. Unity will also automatically detect that this game object has an image. So on top of its transform component, it will also give it a sprite renderer component where we can do many things to handle and adjust the image. I will rename the game object to waypoint. With the waypoint game object selected, I can use the move tool to move it around the scene. Doing that will update its X and Y position. I can also set the object's position manually by changing these values here on the game tab. This is what our game looks like so far. I'll change the aspect ratio to full HD for better rendering. And I'll zoom out using this scale slider. I like to keep my small game preview down here. Now, when I make changes to the scene, I can instantly see how those changes affect the playable version of my game. Now that we understand the basic Unity layout, let's build something more interesting. Inside my assets folder, I create a new folder I call prefabs. I can drag and drop my waypoint game object into this prefabs folder, turning it into a reusable game asset. Prefabs are preconfigured, pre-fabricated game objects. Let's say I have a game object like this waypoint and I configured it in a specific way. Now I want multiple of these game objects all configured the same way. Instead of repeating the process over and over, configuring them one by one, I turn it into a prefab and I can duplicate it as many times as I want. Notice that after turning it into a prefab, it changed color. I can delete it from the scene now and I can just drag in as many as I need. If I want to make change to all of these copies, I can just change the main prefab in assets and it will update all of them. I delete these and I leave just one for now. Game objects in Unity have components that give them special functionalities. Unity has many components we can add and we can also create completely custom script components to write custom logic. So let's say I want to write a simple script that connects all of our way points with a line. And I want enemies to move and follow along that path. Inside assets, I create a scripts folder. In Unity, we use the C programming language. Let me show you how much you can achieve with just a few simple lines of code. Inside the scripts folder, I right click, create, and I choose a Monobehavior script. I call it path. Be careful here. You can't just rename this file later because Unity will use that name to define the class inside the file. So make sure the name of your file and the autogenerated class name inside it match. I select the path game object and I attach the path script as a custom component like this. I double click the script file and Unity opens it in my default code editor. If you've never heard of MonoBehavior before, all you need to know is that it's a special Unity class that allows us to take scripts and attach them to game objects as components, just like we did, and make the code inside that script interact with that game object and with others in the scene. MonoBehavior is a fundamental building block of Unity. You'll see it all the time. MonoBehavior also gives us access to so-called life cycle methods, useful code blocks where we can put our code depending on when we want that code to run. You can also see that Unity created two most important methods. Inside the start method, we can put code we want to run only once at the beginning. And the update method is where we put code we want to run over and over. There are many more built-in life cycle methods and we will use some later on. For now, to keep this clean, I will delete them and I will just declare a variable. We are inside a custom path class we created. I want a variable that will hold an array of way points so that we can connect them together and send enemies along that path. In C, we define variable this way. We start with an access modifier. If the variable is public, then everyone can see it. Or if it's private, it's accessible only from this script. I need my waypoints to be available from other scripts. So I set the access modifier to public. The second [clears throat] word defines the type of value this variable will store. In this case, it will hold an array of prefabs. Each waypoint is a prefab and prefab is a game object. Square brackets mean it will be an array of game objects. And the last part is whatever I want the variable to be called. So who can access this variable? What data type it holds and what name we give it? In C, we define all variables like this. If I save changes and go back to Unity on the path game object inside its custom script component here, I see that waypoints variable. It's an array. So I can click plus to add elements to it. Because of the variable type we defined, you can see these fields expect a value that is of game object type. I go to the prefabs folder and here we have a waypoint prefab, a preconfigured pre-fabricated game object. I will need five of them in total. So I drag them one by one into the scene until I have five copies. I select the path game object again and I will drag this first waypoint into the first field in the array like this. Waypoint one game object into this field and I do the rest ideally in the same order. So we have an increasing sequence of numbers that matches the order in the array. This icon is to show and hide gizmos in Unity. Gizmos are visuals that appear in the scene view but not in the game view. We will use gizmo lines to visualize our path connecting the waypoints. And then we will make enemies move along that path. So let's make sure we have gizmos enabled. And let's open our path script. Unity has a built-in method called ond draw gizmos that we can use to draw visuals in the scene view to help us design or debug our game. If this waypoint array has some elements in it, if the length is more than zero, I create a for loop that will cycle over all the elements in the array. We set the color to gray. And for each of these waypoints inside the waypoints array, we will draw a line connecting them. Unity has a built-in gizmos draw line method that takes the start point and end point and it will draw a line between these two positions. So I want to start the line at the position of the game object at this index. I access its transform position and I want to end the line at the position of the next waypoint. So index + one. As we cycle over the array, this index value is increasing by one and we are drawing lines between the way points. When we reach the last index in the array, there will be no index + one anymore. So that would give us an error. So I only want to draw these lines until we reach the second to last element. only draw the lines between this and the next index if the index is the length of the waypoints array minus one. Let's save that. You can see a line connecting all our way points. Now I can select individual waypoints and I can use the move tool to modify my path in any way I want. Maybe I will have some tile map behind and I will make sure this path matches a road in the artwork that I want the enemies to follow. Notice that because the line is drawn using gizmos, it's visible only in the scene view, but it doesn't render in the game view, which is exactly what we want. I left click the first waypoint and I shift click the last one, selecting all the game objects in between. I will parent all of them under the path game object to keep the hierarchy organized. I can also rename this to path one because we can create multiple different ones in our game later if we want to. With path one selected, I use the move tool and I can also move the entire path. Now, like this, I can select individual waypoints to modify them in any way I need. This technique is extremely useful for tower defense games, racing games, as well as something like MOAS such as League of Legends where you have NPC minions moving automatically along the lanes. You can use the same exact technique we have here for a 3D game as well. Maybe I also want to give the waypoints some labels so I can easily see at a glance which waypoint corresponds to which game object inside onro gizmos. I can do that here. If we want to define styles for our text in Unity, we can use the built-in GUI style class. GUI stands for graphical user interface and we can use this class to define visual styles, fonts, colors, alignment, and so on. Now, I can take that style and I set its normal text color to white. And most importantly, I set alignment to middle center because I want the text to be horizontally centered around each waypoint. I can draw text using handles built-in Unity class. We need to include the Unity editor name space up here. To be able to use handles, handles label will let us draw a label in the scene view. We pass it three parameters. Where we want this label to be drawn. I want it at the position of the waypoint it belongs to. plus 0.7 units above it. The second parameter is what we want the label to say. I will make it display the name of the game object. We can see the names in this field here. And finally, as the third parameter, I apply a custom style we just created. If I save that, you can see we have little labels above each waypoint in the game view. None of this will be visible. Perfect. Now, let's bring in some enemies and make them walk along this path. Inside assets art, I have my sprite sheet. And one of the frames is this simple enemy silhouette. I drag and drop this frame into the scene and I rename it to anime. We can replace it with animated characters later, but first let's focus on the pathf finding logic. I go to assets scripts and create a new folder I call anime. Inside I create a new Monobehavior script. I will also call it anime. With the enemy game object selected here, I drag and drop the enemy script here as a custom component. I open the script. I delete all this code and I will define a variable here. I start with serialized field in square brackets like this and private. The type will be float which is a decimal number. A number with a fraction part. I will call my variable move speed. I will also give it a default value for example 3F f for float. We know we have public variables which are accessible from other scripts and they are also displayed here in the Unity inspector. Private variables are accessible only from the script and they don't show up here in the inspector. If I want a private variable but I want it to display in the inspector, I give it the serialized field attribute like this. So if I save my changes and I go back to Unity, I can see move speed here like this and we see the default value of three I gave it. I want another private variable. This time I don't need to see it here in the inspector. So I don't use serialized field. The type is vector 3 and I call it target position. Vector 3 is a data type that has three components. We use it for position for example because position needs X Y and Z values. In terms of naming conventions, look at the custom names I'm giving these variables. We will follow net style naming conventions here. Public variable names are spelled in Pascal case where the first letter is capitalized. Private variables will be case with an underscore prefix. And my personal choice is that serialized field variable names will be spelled like this so that Unity can turn them into names that are nice to read in the inspector. These are just conventions. If you don't follow these rules, it will not break your code. But people use them because you can see at a glance what is a public and private variable. Inside the update method, which runs over and over, we want the enemy to move towards the target position. We learned about start and update life cycle methods. There are many [clears throat] more. For example, we have the on enable method which runs every time a game object is activated. Inside, we'll put all the code that I want to run when I reuse this object because later on we will learn how to optimize our code for better performance by turning enemies into reusable objects. And every time we reuse the object, every time we activate it, code inside on enable method will run. When the enemy gets activated, we will give it some target position. For example, just as a test. Let's give it a new vector 3 and set the position to 4 0 0 X Y Z. As the update method runs over and over, I want the enemy to move towards that target position. So, I set the enemy's transform position to a built-in move towards method. This method expects three parameters. Current position, target position, and move speed. So, I pass it the enemy's current position, where it is right now, and its target position, where I want the enemy to move, and how fast it will move there, which will depend on the move speed property from line five. I multiply it by time dodla time. Delta time gives us the time in seconds that passed between each call of update. If I account for the actual time passing by like this, we make sure that the enemy moves at a consistent speed on all devices. It's important because the update method might run faster on a more powerful device, but we don't want the devices ability to serve more frames per second to affect how fast things are happening in our game. Accounting for delta time is a very important technique that ensures the speed at which game events happen depends on actual time and not on the frame rate. This is one of the fundamentals of game development and something we have to always make sure we do. Okay, so we have the enemy a target position of 4 0 0. Let's save that and play to see if it worked. We can enter play mode by pressing this play button up here. As you can see, the enemy is moving from wherever it is to the target position of 4 0 0 at the speed of 3 units per second. This is position zero. And these little squares are units. The horizontal x position of plus4 is four units to the right from the middle. 1 2 3 4. I exit play mode. We know our code works. So now instead of making the enemy move to a position like that, I want the enemy to move from waypoint to waypoint following a path. I select my path one game object and I open the path script. To move along the path from waypoint to waypoint, we need a method that will give us the position of a waypoint depending on an index in the waypoints array. When I call it and I ask what is the position of the element at index one in the waypoints array, I want this method to give me x, y, and z position of wherever in the scene we placed this waypoint object. Indexes start from zero. So 0 1 2 3 4. So this method will expect a parameter of integer type a number without decimals. I call it index. And it will return the waypoint array at that index we pass to it. It will give us the transform position of that waypoint at that index. So whenever I need to know where to move next, I will call this public get position method. To access that get position method we just defined from the enemy script, I need a reference to the path one game object and to its path script component which contains that method. So serialize field private type is path because I want this variable to hold a reference to this custom path component and I will call it for example current path. Now it's time to learn about another life cycle method called awake. This method runs once when the script instance is first loaded. Same as start, but it runs before on enable and before start. Inside awake, I take current path and I need to point it from the anime game object to this path one game object. And on it, I need to find this path component. There are multiple ways to do that. For example, I call the built-in find method and I pass it the name of the game object I'm looking for. I need to make sure the spelling matches the name of the object I'm looking for exactly. From it, I call get component because I'm looking for this path component here. So now the current path variable holds a reference to this component. We need it because that component has this public get position method that we will want to call from the enemy script. Inside on enable I set target position to current path this component dot get position and I pass it index. And depending on the index value I pass it, it will give me the position of one of these way points 0 1 2 3 4. For example, if I pass it index three, it would give me the position of this waypoint game object. The reason I had to use awake here is the order of execution. On enable runs before start, so I couldn't put this line inside start. I had to put it inside awake because awake runs before on enable and I needed this reference to be ready at the point when enable runs. Don't worry about this too much. It will become easy to keep track of the more you work in Unity. You can always have a copy of this chart somewhere and check it when you need it. So if I set this to index 3, I go back to Unity and I expect enemy will walk from wherever it is right now to the position of this waypoint. I enter play mode and yes, that worked. Perfect. Let's try to set index to zero. Save. I expect the enemy to walk to this waypoint. Play mode. Great. We have most of the logic. All I need to do now is to make sure the enemy increases target index by one every time it reaches its target position. And that will make it move along the path we defined here. So let's define a private integer I call current waypoint. It will be zero or one or two, three or four as the enemy moves along the path and changes targets. Every time we activate or reactivate this enemy, set its target position to be the first way point on the path index zero. As the game runs, we check when the target position was reached and we set a new target position. We need to know the relative distance. How far is the enemy from the target position? The difference between the enemy's current position and its target position. The enemy can be to the right or to the left. So the value can be negative or positive. But we want to disregard that plus or minus. We only want the distance between these two positions. We don't care about the direction. That's why I want the magnitude of the vector. If the enemy is close enough to the waypoint to its target position, let's say if the distance is less than 0.1 units, we increase the current waypoint value by one. And we set the enemy's target position to the next waypoint in the path like this. The enemy will move towards that next target position. And again, when it's close enough to it, it will increase the index again to move towards the next waypoint. I save and play to see if it works. The enemy is moving towards one way point after another following the path we defined. When it reaches the last way point, we get an error. So, let's fix that. Only increase the current waypoint by one if we are at an index at least one less than the length of the array. else we know the enemy reached the end of the path. So we will deactivate the enemy. We set active to false like this. I save and play. The enemy follows the path and when it reaches the end it gets destroyed. It gets grayed out and deactivated. I create an empty game object and I call it spawner. I want to create a game object that will spawn enemies at a specific interval in the game later. It can be a cave that orcs are coming from or something like that. I reset transform just to make these numbers nicer. No real reason for this otherwise. Inside the assets scripts folder, I create a new Monobehavior script I call spawner. Spawner game object selected here and I attach spawner script as a custom component. I delete all this code and I define a private float spawn timer and spawn interval. I set it to 1 second by default. Spawn timer will count from 1 second down to zero. It will spawn an enemy and it will count again. So as the update method runs over and over, we are reducing the value of spawn timer by a delta time by real time that is passing by between updates. When spawn timer is less than or equal to zero, we set spawn timer back to spawn interval which is 1 second. right now. And we spawn an enemy. This method doesn't exist yet. So, let's write it. Private void spawn enemy. Inside, I create a temporary helper variable. Type will be game object and I call it spawned object. to create a copy of a game object from a prefab with code we call the built-in instantiate method and we pass it the prefab we want to create a copy of this prefab variable doesn't exist yet so let's define it public game object type I call prefab like this Pascal case because this is a public variable so I have to change the spelling here as well notice I'm keeping everything generic so that I can set this prefab to be anime or any other prefab object we will have in the game later. So we can use this spawner class to spawn pretty much anything. It can be very useful. Once we create spawned object from a prefab, we take it and we set its transform position to be the same as transform position of the game object this script is attached to. So the position of spawner game object, wherever I place this spawner in the scene, that's where game objects will be spawning from. So now I have this prefab field and whatever prefab I put there that's what this spawner will start producing copies of. Save that. And in Unity I see that prefab field here. I go to assets prefabs and I drag and drop enemy game object in here turning it into a prefab. I can reset its transform to keep this cleaner. I delete the enemy game object from the scene. spawner selected and I drag the enemy prefab into this prefab field. I enter play mode and enemies start spawning. They start from wherever I placed my spawner game object. They move towards the first waypoint in the path and they follow the path. When they reach the end of the path, they get deactivated and grayed out here in the hierarchy. As you can see, we keep creating new enemies and deactivating the old enemies. I can also destroy the old enemy game objects completely, but for performance reasons, we want to reuse them instead. So, instead of constantly creating and destroying enemy game objects, we want to create a pool of, let's say, 10 enemy game objects, we activate them when we need them, and we deactivate them, returning them back to the object pool instead of destroying them. Object pooling improves performance by reusing inactive objects instead of repeatedly creating and destroying them, which reduces memory allocation and garbage collection overhead. Let me show you a quick and simple object pool logic I like to use for my games. Inside assets scripts, I create a new Monobehavior script I call object pooler. Let's open it. Inside I delete all this code. I define a private variable I call prefab. What kind of game object we want to store in this pool. For example, this would be the enemy prefab. We will also need pool size. How many of these prefabs we want to pre-instantiate. We want to fill this pool with inactive game objects and activate them when we need them. I will give it a default value of five, but we can change that later. I will also need a data structure to hold them private list of game objects. I call for example pool. To be able to use list, we need system collections generic name space up here. Inside the start method, I will create the pool. I assign the pool to a new empty list of game objects first. Then I write a for loop that will cycle over the pool five times in this case and it will create new game object five times. Let's define that method here. Private method that will return a game object. I call it create new object. Helper variable of game object type we call obj. And we already know that to create a game object, we just call the built-in instantiate method and we pass it the prefab we want to create a copy of. After we create it, we set active to false. We are just preparing some inactive objects into the pool here. We take pool from line 8. We add this new inactive object inside and we make this method return that object. Let's save that. In Unity, I create a new empty game object I call enemy pool. I reset its transform. Again, our object pooler class is generic. So, we can reuse this script to create different object pools in our game. Enemy pool selected and I attach object pooler script as a component. Prefab field expects a game object that this pool will consist of. I go to assets prefabs and I drag the enemy prefab into this field. The initial pool size is five by default, but I can change it if I want to. If I enter play mode, our script automatically creates five inactive enemy game objects, but they are all on the top level in hierarchy. I exit play mode. I would prefer if the poolled objects were parented under the enemy pool. In my script, I can pass instantiate an optional second argument that defines a parent. So I pass it transform, but it refers to the game object this script is attached to. In this case, enemy pool. So I'm saying set enemy pool as the parent and nest these game objects under it. Inside spawner, I comment out spawn enemy for a while. Save that. Now if I play start method runs and it creates five inactive enemy game objects and it puts them under enemy pool like this keeping the hierarchy clean. The idea behind object pooling is that when I need a new enemy I activate one of these and when I destroy the enemy in game I just deactivate it putting it back into this pool of available objects. Doing this is good for performance mainly because of automatic garbage collection and memory allocation processes which can cause our game to slow down if we are constantly creating and destroying new game objects. Object pooler will need one more thing a method that will give us one of these inactive game objects when we need it. I will run a for each loop which will iterate over all game objects in the object pool. We have that pool here on line eight. And as we just saw, it will hold five inactive enemy objects. For each loop, we'll check objects in the pool one by one. And when it finds the first inactive one, if active self is false, exclamation mark means false, it will return it, giving us the game object. We are creating five game objects initially. Maybe we are in a situation when all of them are being used and this method will not find any inactive one. In that case, we just create one more and we return it. If needed, we increase the size of the pool like this. So now whenever we need a new game object in the spawner, instead of creating a new one, we will call this public get pulled object method. Inside spawner script, we will not be creating any new game objects here. So, I can delete this line. When we spawn an enemy, we want to take the object pool and take one of those inactive game objects in there. I comment this out for a second. I create a variable type object pooler and I call it pool. What is the object pool? This spawner will be spawning enemies from. Save that. In Unity, we see that pool field here. And I want to be spawning from this enemy pool. So I drag the game object in here. From it, I can call this get pulled object method to get one of the enemies from the pool. So when I want to spawn a new enemy, I take the object pool which we know will be enemy pool in this scene and we call its public get pulled object method which will either give us one of the available game objects and if there are none available it will create a new one. Then we do what we did before. We position it wherever in the scene the spawner is and we activate it. If you remember, when the enemy reaches the last way point, it will get deactivated, which will return it back to the pool of available objects. I will also unccomment spawn enemy here. Now, if I save changes, spawner selected, move tool, and I will move it somewhere around here. I play and enemies start coming from the spawner to the first waypoint and then they will follow from waypoint to waypoint along the path. Notice that if I need more object pooler script will create more and at some point we have enough and we will start reusing the old enemy objects. Now we are reusing the same 11 enemy game objects over and over. Throughout the project, I'll be providing source code [music] checkpoints. At each checkpoint, you can download a zip file containing the complete Unity project [music] and source code to open locally. You'll find a link below where you can download all versions of the source code as the project progresses. Right now, we're at the first stage, source code part one. If you download it, you get access to all the art assets and scripts added to the project so far. To [music] open it in Unity, simply unpack the zip file to a folder on your computer. Then open Unity Hub, [music] click add, add project from disk. Navigate to the folder where you unpacked the project and select it. Unity may take a few moments to install the required packages. When the project opens, you'll likely see an empty scene. Unity creates an untitled scene by default. To view the actual project, go to the scenes folder and open the game scene. Now you have the same version of the project I'm working with. You can play it, explore the scripts, and use all the included art assets. Throughout the course, I'll let you know each time we reach a new source code checkpoint. You can then download the next zip file to explore the project at that exact stage of development if you'd like. Now that we've made sure we are all on the same page and everything is working, let's move on to the next part. We will add multiple enemy types and learn how to use a beginner-friendly datadriven approach to give each one unique properties and behaviors. When I open my project like this, I see just this empty untitled scene. So I have to go to assets scenes and open the game scene. In the game window, I go here and change free aspect back to full HD. I also like to have my game view down here so I can see the scene and the playable version of the game at the same time. Now that you know how to get Unity project from GitHub and open it locally, let's continue building it out further. I created another sprite sheet. I call it tower defense basic. As always, you can download it in the resources section below. It's pixel art. So after I drag and drop it into my assets art folder with that asset selected inside the inspector window, I set filter mode to point no filter and compression to none. I click apply. Up here I open sprite editor. I go to slice grid by cell size. I made these frames 100 times 100 pixels. I click slice and apply. This is not the final graphics for our game. just placeholder art assets that will represent different enemy types, projectiles, and towers. It will be kind of a gray boxing phase. I want to build a complete tower defense game with these placeholders, and then we can choose from many different asset packs to make our game unique. For now, our focus is not on graphics. We'll focus on mechanics, code, and logic. And we'll explain everything in detail to make sure even beginners understand. I want to have different enemy types with different properties. In assets prefabs, I rename my enemy prefab to orc. If I check the enemy pool here, you can see Unity has updated the reference. Orc is shown here. All good. I rename it to Orc pool. If I check my spawner, it has renamed Orcpool here. And if I press play, everything still works. I want to create another enemy type by duplicating my orc prefab. So I go inside assets prefabs and I copy and paste the orc prefab. I rename the copy to dragon. I create another copy and rename it to kaiju. With the dragon prefab selected, I log the inspector, go into the art folder, expand the sprite sheet, and drag the little fly and dragon frame into the sprite renderer into its sprite field. I unlock the inspector, back to prefabs, orc selected, log the inspector, and I replace the sprite with the same one from the new sprite sheet. Just to keep things consistent, I unlock the inspector. Select Kaiju. Lock the inspector and drag the knuckle walking Kaiju monster silhouette into the sprite field. Back to prefabs, and I can now drag out dragon, orc, and kaiju enemy prefabs into the scene. Keep in mind these are just placeholder assets. We will build this codebase in a generic way so that later you can use your own art and themes. It'll be easy to turn these into robots, aliens, cars, whatever you want your final tower defense theme to be. I just picked orc, dragon, and kaiju to make things a bit more interesting. So, we're not working with plain gray boxes. I unlock the inspector. Same as we have a pool of reusable orcs, I need to create one for dragons and kaijus. So I create an empty game object, call it dragon pool, reset its transform, add object pooler component, and drag the dragon prefab into the prefab field. I create another empty game object. I call it kaiju pool. Reset transform. Add object pooler and drag the kaiju prefab into the field. I delete them from the hierarchy. Then I nest all my object pools under the spawner game object to keep the hierarchy clean and organized. I enter play mode and I press pause. I should now have a pool of five inactive kaijus, five dragons, and five orcs. I'm getting a strange error. And when I look in the console, I don't think we did anything wrong. Sometimes Unity can get into an incomplete state and report a random glitch like this. to confirm that it's a glitch and not something we did. I close and reopen the project. Now, if I enter play mode and the error is gone, we know it was nothing to worry about. I exit play mode under spawner. Now, we have our three enemy object pools. The way the code works right now, whichever pool we put into this field, that's the enemy type our game will be spawning. So, now I'm spawning dragons. And now I'm spawning Kaiju. Everything works and no console errors. Perfect. So we have a different prefab for each enemy type, but they all share the same enemy script. Let's open it. We don't need to see this current path field. So I remove the serialized field attribute. To keep it consistent, I rename the current path variable to start with an underscore indicating that it's private. I'm using Visual Studio Code where I can doubleclick current path to select it. Then I tap control D or command D on Mac to multi select all occurrences for editing. I select all five instances and add an underscore in front like this. Save that. And now this field disappears from the inspector. I hit play just to test it. Our enemies still follow the path. Good. Inside the enemy script, I'm currently keeping the move speed value directly in the script. I'd like to move this and other values into a scriptable object. This makes game data easier to manage and reuse. Storing values like move speed directly in scripts is called componentdriven design and it's common in beginner friendly tutorials. But let's structure our project better and use datadriven design. In datadriven design, game logic reads from structured data like scriptable objects, not from hard-coded values. It's very easy to implement it and we'll use it throughout this codebase. Let's learn how to handle clean project architecture in a beginner-friendly way. To understand clean datadriven design, we need to understand what a scriptable object is. So, let's create one. Inside the assets folder, I create a new folder called scriptable objects. Inside, I rightclick create script scriptable object script and I name it enemy data. When I open it, Unity has already generated the basic syntax. We have the create assets menu class attribute which allows us to create new assets from this scriptable object directly in the editor. Let's say each enemy data asset will have a field for lives which will be a float, damage which will be an integer and another float for speed. Save that. Now that we've created the enemy data scriptable object, we can use it to create a data asset for each enemy type. Right click, create scriptable objects, enemy data. I name it enemy_c. I give my orc three lives, one damage, and three speed. The same process for enemy dragon. two lives, one damage, speed of six. It's going to be faster but weaker enemy and enemy Kaiju will have 10 lives, two damage, and speed of two. Slow but strong. Now I want to associate each data asset with its corresponding prefab. Instead of defining move speed in the script, I want to pull it from the data file. So I define a serialized field private enemy data I call just data for short. Then I delete the old move speed. And in the movement logic I use data do speed. Each enemy prefab now has a data field. Let's assign the assets. I drag out dragon kaiju and orc prefabs into the scene. Select dragon. Drag enemy dragon into the data field. Kaiju assign enemy kaiju orc. Assign enemy orc. These changes apply only to the instances in the scene. So we go to overrides apply all for each one to save the data link to the base prefab. We've now defined a scriptable object class and using it we created separate data asset file for each enemy type. These are project assets. They persist outside play mode and between scenes. Great for storing configuration data and avoiding hard coding. Scriptable objects are a key tool in datadriven game development. They let us manage data cleanly, separately from code, and easily inside the Unity editor. Back on the spawner, I set Kaiju pool here. I reduce Kaiju speed to one, very slow. And when I play, they spawn and move very slowly. Change speed to five and they move much faster. Exit play mode. So instead of storing config in the script, we store it in a reusable asset. reference it in the inspector and pull data from it like data speed. This approach has many benefits and we will explore more as we build the game. I delete the prefab instances on spawner. I drag orc pool into the pool field. The spawner now endlessly produces enemy type defined by the pool. Here the orc speed is read from the enemy orc data asset. I can tweak it and see immediate changes. Following this pattern, I want to define enemy waves in the same way with data assets. Each wave defines the enemy type, how many, and how often to spawn in scriptable objects. I right click create scripting scriptable object script call it wave data. I open it and define public float spawn interval public integer enemies per wave and we will define enemy type a bit later. Now I can create one. Right click, create scriptable objects, wave data. I call it wave one. I set spawn interval to 1 second and enemies per wave to three. I also want to define which enemy to spawn in this wave, orc, dragon, or kaiju. I could store the prefab asset itself in the data, but since we're using object pools, and these pools only exist in the scene, Unity won't allow us to reference them in a persistent data asset. So, we need to store a value that lets us associate the wave with a specific pool. To do that, I'll use an enum. Inside the scripts folder, I create a new folder called utils for reusable scripts. Right click, create scripting, empty C script. Name it enemy type. Open it and I change it to an enum. I have a public enum called enemy type and inside I say orc, dragon, and kaiju. No comma there. An enam is a simple list of names you make to represent different options in your game. The goal is to store a reference to an object pool from which we will be spawning enemies for each wave. We can't directly reference the orcpool, dragon pool, and kaiju game objects because they exist only in this one scene. They are not projectwide assets. So instead, we will reference this enam which will then help our code find the correct object pool in the scene. So each of my waves will have a field for enemy type. This enam and I call it enemy type for example. It will be either orc dragon or kaiju. If I save this and I go back to unity inside assets scriptable objects folder, we created this wave 1 data asset. You can see this new enemy type field here and in it Unity presents the enum as a nice clean drop down. So wave 1 will be spawning orcs every 1 second and it will spawn three orcs in total. Great. Now I want to use this wave data scriptable object to create a data asset for the second wave. Right click create scriptable objects wave data and I name it wave 2. It will spawn dragons every 0.5 seconds. Six of them in total. Again, I right click, create scriptable objects, wave data, and I call it wave three. It will spawn kaiju every 2 seconds, two in total. Okay, so this is how we define our wave data. The first wave is orcs. The second wave is dragons. And the third wave of enemies will be Kaiju. Currently, the spawner only spawns whatever object pool we put into this field. Right now, it's the orc pool. Let's open the spawner script and adjust that logic to work with our new wave data assets. I create a serialized field private variable and the data type it will contain will be these wave data assets. This variable will not hold just one. It will hold an array of wave data files. I'll call it, for example, waves. Save that. On the spawner game object, I see this waves list. Now, from assets scriptable objects folder, I drag and drop the wave 1 data file. Wave two, I drop it over the head of the array like this, and it will slot them in order. And wave three as well. We know that each data asset has this enemy type field where we can choose an enemy type from the drop-down. Those values in that dropdown are defined by this enemy type enum. I need a way to connect these values inside the enum with the actual object pools we have in our scene. We have orcpool. So I create a reference for that. Here we also have dragon pool and finally a kaiju pool. For now I put orcpool here on line 26. Save that. The spawner has three new fields. So I drag the orcpool game object into this field. Dragon pool game object into this field. And kaiju pool will go in here. I enter play mode and I pause my game. We know that as soon as the game starts, these object pools fill with inactive enemy prefabs. Scripts are activating and deactivating them, reusing the same set of enemies over and over, which helps to improve performance of the codebase. We know that the wave 1 data asset has the enemy type field set to orc. So, I want to connect that value to this orc pool field. Wave two will be connected to the dragon pool and wave three to this kaiju pool. The reason why I have to do it like this is because I can't directly reference these object pools in my data files which would make it easy. But because these three object pools exist only in this scene and these data assets exist in the project across all scenes, Unity doesn't allow me to directly reference non-persistent objects in the data. That's why I have that enum that stores the name of the enemy. And we will now use that enemy name to point to the correct object pool and match the data with scene objects with the object pools. We can do that in many ways. Let's use a dictionary. This is a good use case for it. Let's just write the code and then explain how it works when we see it. Private variable which will hold a dictionary. We will match enemy type with object pooler. I will call it for example pool dictionary like this. A dictionary is a list of key value pairs. Enemy type will be the key. Object pooler will be the value. The enemy types will be coming from this enum here. Orc dragon kaiju object pooler values will be coming from these object pools. The goal is to match a keyword from the enam, the names of enemies with their associated object pools. That way, if I say in this data file that enemy type is dragon, my spawner will know to take enemy objects from this dragon pool. We defined the dictionary here inside the awake method which runs once as soon as the object is created. We will create a new dictionary. I'm saying make a new empty dictionary where I can store enemy poolers and each one is linked to an enemy type. Inside I define which enemy type is associated with which object pooler. Enemy type.org is associated with orc pool key value pair. If our wave data has the enemy type field with a value of orc, we know that we want this orc pool game object. If enemy type is dragon, we want the dragon pool. If enemy type is kaiju, we want the kaiju pool game object. So now we connected values in our enum with specific object pools and the link from our data to the pools has been established. We used a dictionary to link each enemy type to its corresponding object pool. So we can quickly access the right pool based on the enemy. Now that we have wave data that tells us what enemy to spawn, how often, and how many of them per wave, I want to be able to cycle over this array of wave data, and spawn enemies based on that. We'll need a private integer I call current wave index. Are we currently on wave zero, one, or two? Indexes in arrays start from zero, not from one. We need a way to get data from these wave data assets based on the current wave index. I'll use this syntax. I'll have a property called current wave that whenever I use it, it will look at this waves list from line six and give me the wave data asset at that index zero or one or two. This line is a simple getter. It's a read only property written in shorthand called an expressionbodied property. All we need to know here is that whenever I use current wave in my code, it will give me the data contained in one of these data assets, which one depends on the current value of this index variable. I save that and I'll open the wave 1 data file just so we can see it. Now I can access these values through the current wave property. For example, this spawn interval will be current wave.spawn interval. So, let's actually use it down here. Whenever we spawn an enemy, we reset the timer back to the spawn interval value so that it can count again towards the next spawn. This orc will spawn every 1 second. We replaced this hard-coded value and I can delete it. We are now using datadriven design in the spawner. We are not storing values here in the script. We are pulling data from these asset files. I save and play to test if it works. We are spawning an orc every 1 second. I can change the spawn interval here and I can see my game reacts to it. So, we know this data is connected to our logic. We have a datadriven enemy spawner. I exit play mode and go back to my code. down here inside spawner. Let's select the s
Original Description
Learn how to build a complete 2D Pixel Art Tower Defense game in Unity from scratch! This step-by-step tutorial guides you from a blank project to a fully playable game with a main menu, multiple tower types, enemy waves, and three unique levels.
You’ll learn how to create a modular, data-driven system using Scriptable Objects to manage towers, enemies, and waves, plus how to design forest, lava, and jungle levels with Unity Tilemaps.
Course from @CodeLaboratory
Resources:
Main project files: https://codelaboratory.itch.io/pixel-tower-defenders-asset-pack
Fonts:
https://little-martian.itch.io/phased-font
https://caffinate.itch.io/fibberish
Characters: https://liberatedpixelcup.github.io/Universal-LPC-Spritesheet-Character-Generator/
Tiles: https://opengameart.org/content/lpc-terrain-repack
Trees:
https://opengameart.org/content/lpc-jungle
https://opengameart.org/content/lpc-baobabs
Source code: https://www.frankslaboratory.co.uk/downloads/16/source_code/source_code.zip
⭐️ Chapters ⭐️
– 0:00:00 Course Introduction and Project Overview
– 0:01:40 Setting Up the Project and Art Assets
– 0:02:41 Creating Game Objects and Using Transform
– 0:04:31 Working with Prefabs for Reusable Assets
– 0:05:40 Introduction to C# and MonoBehavior
– 0:08:30 Creating a Path System with Waypoint Arrays
– 0:09:43 Using OnDrawGizmos to Visualize the Path
– 0:14:15 Enemy Setup and Basic Movement Logic
– 0:16:54 Understanding Life Cycle Methods (OnEnable, Awake)
– 0:18:10 Implementing Time.deltaTime for Consistent Speed
– 0:19:30 Pathfinding: Moving Enemies from Waypoint to Waypoint
– 0:24:00 Deactivating Enemies at the End of the Path
– 0:25:55 Creating an Enemy Spawner
– 0:29:19 Optimizing Performance with Object Pooling
– 0:30:26 Building the Object Pooler Script
– 0:38:49 Adding Multiple Enemy Types
– 0:45:00 Data-Driven Design with Scriptable Objects
– 0:50:00 Defining Enemy Waves with Wave Data
– 0:56:33 Using Dictionaries to Link Data to Object Pools
– 1:05
Watch on YouTube ↗
(saves to browser)
Sign in to unlock AI tutor explanation · ⚡30
Playlist
Uploads from freeCodeCamp.org · freeCodeCamp.org · 0 of 60
← Previous
Next →
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
React: Production Server Setup Part 2 - Live Coding with Jesse
freeCodeCamp.org
cookies vs localStorage vs sessionStorage - Beau teaches JavaScript
freeCodeCamp.org
Browser history tutorial - Beau teaches JavaScript
freeCodeCamp.org
Graph Data Structure Intro (inc. adjacency list, adjacency matrix, incidence matrix)
freeCodeCamp.org
React: Parameterized Routing with Next.js - Live Coding with Jesse
freeCodeCamp.org
React: Dealing with jQuery Issues - Live Coding with Jesse
freeCodeCamp.org
setInterval and setTimeout: timing events - Beau teaches JavaScript
freeCodeCamp.org
Browser and Device Testing - Live Coding with Jesse
freeCodeCamp.org
Last Minute Updates - Live Coding with Jesse
freeCodeCamp.org
Post Launch Updates - Live Coding with Jesse
freeCodeCamp.org
React: Setting Up Google Analytics - Live Coding with Jesse
freeCodeCamp.org
React: Masonry Layout - Live Coding with Jesse
freeCodeCamp.org
Load Balancing Digital Ocean Droplets - Live Coding with Jesse
freeCodeCamp.org
try, catch, finally, throw - error handling in JavaScript
freeCodeCamp.org
Load Balancing: SSL Passthrough Setup - Live Coding with Jesse
freeCodeCamp.org
Graphs: breadth-first search - Beau teaches JavaScript
freeCodeCamp.org
React: Masonry Layout Part 2 - Live Coding with Jesse
freeCodeCamp.org
React: WordPress API Live Search - Live Coding with Jesse
freeCodeCamp.org
Creating WordPress Custom Post Types - Live Coding With Jesse
freeCodeCamp.org
Dates - Beau teaches JavaScript
freeCodeCamp.org
Miscellaneous Front End Updates - Live Coding with Jesse
freeCodeCamp.org
Merging a Pull Request from GitHub - Live Coding with Jesse
freeCodeCamp.org
React + Prettier + Standard JS - Live Coding with Jesse
freeCodeCamp.org
React: Sortable Responsive Table - Live Coding with Jesse
freeCodeCamp.org
Geolocation Sorting by Distance - Live Coding with Jesse
freeCodeCamp.org
Tradeoff Matrix - Agile Software Development
freeCodeCamp.org
The Definition of Ready - Agile Software Development
freeCodeCamp.org
Getting first React job without experience - Ask Preethi
freeCodeCamp.org
React: Google Analytics Click Tracking - Live Coding with Jesse
freeCodeCamp.org
Submitting a PR to an Open Source Project - Live Coding with Jesse
freeCodeCamp.org
Should I go back to school to get CS degree? - Ask Preethi
freeCodeCamp.org
Hero Section CSS Changes - Live Coding with Jesse
freeCodeCamp.org
Working Agreement - Agile Software Development
freeCodeCamp.org
A day at Pennybox with Co-Founder Reji Eapen
freeCodeCamp.org
React: Sorting and Filtering Data - Live Coding with Jesse
freeCodeCamp.org
React: Sorting and Filtering Data Part 2 - Live Coding with Jesse
freeCodeCamp.org
React: Building a New UI - Live Coding with Jesse
freeCodeCamp.org
Definition of Done - Agile Software Development
freeCodeCamp.org
Getting started with jQuery (tutorial) - Beau teaches JavaScript
freeCodeCamp.org
Making a React Blog with WordPress Content - Live Coding with Jesse
freeCodeCamp.org
React, NextJS, CSS - Live Coding with Jesse
freeCodeCamp.org
jQuery events - Beau teaches JavaScript
freeCodeCamp.org
React/NextJS Routing and WordPress API Custom Types - Live Coding with Jesse
freeCodeCamp.org
React: Working with API Data - Live Coding with Jesse
freeCodeCamp.org
React: Refactoring Components - Live Streaming with Jesse
freeCodeCamp.org
jQuery effects - Beau teaches JavaScript
freeCodeCamp.org
More React Refactoring - Live Coding with Jesse
freeCodeCamp.org
animate in jQuery - Beau teaches JavaScript
freeCodeCamp.org
"Finishing" My React Site - Live Coding with Jesse
freeCodeCamp.org
Starting a New React Project (P2D1) - Live Coding with Jesse
freeCodeCamp.org
React Project 2 Day 2: Learning Material UI - Live Coding with Jesse
freeCodeCamp.org
The Agile Manifesto - Agile Software Development
freeCodeCamp.org
jQuery: get and set with http, text, val, and attr - Beau teaches JavaScript
freeCodeCamp.org
React Project 2 Day 3 - Live Coding with Jesse
freeCodeCamp.org
The INVEST approach to product backlog items
freeCodeCamp.org
React Project 2 Day 4 - Live Coding with Jesse
freeCodeCamp.org
Chickens and Pigs - Agile Software Development
freeCodeCamp.org
React Project 2 Day 5 - Live Coding with Jesse
freeCodeCamp.org
jQuery: add and remove DOM elements - Beau teaches JavaScript
freeCodeCamp.org
React Project 2 Day 6 - Live Coding with Jesse
freeCodeCamp.org
More on: AI Systems Design
View skill →
🎓
Tutor Explanation
DeepCamp AI