跳到主要内容
游戏模型

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

Schema 即代码

在代码中声明模型,推送,拿回类型。 这是开发者接触 schema 的入口:管理面板和代码编写的是同一个模型,代码生成则为每个引擎补上闭环。

何时使用

  • 数据模型应当放在代码里,像代码一样评审:声明、schema diff、schema push。
  • 引擎类型绝不能与已部署的模型脱节:schema codegen 会重新生成 Unreal C++ 和 Unity C# 类型。
  • 破坏性变更必须在执行前可读、可取消:提议 → 计划 → 应用。
  • 你在多个项目中复用同一组捆绑(Stat、Interactable):把它声明一次,作为预设(preset)。
  • 无需使用:如果运营方(operator)只是在管理面板里调整数值。模型变更照样会以 diff 的形式回到代码中。

分工

主体在本页中的职责
schema-author在代码中声明实体(entity)、部件和枚举,执行 diff 并推送
operator查看面板中的概览,提议并应用迁移(migration)
ci构建流水线,以 backend-service 密钥运行:合并时推送,随后重新生成引擎类型

概览

声明 Item:内嵌 Stats 部件和一个枚举,作为一个 schema 推送
[Entity("item")]
public class Item
{
    public string Name = "";
    public Rarity Rarity;              // an enum declared the same way
    public Stats Stats = new();        // a part — embedded, no lifecycle of its own
}

[Part("stats")]
public class Stats { public int Power; public int Weight; }
@Entity('item')
export class Item {
  name = '';
  rarity!: Rarity;                   // an enum declared the same way
  stats = new Stats();               // a part — embedded, no lifecycle of its own
}

@Part('stats')
export class Stats { power = 0; weight = 0; }
@entity("item")
class Item:
    name: str = ""
    rarity: Rarity                     # an enum declared the same way
    stats: Stats = Stats()             # a part — embedded, no lifecycle of its own

@part("stats")
class Stats:
    power: int = 0
    weight: int = 0
即将推出 —— Go

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

USTRUCT(PSPart = "stats")
struct FItemStats
{
    GENERATED_BODY()

    UPROPERTY() int32 Power;
    UPROPERTY() int32 Weight;
};

UCLASS(PSEntity = "item")
class UItem : public UObject
{
    GENERATED_BODY()

    UPROPERTY() FString    Name;
    UPROPERTY() EPSRarity  Rarity;       // an enum declared the same way
    UPROPERTY() FItemStats Stats;        // a part — embedded, no lifecycle of its own
};
// 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("item")]
public class Item
{
    public string Name = "";
    public Rarity Rarity;              // an enum declared the same way
    public Stats Stats = new();        // a part — embedded, no lifecycle of its own
}

[Part("stats")]
public class Stats { public int Power; public int Weight; }
playserv schema diff       # local declarations vs deployed schema
playserv schema push       # with a revision precondition — no blind overwrites
playserv schema codegen    # regenerate Unreal C++ / Unity C# types

推送是一个显式步骤

保存文件时不会上传任何内容。只有从你的机器或 CI 运行 playserv push(或 schema push)时,声明才会进入已部署的模型,并且会带上它做 diff 时所依据的修订版本。在面板中的修改会在 schema diff 中显示为漂移;无论朝哪个方向同步变更,都要由你亲自运行命令。

模型

声明包含什么。

包含含义
key用来寻址它的稳定名称。在代码中重命名就是重命名,而不是先删除再创建
kind在代码中声明的实体、部件或枚举
所有权模式seed:记录不存在时由代码创建,再次推送不会改动其中的值,此后这些值归管理控制台所有;或 managed:始终归代码所有,每次推送都把值恢复为声明的内容,管理控制台中的修改会被直接拒绝,而不是先应用、再在下次推送时丢失
preset可选:可复用的捆绑,即实体预设,例如 stats 或 world-objects,声明一次,像类型一样应用。它不会引入新的声明种类

推送保证什么。

始终成立含义
匹配按键匹配,绝不按符号:重命名符号后再次推送,只会留下一条记录,而不是两条
报告在应用之前准确说明哪些内容会变化,应用之后说明覆盖了什么
来源可以区分:由代码推送创建的记录,与其他途径创建的记录能够区分开
修订版本随推送一起提交,推送要么整体生效,要么完全不生效

代码生成保证什么。

始终成立含义
重新生成每次推送之后都会执行,生成的类型绝不手动修改:重新生成后再 diff,不会产生任何变化
命名无论声明在哪里编写,命名都跟随声明:Item 上的 Rarity 字段在 Unreal 中成为 UPSItem::Rarity,在 Unity 中成为 Item.Rarity
两个方向不会分叉:代码中声明的一切都会出现在面板中,运营方在面板中编写的一切都能与代码干净地 diff,因此 schema diff 会显示每一处差异

迁移保证什么。

始终成立含义
何时需要迁移持久化声明发生变更,并且会改写现有的值
迁移声明什么版本、预览、有序应用、失败时的回滚,以及可观察的完成结果
共存两个数据版本同时在线时,读写操作会声明自己接受哪些版本——运行时绝不根据字段名推断兼容性

错误

  • 没有相应角色的调用方会收到 forbidden,已部署的模型保持不变:拒绝绝不会导致部分推送。这与修订版本过期是两种不同的拒绝:后者返回 precondition_failed,表示 diff 所依据的 schema 在此之后已经变化——重新 diff 后再推送。
  • 超出声明范围的值会在写入时被拒绝,绝不截断,超过声明长度的字符串同样如此。截断只会得到一个合法但错误的值。
  • 无效的 UTF-8 序列会在写入时被拒绝,而不是修复。
  • 没有声明迁移的破坏性持久化变更根本无法发布。

限制

与声明形态相关的上限会在声明时检查——部署或发布时——而不是在首次使用时,前提是运行时的症状看起来不像一次拒绝。具体数值将随平台限制一章给出。