Roblox · Тайкун · урок 9

Общий: вся игра целиком

Итог курса на одной странице: полная структура тайкуна (у каждого игрока своё производство из шаблона) и финальные версии всех скриптов — без «добавь строку» и «замени блок». Удобно сверить свою игру или собрать её заново.

Структура

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

ServerStorage
├─ TycoonTemplate (Model) — шаблон производства, PrimaryPart = Floor
│  ├─ Decoration_Items (Folder)
│  │  ├─ Floor (Part) — пол
│  │  ├─ Walls (Part) — стены
│  │  ├─ Door (Part) — полупрозрачная, CanCollide выкл.
│  │  │  └─ OwnerGui (SurfaceGui) — OwnedBy, Owner (TextLabel)
│  │  └─ FortuneWheel (Model)
│  │     ├─ Wheel (Part) — цилиндр: Decal, ClickDetector, FortuneWheel (Script)
│  │     └─ Line (Part) — стрелка
│  ├─ CharSpawn (Part) — у входа, прозрачный, CanCollide выкл.
│  ├─ Values (Folder)
│  │  ├─ Owner (ObjectValue) — владелец
│  │  └─ Gain (NumberValue) — касса
│  ├─ Dropped_Items (Folder)
│  ├─ Owned_Items (Folder) — пусто до покупки
│  └─ Buttons (Folder)
│     ├─ BuyButtons (Folder) — BuyScript
│     │  └─ CandyDropper, CandyUpgrader, Garland (Part) — ClickDetector, Price, Conveyor (ObjectValue), CostGui
│     ├─ UpgradeButtons (Folder)
│     │  └─ CandyDropperUpgrade (Part) — ClickDetector → UpgradeDropperScript, Price, Conveyor, CostGui
│     └─ GainButtons (Folder)
│        └─ CollectGainButton (Model) — GainScreen → GainScreenGui → GainText; GainButton → ClickDetector → CollectGainScript, TakeGui
├─ ItemsToDrop (Folder)
│  └─ Candy (Part) — Anchored выкл.; Gain (NumberValue)
├─ Conveyors (Folder) — образцы покупок — на тех же местах, что в шаблоне
│  ├─ CandyDropper (Model) — Conveyor, SpawnPart, Catcher, Upgrade, CandyDropperScript (Rate, GainBonus)
│  ├─ CandyUpgrader (Model) — Upgrader → UpgradeCandyScript
│  └─ Garland (Model) — гирлянда
└─ TycoonSpawn (Part) — образец платформы
Workspace
├─ TycoonSpawns (Folder) — свободные платформы
└─ Tycoons (Folder) — производства игроков (создаёт CreatingTycoon)
ServerScriptService
└─ Stats, CreatingTycoon, PassPurchaseServer (Script)
StarterGui
├─ MoneyGui (ScreenGui) — MoneyFrame → Money → UpdateMoneyLocal
└─ PassGui (ScreenGui) — OpenCloseButton (OpenCloseLocal), PassFrame → CandyMultiplier (PassPurchaseLocal)
Players → игрок (во время игры) — создаёт Stats
└─ leaderstats → Money, GainIndustry, ItemsFolder, HasPass

Шаги

  1. Сверяй сверху вниз: сначала Explorer со «Структурой», потом каждый скрипт целиком.
  2. Всё производство — шаблон TycoonTemplate в ServerStorage (PrimaryPart = Floor). Сам шаблон не работает: CreatingTycoon ставит его копию на свободную платформу для каждого игрока.
  3. Образцы покупок (Conveyors) стоят на тех же местах, что и в шаблоне: покупка встаёт в то же место относительно пола.
  4. Имена связывают всё: кнопка в BuyButtons = предмет в Conveyors (CandyDropper, CandyUpgrader, Garland) — это нужно и для сохранения.
  5. OwnerScript в двери из первого урока больше не нужен — владельца сразу записывает CreatingTycoon.
  6. Платформы того же размера, что Floor; Game Settings → Places → Max Players = число платформ.
  7. Сохранение — после публикации и Game Settings → Security → Enable Studio Access to API Services. PASS_ID — ID своего пропуска в обоих скриптах.

Скрипты

Stats

Где: ServerScriptService · Script
Зачем: Кошелёк, список покупок, загрузка и сохранение.

