Start typing to search...
Tutorials
Creating games on Koji
3. Scoring and losing
Search

Scoring and losing

In the previous section of the remixable game tutorial, you added the player and the collectibles to the falling objects game. In this section, you’ll enable the player to catch a falling collectible and add the logic for scoring and losing the game.

By the end of this section, you should feel comfortable:

  • Checking for a collision between the player and a collectible and adding to the game score when a collectible has been caught.

  • Adding simple animations.

  • Taking away a life from the player when a collectible has been missed.

Checking for collisions

To determine when the player has caught a falling object, you can check for collisions between the player and the collectibles. Then, add to the score every time a collision happens.

In frontend/src/Components/Game/Entities/Player.js, add a new function to check for collisions. Then, call the function from update().

...
import { CollisionCircle } from '../Utils/Collision'

export default class Player extends Entity {
    ...

    update(){
        ...
        this.checkCollisions();
    }

    checkCollisions() {
        const collectibles = game.findByTag('collectible');

        collectibles.forEach(collectible => {
            if (!collectible.isCollected && CollisionCircle(this, collectible)) {
                collectible.onCollect();
            }
        });
    }
}

This code uses game.findByTag('collectible') to get all of the Collectible objects. Then, the code checks each collectible to see whether it’s colliding with the Player.

Note
The game.findByTag(tag: string) function works by filtering the game instance’s entities array, and then returning an array that contains all entities with the given tag.

To check for a collision, the code uses the CollisionCircle() function from frontend/src/Components/Game/Utils/Collision.js. This function takes two Entities as arguments and returns true if they are close enough together to collide (based on the size of the entities and their distance apart).

If the player is colliding with a collectible and the collectible in question hasn’t already been collected, the code triggers the onCollect() handler. You need to define the onCollect() handler in frontend/src/Components/Game/Entities/Collectible.js. This function sets the isCollected flag to true.

...

export default class Collectible extends Entity {
    ...


    onCollect() {
        this.isCollected = true;
    }
}

Adding animations for collecting objects

When a collectible object is caught, you could simply remove it from the game. But, let’s spice things up a bit!

The game template has lots of effects that can make this game even more fun. Instead of the collectible simply disappearing upon contact, you can have the player attract it like a magnet and make it explode into particles.

To add these animations, override the update() function in Collectible.js, but keep the original behavior, which is defined in the parent Entity class.

// Don't forget the imports!
import { game } from '..'
import Entity from './Entity'
import playSound from '../Utils/playSound'
import { Smooth, Ease, EasingFunctions } from '../Utils/EasingFunctions'
import { spawnParticles } from '../Effects/Particle'
import { spawnFloatingText } from '../Effects/FloatingText'

export default class Collectible extends Entity {
    constructor(x, y, options){
        super(x, y, options);
        ...

        this.animTimer = 0;
    }

    onCollect() {
        this.isCollected = true;
    }

    update(){
        super.update();
        this.handleAnimation();
    }

    handleAnimation() {
        if (!this.isCollected) return;

        this.animTimer += game.delta() * 4;

        this.scale.x = Ease(EasingFunctions.easeInCubic, this.animTimer, 1, -0.95);
        this.scale.y = Ease(EasingFunctions.easeInCubic, this.animTimer, 1, -0.95);

        this.moveTowardsPlayer();

        if (this.animTimer >= 1) this.getCollected();
    }

    moveTowardsPlayer() {
        if (!this.isCollected) return;

        this.velocity.y = Smooth(this.velocity.y, 0, 8);
        this.rotSpeed = Smooth(this.rotSpeed, 0, 8);
        this.pos.x = Smooth(this.pos.x, game.player.pos.x, 12);
        this.pos.y = Smooth(this.pos.y, game.player.pos.y, 12);
    }

    getCollected() {
        this.shouldBeRemoved = true;

        spawnParticles(game.player.pos.x, game.player.pos.y, 10, { img: this.img });

        const x = game.player.pos.x;
        const y = game.player.pos.y - game.player.size * 0.75;

        spawnFloatingText("+1", x, y);
        game.addScore(1)
        playSound(game.sounds.collect);
        game.player.pulse();
    }
}

At this point, the player’s pulse() function has not been defined, so the game will crash when there’s a collision. Before adding this function, take a closer look at how the animation code works.

After the isCollected value is set to true, things start to happen.

The handleAnimation() function does the following:

  • Advances the animTimer property by game.delta() * 4.

    Using game.delta() * 4 means that the animTimer is incremented by 1 every 0.25 seconds. The higher the multiplier, the faster the timer is incremented.

    Note
    Here’s a more detailed explanation of how the timer works. Multiplying the delta by a number increments animTimer faster according to the multiplier. So, game.delta() * 2 increases animTimer by 1 in half a second, game.delta() * 4 increases it by 1 in a quarter of a second, and so on. Internally, delta() calls 1 / game.frameRate(), which gives us the time passed since the last frame was rendered. So, if you’re running at 60 frames per second, 60 * (1 / frameRate()) = 1.
  • Uses the animTimer value to apply some EasingFunctions that shrink the scale from 1 to 0.05.

    Going all the way down to 0 might create some minor glitches. Instead, use a tiny value, which doesn’t make any difference visually.

