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

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

Profile

玩家资料是一个视图,其中几乎没有哪一部分归平台所有。 平台为玩家保存的只有 player_id 及其背后的系统资料——身份、会话、提供方关联,这些都在 Auth & Players 中。玩家拥有的一切都是你自己的实体,归该玩家所有。玩家资料就是项目声明的这组实体,针对一个所有者一次读出。

何时使用

  • 某个界面需要用一次调用取得一名玩家的全部相关数据:声明的集合会展开到该玩家拥有的各个实体(entity),不必由客户端拼接多个查询。
  • 平台界面必须显示一个人,而不是一个标识符:排行榜、审核队列和客服工单只保存 player_id,直到项目指定用哪条记录来显示玩家。
  • 需要为另一名玩家展示名片:对另一个所有者执行同样的读取,由 Access & Roles 中已声明的行谓词(row predicate)和字段掩码(field mask)收窄。
  • HUD 必须实时跟踪玩家拥有的状态:这次读取是一个选择集(selection),而选择集可以订阅。
  • 无需使用:如果数据不归某个玩家所有。共享行和全局行用普通的 Entity 选择集读取,没有可以展开的所有者。

分工

主体在本页中的职责
schema-author把实体标记为玩家所有,并声明其中哪些构成玩家资料
player读取自己的玩家资料;写入直接作用于实体本身
room-visitor在对方的谓词和掩码允许的范围内,读取另一名玩家的资料

概览

是否属于玩家资料按实体声明,而不是按字段。实体声明自己属于玩家资料;其他玩家能看到其中哪些内容,由读取它的角色上的字段掩码决定(Access & Roles)。

loadout 和 progress 标记为玩家所有,并加入玩家资料集合
[Entity("loadout"), OwnedBy(Owner.Player), InProfile]
public class Loadout { public string Primary = ""; }

[Entity("progress"), OwnedBy(Owner.Player), InProfile]
public class Progress
{
    public int Level;
    public string Title = "";
    public int SecretMmr;                 // no reading role's mask names it: it stays server-side
}
@Entity('loadout') @OwnedBy(Owner.player) @InProfile()
export class Loadout { primary = ''; }

@Entity('progress') @OwnedBy(Owner.player) @InProfile()
export class Progress {
  level = 0;
  title = '';
  secretMmr = 0;                          // no reading role's mask names it: it stays server-side
}
@entity("loadout")
@owned_by(Owner.PLAYER)
@in_profile
class Loadout:
    primary: str = ""

@entity("progress")
@owned_by(Owner.PLAYER)
@in_profile
class Progress:
    level: int = 0
    title: str = ""
    secret_mmr: int = 0                   # no reading role's mask names it: it stays server-side
即将推出 —— Go

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

UCLASS(PSEntity = "loadout", PSOwnedBy = "Player", PSInProfile)
class ULoadout : public UObject
{
    GENERATED_BODY()

    UPROPERTY() FString Primary;
};

UCLASS(PSEntity = "progress", PSOwnedBy = "Player", PSInProfile)
class UProgress : public UObject
{
    GENERATED_BODY()

    UPROPERTY() int32   Level;
    UPROPERTY() FString Title;
    UPROPERTY() int32   SecretMmr;        // no reading role's mask names it: it stays server-side
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
Unity C# is the same C# API here — 同样的特性在 Unity 中也能编译,基线为 2021.3——这里没有 C# 12 语法
[Entity("loadout"), OwnedBy(Owner.Player), InProfile]
public class Loadout { public string Primary = ""; }

[Entity("progress"), OwnedBy(Owner.Player), InProfile]
public class Progress
{
    public int Level;
    public string Title = "";
    public int SecretMmr;                 // no reading role's mask names it: it stays server-side
}

这次读取是限定所有者的选择集:即 Entity 的查询接口,所有者固定,实体列表取自声明。Profile 是这次读取的名字,背后并没有一个模块:权限、谓词、过滤条件、订阅全都相同,因为它就是同一个操作。

一次读取我自己的数据行并保持实时;再在掩码允许的范围内读取对手的数据
var mine = playserv.Profile.Mine();                       // a selection, not a record
var rows = await mine.Query();                            // loadout + progress, one pass
mine.Subscribe(changed => Hud.Refresh(changed));          // the selection stays live

var rival = await playserv.Profile.Of(rivalId).Query();   // only what the mask leaves
const mine = playserv.profile.mine();                     // a selection, not a record
const rows = await mine.query();                          // loadout + progress, one pass
mine.subscribe((changed) => hud.refresh(changed));        // the selection stays live

const rival = await playserv.profile.of(rivalId).query(); // only what the mask leaves
mine = playserv.profile.mine()                            # a selection, not a record
rows = await mine.query()                                 # loadout + progress, one pass
mine.subscribe(lambda changed: hud.refresh(changed))      # the selection stays live

rival = await playserv.profile.of(rival_id).query()       # only what the mask leaves
即将推出 —— Go

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

TPSSelection<UPSProfile> MyProfile = Client->Entities->Of<UPSProfile>()->Select().GetMine();   // a selection, not a record
MyProfile.Then(TPSOnResult<FPSProfileRows>::CreateWeakLambda(this, [this](const TPSResult<FPSProfileRows>& Result)
{
    if (!Result.HasValue()) { return; }
    Hud->ShowProfile(Result.Value());          // loadout + progress, one pass
}));
TPSSubscription ProfileWatch = MyProfile.Subscribe(
    [this](const FPSProfileChange& Changed) { Hud->Refresh(Changed); });

Client->Entities->Of<UPSProfile>()->Get(RivalId,
    TPSOnResult<FPSProfileRows>::CreateWeakLambda(this, [this](const TPSResult<FPSProfileRows>& Rival)
    {
        if (!Rival.HasValue()) { return; }
        Hud->ShowRival(Rival.Value());
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — 在 Unity 中基于生成的类型原样运行
var mine = playserv.Profile.Mine();                       // a selection, not a record
var rows = await mine.Query();                            // loadout + progress, one pass
mine.Subscribe(changed => Hud.Refresh(changed));          // the selection stays live

var rival = await playserv.Profile.Of(rivalId).Query();   // only what the mask leaves

模型

概念含义
player_id平台对玩家的全部认知,外加其背后的系统资料——见 Auth & Players
玩家资料集合项目声明为玩家资料(profile)的那些玩家所有实体;是否声明可选
所有者选择集即这次读取:输入一个所有者,输出其在集合中的所有数据行——权限、谓词、过滤条件和订阅与任何 Entity 选择集相同
公开读取对另一个所有者执行该选择集,由读取方角色的行谓词和字段掩码收窄(Access & Roles)

每次读取玩家资料都成立的规则。

始终成立含义
不存在玩家资料记录它没有自己的标识符、修订版本、历史和生命周期,因为它是基于数据行的视图,而这四样东西数据行都有
写入落在数据所在的位置修改 progress 行后,每个包含它的玩家资料读取都会在下一次读取时看到新值
不存在公开写入视图没有可写的对象,共享可写的状态由服务端代码处理
所有权是一个谓词而不是平台额外添加的一列(Access & Roles):owner == caller.player 是这一机制的一个实例,“参与者之一”是另一个
派生字段归 hook 管由变更后的观察型 hook 在 Level 越过阈值时写入 Progress.Title。它跟在写入之后,无法拒绝写入

错误

  • 没有声明玩家资料集合的项目不提供玩家资料读取,调用以 unavailable 拒绝——而不是返回空结果,那样等于说玩家有资料、只是恰好为空。
  • 谓词隐藏的数据行返回 not found,不存在的数据行也一样:公开读取绝不会成为探知“存在但不可见”数据的途径。
  • 读取方角色掩码之外的字段不会出现在结果中,而不是出现但为空。
  • 代表玩家写入却未指明玩家,属于校验拒绝。
  • 声明玩家资料集合是 schema 操作——只能在 fn 或 adm 中执行。玩家密钥尝试这样做会以 forbidden 拒绝,整个推送都会驳回,而不是只声明其中一部分。

限制

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

  • 所有者选择集的分页大小:截断到上限,同时“还有更多”标志保持为 true;少返回却不设该标志是禁止的。
  • 每行包含集合的大小:按同样的规则截断,标志设在该包含项上。
  • 单个实例的变更频率:以限流拒绝,并附带截止时间。玩家资料读取和其他选择集一样,沿用实体的上限,不单独声明。