跳到主要内容
游戏周边服务

本页尚未翻译,当前显示英文版本。

Inventory

射击、掉落、技能和移动共用的一组有主数据行。 堆叠按增量变化,每个所有者的上限触顶时如何表现,由你选择。

何时使用

  • 玩家持有东西,而背包(inventory)里的每件持有物都是一行有所有者的数据:按所有者读取,按所有者设上限,溢出时的行为需要声明,而不是走默认。
  • 数量会累加:堆叠通过带幂等键(idempotency key)的增量变化,所以重试扣减不会扣两次。
  • 其他模块都从同一组数据里消耗:射击扣弹药,掉落发放战利品,购买以数据行的形式出现,对应各自的权益(entitlement)。
  • 无需使用:如果这个数值不属于可拥有的东西。生命值、经验值和冷却应该放在属性里。

分工

主体在本页中的职责
player读取自己的持有物,并从中消耗
backend-service代表玩家发放、增量修改和收回,并指明所代表的玩家

概览

在 fn 权威下:发放弹药,把物品移到 primary 装备槽,消耗前先检查余量是否足够
// fn authority — a cloud function, or a dedicated server holding a host key
var bag = await player.Inventory.Container("bag");
var equipment = await player.Inventory.Container("equipment");

await player.Inventory.Grant("ammo.shell", count: 20);
await bag.Move(itemId, to: equipment, slot: "primary");

if (await player.Inventory.CanAfford("ammo.shell", 1))
    await player.Inventory.Consume("ammo.shell", 1);
// fn authority — a cloud function, or a dedicated server holding a host key
const bag = await player.inventory.container('bag');
const equipment = await player.inventory.container('equipment');

await player.inventory.grant('ammo.shell', { count: 20 });
await bag.move(itemId, { to: equipment, slot: 'primary' });

if (await player.inventory.canAfford('ammo.shell', 1))
  await player.inventory.consume('ammo.shell', 1);
# fn authority — a cloud function, or a dedicated server holding a host key
bag = await player.inventory.container("bag")
equipment = await player.inventory.container("equipment")

await player.inventory.grant("ammo.shell", count=20)
await bag.move(item_id, to=equipment, slot="primary")

if await player.inventory.can_afford("ammo.shell", 1):
    await player.inventory.consume("ammo.shell", 1)
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

// fn authority — a cloud function, or a dedicated server holding a host key
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(GrantId), PlayerId, PSKeys::Item::AmmoShell);

// spending is an instance act on the entitlement you hold
Entitlement->Spend(FPSIdempotencyKey(SpendId), /*Amount*/ 1,
    TPSOnResult<void>::CreateLambda([](const TPSResult<void>& Result)
    {
        // short on the item is a declared refusal, not a silent no-op
        if (Result.IsRefused()) { DeclineReload(Result.Refusal()); }
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
// fn authority — a cloud function, or a dedicated server holding a host key
var bag = await player.Inventory.Container("bag");
var equipment = await player.Inventory.Container("equipment");

await player.Inventory.Grant("ammo.shell", count: 20);
await bag.Move(itemId, to: equipment, slot: "primary");

if (await player.Inventory.CanAfford("ammo.shell", 1))
    await player.Inventory.Consume("ammo.shell", 1);

玩家会话用同样的调用完成读取、移动和余量检查。发放、消耗和销毁则不归它做:无论调用来自哪个绑定,平台都会以禁止为由拒绝,并指出调用方缺少哪项权利。

模型

背包不引入任何自己的概念。 它是一个预设:用实体已有的能力拼出的形状,所以下面的一切都是实体声明,而不是本页特有的机制。

声明项含义
一个有主类型持有物属于某个所有者,按所有者选择是实体(entity)自身的操作
指向商品目录物品(item)的 ref引用保存物品的 id,从不保存它的 key,所以改 key 是安全的。定义本身位于 Catalog & Commerce
带增量的堆叠 aspect堆叠按差值变化,而不是覆盖读到的值(见下文)
带边界行为的按所有者上限三选一,没有默认值:refuse · redirect 到声明的所有者桶 · discard with event

每次增量都要传幂等键

增量不是幂等的:两次增量就是两次。它必须带幂等键,否则重试扣减就会扣两次;想知道结果,就做一次定址读取。

a grant arrives; the owner is at the capthe cap's declared behaviour (no default)the loot stays where it wasinto a declared owner bucketgone, with an event refuseredirectdiscard with event
向已满的背包发放:由上限声明的行为决定,没有默认值

每件持有物始终成立的事实。

始终成立含义
上限没有默认值背包满时的三种答复对应三种不同的游戏:拒绝会让玩家眼看着面前的战利品丢掉;重定向意味着邮件或爆满的仓库;丢弃是一次悄无声息的损失,它之所以合规,只因为它事先声明过,而且可以观测。没有哪个默认值对三者都合适,所以由声明来选
所有者不可变改字段不会让任何东西易主:持有物转移就是一次收回加一次声明了来源的新发放,两件事都留在记录里
两个玩家之间的转让是另一种承诺:它需要托管和反欺诈,不在本版本范围内
数据行只是权益的呈现,而不是权益的第二个来源:买到的东西存在 Catalog & Commerce 中,这里的行代表它

错误

  • 实例不存在,或者谓词把它隐藏了:两种情况都返回 not found,所以拒绝永远不会泄露“东西存在,只是不归你”。
  • aspect 中未声明的字段(包括嵌套字段),以及没有值的必填字段,都属于校验拒绝,并指出具体字段。
  • 版本不匹配属于前置条件失败,重新读取后值得重试。
  • 代表玩家写入却没有指明玩家属于校验拒绝,而不是悄悄以别人的身份写入。
  • 没有读取或写入权限返回 forbidden,并区分是读还是写。

限制

具体数值会随平台限制一章给出;触及每个上限时的行为现在就已确定。

  • 每个所有者的实例数:按上文声明的规则处理,没有默认值。
  • 单个存储实例的大小:写入以冲突拒绝,拒绝信息会指出超限的字段和测得的大小。上限是逐步累积到的,所以在那次失败的写入之前,就能观察到它在逼近。
  • 单个实例的变更频率:以限流拒绝,并给出截止时间。
  • 选择集的分页大小:页面截到上限,“还有更多”标志保持为 true;少返回却不设该标志是禁止的。