Roblox · Шутер · урок 9

Общий: вся игра целиком

Итог курса на одной странице: полная структура шутера и финальные версии всех скриптов — пистолет, автомат, снайперка, граната и вид от первого лица, без «замени блок». Удобно сверить свою игру или собрать её заново.

Структура

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

Workspace
├─ SpawnLocation (SpawnLocation) — на стартовой площадке
├─ Locations (Folder)
│  ├─ Start (Model) — безопасная зона
│  ├─ Battle1 (Model) — укрытия
│  ├─ Cache1 (Model) — ящики с оружием
│  └─ Final (Model) — арена с боссом
├─ Signs (Folder) — таблички-указатели
├─ Enemies (Folder) — монстры с Humanoid
├─ Rifle (Tool) — автомат — в тайнике
│  ├─ Handle (Part) — модель; Anchored выкл., Massless вкл.
│  │  └─ Shoot, Empty, Reload (Sound) — выстрел, пустой магазин, перезарядка
│  ├─ Ammo, MaxAmmo (NumberValue) — патроны сейчас и вместимость
│  ├─ Shot (RemoteEvent) — клиент → сервер: выстрел
│  ├─ GunGui (ScreenGui) — счётчик патронов
│  │  └─ Frame (Frame)
│  │     └─ TextLabel (TextLabel) — «30 / 30»
│  ├─ GunLocal (LocalScript)
│  │  └─ Animation (Animation) — перезарядка
│  └─ GunServer (Script)
├─ Sniper (Tool) — снайперка — то же устройство, что у Rifle
└─ Grenade (Tool) — в тайнике
   ├─ Handle (Part)
   │  ├─ Throw, Boom (Sound) — бросок и взрыв
   │  ├─ Attachment (Attachment)
   │  └─ VectorForce (VectorForce) — Attachment0 = Attachment, Force 0
   ├─ Animation (Animation) — бросок, кадр «Release»
   ├─ Throw (RemoteEvent) — клиент → сервер
   ├─ GrenadeLocal (LocalScript)
   └─ GrenadeServer (Script)
StarterPack — выдаётся каждому игроку
└─ Gun (Tool) — пистолет — с самого начала
   ├─ Handle (Part) — модель; Anchored выкл., Massless вкл.
   │  └─ Shoot, Empty, Reload (Sound) — выстрел, пустой магазин, перезарядка
   ├─ Ammo, MaxAmmo (NumberValue) — патроны сейчас и вместимость
   ├─ Shot (RemoteEvent) — клиент → сервер: выстрел
   ├─ GunGui (ScreenGui) — счётчик патронов
   │  └─ Frame (Frame)
   │     └─ TextLabel (TextLabel) — «30 / 30»
   ├─ GunLocal (LocalScript)
   │  └─ Animation (Animation) — перезарядка
   └─ GunServer (Script)
StarterPlayer
├─ StarterPlayerScripts
│  └─ PlayerController (LocalScript)
└─ StarterCharacterScripts
   └─ CharacterController (LocalScript)

Шаги

  1. Сверяй сверху вниз: сначала Explorer со «Структурой», потом каждый скрипт целиком.
  2. Все три ствола устроены одинаково, отличаются только GunLocal (как стреляет) и числа в GunServer (damage, bulletSpeed) и в Ammo/MaxAmmo.
  3. Имена важны: Handle, Shot, Shoot, Empty, Reload, Ammo, MaxAmmo, GunGui → Frame → TextLabel, у гранаты — Throw, Boom, VectorForce, кадр Release. Регистр букв тоже.
  4. Пистолет — в StarterPack: он у игрока с самого начала и после каждой смерти. Остальное оружие лежит на карте, его подбирают, подойдя вплотную.
  5. Оружие на карте должно лежать, а не падать: у Handle CanCollide вкл.; у оружия в StarterPack — выкл.
  6. Анимации перезарядки и броска — свои (Animation Editor, приоритет Action): без них оружие тоже работает, граната летит без замаха.

Скрипты

GunLocal (пистолет)

Где: StarterPack → Gun · LocalScript
Зачем: Выстрел по клику, патроны, перезарядка по R, счётчик, прицел на ПКМ.

