Menu
CoddyTech

Trạng thái của bot

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

Đầu óc của bot đang rối tung. AI trong trò chơi thường được viết theo trạng thái: mỗi nhịp, xác định bot đang ở trạng thái nào, rồi thực hiện hành động của trạng thái đó. Bot này có năm trạng thái:

  • Stunned: đòn combo của bạn khiến nó bất động. Nó không làm gì cả.
  • Retreat: khi máu còn dưới Settings.RetreatAt, nó chạy trốn khỏi bạn.
  • Idle: không thấy ai.
  • Attack: bạn ở trong tầm đánh.
  • Chase: nó nhìn thấy bạn.

Thứ tự kiểm tra thể hiện các mức độ ưu tiên của bot. Trạng thái là một thuộc tính của bot, State, nên bạn có thể xem nó đang nghĩ gì.

challenge icon

Thử thách

Khó

Gán trạng thái cho bot.

  1. Trong vòng lặp của runBot, đặt state thành trạng thái đầu tiên đúng trong danh sách sau: "Stunned", "Retreat", "Idle", "Attack", nếu không thì là "Chase", rồi lưu trạng thái đó vào thuộc tính State của bot.
  2. Ở trạng thái Chase, bot đi về phía mục tiêu. Ở trạng thái Attack, bot đứng yên và đánh như trước. Ở trạng thái Retreat, bot đi cách mục tiêu 12 studs. Với mọi trạng thái khác, bot đứng yên.

Nhấn Chơi rồi đánh bot thật nhiều để bot chạy trốn bạn. 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 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ố lần đánh trúng liên tiếp, thời điểm lần đánh trúng gần nhất }

-- 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 thoáng qua phía trên người bị đánh trúng
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 trúng trong trò chơi đều đi qua đây
local function hurt(humanoid, amount)
	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)
end

-- 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, rồi mọi mục tiêu trong vùng phía trước bạn đều bị đánh trúng
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à đâm tới
	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 phía trước bạn, không bao gồm cơ thể 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 lần 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])
			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 lướt 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)
			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

-- Máy khách yêu cầu dùng một kỹ năng theo tên. Máy chủ kiểm tra xem
-- kỹ năng có tồn tại, người chơi còn sống và thời gian hồi chiêu đã hết chưa. Thời điểm kỹ năng sẵn sàng trở lại
-- là một thuộc tính (attribute) của người chơi để màn hình 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 lần hồi sinh, người chơi nhận một thanh kiếm
Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function()
		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ị hạ,
-- hình nộm sẽ xuất hiện lại sau giây 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 có thể nhìn thấy, cùng 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

-- Bot suy nghĩ: cứ mỗi phần tư giây, nó 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
	while bot.Parent and humanoid.Health > 0 do
		local target, distance = nearestCharacter(root.Position)
		-- Bài học 19: Thêm Bot States vào đây
		if target and distance <= Settings.BotRange then
			-- Đủ gần: dừng lại và tấn công, mỗi BotAttackCooldown một lần
			humanoid:MoveTo(root.Position)
			if time() - lastAttack >= Settings.BotAttackCooldown then
				lastAttack = time()
				humanoid.Animator:LoadAnimation(slash):Play()
				hurt(target.Humanoid, Settings.BotDamage)
			end
		elseif target then
			humanoid:MoveTo(target.HumanoidRootPart.Position)
		else
			humanoid:MoveTo(root.Position)
		end
		task.wait(0.25)
	end
end

-- Một bot mới xuất hiện trên bệ đỏ, 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, nhờ đó lực đẩy từ cú đập có thể làm bot di chuyển
	bot.HumanoidRootPart:SetNetworkOwner(nil)
	bot.Humanoid.HealthChanged:Connect(function(health)
		bot.HealthBar.Back.Fill.Size = UDim2.fromScale(health / bot.Humanoid.MaxHealth, 1)
	end)
	task.spawn(runBot, bot)
end

spawnBot()

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

Tự luyện tập: Playground Roblox