¡Excelente patrón! El Game Object Pattern es fundamental en el desarrollo de juegos para mantener la lógica organizada y sincronizada.
Aquí tienes el artículo adaptado, separando la clase GameObject en su propio archivo (GameObject.ts) y aplicando la gestión de listas en GameHandler.ts, tal cual lo has descrito.
Implementación del Patrón Game Object (Actor)
Vamos a proceder a implementar un Game Object Pattern, ¿y para qué?... bueno, como un objeto va a estar compuesto por un Sprite y por un cuerpo físico (que a su vez tendrá figura de colisión y otras propiedades), sería algo tedioso manejar todo esto por separado, tenerlo disperso dentro del GameHandler. Lo idóneo acá es que al interpretarlo como un objeto con todas estas propiedades, podemos asociarlo al concepto de clases de inmediato.
Entonces de alguna forma, no solo agruparíamos las propiedades y parte de la lógica de nuestro objeto, sino también sincronizaríamos a nuestro Sprite respecto al cuerpo físico, ya que deben corresponder en su ubicación el uno al otro.
Dada esta explicación procedemos, creando para ello la clase Actor o GameObject (el nombre que te guste):
src/GameObject.ts (Nuevo Archivo)
import * as PIXI from 'pixi.js';
import { Body, World, Composite } from 'matter-js';
export class GameObject {
public sprite: PIXI.Sprite;
public body: Body;
constructor(sprite: PIXI.Sprite, body: Body) {
this.sprite = sprite;
this.body = body;
this.sprite.x = this.body.position.x;
this.sprite.y = this.body.position.y;
}
public update() {
this.sprite.x = this.body.position.x;
this.sprite.y = this.body.position.y;
this.sprite.rotation = this.body.angle;
}
public destroy(world: World) {
this.sprite.removeFromParent();
Composite.remove(world, this.body);
}
}
En este código vemos poca diferencia respecto a lo observado anteriormente. Una de ellas es que ahora no solo importamos "World" de la librería Matter.js, sino también "Composite", y esto es porque World no posee acceso a la definición de la función destroy() directamente, así que nos sería imposible eliminar un cuerpo desde allí mismo. En cambio Composite que es una suerte de "Collection" de donde proviene World, va a permitirnos completar esta operación, haciendo "remove".
En el resto del código, no sucede nada difícil de comprender. Simplemente se actualiza la posición del Sprite, para que sea equivalente a la posición del cuerpo físico (body), así como tambien su rotación en radianes.
Como en este (y muchos otros casos) vamos a necesitar actualizar en cada momento a los objetos, lo conveniente es que al instanciar cada objeto lo guardemos en una lista, y luego que GameHandler se encargue de iterar sobre esa lista para ejecutar "update" en cada uno. Empezamos importando la clase GameObject para poder usarla:
src/GameHandler.ts (Importación)
// ... (imports existentes)
import { Engine, World, Body, Composite, Bodies } from 'matter-js'; // Añadimos Body, Composite y Bodies
import { GameObject } from './GameObject';
Ahora creamos una lista vacía donde guardaremos elementos de este tipo:
src/GameHandler.ts (Propiedades de GameHandler class)
// ... (propiedades existentes)
private readonly MAX_FRAME_TIME = 1000 / 30;
private levelObjects: GameObject[] = [];
private dynamicGameObjects: GameObject[] = [];
private staticBodies: Body[] = [];
// ...
En este ejemplo mostraré como instancio un nuevo objeto de la clase GameObject, y a su vez lo agrego a la lista. La parte de la creación del cuerpo físico y del Sprite no está incluída.
const ballObject = new GameObject(ballSprite, ballBody);
this.gameObjects.push(ballObject);
Y tranquilamente en la función update podemos iterar sobre la lista de objetos, y llamar a la función update de cada uno:
this.gameObjects.forEach(obj => obj.update());
Ahora, hay que tener en cuenta una cosa. Nuestro juego puede tener objetos físicos estáticos y dinámicos. Sería muy pobre estar actualizando constantemente las propiedades de cuerpos y Sprites que nunca van a moverse en absoluto. Para esto tenemos varias soluciones posibles, pero la que considero más poderosa es la que se aproxima más a un concepto conocido como ECS. Para hacerlo simple, vamos a crear 2 listas, una de cuerpos que podrán moverse y por lo tanto necesitamos actualizar, y otra para cuerpos estáticos. Podríamos crear incluso una tercer lista con todos los objetos del juego, por si necesitamos buscar algo dentro sin discriminar por física.
Dicho esto, arrancamos de nuevo con otra perspectiva para el GameHandler, creando esta vez 3 listas:
private levelObjects: GameObject[] = [];
private dynamicGameObjects: GameObject[] = [];
private staticBodies: Body[] = [];
Ahora vamos a la creación de los objetos, integración a cada lista y por último agregado al mundo físico y del Stage para su renderizado:
src/GameHandler.ts (Método setupStage modificado, con un ejemplo completo)
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;
// Creación de cuerpo estático de ejemplo (Pared/Suelo)
const wallBody = Bodies.rectangle(this.GAME_W / 2, this.GAME_H - 10, this.GAME_W, 20, { isStatic: true });
this.staticBodies.push(wallBody);
// Creación de objeto dinámico de ejemplo (Pelota)
const ballBody = Bodies.circle(this.GAME_W / 2, 100, 20);
const ballSprite = PIXI.Sprite.from('board1_texture');
ballSprite.anchor.set(0.5);
const ballObject = new GameObject(ballSprite, ballBody);
this.levelObjects.push(ballObject);
this.dynamicGameObjects.push(ballObject);
const dynamicBodies = this.dynamicGameObjects.map(obj => obj.body);
const allBodiesToAdd = [...this.staticBodies, ...dynamicBodies];
World.add(this.engine.world, allBodiesToAdd);
this.levelObjects.forEach(obj => this.app.stage.addChild(obj.sprite));
}
Nuestra función update ahora solo debería iterar sobre la lista de objetos dinámicos:
src/GameHandler.ts (Método update modificado)
private update(ticker: PIXI.Ticker) {
let delta = Math.min(ticker.deltaMS, this.MAX_FRAME_TIME);
this.accumulator += delta;
while (this.accumulator >= this.FIXED_TIMESTEP) {
Engine.update(this.engine, this.FIXED_TIMESTEP);
this.accumulator -= this.FIXED_TIMESTEP;
}
this.dynamicGameObjects.forEach(obj => obj.update());
}
No olvidemos de tener en cuenta todo esto en el método destroy, para limpiar adecuadamente:
src/GameHandler.ts (Método destroy modificado)
public destroy() {
Engine.clear(this.engine);
const world = this.engine.world;
this.levelObjects.forEach(obj => obj.destroy(world));
this.staticBodies.forEach(body => Composite.remove(world, body));
this.app.ticker.remove(this.updateFn);
}
En mi código adicionalmente he hecho que la llamada a init() y alguna otra función se vuelvan asíncronas (async) para permitirle al sistema cargar los recursos necesarios antes de empezar, sin embargo eso va a depender mucho de tu proyecto y como lo tengas estructurado.
Descarga el código de ejemplo:

0 Comentarios