Roblox · Аркада · урок 5
Итог курса: арена с пушками, камера сверху, стрельба в курсор, волны врагов, очки и кнопка для телефона — структура и финальные скрипты.
Синим — что добавляем на этом уроке.
Workspace ├─ Arena (Folder) — Floor, Wall ×4, Turrets (Turret ×4), EnemySpawns ├─ Projectiles, Enemies (Folder) └─ SpawnLocation (SpawnLocation) ReplicatedStorage — атрибут Wave └─ FireEvent (RemoteEvent) ServerStorage └─ Enemy (Model) — R15-риг ServerScriptService ├─ Projectiles (ModuleScript) └─ FireServer, EnemyServer (Script) StarterPlayer ├─ StarterPlayerScripts → CameraLocal (LocalScript) └─ StarterCharacterScripts → AimLocal, FireLocal (LocalScript) StarterGui └─ ScoreGui (ScreenGui) — Info; ScoreLocal
Где: ServerScriptService · ModuleScript
Зачем: Снаряды.
-- удаление по таймеру
local Debris = game:GetService("Debris")
-- игроки
local Players = game:GetService("Players")
-- летящие снаряды
local folder = workspace:WaitForChild("Projectiles")
-- скорость снаряда
local SPEED = 90
-- секунд полёта
local LIFETIME = 2
-- таблица модуля
local Projectiles = {}
-- выстрел: откуда, куда, чей, урон, цвет, бьёт ли игроков
function Projectiles.Fire(from, target, owner, damage, color, hitsPlayers)
-- направление по полу
local flat = Vector3.new(target.X - from.X, 0, target.Z - from.Z)
-- цель прямо под ногами
if flat.Magnitude < 0.1 then return end
-- снаряд
local ball = Instance.new("Part")
-- шар
ball.Shape = Enum.PartType.Ball
-- размер
ball.Size = Vector3.new(0.9, 0.9, 0.9)
-- светится
ball.Material = Enum.Material.Neon
-- цвет: свои и чужие разные
ball.Color = color
-- пролетает, а не толкает
ball.CanCollide = false
-- старт
ball.CFrame = CFrame.new(from)
-- точка силы
local attachment = Instance.new("Attachment")
-- в снаряд
attachment.Parent = ball
-- постоянная скорость (в Алгоритмике — VectorForce, разгонялся)
local velocity = Instance.new("LinearVelocity")
-- за центр
velocity.Attachment0 = attachment
-- держит скорость и не падает
velocity.MaxForce = 100000
-- летит по полу к цели
velocity.VectorVelocity = flat.Unit * SPEED
-- в снаряд
velocity.Parent = ball
-- в мир
ball.Parent = folder
-- полёт считает сервер
ball:SetNetworkOwner(nil)
-- уже попал
local done = false
-- коснулся
ball.Touched:Connect(function(hit)
-- себя и другие снаряды не трогаем
if done or hit:IsDescendantOf(owner) or hit:IsDescendantOf(folder) then return end
-- модель
local model = hit:FindFirstAncestorOfClass("Model")
-- живое?
local humanoid = model and model:FindFirstChildOfClass("Humanoid")
-- да
if humanoid then
-- игрок или враг
local isPlayer = Players:GetPlayerFromCharacter(model) ~= nil
-- мёртвый или свой — пролетаем
if humanoid.Health <= 0 or isPlayer ~= hitsPlayers then return end
-- кто стрелял
local shooter = Players:GetPlayerFromCharacter(owner)
-- запоминаем — очки за убийство
if shooter then humanoid:SetAttribute("LastHit", shooter.UserId) end
-- урон
humanoid:TakeDamage(damage)
-- невидимые области
elseif not hit.CanCollide then
-- пролетаем
return
-- конец проверки
end
-- попал
done = true
-- снаряд исчез
ball:Destroy()
-- конец функции
end)
-- никуда не попал — исчезнет сам
Debris:AddItem(ball, LIFETIME)
-- конец функции
end
-- отдаём модуль
return ProjectilesГде: ServerScriptService · Script
Зачем: Выстрел игрока.
-- события
local RS = game:GetService("ReplicatedStorage")
-- модуль снарядов
local Projectiles = require(script.Parent:WaitForChild("Projectiles"))
-- клиент стреляет
local fireEvent = RS:WaitForChild("FireEvent")
-- чаще нельзя — защита от читов
local COOLDOWN = 0.18
-- урон снаряда игрока
local DAMAGE = 25
-- игрок → время прошлого выстрела
local last = {}
-- выстрел
fireEvent.OnServerEvent:Connect(function(player, target)
-- персонаж
local character = player.Character
-- центр
local root = character and character:FindFirstChild("HumanoidRootPart")
-- Humanoid
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
-- мусор или погиб
if typeof(target) ~= "Vector3" or not root or not humanoid or humanoid.Health <= 0 then return end
-- слишком часто
if os.clock() - (last[player] or 0) < COOLDOWN then return end
-- запоминаем
last[player] = os.clock()
-- чуть впереди персонажа
local from = root.Position + root.CFrame.LookVector * 2.5
-- голубой, бьёт только врагов
Projectiles.Fire(from, target, character, DAMAGE, Color3.fromRGB(90, 200, 255), false)
-- конец функции
end)
-- чистим память
game:GetService("Players").PlayerRemoving:Connect(function(player) last[player] = nil end)Где: ServerScriptService · Script
Зачем: Волны врагов.
-- игроки
local Players = game:GetService("Players")
-- атрибут волны
local RS = game:GetService("ReplicatedStorage")
-- образец врага
local SS = game:GetService("ServerStorage")
-- враг (R15-риг)
local sample = SS:WaitForChild("Enemy")
-- точки появления
local spawns = workspace:WaitForChild("Arena"):WaitForChild("EnemySpawns")
-- живые враги
local folder = workspace:WaitForChild("Enemies")
-- очков за врага
local REWARD = 10
-- урон при касании
local TOUCH_DAMAGE = 10
-- номер волны
local wave = 0
-- очки игрока
local function Score(player)
-- таблица справа
local stats = player:FindFirstChild("leaderstats") or Instance.new("Folder")
-- имя
stats.Name = "leaderstats"
-- в игрока
stats.Parent = player
-- очки
local score = stats:FindFirstChild("Score") or Instance.new("IntValue")
-- имя
score.Name = "Score"
-- в таблицу
score.Parent = stats
-- отдаём
return score
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Score)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do Score(player) end
-- ближайший живой игрок
local function Nearest(position)
-- пока никого
local best, bestDistance = nil, math.huge
-- все игроки
for _, player in ipairs(Players:GetPlayers()) do
-- персонаж
local character = player.Character
-- центр
local root = character and character:FindFirstChild("HumanoidRootPart")
-- Humanoid
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
-- жив и ближе
if root and humanoid and humanoid.Health > 0 and (root.Position - position).Magnitude < bestDistance then
-- запоминаем
best, bestDistance = humanoid, (root.Position - position).Magnitude
-- конец проверки
end
-- конец перебора
end
-- Humanoid цели и расстояние
return best, bestDistance
-- конец функции
end
-- один враг
local function Spawn()
-- точки
local points = spawns:GetChildren()
-- копия
local enemy = sample:Clone()
-- Humanoid
local humanoid = enemy:FindFirstChildOfClass("Humanoid")
-- центр
local root = enemy:FindFirstChild("HumanoidRootPart")
-- на случайную точку
enemy:PivotTo(points[math.random(1, #points)].CFrame + Vector3.new(0, 3, 0))
-- с каждой волной быстрее
humanoid.WalkSpeed = 10 + wave
-- в мир
enemy.Parent = folder
-- движение считает сервер
root:SetNetworkOwner(nil)
-- убит
humanoid.Died:Connect(function()
-- кто добил
local player = Players:GetPlayerByUserId(humanoid:GetAttribute("LastHit") or 0)
-- +10 очков
if player then Score(player).Value += REWARD end
-- тело исчезает
task.delay(1, function() enemy:Destroy() end)
-- конец функции
end)
-- мозг врага
task.spawn(function()
-- время прошлого удара
local lastHit = 0
-- пока жив
while enemy.Parent and humanoid.Health > 0 do
-- к кому бежать
local target, distance = Nearest(root.Position)
-- есть
if target then
-- бежит к игроку
humanoid:MoveTo(target.Parent.HumanoidRootPart.Position)
-- рядом и удар готов
if distance < 4 and os.clock() - lastHit > 1 then
-- запоминаем
lastHit = os.clock()
-- урон
target:TakeDamage(TOUCH_DAMAGE)
-- конец проверки
end
-- конец проверки
end
-- думает 3 раза в секунду
task.wait(0.3)
-- конец цикла
end
-- конец функции
end)
-- конец функции
end
-- волна за волной
while true do
-- ждём игроков
repeat task.wait(1) until #Players:GetPlayers() > 0
-- следующая волна
wave += 1
-- интерфейс покажет номер
RS:SetAttribute("Wave", wave)
-- врагов всё больше
for i = 1, 2 + wave * 2 do
-- враг
Spawn()
-- не все сразу
task.wait(0.4)
-- конец цикла
end
-- ждём, пока всех убьют
repeat task.wait(1) until #folder:GetChildren() == 0
-- передышка
task.wait(4)
-- конец цикла
endГде: Workspace → Arena → Turrets → Turret · Script
Зачем: Пушка.
-- игроки
local Players = game:GetService("Players")
-- модуль снарядов
local Projectiles = require(game:GetService("ServerScriptService"):WaitForChild("Projectiles"))
-- пушка
local turret = script.Parent
-- поворотная башня
local head = turret:WaitForChild("Head")
-- конец ствола — отсюда вылетает снаряд
local gunEnd = turret:WaitForChild("GunEnd")
-- дальность
local RANGE = 60
-- секунд между выстрелами
local RATE = 1.5
-- урон
local DAMAGE = 10
-- где ствол относительно башни
local offset = head.CFrame:ToObjectSpace(gunEnd.CFrame)
-- ближайший живой игрок
local function Nearest()
-- пока никого
local best, bestDistance = nil, RANGE
-- все игроки
for _, player in ipairs(Players:GetPlayers()) do
-- персонаж
local character = player.Character
-- центр
local root = character and character:FindFirstChild("HumanoidRootPart")
-- Humanoid
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
-- жив
if root and humanoid and humanoid.Health > 0 then
-- расстояние
local distance = (root.Position - head.Position).Magnitude
-- ближе
if distance < bestDistance then best, bestDistance = root, distance end
-- конец проверки
end
-- конец перебора
end
-- центр цели или nil
return best
-- конец функции
end
-- пока пушка стоит
while turret.Parent do
-- перезарядка
task.wait(RATE)
-- кого видим
local target = Nearest()
-- есть
if target then
-- башня поворачивается к цели
head.CFrame = CFrame.lookAt(head.Position, Vector3.new(target.Position.X, head.Position.Y, target.Position.Z))
-- ствол — вместе с ней
gunEnd.CFrame = head.CFrame * offset
-- красный снаряд, бьёт игроков
Projectiles.Fire(gunEnd.Position, target.Position, turret, DAMAGE, Color3.fromRGB(255, 80, 60), true)
-- конец проверки
end
-- конец цикла
endГде: StarterPlayer → StarterPlayerScripts · LocalScript
Зачем: Камера сверху.
-- каждый кадр
local RunService = game:GetService("RunService")
-- игроки
local Players = game:GetService("Players")
-- этот игрок
local player = Players.LocalPlayer
-- камера
local camera = workspace.CurrentCamera
-- высота камеры
local HEIGHT = 60
-- сдвиг назад — видно немного в перспективе, а «вперёд» = вверх экрана
local BACK = 25
-- после стандартной камеры
RunService:BindToRenderStep("TopCamera", Enum.RenderPriority.Camera.Value + 1, function()
-- центр персонажа
local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
-- персонажа ещё нет
if not root then return end
-- камерой управляет скрипт
camera.CameraType = Enum.CameraType.Scriptable
-- сверху смотрим на игрока
camera.CFrame = CFrame.lookAt(root.Position + Vector3.new(0, HEIGHT, BACK), root.Position)
-- конец функции
end)Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Взгляд на курсор.
-- каждый кадр
local RunService = game:GetService("RunService")
-- есть ли мышь
local UIS = game:GetService("UserInputService")
-- игроки
local Players = game:GetService("Players")
-- персонаж
local character = script.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- центр
local root = character:WaitForChild("HumanoidRootPart")
-- курсор
local mouse = Players.LocalPlayer:GetMouse()
-- курсор не цепляется за самого себя
mouse.TargetFilter = character
-- телефон без мыши
local touchOnly = UIS.TouchEnabled and not UIS.MouseEnabled
-- с мышью поворачиваем сами, на телефоне — куда идёт
humanoid.AutoRotate = touchOnly
-- каждый кадр
RunService.RenderStepped:Connect(function()
-- телефон или погиб
if touchOnly or humanoid.Health <= 0 then return end
-- точка под курсором
local hit = mouse.Hit.Position
-- смотрим на курсор, не наклоняясь
root.CFrame = CFrame.lookAt(root.Position, Vector3.new(hit.X, root.Position.Y, hit.Z))
-- конец функции
end)Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Огонь.
-- клавиши и кнопки для телефона
local CAS = game:GetService("ContextActionService")
-- есть ли мышь
local UIS = game:GetService("UserInputService")
-- события
local RS = game:GetService("ReplicatedStorage")
-- игроки
local Players = game:GetService("Players")
-- выстрел клиент → сервер
local fireEvent = RS:WaitForChild("FireEvent")
-- персонаж
local character = script.Parent
-- центр
local root = character:WaitForChild("HumanoidRootPart")
-- курсор
local mouse = Players.LocalPlayer:GetMouse()
-- секунд между выстрелами
local RATE = 0.2
-- зажата кнопка огня
local holding = false
-- куда стрелять
local function Target()
-- телефон — вперёд
if UIS.TouchEnabled and not UIS.MouseEnabled then return root.Position + root.CFrame.LookVector * 50 end
-- мышь — в курсор
return mouse.Hit.Position
-- конец функции
end
-- нажали или отпустили
local function OnFire(_, state)
-- зажата?
holding = state == Enum.UserInputState.Begin
-- пробел больше не прыжок
return Enum.ContextActionResult.Sink
-- конец функции
end
-- ЛКМ, пробел, курок геймпада; true — кнопка на телефоне
CAS:BindAction("Fire", OnFire, true, Enum.UserInputType.MouseButton1, Enum.KeyCode.Space, Enum.KeyCode.ButtonR2)
-- надпись на кнопке телефона
CAS:SetTitle("Fire", "Огонь")
-- справа внизу, под большой палец
CAS:SetPosition("Fire", UDim2.new(1, -140, 1, -150))
-- сама кнопка (на ПК — nil)
local button = CAS:GetButton("Fire")
-- крупнее
if button then button.Size = UDim2.fromOffset(90, 90) end
-- погиб — кнопка пропадает
character:WaitForChild("Humanoid").Died:Connect(function() CAS:UnbindAction("Fire") end)
-- пока персонаж жив
while character.Parent do
-- зажато — стреляем очередью
if holding then fireEvent:FireServer(Target()) end
-- пауза
task.wait(RATE)
-- конец цикла
endГде: StarterGui → ScoreGui · LocalScript
Зачем: Волна и очки.
-- игроки
local Players = game:GetService("Players")
-- номер волны
local RS = game:GetService("ReplicatedStorage")
-- этот игрок
local player = Players.LocalPlayer
-- надпись вверху
local label = script.Parent:WaitForChild("Info")
-- постоянно
while true do
-- таблица
local stats = player:FindFirstChild("leaderstats")
-- очки
local score = stats and stats:FindFirstChild("Score")
-- текст
label.Text = "Волна " .. (RS:GetAttribute("Wave") or 0) .. " · Очки " .. (score and score.Value or 0)
-- 4 раза в секунду
task.wait(0.25)
-- конец цикла
endЗапусти Play (или Run) и убедись: