Newer
Older
rpg / packages / engine / src / core / Engine.ts
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();
    }
}