Перейти к основному содержимому

Экономическая система

Экономическая система предоставляет управление валютой с проверкой на сервере, аукционный дом (Global Trade Link) и транзакции магазина на стороне сервера. Эти сервисы инициализируются автоматически NakamaManager (Global Trade Station, Gts, и синхронизация инвентаря, Inventory, находятся рядом с ними).

Браузер Global Trade Link

Баланс в углу — это не число, которое хранит клиент: это то, что сервис кошелька в последний раз услышал от сервера, и только сервер может его изменить. Фильтры (категория, цена, сортировка) входят в запрос — браузер никогда не скачивает весь каталог, чтобы фильтровать его локально, и именно это делает большой рынок дешёвым.

Окно прямого обмена между игроками

Прямой обмен проходит четыре этапа, названные вдоль верхнего края: invite, offering, confirm, done. Оба замка внизу слева должны закрыться, прежде чем Confirm Trade будет что-то значить, а переходом владеет сервер: клиент, который не вовремя заявляет «done», получает отказ — именно поэтому конечный автомат живёт на стороне сервера, а не в окне, на которое вы смотрите.

Обзор архитектуры​

КлассФайлОтветственность
WalletServiceEconomy/EconomyServices.csОтслеживание баланса нескольких валют через кошелёк Nakama
ShopServiceEconomy/EconomyServices.csПокупка/продажа в NPC-магазине с проверкой на сервере
GTLServiceEconomy/EconomyServices.csАукционный дом Global Trade Link для обмена между игроками

Доступ ко всем трём через NakamaManager.Instance:

var wallet = NakamaManager.Instance.Wallet;
var shop = NakamaManager.Instance.Shop;
var gtl = NakamaManager.Instance.GTL;

WalletService​

WalletService управляет балансом нескольких валют игрока. Серверная система кошелька Nakama обеспечивает атомарность и защиту от подделки изменений валюты.

Проверка балансов​

// Refresh from server
await wallet.RefreshBalanceAsync(userId);

// Get balance for a specific currency (defaults to "gold")
long gold = wallet.GetBalance("gold");
long premiumGems = wallet.GetBalance("gems");

// Access the full currency dictionary
Dictionary<string, long> currencies = wallet.Currencies;

События​

wallet.OnBalanceChanged += (long newBalance) => { /* gold changed */ };
wallet.OnCurrencyChanged += (string currencyId, long oldValue, long newValue) =>
{
Debug.Log($"{currencyId}: {oldValue} -> {newValue}");
};

Серверная авторитетность​

Когда OnlineConfig.EnableServerAuthority равно true, все изменения денег проходят через OnlineCommandBridge:

// This is called internally by CommandGraph nodes (GivePlayerMoney, etc.)
bool approved = await bridge.RequestGiveMoneyAsync(amount: 500, source: "npc_reward");

Сервер проверяет источник и сумму перед применением изменения. Это предотвращает эксплойты дублирования денег.

ShopService​

ShopService обрабатывает транзакции NPC-магазина с проверкой на стороне сервера.

Покупка предметов​

bool success = await shop.BuyFromShopAsync(
shopId: "pokemart_viridian",
itemId: "potion",
quantity: 5);

Сервер проверяет:

  • Магазин существует и содержит предмет.
  • У игрока достаточно валюты.
  • Предмет доступен (не распродан для ограниченных предметов).

Продажа предметов​

long revenue = await shop.SellToShopAsync(
itemId: "nugget",
quantity: 1);
// revenue = the amount of gold received

Сервер проверяет, что у игрока действительно есть предмет в инвентаре, перед начислением валюты.

GTL — это система аукционного дома, которая позволяет игрокам выставлять монстров и предметы на продажу другим игрокам по всему миру. Она функционирует аналогично GTS/GTE в популярных играх про захват монстров, но с полной поддержкой предметов и комиссиями за листинг.

Конфигурация​

Поле конфигурацииПо умолчаниюОписание
GTLListingFee0.05 (5%)Процент комиссии, вычитаемый из цены листинга
GTLListingDurationHours48Часов до истечения листинга
MaxGTLListings20Максимум активных листингов на игрока

Выставление монстра​

GTLListing listing = await gtl.ListMonsterAsync(
monsterId: "monster_uuid_here",
price: 50000);

// listing.Id -- unique listing ID
// listing.Fee -- the listing fee (price * GTLListingFee)
// listing.ExpiresAt -- Unix timestamp when the listing expires
// listing.Status -- GTLListingStatus.Active

Выставление предмета​

GTLListing listing = await gtl.ListItemAsync(
itemId: "rare_candy",
quantity: 10,
price: 25000);

Поиск в GTL​

var filter = new GTLSearchFilter
{
Query = "fire",
Type = GTLItemType.Monster,
MinPrice = 1000,
MaxPrice = 100000,
SortBy = "price",
Ascending = true,
Page = 0,
PageSize = 20
};

List<GTLListing> results = await gtl.SearchAsync(filter);

GTLSearchFilter поддерживает:

  • Query -- текстовый поиск по имени предмета/монстра.
  • Type -- фильтр по GTLItemType.Monster или GTLItemType.Item.
  • MinPrice / MaxPrice -- фильтр диапазона цен.
  • SortBy -- имя поля сортировки.
  • Ascending -- направление сортировки.
  • Page / PageSize -- пагинация (размер страницы по умолчанию: 20).

