跳到主要内容
实时游戏

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

Map

静态世界:边界、地形、障碍物,以及“东西能放在哪里?”。 物理模型有意做得比视觉模型简单得多:带占地面积和高度的原语、带规则的层,以及一个其他所有模块都复用的有效位置查询。

何时使用

  • 你需要一个服务器能查询、而不只是渲染的静态世界(边界、地形、障碍物)。
  • 生成(spawn)、掉落和装饰物必须落在合法位置:只用一个基于规则的 RandomPosition 查询,没有绕过的办法。
  • 竞技场应该每局重新生成:声明的 Seed 能在 bug 报告里复现同一张地图(map)。
  • 箱子和墙会被打碎,还会复原:即带生命值和重生计时器的可破坏物体(destructible)。
  • 机器人(bot)和投射物需要针对障碍物集合做射线检测和视线判断。
  • 无需使用:如果世界纯粹是视觉效果,没有任何服务器代码询问东西能放在哪里。

分工

主体在本页中的职责
schema-author声明地图、障碍物原语、可破坏物体、层及其规则
room-owner把地图绑定到房间;请求生成位置;做射线检测
operator在面板中放置或移除障碍物和层

概览

声明 arena 布局:种子和边界、地形、岩石、会重生的箱子、一个规则层
[Map("arena", Seed = 42, Bounds = "160x160")]
public static class Arena
{
    [Terrain(HeightNoise = 0.3f)]                 public static Terrain Height;  // 3D height field
    [Scatter("rock", Count = 40, MinSpacing = 6)] public static ObstacleSet Rocks;
    [Destructible("crate", Count = 12, Hp = 100, RespawnAfter = "30s")] public static ObstacleSet Crates;
    [Layer("ground", NotInside = "water")]        public static Layer Ground;
}
// or: Maps.Named("arena-caves-v3")  — authored in the panel or loaded from an asset
@Map('arena', { seed: 42, bounds: '160x160' })
export class Arena {
  @Terrain({ heightNoise: 0.3 }) height: Terrain;                   // 3D height field
  @Scatter('rock', { count: 40, minSpacing: 6 }) rocks: ObstacleSet;
  @Destructible('crate', { count: 12, hp: 100, respawnAfter: '30s' }) crates: ObstacleSet;
  @Layer('ground', { notInside: 'water' }) ground: Layer;
}
// or: Maps.named('arena-caves-v3')  — authored in the panel or loaded from an asset
@Map("arena", seed=42, bounds="160x160")
class Arena:
    height = terrain(height_noise=0.3)                  # 3D height field
    rocks = scatter("rock", count=40, min_spacing=6)
    crates = destructible("crate", count=12, hp=100, respawn_after="30s")
    ground = layer("ground", not_inside="water")
# or: maps.named("arena-caves-v3")  — authored in the panel or loaded from an asset
即将推出 —— Go

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

USTRUCT(PSMap = (Name = "arena", Seed = 42, Bounds = "160x160"))
struct FArena
{
    GENERATED_BODY()

    UPROPERTY(PSTerrain = (HeightNoise = "0.3"))                             FPSTerrain Height;  // 3D height field
    UPROPERTY(PSScatter = (Obstacle = "rock", Count = 40, MinSpacing = 6))   FPSObstacleSet Rocks;
    UPROPERTY(PSDestructible = (Obstacle = "crate", Count = 12, Hp = 100,
                                RespawnAfter = "30s"))                       FPSObstacleSet Crates;
    UPROPERTY(PSStratum = (Name = "ground", NotInside = "water"))            FPSStratum Ground;
};
// or: PS::Maps::Named(TEXT("arena-caves-v3")) — authored in the panel or loaded from an asset
// 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 语法
[Map("arena", Seed = 42, Bounds = "160x160")]
public static class Arena
{
    [Terrain(HeightNoise = 0.3f)]                 public static Terrain Height;  // 3D height field
    [Scatter("rock", Count = 40, MinSpacing = 6)] public static ObstacleSet Rocks;
    [Destructible("crate", Count = 12, Hp = 100, RespawnAfter = "30s")] public static ObstacleSet Crates;
    [Layer("ground", NotInside = "water")]        public static Layer Ground;
}
// or: Maps.Named("arena-caves-v3")  — authored in the panel or loaded from an asset

Scatter 和 Destructible 是摆放生成器,不是运行时随机。生成器在地图版本发布时求值:四十块岩石变成四十个声明的原语(primitive),发布的版本携带的是原语,而不是规则。因此同一个 Seed 在对局、回放和 bug 报告中得到的都是同样的四十块岩石;几何限制也只在这个求值后的集合上检查一次,在版本进入环境之前完成。

其他所有模块都会调用的查询:

RandomPosition:在地面上公平生成,远离玩家,绝不重复
var spawn = map.RandomPosition(r =>
{
    r.Layer("ground");
    r.AwayFrom(players, minDistance: 12);
    r.NoRepeat(lastN: 3);
});
const spawn = map.randomPosition((r) => {
  r.layer('ground');
  r.awayFrom(players, { minDistance: 12 });
  r.noRepeat({ lastN: 3 });
});
spawn = map.random_position(rules=lambda r: (
    r.layer("ground"),
    r.away_from(players, min_distance=12),
    r.no_repeat(last_n=3),
))
即将推出 —— Go

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

// Dedicated-server host: place a spawn through the same rule-based query
Map->Positions->GetRandom({ .Stratum = PSKeys::Strata::Ground,
                            .AwayFrom = Players,
                            .MinDistance = 12.f,
                            .NoRepeatLastN = 3 },
    TPSOnResult<FVector>::CreateLambda([](const TPSResult<FVector>& Result)
    {
        if (!Result.HasValue()) { return; }
        PlaceSpawn(Result.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 — 主客户端构建运行同样的查询;普通客户端则在得到的位置上生成
var spawn = map.RandomPosition(r =>
{
    r.Layer("ground");
    r.AwayFrom(players, minDistance: 12);
    r.NoRepeat(lastN: 3);
});

模型

static layer — the published map versionone map instancea query — RandomPosition, a raycastan answer, true for a named momentdynamic obstacles brought by entities shared, immutable geometrythis instance only
查询读取已发布的静态层加上某个实例的动态障碍物;答案只对指定时刻成立

两个层,由不同的人声明。

层包含什么,由谁声明
静态带高度的地形、障碍物原语、边界和位置点:由人编写的内容
动态运行时由实体带来的障碍物:门、可破坏物体、平台。因此可破坏物体是一个有生命周期、状态和所有者的实体,只有它带来障碍物的那部分才属于地图:静态岩石在地图中声明,门则是带来一个障碍物的实体。这些障碍物在这里没有自己的生命周期:生命周期属于实体

地图声明什么。

声明项含义
key 和 version地图是编写的内容:在代码中声明,按 key 寻址,并带版本;版本是房间所引用内容的一部分。禁止修改已发布版本的几何;修改即是新版本
地形一个高度场:步长声明好的规则网格,步长就是声明的精度上限,所以高度查询按步长作答,而不是精确值。地形可以不存在:虚空中的竞技场也合法
障碍物一个封闭集合的原语:盒体、球体、胶囊体、声明了顶点上限的凸包。不接受任意三角网格,这是服务器端检查能够成立的前提
每个障碍物的 通行类型不可通行 · 对声明的类别可通行 · 只阻挡视线。同一个原语既可以当墙,也可以当灌木,区别通过声明表达,而不是建模两次
边界位置超出这个体积即不可接受
世界层地图内部声明的空间层:地面、地下、空中。它们是几何与寻址方面的声明
位置点命名的地点或区域:生成点、占领区、走廊。位置点回答在哪里,从不回答发生什么:它不携带任何游戏逻辑
摆放生成器可选:一条生成原语的规则,包括数量、最小间距、区域、种子。它在版本发布时求值,由种子确定性地求出,此后地图保存的是原语而不是规则

世界层和地图实例绝不是同义词。

含义
世界层地图内部的一项声明:地面、地下、空中
地图实例已发布地图的一份独立运行时副本。各实例共享不可变的已发布几何,各自拥有独立的动态障碍物和独立的实体名单。房间占用一个实例,并可以选择其中的若干层

所有查询都成立的事实。

始终成立含义
统一的几何规范所有几何都使用平台声明的坐标规范,每个几何字段的精度都在该字段上声明
世界模型是一种简化:服务器的几何不是美术模型,也没有义务是
答案注明其实例和时刻查询基于地图的静态层加上所问实例的动态障碍物作答,并声明答案成立的时刻:动态障碍物会变化,所以答案是一份快照
取值属于 managed,而不是 seed:几何不是日常调参项,所以平台拒绝来自管理控制台的修改,而不是悄悄保留
移动与接触不在这里处理:地图只回答空间是什么;位置是否可接受、如何响应,归碰撞负责,把结果应用到运动上则归移动负责

错误

  • 找不到的地图、版本或实例返回 not found,已撤回的版本也一样,重试没有意义。
  • 在已有版本下发布改动过的几何属于冲突:请创建新版本。
  • 发布失败发生在声明和部署阶段,绝不在运行时:超出原语上限的地图、超出顶点上限的凸包、用任意网格作障碍物,都会在上线前以校验拒绝的形式报出。
  • 在边界外做高度查询不是错误:它返回声明好的答案“在边界外”,并且与“在障碍物内”可以区分,因为客户端遇到前者要掉头,遇到后者要绕行。
  • 超出查询速率时返回速率限制类别的错误,并附带截止时间。

限制

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

  • 每张地图的障碍物原语数、凸包顶点数、高度场分辨率、边界大小、每张地图的位置点数:这些都在发布时拒绝,而不是在查询时。
  • 每张地图的实例数:再创建会按冲突拒绝;已有实例绝不会为了腾出空间而释放。
  • 保留的版本数:撤回最旧的已弃用版本,但有实时房间在用的版本永远不会撤回。
  • 空间查询的速率:速率限制,并附带截止时间。