Roblox · Королевская битва · урок 9

Световая и дымовая гранаты

Световая граната ослепляет всех, кто рядом и видит вспышку (отвернулся — слабее). Дымовая — облако дыма на 20 секунд. Граната вылетает в момент ключевого кадра «Release» анимации броска.

Структура

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

ServerStorage → Loot
├─ Flashbang (Tool)
│  ├─ Handle (Part) — Throw, Boom (Sound)
│  ├─ Throw (RemoteEvent)
│  ├─ Animation (Animation) — бросок с кадром «Release»
│  ├─ GrenadeLocal (LocalScript)
│  └─ FlashServer (Script)
└─ SmokeGrenade (Tool) — Handle → Throw, Boom, Smoke ×2 (Enabled выкл.); Throw, Animation, GrenadeLocal, SmokeServer
ReplicatedStorage
└─ FlashEvent (RemoteEvent)
StarterPlayer → StarterPlayerScripts
└─ FlashEffect (LocalScript) — ослепление у каждого игрока

Шаги

  1. Граната — Tool с Handle (шарик), звуками «Throw» и «Boom», RemoteEvent «Throw» и Animation.
  2. Анимация броска: Rig Builder → Animation Editor: замах и бросок, приоритет Action. ПКМ по ключевому кадру броска → Rename Key Frame → «Release». Publish — ID в Animation. Без анимации граната летит сразу.
  3. GrenadeLocal одинаковый у обеих гранат: ждёт кадр «Release» (или конца анимации) и сообщает серверу точку под курсором.
  4. Сервер бросает копию Handle по дуге, через 2 секунды — взрыв. Инструмент убирается в Workspace невидимым до конца взрыва: если его удалить сразу, скрипт остановится.
  5. Вспышка: сервер проверяет расстояние (40) и прямую видимость лучом. Ослепление показывает FlashEffect в StarterPlayerScripts — он есть у каждого игрока. В методичке сигнал уходил в LocalScript гранаты, которого у других нет, и никто не ослеплялся.
  6. Дым: в Handle два Smoke (Size 15–20, Opacity 0.5, RiseVelocity 2, Enabled выкл.) — при взрыве включаются. Разные цвета дыма — опознавательные знаки для команды.

Скрипты

GrenadeLocal

Где: Flashbang и SmokeGrenade · LocalScript
Зачем: Бросок по кадру «Release».

-- игроки
local Players = game:GetService("Players")
-- граната
local tool = script.Parent
-- просим сервер бросить
local throwEvent = tool:WaitForChild("Throw")
-- бросок; ключевой кадр «Release» — момент, когда граната вылетает
local anim = tool:WaitForChild("Animation")
-- куда целимся
local mouse = Players.LocalPlayer:GetMouse()
-- бросок уже начат
local busy = false

-- граната вылетает
local function Release()
    -- точка под курсором
    throwEvent:FireServer(mouse.Hit.Position)
-- конец функции
end
-- клик
tool.Activated:Connect(function()
    -- кто держит
    local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
    -- уже бросает или погиб
    if busy or not humanoid or humanoid.Health <= 0 then return end
    -- одна граната — один бросок
    busy = true
    -- анимации нет — бросаем сразу
    if anim.AnimationId == "" then Release() return end
    -- загружаем бросок
    local track = humanoid:WaitForChild("Animator"):LoadAnimation(anim)
    -- граната ещё в руке
    local released = false
    -- анимация дошла до ключевого кадра
    track.KeyframeReached:Connect(function(name)
        -- кадр «Release» — бросок
        if name == "Release" and not released then released = true Release() end
    -- конец функции
    end)
    -- анимация кончилась
    track.Stopped:Once(function()
        -- кадра «Release» нет — бросаем в конце
        if not released then released = true Release() end
    -- конец функции
    end)
    -- замах
    track:Play()
-- конец функции
end)

FlashServer

Где: ServerStorage → Loot → Flashbang · Script
Зачем: Вспышка.

-- игроки
local Players = game:GetService("Players")
-- световая граната
local tool = script.Parent
-- сама граната
local handle = tool:WaitForChild("Handle")
-- клиент бросил
local throwEvent = tool:WaitForChild("Throw")
-- ослепить игрока (ловит FlashEffect у каждого)
local flashEvent = game:GetService("ReplicatedStorage"):WaitForChild("FlashEvent")
-- дальше вспышка не слепит
local RADIUS = 40
-- секунд до взрыва
local FUSE = 2
-- сила броска
local POWER = 70
-- уже брошена
local thrown = false

