跳到主要内容
实时游戏

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

Collision

把变换绑定到障碍物地图,并声明接触时发生什么。 Collision 模块运行在平台的模拟内部。你声明碰撞体、层和响应,再订阅接触事件。

何时使用

  • 移动中的实体(entity)需要在服务器端处理接触(滑动、停止、反弹),又不想手写偏转逻辑。
  • 玩法要对触碰做出反应:重叠时拾取物品,触发区域驱动实体的状态机。
  • Locomotion 和投射物需要针对地图的障碍物集合做扫掠检测。
  • 放置预览或瞄准需要查询“这里放得下吗?”,以及查询体积重叠。
  • 无需使用:如果游戏里没有任何物体发生物理接触。基于记录的请求/响应式玩法用普通的 Data & Subscriptions 即可。

分工

主体在本页中的职责
room-owner声明碰撞体、层和响应;查询重叠和接触

适用于哪些房间

本模块只在由平台推进模拟的地方运行,即声明了 Host = "Backend" 的房间(room)。如果模拟归你自己的游戏服务器所有(PlayServ 作为元服务器),移动、碰撞和预测都留在引擎一侧,本页描述的是平台托管这一备选方案,而不是必须遵守的要求。

概览

形状、层和接触时的行为都直接声明在碰撞体上,不存在在别处单独声明的层对:

坦克上的胶囊体 Body,位于 vehicles 层:沿墙滑动,穿过可拾取物品,与箱子接触时逐次决定
[Entity("tank")]
public class Tank
{
    [Sync] public Vector3 Position;

    [Body(Shape.Capsule, Radius = 0.6f, Layer = "vehicles")]
    [CollidesWith("walls", Response.Slide)]
    [CollidesWith("pickups", Response.Pass)]        // reported, motion passes through
    public Body Body;
}
@Entity('tank')
export class Tank {
  @Sync() position!: Vector3;

  @Body({ shape: 'capsule', radius: 0.6, layer: 'vehicles' })
  @CollidesWith('walls', Response.Slide)
  @CollidesWith('pickups', Response.Pass)        // reported, motion passes through
  body: Body;
}
@entity("tank")
class Tank:
    position: Vector3 = sync()

    body = collision.body(shape="capsule", radius=0.6, layer="vehicles",
                           collides_with=[
                               ("walls", Response.SLIDE),
                               ("pickups", Response.PASS),   # reported, motion passes through
                           ])
即将推出 —— Go

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

// the declaration rides inside the engine's own reflection macros, in the specifier position —
// UHT reads it from the header text, and the member is a reflected property at the same time
UCLASS(PSEntity = "tank")
class UTank : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSSync)
    FVector3f Position;

    // walls slide, pickups report the contact and let motion pass through —
    // multi-entry values are one quoted list (a specifier value is a single token)
    UPROPERTY(PSBody = (Shape = "Capsule", Radius = "0.6", Layer = "vehicles"),
              PSCollidesWith = "walls:Slide, pickups:Pass")
    FPSBody Body;
};
// 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")]
public class Tank
{
    [Sync] public Vector3 Position;

    [Body(Shape.Capsule, Radius = 0.6f, Layer = "vehicles")]
    [CollidesWith("walls", Response.Slide)]
    [CollidesWith("pickups", Response.Pass)]        // reported, motion passes through
    public Body Body;
}

接触是一个事件(event),模块订阅它。接触上没有 hook:接触出现时,这一步已经把它处理完了,没有什么可以拒绝的。如果工作室需要不同的规则,可以用自己的实现覆盖可达性检查和路径检查(见 Extensibility),响应仍然由声明决定。

模型

碰撞体声明什么。

声明项含义
shape取自封闭集合的原语(primitive):球体、胶囊体、盒体,尺寸由声明给出。不支持任意网格,限制和原因都与地图中服务器的世界模型相同
所在位置与变换一起放在同一个 aspect 上:aspect 是策略的单位,碰撞体与它的变换同生共死
路径检查方式stepwise:只检查这一步的最终位置,速度快。swept:检查两个位置之间的线段,开销更大,但能排除一步之内的穿透(tunneling)。检查方式由声明决定,实现不会根据速度自行选择
参与的区域碰撞体计入哪些区域体积
与美术模型的关系不要求有关系:碰撞体只是简化形状,在声明的范围内允许与美术模型不一致

高速的 stepwise 碰撞体会穿过薄障碍物

stepwise 只检查一步结束时的位置,所以高速碰撞体可能直接穿过薄障碍物。swept 检查两个位置之间的线段,排除一步之内的穿透。投射物能不能飞穿墙壁,是你的游戏自身的属性,由你来声明。

响应声明在一对组合上,即“障碍物的通行类型 × 碰撞体类型”,取值来自封闭集合:

响应含义
stop移动停在最后一个可达位置
slide沿障碍物继续移动,保留其中可达的分量
bounce方向反射,速度乘以声明的系数
damp继续移动,速度乘以声明的比例
pass障碍物不影响移动,但接触仍然可以观察到
cease to exist实体消失,例如投射物撞上墙壁

这些系数是声明的值,而不是根据质量和材质算出来的:这份契约里既没有质量,也没有材质。

每次检查都成立的规则。

始终成立含义
每一对组合都有响应:缺少某一对属于声明(declaration)缺陷,部署时就会拒绝,不会等到战斗中才暴露
响应表客户端可读:它就是权威(authority)一方计算时用的那张表,所以拿到同一份声明的客户端和服务器,对同一个接触给出相同的结果
在同一权威方内可复现,跨平台不保证在同一个进程、同一个 build 内,相同顺序的相同输入得到相同结果。不保证不同平台、不同 build 之间结果逐位一致
同时性由声明决定:两个移动的碰撞体在同一步内相撞时,处理顺序是声明好的、确定的。存储遍历顺序、输入到达顺序和随机数都不能作为依据
一次接触,一个事实两个碰撞体之间的接触,双方观察到的是带同一个标识符的同一个事实,而不是两个独立事件
扩展点在步进上,不在接触上步进之前可以修改变换,步进之后只能观察。覆盖(override)后的可达性检查和路径检查也必须对客户端可用
本模块自己不移动任何东西:它只回答某个位置是否可达、响应是什么,真正执行由移动负责
接触是事件所以模块之间靠订阅而不是耦合:陷阱的状态机把状态转换绑定到进入触发区域,掉落物在重叠时被拾取,投射物通过本模块的扫掠检测判定命中

错误

  • “不可达”是一个回答,而不是错误,并且会指明是三种原因中的哪一种:超出边界、被静态障碍物占据,或被其他实体的碰撞体占据。客户端对这三种情况的反应各不相同:掉头、绕行或等待。
  • 声明错误在部署时暴露,不会拖到第一次接触:形状不在封闭集合内的碰撞体、放在没有变换的 aspect 上的碰撞体,以及没有声明响应的组合,都会在部署时被拒绝。
  • 找不到实体或位置时返回 not found,重试没有意义。
  • 超出检查频率时返回限流类错误,并附带一个时间点,在此之前重试没有意义。

限制

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

  • 房间内的碰撞体数量:再声明一个会以冲突被拒绝;不会为了腾出空间而移除已有的碰撞体。
  • 每步的接触数量:超出部分绝不静默丢弃。要么拒绝这一步,要么按声明的顺序截断。
  • 碰撞体尺寸与地图网格步长:部署时就会拒绝,因为比高度场步长还小的碰撞体会从地形中掉下去。
  • 一个碰撞体可同时所在的区域数量:超出时在部署时被拒绝。
  • 每个主体的检查频率:限流,并附带时间点。
  • “这个区域里有谁”的回答中的碰撞体数量:按声明的顺序截断,并且必须带截断标志。