연속 처치
Coddy의 Roblox 게임: 배틀그라운드 코스 레슨. 20개 중 19번째.
죽지 않고 연속으로 처치한 횟수를 연속 처치라고 합니다. 서버는 이를 플레이어의 Streak 속성으로 유지합니다. 처치할 때마다 1씩 증가하고, 캐릭터가 생성되면 다시 0이 됩니다.
클라이언트는 속성의 변경을 감지하고, 3회 이상부터 2초 동안 HUD의 Message를 표시합니다.
me:GetAttributeChangedSignal("Streak"):Connect(function()
local streak = me:GetAttribute("Streak") or 0
if streak >= 3 then
hud.Message.Text = streak .. " kill streak!"
end
end)챌린지
어려움BattleServer에서 플레이어가CharacterAdded될 때마다Streak을 0으로 설정하고, 킬을 올리면 1을 더하세요.BattleClient에서Streak이 3 이상으로 변경되면3 kill streak!을hud.Message에 표시하고, 더 새로운 연속 처치 메시지로 대체되지 않았다면 2초 후 숨기세요.
Play를 누르고 봇 세 명을 처치하세요. 그런 다음 Check를 누르세요.
직접 해보기
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 -> 마지막으로 휘두른 시각
local combo = {} -- player -> { 연속 적중 횟수, 마지막으로 적중한 시각 }
local lastHit = {} -- humanoid -> 마지막으로 공격한 플레이어
-- 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
-- 게임의 모든 공격은 이 함수를 거칩니다 (attacker: 공격한 플레이어 또는 nil)
local function hurt(humanoid, amount, attacker)
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)
if attacker then
lastHit[humanoid] = attacker
end
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], player)
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, 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
-- 클라이언트가 이름으로 능력 사용을 요청한다. 서버는 실제로 존재하는
-- 능력인지, 플레이어가 살아 있는지, 재사용 대기시간이 끝났는지 확인한다. 다시 사용할 수 있는 시각은
-- 플레이어의 속성이므로 화면에 표시할 수 있다.
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
-- 캐릭터를 향해 이동한다: 길이 막히지 않았다면 곧장 다가가고, 막혔다면
-- 장애물을 우회하는 경로의 다음 지점으로 이동한다
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
-- 봇의 판단: 0.25초마다 주변을 살피고 행동을 결정한다
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
-- 빨간 패드에 새 bot을 생성하고, 능력치와 체력 바를 설정합니다
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이 움직이도록 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()
-- 마지막으로 공격한 플레이어가 처치 기록을 얻습니다
local killer = lastHit[bot.Humanoid]
lastHit[bot.Humanoid] = nil
if killer and killer.Parent then
killer.leaderstats.Kills.Value += 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
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.