-- оружие (Tool)
local gun = script.Parent
-- сигнал «выстрел» на сервер
local shotEvent = gun:WaitForChild("Shot")
-- этот игрок
local player = game.Players.LocalPlayer
-- его мышь: mouse.Hit — точка под курсором
local mouse = player:GetMouse()
-- оружие в руках?
local equipped = false
-- нажатия клавиш
local UIS = game:GetService("UserInputService")
-- модель оружия
local handle = gun:WaitForChild("Handle")
-- щелчок пустого магазина
local emptySound = handle:WaitForChild("Empty")
-- звук перезарядки
local reloadSound = handle:WaitForChild("Reload")
-- патроны в магазине сейчас
local ammo = gun:WaitForChild("Ammo")
-- вместимость магазина
local maxAmmo = gun:WaitForChild("MaxAmmo")
-- анимация перезарядки
local reloadAnimation = script:WaitForChild("Animation")
-- загрузим, когда возьмём оружие
local animTrack = nil
-- идёт перезарядка?
local reloading = false
-- счётчик патронов
local gunGui = gun:WaitForChild("GunGui")
-- надпись «30 / 30»
local ammoText = gunGui.Frame.TextLabel
-- камера — для прицела
local camera = workspace.CurrentCamera

-- обновляет счётчик
local function UpdateAmmo()
    -- «патроны / вместимость»
    ammoText.Text = ammo.Value .. " / " .. maxAmmo.Value
-- конец функции
end
-- патроны изменились — обновляем
ammo.Changed:Connect(UpdateAmmo)

-- события каждого кадра
local RunService = game:GetService("RunService")
-- показывать оружие в виде от первого лица
local SHOW_GUN = true
-- сейчас целимся?
local aiming = false
-- режим камеры до прицеливания
local normalMode = nil
-- минимальное приближение до прицеливания
local normalMinZoom = nil
-- подписка «оружие видно»
local showConnection = nil

-- включить или выключить вид от первого лица
local function FirstPerson(on)
    -- уже так — ничего не делаем
    if aiming == on then return end
    -- запоминаем
    aiming = on
    -- начали целиться
    if on then
        -- запоминаем режим камеры
        normalMode = player.CameraMode
        -- и минимальное приближение
        normalMinZoom = player.CameraMinZoomDistance
        -- камера в голове персонажа
        player.CameraMode = Enum.CameraMode.LockFirstPerson
        -- оружие должно быть видно
        if SHOW_GUN then
            -- каждый кадр
            showConnection = RunService.RenderStepped:Connect(function()
                -- все детали оружия
                for _, part in ipairs(gun:GetDescendants()) do
                    -- Roblox прячет их от первого лица — возвращаем
                    if part:IsA("BasePart") then part.LocalTransparencyModifier = 0 end
                -- конец перебора
                end
            -- конец обработчика
            end)
        -- конец проверки
        end
    -- перестали целиться
    else
        -- оружие снова прячется как обычно
        if showConnection then showConnection:Disconnect() end
        -- подписки нет
        showConnection = nil
        -- режим камеры, который был
        player.CameraMode = normalMode or Enum.CameraMode.Classic
        -- камера обычная
        if player.CameraMode == Enum.CameraMode.Classic then
            -- отодвигаем камеру назад — иначе останется в голове
            player.CameraMinZoomDistance = 10
            -- один кадр
            task.wait()
            -- возвращаем приближение, как было
            player.CameraMinZoomDistance = normalMinZoom or 0.5
        -- конец проверки
        end
    -- конец проверки
    end
-- конец функции
end

-- взяли оружие в руки
local function OnEquipped()
    -- запоминаем
    equipped = true
    -- в руках — значит, родитель оружия персонаж
    local humanoid = gun.Parent:FindFirstChild("Humanoid")
    -- анимация задана
    if reloadAnimation.AnimationId ~= "" then
        -- готовим анимацию перезарядки
        animTrack = humanoid:LoadAnimation(reloadAnimation)
    -- конец проверки
    end
    -- показываем счётчик на экране
    gunGui.Parent = player.PlayerGui
    -- с актуальным числом
    UpdateAmmo()
    -- курсор-перекрестье
    mouse.Icon = "rbxassetid://6884107931"
-- конец функции
end
-- каждый раз, когда берут в руки
gun.Equipped:Connect(OnEquipped)
-- подобрали с земли: скрипт запустился, когда оружие уже в руках, — Equipped был раньше
if gun.Parent and gun.Parent:FindFirstChildOfClass("Humanoid") then OnEquipped() end

-- убрали оружие
gun.Unequipped:Connect(function()
    -- запоминаем
    equipped = false
    -- прячем счётчик обратно в оружие
    gunGui.Parent = gun
    -- обычный курсор
    mouse.Icon = ""
    -- выходим из прицела
    camera.FieldOfView = 70
    -- и из вида от первого лица
    FirstPerson(false)
-- конец обработчика
end)

