Crear figuras de colisión 2D para cuerpos físicos por lo general no suele ser algo tan complicado, ya que frecuentemente se busca por simplificar y hacer rendir al máximo, llegando históricamente en la mayoría de casos al uso de rectángulos o formas simples que nos permitan una representación mínima y funcional de esa colisión.

Hay otros casos que también estan cubiertos, como lo son figuras esféricas (como una bola) donde podemos simplemente aprovechar creando una figura cirular.

El problema aparece cuando nuestra figura a representar es compleja y no alcanzan estas primitivas para representarlo adecuadamente. Por ejemplo, en mi caso lo sería un tablero de pinball 2D, donde queremos que la colisión con los bordes internos sea perfecta y coincida con la imagen (recurso) provisto.

En esos casos tenemos varios enfoques, porque lo más importante acá es que tal como dije coincida con el recurso provisto. Para ello se pueden usar herramientas como PhysicsEditor, que nos va a permitir armar la geometría del dibujo, cuya estructura llevamos a un archivo "JSON" para interpretarlo desde nuestro código y librería Matter.js.

Sin embargo la opción que mas me convence a mí (ya que no solo es gratuita sino que permite mayor autonomía y escapa de las licencias), es el uso de archivos SVG.

En mi caso, el recurso es un archivo PNG, así que con una herramienta de vectorizado, voy a trazar en una capa diferente lo que me interesa para la geometría de colisión y exportarla como SVG.

Si bien no es el objetivo de este documento, dejaré una breve explicación de como hacer este trazado en Inkscape.

Primero importamos la imagen original, en este caso como es una referencia hay que poner la propiedad en "link".




Voy a "document properties" (propiedades del documento) y establezco la misma resolución que tiene mi imagen. Es importante pasar el sistema a píxeles, para trabajar con valores semejantes. También en mi caso seteo la escala a 1.



Aún así es probable que la imagen y el documento no coincidan en dimensiones, sobretodo si nuestra imagen ya traía transparencia (ya que esos pixeles puede no contarlos el editor). Para resolver esto, procedo a clickear la imagen y luego en las propiedades de la barra superior editar las dimensiones para que coincidan.



Hecho esto, vamos a "Capas y Objetos" (Layers and Objects), y clickeamos en el candado para hacerle "lock" a cualquier interacción con nuestra capa de referencia. En mi caso también le reduzco algo la opacidad para poder distinguir lo que voy a trazar encima. Creo una nueva capa, y selecciono el "Pen Tool" en la barra de herramientas. Con ella trazarás los "paths" para quedar con el collision shape final.

Una vez terminado, en el panel de capas ocultamos la referencia para poder exportar sin ella. Vamos a "Guardar Como..." (Save As...) y elegimos el formato SVG.

Ahora, con el archivo en nuestra carpeta de assets, voy a agregar algunas líneas a mi proyecto para poder hacer uso de todo esto. Recordemos que mi juego lo estoy haciendo en Pixi.js, sin embargo si estás usando otro sistema vas a encontrar que muchas de las cosas que voy a usar son adaptables simplemente cambiando la sintaxis o ubicación, ya que tendrán en común la librería Matter.js.

Empiezo en mi código, agregando más componentes a la librería de Matter.js:

src/GameHandler.ts (Importación)

TypeScript
import * as PIXI from 'pixi.js';
import { Engine, World, Body, Composite, Bodies, Svg, Vertices } from 'matter-js';
import { GameObject } from './GameObject';

En mi manifest.json he declarado un bundle especial para separar mis colliders de otras texturas:

public/manifest.json

JSON
    {
      "name": "colliders-assets",
      "assets": [
        {
          "alias": "board1_collision",
          "src": "assets/colliders/board1.svg"
        }
      ]
    }

Debemos cargar ese bundle:

src/GameHandler.ts (Función loadAssetsGlobally)

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

Recordá que yo lo que hago es escalar responsive con el aspect ratio y otras movidas, así que el código me quedaría mucho mas largo, por lo tanto voy a omitir algunas partes donde hago eso y mostraré lo principal que es el poder leer el SVG. Primero obtenemos el elemento del bundle, o sea mi recurso SVG específico. Luego buscamos su propiedad "path", que es el trazado que hicimos en Inkscape:

src/GameHandler.ts (Dentro de setupStage)

TypeScript
// ...
    private setupStage() {

        const svgAsset = PIXI.Assets.get('board1_collision'); 
        const paths = svgAsset.getElementsByTagName('path');

        for (let i = 0; i < paths.length; i++) {
            const path = paths[i];
            
            const vertices = Svg.pathToVertices(path, 10); 
            
            const verticesArray = [vertices]; 

            const body = Bodies.fromVertices(
                this.GAME_W / 2, 
                this.GAME_H / 2, 
                verticesArray, 
                { 
                    isStatic: true, 
                    label: `PinballWall_${i}`,
                    render: { visible: false } 
                }, 
                true 
            );

            this.staticBodies.push(body);
        }
    }

Habiendo obtenido los "Paths" o trazados, lo que necesitamos es recorrerlos para ir creando diferentes cuerpos en Matter.js. ¿Por qué no uno solo?... si nuestra colisión la armamos con diferentes piezas trazando diferente "paths", probablemente tenemos lo que se llama una figura cóncava, y prácticamente en todos los engines el cálculo es mas eficiente y preciso en figuras convexas... entonces para evitar problemas, para cada path creamos un cuerpo distinto y de esta forma ahorramos el dolor de cabeza. Sin embargo no es obligatorio hcaerlo de esta forma.

Como podemos ver, obtengo los vértices en un array [], lo guardo en un array bidimensional [][] para que sea compatible con Matter.js.

Las otras líneas simplemente son el constructor de un "body" o cuerpo, al cual le decimos que será estático, donde estará su posición y cuales serán los vértices o puntos de su path que conformarán por lo tanto la figura de colisión.

Esta se podría decir es la forma correcta de usar un archivo SVG, aprovechando el path para conformar la figura de colisión de los cuerpos físicos en Matter.js.

Importante: Matter.js en la versión actual tiene un problema (desde hace años) con polyFill, la cual está obsoleta y no ha sido reemplazada correctamente por algo idoneo. Por lo tanto al leer el path vamos a encontrarnos con errores. Tenemos varias formas de solucionarlo, que van desde parchear manualmente, tanto como instalar manualmente polyFill. Yo en mi caso elegí una alternativa distinta, que es convertir el SVG a JSON, creando mi propio conversor y así poder leer los paths óptimamente.