import { Application, Graphics } from 'pixi.js';
import { Renderer, RendererOptions } from '../render/Renderer';
import { Camera } from '../render/Camera';
import { computeScale } from '../render/scale';
import { GameLoop } from './GameLoop';
import { EventBus } from './EventBus';
import { TweenManager } from './Tween';
import { InputManager } from '../input/InputManager';
import { SceneManager } from '../scene/SceneManager';
/**
* Собирает подсистемы движка. Приложение создаёт Engine, добавляет стартовую
* сцену через scenes.replace() и вызывает start().
*/
export interface EngineOptions extends RendererOptions {
/** FPS фиксированного шага. */
fixedFps: number;
/** Пересчитывать масштаб при ресайзе окна (по умолчанию true). */
autoResize?: boolean;
}
export class Engine {
readonly renderer: Renderer;
readonly camera: Camera;
readonly events = new EventBus();
readonly scenes = new SceneManager();
/** Твины и таймеры: обновляются каждым фиксированным шагом. */
readonly tweens = new TweenManager();
input!: InputManager;
private loop: GameLoop;
private started = false;
private readonly autoResize: boolean;
private onWindowResize = (): void => {
this.renderer.resize(
computeScale(
this.renderer.virtualWidth,
this.renderer.virtualHeight,
window.innerWidth,
window.innerHeight
)
);
};
constructor(options: EngineOptions) {
this.renderer = new Renderer(options);
this.camera = new Camera(options.virtualWidth, options.virtualHeight);
this.autoResize = options.autoResize ?? true;
this.loop = new GameLoop(options.fixedFps, {
update: (dt) => this.tick(dt),
render: () => this.render()
});
}
/**
* Инициализация WebGL и ввода. Вызвать до scenes.replace()/start().
* Возвращает готовый Application (для прямого доступа к Pixi при необходимости).
*/
async init(): Promise<Application> {
await this.renderer.setup();
this.input = new InputManager(
this.renderer.app.canvas,
this.renderer.virtualWidth,
this.renderer.virtualHeight
);
// Оверлей затемнения для переходов между сценами — верхний слой UI.
const fade = new Graphics();
this.renderer.uiRoot.addChild(fade);
this.scenes.setOverlay(fade, this.renderer.virtualWidth, this.renderer.virtualHeight);
if (this.autoResize) {
window.addEventListener('resize', this.onWindowResize);
}
return this.renderer.app;
}
async start(): Promise<void> {
if (this.started) return;
this.started = true;
this.loop.start();
}
stop(): void {
this.loop.stop();
this.input?.dispose();
window.removeEventListener('resize', this.onWindowResize);
this.events.clear();
}
/** Один фиксированный шаг. */
private tick(dt: number): void {
this.input?.update(); // опрос геймпадов до логики
this.camera.update(dt); // тряска и прочее «живое» поведение камеры
this.tweens.update(dt);
this.scenes.update(dt);
this.input?.endTick();
}
private render(): void {
this.camera.apply(this.renderer.worldRoot);
this.scenes.render();
this.renderer.render();
}
}