How to handle a consuming energy bar, or measure the time a player is pressing a button, in your Phaser powered HTML5 games using TypeScript

Last week I published a “Block it” prototype with walls activable when the player touches the canvas, and an energy system which made energy drain as long as the player touches the canvas.

This post covers in detail the process of handling a consuming energy bar, or measuring the time a player is pressing a button.

This process is divided in 3 steps:

1 – The player start touching the canvas, and if the amount of energy is greater than zero, we have to save the timestamp of this event.

2 – The player keeps touching the canvas. At this point, each frame we determine the difference between current timestamp and the timestamp measured at step 1. This amount of time should be smaller than the time required to consume all energy, which as this time shouldn’t be touched. It’s enough for us to check if the amount of time is smaller than the time required to consume all energy.

2a – If the amount of time passed is smaller than the time to consume all energy, everything is ok and we still have energy.

2b – If the amount of time passed is greater than – or equal to – the time required to consume all energy, then we ran out of energy.

3 – The player stops touching the canvas. Now we determine the difference between current timestamp and the timestamp measured at step 1. At this time we update the amount of energy, and we wait for the player to start touching again the canvas, at step 1.

Look at a working prototype:

Press and hold the canvas to drain energy, stop pressing to stop draining. Once you do not have energy, press anywhere to restart the example.

In your console you will see the log of what’s happening.

Now look at the completely commented source code, which consists in one html file, one css file and 4 TypeScript files:

index.html

The web page which hosts the game, to be run inside thegame element.

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="initial-scale=1, maximum-scale=1">
        <link rel="stylesheet" href="style.css">
        </style>
        <script src="main.js"></script>
    </head>
    <body>
        <div id="thegame"></div>
    </body>
</html>

style.css

The cascading style sheets of the main web page.

* {
    padding : 0;
    margin : 0;
}

canvas {
    touch-action : none;
    -ms-touch-action : none;
}

gameOptions.ts

Configurable game options. It’s a good practice to place all configurable game options, if possible, in a single and separate file, for a quick tuning of the game.

// CONFIGURABLE GAME OPTIONS

export const GameOptions = {

    // starting energy
    startingEnergy : 5000,

    // energy draining per second
    energyPerSecond : 1000,

    // energy bar size
    energyBarSize: {
        width : 500,
        height : 50
    }
}

main.ts

This is where the game is created, with all Phaser related options.

// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame } from './playGame';

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    autoCenter : Phaser.Scale.CENTER_BOTH,
    parent : 'thegame',
    width : 600,
    height : 300
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
    type : Phaser.AUTO,
    backgroundColor : 0x444444,
    scale : scaleObject,
    scene : [PreloadAssets, PlayGame]
}

// the game itself
new Phaser.Game(configObject);

preloadAssets.ts

Here we preload all assets to be used in the game, in this case the energy bar.

// CLASS TO PRELOAD ASSETS

// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {

    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }

    // method to be execute during class preloading
    preload(): void {

        // this is how we preload an image
        this.load.image('bar', 'assets/bar.png');
	}

    // method to be called once the instance has been created
	create(): void {

        // call PlayGame class
        this.scene.start('PlayGame');
	}
}

playGame.ts

Main game file, all game logic is stored here.

// THE GAME ITSELF

import { GameOptions } from "./gameOptions";

// this class extends Scene class
export class PlayGame extends Phaser.Scene {

    // variable to save the time the player touched the input to activate the wall
    downTime : number;

    // walls energy
    energy : number;

    // text object to display the energy
    energyText : Phaser.GameObjects.Text;

    // is the energy draining?
    energyDraining : boolean;

    // tile sprite to represent the energy bar
    energyBar : Phaser.GameObjects.TileSprite;

    // constructor
    constructor() {
        super({
            key: 'PlayGame'
        });
    }

