A Scoring System in Your Babylon.js Game

We've come a long way in building our data-driven Babylon.js game! Our entities are moving, animating, colliding realistically, and we even have a basic scorekeeping system. But what if we want to award points for specific actions, like collecting power-ups or defeating enemies? Let's revamp our score system to handle more flexible point additions.

Starting Simple: A Basic ScoreComponent

Our ScoreComponent remains the same, storing an integer representing the player's score:

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

export class ScoreComponent extends Component {
  score: number;

  constructor(data) {
    super(data);
    this.score = 0; // Initialize score to 0
  }
}

A Rudimentary ScoreSystem

Our initial ScoreSystem will be similar, but instead of automatically updating scores, we'll add a method to allow external systems to add points:

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

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

  processEntity(entity: Entity, deltaTime: number) {
    // We'll handle score updates through a separate method
  }

  addScore(entity: Entity, points: number) {
    const scoreComponent = entity.getComponent(ScoreComponent);
    if (scoreComponent) {
      scoreComponent.score += points;
    }
  }
}

Now, other systems can call scoreSystem.addScore(playerEntity, 10) to award 10 points to the player.

Displaying Scores: Using Babylon.js GUI

We'll still use Babylon.js's GUI library to display the scores:

import { EntityManager, Entity, System } from "../lib";
import { ScoreComponent } from "../components/ScoreComponent";
import { AdvancedDynamicTexture, TextBlock } from "@babylonjs/gui";

export class ScoreSystem extends System {
  guiTexture: AdvancedDynamicTexture;

  constructor(entityManager: EntityManager) {
    super(entityManager, [ScoreComponent]);
    console.log("ScoreSystem initialized");

    // Create a GUI texture
    this.guiTexture = AdvancedDynamicTexture.CreateFullscreenUI("UI");
  }

  processEntity(entity: Entity, deltaTime: number) {
    const scoreComponent = entity.getComponent(ScoreComponent);

    // Update or create score label for the entity
    let scoreLabel = this.guiTexture.getControlByName(`score-${entity.id}`) as TextBlock;
    if (!scoreLabel) {
      scoreLabel = new TextBlock(`score-${entity.id}`, `Score: ${scoreComponent.score}`);
      scoreLabel.color = "white";
      scoreLabel.fontSize = 24;
      this.guiTexture.addControl(scoreLabel);
    } else {
      scoreLabel.text = `Score: ${scoreComponent.score}`;
    }
  }

  addScore(entity: Entity, points: number) {
    const scoreComponent = entity.getComponent(ScoreComponent);
    if (scoreComponent) {
      scoreComponent.score += points;
    }
  }
}

The processEntity method now focuses solely on updating the score labels, while the addScore method handles score changes.

Handling Two Players: Distinct Score Labels

We'll position the score labels for our two players as before:

// ... (Previous imports)

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

  processEntity(entity: Entity, deltaTime: number) {
    // ... (Score label update logic)

      // Position labels based on entity name
      if (entity.name === 'Player1') {
        scoreLabel.top = "10%";
        scoreLabel.left = "10%";
      } else if (entity.name === 'Player2') {
        scoreLabel.top = "10%";
        scoreLabel.right = "10%";
      }

      this.guiTexture.addControl(scoreLabel);
    } else {
      scoreLabel.text = `Score: ${scoreComponent.score}`;
    }
  }

  // ... (addScore method)
}

Triggering Score Changes: A Data-Driven Approach

Let's imagine a scenario where players collect coins to earn points. Instead of hardcoding score logic in a separate CoinSystem, we can leverage our existing CollisionSystem and scene.json data to achieve this in a truly data-driven way.

First, let's define a "Coin" entity in our scene.json:

{
  "entities": {
    // ... other entities

    "Coin": {
      "components": {
        "Asset": {
          "name": "Coin",
          "path": "glb/coin.glb"
        },
        "Collision": {
          "type": "passive"
        },
        "Score": {
          "value": 10
        }
      }
    }
  }
}

Notice the new Score component. We'll create a simple ScoreComponent to hold the point value associated with the coin:

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

export class ScoreComponent extends Component {
  value: number;

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

Now, let's modify our CollisionSystem to handle score updates when a player collides with a coin:

// ... (Previous imports)

export class CollisionSystem extends System {
  // ... (Constructor and colliders array)

  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
          ) {
            // Collision detected!

            // Check if the collided entity is a coin
            const otherEntity = this.entityManager.entities.get(collider.name);
            if (otherEntity && otherEntity.hasComponent(ScoreComponent)) {
              const scoreComponent = otherEntity.getComponent(ScoreComponent);
              this.entityManager.scoreSystem.addScore(entity, scoreComponent.value);

              // Optionally: Remove the coin from the scene
              this.entityManager.removeEntity(otherEntity);
            }
          }
        });
      }
    }
  }
}

Explanation:

  1. Inside the collision detection loop, we check if the collided entity has a ScoreComponent.
  2. If it does, we retrieve the value from the ScoreComponent and use our ScoreSystem's addScore method to award points to the player who collided with the coin.
  3. We can optionally remove the coin entity from the scene to prevent the player from collecting it again.

In Sum

We've successfully integrated score updates into our collision system using a data-driven approach! By defining score values within our scene.json and leveraging our existing CollisionSystem, we've created a flexible and elegant way to handle score changes based on entity interactions. This approach keeps our code clean, maintainable, and allows us to easily modify scoring rules by simply tweaking our data files. Keep experimenting, keep iterating, and watch your game become more engaging and dynamic!

The End