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.