Roblox · Doors · урок 7

Монстр охотится, свет мигает

Теперь монстр убивает: кто в его зоне и на виду — погибает. Спасает шкаф. Перед появлением в комнате мигает свет — есть 2 секунды, чтобы спрятаться.

Структура

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

ServerStorage → Enemies → RedMonster
└─ Monster (Part)
   ├─ Appear (Sound) — гул «Drone»
   └─ Disappear (Sound) — «Whoosh», PlayOnRemove вкл.
Workspace
└─ Sounds (Folder)
   └─ Lights (Sound) — мигание «Light flicker»
ServerStorage → RoomSamples → комната
└─ Build (Model)
   └─ Lights (Model) — все лампы комнаты
      └─ Lamp (Part)
         └─ PointLight (PointLight)

Шаги

  1. Звуки в деталь Monster: «Appear» (гул) и «Disappear» (свист) — у Disappear включи PlayOnRemove: звук сыграет, когда монстра удалят.
  2. Как монстр ищет: каждый кадр GetPartsInPart находит персонажей внутри Emitter, потом луч от монстра к игроку проверяет, нет ли между ними стены. Невидимые детали (Trigger, Enter) луч пропускает, стены и барьер шкафа — нет.
  3. Лампы каждой комнаты — в модель «Lights» внутри Build; в лампах — PointLight (или SpotLight).
  4. Workspace → Folder «Sounds» → звук «Lights».
  5. Room и Monster обновились, в MainScript — мигание перед монстром.
  6. Мигание: свет гаснет, вспыхивает, снова гаснет — и за секунду возвращается к обычной яркости.

Скрипты

Monster

Где: ServerScriptService → MainScript · ModuleScript
Зачем: Ищет игроков в зоне и убивает тех, кого видит.

-- события каждого кадра
local RunService = game:GetService("RunService")
-- здесь лежит образец монстра
local SS = game:GetService("ServerStorage")
-- все игроки
local Players = game:GetService("Players")

-- таблица модуля
local monster = {}
-- скорость: studs в секунду
monster.speed = 20

-- видит ли монстр игрока (нет ли стены между ними)
local function CanSee(model, root)
    -- откуда смотрит
    local origin = model.Monster.Position
    -- настройки луча
    local params = RaycastParams.new()
    -- режим «не замечать список»
    params.FilterType = Enum.RaycastFilterType.Exclude
    -- сам монстр не мешает
    local ignore = {model}
    -- не больше 20 попыток
    for _ = 1, 20 do
        -- обновляем список
        params.FilterDescendantsInstances = ignore
        -- луч к игроку
        local result = workspace:Raycast(origin, root.Position - origin, params)
        -- ни во что не попал
        if not result then return false end
        -- попал в игрока — видит
        if result.Instance:IsDescendantOf(root.Parent) then return true end
        -- стена, шкаф, барьер — не видит
        if result.Instance.CanCollide then return false end
        -- невидимая деталь (Trigger, Enter…) — смотрим сквозь неё
        table.insert(ignore, result.Instance)
    -- следующая попытка
    end
    -- слишком много преград
    return false
-- конец функции
end

-- ищет игроков в зоне монстра
function monster.FindPlayers(model)
    -- персонажи всех игроков
    local characters = {}
    -- каждый игрок
    for _, player in ipairs(Players:GetPlayers()) do
        -- есть персонаж — в список
        if player.Character then table.insert(characters, player.Character) end
    -- конец перебора
    end
    -- настройки поиска
    local params = OverlapParams.new()
    -- искать только среди списка
    params.FilterType = Enum.RaycastFilterType.Include
    -- список — персонажи
    params.FilterDescendantsInstances = characters
    -- детали персонажей внутри зоны Emitter
    for _, part in ipairs(workspace:GetPartsInPart(model.Emitter, params)) do
        -- центр персонажа
        if part.Name == "HumanoidRootPart" then
            -- его здоровье
            local humanoid = part.Parent:FindFirstChildOfClass("Humanoid")
            -- жив и на виду
            if humanoid and humanoid.Health > 0 and CanSee(model, part) then
                -- монстр настиг
                humanoid.Health = 0
            -- конец проверки
            end
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- плавно ведёт монстра к детали target
function monster.LerpTo(model, target)
    -- откуда
    local start = model:GetPivot()
    -- сколько лететь
    local distance = (start.Position - target.Position).Magnitude
    -- сколько секунд (не ноль)
    local duration = math.max(distance / monster.speed, 0.05)
    -- доля пути: 0 — начало, 1 — конец
    local alpha = 0
    -- пока не долетел
    while alpha < 1 do
        -- прибавляем долю за прошедший кадр
        alpha = math.min(alpha + RunService.Heartbeat:Wait() / duration, 1)
        -- точка между началом и целью
        model:PivotTo(start:Lerp(target.CFrame, alpha))
        -- каждый кадр ищем игроков
        monster.FindPlayers(model)
    -- конец цикла
    end
-- конец функции
end

-- проводит монстра через комнаты
function monster.Navigate(model, fromNumber, toNumber, rooms)
    -- каждая комната по порядку
    for i = fromNumber, toNumber do
        -- комната
        local roomModel = rooms[i]
        -- ко входу
        if roomModel:FindFirstChild("Enter") then monster.LerpTo(model, roomModel.Enter) end
        -- точки поворота в извитых комнатах
        local waypoints = roomModel:FindFirstChild("Waypoints")
        -- есть
        if waypoints then
            -- по номерам 1, 2, 3…
            for w = 1, #waypoints:GetChildren() do
                -- точка с этим номером
                local point = waypoints:FindFirstChild(tostring(w))
                -- летим к ней
                if point then monster.LerpTo(model, point) end
            -- конец перебора
            end
        -- конец проверки
        end
        -- к выходу
        monster.LerpTo(model, roomModel.Exit)
    -- конец перебора
    end
-- конец функции
end

-- монстр проходит комнату number и следующую
function monster.New(number, rooms)
    -- дальше последней комнаты не идёт
    local toNumber = math.min(number + 1, #rooms)
    -- откуда начинает
    local startRoom = rooms[number]
    -- копия монстра
    local model = SS.Enemies.RedMonster:Clone()
    -- ставим на вход комнаты
    model:PivotTo((startRoom:FindFirstChild("Enter") or startRoom.Exit).CFrame)
    -- появляется
    model.Parent = workspace
    -- гул появления
    model.Monster.Appear:Play()
    -- проходит комнаты
    monster.Navigate(model, number, toNumber, rooms)
    -- исчезает (звук Disappear сыграет сам — PlayOnRemove)
    model:Destroy()
-- конец функции
end

-- отдаём таблицу модуля
return monster

Room

Где: ServerScriptService → MainScript · ModuleScript
Зачем: + мигание ламп комнаты.

-- плавное мигание света
local TS = game:GetService("TweenService")
-- здесь лежат образцы комнат
local SS = game:GetService("ServerStorage")
-- модуль дверей
local door = require(script.Door)
-- модуль мебели
local furniture = require(script.Furniture)
-- модуль предметов (ключ)
local item = require(script.Item)

-- таблица модуля
local room = {}
-- редкость и правила комнат
room.info = require(script.RoomInfo)
-- генератор случайных чисел
room.random = Random.new()
-- куда был последний поворот
room.lastTurnDirection = nil

-- одна лампа мигает
function room.TweenLight(light)
    -- обычная яркость (запоминаем один раз)
    local base = light:GetAttribute("Base") or light.Brightness
    -- храним в самой лампе
    light:SetAttribute("Base", base)
    -- резкое дёрганое мигание
    local tweenInfo = TweenInfo.new(0.3, Enum.EasingStyle.Elastic, Enum.EasingDirection.InOut)
    -- погасить
    local off = TS:Create(light, tweenInfo, {Brightness = 0})
    -- вспышка
    local on = TS:Create(light, tweenInfo, {Brightness = base * 4})
    -- гаснет
    off:Play()
    -- ждём
    off.Completed:Wait()
    -- вспыхивает
    on:Play()
    -- ждём
    on.Completed:Wait()
    -- снова гаснет
    off:Play()
    -- ждём
    off.Completed:Wait()
    -- за секунду возвращаем обычный свет
    TS:Create(light, TweenInfo.new(1), {Brightness = base}):Play()
-- конец функции
end

-- мигают все лампы комнаты
function room.Lights(roomModel)
    -- постройка комнаты
    local build = roomModel:FindFirstChild("Build")
    -- модель с лампами
    local lights = build and build:FindFirstChild("Lights")
    -- ламп нет — нечему мигать
    if not lights then return end
    -- всё внутри ламп
    for _, obj in ipairs(lights:GetDescendants()) do
        -- источник света: PointLight, SpotLight, SurfaceLight
        if obj:IsA("Light") then
            -- мигает одновременно с остальными
            task.spawn(room.TweenLight, obj)
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- можно ли поставить эту комнату следующей
local function Allowed(name, info, prevRoom)
    -- настройки предыдущей (у стартовой их нет)
    local prevInfo = room.info[prevRoom.Name] or {}
    -- образца нет — пропускаем
    if not SS.RoomSamples:FindFirstChild(name) then return false end
    -- две одинаковые подряд — скучно
    if name == prevRoom.Name then return false end
    -- два поворота в одну сторону — коридор замкнётся
    if info.Direction and info.Direction == room.lastTurnDirection then return false end
    -- две лестницы подряд — нельзя
    if info.Stairs and prevInfo.Stairs then return false end
    -- подходит
    return true
-- конец функции
end

-- случайная комната с учётом редкости
function room.GetRandom(prevRoom)
    -- сумма весов подходящих комнат
    local totalRarity = 0
    -- все комнаты из RoomInfo
    for name, info in pairs(room.info) do
        -- подходящие складываем
        if Allowed(name, info, prevRoom) then totalRarity += info.Rarity end
    -- конец перебора
    end
    -- понятная ошибка вместо зависания
    if totalRarity == 0 then error("Нет подходящих комнат в ServerStorage.RoomSamples") end
    -- случайное число от 1 до суммы
    local randomRarity = room.random:NextInteger(1, totalRarity)
    -- накопленный вес
    local currentRarity = 0
    -- снова по комнатам (порядок тот же)
    for name, info in pairs(room.info) do
        -- только подходящие
        if Allowed(name, info, prevRoom) then
            -- прибавляем вес
            currentRarity += info.Rarity
            -- число попало на эту комнату
            if randomRarity <= currentRarity then
                -- запоминаем поворот
                if info.Direction then room.lastTurnDirection = info.Direction end
                -- отдаём образец
                return SS.RoomSamples[name]
            -- конец проверки
            end
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- ставит новую комнату за предыдущей
function room.Generate(prevRoom, number)
    -- копия случайной комнаты
    local newRoom = room.GetRandom(prevRoom):Clone()
    -- двигаем комнату за вход
    newRoom.PrimaryPart = newRoom.Enter
    -- вход новой — точно на выход предыдущей
    newRoom:PivotTo(prevRoom.Exit.CFrame)
    -- дверь пока без замка
    local keyNeeded = false
    -- расставляем мебель; получаем места для ключа
    local locations = furniture.Generate(newRoom)
    -- есть ящики и выпал шанс 1 из 4
    if locations and room.random:NextInteger(1, 4) == 4 then
        -- дверь этой комнаты будет заперта
        keyNeeded = true
        -- кладём ключ в случайный ящик
        item.New(locations[room.random:NextInteger(1, #locations)], "Key")
    -- конец проверки
    end
    -- дверь на выходе — с замком или без
    door.New(newRoom, number, keyNeeded)
    -- показываем комнату
    newRoom.Parent = workspace.GeneratedRooms
    -- она станет «предыдущей» для следующей
    return newRoom
-- конец функции
end

-- отдаём таблицу модуля
return room

MainScript

Где: ServerScriptService · Script
Зачем: Свет мигает, через 2 секунды — монстр.

-- модуль комнат
local room = require(script.Room)
-- модуль дверей
local door = require(script.Room.Door)
-- модуль монстра
local monster = require(script.Monster)
-- номер открытой двери
local enterEvent = game.ServerStorage:WaitForChild("EnterEvent")
-- сколько комнат вместе со стартовой
local ROOMS = 10
-- монстр после каждой 3-й двери
local MONSTER_EVERY = 3

-- первая «предыдущая» — стартовая
local prevRoom = workspace.StartRoom
-- все комнаты по номерам: 1 — стартовая
local generatedRooms = {prevRoom}
-- дверь 01 в стартовой комнате
door.New(prevRoom, 1, false)
-- комнаты 2…10
for i = 2, ROOMS do
    -- новая комната с дверью номер i
    prevRoom = room.Generate(prevRoom, i)
    -- запоминаем под номером
    generatedRooms[i] = prevRoom
-- конец цикла
end

-- открыта дверь number
enterEvent.Event:Connect(function(number)
    -- номер делится на 3 и комната не последняя
    if number % MONSTER_EVERY == 0 and number < #generatedRooms then
        -- комната, куда вошёл игрок
        local nextRoom = generatedRooms[number + 1]
        -- звук мигания
        local sound = workspace:FindFirstChild("Sounds") and workspace.Sounds:FindFirstChild("Lights")
        -- играем, если есть
        if sound then sound:Play() end
        -- лампы мигают — предупреждение
        room.Lights(nextRoom)
        -- 2 секунды, чтобы спрятаться
        task.wait(2)
        -- монстр пролетает комнаты
        monster.New(number, generatedRooms)
    -- конец проверки
    end
-- конец обработчика
end)

Проверка

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