-- сервис сохранений
local DSS = game:GetService("DataStoreService")
-- наше хранилище
local store = DSS:GetDataStore("TycoonData")
-- серверное хранилище
local SS = game:GetService("ServerStorage")
-- пол шаблона — от него отсчитываем места предметов
local templateFloor = SS:WaitForChild("TycoonTemplate").Decoration_Items.Floor

-- сохраняет данные игрока
local function Save(player)
    -- данные не загрузились — не затираем сохранение нулями
    if not player:GetAttribute("DataLoaded") then return end
    -- список купленного
    local items = {}
    -- всё, что игрок купил
    for _, item in pairs(player.ItemsFolder:GetChildren()) do
        -- запоминаем имя
        table.insert(items, item.Name)
    -- конец перебора
    end
    -- кошелёк, касса, покупки
    local data = {Money = player.leaderstats.Money.Value, Gain = player.GainIndustry.Value, Items = items}
    -- защищённый вызов: сеть может подвести
    local success, err = pcall(function()
        -- записываем по ID игрока
        store:SetAsync(player.UserId, data)
    -- конец защищённого вызова
    end)
    -- не получилось — пишем причину в Output
    if not success then warn(err) end
-- конец функции
end

-- игрок зашёл
game.Players.PlayerAdded:Connect(function(player)
    -- папка для таблицы лидеров
    local leaderstats = Instance.new("Folder", player)
    -- имя, которое понимает Roblox
    leaderstats.Name = "leaderstats"
    -- кошелёк
    local money = Instance.new("NumberValue", leaderstats)
    -- называем Money
    money.Name = "Money"
    -- копия кассы — чтобы сохранить её при выходе
    local gainIndustry = Instance.new("NumberValue", player)
    -- называем GainIndustry
    gainIndustry.Name = "GainIndustry"
    -- список покупок игрока
    local itemsFolder = Instance.new("Folder", player)
    -- называем ItemsFolder
    itemsFolder.Name = "ItemsFolder"
    -- ждём, пока CreatingTycoon поставит производство
    local tycoon = workspace.Tycoons:WaitForChild(player.Name, 10)
    -- свободного места не было — сохранять нечего
    if not tycoon then return end
    -- купленное в его производстве
    local ownedItems = tycoon.Owned_Items
    -- его кнопки покупки
    local buttons = tycoon.Buttons.BuyButtons
    -- пол его производства
    local floor = tycoon.Decoration_Items.Floor
    -- что-то куплено
    ownedItems.ChildAdded:Connect(function(child)
        -- записываем в список покупок
        local item = Instance.new("StringValue", itemsFolder)
        -- по имени предмета
        item.Name = child.Name
    -- конец обработчика
    end)

    -- защищённый вызов
    local success, data = pcall(function()
        -- читаем сохранение
        return store:GetAsync(player.UserId)
    -- конец защищённого вызова
    end)
    -- прочитали — при выходе можно сохранять
    if success then player:SetAttribute("DataLoaded", true) end
    -- прочитали, и сохранение есть (у нового игрока его нет)
    if success and data then
        -- кошелёк
        money.Value = data.Money or 0
        -- касса
        gainIndustry.Value = data.Gain or 0
        -- возвращаем деньги в кассу производства
        tycoon.Values.Gain.Value = gainIndustry.Value
        -- все покупки
        for _, name in pairs(data.Items or {}) do
            -- образец в ServerStorage
            local sample = SS.Conveyors:FindFirstChild(name)
            -- образец есть и ещё не стоит
            if sample and not ownedItems:FindFirstChild(name) then
                -- копия
                local item = sample:Clone()
                -- то же место относительно пола, что в шаблоне
                item:PivotTo(floor.CFrame * templateFloor.CFrame:ToObjectSpace(sample:GetPivot()))
                -- ставим в производство
                item.Parent = ownedItems
                -- кнопка с тем же именем
                local button = buttons:FindFirstChild(name)
                -- уже куплено — кнопку убираем
                if button then button:Destroy() end
            -- конец проверки
            end
        -- конец перебора
        end
    -- сервис недоступен
    elseif not success then
        -- причина — в Output
        warn(data)
    -- конец проверки
    end
-- конец обработчика
end)

