Continuando con Pixi.js, vamos a ver como adaptar nuestro proyecto para trabajar con Fixed Logical Resolution y Adaptative Scaling.

Es bastante común que busquemos que nuestro juego o app sea responsive, o que se adapte al menos a los diferentes tamaños de ventana y resizes. Por ello con estas 2 soluciones vamos a trabajar en un sistema donde daremos una resolución "global de referencia", la cual luego adaptaremos al tamaño real de la ventana, calculando para ello el aspect ratio o proporciones, y ajustando al mínimo para no excedernos.

Esto es ideal para poder trabajar con coordenadas en píxeles, y que sigan funcionando perfectamente en diferentes resoluciones (si escalamos), respetando así las ubicaciones originales de los objetos (sino tenderían a desacomodarse en el reajuste).

Lo primero será declarar variables que van a funcionar como una resolución de referencia. En mi caso elegí 800x1200, pero dependerá también un poco del aspect ratio final deseado.

src/GameHandler.ts

TypeScript
import * as PIXI from 'pixi.js';

const loadAssetsGlobally = async () => {
    if ((PIXI.Assets.resolver as any).isInitialized) {
        return; 
    }
    
    await PIXI.Assets.init({ manifest: 'manifest.json' });
    await PIXI.Assets.loadBundle('texture-assets');
};

export const assetsReadyPromise = loadAssetsGlobally(); 

const GAME_WIDTH = 800;
const GAME_HEIGHT = 1200;

Luego crearemos una función que se va a encargar de calcular la escala deseada, según el tamaño de la ventana actual (la cual recibirá como argumentos). Para este cálculo usará una regla de 3 simple, y ajustará basándose en el menor valor de proporción para no excederse de pantalla.

src/GameHandler.ts

TypeScript
export const calculateStageTransform = (rendererWidth: number, rendererHeight: number) => {
    const scaleX = rendererWidth / GAME_WIDTH;
    const scaleY = rendererHeight / GAME_HEIGHT;
    
    const scale = Math.min(scaleX, scaleY);
    
    const scaledGameWidth = GAME_WIDTH * scale;
    const scaledGameHeight = GAME_HEIGHT * scale;
    
    const x = (rendererWidth - scaledGameWidth) / 2; 
    const y = (rendererHeight - scaledGameHeight) / 2;
    
    return { scale, x, y };
};

Ahora por comodidad creamos una función que va a encargarse de llamar a el cálculo anterior cada vez que la ventana se redimensione (o inclusive inicialmente):

src/GameHandler.ts

TypeScript
export const onResize = (app: PIXI.Application) => {
    const rendererWidth = app.renderer.width;
    const rendererHeight = app.renderer.height;

    const { scale, x, y } = calculateStageTransform(rendererWidth, rendererHeight);
    
    app.stage.scale.set(scale);
    app.stage.x = x; 
    app.stage.y = y; 
};

Empezamos pasando en el app.init las dimensiones de referencia:

src/PinballGame.tsx (Parte de initializeGame)

TypeScript
// ... después de await assetsReadyPromise;
const app = new PIXI.Application();
            
await app.init({
    width: 800, 
    height: 1200, 
    resizeTo: containerRef.current,
    background: '#111111',
    autoDensity: true,
});
// ...

Vamos a usar la función de resize en varios bloques, así que crearemos una referencia a la misma:

TypeScript
const onResizeRef = useRef<() => void>(() => {});

Antes de instanciar el GameHandler, forzamos a un primer resize inicial, para que todo se ajuste correctamente:

src/PinballGame.tsx (Parte de initializeGame)

TypeScript
// ... después de la inicialización de la app
containerRef.current?.appendChild(app.canvas);
appRef.current = app; 

const listener = () => onResize(app);
onResizeRef.current = listener;

app.renderer.on('resize', listener);
onResize(app); 

const gameHandler = new GameHandler(app);
gameHandler.init();
gameHandlerRef.current = gameHandler;
// ...

No olvidemos de limpiar correctamente en el destroy:

src/PinballGame.tsx (Parte de return)

TypeScript
// ... dentro del return de useEffect
if (appRef.current && onResizeRef.current) {
    appRef.current.renderer.off('resize', onResizeRef.current);
}

if (gameHandlerRef.current) {
    gameHandlerRef.current.destroy(); 
    gameHandlerRef.current = null;
}

if (appRef.current) {
    appRef.current.destroy(true, { children: true, context: true });
    appRef.current = null;
}
// ...

Excelente. Esto queda fenomenal para el stage, que es nuestro componente principal. Pero vamos a necesitar algo similar para nuestros Sprites dentro del juego, los cuales he decidido manejar en GameHandler.ts. Así que allí, vamos a empezar declarando nuevamente la resolucion de referencia:

src/GameHandler.ts (Dentro de GameHandler class)

TypeScript
export class GameHandler {
    private app: PIXI.Application;
    private updateFn: (ticker: PIXI.Ticker) => void;
    
    private readonly GAME_W = 800; 
    private readonly GAME_H = 1200; 
// ...

En este caso lo importante es solo la escala, así que creamos la siguiente función para obtener el valor según la proporción:

src/GameHandler.ts (Dentro de GameHandler class)

TypeScript
// ...
    private calculateAssetScale(asset: PIXI.Sprite): number {
        const scaleXToLogical = this.GAME_W / asset.width;
        const scaleYToLogical = this.GAME_H / asset.height;
        
        return Math.min(scaleXToLogical, scaleYToLogical);
    }
// ...

Acá un ejemplo de como lo aplicaríamos:

src/GameHandler.ts (Dentro de setupStage method)

TypeScript
// ...
    private setupStage() {
        const background = PIXI.Sprite.from('board1_texture'); 
        
        const scaleToFit = this.calculateAssetScale(background);
        background.scale.set(scaleToFit); 

        background.anchor.set(0.5);
        background.x = this.GAME_W / 2;
        background.y = this.GAME_H / 2;
        
        this.app.stage.addChild(background);
    }
// ...

Codigo descargable aquí: https://www.patreon.com/posts/pixi-js-fixed-145513029?utm_medium=clipboard_copy&utm_source=copyLink&utm_campaign=postshare_creator&utm_content=join_link