Simulate a keyboard input delay with Phaser (why on earth? You’ll see)

Talking about Input Delay game, HTML5, Javascript, Phaser and TypeScript.

I was messing around with Phaser as usual when an idea popped into my head: why not find a system to add delay to user input?

I know it may seem strange in a world where we struggle to minimize lag, but I have an idea for my next game that will take advantage of it.

I started with this official example and simulated a buffer to add a 2.5s delay to all user input, and this is the result:

Focus on the canvas and try to press P, H, A, S, E or R keys. Nothing happens, apparently, but all your input will be processed with 2.5 seconds delay, highlighting the proper letters.

Look at the commented source code which consists in one HTML file, one CSS file and threeTypeScript files.

index.html

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

HTML
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title></title>
    <script type="module" src="/src/main.ts"></script>
  </head>
  <body>
    <div id="thegame"></div>
  </body>
</html>

style.css

The cascading style sheets of the main web page.

CSS
/* remove margin and padding from all elements */
* {
    padding : 0;
    margin : 0;
}

/* set body background color */
body {
    background-color : #000000;    
}

/* Disable browser handling of all panning and zooming gestures. */
canvas {
    touch-action : none;
}

main.ts

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

TypeScript
import 'phaser';                                            // Phaser    
import { PreloadAssets }    from './scenes/preloadAssets';  // PreloadAssets scene
import { PlayGame }         from './scenes/playGame';       // PlayGame scene
import './style.css';                                       // main page stylesheet

// game configuration object
let configObject : Phaser.Types.Core.GameConfig = {
    scale : {
        mode        : Phaser.Scale.FIT,         // set game size to fit the entire screen
        autoCenter  : Phaser.Scale.CENTER_BOTH, // center the canvas both horizontally and vertically in the parent div
        parent      : 'thegame',                // parent div element
        width       : 1024,                     // game width, in pixels
        height      : 400,                      // game height, in pixels
    },
    backgroundColor : 0xefefef,                 // game background color
    scene           : [  
        PreloadAssets,                          // scene to preload all game assets
        PlayGame                                // the game itself
    ]
};
 
new Phaser.Game(configObject);

scenes > preloadAssets.ts

Here we preload all assets to be used in the game.

TypeScript
// CLASS TO PRELOAD ASSETS

// PreloadAssets class extends Phaser.Scene class
export class PreloadAssets extends Phaser.Scene {
  
    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }
  
    // method to be called during class preloading
    preload() : void {
        this.load.image('P', 'assets/sprites/p.png');
        this.load.image('H', 'assets/sprites/h.png');
        this.load.image('A', 'assets/sprites/a.png');
        this.load.image('S', 'assets/sprites/s.png');
        this.load.image('E', 'assets/sprites/e.png');
        this.load.image('R', 'assets/sprites/r.png');
    }
  
    // method to be executed when the scene is created
    create() : void {

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

scenes > playGame.ts

Main game file, all game logic is stored here.

TypeScript
// THE GAME ITSELF

// PlayGame class extends Phaser.Scene class
export class PlayGame extends Phaser.Scene {
   
    constructor() {
        super('PlayGame');
    }

    keysPressed : string[]; // array to store keys currently being pressed
    buffer      : any[];    // array to buffer user input
    delay       : number;   // input delay, in milliseconds

    // PHASER letters
    imageP      : Phaser.GameObjects.Sprite;
    imageH      : Phaser.GameObjects.Sprite;
    imageA      : Phaser.GameObjects.Sprite;
    imageS      : Phaser.GameObjects.Sprite;
    imageE      : Phaser.GameObjects.Sprite;
    imageR      : Phaser.GameObjects.Sprite;
    
    // method to be executed when the scene is created
    create() : void {

        // set input delay to 2.5 seconds
        this.delay = 2500;

        // initialize arrays
        this.keysPressed = [];
        this.buffer = [];
        
        // place images on the stage
        this.imageP = this.add.sprite(50, 100, 'P').setOrigin(0);
        this.imageH = this.add.sprite(200, 100, 'H').setOrigin(0);
        this.imageA = this.add.sprite(350, 100, 'A').setOrigin(0);
        this.imageS = this.add.sprite(500, 100, 'S').setOrigin(0);
        this.imageE = this.add.sprite(650, 100, 'E').setOrigin(0);
        this.imageR = this.add.sprite(800, 100, 'R').setOrigin(0);

        // keys allowed: only the letters to form 'PHASER'
        const allowedKeys : string[] = ['KeyP', 'KeyH', 'KeyA', 'KeyS', 'KeyE', 'KeyR']

        // initialize keyboard plugin
        const keyboard : Phaser.Input.Keyboard.KeyboardPlugin = this.input.keyboard as Phaser.Input.Keyboard.KeyboardPlugin; 

        // event to be triggered when a key is pressed
        keyboard.on('keydown', (event : any) => {

            // is this an allowed key and still missing in keyPressed array?
            if (allowedKeys.indexOf(event.code) != -1 && this.keysPressed.indexOf(event.code) == -1) {
                
                // insert key code in keysPressed array
                this.keysPressed.push(event.code);

                // insert the event in the bugger
                this.buffer.push({
                    code    : event.code,       // which key?
                    start   : event.timeStamp,  // when was the key pressed?
                    end     : Infinity          // when was the key released (not yet, at the moment)
                });
            }
        });

        // event to be triggered when a key is released
        keyboard.on('keyup', (event : any) => {

            // is this an allowed key?
            if (allowedKeys.indexOf(event.code) != -1) {

                // remove the key from keysPressed array 
                this.keysPressed = this.keysPressed.filter(element => element !== event.code);

                // find the index of the element with the same key code and Inifinity as end time
                const index : number = this.buffer.findIndex(element => (element.code == event.code && element.end == Infinity));
                
                // did we find it (we should!)
                if (index > -1) {

                    // update end time with current timestamp
                    this.buffer[index].end = event.timeStamp;
                }
            }
        })
    }

    // method to be executed at each frame
    // time :time passed since the start of the scene, in milliseconds
    update(time: number): void {

        // array with delayed keys pressed
        const delayedKeysPressed : string[] = [];

        // looping through all buffer
        this.buffer.forEach(element => {

            // if start time, once added the delay, is less than curren time...
            if (element.start + this.delay < time) {

                // place the key in delayedKeysPressed array
                delayedKeysPressed.push((element.code));
            }
        })

        // remove from buffer all elements whose end timestamp, once added the delay, is greater tan current time
        this.buffer = this.buffer.filter(element => element.end + this.delay > time);

        // highlight PHASER letters according to delayed keys pressed
        this.imageP.setAlpha((delayedKeysPressed.indexOf('KeyP') > -1) ? 1 : 0.2);
        this.imageH.setAlpha((delayedKeysPressed.indexOf('KeyH') > -1) ? 1 : 0.2);
        this.imageA.setAlpha((delayedKeysPressed.indexOf('KeyA') > -1) ? 1 : 0.2);
        this.imageS.setAlpha((delayedKeysPressed.indexOf('KeyS') > -1) ? 1 : 0.2);
        this.imageE.setAlpha((delayedKeysPressed.indexOf('KeyE') > -1) ? 1 : 0.2);
        this.imageR.setAlpha((delayedKeysPressed.indexOf('KeyR') > -1) ? 1 : 0.2);
    }
}

How would you use an input delay to your games? Download the full source code along with the Vite project. Don’t know what I am talking about? There’s a free minibook to get you started.