-- игрок вышел — сохраняем
game.Players.PlayerRemoving:Connect(Save)
-- сервер выключается (и Stop в Studio)
game:BindToClose(function()
    -- всех, кто ещё в игре
    for _, player in pairs(game.Players:GetPlayers()) do
        -- сохраняем
        Save(player)
    -- конец перебора
    end
-- конец функции
end)

CreatingTycoon

Где: ServerScriptService · Script
Зачем: Производство каждому игроку на свободной платформе.

-- серверное хранилище
local SS = game:GetService("ServerStorage")
-- шаблон производства (бывший IndustryModel)
local template = SS:WaitForChild("TycoonTemplate")
-- свободные платформы
local spawns = workspace:WaitForChild("TycoonSpawns")
-- производства игроков
local tycoons = workspace:WaitForChild("Tycoons")

-- ставит персонажа у входа в его производство
local function MoveCharacter(character, tycoon)
    -- ждём, пока персонаж загрузится
    character:WaitForChild("HumanoidRootPart")
    -- кадр: Roblox сначала сам ставит персонажа на SpawnLocation
    task.wait()
    -- переносим к CharSpawn, чуть выше пола
    character:PivotTo(tycoon.CharSpawn.CFrame + Vector3.new(0, 3, 0))
-- конец функции
end