-- клик, пока оружие в руках
gun.Activated:Connect(function()
    -- во время перезарядки не стреляем
    if reloading then return end
    -- патроны есть
    if ammo.Value > 0 then
        -- выстрел
        shotEvent:FireServer(mouse.Hit.Position)
        -- минус патрон
        ammo.Value -= 1
    -- магазин пуст
    else
        -- щелчок
        emptySound:Play()
    -- конец проверки
    end
-- конец обработчика
end)

-- нажата клавиша
UIS.InputBegan:Connect(function(input, gp)
    -- печатает в чат / оружие не в руках / уже перезаряжаемся
    if gp or not equipped or reloading then return end
    -- R, и магазин не полный
    if input.KeyCode == Enum.KeyCode.R and ammo.Value < maxAmmo.Value then
        -- началась перезарядка
        reloading = true
        -- звук
        reloadSound:Play()
        -- анимация, если задана
        if animTrack then animTrack:Play() end
        -- ждём длину звука (минимум 1 с — даже если звук не загрузился)
        task.wait(math.max(reloadSound.TimeLength, 1))
        -- полный магазин
        ammo.Value = maxAmmo.Value
        -- перезарядка закончилась
        reloading = false
    -- конец проверки
    end
-- конец обработчика
end)

-- нажата ПКМ
mouse.Button2Down:Connect(function()
    -- приближаем камеру — прицел
    if equipped then camera.FieldOfView = 30 end
    -- смотрим от первого лица
    if equipped then FirstPerson(true) end
-- конец обработчика
end)
-- ПКМ отпущена
mouse.Button2Up:Connect(function()
    -- обычный обзор
    camera.FieldOfView = 70
    -- камера снова за спиной
    FirstPerson(false)
-- конец обработчика
end)

GunServer (пистолет и автомат)

Где: StarterPack → Gun, Workspace → Rifle · Script
Зачем: Пуля к точке под курсором, урон 15 один раз.

-- оружие (Tool)
local gun = script.Parent
-- модель оружия
local handle = gun:WaitForChild("Handle")
-- сигнал «выстрел» от клиента
local shotEvent = gun:WaitForChild("Shot")
-- звук выстрела
local shootSound = handle:WaitForChild("Shoot")
-- удаление по таймеру
local Debris = game:GetService("Debris")
-- скорость пули
local bulletSpeed = 500
-- урон за попадание
local damage = 15

-- клиент сообщил о выстреле и точке под курсором
shotEvent.OnServerEvent:Connect(function(player, mousePosition)
    -- стреляет только тот, у кого оружие в руках
    if gun.Parent ~= player.Character then return end
    -- новая пуля
    local bullet = Instance.new("Part")
    -- шарик
    bullet.Shape = Enum.PartType.Ball
    -- маленький
    bullet.Size = Vector3.new(0.3, 0.3, 0.3)
    -- чёрный
    bullet.BrickColor = BrickColor.new("Black")
    -- не отскакивает от своего же оружия
    bullet.CanCollide = false
    -- у ствола, смотрит на курсор
    bullet.CFrame = CFrame.new(handle.Position, mousePosition)
    -- в мир
    bullet.Parent = workspace
    -- физику пули считает сервер — попадания честные
    bullet:SetNetworkOwner(nil)
    -- летит вперёд
    bullet.AssemblyLinearVelocity = bullet.CFrame.LookVector * bulletSpeed
    -- звук выстрела
    shootSound:Play()
    -- пуля ещё никого не задела
    local hit = false
    -- пуля чего-то коснулась
    bullet.Touched:Connect(function(otherPart)
        -- уже попала или задела стрелка — пропускаем
        if hit or otherPart:IsDescendantOf(player.Character) then return end
        -- запоминаем попадание
        hit = true
        -- модель, которой принадлежит деталь
        local model = otherPart:FindFirstAncestorOfClass("Model")
        -- есть ли у неё Humanoid (живой ли)
        local humanoid = model and model:FindFirstChildOfClass("Humanoid")
        -- попали в живого
        if humanoid then
            -- урон
            humanoid:TakeDamage(damage)
        -- конец проверки
        end
        -- пуля исчезает при любом попадании
        bullet:Destroy()
    -- конец обработчика касания
    end)
    -- промах — пуля удалится через 10 секунд
    Debris:AddItem(bullet, 10)
-- конец обработчика
end)

GunLocal (автомат)

Где: Workspace → Rifle · LocalScript
Зачем: То же, но очередь, пока кнопка зажата.

