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:
- Inside the collision detection loop, we check if the collided entity has a
ScoreComponent
. - If it does, we retrieve the
value
from theScoreComponent
and use ourScoreSystem
'saddScore
method to award points to the player who collided with the coin. - 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