Тайловые сетки и устройство 2D-миров: от классики до современных редакторов уровней

Коротко

Тайловая сетка (Tilemap) — это способ построения 2D-уровня из повторяющихся квадратных или прямоугольных блоков фиксированного размера (например, 16×16 или 32×32 пикселя), где карта хранится как таблица числовых индексов, а сама графика загружается один раз в общей картинке — наборе тайлов (Tileset). Это экономит оперативную и видеопамять, ускоряет расчёт столкновений до одного обращения по индексу ячейки и позволяет собирать протяжённые уровни из десятков тысяч клеток за минуты. В статье разобраны исторические ограничения видеопамяти, послойная структура 2D-сцены, формулы битовых масок автотайлинга на 16 и 47 вариантов и проверенный в Node.js код поиска клетки, автотайлинга и отсечения камеры.

Содержание

  1. Почему 2D-миры собирают из клеток, а не рисуют одной картинкой
  2. Как экономили память в классических 8-битных играх
  3. Послойная архитектура 2D-уровня: от фона до физических коллайдеров
  4. Автотайлинг по битовым маскам: формулы на 16 и 47 клеток
  5. Одномерный массив карты и отсечение невидимых клеток камеры
  6. Практический сценарий: участок уровня из тайлов в коде на JavaScript
  7. Когда вам это не подойдёт
  8. Чек-лист проверки 2D-уровня перед запуском

Почему 2D-миры собирают из клеток, а не рисуют одной картинкой

Цельная растровая картинка длинного 2D-уровня занимает сотни мегабайт видеопамяти и не содержит информации о том, где находятся твёрдые стены, а где — пустой воздух. Если уровень платформера имеет ширину 10 000 пикселей и высоту 2 000 пикселей, то один несжатый буфер изображения в формате RGBA (4 байта на пиксель) потребует ровно:

Объём RGBA = 10 000 × 2 000 × 4 байта = 80 000 000 байт ≈ 76,29 МБ

При разбиении того же поля 10 000 × 2 000 пикселей на квадратные клетки (тайлы) размером 32×32 пикселя получается сетка из 313 столбцов и 63 строк — всего 19 719 ячеек. Если хранить номер клетки в 2-байтовом целом числе (Uint16, до 65 535 видов клеток), вся геометрия уровня займёт 39 438 байт (около 38,5 КБ), то есть в 2 029 раз меньше, чем одна полноразмерная текстура.

Для школьника или начинающего автора тайловая сетка работает как лист тетради в клетку и коробка с деталями конструктора. Вместо того чтобы заново рисовать каждый кирпич стены или каждый кусок травы, автор один раз берёт готовый блок «платформа», «монета» или «шипы» и расставляет его по клеткам с точными координатами X и Y.

Как экономили память в классических 8-битных играх

Тайловая графика появилась в конце 1970-х и начале 1980-х годов как аппаратное решение для игровых автоматов и домашних приставок с жёстким лимитом оперативной памяти. В аркаде Galaxian (Namco, 1979 год) и консоли Nintendo Entertainment System / Famicom (выход в Японии — 15 июля 1983 года) видеопроцессор (PPU Ricoh 2C02) вообще не имел кадрового буфера на весь экран и собирал изображение на лету из фоновой сетки 32×30 тайлов размером 8×8 пикселей (источник: техническая документация архитектуры NES PPU на портале NESdev Wiki, nesdev.org/wiki/PPU_nametables, проверено: октябрь 2026).

Вся таблица имен экрана (Nametable) на NES занимала ровно 960 байт (по 1 байту на каждую из 960 клеток 8×8 при разрешении 256×240 пикселей) плюс 64 байта таблицы цветовых атрибутов — итого 1 024 байта (1 КБ) видеопамяти на целый экран. В игре Super Mario Bros. (Nintendo, 1985 год; картридж NROM объёмом 32 КБ PRG-ROM для кода и данных уровней и 8 КБ CHR-ROM для графики спрайтов и тайлов, проверено: октябрь 2026) облака и кусты даже использовали одни и те же графические клетки 8×8 из CHR-ROM, отличаясь только номером палитры (бело-голубая для неба, зелёная для земли).

