squad-proto/docs/03-rendering.md
z.kirill 4382af2322 Документация: границы движка, работа через модель, дизайн игры до лейтгейма
18-engine.md    границы Tile2D, проекции 2D, формат проекта, второй проект
  19-agent.md     инструменты --tool, скиллы, терминал в редакторе
  20-gdd.md       акты, петли, рост отряда, закон масштаба, бюджет 200 часов
  21-damage.md    семь стихий, статусы, десять реакций, формулы, триггеры
  22-loot.md      редкости, аффиксы, артефакты, зачарования, материалы
  23-endgame.md   Бездна, мутации, лидерборды, 262 ачивки

Плюс правки 01/02/07/15/16/17 под переехавшие пути и новые правила забега.

Дизайнерская часть держится на одном законе: угроза растёт квадратично, сила
отряда — линейно с затуханием, разрыв закрывается ЗНАНИЕМ, а не числами. Из
него выведены и экономика опыта (41 млн против 44 млн дохода), и то, почему
реакции считаются от глубины, а не от урона оружия, и почему в Бездне
множитель HP разрешён, а во всём авторском контенте запрещён.

.claude/commands: скиллы /tile2d, /room, /floor — правила этого движка,
которые нельзя вывести из кода. Главное записано первым: не отчитываться об
успехе без прохода --tool validate.

Числа в этих документах — вход для инструментов, а не украшение. Расхождение
между таблицей и выводом инструмента означает, что неправ документ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:07:45 +03:00

7.1 KiB
Raw Blame History

03 — Рендер: софтверный буфер, изометрия, спрайты

Фреймбуфер

engine/framebuffer.hstd::vector<uint32_t> 480×270.

Формат — RGBA8, в памяти байты идут R,G,B,A, значит little-endian uint32 это A<<24 | B<<16 | G<<8 | R. Помощник RGBA(r,g,b,a) собирает цвет правильно.

Примитивы: Clear, Put, Blit, BlitTinted, FillRect, Line (Брезенхем), CircleOutline (средняя точка), CircleFilled. Всё пишет пиксели руками.

Спрайты пишутся альфа-тестом: пиксель с alpha == 0 не пишется, полупрозрачности нет. Для эффектов есть аддитивное смешивание с насыщением (AddSat, AddBeam, AddGlowDisc) и умножение (ScaleColor8, BlitMul), а для света — BlitShaded, пропускающий цвет через световую рампу. Подробности — 10-atmosphere.md.

Blit обрезает спрайт по границам буфера ДО циклов и выходит при пустом диапазоне: иначе адрес строки вычисляется за концом вектора. Цикл при этом не выполняется, разыменования нет, но индекс уже невалиден — Debug-сборка ловит это как vector subscript out of range, Release молча считает мусорный адрес.

Вывод на экран — ScreenBlitter: UpdateTexture + DrawTexturePro с TEXTURE_FILTER_POINT. Это единственное место рендера, знающее про raylib; Texture2D спрятан за void*, чтобы raylib.h не протёк в заголовок.

Изометрия

engine/view2d.h. Тайл 32×16:

sx = (wx - wy) * 16
sy = (wx + wy) * 8

Обратная проекция: wx = sx/32 + sy/16, wy = sy/16 - sx/32.

ScreenDirToWorld переводит направление ввода из экранных осей в мировые — именно это делает WASD интуитивным. Больше изометрия нигде не используется.

Камера следует за интерполированным якорем экспоненциальным сглаживанием (1 - exp(-smooth*dt)), смещение округляется до целых пикселей — иначе при ×3 апскейле картинка дрожит.

Сортировка по глубине

Ключ — wx + wy. Пол рисуется первым сплошным проходом и в сортировке не участвует. Все стены и все сущности попадают в один список Scene::items и сортируются std::stable_sort (стабильная — чтобы порядок был детерминирован при равных ключах).

Отсечение — по экранным координатам с запасом на высоту спрайта стены.

Точка привязки спрайта

У Sprite есть поля ax, ay — точка внутри спрайта, которая совмещается с экранной проекцией мировой позиции. Так блит не зависит от размера спрайта:

Спрайт Размер Привязка Смысл
пол 32×16 (16, 8) центр ромба = центр тайла
стена 32×32 (16, 24) низ блока садится на ромб пола
агент (стрелок) 10×14 (5, 12) «ноги» на земле
агент (клинок) 12×15 (6, 13) шире в плечах, класс читается силуэтом
мишень 12×16 (6, 14) то же

У пули спрайта нет: снаряд рисуется светящимся трассером, см. 10-atmosphere.md.

Процедурные спрайты

src/render/sprites.cpp, файлов с ассетами нет.

  • Пол — ромб 32×16, два оттенка тёмно-серого металла по шахматке (x+y)&1, затемнённый рант по краю и редкие светлые крапины для фактуры.
  • Стена — верхняя грань (тот же ромб) + вертикальные грани: для каждого столбца ищется нижняя кромка ромба и вниз заливается на высоту тайла. Левая грань темнее правой. Обводка по силуэту — пиксели, у которых есть прозрачный 4-сосед.
  • Агент — капсула: пиксель внутри, если расстояние до вертикального отрезка меньше радиуса; внешний пиксель кольца — обводка, слева-сверху блик. 5 различимых цветов, по одному на агента.
  • Агент с клинком — та же капсула, крупнее, плюс стальная вертикаль с бликом и гардой у правого края. Цвет уже занят номером агента, поэтому класс обязан читаться формой. Сам взмах рисуется не спрайтом, а аддитивной дугой (11-melee.md).
  • Мишень — та же капсула, тускло-красная, 12×16. В темноте тело не видно — виден только тлеющий красный «глаз», нарисованный аддитивно поверх сцены.

Порядок отрисовки кадра:

Clear(чёрный) → свет → пол (BlitShaded) → список (стены, мишени, агенты)
→ сортировка → блит → глаза мишеней → трассеры, вспышки и дуги клинков
(аддитивно) → ОДИН пост-проход → Present → оверлей (raylib, dev-only)

Три инварианта, нарушение любого ломает картинку молча:

  1. альфа фреймбуфера всегда 255;
  2. ступень 0 световой рампы совпадает с цветом Clear — только поэтому полностью тёмные тайлы можно не рисовать;
  3. стена никогда не темнее самого яркого соседа-пола — иначе сквозь пропущенный тёмный блок проступит освещённый пол тайла (tx1, ty1), который рисуется раньше по глубине.