MASSLESS LTD.

Phaser with TypeScript and Howler.js - a simple starting point

September 29, 2024

Phaser platformer

Phaser is a game engine for the browser, which allows you to create games using JavaScript or TypeScript. Howler.js is a library for handling sound in the browser. This post will show you how to get started with Phaser and Howler.js using TypeScript.

The code can be found here: https://github.com/campbellgoe/phaser-game

The Howler.js documentation is a useful resource for learning how to work with sound in your game.

Also read the Phaser documentation to learn game development in the browser.

scaffold the project with vite using typescript

npm create vite@latest phaser-game -- --template vanilla-ts

change directory into your phaser-game project

cd phaser-game

open vscode or your preferred editor

code .

install phaser and howler from the npm registry, including the types for howler

npm install phaser howler @types/howler

Define your vite config (vite.config.ts) in the root next to where package.json is:

import { defineConfig } from 'vite';
// This will open the app in the browser when you run `npm run dev`
export default defineConfig({
  server: { open: true }
});

Define your index.html and place this in the root directory (vite will pick this up) too:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<title>Phaser TypeScript Game</title>
</head>
	<body>
	<script type="module" src="/src/main.ts"></script>
	</body>
</html>

Now the starting point for the TypeScript code of a 2D platformer using Phaser (game engine module) and Howler.js (sound handling module) for our game, main.ts in the ./src folder (./src/main.ts):

import Phaser from 'phaser';
import { Howl } from 'howler';

class SimpleGame extends Phaser.Scene {
    private box!: Phaser.GameObjects.Rectangle;
    private floors!: Phaser.GameObjects.Rectangle[];
    private cursors?: Phaser.Types.Input.Keyboard.CursorKeys;
    private moveSound!: Howl;
    private soundPlaying: boolean = false;
    private isJumping: boolean = false;
    private moveDirection: 'left' | 'right' | null = null; // To track the current movement direction

    constructor() {
        super('simple-game');
    }

    preload(): void {
        // Load your walking sound here if needed
    }

    create(): void {
        // Create a box (rectangle)
        this.box = this.add.rectangle(400, 300, 50, 50, 0xffff00);
        this.physics.add.existing(this.box);
        // (this.box.body as Phaser.Physics.Arcade.Body).setCollideWorldBounds(true);

        // Enable gravity
        (this.box.body as Phaser.Physics.Arcade.Body).setGravityY(300);

        // Create the floor (static object)
        this.floors = Array.from({ length: 4 }).map((_, i) =>
            this.add.rectangle(400 * i + 100 * i, 580, 400, 40, 0x00ff00)
        );
        this.floors.forEach(floor => {
            // 'true' makes it static
            this.physics.add.existing(floor, true);
            // Add collision between the box and the floor
            this.physics.add.collider(this.box, floor);
        });

        // Capture keyboard arrow keys
        this.cursors = this.input.keyboard?.createCursorKeys();

        // Initialize Howler sound
        this.moveSound = new Howl({
            loop: true,
            volume: 1,
            src: ['walk.wav'], // Add your move sound file here
        });

        // Make the camera follow the box
        this.cameras.main.startFollow(this.box, true, 0.05, 0.05);  // Slight easing on follow
        this.cameras.main.setBounds(0, 0, 1600, 600);  // Set camera bounds
    }

    update(): void {
        const boxBody = this.box.body as Phaser.Physics.Arcade.Body;
        let isWalking = false;

        // Respawn if the box falls off the bottom of the screen
        if (boxBody.y > this.cameras.main.height) {
            boxBody.reset(400, 300); // Reset position
            boxBody.setVelocity(0, 0); // Reset velocity to stop any momentum
        }

        // Reset horizontal velocity
        boxBody.setVelocityX(0);

        // Direction logic to prioritize first key pressed and prevent changing directions while holding both keys
        if (this.cursors?.left?.isDown && this.cursors?.right?.isDown) {
            // If both keys are pressed, keep moving in the current direction
            if (this.moveDirection === 'left') {
                boxBody.setVelocityX(-160); // Continue moving left
                isWalking = true;
            } else if (this.moveDirection === 'right') {
                boxBody.setVelocityX(160); // Continue moving right
                isWalking = true;
            }
        } else if (this.cursors?.left?.isDown) {
            // Move left and set the movement direction
            this.moveDirection = 'left';
            boxBody.setVelocityX(-160);
            isWalking = true;
        } else if (this.cursors?.right?.isDown) {
            // Move right and set the movement direction
            this.moveDirection = 'right';
            boxBody.setVelocityX(160);
            isWalking = true;
        } else {
            // Reset direction when no key is pressed
            this.moveDirection = null;
        }

        // Jumping - Allow jumping only if the character is touching the floor
        if (this.cursors?.up?.isDown && boxBody.blocked.down && !this.isJumping) {
            // Jump with upward velocity
            boxBody.setVelocityY(-330);
            // Prevent multiple jumps while in the air
            this.isJumping = true;
        }

        // Reset jump state when the box lands back on the floor
        if (boxBody.blocked.down) {
            this.isJumping = false;
        }

        // Handle walking sound state
        if (isWalking && !this.isJumping) {
            if (!this.moveSound.playing() && !this.soundPlaying) {
                this.moveSound.fade(this.moveSound.volume() || 0, 1, 333);
                this.moveSound.play();
                this.soundPlaying = true;
            }
        } else {
            if (this.soundPlaying) {
                this.moveSound.fade(this.moveSound.volume(), 0, 333);
                this.soundPlaying = false;
                this.moveSound.once('fade', () => {
                    this.moveSound.stop();
                });
            }
        }
    }
}

const config: Phaser.Types.Core.GameConfig = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#3498db',
    scale: {
        mode: Phaser.Scale.RESIZE,
        autoCenter: Phaser.Scale.CENTER_BOTH
    },
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { x: 0, y: 300 }, // Global gravity
            debug: false // You can enable this for debugging
        }
    },
    scene: SimpleGame
};

new Phaser.Game(config);

Note you could specify matter for rigid 2d bodies for physics but arcade will suffice for now.

Gravity is defined in this arcade physics mode as x and y number values. For y, positive values give gravity downward, negative values upward.

Settings the scale property of the Phaser config to RESIZE and CENTER_BOTH is a good idea for responsive design, meaning it should scale to the correct resolution for the device.

You can define scenes as classes which extend from Phaser.Scene

class SimpleGame extends Phaser.Scene {
...

The create method is the initialisation function which is where you set your scene up.

The update method is called on every frame, where you can specify input handling and game logic to run in the main game animation loop.

You may want to update the css to remove the border margin and padding, basically a reset.css could suffice.

And there's a starting point for a 2d platformer. You can add more features to this game, such as more levels, enemies, and collectibles. You can also add more sound effects and music using Howler.js.

Conclusion

This post has shown you how to get started with Phaser and Howler.js using TypeScript. You can now create your own games in the browser using these powerful tools. Have fun creating your games!

References