-- игрок зашёл
game.Players.PlayerAdded:Connect(function(player)
    -- список свободных платформ
    local free = spawns:GetChildren()
    -- мест нет
    if #free == 0 then
        -- Max Players больше, чем платформ
        warn("Нет свободных платформ")
        -- производство не создаём
        return
    -- конец проверки
    end
    -- случайная свободная платформа
    local platform = free[math.random(1, #free)]
    -- копия шаблона
    local tycoon = template:Clone()
    -- производство называется именем игрока
    tycoon.Name = player.Name
    -- пол производства — точно на место платформы
    tycoon:PivotTo(platform.CFrame)
    -- владелец — сразу
    tycoon.Values.Owner.Value = player
    -- имя на двери
    tycoon.Decoration_Items.Door.OwnerGui.Owner.Text = player.Name
    -- платформа занята — убираем
    platform:Destroy()
    -- ставим в мир — скрипты внутри начинают работать
    tycoon.Parent = tycoons
    -- каждое появление персонажа
    player.CharacterAdded:Connect(function(character)
        -- у своего производства
        MoveCharacter(character, tycoon)
    -- конец обработчика
    end)
    -- персонаж появился раньше, чем мы подключились
    if player.Character then
        -- переносим и его
        MoveCharacter(player.Character, tycoon)
    -- конец проверки
    end
-- конец обработчика
end)

-- игрок вышел
game.Players.PlayerRemoving:Connect(function(player)
    -- его производство
    local tycoon = tycoons:FindFirstChild(player.Name)
    -- производства не было
    if not tycoon then return end
    -- новая свободная платформа
    local platform = SS.TycoonSpawn:Clone()
    -- на место пола производства
    platform.CFrame = tycoon.Decoration_Items.Floor.CFrame
    -- снова в списке свободных
    platform.Parent = spawns
    -- производство убираем
    tycoon:Destroy()
-- конец обработчика
end)

PassPurchaseServer

Где: ServerScriptService · Script
Зачем: Флажок HasPass — двойной доход.

-- сервис покупок
local MpS = game:GetService("MarketplaceService")
-- вставь тот же ID пропуска
local PASS_ID = 0

-- проверяет, куплен ли пропуск раньше
local function CheckPass(player)
    -- защищённый вызов: сеть может подвести
    local success, owns = pcall(function()
        -- есть ли пропуск у игрока
        return MpS:UserOwnsGamePassAsync(player.UserId, PASS_ID)
    -- конец защищённого вызова
    end)
    -- проверка прошла и пропуск есть
    if success and owns then
        -- включаем бонус
        player.HasPass.Value = true
    -- сервис недоступен
    elseif not success then
        -- причина — в Output
        warn(owns)
    -- конец проверки
    end
-- конец функции
end

-- игрок зашёл
game.Players.PlayerAdded:Connect(function(player)
    -- флажок «пропуск куплен»
    local hasPass = Instance.new("BoolValue", player)
    -- называем HasPass
    hasPass.Name = "HasPass"
    -- покупал раньше — бонус сразу
    CheckPass(player)
-- конец обработчика
end)

-- окно покупки закрылось
MpS.PromptGamePassPurchaseFinished:Connect(function(player, passId, purchased)
    -- игрок действительно купил этот пропуск
    if purchased and passId == PASS_ID then
        -- бонус с этой секунды
        player.HasPass.Value = true
    -- конец проверки
    end
-- конец обработчика
end)

BuyScript

Где: ServerStorage → TycoonTemplate → Buttons → BuyButtons · Script
Зачем: Покупка оборудования и украшений.

-- папка BuyButtons
local buttons = script.Parent
-- производство: BuyButtons → Buttons → производство игрока
local tycoon = buttons.Parent.Parent
-- владелец
local valuesFolder = tycoon.Values
-- куда ставим купленное
local ownedFolder = tycoon.Owned_Items
-- серверное хранилище
local SS = game:GetService("ServerStorage")
-- пол шаблона
local templateFloor = SS:WaitForChild("TycoonTemplate").Decoration_Items.Floor
-- пол этого производства
local floor = tycoon.Decoration_Items.Floor

-- все кнопки покупки
for _, button in pairs(buttons:GetChildren()) do
    -- только парты-кнопки
    if button:IsA("BasePart") then
        -- на кнопку нажали
        button.ClickDetector.MouseClick:Connect(function(player)
            -- нажал владелец, и денег хватает
            if valuesFolder.Owner.Value == player and player.leaderstats.Money.Value >= button.Price.Value then
                -- списываем цену
                player.leaderstats.Money.Value -= button.Price.Value
                -- образец в ServerStorage.Conveyors
                local sample = button.Conveyor.Value
                -- копия
                local item = sample:Clone()
                -- то же место относительно пола, что в шаблоне
                item:PivotTo(floor.CFrame * templateFloor.CFrame:ToObjectSpace(sample:GetPivot()))
                -- ставим в производство
                item.Parent = ownedFolder
                -- кнопку убираем
                button:Destroy()
            -- конец проверки
            end
        -- конец обработчика
        end)
    -- конец проверки типа
    end
-- конец перебора
end

UpgradeDropperScript

Где: … → UpgradeButtons → CandyDropperUpgrade → ClickDetector · Script
Зачем: Улучшение конвейера.

-- ClickDetector кнопки улучшения
local clicker = script.Parent
-- сама кнопка
local button = clicker.Parent
-- производство: UpgradeButtons → Buttons → IndustryModel
local tycoon = button.Parent.Parent.Parent
-- владелец и касса
local valuesFolder = tycoon.Values
-- купленное оборудование
local ownedFolder = tycoon.Owned_Items

-- нажали на кнопку улучшения
clicker.MouseClick:Connect(function(player)
    -- ищем купленный конвейер с этим именем
    local conveyor = ownedFolder:FindFirstChild(button.Conveyor.Value.Name)
    -- конвейер ещё не куплен — улучшать нечего
    if not conveyor then return end
    -- нажал не владелец
    if valuesFolder.Owner.Value ~= player then return end
    -- денег не хватает
    if player.leaderstats.Money.Value < button.Price.Value then return end
    -- списываем цену
    player.leaderstats.Money.Value -= button.Price.Value
    -- предметы чаще: раз в 3 секунды
    conveyor.CandyDropperScript.Rate.Value = 3
    -- каждый предмет дороже на 5
    conveyor.CandyDropperScript.GainBonus.Value += 5
    -- все детали визуального улучшения
    for _, part in pairs(conveyor.Upgrade:GetDescendants()) do
        -- только физические
        if part:IsA("BasePart") then
            -- делаем видимыми
            part.Transparency = 0
        -- конец проверки
        end
    -- конец перебора
    end
    -- улучшение куплено — кнопка больше не нужна
    button:Destroy()
-- конец обработчика
end)

CollectGainScript

Где: … → GainButtons → CollectGainButton → GainButton → ClickDetector · Script
Зачем: Касса → кошелёк, экран кассы.

-- ClickDetector кнопки кассы
local clicker = script.Parent
-- экран: ClickDetector → GainButton → CollectGainButton
local screen = clicker.Parent.Parent.GainScreen
-- Values: CollectGainButton → GainButtons → Buttons → IndustryModel
local valuesFolder = screen.Parent.Parent.Parent.Parent.Values

-- нажали на кнопку кассы
clicker.MouseClick:Connect(function(player)
    -- нажал владелец
    if valuesFolder.Owner.Value == player then
        -- сколько в кассе
        local gain = valuesFolder.Gain.Value
        -- сначала обнуляем — двойной клик не удвоит деньги
        valuesFolder.Gain.Value = 0
        -- переводим деньги в кошелёк
        player.leaderstats.Money.Value += gain
    -- конец проверки
    end
-- конец обработчика нажатия
end)

-- сумма в кассе изменилась
valuesFolder.Gain.Changed:Connect(function(newGain)
    -- показываем её на экране
    screen.GainScreenGui.GainText.Text = newGain .. " $"
    -- владелец
    local owner = valuesFolder.Owner.Value
    -- владелец есть и у него есть копия кассы
    if owner and owner:FindFirstChild("GainIndustry") then
        -- обновляем копию — её сохранит Stats
        owner.GainIndustry.Value = newGain
    -- конец проверки
    end
-- конец обработчика
end)

FortuneWheel

Где: … → Decoration_Items → FortuneWheel → Wheel · Script
Зачем: Колесо фортуны.

-- цилиндр колеса
local wheel = script.Parent
-- клик по колесу
local clicker = wheel:WaitForChild("ClickDetector")
-- плавная анимация
local TS = game:GetService("TweenService")
-- выигрыши по секторам, по кругу от верхнего
local rewards = {10, 2, 15, 5, 20, 7, 10, 5}
-- угол одного сектора
local sector = 360 / #rewards
-- колесо крутится?
local spinning = false
-- сколько градусов колесо уже повернулось за всё время
local turned = 0

-- кликнули по колесу
clicker.MouseClick:Connect(function(player)
    -- уже крутится — ждём
    if spinning then return end
    -- производство игрока
    local tycoon = workspace.Tycoons:FindFirstChild(player.Name)
    -- производства нет — выигрыш класть некуда
    if not tycoon then return end
    -- крутим
    spinning = true
    -- заранее выбираем выигрыш
    local rewardIndex = math.random(1, #rewards)
    -- угол этого сектора, не на границе
    local angle = (rewardIndex - 1) * sector + (math.random() - 0.5) * sector * 0.8
    -- вперёд на 3–8 оборотов и до нужного сектора
    local target = turned - turned % 360 + 360 * math.random(3, 8) + angle
    -- запоминаем, где остановится
    turned = target
    -- 3–6 секунд, к концу замедляется
    local tweenInfo = TweenInfo.new(math.random(3, 6), Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
    -- поворот вокруг оси X
    local tween = TS:Create(wheel, tweenInfo, {Rotation = Vector3.new(target, 0, 0)})
    -- крутим
    tween:Play()
    -- ждём остановки
    tween.Completed:Wait()
    -- выигрыш — в кассу игрока
    tycoon.Values.Gain.Value += rewards[rewardIndex]
    -- можно крутить снова
    spinning = false
-- конец обработчика
end)

CandyDropperScript

Где: ServerStorage → Conveyors → CandyDropper · Script
Зачем: Конфеты на ленте, Rate, GainBonus, ×2 с пропуском.

-- модель конвейера
local candyDropper = script.Parent
-- производство: CandyDropper → Owned_Items → IndustryModel
local tycoon = candyDropper.Parent.Parent
-- папка для упавших предметов
local droppedFolder = tycoon.Dropped_Items
-- касса производства
local gainValue = tycoon.Values.Gain
-- точка, где появляется предмет
local spawnPart = candyDropper.SpawnPart
-- корзина в конце ленты
local catcher = candyDropper:WaitForChild("Catcher")
-- серверное хранилище
local SS = game:GetService("ServerStorage")
-- образец предмета
local candy = SS.ItemsToDrop:WaitForChild("Candy")
-- удаление по таймеру
local Debris = game:GetService("Debris")
-- секунд между предметами
local rate = script.Rate
-- надбавка к цене каждого предмета
local gainBonus = script.GainBonus

-- ×2, если у владельца куплен пропуск
local function PassMultiplier()
    -- владелец — узнаём в момент оплаты, он мог смениться
    local owner = tycoon.Values.Owner.Value
    -- владелец есть и пропуск у него куплен
    if owner and owner:FindFirstChild("HasPass") and owner.HasPass.Value then
        -- двойной доход
        return 2
    -- конец проверки
    end
    -- обычный доход
    return 1
-- конец функции
end

-- каждые Rate секунд (меняется улучшением)
while task.wait(rate.Value) do
    -- копируем предмет
    local candyClone = candy:Clone()
    -- ставим над лентой
    candyClone.Position = spawnPart.Position
    -- кладём в мир
    candyClone.Parent = droppedFolder
    -- упал мимо ленты — сам исчезнет через 30 с
    Debris:AddItem(candyClone, 30)
    -- за этот предмет ещё не платили
    local paid = false
    -- следим за касаниями предмета
    candyClone.Touched:Connect(function(otherPart)
        -- предмет в корзине, оплаты ещё не было
        if otherPart == catcher and not paid then
            -- платим один раз
            paid = true
            -- цена предмета — в кассу
            gainValue.Value += candyClone.Gain.Value * PassMultiplier() + gainBonus.Value
            -- пауза 1 секунда
            task.wait(1)
            -- удаляем предмет
            candyClone:Destroy()
        -- конец проверки
        end
    -- конец обработчика касания
    end)
-- следующий предмет
end

UpgradeCandyScript

Где: ServerStorage → Conveyors → CandyUpgrader → Upgrader · Script
Зачем: Рамка: конфета дороже на 5.

-- рамка, через которую проезжает предмет
local upgrader = script.Parent

-- рамки что-то коснулось
upgrader.Touched:Connect(function(otherPart)
    -- это конфета, и её ещё не улучшали
    if otherPart.Name == "Candy" and not otherPart:GetAttribute("Upgraded") then
        -- метка на самой конфете: второй раз не улучшим
        otherPart:SetAttribute("Upgraded", true)
        -- конфета стала дороже на 5
        otherPart.Gain.Value += 5
    -- конец проверки
    end
-- конец обработчика
end)

UpdateMoneyLocal

Где: StarterGui → MoneyGui → MoneyFrame → Money · LocalScript
Зачем: Деньги на экране.

-- текстовое поле Money
local moneyText = script.Parent
-- игрок, у которого открыт этот экран
local player = game.Players.LocalPlayer
-- ждём, пока сервер создаст кошелёк
local money = player:WaitForChild("leaderstats"):WaitForChild("Money")

-- функция обновления надписи
local function UpdateMoney(newMoney)
    -- показываем сумму со знаком $
    moneyText.Text = newMoney .. " $"
-- конец функции
end

-- сразу показываем текущий баланс
UpdateMoney(money.Value)
-- обновляем надпись при каждом изменении денег
money.Changed:Connect(UpdateMoney)

OpenCloseLocal

Где: StarterGui → PassGui → OpenCloseButton · LocalScript
Зачем: Окно пропусков.

-- кнопка «Open Passes»
local button = script.Parent
-- окно пропусков
local frame = button.Parent:WaitForChild("PassFrame")

-- нажали
button.Activated:Connect(function()
    -- было скрыто — показываем, и наоборот
    frame.Visible = not frame.Visible
    -- окно открыто
    if frame.Visible then
        -- кнопка закроет
        button.Text = "Close Passes"
    -- окно закрыто
    else
        -- кнопка откроет
        button.Text = "Open Passes"
    -- конец проверки
    end
-- конец обработчика
end)

PassPurchaseLocal

Где: StarterGui → PassGui → PassFrame → CandyMultiplier · LocalScript
Зачем: Окно покупки пропуска.

-- сервис покупок
local MpS = game:GetService("MarketplaceService")
-- кнопка пропуска
local button = script.Parent
-- этот игрок
local player = game.Players.LocalPlayer
-- вставь ID своего пропуска
local PASS_ID = 0

-- нажали на пропуск
button.Activated:Connect(function()
    -- окно покупки Roblox; результат узнает сервер
    MpS:PromptGamePassPurchase(player, PASS_ID)
-- конец обработчика
end)

Проверка

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

Собрать всю игру одной командой