2D-сцены (.scene2d.ts)

2D-сцены как данные: один файл экспортирует по умолчанию defineScene2d({...}) — узлы по ключам-именам, у каждого один блок-источник (sprite / tilemap / camera / make, или ни одного = групповой узел), трансформ, аспекты use(...) и children. Та же грамматика и те же экранирования, что у 3D-файлов .scene.ts (asset, use, ref, make, $expr переживает круговой обход); визуальный редактор 2D-сцен (lecodes-2d-editor) читает и пишет эти файлы.

Обзор

TypeScript
// forest.scene2d.ts — листы те же, что на странице спрайт-листов
import props from './props.sprite'
import hero from './hero.sprite'

export default defineScene2d({
  env: { background: '#5a6b3b', pixelPerfect: true, gravity: [0, 0] },
  layers: { 1: { ySort: true } },
  nodes: {
    ground: {
      tilemap: {
        texture: asset('./tiles.png'), tile: 32, atlas: [8, 4],
        cells: cells('BgAAAAQAAAAHAAAAAAAAAAQAAAABAAAAAgAAAAAAAAABAAAAAQAAAAIAAAACAAAAAQAAAAEAAAAHAAAAAAAAAA=='),
      },
      layer: 0,
    },
    tree1: { sprite: { sheet: props, name: 'tree' }, position: [160, 96], layer: 1 },
    player: {
      sprite: { sheet: hero, clip: 'idle' },
      position: [80, 64], layer: 1,
      aspects: [use(CameraFollow, { smooth: 0.12 })],
    },
  },
})
TypeScript
// main.ts
import forest from './forest.scene2d'
forest.open().then(({ scene, nodes }) => {   // nodes.player — типизированный Sprite (+ .anim)
  nodes.player.anim.play('walk', [1, 0])
})

Экспорт по умолчанию — Scene2dHandle: load() инстанцирует один раз (идемпотентно — следующие вызовы возвращают те же { scene, nodes }), open() загружает и делает сцену активной 2D-сценой, def — определение. nodes держит каждый узел по его ключу, включая детей; name каждого узла — его ключ.

Блоки-источники

  • sprite — одна именованная запись импортированного листа .sprite.ts: { sheet, name?, clip?, direction? }. name необязателен, когда лист объявляет ровно один спрайт. Листы — только АРТ: узел, который должен сталкиваться, получает aspects: [use(Shape2D, { box: [14, 9] }), use(Physics2D, { motion: 'static' })] (семантика мира принадлежит сцене).
  • tilemap — { texture, tile, atlas, cells }. cells('…') — непрозрачный однострочный payload (размеры + RLE-ячейки Int32, base64); рукописный код может вместо него передать любое значение { cols, rows, data }, а encodeCells(cols, rows, data) собирает строку payload (генераторы карт). Строка 0 — верхняя; позиция узла — левый нижний угол карты. С аспектом Autotile2D на узле ячейки — это id материалов, а не индексы атласа.
  • camera — маркер, чья мировая позиция (+ zoom) задаёт стартовый вид. Он ни за чем не следует; для этого прикрепи CameraFollow к отслеживаемому узлу. Верхнеуровневый проп camera: { position, zoom } — альтернатива без узла (узел камеры побеждает).
  • make — make(factoryFn, { ...literal args }): поддерево, собранное кодом (фабрика возвращает Node2D или промис с ним), запускается после того, как все узлы существуют, чтобы аргументы ref() разрешились. Возвращённый узел становится дочерним этому и добавляется в сцену вместе со своими детьми.
  • ни одного — обычный групповой узел / маркер (зоны Trigger2D, точки спавна).
TypeScript
cells(payload: string): { cols, rows, data: Int32Array }   // декодирует; бросает на повреждённом payload
encodeCells(cols, rows, data: ArrayLike<number>): string   // кодирует; data.length должен быть cols*rows

Пропы узла

position · rotation (градусы, против часовой) · scale (число или [x, y]) · layer · z · visible · color / opacity / flipX / flipY (узлы-спрайты) · locked (только для редактора) · aspects · children.

env, layers, physics

  • env.background, env.filter — передаются в Scene2D.
  • env.gravity — наличие вызывает Physics2D.configure({ gravity }) до создания любого тела ([0, 0] для вида сверху). Опусти, чтобы не трогать гравитацию мира.
  • env.pixelPerfect — привязывать камеру к целым экранным пикселям: задаёт CameraFollow.pixelSnap, а стартовая позиция узла камеры округляется до целых единиц.
  • layers: { 1: { ySort: true } } — настройка по слоям; Y-сортировка заставляет спрайты ниже на экране перекрывать те, что выше (глубина вида сверху).

CameraFollow

TypeScript
node.aspect(CameraFollow, {
  scene?: Scene2D              // чья камера следует — загрузчик файла сцены подключает её сам
  smooth?: number              // постоянная времени сглаживания, секунды; по умолчанию 0.12; 0 = жёстко к узлу
  offset?: [x, y]              // смещение камеры от узла, мировые единицы; по умолчанию [0, 0]
  pixelSnap?: boolean          // целые экранные пиксели; по умолчанию true (env.pixelPerfect в файле сцены)
})

Встроенная следящая камера — обычный аспект, прикреплённый к узлу, за которым камера должна следить. Работает в поздней фазе (update), после физики, так что камера попадает на позицию, нарисованную в этом кадре. pixelSnap измеряется по реальной проекции, поэтому держится при любом зуме. Из рукописного кода передай сцену сам: hero.aspect(CameraFollow, { scene }).

Note

проверка типов проекта сейчас отвергает node.aspect(CameraFollow, …) с ошибкой this-контекста (дефект типизации SDK); код компилируется и работает. use(CameraFollow, …) в файле сцены проверку типов проходит.

В режиме редактирования аспекты хранятся как данные (при редактировании ничего не падает и не стреляет); воспроизведение клипов продолжается. Префабов (сцена в сцене) в 2D-грамматике пока нет.