-- оружие (Tool)
local gun = script.Parent
-- сигнал «выстрел» на сервер
local shotEvent = gun:WaitForChild("Shot")
-- этот игрок
local player = game.Players.LocalPlayer
-- его мышь: mouse.Hit — точка под курсором
local mouse = player:GetMouse()
-- оружие в руках?
local equipped = false
-- нажатия клавиш
local UIS = game:GetService("UserInputService")
-- модель оружия
local handle = gun:WaitForChild("Handle")
-- щелчок пустого магазина
local emptySound = handle:WaitForChild("Empty")
-- звук перезарядки
local reloadSound = handle:WaitForChild("Reload")
-- патроны в магазине сейчас
local ammo = gun:WaitForChild("Ammo")
-- вместимость магазина
local maxAmmo = gun:WaitForChild("MaxAmmo")
-- анимация перезарядки
local reloadAnimation = script:WaitForChild("Animation")
-- загрузим, когда возьмём оружие
local animTrack = nil
-- идёт перезарядка?
local reloading = false
-- счётчик патронов
local gunGui = gun:WaitForChild("GunGui")
-- надпись «30 / 30»
local ammoText = gunGui.Frame.TextLabel
-- камера — для прицела
local camera = workspace.CurrentCamera
-- пауза между выстрелами очереди
local fireRate = 0.1
-- кнопка стрельбы зажата?
local shooting = false
-- очередь уже идёт?
local looping = false

-- обновляет счётчик
local function UpdateAmmo()
    -- «патроны / вместимость»
    ammoText.Text = ammo.Value .. " / " .. maxAmmo.Value
-- конец функции
end
-- патроны изменились — обновляем
ammo.Changed:Connect(UpdateAmmo)

-- события каждого кадра
local RunService = game:GetService("RunService")
-- показывать оружие в виде от первого лица
local SHOW_GUN = true
-- сейчас целимся?
local aiming = false
-- режим камеры до прицеливания
local normalMode = nil
-- минимальное приближение до прицеливания
local normalMinZoom = nil
-- подписка «оружие видно»
local showConnection = nil

-- включить или выключить вид от первого лица
local function FirstPerson(on)
    -- уже так — ничего не делаем
    if aiming == on then return end
    -- запоминаем
    aiming = on
    -- начали целиться
    if on then
        -- запоминаем режим камеры
        normalMode = player.CameraMode
        -- и минимальное приближение
        normalMinZoom = player.CameraMinZoomDistance
        -- камера в голове персонажа
        player.CameraMode = Enum.CameraMode.LockFirstPerson
        -- оружие должно быть видно
        if SHOW_GUN then
            -- каждый кадр
            showConnection = RunService.RenderStepped:Connect(function()
                -- все детали оружия
                for _, part in ipairs(gun:GetDescendants()) do
                    -- Roblox прячет их от первого лица — возвращаем
                    if part:IsA("BasePart") then part.LocalTransparencyModifier = 0 end
                -- конец перебора
                end
            -- конец обработчика
            end)
        -- конец проверки
        end
    -- перестали целиться
    else
        -- оружие снова прячется как обычно
        if showConnection then showConnection:Disconnect() end
        -- подписки нет
        showConnection = nil
        -- режим камеры, который был
        player.CameraMode = normalMode or Enum.CameraMode.Classic
        -- камера обычная
        if player.CameraMode == Enum.CameraMode.Classic then
            -- отодвигаем камеру назад — иначе останется в голове
            player.CameraMinZoomDistance = 10
            -- один кадр
            task.wait()
            -- возвращаем приближение, как было
            player.CameraMinZoomDistance = normalMinZoom or 0.5
        -- конец проверки
        end
    -- конец проверки
    end
-- конец функции
end

-- взяли оружие в руки
local function OnEquipped()
    -- запоминаем
    equipped = true
    -- в руках — значит, родитель оружия персонаж
    local humanoid = gun.Parent:FindFirstChild("Humanoid")
    -- анимация задана
    if reloadAnimation.AnimationId ~= "" then
        -- готовим анимацию перезарядки
        animTrack = humanoid:LoadAnimation(reloadAnimation)
    -- конец проверки
    end
    -- показываем счётчик на экране
    gunGui.Parent = player.PlayerGui
    -- с актуальным числом
    UpdateAmmo()
    -- курсор-перекрестье
    mouse.Icon = "rbxassetid://6884107931"
