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.
<!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.
/* 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.
// 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.
// 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.
// 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.
// 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.