Roblox · Doors · урок 4

Ключ и замок

В случайной комнате (шанс 1 из 4) в случайном ящике лежит ключ, а дверь этой комнаты заперта — на ней висит замок. Нашёл ключ — подошёл к двери — замок щёлкает, дверь открывается.

Структура

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

ServerStorage
├─ Door (Model)
│  ├─ Door (Part)
│  │  └─ Unlock (Sound) — щелчок замка
│  └─ Lock (Part) — замок на двери; приварен к Door
└─ Items (Folder)
   └─ Key (Tool)
      └─ Handle (Part) — модель ключа
ServerScriptService
└─ MainScript → Room (ModuleScript)
   └─ Item (ModuleScript)

Шаги

  1. Замок «Lock» (одна деталь) на двери, в модели Door; приварен к полотну WeldConstraint, Anchored выкл.
  2. Звук «Unlock» — в деталь Door рядом с Open.
  3. Ключ: модель → Tool «Key», главная деталь — Handle. Хват — плагином Tool Grip Editor.
  4. ServerStorage → Folder «Items», ключ — туда.
  5. В Room — ModuleScript «Item». Door, Furniture и Room обновились — тексты ниже целиком.
  6. Как работает: Furniture собирает все точки Location комнаты. Room с шансом 1 из 4 кладёт ключ в случайную точку и ставит дверь с замком. «Взять» видно, только когда ящик открыт. Ключ в руках или в рюкзаке открывает дверь.

Скрипты

Item

Где: 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

Door

Где: ServerScriptService → MainScript → Room · ModuleScript
Зачем: Замок: дверь открывается только с ключом.

-- плавный поворот двери
local TS = game:GetService("TweenService")
-- здесь лежит образец двери
local SS = game:GetService("ServerStorage")

-- таблица модуля
local door = {}

-- открывает дверь
function door.Open(doorModel)
    -- отмечаем: открыта
    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()
-- конец функции
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)
            -- готово
            return
        -- конец проверки
        end
        -- есть ли у игрока ключ
        local key = FindKey(character)
        -- есть
        if key then
            -- дверь больше не заперта
            locked = false
            -- ключ тратится
            key:Destroy()
            -- замок исчезает
            doorModel.Lock:Destroy()
            -- щелчок замка
            doorModel.Door.Unlock:Play()
            -- открываем
            door.Open(doorModel)
        -- конец проверки
        end
    -- конец обработчика
    end)
    -- дверь — часть комнаты
    doorModel.Parent = roomModel
    -- отдаём дверь
    return doorModel
-- конец функции
end

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

Furniture

Где: ServerScriptService → MainScript → Room · ModuleScript
Зачем: Отдаёт места внутри ящиков.

-- плавное движение ящиков
local TS = game:GetService("TweenService")
-- здесь лежат образцы мебели
local SS = game:GetService("ServerStorage")
-- на сколько 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
        -- ставим мебель
        local locations = furniture.New(sample)
        -- поставили
        if locations then
            -- её места
            for _, location in ipairs(locations) do
                -- добавляем к местам комнаты
                table.insert(roomLocations, location)
            -- конец перебора
            end
        -- конец проверки
        end
    -- конец перебора
    end
    -- есть куда положить ключ
    if #roomLocations > 0 then return roomLocations end
    -- ящиков нет
    return nil
-- конец функции
end

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

Room

Где: ServerScriptService → MainScript · ModuleScript
Зачем: Иногда прячет ключ и запирает дверь.

-- здесь лежат образцы комнат
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

-- можно ли поставить эту комнату следующей
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
Зачем: Стартовая дверь — без замка.

-- модуль комнат
local room = require(script.Room)
-- модуль дверей
local door = require(script.Room.Door)
-- сколько комнат вместе со стартовой
local ROOMS = 10

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

Проверка

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