本页尚未翻译,当前显示英文版本。
Entity presets
预设(preset)是一组具名的实体 aspect,包括数据、状态、RPC、事件和 hook,针对某一种游戏场景打包而成。你可以应用预设、调整它的数值,或者派生自己的预设。应用预设会给你的类型增加 aspect,但不会把你的类型置于任何东西之下:预设不是模块,本身也没有可供继承的东西。属性(stat)、技能、投射物、掉落表和世界物件是五个预设,而不是五个子系统:同样的声明,同样的同步,同样的 hook 执行顺序。
何时使用
- 游戏中的某个东西带 有数值:数值有上下限、会恢复,到达边界时会触发状态转换。
- 某个动作需要消耗、冷却、阶段和效果,并且由一个客户端动作触发。
- 有东西在飞行,而且对延迟较高的射击者,命中也必须判定得公平。
- 战利品必须来自加权概率,玩家对掉落有异议时能够准确重放。
- 地图上有各种摆设——门、按钮、陷阱、可破坏物体——它们的状态必须在中途加入对局时依然保留。
- 无需使用:如果实体(entity)只是普通的同步数据。声明字段就够了。
分工
| 主体 | 在本页中的职责 |
|---|---|
schema-author | 声明属性、技能、投射物、掉落表和世界物件 |
room-owner | 调整预设数值,按掉落表抽取,创建世界物件 |
player | 释放技能、开火、拾取战利品、与物件交互 |
概览
从两个内置预设派生
Crate,然后每行创建一个,并调到 250 HP// derived once, in the schema
[Entity("crate", Persistence = Persistence.Runtime, Presets = new[] { Preset.WorldObjects, Preset.Stats })]
public class Crate { [Stat(Max = 100, AtMin = "broken")] public Stat Hp; }
// then one line per crate, on the room host
var crate = await room.Create<Crate>(at: pos, tune: c => c.Hp.Max = 250);// derived once, in the schema
@Entity('crate', { persistence: Persistence.Runtime, presets: [Preset.WorldObjects, Preset.Stats] })
export class Crate { @Stat({ max: 100, atMin: 'broken' }) hp: Stat; }
// then one line per crate, on the room host
const crate = await room.create<Crate>({ at: pos, tune: (c) => { c.hp.max = 250; } });# derived once, in the schema
@entity("crate", persistence=Persistence.RUNTIME, presets=[Preset.WORLD_OBJECTS, Preset.STATS])
class Crate:
hp = stat(max=100, at_min="broken")
# then one line per crate, on the room host
crate = await room.create(Crate, at=pos, tune={"hp.max": 250})即将推出 —— Go
Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
// declarations compile into the same pushed model — playserv push from the UE project or CI
UCLASS(PSEntity = (Name = "crate", Persistence = "Runtime", Presets = "world-objects, stats"))
class UCrate : public UObject
{
GENERATED_BODY()
UPROPERTY(PSStat = (Max = 100, AtMin = "broken")) FPSStat Hp;
};
// then one line per crate, on the room host (dedicated server / master-client)
Room->Entities->Of<UCrate>()->Create(FPSIdempotencyKey(CrateId),
[SpawnPosition](UCrate& Crate) { Crate.Position = SpawnPosition; }); // Position — from the world-objects preset
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.// derived once, in the schema
[Entity("crate", Persistence = Persistence.Runtime, Presets = new[] { Preset.WorldObjects, Preset.Stats })]
public class Crate { [Stat(Max = 100, AtMin = "broken")] public Stat Hp; }
// then one line per crate, on the room host
var crate = await room.Create<Crate>(at: pos, tune: c => c.Hp.Max = 250);模型
预设不引入任何新概念。 它添加的一切,都能用实体已有的手段表达:aspect、状态机、hook、持久化。
每个预设提供什么,在哪里调整。
| 预设 | 契约为它提供的内容 | 在哪里调整 |
|---|---|---|
stats | 一个数值特征 aspect,带上下限、恢复和修正值,外加一个到达阈值时触发的 hook——0 HP 变成一次状态机转换,而不是你代码里的一个 if | 字段的声明;数值在面板中仍可实时编辑 |
abilities | 一个技能集合 aspect、一个表示施放阶段的状态机,以及消耗和冷却 | 技能的声明 |
projectiles | 一个持久化模式为 runtime 的类型、一个弹道 aspect 和一个命中事件 | 投射物的声明——换掉一个特性就能改变飞行模型 |
drops | 一个 带权重的掉落表 aspect,以及一个在死亡之后运行的 hook | 掉落表的条目和权重 |
世界物件 | 一个可交互物件的状态机,以及一个表示交互条件的 aspect | 预设的声明,或在创建时按实例调整 |
| Inventory | 一个有归属者的类型,带有指向商品目录物品的 ref、一个可递增的堆叠 aspect,以及按归属者计算的上限,溢出行为需要声明 | 类型的声明 |
Inventory 也有自己单独的页面。
所有预设都成立的规则。
| 始终成立 | 含义 |
|---|---|
位置 | 以 aspect 的形式位于实体上:它的数据与其他数据一样同步,它的状态就是实体状态,它的 RPC 就是实体 RPC,它的 hook 按实体的 hook 顺序运行 |
调整 | 是实时配置,而不是重新部署,因此面板会在同一棵树中显示属性上限、冷却时间和掉落权重 |
派生自己的预设 | 是组合而不是子类化:Crate 就是 world-objects 加 stats,派生出的东西仍然是实体上的 aspect |
错误
- 声明属性、技能、投射物、掉落表或世界物件属于 schema 操作,需要
fn或adm。玩家密钥或客户端密钥尝试这样做会收到forbidden,不会声明任何内容,也不会只声明一半。这与玩家在允许发起的调用中遇到的拒绝不同,比如冷却中、无法支付、缺少item:key.bronze,这些拒绝各有自己的错误码。
预设的其余拒绝都与实体相同。 其中两种是预设特有的:
- 只填了一部分的预设,会在部署时校验失败。 预设承载的是一个自洽的整体:只声明一半的内容会在发布前就被拒绝,而不是在比赛中表现异常。
- 不能给预设打上与它自身机制相矛盾的标记:客户端没有规则的 aspect 不能声明为可预测,这同样会在部署时被发现。
限制
上限与实体相同:每个类型的 aspect 数、每个类型的状态机数、存储实例的大小、单个实例的变更频率。预设自己声明的唯一上限,是有归属者的预设所携带的按归属者计算的上限,它有三种边界行为可选,而且没有默认值:refuse · redirect(重定向到一个声明的归属者存储桶)· discard with event。具体数值将随平台限制一章给出。