Newer
Older
rpg / docs / engine / render.md

Рендер: Renderer, pixel-perfect, камера, IsoDepthLayer, частицы

Renderer и pixel-perfect

Виртуальное разрешение (например 480×270) определяет логическую систему координат: всё игровое кодирование позиций — в виртуальных пикселях. Отрисовка двухслойная:

  • CSS-размер канваса — виртуальное разрешение, растянутое целым числом (computeScale), без зазоров и обрезки;
  • бэкинг-стор канваса — в пикселях устройства (scale × devicePixelRatio): UI и текст рендерятся 1:1 с экраном и остаются резкими, в том числе на HiDPI;
  • мир при этом остаётся пиксельным: текстуры, загруженные через AssetLoader, сэмплируются nearest (чёткие квадратные пиксели арта).

  • computeScale(vw, vh, winW, winH) — максимальный целый масштаб, при котором виртуальный экран влезает в окно;

  • renderer.resize(newScale) — смена масштаба; движок сам пересчитывает resolution устройства и растеризацию текста (см. PixelText); Engine делает это автоматически при ресайзе окна (autoResize: true);
  • канвасу выставлены image-rendering: pixelated, antialias: false, roundPixels: true.

Корневые контейнеры:

engine.renderer.worldRoot  // мир — сюда применяется камера
engine.renderer.uiRoot     // UI поверх мира, камерой не двигается

Оверлей fade-переходов добавляется в uiRoot самим Engine — всегда верхний слой.

Camera

Позиция камеры — центр взгляда в мировых виртуальных пикселях:

const camera = engine.camera;
camera.follow(targetX, targetY);        // следить за точкой
camera.bounds = { x, y, width, height }; // прямоугольник мира

Важно: границы могут начинаться с отрицательных координат. Изометрическая карта (ромб) уходит в минус по X — её западный угол при 28×28 тайлах ≈ -448:

const size = map.screenSize; // { width, height } в экранных пикселях
camera.bounds = { x: -size.width / 2, y: 0, width: size.width, height: size.height };

Позиция округляется до целого пикселя (apply), поэтому арт не дрожит при движении.

«Мёртвая зона» — современное поведение камеры: пока цель внутри окна, камера неподвижна; у границ окна цель «толкает» камеру (каждая ось независимо):

camera.deadZone = { width: 180, height: 120 }; // окно в виртуальных пикселях
camera.deadZone = null;                        // снова жёсткое центрирование
camera.snap(x, y);                             // мгновенно, минуя окно (спавн/респаун)

Тряска — для ударов, взрывов и урона. Движок сам тикает камеру (camera.update(dt) внутри фиксированного шага), игре достаточно запустить толчок:

camera.addShake(3, 0.25); // амплитуда 3 px, 0.25 сек
camera.shaking;           // true, пока трясёт
camera.clearShake();      // сбросить

Смещение детерминировано (внутри Shake — движковый Rng) и округляется до целых пикселей, так что пиксель-арт не размывается.

IsoDepthLayer

Сортировка глубины для сущностей на изометрической карте: глубина = tx + ty (чем юго-восточнее, тем «ближе»). Герой, NPC, деревья добавляются сюда, а не в слой карты:

import { IsoDepthLayer } from '@rpg/engine';

const actors = new IsoDepthLayer();       // sortableChildren = true внутри
engine.renderer.worldRoot.addChild(actors);

actors.add(heroView, tileX, tileY);       // добавить и выставить глубину
actors.setDepth(heroView, tileX, tileY);  // обновить при движении (по смене тайла)

Слой карты (IsometricTileMap.view) и слой сущностей — соседи: карта рисует высокие объекты со своей внутренней сортировкой, сущности сортируются отдельно. Для корректного перекрытия герой/дерево должны быть в одном слое с деревьями — либо используйте tall-объекты карты и держите сущности поверх (см. recipes).

Particles

Эмиттер частиц для атмосферы (пепел, мотыльки, пыль, искры). Частицы — подкрашенные квадратики 1–4 виртуальных пикселя, детерминированные по seed:

import { ParticleEmitter } from '@rpg/engine';

const moths = new ParticleEmitter({
    color: 0xf0d878,
    rate: 3,                            // частиц в секунду
    lifetime: [2, 5],                   // сек
    velocity: { x: [-6, 6], y: [-9, -3] },
    acceleration: { y: 1.5 },           // «тяжёлый» полёт мотылька
    size: 2,
    spawnArea: { width: 480, height: 200 },
    seed: 42
});
moths.position.set(240, 135);
engine.renderer.worldRoot.addChild(moths);

// в update сцены:
moths.update(dt);
// при выходе со сцены:
moths.clear(); moths.destroy();

Для боевых эффектов (удар, смерть, попадание) есть разовые всплески:

// burst: мгновенно выпустить частицы из существующего эмиттера
hitFx.burst(12, { color: 0x999988, radialSpeed: [40, 90], lifetime: [0.2, 0.5] });

// oneShot: самостоятельный эмиттер-взрыв, уничтожается сам, когда все частицы умерли
const boom = ParticleEmitter.oneShot(16, {
    color: 0x777766, rate: 0, lifetime: [0.3, 0.6],
    velocity: { x: [0, 0], y: [0, 0] }, // не используется при radialSpeed
    radialSpeed: [50, 120], size: 2, seed: 7
});
boom.position.set(x, y);
worldRoot.addChild(boom);
// обновлять в update сцены: boom.update(dt)

Интеграция частицы (stepParticle, sampleSpawn, sampleBurst) вынесена в чистый модуль render/particleSim — её можно тикать и без Pixi (например, в тестах боя).

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

Стек сцен рисуется снизу вверх; внутри сцены порядок задают дочерние контейнеры (worldRoot: карта → сущности → эффекты; uiRoot: HUD → диалоги → fade-оверлей). SceneManager.render() вызывает render() всех сцен в стеке (не только верхней).