-- конец функции
end
-- каждый раз, когда берут в руки
gun.Equipped:Connect(OnEquipped)
-- подобрали с земли: скрипт запустился, когда оружие уже в руках, — Equipped был раньше
if gun.Parent and gun.Parent:FindFirstChildOfClass("Humanoid") then OnEquipped() end

-- убрали оружие
gun.Unequipped:Connect(function()
    -- запоминаем
    equipped = false
    -- прячем счётчик обратно в оружие
    gunGui.Parent = gun
    -- обычный курсор
    mouse.Icon = ""
    -- выходим из прицела
    camera.FieldOfView = 70
    -- и из вида от первого лица
    FirstPerson(false)
    -- очередь прекращается
    shooting = false
-- конец обработчика
end)

-- кнопку стрельбы нажали
gun.Activated:Connect(function()
    -- зажата
    shooting = true
    -- очередь уже идёт — вторую не запускаем
    if looping then return end
    -- начинаем очередь
    looping = true
    -- пока кнопка зажата и оружие в руках
    while shooting and equipped do
        -- не перезаряжаемся
        if not reloading then
            -- патроны есть
            if ammo.Value > 0 then
                -- выстрел
                shotEvent:FireServer(mouse.Hit.Position)
                -- минус патрон
                ammo.Value -= 1
            -- магазин пуст
            else
                -- щелчок
                emptySound:Play()
                -- очередь прекращается
                shooting = false
            -- конец проверки патронов
            end
        -- конец проверки перезарядки
        end
        -- пауза — всегда, иначе цикл повесит игру
        task.wait(fireRate)
    -- следующий выстрел
    end
    -- очередь закончилась
    looping = false
-- конец обработчика
end)
-- кнопку отпустили
gun.Deactivated:Connect(function()
    -- очередь прекращается
    shooting = false
-- конец обработчика
end)

-- нажата клавиша
UIS.InputBegan:Connect(function(input, gp)
    -- печатает в чат / оружие не в руках / уже перезаряжаемся
    if gp or not equipped or reloading then return end
    -- R, и магазин не полный
    if input.KeyCode == Enum.KeyCode.R and ammo.Value < maxAmmo.Value then
        -- началась перезарядка
        reloading = true
        -- звук
        reloadSound:Play()
        -- анимация, если задана
        if animTrack then animTrack:Play() end
        -- ждём длину звука (минимум 1 с — даже если звук не загрузился)
        task.wait(math.max(reloadSound.TimeLength, 1))
        -- полный магазин
        ammo.Value = maxAmmo.Value
        -- перезарядка закончилась
        reloading = false
    -- конец проверки
    end
-- конец обработчика
end)

-- нажата ПКМ
mouse.Button2Down:Connect(function()
    -- приближаем камеру — прицел
    if equipped then camera.FieldOfView = 30 end
    -- смотрим от первого лица
    if equipped then FirstPerson(true) end
-- конец обработчика
end)
-- ПКМ отпущена
mouse.Button2Up:Connect(function()
    -- обычный обзор
    camera.FieldOfView = 70
    -- камера снова за спиной
    FirstPerson(false)
-- конец обработчика
end)

GunLocal (снайперка)

Где: Workspace → Sniper · LocalScript
Зачем: То же, но после выстрела 2 секунды на затвор.

-- оружие (Tool)
local gun = script.Parent
-- сигнал «выстрел» на сервер
local shotEvent = gun:WaitForChild("Shot")
-- этот игрок
local player = game.Players.LocalPlayer
-- его мышь: mouse.Hit — точка под курсором
local mouse = player:GetMouse()
-- оружие в руках?
local equipped = false
-- нажатия клавиш
local UIS = game:GetService("UserInputService")
-- модель оружия
local handle = gun:WaitForChild("Handle")
-- щелчок пустого магазина
local emptySound = handle:WaitForChild("Empty")
-- звук перезарядки
local reloadSound = handle:WaitForChild("Reload")
-- патроны в магазине сейчас
local ammo = gun:WaitForChild("Ammo")
-- вместимость магазина
local maxAmmo = gun:WaitForChild("MaxAmmo")
-- анимация перезарядки
local reloadAnimation = script:WaitForChild("Animation")
-- загрузим, когда возьмём оружие
local animTrack = nil
-- идёт перезарядка?
local reloading = false
-- счётчик патронов
local gunGui = gun:WaitForChild("GunGui")
-- надпись «30 / 30»
local ammoText = gunGui.Frame.TextLabel
-- камера — для прицела
local camera = workspace.CurrentCamera
-- идёт затвор после выстрела?
local shooting = false