В современных браузерных 2D-движках, работающих поверх WebGPU и WebGL2, гигабайты памяти давно доступны, но принцип атласа клеток по-прежнему даёт два инженерных преимущества:

  1. Один вызов отрисовки (Draw Call) на слой: видеокарта берёт координаты всех видимых клеток из одного текстурного атласа и рисует их за один проход шейдера без переключения текстур.
  2. Мгновенный поиск препятствий: чтобы узнать, стоит ли персонаж на твёрдой платформе в точке (x, y), движку не нужно перебирать тысячи объектов — достаточно разделить координаты на размер клетки:
col = floor(x / tileSize)
row = floor(y / tileSize)

Послойная архитектура 2D-уровня: от фона до физических коллайдеров

Профессиональный 2D-уровень никогда не сваливает декорации, стены и ловушки в одну плоскую сетку, а разделяет их на 4–5 независимых слоёв по порядку отрисовки (Z-order) и физическому поведению. В таблице ниже приведена стандартная схема слоёв для платформера или приключенческой игры с видом сверху.

Стандартная структура слоёв 2D-сцены
Порядок (от дальнего к ближнему)Название слояФизика и столкновенияСкорость движения относительно камерыЧто размещается на слое
1Дальний фон (Background)Нет0.2x – 0.5x (параллакс)Небо, далёкие горы, облака, силуэты башен
2Фоновые декорации (Back Props)Нет1.0x (вместе со сценой)Задние стены подземелья, окна, факелы, деревья позади героя
3Основной игровой слой (Solids & Gameplay)Да (коллайдеры)1.0xТвёрдые платформы, стены, шипы, монеты, ключи, враги и сам персонаж
4Передний план (Foreground)Нет1.0x – 1.2xТрава и арки, частично перекрывающие ноги персонажа для ощущения глубины
5Экранный интерфейс (UI / HUD)Нет0.0x (закреплён на экране)Текстовые счётчики очков и монет, таймер, подсказки управления

Разделение фоновых декораций и твёрдых платформ избавляет игрока от главной проблемы любительских платформеров — визуальной путаницы, когда фоновый кирпич выглядит точно так же, как уступ, на который можно запрыгнуть. Практическое правило читаемости сцены: твёрдые платформы на игровом слое всегда имеют более высокий контраст контура и насыщенность, чем элементы фонового слоя.

Автотайлинг по битовым маскам: формулы на 16 и 47 клеток

Автотайлинг (Autotiling) автоматически выбирает нужную картинку угла, края или середины стены в зависимости от того, какие соседние клетки уже заполнены тем же материалом. Этот алгоритм избавляет автора от ручного подбора сотен уголков при рисовании уровня.

Базовая 4-соседняя битовая маска (16 вариантов)

При проверке четырёх ортогональных соседей (С — север/верх, В — восток/право, Ю — юг/низ, З — запад/лево) каждому направлению присваивается вес степени двойки:

  • Север (x, y - 1): бит 2⁰ = 1;
  • Восток (x + 1, y): бит 2¹ = 2;
  • Юг (x, y + 1): бит 2² = 4;
  • Запад (x - 1, y): бит 2³ = 8.

Итоговый индекс тайла mask вычисляется побитовым сложением заполненных соседей и принимает значения от 0 (одиночный блок в воздухе) до 15 (блок, окружённый землёй со всех четырёх сторон):

mask_4 = 1 × N + 2 × E + 4 × S + 8 × W   (диапазон 0..15)

Например, для верхнего левого угла платформы заполнены соседи справа (E = 1) и снизу (S = 1), а сверху и слева — пусто (N = 0, W = 0). Формула даёт индекс mask_4 = 2 + 4 = 6, и редактор подставляет спрайт №6 (верхний левый угол с травой сверху и обрывом слева).

Расширенная 8-соседняя маска с внутренними углами (47 вариантов)