Покупка листинга​

bool success = await gtl.BuyListingAsync(listingId);

Отмена листинга​

bool cancelled = await gtl.CancelListingAsync(listingId);

Просмотр ваших листингов​

List<GTLListing> myListings = await gtl.GetMyListingsAsync();

События GTL​

gtl.OnListingSold += (GTLListing listing) =>
{
Debug.Log($"Your {listing.ItemName} sold for {listing.Price}!");
};

gtl.OnListingExpired += (GTLListing listing) =>
{
Debug.Log($"Listing for {listing.ItemName} expired.");
};

Модель данных GTL​

Класс GTLListing содержит:

ПолеТипОписание
IdstringУникальный ID листинга
SellerIdstringID пользователя-продавца
SellerNamestringОтображаемое имя продавца
TypeGTLItemTypeMonster или Item
ItemIdstringID монстра или предмета
ItemNamestringОтображаемое имя
QuantityintКоличество (для предметов)
PricelongЗапрашиваемая цена
FeelongВычтенная комиссия за листинг
StatusGTLListingStatusActive, Sold, Expired или Cancelled
ExpiresAtlongUnix timestamp (миллисекунды)
CreatedAtlongUnix timestamp (миллисекунды)

Клиентский UI​

GTL поставляется с настоящим, подключённым клиентским UI. GTLBrowserUgui (Assets/OpenMon/Online/UI/UGUI/GTLBrowserUgui.cs) — это UGUI-браузер аукционного дома, который вызывает NakamaManager.Instance.GTL напрямую — SearchAsync, GetMyListingsAsync, BuyListingAsync — и подписывается на OnListingSold. Он откатывается к честному состоянию «Offline — GTL недоступен», когда не подключён. Наряду с методами, возвращающими bool, документированными выше, GTLService также предоставляет варианты *WithResultAsync (SearchListingsAsync, BuyListingWithResultAsync, CancelListingWithResultAsync, …), которые возвращают OnlineResult<T>, чтобы вызывающие могли показать ошибку сервера вместо голого false.

GTS — Global Trade Station​

Вторая биржа, GTS, — это асинхронный обмен монстра на монстра. GtsService (NakamaManager.Instance.Gts, в Economy/EconomyServices.cs) — её клиентский транспорт поверх серверных RPC gts_deposit, gts_search и gts_withdraw:

МетодRPCЧто делает
DepositAsync / DepositWithResultAsyncgts_depositПомещает монстра из партии на хранение вне партии игрока и выставляет то, что хочется получить взамен (GtsWantSpec). Сервер сразу ищет депозит, который устраивает обе стороны: если он есть, два монстра атомарно меняются местами в этом же вызове; иначе депозит остаётся на доске
SearchAsync / SearchWithResultAsyncgts_searchЧитает доску активных непросроченных депозитов (только чтение: здесь ничего не обменивается). То, что вы предлагаете и что хотите, может заранее отфильтровать доску
WithdrawAsync / WithdrawWithResultAsyncgts_withdrawЗабирает обратно один из ваших собственных активных депозитов

Как и у GTL, у каждой операции есть перегрузка для UI, которая деградирует до пустого списка или false, и перегрузка *WithResultAsync, возвращающая OnlineResult<T> с ошибкой сервера. Сопоставимая карточка монстра выводится на сервере из монстра, находящегося на хранении, поэтому присланной клиентом карточке не доверяют. За этим стоит интерфейс IGtsTransport, так что UI можно тестировать на заглушке.

GtsBrowserUgui (Assets/OpenMon/Online/UI/UGUI/GtsBrowserUgui.cs) — фронтенд на UGUI: он собирает GtsWantSpec из своих элементов фильтра и использует чистый GtsMonsterMatcher (без Unity, без Nakama) для фильтрации доски депозитов.

Балансировка экономики​

Дизайн экономики MCE включает несколько встроенных стоков валюты:

  1. Комиссии за листинг GTL (по умолчанию 5%) -- убирают валюту из обращения с каждым обменом.
  2. Цены магазина с проверкой на сервере -- сервер контролирует соотношения покупки/продажи, предотвращая арбитраж.
  3. Боевые награды, контролируемые на стороне сервера -- сервер определяет EXP, деньги и выпадение предметов.
  4. Аудит инвентаря античитом -- InventoryAuditor обнаруживает дублирование предметов и несоответствия количества (см. раздел Античит для подробностей).

Меры против мошенничества​

Когда EnableServerAuthority активен:

  • Все изменения валюты проходят через OnlineCommandBridge.RequestGiveMoneyAsync(), который вызывает RPC mce_give_money. Сервер проверяет источник (боевая награда, продажа в магазине, квест и т. д.) перед применением.
  • Все изменения предметов проходят через OnlineCommandBridge.RequestGiveItemAsync(), который вызывает RPC mce_give_item.
  • InventoryAuditor делает снимки после каждого проверенного действия и сравнивает их с инвентарями, сообщёнными клиентом. Несоответствия запускают события AuditViolation.
  • ActionRateLimiter предотвращает эксплойты быстрой покупки/продажи (по умолчанию 0.3с, как настроено в AntiCheatManager; значение класса по умолчанию 0.5с).