Skip to main content

Step 9 - Scoring Points!

Any good game needs points, so let's add some!

First we'll add a score label and best score to our Level to keep track of the current score for us. Additionally we'll add an incrementScore() to up the value. We can use the browser's localStorage feature to keep track of our best score.

typescript
// level.ts
export class Level extends ex.Scene {
...
score: number = 0;
best: number = 0;
scoreLabel = new ex.Label({
text: 'Score: 0',
x: 0,
y: 0,
z: 1,
font: new ex.Font({
size: 20,
color: ex.Color.White
})
});
bestLabel = new ex.Label({
text: 'Best: 0',
x: 400,
y: 0,
z: 1,
font: new ex.Font({
size: 20,
color: ex.Color.White,
textAlign: ex.TextAlign.End
})
});
onInitialize(engine: ex.Engine): void {
...
this.add(this.scoreLabel);
this.add(this.bestLabel);
const bestScore = localStorage.getItem('bestScore');
if (bestScore) {
this.best = +bestScore;
this.setBestScore(this.best);
} else {
this.setBestScore(0);
}
}
incrementScore() {
this.scoreLabel.text = `Score: ${++this.score}`;
this.setBestScore(this.score);
}
setBestScore(score: number) {
if (score > this.best) {
localStorage.setItem('bestScore', this.score.toString());
this.best = score;
}
this.bestLabel.text = `Best: ${this.best}`;
}
}
typescript
// level.ts
export class Level extends ex.Scene {
...
score: number = 0;
best: number = 0;
scoreLabel = new ex.Label({
text: 'Score: 0',
x: 0,
y: 0,
z: 1,
font: new ex.Font({
size: 20,
color: ex.Color.White
})
});
bestLabel = new ex.Label({
text: 'Best: 0',
x: 400,
y: 0,
z: 1,
font: new ex.Font({
size: 20,
color: ex.Color.White,
textAlign: ex.TextAlign.End
})
});
onInitialize(engine: ex.Engine): void {
...
this.add(this.scoreLabel);
this.add(this.bestLabel);
const bestScore = localStorage.getItem('bestScore');
if (bestScore) {
this.best = +bestScore;
this.setBestScore(this.best);
} else {
this.setBestScore(0);
}
}
incrementScore() {
this.scoreLabel.text = `Score: ${++this.score}`;
this.setBestScore(this.score);
}
setBestScore(score: number) {
if (score > this.best) {
localStorage.setItem('bestScore', this.score.toString());
this.best = score;
}
this.bestLabel.text = `Best: ${this.best}`;
}
}

When our Bird flies between two pipes we want to increment the score. To do this will create a new file score-trigger.ts to detect this.

typescript
// score-trigger.ts
export class ScoreTrigger extends ex.Actor {
constructor(pos: ex.Vector, private level: Level) {
super({
pos,
width: 32,
height: Config.PipeGap,
anchor: ex.vec(0, 0),
vel: ex.vec(-Config.PipeSpeed, 0)
})
this.on('exitviewport', () => {
this.kill();
});
}
override onCollisionStart(): void {
this.level.incrementScore();
}
}
typescript
// score-trigger.ts
export class ScoreTrigger extends ex.Actor {
constructor(pos: ex.Vector, private level: Level) {
super({
pos,
width: 32,
height: Config.PipeGap,
anchor: ex.vec(0, 0),
vel: ex.vec(-Config.PipeSpeed, 0)
})
this.on('exitviewport', () => {
this.kill();
});
}
override onCollisionStart(): void {
this.level.incrementScore();
}
}

Now in our pipe factory we put these triggers in between our pipes, and add ScoreTrigger to our reset() and stop() routines

typescript
// pipe-factory.ts
export class PipeFactory {
...
spawnPipes() {
...
const scoreTrigger = new ScoreTrigger(
ex.vec(
this.level.engine.screen.drawWidth,
randomPipePosition),
this.level
);
this.level.add(scoreTrigger);
}
...
reset() {
for (const actor of this.level.actors) {
if (actor instanceof Pipe ||
actor instanceof ScoreTrigger
) {
actor.kill();
}
}
}
stop() {
this.timer.stop();
for (const actor of this.level.actors) {
if (actor instanceof Pipe ||
actor instanceof ScoreTrigger
) {
actor.vel = ex.vec(0, 0);
}
}
}
}
typescript
// pipe-factory.ts
export class PipeFactory {
...
spawnPipes() {
...
const scoreTrigger = new ScoreTrigger(
ex.vec(
this.level.engine.screen.drawWidth,
randomPipePosition),
this.level
);
this.level.add(scoreTrigger);
}
...
reset() {
for (const actor of this.level.actors) {
if (actor instanceof Pipe ||
actor instanceof ScoreTrigger
) {
actor.kill();
}
}
}
stop() {
this.timer.stop();
for (const actor of this.level.actors) {
if (actor instanceof Pipe ||
actor instanceof ScoreTrigger
) {
actor.vel = ex.vec(0, 0);
}
}
}
}

You might notice that sometimes the score double counts sometimes! Well this is because the bird is using a box collider and is rotating into the score trigger! This is an easy fix, we'll switch to a circle collider in the bird.

typescript
// bird.ts
export class Bird extends ex.Actor {
...
constructor(private level: Level) {
super({
pos: Config.BirdStartPos,
// width: 16,
// height: 16,
radius: 8,
color: ex.Color.Yellow
});
}
...
}
typescript
// bird.ts
export class Bird extends ex.Actor {
...
constructor(private level: Level) {
super({
pos: Config.BirdStartPos,
// width: 16,
// height: 16,
radius: 8,
color: ex.Color.Yellow
});
}
...
}