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

Шаги

  1. Сверяй сверху вниз: сначала Explorer, потом каждый скрипт целиком.
  2. Снаряды делает один модуль Projectiles — и для игроков, и для пушек; кто кого бьёт, решает hitsPlayers.
  3. Очки — leaderstats → Score; кто добил врага, хранится в атрибуте LastHit его Humanoid.
  4. Сложность меняется в одном месте: EnemyServer (врагов в волне, скорость), TurretServer (RANGE, RATE, DAMAGE).
  5. Свой корабль вместо человечка: StarterCharacter в StarterPlayer (Humanoid, HumanoidRootPart, Head).

Скрипты

Projectiles

Где: 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

FireServer

Где: 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)

EnemyServer

Где: 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

TurretServer

Где: 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

CameraLocal

Где: 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)

AimLocal

Где: 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)

FireLocal

Где: 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

ScoreLocal

Где: 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) и убедись:

Собрать всю игру одной командой