メインコンテンツまでスキップ

経済システム

経済システムは、サーバー検証された通貨管理、オークションハウス(グローバルトレードリンク)、サーバー側のショップ取引を提供します。これらのサービスは NakamaManager によって自動的に初期化されます(グローバルトレードステーションの Gts とインベントリ同期の Inventory も、その隣に置かれています)。

Global Trade Link ブラウザ

隅にある残高は、クライアントが保持している数値ではありません。ウォレットサービスがサーバーから最後に受け取った値であり、それを変更できるのもサーバーだけです。フィルタ(カテゴリ、価格、並べ替え)はクエリに含まれます。ブラウザがカタログ全体をダウンロードしてローカルで絞り込むことはなく、だからこそ大規模なマーケットでも負荷が小さく済みます。

プレイヤー同士のトレードウィンドウ

直接トレードは、上部に並んだ4つの段階(invite、offering、confirm、done)を順に進みます。左下の2つの南京錠が両方とも閉じて初めて Confirm Trade に意味が生まれ、遷移を管理するのはサーバーです。順番を無視して「done」と言ってくるクライアントは拒否されます。これが、ステートマシンが目の前のウィンドウではなくサーバー側にある理由のすべてです。

アーキテクチャ概要​

クラスファイル責務
WalletServiceEconomy/EconomyServices.csNakama のウォレットを介したマルチ通貨残高の追跡
ShopServiceEconomy/EconomyServices.csサーバー検証された NPC ショップの購入/売却
GTLServiceEconomy/EconomyServices.csプレイヤー間トレードのためのグローバルトレードリンクオークションハウス

3つすべてに 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)​

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
SellerIdstring出品者のユーザー ID
SellerNamestring出品者の表示名
TypeGTLItemTypeMonster または Item
ItemIdstringモンスターまたはアイテムの ID
ItemNamestring表示名
Quantityint数量(アイテムの場合)
Pricelong希望価格
Feelong差し引かれた出品手数料
StatusGTLListingStatusActive、Sold、Expired、または Cancelled
ExpiresAtlongUnix タイムスタンプ(ミリ秒)
CreatedAtlongUnix タイムスタンプ(ミリ秒)

クライアント UI​

GTL は実際に接続されたクライアント UIを同梱しています。GTLBrowserUgui (Assets/OpenMon/Online/UI/UGUI/GTLBrowserUgui.cs)は、NakamaManager.Instance.GTL を直接呼び出す(SearchAsync、GetMyListingsAsync、BuyListingAsync)UGUI のオークションハウスブラウザで、OnListingSold をサブスクライブします。接続していないときは、正直な「オフライン — GTL は利用できません」状態にフォールバックします。上記で文書化したブール値を返すメソッドに加えて、GTLService は OnlineResult<T> を返す *WithResultAsync バリアント(SearchListingsAsync、BuyListingWithResultAsync、CancelListingWithResultAsync など)も公開しており、呼び出し側が単なる false の代わりにサーバーエラーを表面化できます。

GTS — グローバルトレードステーション​

2つ目の取引所である GTS は、モンスター対モンスターの非同期トレードです。GtsService(NakamaManager.Instance.Gts、Economy/EconomyServices.cs 内)は、サーバー RPC の gts_deposit、gts_search、gts_withdraw に対するクライアント側のトランスポートです:

メソッドRPC動作
DepositAsync / DepositWithResultAsyncgts_depositパーティのモンスターをプレイヤーのパーティから外してエスクローに預け、引き換えに求めるもの(GtsWantSpec)を掲示します。サーバーは双方の条件を満たすデポジットをただちに探し、見つかればその同じ呼び出しの中で2体のモンスターがアトミックに交換されます。見つからなければ、デポジットはボードに残ります
SearchAsync / SearchWithResultAsyncgts_search有効で期限内のデポジットのボードを読み取ります(読み取り専用で、ここでは何も交換されません)。自分が出すものと求めるものでボードを事前に絞り込めます
WithdrawAsync / WithdrawWithResultAsyncgts_withdraw自分の有効なデポジットの1つを取り戻します

GTL と同様に、各操作には、空のリストまたは false に縮退する UI 向けのオーバーロードと、サーバーのエラーを含む OnlineResult<T> を返す *WithResultAsync オーバーロードがあります。モンスターのマッチ用カードは、エスクローされたモンスターからサーバー側で導出されるため、クライアントが送ったカードは信用されません。その背後にあるインターフェースが IGtsTransport で、UI をフェイクに対してテストできます。

GtsBrowserUgui(Assets/OpenMon/Online/UI/UGUI/GtsBrowserUgui.cs)は UGUI のフロントエンドです。フィルタコントロールから GtsWantSpec を組み立て、純粋な GtsMonsterMatcher(Unity なし、Nakama なし)でデポジットボードをフィルタリングします。

経済のバランス調整​

MCE の経済設計には、いくつかの組み込みの通貨シンクが含まれます:

  1. GTL 出品手数料(デフォルト5%)-- トレードごとに通貨を流通から取り除きます。
  2. サーバー検証されたショップ価格 -- サーバーが購入/売却の比率を制御し、裁定取引を防ぎます。
  3. サーバー側で制御されるバトル報酬 -- サーバーが経験値、お金、アイテムドロップを決定します。
  4. アンチチートのインベントリ監査 -- InventoryAuditor がアイテムの複製と数量の不一致を検出します(詳細はアンチチートのセクションを参照)。

不正防止対策​

EnableServerAuthority がアクティブなとき:

  • すべての通貨変更は OnlineCommandBridge.RequestGiveMoneyAsync() を経由し、mce_give_money RPC を呼び出します。サーバーは適用前にソース(バトル報酬、ショップ売却、クエストなど)を検証します。
  • すべてのアイテム変更は OnlineCommandBridge.RequestGiveItemAsync() を経由し、mce_give_item RPC を呼び出します。
  • InventoryAuditor は、検証された各アクションの後にスナップショットを取り、クライアント報告のインベントリと比較します。不一致は AuditViolation イベントをトリガーします。
  • ActionRateLimiter は、急速な購入/売却エクスプロイトを防ぎます(AntiCheatManager で設定されたデフォルトは0.3秒。クラスのデフォルトは0.5秒)。