Wrangling Game Objects with an EntityManager

Let's talk about managing game objects in our data-driven Babylon.js world. We need a way to create, track, update, and destroy entities efficiently. That's where the EntityManager class comes in, acting as the conductor of our ECS orchestra.

The EntityManager: Your Entity Command Center

The EntityManager class is the central hub for managing all the entities in our game. It provides methods for creating, finding, updating, and deleting entities, abstracting away the complexities of entity management.

import { Scene } from "@babylonjs/core";

export class EntityManager {
  entities: Map<String, Entity>; // Stores our entities, keyed by their names
  scene: Scene;  // A reference to the Babylon.js scene

  constructor(scene: Scene) {
    this.entities = new Map();
    this.scene = scene;
  }

  search(name: string[] | string) {
    // Find one or more entities by name
    if (Array.isArray(name)) {
      return name.map((n) => {
        return this.entities.get(n);
      });
    }
    return this.entities.get(name);
  }

  getPlayers() {
    // A helper to quickly get player entities
    return [this.entities["Player"], this.entities["Player2"]];
  }

  createEntity(name) {
    // Creates a new entity and adds it to our collection
    const entity = new Entity(name);
    this.entities.set(entity.name, entity);
    if (name == "Player") {
      this.playerEntity = entity;
    }
    if (name == "Player2") {
      this.player2Entity = entity;
    }
    return entity;
  }

  removeEntity(entity: Entity) {
    // Removes an entity from our game
    this.entities.delete(entity.id);
  }

  serialize() {
    // Serializes entity data, useful for saving/loading
    const entitiesData = [];
    for (const entity of this.entities) {
      entitiesData.push(entity.serialize());
    }
    return JSON.stringify(entitiesData, null, 2); // Indented JSON string
  }
}

Breakdown:

  • entities Map: This stores our entities. We're using a Map for efficient key-value lookups.
  • scene Reference: Keeps a handy reference to our Babylon.js scene.
  • search(name): Finds one or more entities based on their names.
  • getPlayers(): A convenience method for fetching player entities (you can add more specific getters as needed).
  • createEntity(name): Creates a new Entity and adds it to our entities map.
  • removeEntity(entity): Removes an entity, effectively destroying it in our game world.
  • serialize(): Converts our entity data into a JSON string, useful for saving game state.

The Entity Class: A Container for Components

Now, let's define the Entity class. Remember, entities are primarily containers for components:

// Entity.ts
export class Entity {
  private static _nextId = 1; // Generates unique IDs for our entities
  public name: string; // The name of the entity
  private _id: number; // The entity's unique ID
  private _components: Map<string, any>; // Stores the entity's components

  constructor(name: string) {
    this._id = Entity._nextId++;
    this._components = new Map();
    this.name = name;
  }

  get id(): number {
    return this._id;
  }

  // Add a component to this entity
  addComponent(component: any): Entity {
    this._components.set(component.constructor.name, component);
    return this;
  }

  // Remove a component from this entity by its class
  removeComponent(componentClass: any): Entity {
    this._components.delete(componentClass.name);
    return this;
  }

  // Get a component by its class
  getComponent<T>(componentClass: new (...args: any[]) => T): T {
    return this._components.get(componentClass.name) as T;
  }

  // Check if the entity has a specific component
  hasComponent(componentClass: any): boolean {
    return this._components.has(componentClass.name);
  }

  serialize() {
    // Convert component data to a serializable format
    const data = {
      id: this.id,
      components: {},
    };

    for (const component of this._components) {
      data.components[component.constructor.name] = component.serialize();
    }

    return data;
  }
}

Key Points:

  • _components Map: This stores the components attached to the entity, again using a Map for efficient lookups.
  • addComponent(component): Attaches a component to the entity.
  • removeComponent(componentClass): Detaches a component from the entity.
  • getComponent(componentClass): Retrieves a component by its class (e.g., getComponent(PositionComponent)).
  • hasComponent(componentClass): Checks if the entity has a specific component attached.
  • serialize(): Like the EntityManager, this prepares component data for saving or other operations.

In Sum

With our EntityManager and Entity classes in place, we have a robust foundation for managing game objects in our data-driven Babylon.js game. We can create entities, attach components to them (like position, movement, health, etc.), and use systems to process these components and bring our game logic to life.

Next we'll dive into creating our first component and system so we can load some 3d meshes and start moving them around the scene with player input.