Перейти к основному содержимому
Живая игра

Эта страница ещё не переведена — показана английская версия.

Map

Статичный мир: границы, рельеф, препятствия и вопрос «куда что можно поставить?». Физическая модель намеренно гораздо проще визуальной: примитивы с площадью основания и высотой, слои с правилами и один запрос допустимой позиции, которым пользуются все остальные модули.

Когда это нужно

  • Вам нужен статичный мир — границы, рельеф, препятствия, — к которому сервер может обращаться с запросами, а не только отрисовывать его.
  • Точки появления, выпадающие предметы и декорации должны попадать в допустимые места: для этого есть один запрос RandomPosition на правилах, и обойти его нельзя.
  • Арены должны генерироваться заново на каждый матч: объявленный Seed воспроизводит ту же карту (map) в баг-репорте.
  • Ящики и стены ломаются и появляются снова: разрушаемые объекты с HP и таймерами респауна.
  • Ботам (bot) и снарядам нужны raycast и проверка прямой видимости относительно набора препятствий.
  • Не нужно, если мир чисто визуальный и серверный код не спрашивает, куда что можно поставить.

Кто что делает

АкторЧто делает на этой странице
schema-authorобъявляет карты, примитивы препятствий, разрушаемые объекты, слои и их правила
room-ownerпривязывает карту к комнате (room); запрашивает позиции появления; делает raycast
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 — генераторы расстановки, а не броски кубика во время игры. Генератор срабатывает при публикации версии карты: сорок камней превращаются в сорок объявленных примитивов, и в опубликованной версии лежат примитивы, а не правило. Поэтому один и тот же Seed даёт те же сорок камней в матче, в повторе и в баг-репорте. Геометрические ограничения проверяются один раз, на этом готовом наборе, ещё до того, как версия попадёт в окружение (environment).

Запрос, которым пользуются все остальные:

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
Запрос читает опубликованный статический слой и динамические препятствия одного экземпляра; ответ верен для названного момента

Два слоя, и объявляют их разные люди.

СлойЧто в нём и кто его объявляет
статическийрельеф с высотами, примитивы препятствий, границы и локации — авторский контент
динамическийпрепятствия, которые во время игры приносят сущности: двери, разрушаемые объекты, платформы. Поэтому разрушаемый объект — это сущность с жизненным циклом, состояниями и владельцем, и картой она становится только в той части, где приносит препятствие. Статичный камень объявляется в карте, а дверь — это сущность (entity), которая приносит препятствие. Собственного жизненного цикла здесь у них нет: он принадлежит сущности

Что объявляет карта.

Что объявляетсяЧто это
key и versionкарта — это авторский контент: она объявляется в коде, адресуется по key и версионируется, причём версия входит в то, на что ссылается комната. Менять геометрию выпущенной версии запрещено: любая правка — это новая версия
рельефкарта высот — регулярная сетка с объявленным шагом. Шаг — это объявленный предел точности, поэтому запрос высоты отвечает с этой точностью, а не точно. Рельефа может не быть: арена в пустоте вполне допустима
препятствиязакрытый набор примитивов: параллелепипед, сфера, капсула и выпуклая оболочка с объявленным лимитом вершин. Произвольный треугольный меш не принимается — только при этом условии проверка на сервере вообще возможна
тип проходимости для каждого препятствиянепроходимое · проходимое для объявленного класса · только перекрывает прямую видимость. Один и тот же примитив служит и стеной, и кустом, а разница объявляется, а не моделируется дважды
границыобъём, за пределами которого позиция недопустима
ярусы мираобъявленные пространственные ярусы внутри карты — земля, подземелье, воздух. Это объявления геометрии и адресации
локацииименованные места или области — точка появления, зона захвата, коридор. Локация отвечает на вопрос где, но никогда — что происходит: игровой логики в ней нет
генератор расстановкипри желании — правило, которое порождает примитивы: количество, минимальное расстояние, область, сид. Он срабатывает при публикации версии, детерминированно по сиду, и после этого карта хранит примитивы, а не правило

Ярус мира и экземпляр карты — никогда не синонимы.

Что это
ярус мираобъявление внутри карты — земля, подземелье, воздух
экземпляр картынезависимая копия опубликованной карты во время игры. Экземпляры делят неизменяемую опубликованную геометрию, но динамические препятствия и списки сущностей у них свои. Комната занимает экземпляр и может выбрать ярусы внутри него

Что верно для любого запроса.

ВсегдаЧто это
единый геометрический канонвся геометрия задаётся в объявленном платформой каноне координат, а точность каждого геометрического поля (field) объявляется на самом поле
модель мираэто упрощение: геометрия на сервере — не арт-модель и не обязана ею быть
ответ называет свой экземпляр и моментна запрос отвечают по статическому слою карты и динамическим препятствиям того экземпляра, о котором спросили, и в ответе указан момент, для которого он верен. Динамические препятствия меняются, поэтому ответ — это снапшот
значенияотносятся к managed, а не к seed: геометрию не подкручивают каждый день, поэтому правка из админ-консоли отклоняется, а не сохраняется молча
движение и контактздесь не обрабатываются: карта отвечает, что представляет собой пространство. Допустима ли позиция и какой будет реакция, решают столкновения, а применяет это к движению перемещение

Ошибки

  • Не найдена карта, версия или экземпляр — ответ not found; так же отвечает и отозванная версия. Повторять запрос бессмысленно.
  • Публикация изменённой геометрии под существующей версией — конфликт: сделайте новую версию.
  • Ошибки публикации возникают при объявлении и при деплое, но никогда во время игры: карта сверх лимита примитивов, выпуклая оболочка сверх лимита вершин, произвольный меш в роли препятствия — всё это отказы при валидации ещё до выпуска.
  • Запрос высоты за границами — не ошибка, а объявленный ответ «за границами». Его можно отличить от ответа «внутри препятствия», потому что в одном случае клиент разворачивается, а в другом — объезжает.
  • Превышена частота запросов — ответ из категории ограничения частоты с указанием, когда можно повторить.

Ограничения

Конкретные числа появятся в главе об ограничениях платформы, а что происходит при достижении каждого потолка, определено уже сейчас.

  • Примитивов препятствий на карту, вершин выпуклой оболочки, разрешение карты высот, размер границ, локаций на карту: всё это отклоняется при публикации, а не во время запроса.
  • Экземпляров на карту: создание ещё одного отклоняется как конфликт; существующие экземпляры ради свободного места никогда не освобождаются.
  • Хранимые версии: отзывается самая старая версия в статусе deprecating, но версия, на которой работает живая комната, — никогда.
  • Частота запросов к пространству: ограничение частоты с указанием, когда можно повторить.