Un bot que persigue
Lección 14 de 20 del curso Juego de Roblox: Combates en la arena de Coddy.
Ahora, un oponente. Un bot es un modelo en ServerStorage.Bot con un Humanoid, clonado sobre la plataforma roja. Su mente es un bucle en su propio hilo: cada cuarto de segundo, busca al jugador vivo más cercano al que pueda ver (dentro de Settings.BotSight) y camina hacia él con Humanoid:MoveTo.
while bot.Parent and humanoid.Health > 0 do
local target = nearestCharacter(root.Position)
if target then
humanoid:MoveTo(target.HumanoidRootPart.Position)
end
task.wait(0.25)
endEncontrar al más cercano es el bucle clásico: empieza sin nadie y con el alcance de visión como la mejor distancia, y conserva a cualquiera que esté más cerca.
Roblox permite que el cliente de un jugador cercano ejecute la física de un modelo sin anclar, y entonces una llamada del servidor como ApplyImpulse no hace nada. Tu golpe hace retroceder a los bots desde el servidor, así que mantén allí la física de cada bot: bot.HumanoidRootPart:SetNetworkOwner(nil), una vez que el bot esté en el espacio de trabajo.
Desafío
Difícil- Añade
local bots = workspace.Bots. - Escribe
nearestCharacter(position): el personaje del jugador vivo más cercano dentro deSettings.BotSighty su distancia (o nil). - Escribe
runBot(bot): mientras el bot siga con vida, camina hacia el personaje más cercano o quédate quieto, y espera 0.25 segundos. - Escribe
spawnBot(): clona el Bot 3 studs por encima deworkspace.BotSpawn, establece sus MaxHealth, Health y WalkSpeed a partir de Settings, asígnalo como hijo debots, mantén su física en el servidor, haz que su barra de salud refleje su salud e iniciarunBot. Genera uno.
Pulsa Play y camina hacia la plataforma roja. Después, Check.
Pruébalo tú mismo
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 lastSwing = {} -- player -> cuándo dio su último espadazo
local combo = {} -- player -> { golpes consecutivos, cuándo acertó el último }
-- Las animaciones de espada propias de 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 número que flota un momento sobre quien recibió el golpe
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
-- Todos los golpes del juego pasan por aquí
local function hurt(humanoid, amount)
if humanoid.Health <= 0 then
return
end
local character = humanoid.Parent
-- un bloqueo absorbe la mayor parte del golpe
if character:GetAttribute("Blocking") then
amount *= Settings.BlockMultiplier
end
humanoid:TakeDamage(amount)
damageNumber(character, amount)
end
-- Un aturdimiento inmoviliza a alguien por un momento
local function stun(humanoid)
local character = humanoid.Parent
if character:GetAttribute("Stunned") then
return
end
character:SetAttribute("Stunned", true)
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 espadazo: primero la animación y luego reciben un golpe todos los que están en la caja frente a ti
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
-- tres espadazos seguidos forman un combo; el tercero es una estocada
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()
-- la caja frente a ti, nunca tu propio cuerpo
local params = OverlapParams.new()
params.FilterDescendantsInstances = { character }
local box = root.CFrame * CFrame.new(0, 0, -3)
local hit = {} -- cada humanoid, una sola vez por espadazo
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
-- Las habilidades, por nombre: qué hace cada una
local abilities = {}
function abilities.Dash(player, character, root)
root.AssemblyLinearVelocity = root.CFrame.LookVector * Settings.DashSpeed + Vector3.new(0, 15, 0)
end
-- Todos los que te rodean reciben un golpe y salen despedidos lejos de ti
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
-- El cliente solicita una habilidad por su nombre. El servidor comprueba que sea una
-- habilidad real, que el jugador esté vivo y que haya terminado su tiempo de espera. El momento en que vuelve a estar disponible
-- se guarda como atributo del jugador, para que pueda mostrarse en su pantalla.
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)
-- Cada jugador recibe Kills en la tabla de clasificación y una espada en cada vida
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.CharacterAdded:Connect(function()
local sword = ServerStorage.Sword:Clone()
sword.Parent = player.Backpack
sword.Activated:Connect(function()
swing(player)
end)
end)
end)
-- El muñeco de entrenamiento: su barra de salud refleja su salud y, cuando es derrotado,
-- el muñeco reaparece al cabo de un momento como una copia nueva en el mismo lugar
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)
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.