本页尚未翻译,当前显示英文版本。
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[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 assetScatter 和 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.var spawn = map.RandomPosition(r =>
{
r.Layer("ground");
r.AwayFrom(players, minDistance: 12);
r.NoRepeat(lastN: 3);
});模型
两个层,由不同的人声明。
| 层 | 包含什么,由谁声明 |
|---|---|
静态 | 带高度的地形、障碍物原语、边界和位置点:由人编写的内容 |
动态 | 运行时由实体带来的障碍物:门、可破坏物体、平台。因此可破坏物体是一个有生命周期、状态和所有者的实体,只有它带来障碍物的那部分才属于地图:静态岩石在地图中声明,门则是带来一个障碍物的实体。这些障碍物在这里没有自己的生命周期:生命周期属于实体 |
地图声明什么。
| 声明项 | 含义 |
|---|---|
key 和 version | 地图是编写的内容:在代码中声明,按 key 寻址,并带版本;版本是房间所引用内容的一部分。禁止修改已发布版本的几何;修改即是新版本 |
地形 | 一个高度场:步 长声明好的规则网格,步长就是声明的精度上限,所以高度查询按步长作答,而不是精确值。地形可以不存在:虚空中的竞技场也合法 |
障碍物 | 一个封闭集合的原语:盒体、球体、胶囊体、声明了顶点上限的凸包。不接受任意三角网格,这是服务器端检查能够成立的前提 |
每个障碍物的 通行类型 | 不可通行 · 对声明的类别可通行 · 只阻挡视线。同一个原语既可以当墙,也可以当灌木,区别通过声明表达,而不是建模两次 |
边界 | 位置超出这个体积即不可接受 |
世界层 | 地图内部声明的空间层:地面、地下、空中。它们是几何与寻址方面的声明 |
位置点 | 命名的地点或区域:生成点、占领区、走廊。位置点回答在哪里,从不回答发生什么:它不携带任何游戏逻辑 |
摆放生成器 | 可选:一条生成原语的规则,包括数量、最小间距、区域、种子。它在版本发布时求值,由种子确定性地求出,此后地图保存的是原语而不是规则 |
世界层和地图实例绝不是同义词。
| 含义 | |
|---|---|
世界层 | 地图内部的一项声明:地面、地下、空中 |
地图实例 | 已发布地图的一份独立运行时副本。各实例共享不可变的已发布几何,各自拥有独立的动态障碍物和独立的实体名单。房间占用一个实例,并可以选择其中的若干层 |
所有查询都成立的事实。
| 始终成立 | 含义 |
|---|---|
统一的几何规范 | 所有几何都使用平台声明的坐标规范,每个几何字段的精度都在该字段上声明 |
世界模型 | 是一种简化:服务器的几何不是美术模型,也没有义务是 |
答案注明其实例和时刻 | 查询基于地图的静态层加上所问实例的动态障碍物作答,并声明答案成立的时刻:动态障碍物会变化,所以答案是一份快照 |
取值 | 属于 managed,而不是 seed:几何不是日常调参项,所以平台拒绝来自管理控制台的修改,而不是悄悄保留 |
移动与接触 | 不在这里处理:地图只回答空间是什么;位置是否可接受 、如何响应,归碰撞负责,把结果应用到运动上则归移动负责 |
错误
- 找不到的地图、版本或实例返回 not found,已撤回的版本也一样,重试没有意义。
- 在已有版本下发布改动过的几何属于冲突:请创建新版本。
- 发布失败发生在声明和部署阶段,绝不在运行时:超出原语上限的地图、超出顶点上限的凸包、用任意网格作障碍物,都会在上线前以校验拒绝的形式报出。
- 在边界外做高度查询不是错误:它返回声明好的答案“在边界外”,并且与“在障碍物内”可以区分,因为客户端遇到前者要掉头,遇到后者要绕行。
- 超出查询速率时返回速率限制类别的错误,并附带截止时间。
限制
具体数值将随平台限制一章给出;到达每个上限时的行为现在已经确定。
- 每张地图的障碍物原语数、凸包顶点数、高度场分辨率、边界大小、每张地图的位置点数:这些都在发布时拒绝,而不是在查询时。
- 每张地图的实例数:再创建会按冲突拒绝;已有实例绝不会为了腾出空间而释放。
- 保留的版本数:撤回最旧的已弃用版本,但有实时房间在用的版本永远不会撤回。
- 空间查询的速率:速率限制,并附带截止时间。