Схема из 16 клеток не умеет оформлять внутренние углы (когда все 4 прямых соседа заполнены, но одна из диагоналей пуста — например, поворот пещеры буквой «Г»). Полный перебор 8 соседей даёт 2⁸ = 256 комбинаций, однако диагональный сосед влияет на форму угла только тогда, когда оба прилегающих к нему прямых соседа тоже заполнены:

  • Северо-Восток (NE) учитывается только при N = 1 и E = 1;
  • Юго-Восток (SE) учитывается только при S = 1 и E = 1;
  • Юго-Запад (SW) учитывается только при S = 1 и W = 1;
  • Северо-Запад (NW) учитывается только при N = 1 и W = 1.

Отбрасывание избыточных диагональных состояний сокращает 256 комбинаций ровно до 47 различных тайлов (или 48 с учётом пустой клетки; стандарт «Blob tileset», популяризированный редакторами Tiled Map Editor и LDTK — документация Tiled 1.11, раздел «Using Terrains», doc.mapeditor.org/en/stable/manual/terrain/, проверено: октябрь 2026).

Одномерный массив карты и отсечение невидимых клеток камеры

Хранение двумерной сетки шириной W и высотой H в одномерном массиве длиной W × H исключает вложенные массивы в памяти и повышает попадание в кэш процессора при обходе строк. Перевод координат клетки (col, row) в линейный индекс index и обратно выполняется по формулам:

index = row * W + col
col   = index % W
row   = floor(index / W)

При отрисовке кадра движок не проверяет все клетки уровня, а вычисляет прямоугольное окно видимости камеры (Viewport Culling). Если камера с верхним левым углом (C_x, C_y) показывает область экрана V_w × V_h пикселей, а размер клетки равен T пикселей, то цикл отрисовки перебирает только диапазон с запасом в 1 клетку по краям:

col_min = max(0, floor(C_x / T) - 1)
col_max = min(W - 1, ceil((C_x + V_w) / T) + 1)
row_min = max(0, floor(C_y / T) - 1)
row_max = min(H - 1, ceil((C_y + V_h) / T) + 1)

При разрешении игрового окна 1280×720 пикселей и размере клетки 32×32 пикселя в кадре находится всего (40 + 2) × (23 + 2) = 1 050 клеток, независимо от того, составляет ли общая площадь карты десять тысяч или миллион ячеек.

Практический сценарий: участок уровня из тайлов в коде на JavaScript

Участок платформера с пропастью в 4 клетки (128 пикселей), шипами на дне, монетой и флагом описывается 15 строками текста, а поиск клетки, автотайлинг и отсечение невидимых клеток укладываются в четыре функции на JavaScript. Код ниже проверен в Node.js 24; комментарии справа показывают его фактический вывод.

// Участок уровня: '#' — земля, '^' — шипы, 'C' — монета, 'F' — флаг, '.' — воздух. Клетка 32 пикс.
const T = 32;
const level = [
  "....................",
  "....................",
  "....................",
  "....................",
  "....................",
  "....................",
  "....................",
  "....................",
  "....................",
  "....................",
  "....................",
  ".........C........F.",
  "....................",
  "########....########",
  "########^^^^########",
];
const W = level[0].length, H = level.length;

function tileAt(col, row) {
  if (col < 0 || row < 0 || col >= W || row >= H) return ".";
  return level[row][col];
}
// Клетка по координатам в пикселях: col = floor(x / T), row = floor(y / T)
function tileAtPoint(x, y) { return tileAt(Math.floor(x / T), Math.floor(y / T)); }
function isSolid(x, y) { return tileAtPoint(x, y) === "#"; }

// Автотайлинг по 4 соседям: N = 1, E = 2, S = 4, W = 8
function mask4(col, row) {
  const s = function (c, r) { return tileAt(c, r) === "#" ? 1 : 0; };
  return 1 * s(col, row - 1) + 2 * s(col + 1, row) + 4 * s(col, row + 1) + 8 * s(col - 1, row);
}

// Диапазон клеток, которые попадают в камеру, с запасом в 1 клетку
function visibleRange(camX, camY, viewW, viewH, mapW, mapH) {
  return {
    colMin: Math.max(0, Math.floor(camX / T) - 1),
    colMax: Math.min(mapW - 1, Math.ceil((camX + viewW) / T) + 1),
    rowMin: Math.max(0, Math.floor(camY / T) - 1),
    rowMax: Math.min(mapH - 1, Math.ceil((camY + viewH) / T) + 1),
  };
}
console.log("стоит на земле в (100, 420):", isSolid(100, 420));       // true
console.log("над пропастью (300, 420):", isSolid(300, 420));           // false
console.log("шипы на дне (300, 460):", tileAtPoint(300, 460));          // ^
console.log("маска угла (0,13):", mask4(0, 13), " края (7,13):", mask4(7, 13), " середины (3,13):", mask4(3, 13));
const r = visibleRange(0, 0, 1280, 720, 313, 63);
console.log("клеток в кадре:", (r.colMax - r.colMin + 1) * (r.rowMax - r.rowMin + 1)); // 1050

Строковая карта удобна для прототипа: уровень видно прямо в коде. Когда карта вырастет до десятков тысяч клеток, те же функции переводят на одномерный массив Uint16Array с индексом row * W + col — формулы из раздела выше не меняются.

Когда вам это не подойдёт

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

  • Свободная органическая геометрия и плавные склоны: если геймплей строится вокруг катания шара по произвольным кривым или разрушаемого попиксельно грунта (как в Worms, Team17, 1995 год), жёсткая сетка из квадратных блоков создаст «ступеньки». Для таких задач используют полигональные сплайны или битовые маски разрушаемости.
  • Изометрические и шестиугольные карты: формулы floor(x / T) и маски на 4 соседа рассчитаны на квадратную сетку; для ромбической или гексагональной сетки перевод координат и набор соседей другие.
  • Уровень на один экран: если вся игра помещается в окно 1280×720 и на сцене 10–20 объектов, отсечение невидимых клеток не даёт выигрыша, а сетку можно заменить списком прямоугольников.
  • Рисование сложного тайлсета с нуля: сетка задаёт только расположение клеток; саму графику удобнее рисовать в специализированных пакетах вроде Aseprite или Krita.

Чек-лист проверки 2D-уровня перед запуском

Уровень готов к проверке игроками, когда выполнены пять пунктов ниже.

  • Все опорные платформы и преграды выровнены по единому шагу сетки (например, кратно 32 пикселям).
  • Фоновые декорации вынесены на отдельные слои и визуально контрастируют с твёрдыми платформами игрового слоя.
  • Самая широкая пропасть на уровне проходится при тестовом запуске с запасом минимум в 1 клетку (32 пикселя).
  • Все опасные зоны (шипы, падение ниже Y = 640) и флаг финиша подключены к правилам перезапуска и победы.
  • Уровень проверен в окне целевого размера (например, 1280×720) при масштабе браузера 100%, чтобы между клетками не появлялись щели.

Мы разрабатываем J2DRU — браузерный 2D игровой движок; он в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).

Читайте также:

Частые вопросы

Какой размер клетки выбрать для первой 2D-игры?

Для пиксель-арта в стиле 8-битных и 16-битных приставок выбирают клетку 16×16 пикселей, для чёткой современной 2D-графики на экранах 1920×1080 — 32×32 или 64×64 пикселя.

Почему при движении камеры между соседними клетками иногда мелькают тонкие щели?

Артефакт возникает при дробном смещении камеры, когда видеокарта при билинейной фильтрации захватывает соседний пиксель из текстурного атласа. Проблема решается округлением координат камеры до целых пикселей, режимом фильтрации по ближайшему соседу (Nearest Neighbor) или защитным отступом (extrude) в 1–2 пикселя вокруг каждого тайла в атласе.

Как узнать, в какой клетке карты находится точка?

Разделите мировые координаты точки на размер клетки и округлите вниз — col = floor(x / 32) и row = floor(y / 32) для клетки 32×32 пикселя. Если камера сдвинута, сначала прибавьте к экранным координатам смещение камеры, а затем делите.

Чем тайловая сетка отличается от свободного размещения спрайтов?

В тайловой сетке каждая ячейка привязана к целочисленным индексам строки и столбца (col, row), что ускоряет проверку столкновений до одного обращения к массиву за O(1). Свободное размещение хранит произвольные координаты X и Y каждого объекта и удобнее для наклонных декораций и некратных сетке препятствий.