-- обновляет счётчик
local function UpdateAmmo()
    -- «патроны / вместимость»
    ammoText.Text = ammo.Value .. " / " .. maxAmmo.Value
-- конец функции
end
-- патроны изменились — обновляем
ammo.Changed:Connect(UpdateAmmo)

-- события каждого кадра
local RunService = game:GetService("RunService")
-- в прицеле винтовку не показываем — она закрыла бы обзор
local SHOW_GUN = false
-- сейчас целимся?
local aiming = false
-- режим камеры до прицеливания
local normalMode = nil
-- минимальное приближение до прицеливания
local normalMinZoom = nil
-- подписка «оружие видно»
local showConnection = nil

-- включить или выключить вид от первого лица
local function FirstPerson(on)
    -- уже так — ничего не делаем
    if aiming == on then return end
    -- запоминаем
    aiming = on
    -- начали целиться
    if on then
        -- запоминаем режим камеры
        normalMode = player.CameraMode
        -- и минимальное приближение
        normalMinZoom = player.CameraMinZoomDistance
        -- камера в голове персонажа
        player.CameraMode = Enum.CameraMode.LockFirstPerson
        -- оружие должно быть видно
        if SHOW_GUN then
            -- каждый кадр
            showConnection = RunService.RenderStepped:Connect(function()
                -- все детали оружия
                for _, part in ipairs(gun:GetDescendants()) do
                    -- Roblox прячет их от первого лица — возвращаем
                    if part:IsA("BasePart") then part.LocalTransparencyModifier = 0 end
                -- конец перебора
                end
            -- конец обработчика
            end)
        -- конец проверки
        end
    -- перестали целиться
    else
        -- оружие снова прячется как обычно
        if showConnection then showConnection:Disconnect() end
        -- подписки нет
        showConnection = nil
        -- режим камеры, который был
        player.CameraMode = normalMode or Enum.CameraMode.Classic
        -- камера обычная
        if player.CameraMode == Enum.CameraMode.Classic then
            -- отодвигаем камеру назад — иначе останется в голове
            player.CameraMinZoomDistance = 10
            -- один кадр
            task.wait()
            -- возвращаем приближение, как было
            player.CameraMinZoomDistance = normalMinZoom or 0.5
        -- конец проверки
        end
    -- конец проверки
    end
-- конец функции
end

-- взяли оружие в руки
local function OnEquipped()
    -- запоминаем
    equipped = true
    -- в руках — значит, родитель оружия персонаж
    local humanoid = gun.Parent:FindFirstChild("Humanoid")
    -- анимация задана
    if reloadAnimation.AnimationId ~= "" then
        -- готовим анимацию перезарядки
        animTrack = humanoid:LoadAnimation(reloadAnimation)
    -- конец проверки
    end
    -- показываем счётчик на экране
    gunGui.Parent = player.PlayerGui
    -- с актуальным числом
    UpdateAmmo()
    -- курсор-перекрестье
    mouse.Icon = "rbxassetid://6884107931"
-- конец функции
end
-- каждый раз, когда берут в руки
gun.Equipped:Connect(OnEquipped)
-- подобрали с земли: скрипт запустился, когда оружие уже в руках, — Equipped был раньше
if gun.Parent and gun.Parent:FindFirstChildOfClass("Humanoid") then OnEquipped() end

-- убрали оружие
gun.Unequipped:Connect(function()
    -- запоминаем
    equipped = false
    -- прячем счётчик обратно в оружие
    gunGui.Parent = gun
    -- обычный курсор
    mouse.Icon = ""
    -- выходим из прицела
    camera.FieldOfView = 70
    -- и из вида от первого лица
    FirstPerson(false)
-- конец обработчика
end)

-- клик, пока оружие в руках
gun.Activated:Connect(function()
    -- перезарядка или затвор — не стреляем
    if reloading or shooting then return end
    -- патроны есть
    if ammo.Value > 0 then
        -- затвор занят
        shooting = true
        -- выстрел
        shotEvent:FireServer(mouse.Hit.Position)
        -- минус патрон
        ammo.Value -= 1
        -- 2 секунды на затвор
        task.wait(2)
        -- можно стрелять снова
        shooting = false
    -- магазин пуст
    else
        -- щелчок
        emptySound:Play()
    -- конец проверки
    end
-- конец обработчика
end)

