Menu
CoddyTech

건너뛰고 이기기

Coddy의 Roblox 게임: 타워 디펜스 코스 레슨. 18개 중 17번째.

준비가 끝난 상태에서 다음 웨이브를 기다리는 것은 지루합니다. HUD의 Skip wait 버튼은 Remotes.Skip을 통해 서버에 지금 다음 웨이브를 시작하도록 요청합니다.

서버는 휴식 시간을 짧은 단계로 나누어 기다리므로, 일찍 중단할 수 있습니다. workspace 속성 Break는 휴식 시간이 진행 중인지 나타내며, 변수 skipped는 매 휴식 시간이 시작될 때 초기화됩니다.

skipped = false
local started = time()
while time() - started < Settings.WaveBreak and not skipped do
	task.wait(0.1)
end

마지막 웨이브가 끝나면 기지가 버텨낸 것입니다. 서버는 workspace 속성 Status를 "Won"으로 설정하고, 클라이언트는 HUD의 Message를 표시합니다.

challenge icon

챌린지

어려움
  1. DefenseServer에서 local skipped = false를 추가하고, Break 속성이 켜져 있는 동안 true로 설정하도록 remotes.Skip.OnServerEvent를 연결합니다.
  2. 각 웨이브 전에 Break를 true로 설정하고, skipped를 초기화한 다음, 휴식 시간이 끝나거나 건너뛸 때까지 (0.1 간격으로) 기다리고 Break를 false로 설정합니다.
  3. 루프가 끝난 뒤, workspace 속성 Status를 "Won"으로 설정합니다.
  4. DefenseClient에서 Skip 버튼은 remotes.Skip을 호출합니다. 그리고 한 번 호출되고 Status가 변경될 때마다 호출되는 showEnd()는 Status가 "Won"일 때 Message 레이블에 You defended the base!를 표시합니다.

이 레슨의 월드에는 짧은 웨이브가 두 개 있습니다. Play를 누른 다음 Check를 누르세요.

직접 해보기

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
local towerPad = {} -- 각 타워 -> 타워가 놓인 패드

print("The base can take " .. Settings.BaseHealth .. " hits")

-- 순서대로 정렬된 경유 지점들 (이름은 1, 2, 3...)
local waypoints = workspace.Path.Waypoints:GetChildren()
table.sort(waypoints, function(a, b)
	return tonumber(a.Name) < tonumber(b.Name)
end)

-- workspace의 속성으로 저장된 기지의 체력: 모든 클라이언트가 읽을 수 있음
workspace:SetAttribute("BaseHealth", Settings.BaseHealth)

-- 모든 플레이어가 리더보드에서 Money를 받음
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)

-- 적이 기지에 도달함
local function damageBase(amount)
	local health = math.max(0, workspace:GetAttribute("BaseHealth") - amount)
	workspace:SetAttribute("BaseHealth", health)
end

-- 적을 경로를 따라 이동시킴. MoveTo는 8초 후 시도를 포기하고 이를 알림
-- (MoveToFinished가 false를 반환함). 따라서 느린 적은 도착할 때까지 다시 이동 명령을 받음.
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
	-- 기지에 도달함
	damageBase(Settings.Enemies[enemy.Name].Damage)
	enemy:Destroy()
end

-- 입구에서 나타나 경로를 따라 이동하는 새로운 적
local function spawnEnemy(kind)
	local enemy = ServerStorage.Enemies[kind]:Clone()
	-- 첫 번째 경유 지점 바로 위에 생성됨
	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.HealthChanged:Connect(function(health)
		enemy.HealthBar.Back.Fill.Size = UDim2.fromScale(health / enemy.Humanoid.MaxHealth, 1)
	end)
	enemy.Humanoid.Died:Connect(function()
		-- 모든 플레이어가 보상을 받음
		for _, player in Players:GetPlayers() do
			player.leaderstats.Money.Value += info.Reward
		end
		enemy:Destroy()
	end)
	task.spawn(walkPath, enemy)
	return enemy
end