-- клиент прислал точку
throwEvent.OnServerEvent:Connect(function(player, target)
    -- кто держит
    local character = tool.Parent
    -- уже, чужая граната или мусор
    if thrown or Players:GetPlayerFromCharacter(character) ~= player or typeof(target) ~= "Vector3" then return end
    -- центр
    local root = character:FindFirstChild("HumanoidRootPart")
    -- персонажа нет
    if not root then return end
    -- брошена
    thrown = true
    -- летящая копия
    local grenade = handle:Clone()
    -- имя
    grenade.Name = "Thrown" .. tool.Name
    -- отскакивает от стен и пола
    grenade.CanCollide = true
    -- у неё есть вес
    grenade.Massless = false
    -- перед лицом
    grenade.CFrame = root.CFrame * CFrame.new(0, 2, -2)
    -- в мир
    grenade.Parent = workspace
    -- к точке
    local direction = (target - grenade.Position).Unit
    -- бросок чуть вверх — по дуге
    grenade.AssemblyLinearVelocity = (direction + Vector3.new(0, 0.35, 0)).Unit * POWER
    -- полёт считает сервер — у всех одинаково
    grenade:SetNetworkOwner(nil)
    -- звук броска
    grenade.Throw:Play()
    -- в руке гранаты больше нет
    handle.Transparency = 1
    -- её нельзя подобрать
    handle.CanTouch = false
    -- не падает сквозь карту
    handle.Anchored = true
    -- инструмент из рук убираем; скрипт продолжает работать (если удалить — остановится)
    tool.Parent = workspace
    -- фитиль
    task.wait(FUSE)
    -- взрыв
    grenade.Boom:Play()
    -- проверка прямой видимости
    local params = RaycastParams.new()
    -- исключаем…
    params.FilterType = Enum.RaycastFilterType.Exclude
    -- …саму гранату
    params.FilterDescendantsInstances = {grenade}
    -- все игроки
    for _, other in ipairs(Players:GetPlayers()) do
        -- голова
        local head = other.Character and other.Character:FindFirstChild("Head")
        -- близко (в методичке вычитали длины векторов)
        if head and (head.Position - grenade.Position).Magnitude <= RADIUS then
            -- луч от гранаты к голове
            local result = workspace:Raycast(grenade.Position, head.Position - grenade.Position, params)
            -- стены между ними нет
            if not result or result.Instance:IsDescendantOf(other.Character) then
                -- ослепить
                flashEvent:FireClient(other, grenade.Position)
            -- конец проверки
            end
        -- конец проверки
        end
    -- конец перебора
    end
    -- звук доиграет
    task.wait(1)
    -- гранаты больше нет
    grenade:Destroy()
    -- и инструмента
    tool:Destroy()
-- конец функции
end)

SmokeServer

Где: ServerStorage → Loot → SmokeGrenade · Script
Зачем: Дым.

-- игроки
local Players = game:GetService("Players")
-- дымовая граната
local tool = script.Parent
-- сама граната
local handle = tool:WaitForChild("Handle")
-- клиент бросил
local throwEvent = tool:WaitForChild("Throw")
-- сколько секунд держится дым
local SMOKE_TIME = 20
-- секунд до взрыва
local FUSE = 2
-- сила броска
local POWER = 70
-- уже брошена
local thrown = false

-- клиент прислал точку
throwEvent.OnServerEvent:Connect(function(player, target)
    -- кто держит
    local character = tool.Parent
    -- уже, чужая граната или мусор
    if thrown or Players:GetPlayerFromCharacter(character) ~= player or typeof(target) ~= "Vector3" then return end
    -- центр
    local root = character:FindFirstChild("HumanoidRootPart")
    -- персонажа нет
    if not root then return end
    -- брошена
    thrown = true
    -- летящая копия
    local grenade = handle:Clone()
    -- имя
    grenade.Name = "Thrown" .. tool.Name
    -- отскакивает от стен и пола
    grenade.CanCollide = true
    -- у неё есть вес
    grenade.Massless = false
    -- перед лицом
    grenade.CFrame = root.CFrame * CFrame.new(0, 2, -2)
    -- в мир
    grenade.Parent = workspace
    -- к точке
    local direction = (target - grenade.Position).Unit
    -- бросок чуть вверх — по дуге
    grenade.AssemblyLinearVelocity = (direction + Vector3.new(0, 0.35, 0)).Unit * POWER
    -- полёт считает сервер — у всех одинаково
    grenade:SetNetworkOwner(nil)
    -- звук броска
    grenade.Throw:Play()
    -- в руке гранаты больше нет
    handle.Transparency = 1
    -- её нельзя подобрать
    handle.CanTouch = false
    -- не падает сквозь карту
    handle.Anchored = true
    -- инструмент из рук убираем; скрипт продолжает работать (если удалить — остановится)
    tool.Parent = workspace
    -- фитиль
    task.wait(FUSE)
    -- взрыв
    grenade.Boom:Play()
    -- граната лежит на месте
    grenade.Anchored = true
    -- эффекты дыма
    for _, smoke in ipairs(grenade:GetChildren()) do
        -- включаем
        if smoke:IsA("Smoke") then smoke.Enabled = true end
    -- конец перебора
    end
    -- дым держится
    task.wait(SMOKE_TIME)
    -- гранаты больше нет
    grenade:Destroy()
    -- и инструмента
    tool:Destroy()
-- конец функции
end)

FlashEffect

Где: StarterPlayer → StarterPlayerScripts · LocalScript
Зачем: Белый экран.

-- события
local RS = game:GetService("ReplicatedStorage")
-- плавность
local TS = game:GetService("TweenService")
-- эффекты экрана
local Lighting = game:GetService("Lighting")
-- сервер: тебя ослепило
local flashEvent = RS:WaitForChild("FlashEvent")

-- точка взрыва
flashEvent.OnClientEvent:Connect(function(position)
    -- камера
    local camera = workspace.CurrentCamera
    -- направление на вспышку
    local toFlash = (position - camera.CFrame.Position).Unit
    -- 1 — смотрит прямо, −1 — спиной
    local facing = camera.CFrame.LookVector:Dot(toFlash)
    -- отвернулся — слепит слабее
    local strength = facing > 0.3 and 1 or 0.35
    -- белый экран — только у этого игрока
    local flash = Instance.new("ColorCorrectionEffect")
    -- имя
    flash.Name = "Flash"
    -- яркость
    flash.Brightness = 2 * strength
    -- включаем
    flash.Parent = Lighting
    -- ослеплён
    task.wait(1.5 * strength)
    -- проходит
    local tween = TS:Create(flash, TweenInfo.new(1.5), {Brightness = 0})
    -- плавно
    tween:Play()
    -- ждём
    tween.Completed:Wait()
    -- убираем
    flash:Destroy()
-- конец функции
end)

Проверка

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