Stage 6: Прочее

Движение между секторами

2.5D Renderer

2D Renderer

Управление камерой WASD и ZX

До этого предмет был статичным: мы положили его в sector.itemsи при отрисовке сектора доставали его оттуда. Для аптечки на полу этого достаточно. Но враг, ракета или подвижный бонус могут перейти через портал в соседний сектор, и тогда хранить его только в исходном секторе уже нельзя.

Для движущихся сущностей удобнее думать не о “предмете сектора”, а об actor с текущей мировой позицией. Сектор становится вычисляемым состоянием: после движения мы снова определяем, в каком секторе находится точкаx/y, и берем оттудаfloorHeight,ceilHeight и яркость.

  interface Actor extends Item {
    sector?: Sector;
    floorZ?: number;
    ceilZ?: number;
    vx: number;
    vy: number;
  }

При движении actor сначала проверяет новую позицию: можно ли туда пройти, не пересекается ли радиус со стеной, хватает ли высоты между полом и потолком. Если позиция допустима, actor меняет координаты и перепривязывается к сектору, найденному под его центром.

  function moveActor(actor: Actor, dx: number, dy: number, level: Level) {
    const nextX = actor.x + dx;
    const nextY = actor.y + dy;
    const nextSector = findSectorAt(level, nextX, nextY);

    if (!nextSector) {
      return;
    }

    actor.x = nextX;
    actor.y = nextY;
    actor.sector = nextSector;
    actor.floorZ = nextSector.floorHeight ?? 0;
    actor.ceilZ = nextSector.ceilHeight ?? actor.floorZ;
  }

Для рендера есть два рабочих варианта. Первый — каждый кадр собирать временную таблицуactorsBySector из глобального списка actors. Второй — хранить двусвязные списки actors прямо в секторах и обновлять эти списки только при переходе между секторами. В обоих случаях во время обхода BSP мы рисуем actors того сектора, который сейчас стал видимым.

  const actorsBySector = new Map<Sector, Actor[]>();

  for (const actor of level.actors) {
    const sector = actor.sector ?? findSectorAt(level, actor.x, actor.y);
    if (!sector) continue;

    const actors = actorsBySector.get(sector) ?? [];
    actors.push(actor);
    actorsBySector.set(sector, actors);
  }

В DOOM использовалась очень похожая идея, только в более практичной форме. Подвижные объекты называлисьmobj_t: у них былиx/y/z, радиус, высота, текущий subsector и ссылки для списков. Сектор находился по текущим координатам черезR_PointInSubsector(x, y), а уже из subsector брали sector.

Важное разделение: секторные списки DOOM были нужны рендеру, а не игровой симуляции. Для столкновений и поиска соседних объектов actor дополнительно связывался с blockmap — сеткой мира, где каждая ячейка знала о линиях и объектах внутри нее. Поэтому переход врага между секторами был не “перекладыванием между комнатами” вручную, а обновлением пространственных ссылок после изменения координат.

Для нашего рендера это означает: статичные предметы могут оставаться вsector.items, но движущиеся сущности лучше хранить отдельно, например в level.actors. Каждый тик обновляет их позицию и текущий сектор, а рендер использует этот сектор только как источник высоты, освещения и принадлежности к видимой области.

Источники по DOOM:комментарии к mobj_tисоздание mobj и P_SetThingPosition.