Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about Vampire Survivors game, Game development, HTML5, Javascript, Phaser and TypeScript.

Here we go with the second step of “Vampire Survivors” HTML5 prototype built with Phaser, the framework which has been used to build the original game before the switch to Unity.

In first step I showed you the basics of the game: firing to the closest enemy while all enemies continuously chase you.

Now it’s time to make dead enemies drop coins to be collected by the player.

Just like in the original game, it’s not necessary to walk over a coin to pick it up, because there is a kind of invisible magnet that attracts coins near the player.

This can be easily achieved with this.physics.overlapCirc method.

Let’s have a look at the game:

Move with WASD keys, player fires automatically to closest enemy.

Collect coins and avoid enemy contact or it’s game over.

Now, look at the completely commented source code, which consists in one HTML file, one CSS file and four TypeScript 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, maximum-scale=1">
        <link rel="stylesheet" href="style.css">
        <script src="main.js"></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;
}

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.

TypeScript
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay

export const GameOptions : any = {

    gameSize : {
        width               : 800,      // width of the game, in pixels
        height              : 800       // height of the game, in pixels
    },
    gameBackgroundColor     : 0x222222, // game background color

    playerSpeed             : 100,      // player speed, in pixels per second
    enemySpeed              : 50,       // enemy speed, in pixels per second
    bulletSpeed             : 200,      // bullet speed, in pixels per second
    bulletRate              : 1000,     // bullet rate, in milliseconds per bullet
    enemyRate               : 800,      // enemy rate, in milliseconds per enemy
    magnetRadius            : 100       // radius of the circle within which the coins are being attracted
    
}

main.ts

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

TypeScript
// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';                            // Phaser
import { PreloadAssets } from './scenes/preloadAssets'; // preloadAssets scene
import { PlayGame } from './scenes/playGame';           // playGame scene
import { GameOptions } from './gameOptions';            // game options

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode        : Phaser.Scale.FIT,                     // adjust size to automatically fit in the window
    autoCenter  : Phaser.Scale.CENTER_BOTH,             // center the game horizontally and vertically
    parent      : 'thegame',                            // DOM id where to render the game
    width       : GameOptions.gameSize.width,           // game width, in pixels
    height      : GameOptions.gameSize.height           // game height, in pixels
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = { 
    type            : Phaser.WEBGL,                     // game renderer
    backgroundColor : GameOptions.gameBackgroundColor,  // game background color
    scale           : scaleObject,                      // scale settings
    scene           : [                                 // array with game scenes
        PreloadAssets,                                  // PreloadAssets scene
        PlayGame                                        // PlayGame scene
    ],
    physics : {                                                                             
        default : 'arcade'                              // physics engine used is arcade physics
    }
}

// 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 {
 
        // load images
        this.load.image('enemy', 'assets/sprites/enemy.png');   // the enemy
        this.load.image('player', 'assets/sprites/player.png'); // the player
        this.load.image('bullet', 'assets/sprites/bullet.png'); // the bullet 
        this.load.image('coin', 'assets/sprites/coin.png');    // the bullet 
    }
  
    // 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