    // method to be executed when the scene has been created
    create() : void {

        // we aren't draining energy
        this.energyDraining = false;

        // fill the energy according to game options
        this.energy = GameOptions.startingEnergy;

        // just a couple of variables to store game width and height
        let gameWidth : number = this.game.config.width as number;
        let gameHeight : number = this.game.config.height as number;

        // another couple of variables to have the energy bar centered
        let barPositionX : number = (gameWidth - GameOptions.energyBarSize.width) / 2;
        let barPositionY : number = (gameHeight - GameOptions.energyBarSize.height) / 2;

        // energy backgroud is a fixed, semi trasparent energy bar just to make the prototype look better
        let energyBackground : Phaser.GameObjects.TileSprite = this.add.tileSprite(barPositionX, barPositionY, GameOptions.energyBarSize.width, GameOptions.energyBarSize.height, 'bar');
        
        // set energy background alpha to almost transparent
        energyBackground.setAlpha(0.2);

        // set energy background origin to left, top
        energyBackground.setOrigin(0);

        // actual energy bar
        this.energyBar = this.add.tileSprite(barPositionX, barPositionY, GameOptions.energyBarSize.width, GameOptions.energyBarSize.height, 'bar');
        
        // set energy background origin to left, top
        this.energyBar.setOrigin(0);

        // get energy bar bounds
        let energyBarBounds : Phaser.Geom.Rectangle = this.energyBar.getBounds();
        
        // add the text object to show the amount of energy
        this.energyText = this.add.text(energyBarBounds.right, energyBarBounds.bottom + 10, '', {
            font : '32px Arial'
        });

        // set energy text origin to right, top
        this.energyText.setOrigin(1, 0);

        // update energy text;
        this.updateEnergyText(this.energy);

        // wait for input start to call startDraining method
        this.input.on('pointerdown', this.startDraining, this);

        // wait for input end to call stopDraining method
        this.input.on('pointerup', this.stopDraining, this); 

        // log variables
        this.log('Scene started');
    }

    // method to update energy text
    updateEnergyText(energy : number) : void {

        // set energy text to energy value itself or zero
        this.energyText.setText(Math.max(0, energy).toString() + '/' + GameOptions.startingEnergy.toString());
    }

    // method to update energy bar
    updateEnergyBar(energy : number) : void {

        // set energy bar display width according to energy
        this.energyBar.displayWidth = GameOptions.energyBarSize.width / GameOptions.startingEnergy * energy;
    }

    // method to start draining
    startDraining(e : Phaser.Input.Pointer) : void {

        // do we have energy?
        if (this.energy > 0) {

            // save the current timestamp
            this.downTime = e.downTime;

            // now we are draining energy
            this.energyDraining = true;

            this.log('Start draining');
        }

        // we do not have energy
        else {

            // output some log
            this.log('About to restart scene');
            
            // just restart the scene
            this.scene.start('PlayGame');
        }
    }

    // method to stop draining
    stopDraining(e : Phaser.Input.Pointer) : void {

        // are we draining?
        if (this.energyDraining) {

            // we aren't draining anymore
            this.energyDraining = false;
       
            // determine how long the input has been pressed
            let ellapsedTime : number = e.upTime - e.downTime;
            
            // subtract the ellapsed time from energy
            this.energy -= Math.min(this.energy, Math.round(ellapsedTime / 1000 * GameOptions.energyPerSecond));

            // update energy text
            this.updateEnergyText(this.energy);

            // update energy bar
            this.updateEnergyBar(this.energy);

            // output some log
            this.log('Stop draining');
        }
    }

    // method to be called at each frame
    update(time : number) : void {

        // are we draining energy?
        if (this.energyDraining) {

            // determine remaining energy subtracting from current energy the amount of time we are pressing the input
            let remainingEnergy : number = this.energy -  Math.round((time - this.downTime) / 1000 * GameOptions.energyPerSecond);

            // if remaining energy is less than zero...
            if (remainingEnergy < 0) {

                // set energy to zero
                remainingEnergy = 0;  
            }

            // update energy text
            this.updateEnergyText(remainingEnergy);

            // update energy bar
            this.updateEnergyBar(remainingEnergy);

            // output some log
            this.log('Draining, remaining energy: ' + remainingEnergy.toString());
        }
    }

    // prompt some output to the console
    log(text : string) : void {
        console.log(text);
        console.log('Energy :', this.energy);
        console.log('Energy draining :', this.energyDraining);
        console.log('--------------------------');
    }
}

Now the concept about handling an energy bar should be simpler. Download the source code.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers