Menu
CoddyTech

Chiêu cuối

Bài học 25 trên 25 trong khóa học Trò chơi Roblox: Đấu trường của Coddy.

Khi thanh năng lượng đầy, R đánh trúng mọi người trong phạm vi Settings.UltimateRadius với Settings.UltimateDamage rồi làm thanh cạn sạch. Sau đó, chiêu này cần chờ hồi chiêu Settings.Cooldowns.Ultimate.

Chiêu tối thượng khi thanh năng lượng đã cạn không nên làm gì và cũng không tốn gì, vì vậy giờ đây một kỹ năng có thể từ chối bằng cách trả về false, còn trình xử lý chỉ đặt thời gian hồi chiêu khi kỹ năng không trả về giá trị đó:

if abilities[name](player, character, root) ~= false then
	-- đặt ReadyAt của nó
end

Đó là toàn bộ trò chơi: chiến đấu, kết hợp đòn, đỡ đòn và làm đầy thanh năng lượng.

challenge icon

Thử thách

Khó

Dùng tuyệt chiêu bằng phím R khi thanh năng lượng đầy.

  1. Thêm abilities.Ultimate: trả về false nếu thanh năng lượng chưa đầy; nếu đầy, gây sát thương cho tất cả mọi người trong phạm vi Settings.UltimateRadius (mỗi người một lần, không tính bạn) bằng Settings.UltimateDamage, rồi làm trống thanh năng lượng.
  2. Trình xử lý chỉ đặt thời gian hồi chiêu nếu khả năng không trả về false.
  3. Trong BattleClient, gán "Ultimate" cho phím R (không thêm vào shown).

Nhấn Chơi, nạp đầy thanh năng lượng rồi nhấn R: mọi người ở gần mất 50 máu. Sau đó nhấn Kiểm tra.

Tự làm thử

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local Debris = game:GetService("Debris")
local PathfindingService = game:GetService("PathfindingService")
local Settings = require(game.ReplicatedStorage.Settings)

local remotes = game.ReplicatedStorage.Remotes
local bots = workspace.Bots
local lastSwing = {} -- player -> thời điểm vung kiếm gần nhất
local combo = {} -- player -> { số đòn đánh liên tiếp, thời điểm đòn gần nhất trúng mục tiêu }
local lastHit = {} -- humanoid -> người chơi đánh trúng lần cuối

-- Các hoạt ảnh vung kiếm có sẵn của Roblox
local slash = Instance.new("Animation")
slash.AnimationId = "rbxassetid://522635514"
local lunge = Instance.new("Animation")
lunge.AnimationId = "rbxassetid://522638767"

print("Swords deal " .. Settings.SwordDamage .. " damage")

-- Con số hiện nổi trên người vừa trúng đòn trong chốc lát
local function damageNumber(character, amount)
	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then
		return
	end
	local tag = Instance.new("BillboardGui")
	tag.Name = "Damage"
	tag.Size = UDim2.fromOffset(60, 30)
	tag.StudsOffset = Vector3.new(0, 4.5, 0)
	tag.Adornee = root
	local text = Instance.new("TextLabel")
	text.Size = UDim2.fromScale(1, 1)
	text.BackgroundTransparency = 1
	text.TextColor3 = Color3.fromRGB(255, 220, 80)
	text.TextScaled = true
	text.Text = "-" .. math.round(amount)
	text.Parent = tag
	tag.Parent = character
	Debris:AddItem(tag, 0.8)
end

-- Mọi đòn đánh trong trò chơi đều đi qua đây (attacker: người chơi tung đòn, hoặc nil)
local function hurt(humanoid, amount, attacker)
	if humanoid.Health <= 0 then
		return
	end
	local character = humanoid.Parent
	-- Đỡ đòn giúp giảm phần lớn sát thương
	if character:GetAttribute("Blocking") then
		amount *= Settings.BlockMultiplier
	end
	humanoid:TakeDamage(amount)
	damageNumber(character, amount)
	if attacker then
		lastHit[humanoid] = attacker
		-- Đánh trúng sẽ nạp thanh tuyệt kỹ
		local meter = attacker:GetAttribute("Ultimate") or 0
		attacker:SetAttribute("Ultimate", math.min(Settings.UltimateMax, meter + Settings.UltimatePerHit))
	end
end

-- Hiệu ứng choáng khiến mục tiêu đứng yên trong chốc lát
local function stun(humanoid)
	local character = humanoid.Parent
	if character:GetAttribute("Stunned") then
		return
	end
	character:SetAttribute("Stunned", true)
	print(character.Name .. " is stunned")
	local speed = humanoid.WalkSpeed
	humanoid.WalkSpeed = 0
	task.delay(Settings.StunTime, function()
		if character.Parent then
			humanoid.WalkSpeed = speed
			character:SetAttribute("Stunned", nil)
		end
	end)
end

-- Một lần vung kiếm: hoạt ảnh chạy trước, rồi mọi mục tiêu trong vùng phía trước bạn trúng đòn
local function swing(player)
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	if not root or character.Humanoid.Health <= 0 then
		return
	end
	local now = time()
	if lastSwing[player] and now - lastSwing[player] < Settings.SwingCooldown then
		return
	end
	lastSwing[player] = now
	-- Ba lần vung kiếm liên tiếp tạo thành một chuỗi đòn; lần thứ ba là đòn đâm
	local c = combo[player]
	if c and now - c.last <= Settings.ComboWindow then
		c.count = c.count % 3 + 1
	else
		c = { count = 1 }
		combo[player] = c
	end
	c.last = now
	local finisher = c.count == 3
	character.Humanoid.Animator:LoadAnimation(finisher and lunge or slash):Play()
	-- Vùng đánh ở phía trước bạn, không bao gồm chính bạn
	local params = OverlapParams.new()
	params.FilterDescendantsInstances = { character }
	local box = root.CFrame * CFrame.new(0, 0, -3)
	local hit = {} -- Mỗi humanoid chỉ trúng đòn một lần trong mỗi cú vung kiếm
	for _, part in workspace:GetPartBoundsInBox(box, Settings.HitboxSize, params) do
		local humanoid = part.Parent:FindFirstChildOfClass("Humanoid")
		if humanoid and not hit[humanoid] then
			hit[humanoid] = true
			hurt(humanoid, Settings.ComboDamage[c.count], player)
			if finisher then
				stun(humanoid)
			end
		end
	end
end

-- Các kỹ năng theo tên và tác dụng của từng kỹ năng
local abilities = {}

-- Thiết bị của người chơi điều khiển nhân vật, nên thao tác lao tới diễn ra trên thiết bị đó
function abilities.Dash(player)
	remotes.Ability:FireClient(player, "Dash")
end

-- Mọi người xung quanh bạn đều trúng đòn và bị hất ra xa
function abilities.Slam(player, character, root)
	local params = OverlapParams.new()
	params.FilterDescendantsInstances = { character }
	local hit = {}
	for _, part in workspace:GetPartBoundsInRadius(root.Position, Settings.SlamRadius, params) do
		local humanoid = part.Parent:FindFirstChildOfClass("Humanoid")
		local theirRoot = part.Parent:FindFirstChild("HumanoidRootPart")
		if humanoid and theirRoot and not hit[humanoid] then
			hit[humanoid] = true
			hurt(humanoid, Settings.SlamDamage, player)
			local away = (theirRoot.Position - root.Position) * Vector3.new(1, 0, 1)
			if away.Magnitude > 0 then
				away = away.Unit
			end
			theirRoot:ApplyImpulse((away * Settings.SlamKnockback + Vector3.new(0, 40, 0)) * theirRoot.AssemblyMass)
		end
	end
end

function abilities.Block(player, character)
	character:SetAttribute("Blocking", true)
	task.delay(Settings.BlockTime, function()
		if character.Parent then
			character:SetAttribute("Blocking", nil)
		end
	end)
end

-- Bài học 25: thêm Ultimate vào đây

-- Client yêu cầu một kỹ năng theo tên. Máy chủ kiểm tra: kỹ năng có thật,
-- người chơi còn sống và thời gian hồi chiêu đã hết. Thời điểm kỹ năng sẵn sàng trở lại
-- là một thuộc tính của người chơi để màn hình của họ có thể hiển thị.
remotes.Ability.OnServerEvent:Connect(function(player, name)
	if typeof(name) ~= "string" or not abilities[name] then
		return
	end
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	if not root or character.Humanoid.Health <= 0 then
		return
	end
	local now = workspace:GetServerTimeNow()
	if now < (player:GetAttribute(name .. "ReadyAt") or 0) then
		return
	end
	player:SetAttribute(name .. "ReadyAt", now + Settings.Cooldowns[name])
	abilities[name](player, character, root)
end)

-- Mỗi người chơi có Kills trên bảng xếp hạng và nhận một thanh kiếm mỗi khi hồi sinh
Players.PlayerAdded:Connect(function(player)
	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"
	leaderstats.Parent = player

	local kills = Instance.new("IntValue")
	kills.Name = "Kills"
	kills.Parent = leaderstats
	player:SetAttribute("Ultimate", 0)

	player.CharacterAdded:Connect(function()
		player:SetAttribute("Streak", 0)
		local sword = ServerStorage.Sword:Clone()
		sword.Parent = player.Backpack
		sword.Activated:Connect(function()
			swing(player)
		end)
	end)
end)

-- Hình nộm tập luyện: thanh máu thay đổi theo lượng máu, và khi bị đánh bại,
-- hình nộm sẽ trở lại sau một lát dưới dạng bản sao mới ở cùng vị trí
local function setupDummy(dummy)
	local humanoid = dummy.Humanoid
	humanoid.HealthChanged:Connect(function(health)
		dummy.HealthBar.Back.Fill.Size = UDim2.fromScale(health / humanoid.MaxHealth, 1)
	end)
	humanoid.Died:Connect(function()
		local spot = dummy:GetPivot()
		task.wait(0.5)
		dummy:Destroy()
		task.wait(Settings.DummyRespawn)
		local fresh = ServerStorage.Dummy:Clone()
		fresh:PivotTo(spot)
		fresh.Parent = workspace
		setupDummy(fresh)
	end)
end
setupDummy(workspace.Dummy)

-- Nhân vật còn sống gần nhất mà bot nhìn thấy và khoảng cách đến nhân vật đó
local function nearestCharacter(position)
	local best, bestDistance = nil, Settings.BotSight
	for _, player in Players:GetPlayers() do
		local character = player.Character
		local root = character and character:FindFirstChild("HumanoidRootPart")
		if root and character.Humanoid.Health > 0 then
			local distance = (root.Position - position).Magnitude
			if distance < bestDistance then
				best, bestDistance = character, distance
			end
		end
	end
	return best, bestDistance
end

-- Đi về phía nhân vật: đi thẳng khi không có vật cản, hoặc
-- đi đến điểm tiếp theo trên đường vòng tránh vật cản
local function walkToward(bot, target, path)
	local root = bot.HumanoidRootPart
	local goal = target.HumanoidRootPart.Position
	local params = RaycastParams.new()
	params.FilterDescendantsInstances = { bot }
	local hit = workspace:Raycast(root.Position, goal - root.Position, params)
	if not hit or hit.Instance:IsDescendantOf(target) then
		bot.Humanoid:MoveTo(goal)
		return
	end
	path:ComputeAsync(root.Position, goal)
	local points = path:GetWaypoints()
	if path.Status == Enum.PathStatus.Success and points[2] then
		bot.Humanoid:MoveTo(points[2].Position)
	end
end

-- Cách bot suy nghĩ: cứ mỗi một phần tư giây, bot quan sát xung quanh rồi quyết định
local function runBot(bot)
	local humanoid = bot.Humanoid
	local root = bot.HumanoidRootPart
	local lastAttack = 0
	local path = PathfindingService:CreatePath()
	while bot.Parent and humanoid.Health > 0 do
		local target, distance = nearestCharacter(root.Position)
		local state
		if bot:GetAttribute("Stunned") then
			state = "Stunned"
		elseif humanoid.Health < humanoid.MaxHealth * Settings.RetreatAt then
			state = "Retreat"
		elseif not target then
			state = "Idle"
		elseif distance <= Settings.BotRange then
			state = "Attack"
		else
			state = "Chase"
		end
		bot:SetAttribute("State", state)
		if state == "Chase" then
			walkToward(bot, target, path)
		elseif state == "Attack" then
			humanoid:MoveTo(root.Position)
			if time() - lastAttack >= Settings.BotAttackCooldown then
				lastAttack = time()
				humanoid.Animator:LoadAnimation(slash):Play()
				hurt(target.Humanoid, Settings.BotDamage)
			end
		elseif state == "Retreat" and target then
			local away = (root.Position - target.HumanoidRootPart.Position) * Vector3.new(1, 0, 1)
			if away.Magnitude > 0 then
				humanoid:MoveTo(root.Position + away.Unit * 12)
			end
		else
			humanoid:MoveTo(root.Position)
		end
		task.wait(0.25)
	end
end

-- Một bot mới trên bệ màu đỏ, với các chỉ số và thanh máu
local function spawnBot()
	local bot = ServerStorage.Bot:Clone()
	bot:PivotTo(workspace.BotSpawn.CFrame + Vector3.new(0, 3, 0))
	bot.Humanoid.MaxHealth = Settings.BotHealth
	bot.Humanoid.Health = Settings.BotHealth
	bot.Humanoid.WalkSpeed = Settings.BotSpeed
	bot.Parent = bots
	-- Để máy chủ xử lý vật lý của bot, giúp lực đẩy từ cú đập di chuyển bot
	bot.HumanoidRootPart:SetNetworkOwner(nil)
	bot.Humanoid.HealthChanged:Connect(function(health)
		bot.HealthBar.Back.Fill.Size = UDim2.fromScale(health / bot.Humanoid.MaxHealth, 1)
	end)
	bot.Humanoid.Died:Connect(function()
		-- Người chơi đánh bot cuối cùng được tính là người hạ gục
		local killer = lastHit[bot.Humanoid]
		lastHit[bot.Humanoid] = nil
		if killer and killer.Parent then
			killer.leaderstats.Kills.Value += 1
			killer:SetAttribute("Streak", (killer:GetAttribute("Streak") or 0) + 1)
		end
		task.wait(1)
		bot:Destroy()
		task.wait(Settings.BotRespawn)
		spawnBot()
	end)
	task.spawn(runBot, bot)
end

for _ = 1, Settings.Bots do
	spawnBot()
end
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Trò chơi Roblox: Đấu trường

Tự luyện tập: Playground Roblox