An In-Depth Guide to a Tower Defense Game [Part #1]

+---+ TOWER DEFENSE GUIDE +---+

As stated in the title, this resource will aid the readers with developing their games focused on tower defense. This tutorial will cover the fundamentals of a classic tower defense game. From tower placements, as well as the upgrading system, this material will definitely benefit readers. Let’s begin.


CONTENTS

This resource will cover the following:

  1. Path & Spawning System

  2. Tower Placement, Functionality & Upgrades

  3. Wave Implementation

  4. Lobbies & Matchmaking

  5. Effects??? (Still undecided)


PATH & SPAWNING SYSTEM

  • To start of, let us create a rig for the enemies that will spawn, as well as a simple path for them to walk on.

Here is a sample enemy rig that we will use:

And here is a sample path for the enemies to walk on:

  • Now that that’s done, we’ll move on to the actual path positions that the enemies will be moving to. To get started, place 1x1 parts on the starting and ending point of the path, as well as on every intersection of the paths you’ve made. You can place the parts 1 stud above. Like so:

Also, make sure that the 1x1 parts are facing the parts before them. It will come in handy later. Like this:

To achieve this, open up CustomizeQuickAccessToolbar, which is the arrow bar before the Home tab, then click Customize, search Show Orientation Indicator and then click Add. Once added in, click on each of the 1x1 parts and rotate them to the desired rotation, which you can easily do using Ctrl + R.

  • After that, rename all the 1x1 parts in numerical order depending on which 1x1 part you want the path to start. Place them in a folder in the Workspace named “Path”

image

  • Lastly, place the enemy model inside a folder named “Enemies” located inside the ReplicatedStorage

image


Now we move on the the scripting part of the path system. First of, create a module script inside the ServerScriptService and name it EnemyHandler. This is the script for the EnemyHandler module. This code contains a function named Spawn that will take in two parameters. EnemyName which is the name of the enemy model to spawn, and SpawnCFrame which is the CFrame that the enemy model would be positioned after spawning. The function checks whether the first parameter, EnemyName, is a valid and existing model inside the Enemies folder, and not a nil and nonexistent object. Then if it passes through the if-statement, the enemy model would be cloned and shall be positioned based on the SpawnCFrame parameter.

Additionally, once cloned, another function would run. This is the Init_Scripts function. This function takes in a parameter named EnemyModel and first checks if the parameter passed is existent. If it passes, it loops through a folder inside the module named EnemyScripts, and it checks whether the object it has found is a script. If it is a script, it clones it, enables it, and places it into the enemy model.

local EnemyHandler = {}

local Enemies = game.ServerStorage:WaitForChild("Enemies")

local function Init_Scripts(EnemyModel:Model)
	if EnemyModel then
		for _,item in script:WaitForChild("EnemyScripts"):GetChildren() do
			if item:IsA("Script") then
				local clone = item:Clone()
				clone.Parent = EnemyModel
				clone.Enabled = true
			end
		end
	end
end

function EnemyHandler.Spawn(EnemyName:string, SpawnCFrame:CFrame)
	if Enemies:FindFirstChild(EnemyName) then
		local EnemyClone = Enemies:FindFirstChild(EnemyName):Clone()
		EnemyClone:SetPrimaryPartCFrame(SpawnCFrame)
		Init_Scripts(EnemyClone)
		EnemyClone.Parent = workspace
	end
end

return EnemyHandler

Now let’s get to the folder inside the module, specifically the EnemyScripts folder. But first, go ahead to your enemy model and put in an ObjectValue named TargetWaypoint, as well as an Animation object named Walk. You may choose to insert an AnimationId into the Animation object now, or leave it blank.

Now, create a folder named, you guessed it, EnemyScripts inside the module. Then, create 2 scripts with the first one being named Move and the other one being named Animations.

The Move script handles the enemy’s movement through the pathways. It has a function named MoveToWaypoint which is the function that actually moves the enemy. It uses the built-in Humanoid function called MoveTo, which takes in a Position and moves the enemy to that specified position. Then, the function has a MoveToFinished event, which is also built-in into the Humanoid. This MoveToFinished event runs whenever the Humanoid has successfully reached the target position within 8 seconds. If it succeeds, it returns true, and false if not. If the function detects that it got true, it would disconnect the event and move on to the next waypoint. If not, then the function would return the same MoveToWaypoint function in order for the Humanoid to successfully reach the target position. Then the script has a for loop that loops through the path waypoints and runs the MoveToWaypoint function on each iteration on each path.

local Path = workspace:WaitForChild("Path")

local TargetWaypoint = script.Parent:WaitForChild("TargetWaypoint")
local Humanoid = script.Parent:WaitForChild("Humanoid")

local ReachedCurrentWaypoint = false

local Finished = nil

local function MoveToWaypoint(point)
	Humanoid:MoveTo(point)
	Finished = Humanoid.MoveToFinished:Connect(function(reached)
		print(reached)
		if reached == false then
			Finished:Disconnect()
			return MoveToWaypoint(point)
		else
			Finished:Disconnect()
			ReachedCurrentWaypoint = true
			return
		end
	end)
end

for i=2,#Path:GetChildren(),1 do
	ReachedCurrentWaypoint = false
	
	local CurrentPath = Path:FindFirstChild(tostring(i))
	TargetWaypoint = CurrentPath
	local DesiredPosition = (CurrentPath.CFrame * CFrame.new(0,0,1)).Position
	
	MoveToWaypoint(DesiredPosition)
	
	repeat task.wait() until ReachedCurrentWaypoint == true
end

Lastly, the Animations script basically just plays and loops the Walk animation it found within the enemy model. You may change the number between the parenthesis on the Play() so that the animation speed matches the enemy’s walking speed.

local Humanoid = script.Parent:WaitForChild("Humanoid")
local WalkAnimation = Humanoid:WaitForChild("Animator"):LoadAnimation(script.Parent:WaitForChild("Walk"))

WalkAnimation:Play(1) --// Change this value to match the enemy's walk speed
WalkAnimation.Looped = true

TESTING

We can test the system we’ve made by creating a Script inside ServerScriptService. You could name it to anything you want since it will only be used for testing. The script requires the EnemyModule that we’ve scripted and it uses it to run the Spawn function for every iteration of the for loop. You can also see that we used the first 1x1 part as the spawn position of the enemies. You may change the value of the NumberOfEnemyToSpawn as well as the value of the SpawnDelay. Note that the SpawnDelay is in seconds.

local EnemyHandler = require(script.Parent:WaitForChild("EnemyHandler"))
local Path = workspace:WaitForChild("Path"):GetChildren()
local NumberOfEnemyToSpawn = 5
local SpawnDelay = .75

task.wait(5)

for i=1,NumberOfEnemyToSpawn do
	EnemyHandler.Spawn("Enemy", Path[1].CFrame)
	task.wait(SpawnDelay)
end

VIDEO TEST


REFERENCES & DOCUMENTATION

Additional Material/s:


THANK YOU!

+----+ STAY TUNED FOR PART 2 +-----+

8 Likes

Have you tested this with 100s of zombies? What is the performance like? From what I’ve heard, humanoids are awfully unoptimised.

4 Likes

As @SkrubDaNub said, since the title includes “in depth”, I would atleast warn about the potential performance loss of using lots of humanoids. However, this is still a very helpful tutorial. Great job :+1:

4 Likes

Using humanoids wouldn’t be the optimized version of tower defense games, as they mostly use client-side rendering for their enemies and units in order to run a lot of enemies without server performance dropping.

4 Likes