Can't Afford It
Lesson 10 of 18 in Coddy's Roblox Game: Tower Defense course.
So far towers are free, and the server builds whatever it is asked. A player controls their own device, so an exploiter can fire Remotes.Place with anything: a tower on top of another, on the baseplate, a tower that does not exist, with no money at all.
So the server checks every request before it builds, and returns at the first thing that is wrong:
- the pad is an Instance in
workspace.Padsand has no tower yet (an attributeUsedthatplaceTowersets), - the kind is a string naming a real tower,
- the player has the money; then the server takes it.
if typeof(pad) ~= "Instance" or pad.Parent ~= workspace.Pads then
return
endThe client asks, the server decides. The checks in this lesson play the exploiter.
Challenge
MediumIn the Place handler, return without building when:
- the pad is not an Instance, is not in
workspace.Pads, or has the attributeUsed, - the kind is not a string naming a tower in
Settings.Towers, - the player's Money is below the tower's
Price.
Otherwise take the price and build. In placeTower, set the pad's Used attribute to true.
Press Play, then Check.
Try it yourself
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local Debris = game:GetService("Debris")
local Settings = require(game.ReplicatedStorage.Settings)
local remotes = game.ReplicatedStorage.Remotes
local enemies = workspace.Enemies
local towers = workspace.Towers
print("The base can take " .. Settings.BaseHealth .. " hits")
-- The waypoints, in order (they are named 1, 2, 3...)
local waypoints = workspace.Path.Waypoints:GetChildren()
table.sort(waypoints, function(a, b)
return tonumber(a.Name) < tonumber(b.Name)
end)
-- The base's health, as an attribute of the workspace: every client can read it
workspace:SetAttribute("BaseHealth", Settings.BaseHealth)
-- Every player gets Money on the leaderboard
Players.PlayerAdded:Connect(function(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local money = Instance.new("IntValue")
money.Name = "Money"
money.Value = Settings.StartMoney
money.Parent = leaderstats
end)
-- An enemy reached the base
local function damageBase(amount)
local health = math.max(0, workspace:GetAttribute("BaseHealth") - amount)
workspace:SetAttribute("BaseHealth", health)
end
-- Walk an enemy along the path. MoveTo gives up after 8 seconds and says so
-- (MoveToFinished with false), so a slow enemy is sent again until it arrives.
local function walkPath(enemy)
local humanoid = enemy.Humanoid
for i = 2, #waypoints do
repeat
humanoid:MoveTo(waypoints[i].Position)
until humanoid.MoveToFinished:Wait() or not enemy.Parent
if not enemy.Parent then
return
end
end
-- it reached the base
damageBase(Settings.Enemies[enemy.Name].Damage)
enemy:Destroy()
end
-- A new enemy at the gate, walking the path
local function spawnEnemy(kind)
local enemy = ServerStorage.Enemies[kind]:Clone()
-- dropped in just above the first waypoint
enemy:PivotTo(waypoints[1].CFrame + Vector3.new(0, 5, 0))
local info = Settings.Enemies[kind]
enemy.Humanoid.MaxHealth = info.Health
enemy.Humanoid.Health = info.Health
enemy.Humanoid.WalkSpeed = info.Speed
enemy.Parent = enemies
enemy.Humanoid.Died:Connect(function()
-- every player earns the reward
for _, player in Players:GetPlayers() do
player.leaderstats.Money.Value += info.Reward
end
enemy:Destroy()
end)
task.spawn(walkPath, enemy)
return enemy
end
-- The nearest living enemy within range of a point, or nil
local function nearestEnemy(position, range)
local best, bestDistance = nil, range
for _, enemy in enemies:GetChildren() do
local root = enemy:FindFirstChild("HumanoidRootPart")
if root and enemy.Humanoid.Health > 0 then
local distance = (root.Position - position).Magnitude
if distance <= bestDistance then
best, bestDistance = enemy, distance
end
end
end
return best
end
-- A flash of light from a tower to its target
local function beam(from, to)
local ray = Instance.new("Part")
ray.Anchored = true
ray.CanCollide = false
ray.CanTouch = false
ray.Material = Enum.Material.Neon
ray.Color = Color3.fromRGB(255, 240, 150)
ray.Size = Vector3.new(0.2, 0.2, (to - from).Magnitude)
ray.CFrame = CFrame.lookAt((from + to) / 2, to)
ray.Parent = workspace
Debris:AddItem(ray, 0.1)
end
-- A shot: damage the target
local function shoot(tower, target, info)
local damage = info.Damage
local at = target.HumanoidRootPart.Position
target.Humanoid:TakeDamage(damage)
beam(tower.Head.Position, at)
end
-- A tower's life: aim at the nearest enemy in range, shoot, wait its cooldown
local function runTower(tower)
local info = Settings.Towers[tower.Name]
while tower.Parent do
local target = nearestEnemy(tower.Head.Position, info.Range)
if target then
tower.Head.CFrame = CFrame.lookAt(tower.Head.Position, target.HumanoidRootPart.Position)
shoot(tower, target, info)
task.wait(info.Cooldown)
else
task.wait(0.1)
end
end
end
-- Build a tower on a pad
local function placeTower(pad, kind)
local tower = ServerStorage.Towers[kind]:Clone()
tower:PivotTo(pad.CFrame + Vector3.new(0, pad.Size.Y / 2 + 1, 0))
tower.Parent = towers
task.spawn(runTower, tower)
end
-- The client asks for a tower on a pad
remotes.Place.OnServerEvent:Connect(function(player, pad, kind)
placeTower(pad, kind)
end)
-- One wave: its enemies one by one, then wait until they are all gone
local function runWave(list)
for _, kind in list do
spawnEnemy(kind)
task.wait(Settings.SpawnGap)
end
while #enemies:GetChildren() > 0 do
task.wait(0.2)
end
end
-- The waves, each after a break
for i, list in Settings.Waves do
task.wait(Settings.WaveBreak)
runWave(list)
end
This lesson includes a short quiz. Start the lesson to answer it and track your progress.