import * as Pixi from "pixi.js"; import { Vector2 } from "../lib/util/geometry/vector2"; import { GameState, WindowState } from "../data/GameState"; // eslint-disable-next-line import { assertOnlyCalledOnce, Const } from "../lib/util/misc"; import { RootComponent } from "./components/RootComponent"; import { UpdaterGeneratorType2 } from "../lib/util/updaterGenerator"; type Props = { args: { fireBatch: () => void, isSecondConstructorCall: boolean }, updaters: UpdaterGeneratorType2<GameState>, // aka updaters windowState: Const<WindowState>, gameState: Const<GameState>, } type State = { appSize: Vector2, originalAppSize: Vector2, } function appSizeFromWindowSize(window?: Const<Vector2>): Vector2 { return new Vector2({ x: Math.min(1920, (window?.x || Infinity) - 24), y: Math.min(1080, (window?.y || Infinity) - 24), }); } /** * TODO(bowei): move the resizing out of this function and into root component, * and only handle react/pixi state management in this class */ export class PixiReactBridge { public app!: Pixi.Application; state!: State; props!: Props; rootComponent: RootComponent | undefined; onTick!: (d: number) => void; /** * NOTE: for lifecycle convenience, we allow initializing with essentially empty props, and to finish the initialization * lazily at the first rerender() call */ constructor(props?: Props, isSecondConstructorCall: boolean = false) { // verify that we are not loading this twice when we expect to load it only once -- bad for performance!! if (!(props?.args?.isSecondConstructorCall || isSecondConstructorCall)) { // assertOnlyCalledOnce("Pixi react bridge constructor"); // annoying with react hot reload, disable for now} } let appSize = new Vector2(800, 600); this.state = { appSize, originalAppSize: appSize } this.app = new Pixi.Application({ width: this.state.appSize.x, height: this.state.appSize.y, antialias: true, // both about the same FPS, i get around 30 fps on 1600 x 900 transparent: true, // true -> better fps?? https://github.com/pixijs/pixi.js/issues/5580 resolution: window.devicePixelRatio || 1, // lower -> more FPS but uglier // resolution: 0.5, // resolution: 2, autoDensity: true, powerPreference: "low-power", // the only valid one for webgl backgroundColor: 0xffffff, // immaterial - we recommend setting color in backdrop graphics }); // test // createBunnyExample({ parent: this.app.stage, ticker: this.app.ticker, x: this.app.screen.width / 2, y: this.app.screen.height / 2 }); } public pause() { this.app.ticker.remove(this.onTick); } public destroy() { this.app.destroy(true, { children: true, texture: true, baseTexture: true }); } public didMount() { this.onTick = (delta) => this.baseGameLoop(delta); this.onTick = this.onTick.bind(this); this.app.ticker.add(this.onTick); } /** * Please only call once!! * Usage: const container = useRef<HTMLDivElement>(null); useEffect(() => { application.register(container.current!); }, []); */ public register(curr: HTMLDivElement) { curr.appendChild(this.app.view); } updateSelf(props: Props) { this.state.appSize = appSizeFromWindowSize(new Vector2(props.windowState.innerWidth, props.windowState.innerHeight)); } // shim, called from react, possibly many times , possibly at any time, including during the baseGameLoop below // props should be a referentially distinct object from props the last time this was called rerender(props: Props) { console.log("base app rerender called", { playerUI: props.gameState.playerUI }); this.props = props; if (!this.rootComponent) { // finish initialization this.rootComponent = new RootComponent({ args: { renderer: this.app.renderer, markForceUpdate: () => { }, }, updaters: this.props.updaters, delta: 0, gameState: this.props.gameState, appSize: this.state.appSize, }) this.app.stage.addChild(this.rootComponent.container); this.renderSelf(this.props); // test // createBunnyExample({ parent: this.app.stage, ticker: this.app.ticker, x: this.app.screen.width / 2, y: this.app.screen.height / 2 }); this.didMount(); } } renderSelf(props: Props) { this.app.renderer.resize(this.state.appSize.x, this.state.appSize.y); } baseGameLoop(delta: number) { // assume props is up to date this.updateSelf(this.props); // send props downwards this.rootComponent?.update({ args: { renderer: this.app.renderer, markForceUpdate: () => { }, }, updaters: this.props.updaters, delta, gameState: this.props.gameState, appSize: this.state.appSize, }); this.renderSelf(this.props); this.props.args.fireBatch(); // fire enqueued game state updates, which should come back from react in the rerender() } }