Виртуальное разрешение (например 480×270) растягивается на экран целым числом — каждый виртуальный пиксель всегда N×N экранных. Это и есть pixel-perfect:
computeScale(vw, vh, winW, winH) — максимальный целый масштаб, при котором виртуальный экран влезает в окно;renderer.resize(newScale) — смена масштаба (CSS-размер канваса); Engine делает это автоматически при ресайзе окна (autoResize: true);image-rendering: pixelated, antialias: false, roundPixels: true.Корневые контейнеры:
engine.renderer.worldRoot // мир — сюда применяется камера engine.renderer.uiRoot // UI поверх мира, камерой не двигается
Оверлей fade-переходов добавляется в uiRoot самим Engine — всегда верхний слой.
Позиция камеры — центр взгляда в мировых виртуальных пикселях:
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.update(dt) внутри фиксированного шага), игре достаточно запустить толчок:
camera.addShake(3, 0.25); // амплитуда 3 px, 0.25 сек camera.shaking; // true, пока трясёт camera.clearShake(); // сбросить
Смещение детерминировано (внутри Shake — движковый Rng) и округляется до целых пикселей, так что пиксель-арт не размывается.
Сортировка глубины для сущностей на изометрической карте: глубина = 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).
Эмиттер частиц для атмосферы (пепел, мотыльки, пыль, искры). Частицы — подкрашенные квадратики 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() всех сцен в стеке (не только верхней).