Тайлсеты (.tiles.ts) и Autotile2D
Файл тайлсета именует материалы атласа тайлов — правила автотайлинга, которые превращают карту,
нарисованную id материалов (намерение), в индексы тайлов атласа (отображение). Один файл на атлас,
export default defineTileset({...}); визуальный редактор тайлсетов (lecodes-2d-editor) читает и
пишет эти файлы, а инструмент Tiles редактора сцен рисует материалами.
Обзор
// world.tiles.ts
export default defineTileset({
image: asset('./TileSet.png'),
tile: 32, // размер ячейки атласа в пикселях изображения
materials: {
grass: { id: 1, kind: 'patch', color: '#5a9e58', fill: [130, [45, 0.05], [48, 0.006]] },
dirt: {
id: 2, kind: 'patch', color: '#a3824c', fill: 135,
over: { '*': { edges: [71, 136, 199, 134], outer: [72, 200, 198, 70], inner: [192, 64, 9, 196] } },
},
fence: { id: 3, kind: 'path', isolated: 377, ends: [187, 121, 251, 122], straights: [379, 378], corners: [249, 57, 58, 250] },
},
})// meadow.scene2d.ts — блок tilemap не меняет форму; АСПЕКТ меняет смысл того же
// payload cells('…') с сырых индексов атласа на id материалов (здесь: трава вокруг земли)
import world from './world.tiles'
export default defineScene2d({
nodes: {
ground: {
tilemap: {
texture: asset('./TileSet.png'), tile: 32, atlas: [64, 64],
cells: cells('BgAAAAQAAAAHAAAAAQAAAAQAAAACAAAAAgAAAAEAAAAEAAAAAgAAAAcAAAABAAAA'),
},
aspects: [use(Autotile2D, { tileset: world, seed: 7 })],
},
},
})Загрузчик выводит индексы отображения до построения Tilemap — детерминированно (автотайл по
маске соседей + разброс с сидом на ячейку), так что одна и та же карта рендерится одинаково на
каждой платформе, а выведенные индексы никогда не пишутся в файл.
Материалы
Два вида, различающихся только классификатором соседей:
kind: 'patch'— площадные материалы (трава, земля, вода, земля платформера). Классификатор = 8-соседняя blob-маска. Слоты:fill(один индекс или массив вариантов разброса — каждыйindexили[index, weight], голые индексы весят 1) плюс наборы переходов подover, по имени соседнего материала ('*'= всё остальное, включая пустоту):edges[n, e, s, w], внешние углыouterи внутренние (вогнутые) углыinner[ne, se, sw, nw].kind: 'path'— линейные материалы (дороги, заборы, стены, рельсы). Классификатор = 4-соседняя маска:isolated,ends[n, e, s, w](сторона, к которой кусок присоединяется),straights[vertical, horizontal],corners[ne, se, sw, nw],tees[n, e, s, w](тройник, у которого НЕТ этой стороны),cross. Сsizeлиния занимает столько ячеек ПОПЕРЁК, и каждый кусок — блокsize² — двухполосная дорога этоsize: 2, еёcross— четыре угла бордюра перекрёстка.
Отсутствующие слоты откатываются к более простым (несовпавший угловой случай рендерится как
край, затем как fill), так что частичные наборы всегда рендерятся — -1 внутри массива слота
значит «не назначено». Ячейки за пределами карты считаются тем же материалом (patch продолжается
за край). Значения карты, не совпавшие ни с одним id материала, проходят как сырые индексы; -1
остаётся пустым.
Многоклеточные материалы (size)
У многого арта есть ФАЗА узора — ёлочка, которая сходится только как 2×2, дорожные тайлы, у
которых штрихи разметки чередуются через тайл. size: 2 (или 3) кладёт материал на решётку блоков
size×size, привязанную к началу карты. Для patch это блок узора; для path — ширина
линии:
// двухполосная дорога: каждый кусок — блок 2×2, так что линия шириной в один блок даёт бордюры,
// разделитель полос и правильные перекрёстки
road: {
id: 1, kind: 'path', size: 2,
straights: [[10, 11, 26, 27], [72, 73, 88, 89]], // [vertical, horizontal]
tees: [[72, 73, 90, 91], [74, 11, 90, 27], [74, 75, 88, 89], [10, 75, 26, 91]],
cross: [74, 75, 90, 91],
}
// patch на той же решётке: fill — size² тайлов, каждая сторона — `size` тайлов, углы — одна ячейка
sidewalk: { id: 2, kind: 'patch', size: 2, fill: [14, 15, 30, 31] }- Классификация идёт блок к блоку; внутри блока каждая ячейка выбирает по своей позиции. Patch:
угловые ячейки берут угловой тайл, краевые строки/столбцы берут
edges[dir][along-index](n/s идут слева→направо, w/e сверху→вниз), остальные берутfill[ly * size + lx]. Path: блок выбранного куска читается так же. - Любой слот может остаться одним индексом — тогда он покрывает каждую ячейку своего блока/стороны.
- Варианты разброса — целые блоки (
fill: [[a, b, c, d], [[e, f, g, h], 0.2]]) и бросаются один раз на блок, так что узор никогда не рвётся. - Рисование в редакторе привязывается к решётке. Для patch полоса шириной в один блок несёт оба своих края по построению.
size: 1(по умолчанию) — обычный материал «один тайл на ячейку».
Элементы, ставящиеся вручную (parts)
Пешеходный переход на дороге, калитка в заборе: арт, который принадлежит материалу, но ставится вручную, а не выбирается правилом. Каждый элемент несёт свой id, так что карта хранит его как любой материал — но он соединяется как родитель, и линия проходит сквозь него напрямую:
road: {
id: 1, kind: 'path', size: 2,
straights: [[10, 11, 26, 27], [72, 73, 88, 89]],
parts: {
crossing: { id: 4, axis: 'h', tiles: [76, 92] }, // один тайл на ПОЛОСУ
crossingNS: { id: 5, axis: 'v', tiles: [106, 107] },
},
}- Элементы рисуются по одной ячейке — без привязки к блокам, так что переход может стоять в любом месте дороги и быть такой ширины, какую протянешь.
axis— направление, для которого нарисован арт ('v'/'h'), и он выбирает тайл: элемент меняется ПОПЕРЁК линии и повторяется вдоль неё, так чтоtilesимеет длинуsize— индексируется по строке для'h', по столбцу для'v'. Без оси это обычный блок size².- Неназначенные записи (
-1) откатываются к обычному куску материала, так что наполовину описанный элемент никогда не исчезает. - Id живут в том же числовом пространстве, что материалы: уникальны во всём тайлсете.
- В редакторе они показываются как образцы под своим материалом в палитре Tiles и как секция
Elements инспектора материала (
+добавляет).
Блок считается принадлежащим материалу, когда любая его ячейка принадлежит, так что линия, нарисованная или подрезанная по ячейке, всё равно соединяется — привязка кисти к блокам это удобство (переключатель в палитре), а не ограничение данных.
Сырые тайлы в карте материалов
Ячейка может держать и обычный тайл атласа — для единичного арта, которому не нужно правило.
Поскольку карта материалов хранит намерение, сырые тайлы живут ниже -1, чтобы их нельзя было
спутать с id:
rawCell(index: number): number // значение ячейки, рисующее тайл атласа `index` как есть
isRawCell(value: number): boolean // value <= -2
rawIndex(value: number): number // индекс атласа за сырой ячейкой (проверяй через isRawCell)
data[i] = rawCell(137) // -139 — рисует тайл атласа 137 как есть-1 по-прежнему пусто, 0…n — id материалов и элементов, а всё <= -2 — сырой тайл. В редакторе
это кнопка custom tile: она открывает весь атлас на поверхности с панорамированием и зумом, и
один клик делает тайл кистью. Сырые ячейки не участвуют в автотайлинге — соседние материалы просто
видят «не я» и рисуют свой край.
Повёрнутые / зеркальные тайлы
Любое значение тайла — слот, сырая ячейка, обычный индекс в сырой карте или data
Tilemap — может нести ориентацию, упакованную в тот же int, так что
неполный атлас никогда не вынуждает править изображение: один нарисованный край служит всем
четырём сторонам.
fence: {
id: 3, kind: 'path',
straights: [379, rot90(379)], // одна прямая, повёрнутая для другой оси
ends: [187, rot90(187), rot180(187), rot270(187)], // один торец, все четыре стороны
corners: [57, rot90(57), rot180(57), rot270(57)],
}
edges: [652, flipX(rot270(652)), rot180(652), rot270(652)] // зеркало, когда поворот выглядит неправильноrot90 / rot180 / rot270поворачивают по часовой,flipXзеркалит по горизонтали; они компонуются (групповая математика —rot90(rot90(t))этоrot180(t), достижимы все 8 ориентаций тайла).- Биты 0–27 держат индекс, бит 28 — зеркало, биты 29–30 — повороты; значение остаётся
положительным int, так что проходит через
cells('…'), вывод и движок без изменений (рендереры переставляют UV ячейки атласа; те же квады, те же батчи, нулевая цена). - Поворот художественно верен только для симметричного арта — у затенённого края поворачивается и блик. Поэтому ориентация задаётся на слот и вручную, никогда автоматически.
- В редакторе тайлсетов: выбери слот, нажми
Rдля поворота /Fдля зеркала (или кнопки на панели) — в файле появляется канонический вызов, напримерrot90(flipX(187)). В редакторе сцен:R/Fпереориентируют однотайловую пользовательскую кисть.
То же значение, собранное и прочитанное по полям:
packTile(index: number, turns = 0, flip = false): number // сначала зеркало (flip), затем `turns` четвертей по часовой
tileIndex(value: number): number // индекс атласа (биты 0–27)
tileTurns(value: number): number // четверти оборота по часовой, 0–3
tileFlip(value: number): boolean // отзеркален по горизонтали?
tileOriented(value: number): boolean // несёт хоть какую-то ориентациюpackTile(i) без ориентации — это сам i, а rot90(t) — это
packTile(tileIndex(t), tileTurns(t) + 1, tileFlip(t)).
Практические правила: карта либо материалы (несёт Autotile2D), либо сырая — никогда вперемешку;
прозрачные path-материалы (заборы) клади на свой узел тайлмапа над землёй, чтобы трава оставалась
видна под рельсами; seed перетасовывает варианты разброса (тот же сид → та же карта).
Хендл Tileset: def, materialNames, material(name), id(name) и
derive(cols, rows, data, seed?) для процедурных карт, которым нужны те же правила вручную.