Talking about DROP'd game, Game development, HTML5, Javascript, Phaser and TypeScript.
The process of conversion from JavaScript to TypeScript continues. I already built Perfect Square! and a DROP’d prototype using TypeScript, but both source codes were uncommented.
Now, I commented line by line the DROP’d prototype, and I also added some new features, like multiple cameras, tweens and time events, and now following the source code you will learn, among other things, how to use:
* Arcade Physics simulation with gravity, velocity and collisions.
* Input listeners
* Tweens
* Time Events
* Sprites
* Groups
* Render Textures
* Particle explosions
* Multiple cameras
Look at the game we are going to build:
Tap to destroy your platform, and try to land on green platform. If you miss it, it’s game over.
If you don’t know how to configure your system to start conding and publishing with Phaser and TypeScript, I wrote a four steps guide about the migration from JavaScript to TypeScript, check steps 1, 2, 3 and 4.
To ensure maximum code reusability, the source code is split in 6 TypeScript file and one HTML file.
Let’s see them in detail:
index.html
The webpage which hosts the game, just the bare bones of HTML and main.ts
is called.
<!DOCTYPE html>
<html>
<head>
<style type = "text/css">
body {
background: #000000;
padding: 0px;
margin: 0px;
}
</style>
<script src = "scripts/main.ts"></script>
</head>
<body>
<div id = "thegame"></div>
</body>
</html>
main.ts
The main TypeScript file, the one called by index.html
.
Here we import most of the game libraries and define both Scale Manager object and Physics object.
Here we also initialize the game itself.
// MAIN GAME FILE
// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame} from './playGame';
import { GameOptions } from './gameOptions';
// object to initialize the Scale Manager
const scaleObject: Phaser.Types.Core.ScaleConfig = {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
parent: 'thegame',
width: 750,
height: 1334
}
// obhect to initialize Arcade physics
const physicsObject: Phaser.Types.Core.PhysicsConfig = {
default: 'arcade',
arcade: {
gravity: {
y: GameOptions.gameGravity
}
}
}
// game configuration object
const configObject: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
backgroundColor:0x87ceea,
scale: scaleObject,
scene: [PreloadAssets, PlayGame],
physics: physicsObject
}
// the game itself
new Phaser.Game(configObject);
preloadAssets.ts
Class to preload all assets used in the game.
// CLASS TO PRELOAD ASSETS
// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {
// constructor
constructor() {
super({
key: 'PreloadAssets'
});
}
// preloading assets, the good old way
preload(): void {
this.load.image('hero', 'assets/hero.png');
this.load.image('pattern', 'assets/pattern.png');
this.load.image('eyes', 'assets/eyes.png');
this.load.image('particle', 'assets/particle.png');
this.load.image('sky', 'assets/sky.png');
}
create(): void {
this.scene.start('PlayGame');
}
}
gameOptions.ts
Game options which can be changed to tune the gameplay are stored in a separate module, ready to be reused.
// CONFIGURABLE GAME OPTIONS
export const GameOptions = {
// first platform position, in height ratio. In this case, in the top 2/10 of the screen
firstPlatformPosition: 2 / 10,
// Arcade physics gravity
gameGravity: 1700,
// platform horizontal speed range, in pixels per second
platformHorizontalSpeedRange: [250, 400],
// platform width range, in pixels
platformWidthRange: [120, 300],
// platform vertical distance range, in pixels
platformVerticalDistanceRange: [150, 250],
// platform height, in pixels
platformHeight: 50
}
playGame.ts
The game itself, the biggest class, game logic is stored here.
// THE GAME ITSELF
// modules to import
import { GameOptions } from './gameOptions';
import PlayerSprite from './playerSprite';
import PlatformSprite from './platformSprite';
// this class extends Scene class
export class PlayGame extends Phaser.Scene {
// sprite with platform eyes
eyes: Phaser.GameObjects.Sprite;
// graphics where to draw platform borders
borderGraphics: Phaser.GameObjects.Graphics;
// tile sprite, used for platform background
spritePattern: Phaser.GameObjects.TileSprite;
// the hero!
hero: PlayerSprite;
// explosion emitter
emitter: Phaser.GameObjects.Particles.ParticleEmitter;
// particles to be emitted
particles: Phaser.GameObjects.Particles.ParticleEmitterManager;
// group which will contain all platforms
platformGroup: Phaser.Physics.Arcade.Group;
// here we store game width once for all
gameWidth: number;
// here we store game height once for all
gameHeight: number;
// camera following game action
actionCamera: Phaser.Cameras.Scene2D.Camera;
// background sky
sky: Phaser.GameObjects.Sprite;
// constructor
constructor() {
super({
key: 'PlayGame'
});
}
// method to be called once the class has been created
create(): void {
// save game width value
this.gameWidth = this.game.config.width as number;
// save game height value
this.gameHeight = this.game.config.height as number;
// method to add the background sky
this.addSky();
// add a new Graphics object...
this.borderGraphics = this.add.graphics();
// ... and set it to invisible
this.borderGraphics.setVisible(false);
// add a new tile sprite...
this.spritePattern = this.add.tileSprite(this.gameWidth / 2, GameOptions.platformHeight / 2,this.gameWidth, GameOptions.platformHeight * 2, 'pattern');
// ... and set it to invisible
this.spritePattern.setVisible(false);
// add a new sprite...
this.eyes = this.add.sprite(0, 0, 'eyes');
// ... and set it to invisible
this.eyes.setVisible(false);
// create a new physics group
this.platformGroup = this.physics.add.group();
// we are going to add 12 platforms, and reuse them when we need them
for (let i: number = 0; i < 12; i ++) {
// method to add a platform
this.addPlatform(i == 0);
}
// add the hero
this.hero = new PlayerSprite(this, this.gameWidth / 2, 0, 'hero');
// input listener
this.input.on('pointerdown', this.destroyPlatform, this);
// method to add the emitter
this.createEmitter();
// method to set the cameras
this.setCameras();
}
// method to add the sky
addSky(): void {
// add sky sprite
this.sky = this.add.sprite(0, 0, 'sky');
// adjust sprite size to cover the entire screen
this.sky.displayWidth = this.gameWidth;
this.sky.displayHeight = this.gameHeight;
// set sprite origin to top left corner
this.sky.setOrigin(0, 0);
}
// method to set the cameras
setCameras(): void {
// add a new camera
this.actionCamera = this.cameras.add(0, 0, this.gameWidth, this.gameHeight);
// action camera won't render the sky
this.actionCamera.ignore([this.sky]);
// make action camera follow the hero
this.actionCamera.startFollow(this.hero, true, 0, 0.5, 0, - (this.gameHeight / 2 - this.gameHeight * GameOptions.firstPlatformPosition));
// main camera won't render, particles, hero and platforms
this.cameras.main.ignore([this.particles]);
this.cameras.main.ignore([this.hero]);
this.cameras.main.ignore(this.platformGroup);
}
// method to crate the emitter
createEmitter(): void {
// create new particles
this.particles = this.add.particles('particle');
// create the emitter
this.emitter = this.particles.createEmitter({
// particles will scale from 1 (original size) to zero (too tiny to be seen)
scale: {
start: 1,
end: 0
},
// speed will scale from zero to 200 pixels per second
speed: {
min: 0,
max: 200
},
// emitter is not active at the moment
active: false,
// particle lifespan, in milliseconds
lifespan: 500,
// quantity of particles to be used
quantity: 50
});
}
// method to add a platform
addPlatform(isFirst: Boolean): void {
// creation of a new platform sprite
let platform: PlatformSprite = new PlatformSprite(this, this.gameWidth / 2, isFirst ? this.gameWidth * GameOptions.firstPlatformPosition : 0, this.gameWidth / 8, GameOptions.platformHeight);
// add platform to platformGroup group
this.platformGroup.add(platform);
// call platform setPhysics method
platform.setPhysics();
// call platform drawTexture method
platform.drawTexture(this.borderGraphics, this.spritePattern, this.eyes);
// is this the first platform?
if (isFirst) {
// paint it green
platform.setTint(0x00ff00);
}
else {
// initialize the platform
this.initPlatform(platform);
}
}
// method to destroy a platform
destroyPlatform(): void {
// can the hero destroy the platform and is the hero alive?
if (this.hero.canDestroyPlatform && !this.hero.isDying) {
// hero can't destroy the platform anymore
this.hero.canDestroyPlatform = false;
// get the platform body which is the closest to the hero
let closestPlatform: Phaser.Physics.Arcade.Body = this.physics.closest(this.hero) as Phaser.Physics.Arcade.Body;
// given the body, get the sprite
let platform: PlatformSprite = closestPlatform.gameObject as PlatformSprite;
// make platform explode
platform.explodeAndDestroy(this.emitter);
// initialize the platform again
this.initPlatform(platform);
}
}
// method to initialize the platform
initPlatform(platform: PlatformSprite): void {
// set assignedVelocity property a random value
platform.assignedVelocity = this.randomValue(GameOptions.platformHorizontalSpeedRange) * Phaser.Math.RND.sign();
// change platform size
platform.transformTo(this.gameWidth / 2, this.getLowestPlatform() + this.randomValue(GameOptions.platformVerticalDistanceRange), this.randomValue(GameOptions.platformWidthRange), GameOptions.platformHeight);
// draw a new texture on the platform
platform.drawTexture(this.borderGraphics, this.spritePattern, this.eyes);
}
// method to get the lowest platform position
getLowestPlatform(): number {
// set lowestplatform to zero
let lowestPlatform: number = 0;
// get all platforms
let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
// iterate through all platforms
for (let platform of platforms) {
// lowestplatform is the max value between itself and platform vertical position
lowestPlatform = Math.max(lowestPlatform, platform.y);
}
// return lowestPlatform value
return lowestPlatform;
}
// method to get the highest platform
getHighestPlatform(maxHeight: number): PlatformSprite {
// set highest platform y position to Infinity
let highestPlatformY = Infinity;
// get all platforms
let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
// set an arbitrary platform as highest platform
let highestPlatform: PlatformSprite = platforms[0];
// iterate through all platforms
for (let platform of platforms) {
// is platform y position is greater than maxHeight and less than highest platform y?
if (platform.y > maxHeight && platform.y < highestPlatformY) {
// ... highestPlatform is now the current platform
highestPlatform = platform;
// update highest platform y
highestPlatformY = platform.y;
}
}
// return highestPlatform
return highestPlatform;
}
// method to toss a random value between two elements in an array
randomValue(a: number[]): number {
return Phaser.Math.Between(a[0], a[1]);
}
// method to check for collision between two bodies
handleCollision(body1: Phaser.GameObjects.GameObject, body2: Phaser.GameObjects.GameObject): void {
// first body is the hero
let hero: PlayerSprite = body1 as PlayerSprite;
// second body is the platform
let platform: PlatformSprite = body2 as PlatformSprite;
// if the hero is not on the platform...
if (!platform.isHeroOnIt) {
// is the hero too close to left platform edges or landing on the left side of an illegal platform?
if (hero.x < platform.getBounds().left || (!platform.isTinted && hero.x <= platform.x)) {
// make hero fall down and die to the left
this.fallAndDie(-1);
// exit the function
return;
}
// is the hero too close to right platform edges or landing on the right side of an illegal platform?
if (hero.x > platform.getBounds().right || (!platform.isTinted && hero.x > platform.x)) {
// make hero fall down and die to the right
this.fallAndDie(1);
// exit the function
return;
}
// now this platform has the hero on it
platform.isHeroOnIt = true;
// method to paint safe platforms
this.paintSafePlatforms();
// hero can destroy platforms once more
this.hero.canDestroyPlatform = true;
}
}
// method to make the hero fall and die
fallAndDie(mult: number): void {
// call hero's die method
this.hero.die(mult);
// create a time event
this.time.addEvent({
// after 800 milliseconds...
delay: 800,
callbackScope: this,
callback: function() {
// make action camera to stop following the hero
this.actionCamera.stopFollow();
}
});
}
// method to paint safe platforms
paintSafePlatforms(): void {
// set floor platform as the highest platform
let floorPlatform: PlatformSprite = this.getHighestPlatform(0);
// tint floor platform red
floorPlatform.setTint(0xff0000);
// set target platform as the highest platform, but below floor platform
let targetPlatform: PlatformSprite = this.getHighestPlatform(floorPlatform.y);
// tint target platform green
targetPlatform.setTint(0x00ff00);
// player can land on this platform
targetPlatform.canLandOnIt = true;
}
// method to be called at each frame
update(): void {
// is hero alive?
if (!this.hero.isDying) {
// manage collisions between the hero and platforms
this.physics.world.collide(this.hero, this.platformGroup, this.handleCollision, undefined, this);
}
// get all platforms
let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
// iterate through all platforms
for (let platform of platforms) {
// if platform y position plus game height is less than hero y position...
if (platform.y + this.gameHeight < this.hero.y) {
// game over, let's start again
this.scene.start('PlayGame');
}
// check distance between the platform and game edges
let distance: number = Math.max(0.2, 1 - ((Math.abs(this.gameWidth / 2 - platform.x) / (this.gameWidth / 2)))) * Math.PI / 2;
// assign platform velocity
platform.body.setVelocityX(platform.assignedVelocity * distance);
// if a platform reached the left or right edge...
if ((platform.body.velocity.x < 0 && platform.getBounds().left < this.hero.displayWidth / 2) || (platform.body.velocity.x > 0 && platform.getBounds().right > this.gameWidth - this.hero.displayWidth / 2)) {
// ... invert its velocity
platform.assignedVelocity *= -1;
}
}
}
}
playerSprite.ts
Class to define the player Sprite.
// PLATFORM SPRITE CLASS
// player sprite extends Sprite class
export default class PlayerSprite extends Phaser.Physics.Arcade.Sprite {
// can the player destroy the platform?
canDestroyPlatform: Boolean = false;
// is the hero dying?
isDying: Boolean = false;
// scene which called this class
mainScene: Phaser.Scene;
// constructor
constructor(scene: Phaser.Scene, x: number, y: number, key: string) {
super(scene, x, y, key);
// add the player to the scnee
scene.add.existing(this);
// add physics body to platform
scene.physics.add.existing(this);
// save the scene which called this class
this.mainScene = scene;
}
// method to make the player die
die(mult: number): void {
// hero is dying
this.isDying = true;
// set vertical velocity to -200 pixels per second (going up)
this.setVelocityY(-200);
// set horizontal velocity to 200 or -200 pixels per second
this.setVelocityX(200 * mult);
// set player angle to 45 or -45 using a 0.5s tween
this.mainScene.tweens.add({
targets: this,
angle: 45 * mult,
duration: 500
});
}
}
platformSprite.ts
Class to define the platform Sprite, which is the main actor of the game. We don’t control the player, but we can contro platforms by destroying them.
Also, platforms are rendered using RenderTexture object.
// PLATFORM SPRITE CLASS
// modules to import
import { GameOptions } from './gameOptions';
// platform sprite extends RenderTexture class
export default class PlatformSprite extends Phaser.GameObjects.RenderTexture {
// does this platform have the player on it?
isHeroOnIt: Boolean = false;
// platform physics body
body: Phaser.Physics.Arcade.Body;
// platform assigned velocity
assignedVelocity: number = 0;
// can the player land on this platform?
canLandOnIt: Boolean = false;
// constructor
constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number) {
super(scene, x, y, width, height);
// set platform origin in horizontal and vertical middle
this.setOrigin(0.5);
// add existing platform to scene
scene.add.existing(this);
// add a physics body to platform
scene.physics.add.existing(this);
}
// method to set physics properties
setPhysics(): void {
// set the body immovable, it won't react to collisions
this.body.setImmovable(true);
// do not allow gravity to move the body
this.body.setAllowGravity(false);
// set body friction to max (1), or player sprite will slip away
this.body.setFrictionX(1);
}
// method to draw platform texture
drawTexture(border: Phaser.GameObjects.Graphics, pattern: Phaser.GameObjects.TileSprite, eyes: Phaser.GameObjects.Sprite): void {
// clear border graphics
border.clear();
// set border line style
border.lineStyle(8, 0x000000, 1);
// stroke a rectangle
border.strokeRect(0, 0, this.displayWidth, this.displayHeight);
// draw pattern texture on platform
this.draw(pattern, this.displayWidth / 2, Phaser.Math.Between(0, GameOptions.platformHeight));
// draw eyes texture on platform
this.draw(eyes, this.displayWidth / 2, this.displayHeight / 2);
// draw border graphics on platform
this.draw(border);
}
// method to resize the platform
transformTo(x: number, y: number, width: number, height: number): void {
// change platform x position
this.x = x;
// change platform y position
this.y = y;
// set new platform size
this.setSize(width, height);
// set new Arcade body size
this.body.setSize(width, height);
}
// method to explode and destroy the platform
explodeAndDestroy(emitter: Phaser.GameObjects.Particles.ParticleEmitter): void {
// get platform bounds
let platformBounds: Phaser.Geom.Rectangle = this.getBounds();
// place emitter in top left vertex of the platform
emitter.setPosition(platformBounds.left, platformBounds.top);
// set the emitter as active
emitter.active = true;
// set emit zone to cover the entire platform
emitter.setEmitZone({
source: new Phaser.Geom.Rectangle(0, 0, platformBounds.width, platformBounds.height),
type: 'random',
quantity: 50
});
// make emitter explode
emitter.explode(50, this.x - this.displayWidth / 2, this.y - this.displayHeight / 2);
// remove platform tint
this.clearTint();
// player is no longer on this platform
this.isHeroOnIt = false;
// player can't land on this platform
this.canLandOnIt = false;
}
}
And that’s it TypeScript is not hard to learn, and thanks to Visual Studio Code IDE, development could be even faster and easier.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.