봇 상태
Coddy의 Roblox 게임: 배틀그라운드 코스 레슨. 20개 중 16번째.
봇의 판단이 복잡해지고 있습니다. 게임 AI는 보통 상태로 작성됩니다. 매 틱마다 봇이 어떤 상태인지 결정한 다음, 그 상태에 해당하는 동작을 합니다. 이 봇에는 다섯 가지 상태가 있습니다.
Stunned: 콤보 공격으로 봇이 얼어붙었습니다. 아무것도 하지 않습니다.Retreat: 체력이Settings.RetreatAt미만이면 당신에게서 도망칩니다.Idle: 시야에 아무도 없습니다.Attack: 당신이 공격 범위 안에 있습니다.Chase: 당신이 시야에 있습니다.
검사 순서는 봇의 우선순위입니다. 상태는 봇의 속성인 State에 저장되므로, 봇이 어떤 판단을 하고 있는지 확인할 수 있습니다.
챌린지
어려움runBot의 루프 안을 상태들로 다시 작성하세요. 위에 나온 순서대로 state를 결정하고 봇의 State 속성을 설정한 다음, Chase 상태에서는 대상에게 걸어가고, Attack 상태에서는 멈춰서 공격하며(이전과 같이), Retreat 상태에서는 대상에게서 곧장 12 studs 떨어진 곳으로 걸어가고, 그 외의 상태에서는 가만히 서 있도록 하세요.
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 bots = workspace.Bots
local lastSwing = {} -- player -> 마지막으로 검을 휘두른 시각
local combo = {} -- player -> { 연속 적중 횟수, 마지막으로 적중한 시각 }
-- 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")
-- 맞은 대상 위에 잠시 떠 있는 숫자
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
-- 게임의 모든 타격은 여기를 거칩니다
local function hurt(humanoid, amount)
if humanoid.Health <= 0 then
return
end
local character = humanoid.Parent
-- 방어하면 피해를 대부분 막습니다
if character:GetAttribute("Blocking") then
amount *= Settings.BlockMultiplier
end
humanoid:TakeDamage(amount)
damageNumber(character, amount)
end
-- 기절시키면 대상을 잠시 움직이지 못하게 합니다
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
-- 검 휘두르기: 애니메이션이 재생된 뒤 앞쪽 상자 범위 안의 모든 대상이 맞습니다
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
-- 검을 세 번 연속 휘두르면 콤보가 됩니다. 세 번째는 찌르기 공격입니다
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()
-- 앞쪽 상자 범위이며, 자신의 몸은 포함하지 않습니다
local params = OverlapParams.new()
params.FilterDescendantsInstances = { character }
local box = root.CFrame * CFrame.new(0, 0, -3)
local hit = {} -- 검을 한 번 휘두를 때 각 humanoid를 한 번씩만 공격합니다
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
-- 이름별 능력: 각 능력이 하는 일
local abilities = {}
function abilities.Dash(player, character, root)
root.AssemblyLinearVelocity = root.CFrame.LookVector * Settings.DashSpeed + Vector3.new(0, 15, 0)
end
-- 주변에 있는 모두가 공격을 받고 당신에게서 멀리 튕겨 나갑니다
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
-- 클라이언트가 이름으로 능력 사용을 요청합니다. 서버는 유효한
-- 능력인지, 플레이어가 살아 있는지, 재사용 대기시간이 끝났는지 확인합니다. 다시 사용할 수 있는 시각은
-- 플레이어의 속성에 저장되므로 화면에 표시할 수 있습니다.
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)
-- 모든 플레이어는 리더보드에 Kills를 받고, 부활할 때마다 검을 받습니다
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)
-- 훈련용 더미: 체력 막대는 체력에 따라 변하고, 쓰러진
-- 더미는 잠시 후 같은 자리에 새 복제본으로 돌아옵니다
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)
-- 봇이 볼 수 있는 가장 가까운 살아 있는 플레이어의 캐릭터와 그 캐릭터까지의 거리
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
-- 봇의 판단: 0.25초마다 주변을 살피고 행동을 결정합니다
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)
if target and distance <= Settings.BotRange then
-- 충분히 가까움: 멈춰서 공격합니다. 공격 간격은 BotAttackCooldown입니다
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
-- 빨간 발판에 새 봇을 생성하고, 능력치와 체력 막대를 설정합니다
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
-- slam의 밀쳐내기 효과로 봇이 움직이도록 봇의 물리 연산을 서버에서 처리합니다
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()
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.