Roblox · RPG · урок 8

Маг: посох и телепортация

Посох выпускает огненный шар: взрыв, урон и замедление цели. Навык — телепортация: держишь E, выбираешь место маркером, отпускаешь — ты там.

Структура

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

ReplicatedStorage
└─ MageEffects (Folder)
   ├─ FireBall (Part) — шар с эффектами, прозрачный, CanCollide выкл.
   ├─ Explosion (Part) — эффект взрыва
   └─ TeleportLocation (Part) — плоский цилиндр-маркер
ServerStorage
├─ Weapons (Folder)
│  └─ Staff (Tool)
│     ├─ Handle (Part) — посох; Fire (Sound)
│     ├─ SpellCast (Part) — невидимый, на конце посоха
│     ├─ FireBall (RemoteEvent)
│     ├─ YouAreNotGui (ScreenGui) — «Ты не маг»
│     ├─ FireBallLocal (LocalScript) — Animation — выпад, кадр Release
│     └─ FireBallServer (Script)
└─ Classes → Mage (Folder)
   └─ Teleportation (LocalScript)

Шаги

  1. Tool «Staff»: посох — «Handle», на конце невидимый «SpellCast» (приварен к Handle). Звук «Fire» (Magic) — в Handle.
  2. Два эффекта — детали с ParticleEmitter, Fire и др.: «FireBall» (шар) и «Explosion» (взрыв), прозрачные, CanCollide выкл. Обе — в ReplicatedStorage → Folder «MageEffects».
  3. Анимация выпада: кадр, где вылетает шар, — правый клик → Rename Key Keyframe → «Release». Приоритет Action. ID — в Animation внутри FireBallLocal.
  4. Шар вылетает на кадре Release; если кадра нет или анимации нет — сразу. Летит прямо (LinearVelocity), взрывается о стену или моба.
  5. Ошибки методички: взрыв был копией шара, замедление потом ставило цели скорость 16. Здесь — свой Explosion и возврат к собственной скорости цели.
  6. Маркер телепорта: плоский цилиндр «TeleportLocation» в MageEffects. Скрипт берёт его копию — у каждого мага свой маркер.
  7. LocalScript «Teleportation» в папке Mage: держи E — маркер под курсором (голубой — можно, красный — далеко), отпусти — перенос.

Скрипты

FireBallLocal

Где: ServerStorage → Weapons → Staff · LocalScript
Зачем: Выпад и кадр Release.

-- игроки
local Players = game:GetService("Players")
-- посох
local tool = script.Parent
-- сигнал «заклинание» на сервер
local fireballEvent = tool:WaitForChild("FireBall")
-- «Ты не маг»
local notGui = tool:WaitForChild("YouAreNotGui")
-- выпад посохом; кадр Release — момент выстрела
local anim = script:WaitForChild("Animation")
-- этот игрок
local player = Players.LocalPlayer
-- куда целимся
local mouse = player:GetMouse()
-- чей посох
local CLASS = "Mage"
-- перезарядка
local COOLDOWN = 1
-- дорожка анимации
local track = nil
-- колдуем
local busy = false

-- взяли посох
local function OnEquipped()
    -- Humanoid
    local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
    -- анимации
    local animator = humanoid and humanoid:FindFirstChildOfClass("Animator")
    -- готовим выпад
    if animator and anim.AnimationId ~= "" then track = animator:LoadAnimation(anim) end
-- конец функции
end
-- каждый раз, когда берут
tool.Equipped:Connect(OnEquipped)
-- уже в руках
if tool.Parent and tool.Parent:FindFirstChildOfClass("Humanoid") then OnEquipped() end

-- заклинание
local function Cast()
    -- колдуем
    busy = true
    -- шар ещё не вылетел
    local fired = false
    -- выпустить шар
    local function Fire()
        -- только один раз
        if fired then return end
        -- вылетел
        fired = true
        -- серверу — куда лететь
        fireballEvent:FireServer(mouse.Hit.Position)
    -- конец функции
    end
    -- анимация есть
    if track then
        -- анимация дошла до именованного кадра
        local connection = track.KeyframeReached:Connect(function(name)
            -- кадр выстрела
            if name == "Release" then Fire() end
        -- конец функции
        end)
        -- выпад
        track:Play()
        -- ждём кадр Release
        task.wait(0.6)
        -- подписка только на этот выпад
        connection:Disconnect()
    -- конец проверки
    end
    -- кадра не было или анимации нет — стреляем сейчас
    Fire()
    -- перезарядка
    task.wait(COOLDOWN)
    -- можно снова
    busy = false
-- конец функции
end

-- клик с посохом
tool.Activated:Connect(function()
    -- ещё колдуем
    if busy then return end
    -- чужой класс
    if player:GetAttribute("Class") ~= CLASS then
        -- надпись ещё не показана
        if notGui.Parent == tool then
            -- «Ты не …» на экран
            notGui.Parent = player.PlayerGui
            -- 2 секунды
            task.wait(2)
            -- прячем обратно в оружие
            notGui.Parent = tool
        -- конец проверки
        end
        -- не колдуем
        return
    -- конец проверки
    end
    -- колдуем
    Cast()
-- конец функции
end)

FireBallServer

Где: ServerStorage → Weapons → Staff · Script
Зачем: Огненный шар.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- удаляет эффекты
local Debris = game:GetService("Debris")
-- посох
local tool = script.Parent
-- откуда вылетает шар
local spell = tool:WaitForChild("SpellCast")
-- звук заклинания
local fireSound = tool:WaitForChild("Handle"):WaitForChild("Fire")
-- заклинание от клиента
local fireballEvent = tool:WaitForChild("FireBall")
-- образцы эффектов
local effects = RS:WaitForChild("MageEffects")
-- огненный шар
local fireballSample = effects:WaitForChild("FireBall")
-- взрыв (в методичке по ошибке брался FireBall)
local explosionSample = effects:WaitForChild("Explosion")
-- кто колдует
local CLASS = "Mage"
-- скорость шара
local SPEED = 80
-- перезарядка (проверка на сервере)
local COOLDOWN = 1
-- время прошлого заклинания
local lastCast = 0

-- клиент колдует
fireballEvent.OnServerEvent:Connect(function(player, target)
    -- кто держит посох
    local character = tool.Parent
    -- посох не у него или он не маг
    if Players:GetPlayerFromCharacter(character) ~= player or player:GetAttribute("Class") ~= CLASS then return end
    -- мусор вместо точки или слишком часто
    if typeof(target) ~= "Vector3" or os.clock() - lastCast < COOLDOWN then return end
    -- запоминаем
    lastCast = os.clock()
    -- звук
    fireSound:Play()
    -- новый шар
    local ball = fireballSample:Clone()
    -- летит
    ball.Anchored = false
    -- сквозь своих
    ball.CanCollide = false
    -- у конца посоха, смотрит на цель
    ball.CFrame = CFrame.new(spell.Position, target)
    -- в мир
    ball.Parent = workspace
    -- полёт считает сервер
    ball:SetNetworkOwner(nil)
    -- точка для силы
    local attachment = Instance.new("Attachment", ball)
    -- держит скорость
    local move = Instance.new("LinearVelocity")
    -- за центр шара
    move.Attachment0 = attachment
    -- без предела — летит прямо, без падения
    move.MaxForce = math.huge
    -- к цели
    move.VectorVelocity = ball.CFrame.LookVector * SPEED
    -- в шар
    move.Parent = ball
    -- ещё не взорвался
    local done = false
    -- шар коснулся
    ball.Touched:Connect(function(hit)
        -- уже взорвался или задел мага
        if done or hit:IsDescendantOf(character) then return end
        -- чья деталь
        local model = hit:FindFirstAncestorOfClass("Model")
        -- Humanoid цели
        local target = model and model:FindFirstChildOfClass("Humanoid")
        -- сквозь эффекты и невидимые зоны
        if not target and not hit.CanCollide then return end
        -- взрываемся
        done = true
        -- взрыв
        local boom = explosionSample:Clone()
        -- на месте
        boom.Anchored = true
        -- не мешает
        boom.CanCollide = false
        -- где шар
        boom.Position = ball.Position
        -- показываем
        boom.Parent = workspace
        -- секунду
        Debris:AddItem(boom, 1)
        -- шара больше нет
        ball:Destroy()
        -- попали в моба
        if target and target.Health > 0 and not Players:GetPlayerFromCharacter(model) then
            -- прошлая метка «кто ударил»
            local old = target:FindFirstChild("creator")
            -- убираем
            if old then old:Destroy() end
            -- новая метка
            local tag = Instance.new("ObjectValue")
            -- стандартное имя метки в Roblox
            tag.Name = "creator"
            -- кто ударил — получит опыт за победу
            tag.Value = player
            -- в Humanoid цели
            tag.Parent = target
            -- через 3 секунды метка исчезнет
            Debris:AddItem(tag, 3)
            -- урон
            target:TakeDamage(15)
            -- обычная скорость цели (запоминаем один раз)
            local base = target:GetAttribute("BaseSpeed") or target.WalkSpeed
            -- храним в самой цели
            target:SetAttribute("BaseSpeed", base)
            -- замедление
            target.WalkSpeed = base * 0.3
            -- через 3 секунды
            task.delay(3, function()
                -- её собственная скорость, а не 16
                if target.Parent then target.WalkSpeed = base end
            -- конец функции
            end)
        -- конец проверки
        end
    -- конец функции
    end)
    -- промах — через 4 секунды исчезнет
    Debris:AddItem(ball, 4)
-- конец функции
end)

Teleportation

Где: ServerStorage → Classes → Mage · LocalScript
Зачем: Телепорт на E.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- каждый кадр
local RunService = game:GetService("RunService")
-- игроки
local Players = game:GetService("Players")
-- персонаж
local character = script.Parent.Parent
-- центр
local root = character:WaitForChild("HumanoidRootPart")
-- куда указывает курсор
local mouse = Players.LocalPlayer:GetMouse()
-- своя копия маркера — образец не трогаем
local marker = RS:WaitForChild("MageEffects"):WaitForChild("TeleportLocation"):Clone()
-- не падает
marker.Anchored = true
-- сквозь
marker.CanCollide = false
-- курсор его не замечает
marker.CanQuery = false
-- и mouse.Hit тоже
mouse.TargetFilter = marker
-- держать E — выбор места, отпустить — телепорт
local KEY = Enum.KeyCode.E
-- перезарядка
local COOLDOWN = 3
-- дальность телепорта
local function Range() return 35 end
-- выбираем место
local aiming = false
-- можно телепортироваться
local ready = true

-- место достаточно близко?
local function InRange()
    -- Magnitude — длина вектора
    return (mouse.Hit.Position - root.Position).Magnitude < Range()
-- конец функции
end

-- нажали
UIS.InputBegan:Connect(function(input, processed)
    -- не E или перезарядка
    if processed or input.KeyCode ~= KEY or not ready or aiming then return end
    -- выбираем место
    aiming = true
    -- маркер виден только этому игроку
    marker.Parent = workspace
-- конец функции
end)
-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- не выбираем
    if not aiming then return end
    -- маркер под курсором
    marker.Position = mouse.Hit.Position
    -- голубой — можно, красный — далеко
    marker.Color = InRange() and Color3.fromRGB(0, 255, 255) or Color3.fromRGB(255, 0, 0)
-- конец функции
end)
-- отпустили
UIS.InputEnded:Connect(function(input)
    -- не E или не выбирали
    if input.KeyCode ~= KEY or not aiming then return end
    -- выбор закончен
    aiming = false
    -- прячем маркер
    marker.Parent = nil
    -- далеко — не телепортируемся
    if not InRange() then return end
    -- перезарядка
    ready = false
    -- переносимся, смотрим в ту же сторону
    character:PivotTo(root.CFrame.Rotation + mouse.Hit.Position + Vector3.new(0, 3, 0))
    -- ждём
    task.wait(COOLDOWN)
    -- можно снова
    ready = true
-- конец функции
end)

Проверка

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