Menu
CoddyTech

Altri bot

Lezione 21 di 25 del corso Gioco Roblox: Battlegrounds di Coddy.

Un bot è un duello. L’arena ne riceve Settings.Bots e un bot sconfitto ritorna.

Quando il bot Died, resta per un secondo (così un colpo o uno schianto che lo attraversa può terminare), se ne va e, dopo Settings.BotRespawn secondi, un nuovo bot prende il suo posto. spawnBot può richiamare sé stessa per farlo, come faceva setupDummy:

for _ = 1, 3 do
	spawnBot()
end
challenge icon

Sfida

Difficile

Riempi l'arena e fai tornare i bot sconfitti.

  1. In spawnBot, alla Died del bot: attendi 1 secondo, distruggi il bot, attendi Settings.BotRespawn e chiama spawnBot().
  2. Invece di una sola chiamata a spawnBot(), generane Settings.Bots con un ciclo for.

Premi «Gioca»: due bot ti attaccano e uno sconfitto torna 4 secondi dopo. Poi premi «Verifica».

Provalo tu

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 -> quando ha sferrato l'ultimo colpo
local combo = {} -- player -> { colpi consecutivi, quando è andato a segno l'ultimo }

-- Le animazioni della spada di 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")

-- Un numero che appare per un momento sopra chi è stato colpito
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

-- Ogni colpo nel gioco passa da qui
local function hurt(humanoid, amount)
	if humanoid.Health <= 0 then
		return
	end
	local character = humanoid.Parent
	-- una parata assorbe gran parte del colpo
	if character:GetAttribute("Blocking") then
		amount *= Settings.BlockMultiplier
	end
	humanoid:TakeDamage(amount)
	damageNumber(character, amount)
end

-- Uno stordimento immobilizza qualcuno per un momento
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

-- Un fendente: parte l'animazione, poi vengono colpiti tutti quelli nell'area davanti a te
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
	-- tre fendenti consecutivi formano una combo; il terzo è un affondo
	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()
	-- l'area davanti a te, mai il tuo stesso corpo
	local params = OverlapParams.new()
	params.FilterDescendantsInstances = { character }
	local box = root.CFrame * CFrame.new(0, 0, -3)
	local hit = {} -- ogni humanoid una sola volta per fendente
	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

-- Le abilità, per nome: cosa fa ciascuna
local abilities = {}

-- Il dispositivo del giocatore muove il suo personaggio, quindi lo scatto avviene lì
function abilities.Dash(player)
	remotes.Ability:FireClient(player, "Dash")
end

-- tutti quelli intorno a te vengono colpiti e scagliati lontano da te
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

-- Il client richiede un'abilità per nome. Il server verifica che sia una vera
-- abilità, che il giocatore sia vivo e che il tempo di ricarica sia terminato. Il momento in cui sarà di nuovo pronta
-- è un attributo del giocatore, così può essere mostrato sul suo schermo.
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)

-- Ogni giocatore riceve una spada a ogni nuova vita
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)

-- Il manichino da allenamento: la sua barra della salute segue la sua salute e, quando viene sconfitto,
-- il manichino ricompare dopo un momento come una nuova copia nello stesso punto
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)

-- Il personaggio del giocatore vivo più vicino che un bot riesce a vedere, e la distanza a cui si trova
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

-- Cammina verso un personaggio: direttamente verso di lui se non ci sono ostacoli, oppure
-- verso il punto successivo di un percorso che aggira gli ostacoli
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

-- La mente di un bot: ogni quarto di secondo si guarda intorno e decide
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

-- Un nuovo bot sulla piattaforma rossa, con le sue statistiche e una barra della salute
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
	-- Mantieni la fisica del bot sul server, così il contraccolpo dello schianto lo sposta
	bot.HumanoidRootPart:SetNetworkOwner(nil)
	bot.Humanoid.HealthChanged:Connect(function(health)
		bot.HealthBar.Back.Fill.Size = UDim2.fromScale(health / bot.Humanoid.MaxHealth, 1)
	end)
	-- Lezione 21: qui va More Bots
	task.spawn(runBot, bot)
end

spawnBot()

Tutte le lezioni di Gioco Roblox: Battlegrounds

Esercitati da solo: Playground Roblox