// modules to import
import { GameOptions } from '../gameOptions';   // game options   

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

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

    controlKeys     : any;                                                  // keys used to move the player
    player          : Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;    // the player
    enemyGroup      : Phaser.Physics.Arcade.Group;                          // group with all enemies
    coinGroup       : Phaser.Physics.Arcade.Group;                          // group with all coins

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

        // add player, enemies group, coins group and bullets group
        this.player = this.physics.add.sprite(GameOptions.gameSize.width / 2, GameOptions.gameSize.height / 2, 'player');
        this.enemyGroup = this.physics.add.group();
        this.coinGroup = this.physics.add.group();
        const bulletGroup : Phaser.Physics.Arcade.Group = this.physics.add.group();

        // set keyboard controls
        const keyboard : Phaser.Input.Keyboard.KeyboardPlugin = this.input.keyboard as Phaser.Input.Keyboard.KeyboardPlugin; 
        this.controlKeys = keyboard.addKeys({
            'up'    : Phaser.Input.Keyboard.KeyCodes.W,
            'left'  : Phaser.Input.Keyboard.KeyCodes.A,
            'down'  : Phaser.Input.Keyboard.KeyCodes.S,
            'right' : Phaser.Input.Keyboard.KeyCodes.D
        });
        
        // set outer rectangle and inner rectangle; enemy spawn area is between these rectangles
        const outerRectangle : Phaser.Geom.Rectangle = new Phaser.Geom.Rectangle(-100, -100, GameOptions.gameSize.width + 200, GameOptions.gameSize.height + 200);
        const innerRectangle : Phaser.Geom.Rectangle = new Phaser.Geom.Rectangle(-50, -50, GameOptions.gameSize.width + 100, GameOptions.gameSize.height + 100);

        // timer event to add enemies
        this.time.addEvent({
            delay       : GameOptions.enemyRate,
            loop        : true,
            callback    : () => {
                const spawnPoint : Phaser.Geom.Point = Phaser.Geom.Rectangle.RandomOutside(outerRectangle, innerRectangle);
                const enemy : Phaser.Types.Physics.Arcade.SpriteWithDynamicBody = this.physics.add.sprite(spawnPoint.x, spawnPoint.y, 'enemy'); 
                this.enemyGroup.add(enemy); 
            },
        });

        // timer event to fire bullets
        this.time.addEvent({
            delay       : GameOptions.bulletRate,
            loop        : true,
            callback    : () => {
                const closestEnemy : any = this.physics.closest(this.player, this.enemyGroup.getMatching('visible', true));
                if (closestEnemy != null) {
                    const bullet : Phaser.Types.Physics.Arcade.SpriteWithDynamicBody = this.physics.add.sprite(this.player.x, this.player.y, 'bullet'); 
                    bulletGroup.add(bullet); 
                    this.physics.moveToObject(bullet, closestEnemy, GameOptions.bulletSpeed);
                }
            },
        });

        // bullet Vs enemy collision
        this.physics.add.collider(bulletGroup, this.enemyGroup, (bullet : any, enemy : any) => {
            const coin : Phaser.Types.Physics.Arcade.SpriteWithDynamicBody = this.physics.add.sprite(enemy.x, enemy.y, 'coin'); 
            this.coinGroup.add(coin); 
            bulletGroup.killAndHide(bullet);
            bullet.body.checkCollision.none = true;
            this.enemyGroup.killAndHide(enemy);
            enemy.body.checkCollision.none = true;
        });

        // player Vs enemy collision
        this.physics.add.collider(this.player, this.enemyGroup, () => {
            this.scene.restart();
        });  

        // player Vs coin collision
        this.physics.add.collider(this.player, this.coinGroup, (player : any, coin : any) => {
            this.coinGroup.killAndHide(coin);
            coin.body.checkCollision.none = true;
        });  
    }

    // metod to be called at each frame
    update() {   
        
        // set movement direction according to keys pressed
        let movementDirection : Phaser.Math.Vector2 = new Phaser.Math.Vector2(0, 0);  
        if (this.controlKeys.right.isDown) {
            movementDirection.x ++;  
        }
        if (this.controlKeys.left.isDown) {
            movementDirection.x --;
        }
        if (this.controlKeys.up.isDown) {
            movementDirection.y --;    
        }
        if (this.controlKeys.down.isDown) {
            movementDirection.y ++;    
        }
        
        // set player velocity according to movement direction
        this.player.setVelocity(0, 0);
        if (movementDirection.x == 0 || movementDirection.y == 0) {
            this.player.setVelocity(movementDirection.x * GameOptions.playerSpeed, movementDirection.y * GameOptions.playerSpeed);
        }
        else {
            this.player.setVelocity(movementDirection.x * GameOptions.playerSpeed / Math.sqrt(2), movementDirection.y * GameOptions.playerSpeed / Math.sqrt(2));    
        } 

        // get coins inside magnet radius and move them towards player
        const coinsInCircle : Phaser.Physics.Arcade.Body[] = this.physics.overlapCirc(this.player.x, this.player.y, GameOptions.magnetRadius, true, true) as Phaser.Physics.Arcade.Body[];
        coinsInCircle.forEach((body : any) => {
            const bodySprite : Phaser.Physics.Arcade.Sprite = body.gameObject;
            if (bodySprite.texture.key == 'coin') {
                this.physics.moveToObject(bodySprite, this.player, 500);
            }
        })

        // move enemies towards player
        this.enemyGroup.getMatching('visible', true).forEach((enemy : any) => {
            this.physics.moveToObject(enemy, this.player, GameOptions.enemySpeed);
        });
    }
}

Now you can collect coins. Next time I will add scrolling, meanwhile download the commented source code along with the entire wepack project.

Don’t know where to start developing with Phaser and TypeScript? I’ll explain it to you step by step in this free minibook.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.