-- нажата клавиша
UIS.InputBegan:Connect(function(input, gp)
    -- печатает в чат / оружие не в руках / уже перезаряжаемся
    if gp or not equipped or reloading then return end
    -- R, и магазин не полный
    if input.KeyCode == Enum.KeyCode.R and ammo.Value < maxAmmo.Value then
        -- началась перезарядка
        reloading = true
        -- звук
        reloadSound:Play()
        -- анимация, если задана
        if animTrack then animTrack:Play() end
        -- ждём длину звука (минимум 1 с — даже если звук не загрузился)
        task.wait(math.max(reloadSound.TimeLength, 1))
        -- полный магазин
        ammo.Value = maxAmmo.Value
        -- перезарядка закончилась
        reloading = false
    -- конец проверки
    end
-- конец обработчика
end)

-- нажата ПКМ
mouse.Button2Down:Connect(function()
    -- приближаем камеру — прицел
    if equipped then camera.FieldOfView = 30 end
    -- смотрим от первого лица
    if equipped then FirstPerson(true) end
-- конец обработчика
end)
-- ПКМ отпущена
mouse.Button2Up:Connect(function()
    -- обычный обзор
    camera.FieldOfView = 70
    -- камера снова за спиной
    FirstPerson(false)
-- конец обработчика
end)

GunServer (снайперка)

Где: Workspace → Sniper · Script
Зачем: Урон 60, пуля вдвое быстрее.

-- оружие (Tool)
local gun = script.Parent
-- модель оружия
local handle = gun:WaitForChild("Handle")
-- сигнал «выстрел» от клиента
local shotEvent = gun:WaitForChild("Shot")
-- звук выстрела
local shootSound = handle:WaitForChild("Shoot")
-- удаление по таймеру
local Debris = game:GetService("Debris")
-- скорость пули
local bulletSpeed = 1000
-- урон за попадание
local damage = 60

-- клиент сообщил о выстреле и точке под курсором
shotEvent.OnServerEvent:Connect(function(player, mousePosition)
    -- стреляет только тот, у кого оружие в руках
    if gun.Parent ~= player.Character then return end
    -- новая пуля
    local bullet = Instance.new("Part")
    -- шарик
    bullet.Shape = Enum.PartType.Ball
    -- маленький
    bullet.Size = Vector3.new(0.3, 0.3, 0.3)
    -- чёрный
    bullet.BrickColor = BrickColor.new("Black")
    -- не отскакивает от своего же оружия
    bullet.CanCollide = false
    -- у ствола, смотрит на курсор
    bullet.CFrame = CFrame.new(handle.Position, mousePosition)
    -- в мир
    bullet.Parent = workspace
    -- физику пули считает сервер — попадания честные
    bullet:SetNetworkOwner(nil)
    -- летит вперёд
    bullet.AssemblyLinearVelocity = bullet.CFrame.LookVector * bulletSpeed
    -- звук выстрела
    shootSound:Play()
    -- пуля ещё никого не задела
    local hit = false
    -- пуля чего-то коснулась
    bullet.Touched:Connect(function(otherPart)
        -- уже попала или задела стрелка — пропускаем
        if hit or otherPart:IsDescendantOf(player.Character) then return end
        -- запоминаем попадание
        hit = true
        -- модель, которой принадлежит деталь
        local model = otherPart:FindFirstAncestorOfClass("Model")
        -- есть ли у неё Humanoid (живой ли)
        local humanoid = model and model:FindFirstChildOfClass("Humanoid")
        -- попали в живого
        if humanoid then
            -- урон
            humanoid:TakeDamage(damage)
        -- конец проверки
        end
        -- пуля исчезает при любом попадании
        bullet:Destroy()
    -- конец обработчика касания
    end)
    -- промах — пуля удалится через 10 секунд
    Debris:AddItem(bullet, 10)
-- конец обработчика
end)

GrenadeLocal

Где: Workspace → Grenade · LocalScript
Зачем: Замах по клику; без анимации — бросок сразу.

-- граната (Tool)
local grenade = script.Parent
-- анимация броска
local anim = grenade:WaitForChild("Animation")
-- сигнал «бросок» на сервер
local throwEvent = grenade:WaitForChild("Throw")
-- загрузим, когда возьмём гранату
local animTrack = nil
-- бросок уже начат?
local thrown = false

-- взяли гранату
local function OnEquipped()
    -- анимации ещё нет — бросим без замаха
    if anim.AnimationId == "" then return end
    -- Humanoid персонажа
    local humanoid = grenade.Parent:WaitForChild("Humanoid")
    -- готовим анимацию
    animTrack = humanoid.Animator:LoadAnimation(anim)
    -- анимация дошла до именованного кадра
    animTrack.KeyframeReached:Connect(function(kfName)
        -- кадр, где рука отпускает гранату
        if kfName == "Release" then
            -- просим сервер бросить
            throwEvent:FireServer()
        -- конец проверки
        end
    -- конец обработчика кадра
    end)