-- 한 지점에서 사거리 안에 있는 가장 가까운 살아 있는 적, 없으면 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

-- 타워에서 목표물로 뻗는 섬광
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

-- Frost: 적이 2초 동안 원래 속도의 일부로 이동함
local function slow(enemy, factor)
	if enemy:GetAttribute("Slowed") then
		return
	end
	enemy:SetAttribute("Slowed", true)
	local speed = Settings.Enemies[enemy.Name].Speed
	enemy.Humanoid.WalkSpeed = speed * factor
	task.delay(2, function()
		if enemy.Parent then
			enemy.Humanoid.WalkSpeed = speed
			enemy:SetAttribute("Slowed", nil)
		end
	end)
end

-- 공격: 목표물에 피해를 줌(Cannon이라면 주변의 모든 적에게도 피해를 줌)
local function shoot(tower, target, info)
	local damage = info.Damage * tower:GetAttribute("Level")
	local at = target.HumanoidRootPart.Position
	if info.Splash then
		for _, enemy in enemies:GetChildren() do
			local root = enemy:FindFirstChild("HumanoidRootPart")
			if root and (root.Position - at).Magnitude <= info.Splash then
				enemy.Humanoid:TakeDamage(damage)
			end
		end
	else
		target.Humanoid:TakeDamage(damage)
	end
	if info.Slow and target.Parent then
		slow(target, info.Slow)
	end
	beam(tower.Head.Position, at)
end

-- 타워의 동작: 사거리 안에서 가장 가까운 적을 조준하고 공격한 뒤 재사용 대기시간을 기다림
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

-- 패드 위에 타워를 건설함
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:SetAttribute("Level", 1)
	tower.Parent = towers
	pad:SetAttribute("Used", true)
	towerPad[tower] = pad
	task.spawn(runTower, tower)
end

-- 클라이언트가 패드 위에 타워를 요청함. 모든 조건을 확인: 실제로 존재하고 비어 있는 패드,
-- 실제로 존재하는 타워, 그리고 구매에 필요한 돈.
remotes.Place.OnServerEvent:Connect(function(player, pad, kind)
	if typeof(pad) ~= "Instance" or pad.Parent ~= workspace.Pads or pad:GetAttribute("Used") then
		return
	end
	local info = typeof(kind) == "string" and Settings.Towers[kind]
	if not info then
		return
	end
	local money = player.leaderstats.Money
	if money.Value < info.Price then
		return
	end
	money.Value -= info.Price
	placeTower(pad, kind)
end)

-- 업그레이드: 다음 레벨의 비용은 매번 증가하고, 레벨이 오를 때마다
-- 타워의 공격력이 추가됨
remotes.Upgrade.OnServerEvent:Connect(function(player, tower)
	if typeof(tower) ~= "Instance" or tower.Parent ~= towers then
		return
	end
	local level = tower:GetAttribute("Level")
	local cost = math.floor(Settings.Towers[tower.Name].Price * Settings.UpgradeCost * level)
	local money = player.leaderstats.Money
	if money.Value < cost then
		return
	end
	money.Value -= cost
	tower:SetAttribute("Level", level + 1)
end)

-- 판매: 가격의 절반을 돌려받고 패드를 다시 사용할 수 있음
remotes.Sell.OnServerEvent:Connect(function(player, tower)
	if typeof(tower) ~= "Instance" or tower.Parent ~= towers then
		return
	end
	player.leaderstats.Money.Value += math.floor(Settings.Towers[tower.Name].Price / 2)
	towerPad[tower]:SetAttribute("Used", nil)
	towerPad[tower] = nil
	tower:Destroy()
end)

-- 한 웨이브: 적들이 하나씩 나타난 뒤 모두 사라질 때까지 기다림
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

-- 각 웨이브는 휴식 시간이 지난 뒤 시작
for i, list in Settings.Waves do
	workspace:SetAttribute("Wave", i)
	task.wait(Settings.WaveBreak)
	runWave(list)
end
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

Roblox 게임: 타워 디펜스의 모든 레슨