Внутриигровая экономика 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 сервер не должен выполнять операцию дважды. Это защищает от сетевых дублей и намеренных атак.

  • Отказоустойчивость при сохранении.
    Операция "списать валюту и выдать предмет" должна быть атомарной: либо оба действия выполнены, либо ни одно. Для этого применяется паттерн двухфазного сохранения:

    1. Сервер временно блокирует баланс игрока в памяти.
    2. Выполняет выдачу предмета (например, добавляет в PlayerData.Inventory).
    3. Выполняет сохранение в DataStore.
    4. При успехе — фиксирует списание; при ошибке — откатывает изменения.

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 — для синхронных запросов ("можно ли купить?", "получить текущий баланс?").

Пример — запрос на покупку
  1. Клиент (LocalScript в GUI):
   local BuyEvent = game:GetService("ReplicatedStorage"):WaitForChild("RemoteEvents"):WaitForChild("BuyItem")
   BuyEvent:FireServer("Sword_001", "tx_" .. tick())
  1. Сервер (Script в ServerScriptService):

Ключевые моменты:

  • isTransactionProcessed проверяет PurchaseHistory.
  • commitPurchase выполняет — списание валюты, добавление в инвентарь, сохранение в DataStore, запись в историю — и только при полном успехе возвращает true.
  • Клиент получает только событие — без деталей баланса (во избежание утечки информации).

4.4. Интеграция с Robux (Developer Products)

Для продажи через Robux используется MarketplaceService. Важно: никогда не доверяйте клиенту информацию о покупке.

Правильная последовательность:

  1. Клиент вызывает MarketplaceService:PromptPurchase(player, productId).
  2. Игрок подтверждает покупку (всплывающее окно от Roblox).
  3. Roblox отправляет событие PromptPurchaseFinished на сервер.
  4. Сервер проверяет:
    • Принадлежит ли productId ожидаемому товару?
    • Не была ли покупка уже обработана?
    • Корректен ли player?
  5. При успехе — выдаёт предмет, сохраняет.

Пример обработчика на сервере:

Замечание: Roblox гарантирует, что PromptPurchaseFinished срабатывает только после подтверждения платёжа на сервере Roblox. Это делает его безопасным.


4.5. Безопасность — типичные уязвимости и защита

4.5.1. Подделка запросов на клиенте

Если логика "хватает предмет из каталога и вычитает цену" реализована в LocalScript, злоумышленник может изменить цену на 0 и купить всё.
Защита: Вся валидация — на сервере. Клиент отправляет только itemId, сервер сам смотрит цену в каталоге.


4.5.2. Race condition при одновременных покупках

Два запроса на покупку одного и того же предмета (например, последнего в лимитированной серии) могут пройти, если проверка баланса и списание не атомарны.
Защита: Блокировка игрока на время транзакции (например, через coroutine или флаг playerData.Locked), либо использование DataStore:UpdateAsync, который гарантирует сериализацию.


4.5.3. Потеря данных при ошибке сохранения

Если после списания валюты произошла ошибка сохранения, игрок потеряет деньги.
Защита: Паттерн "сначала сохранить, потом применить":

  1. Создать копию playerData.
  2. Внести изменения в копию.
  3. Попытаться сохранить копию через UpdateAsync.
  4. При успехе — применить изменения в текущее состояние игрока.

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:
Чтобы это работало, нужно:

  1. Зарегистрировать Developer Product в DevHub.
  2. Запомнить его Product ID (число).
  3. Создать Script в ServerScriptService с именем DevProductMapping, содержащий:
    script:SetAttribute("Products", {
        prod_coins_1k = 123456789 -- замените на реальный ID
    })
    
    Это позволяет не хардкодить ID в основном коде.

Шаг 7. Клиентская часть — интерфейс магазина

StarterGui/ShopGUI/ShopFrame.lua (ScreenGui → Frame → LocalScript)

💡 Важно для клиента:

  • Никакого подсчёта баланса — только отображение.
  • Никакой проверки "хватит ли денег" перед отправкой — это делает сервер.
  • tick() + math.random даёт уникальный transactionId без коллизий.

Шаг 8. Тестирование в Studio

  1. Запустите игру локально (Play в Studio).
  2. Откройте магазин — должен отобразиться баланс 100 (стартовый бонус).
  3. Нажмите "Купить меч" — баланс уменьшится на 500, предмет разблокируется.
  4. Перезайдите — баланс и инвентарь сохранятся (Roblox эмулирует DataStore локально).
  5. Для теста Robux-покупок:
    • В Studio: Test > Test Purchases.
    • Добавьте Developer Product (введите ID и название).
    • Нажмите "Купить монеты" — появится окно подтверждения.
    • После подтверждения — 1000 монет добавятся.

✅ Если всё работает — система готова к деплою.


Что делать дальше (продвинутые шаги)

Задача Как реализовать
Поддержка нескольких валют Расширить 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.