跳到主要内容
实时游戏

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

Visibility

40 名玩家时,整房间快照没问题;到了 200 名就不行了。 可见区域决定谁收到什么,它是一个声明的谓词,而不是逐个对象拨动的开关。广播和按主体数据包是同一模型下声明的两种投递模式,所以在两者之间切换只需改配置,不必重写。它是一种通道优化,不是权限;权限请看 Access & Roles。

同一个声明的模型(谓词、分层、细节层级(detail tier))有两种读取方式:

广播按主体数据包
发送整个房间,发给所有人每个玩家只收到其规则选中的那一部分
适用小房间;这是默认模式人多的场景,数据包大小必须可预测
读取声明每个房间一次每个主体一次

不能泄露的内容根本不在数据包里,而不是在客户端上隐藏:从不发送,因此这是一项安全属性,而不是带宽属性。

何时使用

  • 房间规模超出了整房间广播的承受范围:200 名玩家需要的是每个客户端各自的邻近区域流,而不是每一个 delta。
  • 状态不能泄露:战争迷雾和仅所有者可见的字段应该根本不发送,而不是在客户端隐藏。
  • 多个会话共用一张地图(map),彼此不能看见:分层只是再加一个谓词。
  • 人多时数据包大小必须可预测:给对象数设上限并声明排序,“最近的 N 个”才是一项承诺,而不是密度碰巧造成的结果。
  • 位于边界的玩家必须能看到边界另一侧:声明哪些相邻区域可见,因为默认只有玩家自己所在的区域可见。
  • 无需使用:如果房间(room)很小。共享数据包这一投递模式(delivery mode)已经够用。

分工

主体在本页中的职责
schema-author声明可见性(visibility)谓词、对象上限及其排序、哪些相邻区域可见,以及投递模式
any订阅并接收区域允许的内容;可以在声明的范围内为自己调低对象上限

概览

在 Tank 上声明半径和分层规则;Ammo 在字段旁限定为仅所有者可见
[Entity("tank")]
[Visible(Radius = 60)]                           // spatial
[Visible(Rule.SameLayer)]                        // layers of one map
public class Tank
{
    [Sync] public Vector3 Position;
    [Sync(To = Scope.Owner)] public int Ammo;    // per-field scope
}
@Entity('tank')
@Visible({ radius: 60 })                       // spatial
@Visible(Rule.SameLayer)                       // layers of one map
export class Tank {
  @Sync() position!: Vector3;
  @Sync({ to: Scope.Owner }) ammo = 0;         // per-field scope
}
@entity("tank")
@visible(radius=60)                              # spatial
@visible(Rule.SAME_LAYER)                        # layers of one map
class Tank:
    position: Vector3 = sync()
    ammo: int = sync(to=Scope.OWNER)             # per-field scope
即将推出 —— Go

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

// multi-entry values are one quoted list (a specifier value is a single token)
UCLASS(PSEntity = "tank",
       PSVisible = "radius:60, rule:SameMapInstance")   // spatial + instances of one map
class UTank : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSSync)                  FVector3f Position;
    UPROPERTY(PSSync = (To = "Owner")) int32     Ammo;   // per-field scope
};
// 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("tank")]
[Visible(Radius = 60)]                           // spatial
[Visible(Rule.SameLayer)]                        // layers of one map
public class Tank
{
    [Sync] public Vector3 Position;
    [Sync(To = Scope.Owner)] public int Ammo;    // per-field scope
}

模型

可见性规则声明什么。

声明项含义
谓词规则本身,使用与访问谓词、状态转换守卫条件相同的谓词语言。半径、地图实例、队伍和所有权都只是谓词的特例,不是独立的机制:契约中只有这一种谓词语言
对象上限 及其排序规则可以限制对象数量,此时选择顺序需要声明,而不是推断:“最近的 N 个”等于谓词加上按距离排序再加上限。单靠谓词无法表达它,因为谓词回答的是“这一行是否符合”,而不是“符合的行里哪个更近”
相邻区域相邻房间或地图实例中的对象是否可见,以及具体哪些可见。绝不隐式生效:不声明时,区域就是接收方所在的那一个
投递模式共享数据包:所有人收到同样的内容,CPU 开销低。按主体数据包:每个接收方按自己的区域收到专属数据包;CPU 开销高,但在大规模人数下必不可少

所有区域都成立的事实。

始终成立含义
可见性不是权限:区域隐藏的内容可能凭权限(permission)拿到,反之亦然(见下文)
接收方可以调低上限:在声明的最大值以内,且绝不低于声明的最小值。因为对上下文匹配的所有人来说谓词都一样,而数据包大小是接收方自己的问题
截断可观测:接收方会得知数据包被截断,以及按哪种排序截断。禁止静默截断,因为它和“没有更多对象”无法区分
降级需要声明:组装按主体数据包的预算耗尽时,平台按声明回退到共享数据包,而不是开始随意丢掉接收方
数据包形态只是一项弱承诺:数据包的大小和构成不应让人推断出隐藏对象的存在

可见性不是权限

区域是通道优化,不是访问控制。不要把两者混为一谈:否则战争迷雾设置会悄悄扩大权限,或者权限会开始取决于距离。数据包大小可能暗示隐藏对象的存在,所以在隐藏对象的存在本身不能泄露的地方,请用 Access & Roles 权限,而不是区域。

“谁看得到这个?”和“他们看到什么?”都可以查询。按主体数据包的开销是一等的读取项,代码和面板中都能读取。

错误

  • 未声明的订阅目标属于校验拒绝。
  • 没有订阅权限时返回 forbidden 或 not found,取决于目标是否存在本身是否是秘密:拒绝结果本身不能泄露它拒绝的是什么。
  • 无法解析的恢复位置属于错误请求,而不是静默地从当前时刻重新开始。
  • 平台关闭的订阅和订阅数量用尽,都属于冲突。
  • 扩大视图只能在 fn 中进行。 授予覆盖视图和设置细节层级对玩家会话一律以 forbidden 拒绝,其视图保持不变:观战客户端无法扩大自己的授权。
  • 调用方视图排除在外的实例返回 not found,与不存在的实例答复相同;返回 forbidden 就等于确认墙后确实有东西。
  • 读取按主体数据包开销只能在 fn adm 中进行:即云函数或面板,绝不是客户端。

限制

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

  • 按主体数据包的开销:耗尽时按声明降级到共享数据包并发出通知,绝不随意丢掉接收方。
  • 每条规则的对象数:按声明的排序设上限,并带有可观测的截断标记。
  • 每个主体的订阅数:拒绝新订阅,已有订阅继续有效。
  • Delta 大小:delta 拆分而不截断,拆分可观测。
  • 发送速率:只是上限,不是保证。