-- конец функции
end
-- каждый раз, когда берут в руки
grenade.Equipped:Connect(OnEquipped)
-- подобрали с земли: скрипт запустился уже в руках — Equipped был раньше
if grenade.Parent and grenade.Parent:FindFirstChildOfClass("Humanoid") then OnEquipped() end

-- убрали гранату
grenade.Unequipped:Connect(function()
    -- бросок отменён — можно снова
    thrown = false
-- конец обработчика
end)

-- клик, пока граната в руках
grenade.Activated:Connect(function()
    -- уже бросаем — второй раз не надо
    if thrown then return end
    -- бросок начат
    thrown = true
    -- анимация есть
    if animTrack then
        -- замах — бросок на кадре Release
        animTrack:Play()
    -- анимации нет
    else
        -- бросаем сразу
        throwEvent:FireServer()
    -- конец проверки
    end
-- конец обработчика
end)

GrenadeServer

Где: Workspace → Grenade · Script
Зачем: Бросок, взрыв через 1,5 с, граната пропадает из инвентаря.

-- граната (Tool)
local grenade = script.Parent
-- модель гранаты
local handle = grenade:WaitForChild("Handle")
-- сигнал «бросок» от клиента
local throwEvent = grenade:WaitForChild("Throw")
-- удаление по таймеру
local Debris = game:GetService("Debris")
-- граната одна — бросок один
local thrown = false

-- клиент дошёл до кадра Release
throwEvent.OnServerEvent:Connect(function(player)
    -- уже брошена или не в руках у этого игрока
    if thrown or grenade.Parent ~= player.Character then return end
    -- запоминаем
    thrown = true
    -- звук броска
    handle.Throw:Play()
    -- летящая копия гранаты
    local grenadeClone = handle:Clone()
    -- отскакивает от земли и стен
    grenadeClone.CanCollide = true
    -- в мир
    grenadeClone.Parent = workspace
    -- граната в руке исчезла
    handle.Transparency = 1
    -- корпус персонажа
    local hrp = player.Character:WaitForChild("HumanoidRootPart")
    -- направление: вперёд и вверх
    local dir = hrp.CFrame.LookVector + hrp.CFrame.UpVector
    -- толчок; учитываем массу — любая граната летит одинаково
    grenadeClone.VectorForce.Force = dir * grenadeClone.AssemblyMass * 300
    -- толкаем 0,2 секунды
    task.wait(0.2)
    -- дальше летит сама
    grenadeClone.VectorForce.Enabled = false
    -- запал
    task.wait(1.5)
    -- звук взрыва
    grenadeClone.Boom:Play()
    -- взрыв
    local exp = Instance.new("Explosion")
    -- там, где граната
    exp.Position = grenadeClone.Position
    -- радиус
    exp.BlastRadius = 20
    -- сила отбрасывания
    exp.BlastPressure = 500000
    -- взрываем
    exp.Parent = workspace
    -- граната исчезла
    grenadeClone.Transparency = 1
    -- стоит на месте, пока доигрывает звук
    grenadeClone.Anchored = true
    -- ни с чем не сталкивается
    grenadeClone.CanCollide = false
    -- через 5 с удаляется
    Debris:AddItem(grenadeClone, 5)
    -- убираем гранату из инвентаря — в самом конце: скрипт внутри неё
    grenade:Destroy()
-- конец обработчика
end)

PlayerController

Где: StarterPlayer → StarterPlayerScripts · LocalScript
Зачем: Вид от первого лица.

-- этот игрок
local player = game.Players.LocalPlayer
-- камера всегда от первого лица
player.CameraMode = Enum.CameraMode.LockFirstPerson

CharacterController

Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Свои руки видны от первого лица.

-- персонаж (скрипт кладётся в него)
local character = script.Parent
-- события каждого кадра
local RunService = game:GetService("RunService")

-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- все части персонажа
    for _, part in pairs(character:GetChildren()) do
        -- в имени есть Arm или Hand — это рука
        if string.match(part.Name, "Arm") or string.match(part.Name, "Hand") then
            -- показываем её этому игроку
            part.LocalTransparencyModifier = 0
        -- конец проверки
        end
    -- конец перебора
    end
-- конец обработчика
end)

Проверка

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

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