At the same time, the moveTowardsPlayer() function does several things at once.

  • this.velocity.y = Smooth(this.velocity.y, 0, 8) – Gradually decreases the existing vertical velocity.

  • this.rotSpeed = Smooth(this.rotSpeed, 0, 8) – Starts spinning wildly.

  • this.pos.x = Smooth(this.pos.x, game.player.pos.x, 12) and this.pos.y = Smooth(this.pos.y, game.player.pos.y, 12) – Quickly moves toward the player location.

After animTimer reaches 1 (in about 0.25 seconds, since you’re multiplying the delta by 4), the easing animation will be over, and that’s when the actual collecting happens with getCollected().

The getCollected() function does the following:

  • Sets the shouldBeRemoved flag to true.

    This game template already has code that handles removal of entities when the shouldBeRemoved flag is set, so that’s all you need to do.

  • Spawns 10 particles at the player’s position and uses the same image as the Collectible.

  • Spawns a +1 floating text a little above the player.

  • Adds 1 to the game score.

  • Plays the collect sound.

  • Calls game.player.pulse(), which resets the player’s pulse animation.

To set up the pulse animation, make the following changes to frontend/src/Components/Game/Entities/Player.js.

// Don't forget to import `Ease` and `EasingFunctions`.
import { game } from '..'
import Entity from './Entity'
import { Smooth, Ease, EasingFunctions } from '../Utils/EasingFunctions'
import { CollisionCircle } from '../Utils/Collision'

export default class Player extends Entity {
    constructor(x, y, options){
        super(x, y, options);
        ...

        this.animTimer = 0;
    }

    update(){
        ...
        this.handleAnimation();
    }

    handleAnimation() {
        if (this.animTimer > 1) return;

        this.animTimer += game.delta();

        const intensity = 0.3;
        this.scale.x = Ease(EasingFunctions.easeOutElastic, this.animTimer, 1 + intensity, -intensity);
        this.scale.y = Ease(EasingFunctions.easeOutElastic, this.animTimer, 1 - intensity, +intensity);
    }

    pulse() {
        this.animTimer = 0;
    }
}
Note
Remember when you assigned the game.player property to the gameInstance? You make use of it here. Another way to find the player object would be to set the "player" tag inside of Player, then use something like const player = game.findByTag('player')[0];.

As you can see, this code is similar to the animation setup in Collectible.

You increment the animTimer property as long as it’s below 1, because the EasingFunctions only work for values between 0 and 1. Then, you modify the scale again. In this case, you’re using the easeOutElastic function, which generates a nice bouncy effect.

The pulse() function just resets the animTimer to 0, which restarts the animation.

Animations when collectibles are caught

Now it’s looking better!

Checking for missed collectibles

You need to add a way to lose the game, too! If a collectible falls to the bottom without getting caught, you can take away a life from the player. When the player loses all its lives, the game ends.

To implement this logic for losing the game, you first have to check if any of the collectibles went past the player and off the screen.

Open frontend/src/Components/Game/Entities/Collectible.js and add the following code.

import { game } from '..'
...

export default class Collectible extends Entity {
    ...

    update(){
        ...
        this.checkIfMissed();
    }

    checkIfMissed() {
        if (game.gameOver) return;

        const isBelowScreen = this.pos.y > game.height + this.size / 2;
        if (isBelowScreen) this.onMiss();
    }

    onMiss() {
        game.loseLife();
        playSound(game.sounds.loselife);
        game.camera.shake(0.25, 12);
        this.shouldBeRemoved = true;
    }
}

This code checks the Collectible’s pos.y coordinate. If it’s higher than the lower edge of the screen, it triggers the onMiss() function.

The onMiss() funtion does the following:

  • Triggers a game.loseLife function.

    The template automatically ends the game when there are no lives left.

  • Plays a loselife sound.

  • Shakes the camera a bit to amplify the negative effect.

  • Sets the shouldBeRemoved flag to true, so that the object will be deleted from memory in the next frame.

    Important
    Deleting unused objects from memory is an especially important step in every game to prevent memory leaks, which can result in a performance slowdown and, eventually, a crash.
Animations when collectibles are missed

Wrapping up

Your game is now playable!

In this section, you enabled the player to catch objects, which increases the game score. You also removed a life when the player misses an object, which eventually leads to losing the game.

In the next section, you’ll add some difficulty management, so that the game gets progressively harder as it’s played.