跳到主要内容

经济系统

经济系统提供服务器校验的货币管理、一个拍卖行(全球交易链接)和服务器端商店交易。这些服务均由 NakamaManager 自动初始化(全球交易站 Gts 和库存同步 Inventory 就位于它们旁边)。

Global Trade Link 浏览器

角落里的余额并不是客户端保存的数字:它是钱包服务最后一次从服务器得知的值,而服务器也是唯一能改变它的东西。过滤条件(类别、价格、排序)会放进查询中 —— 浏览器从不下载整份目录再在本地过滤,这正是让大型市场保持低成本的原因。

玩家之间的交易窗口

直接交易会经过顶部所标出的四个阶段:invite、offering、confirm、done。左下角的两把挂锁都必须合上,Confirm Trade 才有意义,而状态转换由服务器掌控 —— 一个不按顺序就宣称“done”的客户端会被拒绝,这正是状态机位于服务器端、而不是你眼前这个窗口里的全部原因。

架构概览​

类文件职责
WalletServiceEconomy/EconomyServices.cs通过 Nakama 钱包进行多货币余额追踪
ShopServiceEconomy/EconomyServices.cs服务器校验的 NPC 商店买入/卖出
GTLServiceEconomy/EconomyServices.cs用于玩家间交易的全球交易链接拍卖行

通过 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)是一个 UGUI 拍卖行浏览器, 它直接调用 NakamaManager.Instance.GTL —— SearchAsync、 GetMyListingsAsync、BuyListingAsync —— 并订阅 OnListingSold。当未连接时, 它会回退到一个诚实的"Offline — the GTL is unavailable"状态。 除了上文记录的返回 bool 的方法之外,GTLService 还暴露了 *WithResultAsync 变体(SearchListingsAsync、BuyListingWithResultAsync、 CancelListingWithResultAsync 等),它们返回 OnlineResult<T>,使调用方能够 呈现服务器错误,而非一个赤裸裸的 false。

GTS —— 全球交易站​

第二个交易所 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),以及一个返回带有服务器错误的 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.3s;类默认值为 0.5s)。