Roblox · Аркада · урок 2

Атаки: снаряды и пушки

ЛКМ или пробел — очередь светящихся снарядов в курсор. По арене стоят пушки: поворачиваются к ближайшему игроку и стреляют красными снарядами.

Структура

Синим — что добавляем на этом уроке.

Workspace
├─ Projectiles (Folder) — летящие снаряды
└─ Arena → Turrets (Folder)
   └─ Turret (Model) — Base, Head, GunEnd; TurretServer
ReplicatedStorage
└─ FireEvent (RemoteEvent)
ServerScriptService
├─ Projectiles (ModuleScript)
└─ FireServer (Script)
StarterPlayer → StarterCharacterScripts
└─ FireLocal (LocalScript)

Шаги

  1. Модуль Projectiles.Fire создаёт снаряд: неоновый шар с LinearVelocity — летит с постоянной скоростью по полу. Его используют и игроки, и пушки.
  2. Последний аргумент hitsPlayers: снаряды игроков бьют только врагов, снаряды пушек — только игроков.
  3. FireLocal: ContextActionService привязывает огонь к ЛКМ, пробелу и курку геймпада. Пока зажато — FireServer каждые 0.2 с. Сервер всё равно не даст стрелять чаще COOLDOWN.
  4. Пушка: модель «Turret» — основание «Base», поворотная башня «Head» и деталь «GunEnd» на конце ствола (оттуда вылетает снаряд). Пушки из Toolbox подойдут — переименуй детали. Все пушки — в Arena → «Turrets».
  5. TurretServer поворачивает башню к ближайшему игроку в радиусе 60 и стреляет раз в 1.5 с.
  6. Добавь атмосферы: тёмное время суток (Lighting → ClockTime 0), неоновые стены, PointLight на пушках.

Скрипты

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)

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)
-- погиб — кнопка пропадает
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

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

Проверка

Запусти Play (или Run) и убедись: