Roblox · Doors · урок 9
Итог курса на одной странице: полная структура Doors и финальные версии всех скриптов — без «добавь строку» и «замени блок». Удобно сверить свою игру или собрать её заново.
Синим — что добавляем на этом уроке.
Workspace ├─ StartRoom (Model) — Build (+ Lights), Exit, SpawnLocation ├─ EndRoom (Model) — Build (+ Lights), Enter, CameraPart ├─ GeneratedRooms (Folder) — комнаты появляются при запуске └─ Sounds (Folder) └─ Lights (Sound) — мигание света ServerStorage ├─ RoomSamples (Folder) │ └─ SmallRoom, LargeRoom, LeftCorner, RightCorner, … (Model) — PrimaryPart = Enter │ ├─ Build (Model) — стены, пол, потолок │ │ └─ Lights (Model) — лампы с PointLight │ ├─ Enter, Exit (Part) — смотрят в одну сторону │ ├─ Furniture (Folder) — локаторы Bedside, Table, Wardrobe, Closet │ └─ Waypoints (Folder) — точки 1, 2… — в извитых комнатах ├─ FurnitureSamples (Folder) │ ├─ Bedside, Table (Model) — Primary + каркас + Bottoms (Bottom с Location и Handle) │ ├─ Wardrobe (Model) — + Spinner с дверцей │ └─ Closet (Model) — Primary, Barrier, Outside, Inside, Left/RightSpinner с дверями ├─ Door (Model) — Primary, Spinner, Door (Open, Unlock), Label → NumberGui → Number, Lock, Trigger ├─ Items (Folder) │ └─ Key (Tool) — Handle ├─ Enemies (Folder) │ └─ RedMonster (Model) — Monster (Decal, Appear, Disappear) + Emitter (ParticleEmitter) └─ EnterEvent (BindableEvent) ReplicatedStorage └─ EndEvent (RemoteEvent) ServerScriptService └─ MainScript (Script) ├─ Room (ModuleScript) │ ├─ RoomInfo (ModuleScript) │ ├─ Door (ModuleScript) │ ├─ Furniture (ModuleScript) │ │ └─ Closet (ModuleScript) │ └─ Item (ModuleScript) └─ Monster (ModuleScript) StarterGui └─ EndGui (ScreenGui) └─ Frame (Frame) ├─ EndText (TextLabel) └─ EndLocal (LocalScript)
Где: ServerScriptService · Script
Зачем: Строит 10 комнат, запускает монстра и финал.
-- модуль комнат
local room = require(script.Room)
-- модуль дверей
local door = require(script.Room.Door)
-- модуль монстра
local monster = require(script.Monster)
-- номер открытой двери
local enterEvent = game.ServerStorage:WaitForChild("EnterEvent")
-- финал — всем игрокам
local endEvent = game.ReplicatedStorage:WaitForChild("EndEvent")
-- сколько комнат вместе со стартовой
local ROOMS = 10
-- монстр после каждой 3-й двери
local MONSTER_EVERY = 3
-- первая «предыдущая» — стартовая
local prevRoom = workspace.StartRoom
-- финальная комната
local endRoom = workspace.EndRoom
-- все комнаты по номерам: 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
-- двигаем финальную комнату за вход
endRoom.PrimaryPart = endRoom.Enter
-- ставим за последней комнатой
endRoom:PivotTo(prevRoom.Exit.CFrame)
-- финал уже показан?
local finished = false
-- открыта дверь number
enterEvent.Event:Connect(function(number)
-- открыта последняя дверь — финал
if number == #generatedRooms then
-- уже показали
if finished then return end
-- больше не показываем
finished = true
-- игрок заходит в финальную комнату
task.wait(2)
-- катсцена у всех игроков
endEvent:FireAllClients()
-- камера разворачивается
task.wait(2)
-- дверь за спиной медленно закрывается
door.Close(prevRoom.Door)
-- свет мигает
room.Lights(endRoom)
-- номер делится на 3 — монстр
elseif number % MONSTER_EVERY == 0 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)Где: 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Где: ServerScriptService → MainScript → Room · ModuleScript
Зачем: Редкость и правила комнат.
-- таблица: имя комнаты → её настройки
local roomInfo = {
-- маленькая; Rarity — вес: чем больше, тем чаще
["SmallRoom"] = {Rarity = 4},
-- большая
["LargeRoom"] = {Rarity = 4},
-- лестница вверх
["Upstairs"] = {Stairs = true, Rarity = 2},
-- лестница вниз
["Downstairs"] = {Stairs = true, Rarity = 2},
-- поворот налево
["LeftCorner"] = {Direction = "Left", Rarity = 6},
-- поворот направо
["RightCorner"] = {Direction = "Right", Rarity = 6},
-- зигзаг влево
["LeftTwist"] = {Rarity = 5},
-- зигзаг вправо
["RightTwist"] = {Rarity = 5},
-- конец таблицы
}
-- отдаём таблицу модулю Room
return roomInfoГде: ServerScriptService → MainScript → Room · ModuleScript
Зачем: Дверь с номером, замок, открытие и закрытие.
-- плавный поворот двери
local TS = game:GetService("TweenService")
-- здесь лежит образец двери
local SS = game:GetService("ServerStorage")
-- сообщает главному скрипту номер открытой двери
local enterEvent = SS:WaitForChild("EnterEvent")
-- таблица модуля
local door = {}
-- открывает дверь
function door.Open(doorModel, number)
-- отмечаем: открыта
doorModel:SetAttribute("Open", true)
-- петля повёрнута на 90°
local cframe = doorModel.Spinner.CFrame * CFrame.Angles(0, math.rad(90), 0)
-- за полсекунды
local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Linear)
-- поворачиваем петлю — дверь приварена и едет следом
TS:Create(doorModel.Spinner, tweenInfo, {CFrame = cframe}):Play()
-- скрип двери
doorModel.Door.Open:Play()
-- игрок вошёл в комнату number + 1
enterEvent:Fire(number)
-- конец функции
end
-- медленно захлопывает дверь (финал)
function door.Close(doorModel)
-- обратно на 90°
local cframe = doorModel.Spinner.CFrame * CFrame.Angles(0, math.rad(-90), 0)
-- 10 секунд, с отскоком в конце
local tweenInfo = TweenInfo.new(10, Enum.EasingStyle.Bounce, Enum.EasingDirection.Out)
-- закрываем
TS:Create(doorModel.Spinner, tweenInfo, {CFrame = cframe}):Play()
-- конец функции
end
-- ключ в руках или в рюкзаке
local function FindKey(character)
-- чей это персонаж
local player = game.Players:GetPlayerFromCharacter(character)
-- Tool «Key» или nil
return character:FindFirstChild("Key") or (player and player.Backpack:FindFirstChild("Key"))
-- конец функции
end
-- ставит дверь на выход комнаты
function door.New(roomModel, number, locked)
-- копия образца
local doorModel = SS.Door:Clone()
-- в дверной проём
doorModel:PivotTo(roomModel.Exit.CFrame)
-- закрыта
doorModel:SetAttribute("Open", false)
-- номер на табличке: 01, 02…
doorModel.Label.NumberGui.Number.Text = string.format("%02d", number)
-- замок виден, только если дверь заперта
doorModel.Lock.Transparency = locked and 0 or 1
-- кто-то подошёл к двери
doorModel.Trigger.Touched:Connect(function(otherPart)
-- модель, которой принадлежит деталь
local character = otherPart.Parent
-- это персонаж?
local humanoid = character:FindFirstChildOfClass("Humanoid")
-- не персонаж или уже открыта
if not humanoid or doorModel:GetAttribute("Open") then return end
-- замка нет
if not locked then
-- просто открываем
door.Open(doorModel, number)
-- готово
return
-- конец проверки
end
-- есть ли у игрока ключ
local key = FindKey(character)
-- есть
if key then
-- дверь больше не заперта
locked = false
-- ключ тратится
key:Destroy()
-- замок исчезает
doorModel.Lock:Destroy()
-- щелчок замка
doorModel.Door.Unlock:Play()
-- открываем
door.Open(doorModel, number)
-- конец проверки
end
-- конец обработчика
end)
-- дверь — часть комнаты
doorModel.Parent = roomModel
-- отдаём дверь
return doorModel
-- конец функции
end
-- отдаём таблицу модуля
return doorГде: ServerScriptService → MainScript → Room · ModuleScript
Зачем: Мебель по локаторам, ящики и дверцы.
-- плавное движение ящиков
local TS = game:GetService("TweenService")
-- здесь лежат образцы мебели
local SS = game:GetService("ServerStorage")
-- шкаф для пряток
local closet = require(script.Closet)
-- на сколько studs выдвигается ящик
local SLIDE = 1.5
-- таблица модуля
local furniture = {}
-- открыть или закрыть ящик / дверцу
function furniture.Open(bottom)
-- двигается — второе нажатие не примем
bottom:SetAttribute("Moving", true)
-- сейчас открыт?
local opened = bottom:GetAttribute("Open")
-- открыт — задвигаем (1), закрыт — выдвигаем (−1)
local direction = opened and 1 or -1
-- Bottom → папка Bottoms → модель мебели
local model = bottom.Parent.Parent
-- анимация
local tween
-- у мебели есть петля — это дверца (шкаф-гардероб)
if model:FindFirstChild("Spinner") then
-- поворот петли на 90°
local cframe = model.Spinner.CFrame * CFrame.Angles(0, math.rad(90 * direction), 0)
-- поворачиваем петлю
tween = TS:Create(model.Spinner, TweenInfo.new(0.5), {CFrame = cframe})
-- ящик
else
-- сдвиг вперёд или назад
local cframe = bottom.CFrame * CFrame.new(0, 0, SLIDE * direction)
-- двигаем дно — стенки приварены
tween = TS:Create(bottom, TweenInfo.new(0.5), {CFrame = cframe})
-- конец проверки
end
-- запускаем
tween:Play()
-- ждём конца
tween.Completed:Wait()
-- запоминаем новое положение
bottom:SetAttribute("Open", not opened)
-- можно нажимать снова
bottom:SetAttribute("Moving", false)
-- конец функции
end
-- ставит мебель на место локатора
function furniture.New(sample)
-- образец с тем же именем
local template = SS.FurnitureSamples:FindFirstChild(sample.Name)
-- образца нет
if not template then
-- подсказка в Output
warn("Нет мебели " .. sample.Name .. " в ServerStorage.FurnitureSamples")
-- ничего не ставим
return nil
-- конец проверки
end
-- копия мебели
local model = template:Clone()
-- на место и с поворотом локатора
model:PivotTo(sample.CFrame)
-- места внутри ящиков, куда можно положить ключ
local locations = {}
-- папка с донышками ящиков
local bottoms = model:FindFirstChild("Bottoms")
-- ящики есть
if bottoms then
-- каждый ящик
for _, bottom in ipairs(bottoms:GetChildren()) do
-- запоминаем место внутри
table.insert(locations, bottom.Location)
-- закрыт
bottom:SetAttribute("Open", false)
-- не двигается
bottom:SetAttribute("Moving", false)
-- надпись «Открыть» (E / тап / X)
local prompt = Instance.new("ProximityPrompt")
-- текст
prompt.ActionText = "Открыть"
-- с 5 studs
prompt.MaxActivationDistance = 5
-- невидимая деталь Primary не мешает
prompt.RequiresLineOfSight = false
-- на ручке ящика
prompt.Parent = bottom.Handle
-- нажали
prompt.Triggered:Connect(function()
-- ещё едет — ждём
if bottom:GetAttribute("Moving") then return end
-- открываем или закрываем
furniture.Open(bottom)
-- меняем надпись
prompt.ActionText = bottom:GetAttribute("Open") and "Закрыть" or "Открыть"
-- конец обработчика
end)
-- конец перебора
end
-- конец проверки
end
-- мебель в комнату
model.Parent = sample.Parent
-- локатор больше не нужен
sample:Destroy()
-- отдаём места для ключа
return locations
-- конец функции
end
-- расставляет всю мебель комнаты
function furniture.Generate(roomModel)
-- все места для ключа в этой комнате
local roomLocations = {}
-- папка с локаторами
local folder = roomModel:FindFirstChild("Furniture")
-- мебели в комнате нет
if not folder then return nil end
-- каждый локатор
for _, sample in ipairs(folder:GetChildren()) do
-- локатор шкафа для пряток
if sample.Name == "Closet" then
-- у него свой модуль
closet.New(sample)
-- обычная мебель
else
-- ставим мебель
local locations = furniture.New(sample)
-- поставили
if locations then
-- её места
for _, location in ipairs(locations) do
-- добавляем к местам комнаты
table.insert(roomLocations, location)
-- конец перебора
end
-- конец проверки
end
-- конец проверки
end
-- конец перебора
end
-- есть куда положить ключ
if #roomLocations > 0 then return roomLocations end
-- ящиков нет
return nil
-- конец функции
end
-- отдаём таблицу модуля
return furnitureГде: ServerScriptService → MainScript → Room → Furniture · ModuleScript
Зачем: Шкаф для пряток.
-- плавное движение дверей
local TS = game:GetService("TweenService")
-- здесь лежит образец шкафа
local SS = game:GetService("ServerStorage")
-- обычная скорость и прыжок персонажа
local SP = game:GetService("StarterPlayer")
-- таблица модуля
local closet = {}
-- поворачивает одну петлю
function closet.MoveSpinner(spinner, direction)
-- на 100° в нужную сторону
local cframe = spinner.CFrame * CFrame.Angles(0, math.rad(100 * direction), 0)
-- за полсекунды
TS:Create(spinner, TweenInfo.new(0.5), {CFrame = cframe}):Play()
-- конец функции
end
-- обе двери: 1 — открыть, −1 — закрыть
function closet.MoveDoors(model, direction)
-- левая
closet.MoveSpinner(model.LeftSpinner, direction)
-- правая — в зеркальную сторону
closet.MoveSpinner(model.RightSpinner, -direction)
-- ждём, пока повернутся
task.wait(0.5)
-- конец функции
end
-- запретить или вернуть ходьбу и прыжок
local function Freeze(humanoid, frozen)
-- скорость
humanoid.WalkSpeed = frozen and 0 or SP.CharacterWalkSpeed
-- прыжок (если в игре JumpPower)
humanoid.JumpPower = frozen and 0 or SP.CharacterJumpPower
-- прыжок (если в игре JumpHeight)
humanoid.JumpHeight = frozen and 0 or SP.CharacterJumpHeight
-- конец функции
end
-- игрок прячется
function closet.Enter(player, model)
-- его персонаж
local character = player.Character
-- Humanoid
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
-- персонажа нет или погиб
if not humanoid or humanoid.Health <= 0 then return false end
-- шкаф занят
model.HasPlayer.Value = player
-- стоит на месте
Freeze(humanoid, true)
-- открываем двери
closet.MoveDoors(model, 1)
-- переносим внутрь
character:PivotTo(model.Inside.CFrame)
-- закрываем
closet.MoveDoors(model, -1)
-- спрятался
return true
-- конец функции
end
-- игрок выходит
function closet.Exit(player, model)
-- его персонаж
local character = player.Character
-- Humanoid
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
-- открываем двери
closet.MoveDoors(model, 1)
-- персонаж на месте
if humanoid then
-- переносим наружу
character:PivotTo(model.Outside.CFrame)
-- снова может ходить
Freeze(humanoid, false)
-- конец проверки
end
-- секунду двери открыты
task.wait(1)
-- закрываем
closet.MoveDoors(model, -1)
-- шкаф свободен
model.HasPlayer.Value = nil
-- конец функции
end
-- ставит шкаф на место локатора
function closet.New(sample)
-- копия образца
local model = SS.FurnitureSamples.Closet:Clone()
-- на место локатора
model:PivotTo(sample.CFrame)
-- в комнату
model.Parent = sample.Parent
-- локатор больше не нужен
sample:Destroy()
-- кто сейчас внутри
local hasPlayer = Instance.new("ObjectValue")
-- имя
hasPlayer.Name = "HasPlayer"
-- в шкаф
hasPlayer.Parent = model
-- надпись снаружи
local outsidePrompt = Instance.new("ProximityPrompt")
-- текст
outsidePrompt.ActionText = "Спрятаться"
-- с 5 studs
outsidePrompt.MaxActivationDistance = 5
-- невидимые детали шкафа не мешают
outsidePrompt.RequiresLineOfSight = false
-- перед шкафом
outsidePrompt.Parent = model.Outside
-- надпись внутри
local insidePrompt = outsidePrompt:Clone()
-- текст
insidePrompt.ActionText = "Выйти"
-- только изнутри
insidePrompt.MaxActivationDistance = 3
-- пока внутри никого
insidePrompt.Enabled = false
-- внутри шкафа
insidePrompt.Parent = model.Inside
-- освободить шкаф
local function Free()
-- внутри никого
hasPlayer.Value = nil
-- «Выйти» не нужно
insidePrompt.Enabled = false
-- снова можно спрятаться
outsidePrompt.Enabled = true
-- конец функции
end
-- нажали «Спрятаться»
outsidePrompt.Triggered:Connect(function(player)
-- занято (и тот игрок ещё в игре)
if hasPlayer.Value and hasPlayer.Value.Parent then return end
-- второй не войдёт
outsidePrompt.Enabled = false
-- не получилось
if not closet.Enter(player, model) then
-- шкаф снова свободен
Free()
-- выходим
return
-- конец проверки
end
-- можно выйти
insidePrompt.Enabled = true
-- погиб внутри
player.Character.Humanoid.Died:Once(function()
-- шкаф не останется занятым навсегда
if hasPlayer.Value == player then Free() end
-- конец обработчика
end)
-- конец обработчика
end)
-- нажали «Выйти»
insidePrompt.Triggered:Connect(function(player)
-- выйти может только тот, кто внутри
if hasPlayer.Value ~= player then return end
-- второй раз не нажать
insidePrompt.Enabled = false
-- выходим
closet.Exit(player, model)
-- шкаф свободен
Free()
-- конец обработчика
end)
-- конец функции
end
-- отдаём таблицу модуля
return closetГде: ServerScriptService → MainScript → Room · ModuleScript
Зачем: Ключ в ящике.
-- здесь лежат предметы
local SS = game:GetService("ServerStorage")
-- таблица модуля
local item = {}
-- игрок взял предмет
function item.Interact(player, prompt, handle, itemName)
-- его персонаж
local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
-- персонажа нет
if not humanoid then return end
-- второй раз не взять
prompt.Enabled = false
-- предмет пропадает из ящика
handle:Destroy()
-- это ключ
if itemName == "Key" then
-- копия Tool «Key»
local tool = SS.Items.Key:Clone()
-- в рюкзак
tool.Parent = player.Backpack
-- и сразу в руки
humanoid:EquipTool(tool)
-- конец проверки
end
-- конец функции
end
-- кладёт предмет в ящик
function item.New(location, itemName)
-- образец предмета
local template = SS.Items:FindFirstChild(itemName)
-- нет такого
if not template then return end
-- дно ящика, где точка Location
local bottom = location.Parent
-- только модель, без Tool
local handle = template.Handle:Clone()
-- поедет вместе с ящиком
handle.Anchored = false
-- не мешает ящику двигаться
handle.CanCollide = false
-- ничего не весит
handle.Massless = true
-- в точку Location
handle.CFrame = location.WorldCFrame
-- в ящик
handle.Parent = bottom
-- приварка к дну
local weld = Instance.new("WeldConstraint")
-- предмет
weld.Part0 = handle
-- к дну ящика
weld.Part1 = bottom
-- в предмет
weld.Parent = handle
-- надпись «Взять»
local prompt = Instance.new("ProximityPrompt")
-- текст
prompt.ActionText = "Взять"
-- что берём
prompt.ObjectText = itemName
-- с 5 studs
prompt.MaxActivationDistance = 5
-- мебель не загораживает
prompt.RequiresLineOfSight = false
-- видно, только когда ящик открыт
prompt.Enabled = bottom:GetAttribute("Open") == true
-- на предмете
prompt.Parent = handle
-- ящик открыли или закрыли
bottom:GetAttributeChangedSignal("Open"):Connect(function()
-- предмет ещё здесь — показываем/прячем
if handle.Parent then prompt.Enabled = bottom:GetAttribute("Open") == true end
-- конец обработчика
end)
-- нажали «Взять»
prompt.Triggered:Connect(function(player)
-- отдаём игроку
item.Interact(player, prompt, handle, itemName)
-- конец обработчика
end)
-- конец функции
end
-- отдаём таблицу модуля
return itemГде: 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Где: StarterGui → EndGui → Frame · LocalScript
Зачем: Финальная катсцена.
-- плавные изменения
local TS = game:GetService("TweenService")
-- чёрный экран
local frame = script.Parent
-- надпись
local text = frame:WaitForChild("EndText")
-- камера игрока
local camera = workspace.CurrentCamera
-- сигнал финала
local endEvent = game.ReplicatedStorage:WaitForChild("EndEvent")
-- 2 секунды, ровно
local tweenInfo = TweenInfo.new(2, Enum.EasingStyle.Linear)
-- экран пока прозрачный
frame.BackgroundTransparency = 1
-- текст тоже
text.TextTransparency = 1
-- сервер прислал финал
endEvent.OnClientEvent:Connect(function()
-- точка камеры — берём сейчас, когда комната уже на месте
local cameraPart = workspace.EndRoom:WaitForChild("CameraPart")
-- развернуться к двери
local goal = cameraPart.CFrame * CFrame.Angles(0, math.rad(180), 0)
-- камерой управляет скрипт
camera.CameraType = Enum.CameraType.Scriptable
-- камера плывёт и разворачивается
TS:Create(camera, tweenInfo, {CFrame = goal}):Play()
-- смотрим, как закрывается дверь
task.wait(10)
-- экран темнеет
TS:Create(frame, tweenInfo, {BackgroundTransparency = 0}):Play()
-- чуть позже
task.wait(0.4)
-- появляется надпись
TS:Create(text, tweenInfo, {TextTransparency = 0}):Play()
-- конец обработчика
end)Запусти Play (или Run) и убедись: