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
Где: 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)Где: 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)Где: 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)Где: 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Где: … → 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)Где: … → 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)Где: … → 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)Где: 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Где: 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)Где: 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)Где: 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)Где: 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) и убедись: