Simple Collision Detection in Your Babylon.js Game

In our previous blog posts, we've covered a lot of ground in building a data-driven game with Babylon.js. We've loaded assets, implemented movement and animations, and even set up cameras and lights. But our entities are still ghosting through each other! Let's rectify that by adding collision detection.

Starting Simple: A Basic Collision Component

We'll begin with a straightforward CollisionComponent to mark entities that can collide:

import { Component } from "../lib";

export class CollisionComponent extends Component {
  constructor(data) {
    super(data);
  }
}

For now, it's just an empty shell, inheriting from our base Component class.

A Rudimentary Collision System

Next, let's create a basic CollisionSystem:

import { EntityManager, Entity, System } from "../lib";
import { CollisionComponent } from "../components/CollisionComponent";

export class CollisionSystem extends System {
  constructor(entityManager: EntityManager) {
    super(entityManager, [CollisionComponent]);
    console.log("CollisionSystem initialized");
  }

  processEntity(entity: Entity, deltaTime: number) {
    // Placeholder: Collision logic will go here
  }
}

This system doesn't do much yet. It simply processes entities that have a CollisionComponent.

Adding Collision Logic: Using Babylon.js's intersectsMesh

Let's enhance our CollisionSystem to actually detect collisions:

import { EntityManager, Entity, System } from "../lib";
import { CollisionComponent } from "../components/CollisionComponent";
import { AssetComponent } from "../components/AssetComponent";
import { Mesh } from "@babylonjs/core";

export class CollisionSystem extends System {
  constructor(entityManager: EntityManager) {
    super(entityManager, [CollisionComponent, AssetComponent]);
    console.log("CollisionSystem initialized");
  }

  processEntity(entity: Entity, deltaTime: number) {
    const collisionComponent = entity.getComponent(CollisionComponent);
    const assetComponent = entity.getComponent(AssetComponent);

    if (collisionComponent && assetComponent && assetComponent.colliderMesh) {
      const colliderMesh = assetComponent.colliderMesh;

      // Iterate through other entities with colliders
      for (const otherEntity of this.entityManager.entities.values()) {
        if (otherEntity !== entity && otherEntity.hasComponent(CollisionComponent)) {
          const otherAssetComponent = otherEntity.getComponent(AssetComponent);
          if (otherAssetComponent && otherAssetComponent.colliderMesh) {
            const otherColliderMesh = otherAssetComponent.colliderMesh;

            // Check for intersection
            if (colliderMesh.intersectsMesh(otherColliderMesh, true)) {
              console.log(`${entity.name} collided with ${otherEntity.name}`);
              // Implement collision response here
            }
          }
        }
      }
    }
  }
}

Now, our CollisionSystem iterates through all entities with CollisionComponent and AssetComponent components. It retrieves their colliderMesh (assuming you've set this up in your AssetComponent) and checks for intersections using Babylon.js's intersectsMesh method.

Refining Collision Response: Preventing Overlap

Let's add a simple collision response to prevent entities from overlapping:

import { EntityManager, Entity, System } from "../lib";
import { CollisionComponent } from "../components/CollisionComponent";
import { AssetComponent } from "../components/AssetComponent";
import { Mesh, Vector3 } from "@babylonjs/core";
import { MovementComponent } from "../components/MovementComponent";

export class CollisionSystem extends System {
  constructor(entityManager: EntityManager) {
    super(entityManager, [CollisionComponent, AssetComponent]);
    console.log("CollisionSystem initialized");
  }

  processEntity(entity: Entity, deltaTime: number) {
    // ... (Collision detection logic from previous example)

            if (colliderMesh.intersectsMesh(otherColliderMesh, true)) {
              console.log(`${entity.name} collided with ${otherEntity.name}`);

              // Collision Response:
              if (entity.hasComponent(MovementComponent)) {
                const movementComponent = entity.getComponent(MovementComponent);
                movementComponent.velocity = Vector3.Zero(); // Stop movement
                // You could add additional logic here to push the entity back slightly
              }
            }
          }
        }
      }
    }
  }
}

We check if the colliding entity has a MovementComponent. If so, we stop its movement by setting its velocity to zero. You could add more sophisticated logic here, like pushing the entity back along its collision normal.

Introducing Collision Types: Active vs. Passive

Let's add the type property to our CollisionComponent to differentiate between active and passive colliders:

import { Component } from "../lib";

export class CollisionComponent extends Component {
  type: string; // 'active' or 'passive'

  constructor(data) {
    super(data);
    this.type = data.type;
  }
}

Now, we can modify our CollisionSystem to only process collisions involving "active" colliders:

// ... (Previous imports)

export class CollisionSystem extends System {
  // ... (Constructor)

  processEntity(entity: Entity, deltaTime: number) {
    const collisionComponent = entity.getComponent(CollisionComponent);
    const assetComponent = entity.getComponent(AssetComponent);

    if (collisionComponent.type === 'active' && assetComponent && assetComponent.colliderMesh) {
      // ... (Collision detection and response logic)
    }
  }
}

Optimizing for Performance: Storing Colliders

We can improve performance by storing references to all colliders in an array, avoiding the need to iterate through all entities every frame:

// ... (Previous imports)

export class CollisionSystem extends System {
  colliders: Mesh[] = [];

  // ... (Constructor)

  processEntity(entity: Entity, deltaTime: number) {
    const collisionComponent = entity.getComponent(CollisionComponent);
    const assetComponent = entity.getComponent(AssetComponent);

    if (collisionComponent && assetComponent && assetComponent.colliderMesh) {
      const m = assetComponent.colliderMesh;
      if (m && !m.checkCollisions) {
        m.checkCollisions = true;
        this.colliders.push(m);
      }
      // ... (Collision detection and response logic using this.colliders)
    }
  }
}

The Final CollisionComponent and CollisionSystem

Here's our final, optimized code:

// CollisionComponent.ts
import { Component } from "../lib";

export class CollisionComponent extends Component {
  type: string; // 'active' or 'passive'

  constructor(data) {
    super(data);
    this.type = data.type;
  }
}
// CollisionSystem.ts
import { EntityManager, Entity, System } from "../lib";
import { CollisionComponent } from "../components/CollisionComponent";
import { AssetComponent } from "../components/AssetComponent";
import { Mesh, Vector3 } from "@babylonjs/core";
import { MovementComponent } from "../components/MovementComponent";

export class CollisionSystem extends System {
  colliders: Mesh[] = [];
  constructor(
    entityManager: EntityManager,
    componentClasses = [CollisionComponent, AssetComponent],
  ) {
    super(entityManager, componentClasses);
    console.log("CollisionSystem initialized");
  }
  processEntity(entity: Entity, deltaTime: number) {
    const collisionComponent = entity.getComponent(CollisionComponent);
    const assetComponent = entity.getComponent(AssetComponent);
    let movementComponent;
    if (collisionComponent.type == "active") {
      movementComponent = entity.getComponent(MovementComponent);
    }
    if (collisionComponent && assetComponent && assetComponent.colliderMesh) {
      const m = assetComponent.colliderMesh;
      if (m && m.checkCollisions == false) {
        m.checkCollisions = true;
        this.colliders.push(m);
      } else {
        return;
        this.colliders.forEach((collider) => {
          if (
            m != collider &&
            m.intersectsMesh(collider, true) &&
            movementComponent?.velocity.length &&
            movementComponent.overrideTimer == 0
          ) {
            movementComponent.velocity = Vector3.Zero();
            movementComponent.position.addInPlace(
              movementComponent.velocity
                .scale(-1)
                .multiplyByFloats(1.2, 0, 1.2),
            );
            movementComponent.overrideTimer = 5;
          }
        });
      }
    }
  }
}

In Sum

We've successfully implemented collision detection in our Babylon.js game! By starting with a simple component and system and gradually adding complexity, we've created a robust and efficient solution for handling entity collisions. Remember, game development is often an iterative process. Start with a basic implementation and gradually refine it, adding features and optimizations as needed.