Внутриигровая экономика Roblox
Roblox
Продолжение курса Разработка на Roblox: здесь — транзакции, проектирование магазина и полный практический гайд. Базовая архитектура клиент–сервер и RemoteEvent разобраны в основной статье.
Если вы ещё не собирали магазин в игре, начните с практикума "обби" (внутриигровая валюта и Developer Product). Затем возвращайтесь сюда за архитектурой и идемпотентностью транзакций.
Транзакции и сервер
Что такое транзакция?
Транзакция — операция, которая должна быть выполнена целиком или не выполнена вовсе.
Пример: "списать 100 монет и выдать меч".
Если списание прошло, а выдача — нет → игрок потерял деньги → недоверие.
Почему сервер — источник истины?
| Операция | Клиентская проверка | Серверная проверка |
|---|---|---|
| Проверка баланса | if balance >= 100 |
CurrencyManager:GetBalance(player) >= 100 |
| Списание | balance -= 100 |
CurrencyManager:SpendCoins(player, 100) |
| Выдача предмета | inventory:add("Sword") |
InventorySystem:UnlockItem(player, "Sword") |
Проблема клиента:
- Любой может открыть DevConsole (F9) и выполнить:
game.Players.LocalPlayer.leaderstats.Coins.Value = 999999
- Или заменить цену в GUI-скрипте.
Сервер защищён: клиент может попросить, но решает сервер.
Как проходит покупка?
Рассмотрим покупку меча за 500 монет:
| Этап | Клиент | Сервер | Платформа |
|---|---|---|---|
| 1. Показ | Отображает кнопку "Купить (500 монет)" | — | — |
| 2. Инициация | BuyEvent:FireServer("Sword", txId) |
Получает запрос | — |
| 3. Валидация | — | Проверяет: — txId уникален? — предмет существует? — баланс ≥ 500? |
— |
| 4. Исполнение | — | 1. Списывает 500 монет; 2. Добавляет меч в Inventory; 3. Сохраняет в DataStore |
— |
| 5. Подтверждение | Получает BuyEvent.OnClientEvent("Success") |
— | — |
| 6. Отображение | Обновляет GUI, показывает эффект | — | — |
Если на шаге 4 произошла ошибка → сервер возвращает "Failed" → клиент не меняет баланс (он запрашивает актуальный после ответа).
🔁 При повторной отправке с тем же
txIdсервер отклонит запрос как дубль — это и есть идемпотентность.
Система магазина и монет в Roblox Studio
Внутриигровая экономика — один из ключевых элементов современных игровых проектов, особенно в многопользовательских и сервисных играх, ориентированных на длительное вовлечение. Roblox, как платформа, предоставляет разработчикам инструменты для построения собственных экономических систем, включая магазины, валюту, транзакции и интеграцию с платёжными средствами. Однако, несмотря на высокий уровень абстракции, которую предлагает движок, понимание принципов построения таких систем требует системного подхода: от проектирования логики до обеспечения безопасности и масштабируемости.
Настоящий раздел посвящён реализации магазина в Roblox Studio, и архитектурному осмыслению того, что представляет собой внутриигровой магазин как подсистема, как он интегрируется в общую структуру проекта, и какие ограничения и возможности накладывает платформа Roblox на разработчика. Мы последовательно разберём концепции, их взаимосвязи и реализацию, при этом сохраняя акцент на устойчивости, читаемости кода и перспективе дальнейшего развития проекта.
1. Что такое внутриигровой магазин
Внутриигровой магазин — это программная подсистема, которая реализует функции выбора, приобретения и выдачи игровых активов (предметов, улучшений, скинов, способностей и т.д.) в обмен на одну или несколько форм внутриигровой валюты. Магазин — это целостный сервис, включающий:
- Каталог товаров — структура данных, описывающая доступные предметы, их стоимость, условия приобретения и эффекты после покупки;
- Систему валюты — механизм учёта баланса игрока, валидации транзакций и обновления данных;
- Механизмы выдачи и применения — логика, которая реагирует на успешную транзакцию и производит соответствующие изменения в состоянии игры (например, надевает скин, открывает уровень, выдаёт инвентарь);
- Интерфейс взаимодействия — визуальный и поведенческий слой, через который игрок взаимодействует с магазином;
- Систему сохранения и синхронизации — способ гарантированного сохранения состояния покупок между сессиями и при переходе между серверами.
Важнейшее свойство хорошего магазина — идемпотентность операций. Это означает, что повторное выполнение одной и той же операции (например, повторная отправка запроса на покупку) не приводит к нежелательным последствиям, таким как многократное списание валюты или дублирование предмета. В распределённых системах, какими являются игры на Roblox, такая устойчивость к сетевым артефактам критична.
Подсистема магазина должна быть изолирована от основной игровой логики. Это достигается за счёт чёткого разделения ответственности: модуль магазина не должен напрямую менять состояние персонажа или мира, а только отправлять события или вызывать строго определённые методы других систем (например, через шину событий или менеджер инвентаря). Такой подход обеспечивает тестируемость, модульность и упрощает дальнейшее расширение функционала.
2. Что такое магазин и монеты в Roblox — платформенные особенности
Roblox предоставляет две основные формы валюты — Robux — официальная платёжная валюта платформы, приобретаемая за реальные деньги, и внутриигровые валюты — пользовательские единицы, создаваемые разработчиком (например, "золото", "кристаллы", "очки опыта"). Эти два типа валют принципиально различаются по происхождению, способу управления и правовым последствиям.
2.1. Robux и Developer Products
Robux — это валюта, принадлежащая платформе. Разработчик не может напрямую управлять балансом Robux игрока. Вместо этого используется механизм Developer Products — зарегистрированных в системе DevHub цифровых товаров, привязанных к конкретной игре. При покупке такого товара Roblox обрабатывает платёж, списывает Robux с аккаунта игрока и отправляет подтверждённое событие в игру через MarketplaceService:PromptPurchase() и последующий MarketplaceService.PromptPurchaseFinished.
Ключевые особенности:
- Все транзакции с Robux проходят через серверы Roblox — клиентская сторона не может инициировать списание напрямую.
- Разработчик получает уведомление о покупке асинхронно, и только после получения подтверждения от сервера может выдать предмет.
- Поддерживается механизм возврата (refund) — в течение определённого времени игрок может отменить покупку, и разработчик обязан корректно обработать отмену (например, отозвать предмет).
- Developer Products неизменяемы после публикации: нельзя изменить цену или название без создания нового продукта. Это требует тщательного планирования при запуске проекта.
Таким образом, магазин, использующий Robux, всегда является гибридной системой — клиент отображает интерфейс, сервер Roblox обрабатывает платёж, а сервер игры (Game Server) — выдаёт награду.
2.2. Внутриигровые валюты
Любая валюта, не являющаяся Robux (например, "монеты", "жетоны"), полностью управляется разработчиком. Её баланс хранится в DataStore, в плейерских атрибутах (Player:SetAttribute / GetAttribute) или в пользовательских объектах (например, Folder в PlayerGui или Backpack), но только в рамках сессии — для постоянного хранения обязателен DataStore.
Особенности:
- Разработчик сам определяет правила начисления (за уровень, за задание, за время) и расходования.
- Отсутствует встроенная защита от мошенничества: клиентская часть может быть взломана, если логика проверки баланса реализована только на клиенте.
- Нет автоматической интеграции с платёжными системами: конвертация реальных денег в такую валюту возможна только через Developer Product ("купить 1000 монет за 50 Robux").
Важно подчеркнуть: никакая внутриигровая валюта не может быть обменена на Robux игроком — это нарушает условия использования платформы. Обратный обмен (Robux → внутриигровая валюта) разрешён и является стандартной практикой.
3. Монетизация в Roblox и как разработчики получают доход
Монетизация в Roblox строится вокруг нескольких официальных каналов, каждый из которых требует соблюдения политик и технических требований платформы.
3.1. Developer Products (одноразовые покупки)
Это основной способ продажи цифровых товаров — скины, улучшения, внутриигровая валюта, косметика. Разработчик создаёт продукт в DevHub, задаёт цену в Robux, привязывает его к игре и обрабатывает событие покупки в коде. Roblox берёт комиссию ~30% (в зависимости от условий партнёрской программы и типа аккаунта — individual vs group).
3.2. Game Passes (игровые пропуска)
Game Pass — это разовая покупка, дающая постоянное преимущество — доступ к эксклюзивному контенту, бонусам, привилегиям (например, двойной опыт, уникальный персонаж). Game Pass привязан к аккаунту игрока и действует во всех сессиях игры. Технически проверка наличия Game Pass осуществляется через GamePassService:UserOwnsGamePassAsync().
Преимущество Game Pass перед Developer Product — в постоянстве эффекта и простоте проверки. Недостаток — невозможность динамического изменения функционала после выпуска (за исключением программной логики, активируемой наличием пропуска).
3.3. Premium Payouts (доход от подписки Roblox Premium)
Разработчики получают долю от времени, проведённого подписчиками Premium в их игре, через систему Engagement-Based Payouts. Это пассивный доход, не требующий реализации магазина, но зависящий от удержания аудитории. Расчёт производится ежемесячно и зависит от доли активного времени подписчиков в конкретной игре относительно всего экосистемного времени.
3.4. Виртуальные товары и пользовательский контент (UGC)
С 2023 года Roblox активно развивает систему UGC (User-Generated Content) — пользователи могут создавать и продавать свои ассеты (одежда, аксессуары, анимации), а разработчики — интегрировать их в игру через официальные API. Это создаёт дополнительные возможности для монетизации через комиссии и кураторство.
Важное ограничение: все платёжные операции должны проходить через официальные сервисы Roblox. Попытки организовать сторонние платежи (например, через Telegram-бота или внешний сайт) ведут к бану игры и аккаунта.
4. Как создать свою систему магазина и монет в Roblox Studio
Построение собственной экономической системы в Roblox требует технической реализации и проектирования архитектуры, устойчивой к ошибкам, мошенничеству и изменениям в требованиях. Ниже рассмотрены ключевые компоненты такой системы — от концептуальной модели до кодовой структуры. Мы будем избегать "быстрых решений" вроде скриптов в StarterGui, ориентируясь на промышленные практики разработки — разделение ответственности, защита от клиентских атак, восстанавливаемость состояния и поддерживаемость.
4.1. Архитектурные принципы
Любая устойчивая система магазина в Roblox должна соответствовать следующим принципам:
-
Сервер — единственный источник истины.
Вся логика, связанная с проверкой баланса, списанием валюты и выдачей предметов, должна выполняться на сервере (ServerScriptServiceилиReplicatedStorage). Клиент (StarterGui,LocalScript) может инициировать запрос, но не принимать решение о допустимости операции. -
Полная изоляция данных.
Данные об инвентаре, валюте и истории покупок хранятся вDataStore, а не вPlayerGuiилиBackpack. Временные копии могут быть на клиенте для отображения, но они всегда должны синхронизироваться с сервером и считаться недостоверными до подтверждения. -
Событийная модель взаимодействия.
Клиент отправляет запросы. Сервер обрабатывает их, валидирует, и при успехе отправляет подтверждение или отказ. Это предотвращает race-conditions и обеспечивает предсказуемость. -
Идемпотентность транзакций.
Каждая покупка должна иметь уникальный идентификатор (например,transactionId = os.time() .. "_" .. playerId). При повторной отправке запроса с тем же ID сервер не должен выполнять операцию дважды. Это защищает от сетевых дублей и намеренных атак. -
Отказоустойчивость при сохранении.
Операция "списать валюту и выдать предмет" должна быть атомарной: либо оба действия выполнены, либо ни одно. Для этого применяется паттерн двухфазного сохранения:- Сервер временно блокирует баланс игрока в памяти.
- Выполняет выдачу предмета (например, добавляет в
PlayerData.Inventory). - Выполняет сохранение в
DataStore. - При успехе — фиксирует списание; при ошибке — откатывает изменения.
4.2. Проектирование структуры данных
Перед написанием кода необходимо определить, как будут храниться и передаваться данные. Это влияет на масштабируемость и удобство отладки.
4.2.1. Структура каталога товаров
Каталог товаров — это статическая или полу-статическая конфигурация. Лучше всего её хранить в ReplicatedStorage в виде ModuleScript, например:
Обратите внимание:
- Цена может быть указана как в пользовательской валюте (
Coins), так и через ссылку наDeveloperProduct(по ID или имени). - Все метаданные предмета — строго структурированы. Это позволяет системе выдачи интерпретировать их без жёсткой привязки к конкретным скриптам.
- Каталог не содержит информации о наличии у игрока — только описание товара.
4.2.2. Структура данных игрока
Данные игрока (PlayerData) хранятся в DataStore и должны включать:
{
Coins = 1250,
Inventory = {
["Sword_001"] = { Count = 1, Equipped = true },
["Skin_Warrior_Red"] = { Unlocked = true }
},
PurchaseHistory = {
{ TransactionId = "1731156480_12345", ItemId = "Sword_001", Timestamp = 1731156480 },
{ TransactionId = "1731156800_12345", ItemId = "prod_123abc", Timestamp = 1731156800 }
}
}
Элементы:
Coins— баланс пользовательской валюты.Inventory— карта предметов. Каждый предмет описывается минимально — достаточно флагов (Unlocked,Equipped,Count), а не полной копии каталога.PurchaseHistory— журнал транзакций для аудита, отладки и реализации идемпотентности.
Важно: никогда не храните пароли, токены или приватные ключи в DataStore. Все данные, сохраняемые через DataStoreService, шифруются Roblox, но не предназначены для хранения секретов.
4.3. Реализация клиент-серверного взаимодействия
Взаимодействие между клиентом и сервером строится на RemoteEvent и RemoteFunction. Используйте два отдельных канала:
RemoteEvent— для асинхронных действий (покупка, запрос обновления баланса).RemoteFunction— для синхронных запросов ("можно ли купить?", "получить текущий баланс?").
Пример — запрос на покупку
- Клиент (
LocalScriptв GUI):
local BuyEvent = game:GetService("ReplicatedStorage"):WaitForChild("RemoteEvents"):WaitForChild("BuyItem")
BuyEvent:FireServer("Sword_001", "tx_" .. tick())
- Сервер (
ScriptвServerScriptService):
Ключевые моменты:
isTransactionProcessedпроверяетPurchaseHistory.commitPurchaseвыполняет — списание валюты, добавление в инвентарь, сохранение в DataStore, запись в историю — и только при полном успехе возвращаетtrue.- Клиент получает только событие — без деталей баланса (во избежание утечки информации).
4.4. Интеграция с Robux (Developer Products)
Для продажи через Robux используется MarketplaceService. Важно: никогда не доверяйте клиенту информацию о покупке.
Правильная последовательность:
- Клиент вызывает
MarketplaceService:PromptPurchase(player, productId). - Игрок подтверждает покупку (всплывающее окно от Roblox).
- Roblox отправляет событие
PromptPurchaseFinishedна сервер. - Сервер проверяет:
- Принадлежит ли
productIdожидаемому товару? - Не была ли покупка уже обработана?
- Корректен ли
player?
- Принадлежит ли
- При успехе — выдаёт предмет, сохраняет.
Пример обработчика на сервере:
Замечание: Roblox гарантирует, что PromptPurchaseFinished срабатывает только после подтверждения платёжа на сервере Roblox. Это делает его безопасным.
4.5. Безопасность — типичные уязвимости и защита
4.5.1. Подделка запросов на клиенте
Если логика "хватает предмет из каталога и вычитает цену" реализована в LocalScript, злоумышленник может изменить цену на 0 и купить всё.
Защита: Вся валидация — на сервере. Клиент отправляет только itemId, сервер сам смотрит цену в каталоге.
4.5.2. Race condition при одновременных покупках
Два запроса на покупку одного и того же предмета (например, последнего в лимитированной серии) могут пройти, если проверка баланса и списание не атомарны.
Защита: Блокировка игрока на время транзакции (например, через coroutine или флаг playerData.Locked), либо использование DataStore:UpdateAsync, который гарантирует сериализацию.
4.5.3. Потеря данных при ошибке сохранения
Если после списания валюты произошла ошибка сохранения, игрок потеряет деньги.
Защита: Паттерн "сначала сохранить, потом применить":
- Создать копию
playerData. - Внести изменения в копию.
- Попытаться сохранить копию через
UpdateAsync. - При успехе — применить изменения в текущее состояние игрока.
4.5.4. Спам-атаки
Злоумышленник может отправлять тысячи запросов на покупку в секунду.
Защита: Rate-limiting на уровне сервера — например, разрешать не более 3 запросов в секунду на игрока, с использованием Debounce.
4.6. Тестирование и отладка
- Используйте
TestServiceдля эмуляции покупок в Studio без траты Robux. - Создайте
Debug-режим: приgame.PlaceId == 0(локальный запуск) разрешать выдачу валюты по нажатию клавиши. - Логируйте все транзакции в
print()или черезHttpServiceв внешний сервис (например, Discord-вебхук для разработчиков). - Пишите unit-тесты для
commitPurchase,isTransactionProcessedи других критичных функций — с использованием mock-объектов.
4.7. Расширяемость и поддержка
Хорошая система магазина должна позволять:
- Добавлять новые валюты без изменения ядра.
- Подключать модули выдачи (например,
ToolSystem,AppearanceSystem) через интерфейсы. - Поддерживать A/B-тестирование цен (через конфигурационный
ModuleScript, переключаемый по флагу).
Рекомендуется выделить следующие модули:
CurrencyManager— управление балансами.InventorySystem— хранение и синхронизация инвентаря.TransactionProcessor— валидация и выполнение покупок.CatalogService— загрузка и кэширование каталога.DataStoreAdapter— абстракция надDataStoreService.
Это позволяет заменять компоненты (например, перейти с GlobalDataStore на OrderedDataStore для лидербордов) без переписывания всей логики.
🛠️ Гайд — Создание собственной системы магазина и монет в Roblox Studio
Цель: построить систему, в которой игрок может:
- зарабатывать внутриигровые монеты (Coins);
- тратить их на предметы в магазине;
- покупать монеты за Robux через Developer Product;
- сохранять прогресс между сессиями;
- быть защищён от подделки транзакций.
Ограничения:
- Никакой логики на клиенте, кроме отображения и отправки запросов.
- Все данные — через
DataStore.- Поддержка идемпотентности и предотвращение race condition.
Шаг 1. Подготовка проекта — структура
Создайте следующую иерархию в Explorer:
ReplicatedStorage/
├── Catalog/
│ └── Items.lua -- каталог товаров (ModuleScript)
├── Services/
│ ├── CurrencyManager.lua
│ ├── InventorySystem.lua
│ ├── TransactionProcessor.lua
│ └── DataStoreAdapter.lua
├── RemoteEvents/
│ ├── BuyItem.lua -- RemoteEvent
│ └── RequestBalance.lua -- RemoteEvent
ServerScriptService/
├── MainEconomySystem.lua -- точка входа
StarterGui/
└── ShopGUI/
└── ShopFrame.lua -- ScreenGui с LocalScript внутри
💡 Почему так?
Разделение по папкам обеспечивает читаемость и предотвращает "скриптовый хаос". Серверные сервисы — вReplicatedStorage/Services, клиентская логика — вStarterGui, события — отдельно. Это соответствует best practices Roblox и позволяет легко находить компоненты.
Шаг 2. Создание каталога товаров
ReplicatedStorage/Catalog/Items.lua (ModuleScript)
💡 Важно:
Price— таблица, чтобы в будущем можно было добавитьGems = 10без изменений в логике.RobuxProduct— символьное имя, а не числовой ID. Это позволяет избежать ошибок при переносе проекта (ID меняются между местами).Metadataстрого типизирован: система выдачи будет проверять наличие полей, а не их значения.
Шаг 3. Реализация адаптера к DataStore
ReplicatedStorage/Services/DataStoreAdapter.lua (ModuleScript)
💡 Почему
SetAsync, а неUpdateAsync?
Для простоты этого гайда используетсяSetAsync. В продакшене обязательно перейдите наUpdateAsync, чтобы предотвратить перезапись при одновременных запросах. НоSetAsyncпроще для первого шага и демонстрирует базовую идею.
Шаг 4. Менеджер валюты и инвентаря
ReplicatedStorage/Services/CurrencyManager.lua
ReplicatedStorage/Services/InventorySystem.lua
💡 Замечание:
Оба модуля используют кэширование в_EconomyData, чтобы избежать частых вызововDataStore. Это повышает производительность. Синхронизация сDataStoreпроисходит только при изменении.
Шаг 5. Обработка транзакций
ReplicatedStorage/Services/TransactionProcessor.lua
💡 Ключевой момент:
Здесь нетRemoteEvent. Это чистая бизнес-логика, которую можно тестировать изолированно. Интеграция с событиями — на следующем шаге.
Шаг 6. Серверная точка входа
ServerScriptService/MainEconomySystem.lua
💡 Про Developer Products:
Чтобы это работало, нужно:
- Зарегистрировать Developer Product в DevHub.
- Запомнить его Product ID (число).
- Создать
ScriptвServerScriptServiceс именемDevProductMapping, содержащий:Это позволяет не хардкодить ID в основном коде.script:SetAttribute("Products", { prod_coins_1k = 123456789 -- замените на реальный ID })
Шаг 7. Клиентская часть — интерфейс магазина
StarterGui/ShopGUI/ShopFrame.lua (ScreenGui → Frame → LocalScript)
💡 Важно для клиента:
- Никакого подсчёта баланса — только отображение.
- Никакой проверки "хватит ли денег" перед отправкой — это делает сервер.
tick()+math.randomдаёт уникальныйtransactionIdбез коллизий.
Шаг 8. Тестирование в Studio
- Запустите игру локально (
Playв Studio). - Откройте магазин — должен отобразиться баланс
100(стартовый бонус). - Нажмите "Купить меч" — баланс уменьшится на 500, предмет разблокируется.
- Перезайдите — баланс и инвентарь сохранятся (Roblox эмулирует DataStore локально).
- Для теста Robux-покупок:
- В Studio:
Test > Test Purchases. - Добавьте Developer Product (введите ID и название).
- Нажмите "Купить монеты" — появится окно подтверждения.
- После подтверждения — 1000 монет добавятся.
- В Studio:
✅ Если всё работает — система готова к деплою.
Что делать дальше (продвинутые шаги)
| Задача | Как реализовать |
|---|---|
| Поддержка нескольких валют | Расширить Price = { Coins = 100, Gems = 5 }, изменить CurrencyManager на работу с таблицей балансов ({ Coins = 500, Gems = 20 }). |
| Лидерборды по богатству | Использовать OrderedDataStore для хранения топ-100. |
| Возвраты (refunds) | Обрабатывать MarketplaceService.RefundOccurred. |
| A/B-тестирование цен | Хранить цены в Configuration-модуле, управляемом через Game.Settings или внешний JSON. |
| Модульные эффекты (например, надеть скин) | Создать AppearanceService, который слушает InventorySystem.ItemUnlocked через BindableEvent. |