Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (raylib только окно, ввод и финальный блит). Ядро (спек, раздел 6): - LaneClear: проверка линии огня по своим, стенам и дальности; - решатель огневых позиций с гистерезисом и коммитом; - резервирование линий огня и расступание из чужих секторов; - пули-снаряды, дружественный урон физически возможен. Хоррор-слой (render-only): свет и туман войны с памятью карты, светящиеся трассеры, кровь, виньетка, зерно, напряжение. Ближний бой: сектор удара, три фазы, связка из трёх ударов, своя дисциплина «не бить сквозь своего». Проверка: squad_proto.exe --accept прогоняет критерии приёмки раздела 11 плюс блок M7 по ближнему бою — все PASS, FRIENDLY_HITS = 0 за 3 минуты боя. Сборка: cmake -S . -B build && cmake --build build --config Release raylib 6.0 подтягивается через FetchContent. Документация — CLAUDE.md как оглавление, содержание в docs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.1 KiB
03 — Рендер: софтверный буфер, изометрия, спрайты
Фреймбуфер
src/render/framebuffer.h — std::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 не протёк в заголовок.
Изометрия
src/render/iso.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)
Три инварианта, нарушение любого ломает картинку молча:
- альфа фреймбуфера всегда 255;
- ступень 0 световой рампы совпадает с цветом
Clear— только поэтому полностью тёмные тайлы можно не рисовать; - стена никогда не темнее самого яркого соседа-пола — иначе сквозь пропущенный тёмный блок проступит освещённый пол тайла (tx−1, ty−1), который рисуется раньше по глубине.