Коротко
Тайловая сетка (Tilemap) — это способ построения 2D-уровня из повторяющихся квадратных или прямоугольных блоков фиксированного размера (например, 16×16 или 32×32 пикселя), где карта хранится как таблица числовых индексов, а сама графика загружается один раз в общей картинке — наборе тайлов (Tileset). Это экономит оперативную и видеопамять, ускоряет расчёт столкновений до одного обращения по индексу ячейки и позволяет собирать протяжённые уровни из десятков тысяч клеток за минуты. В статье разобраны исторические ограничения видеопамяти, послойная структура 2D-сцены, формулы битовых масок автотайлинга на 16 и 47 вариантов и проверенный в Node.js код поиска клетки, автотайлинга и отсечения камеры.
Содержание
- Почему 2D-миры собирают из клеток, а не рисуют одной картинкой
- Как экономили память в классических 8-битных играх
- Послойная архитектура 2D-уровня: от фона до физических коллайдеров
- Автотайлинг по битовым маскам: формулы на 16 и 47 клеток
- Одномерный массив карты и отсечение невидимых клеток камеры
- Практический сценарий: участок уровня из тайлов в коде на JavaScript
- Когда вам это не подойдёт
- Чек-лист проверки 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, гигабайты памяти давно доступны, но принцип атласа клеток по-прежнему даёт два инженерных преимущества:
- Один вызов отрисовки (Draw Call) на слой: видеокарта берёт координаты всех видимых клеток из одного текстурного атласа и рисует их за один проход шейдера без переключения текстур.
- Мгновенный поиск препятствий: чтобы узнать, стоит ли персонаж на твёрдой платформе в точке
(x, y), движку не нужно перебирать тысячи объектов — достаточно разделить координаты на размер клетки:
col = floor(x / tileSize)
row = floor(y / tileSize)
Послойная архитектура 2D-уровня: от фона до физических коллайдеров
Профессиональный 2D-уровень никогда не сваливает декорации, стены и ловушки в одну плоскую сетку, а разделяет их на 4–5 независимых слоёв по порядку отрисовки (Z-order) и физическому поведению. В таблице ниже приведена стандартная схема слоёв для платформера или приключенческой игры с видом сверху.
| Порядок (от дальнего к ближнему) | Название слоя | Физика и столкновения | Скорость движения относительно камеры | Что размещается на слое |
|---|---|---|---|---|
| 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 игровой движок; он в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).
Читайте также: