Talking about DROP'd game, Game development, HTML5, Javascript, Phaser and TypeScript.
Here we go with another conversion from JavaScript to TypeScript, to learn coding the modern way.
Last week we saw Perfect Square! full game ported from JavaScript to TypeScript, now it’s time to see a prototype I started to code in January: DROP’d.
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.
The game features some nice tricks you can see explained in the tutorial series. The code which follows is the conversion from the latest JavaScript step.
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, we import most of the game and define both Scale Manager object and Physics object.
Here we also initialize the game itself.
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame} from './playGame';
import { GameOptions } from './gameOptions';
const scaleObject: Phaser.Types.Core.ScaleConfig = {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
parent: 'thegame',
width: 750,
height: 1334
}
const physicsObject: Phaser.Types.Core.PhysicsConfig = {
default: 'arcade',
arcade: {
gravity: {
y: GameOptions.gameGravity
}
}
}
const configObject: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
backgroundColor:0x87ceea,
scale: scaleObject,
scene: [PreloadAssets, PlayGame],
physics: physicsObject
}
new Phaser.Game(configObject);
preloadAssets.ts
Class to preload all assets
export class PreloadAssets extends Phaser.Scene {
constructor() {
super({
key: 'PreloadAssets'
});
}
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');
}
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.
export const GameOptions = {
firstPlatformPosition: 2 / 10,
gameGravity: 1700,
platformHorizontalSpeedRange: [250, 400],
platformLengthRange: [120, 300],
platformVerticalDistanceRange: [150, 250],
platformHeight: 50
}
playGame.ts
The game itself.
import { GameOptions } from './gameOptions';
import PlayerSprite from './playerSprite';
import PlatformSprite from './platformSprite';
export class PlayGame extends Phaser.Scene {
eyes: Phaser.GameObjects.Sprite;
hero: PlayerSprite;
canDestroy: Boolean;
emitter: Phaser.GameObjects.Particles.ParticleEmitter;
platformGroup: Phaser.Physics.Arcade.Group;
gameWidth: number;
gameHeight: number;
borderGraphics: Phaser.GameObjects.Graphics;
spritePattern: Phaser.GameObjects.TileSprite;
constructor() {
super({
key: 'PlayGame'
});
}
create(): void {
this.gameWidth = this.game.config.width as number;
this.gameHeight = this.game.config.height as number;
this.borderGraphics = this.add.graphics();
this.borderGraphics.setVisible(false);
this.spritePattern = this.add.tileSprite(this.gameWidth / 2, GameOptions.platformHeight / 2,this.gameWidth, GameOptions.platformHeight * 2, 'pattern')
this.spritePattern.setVisible(false);
this.eyes = this.add.sprite(0, 0, 'eyes');
this.eyes.setVisible(false);
this.platformGroup = this.physics.add.group();
for (let i: number = 0; i < 7; i ++) {
this.addPlatform(i == 0);
}
this.hero = new PlayerSprite(this, this.gameWidth / 2, 0, 'hero');
this.cameras.main.startFollow(this.hero, true, 0, 0.5, 0, - (this.gameHeight / 2 - this.gameHeight * GameOptions.firstPlatformPosition));
this.input.on('pointerdown', this.destroyPlatform, this);
this.createEmitter();
}
createEmitter(): void {
this.emitter = this.add.particles('particle').createEmitter({
scale: {
start: 1,
end: 0
},
speed: {
min: 0,
max: 200
},
active: false,
lifespan: 500,
quantity: 50
});
}
addPlatform(isFirst: Boolean): void {
let platform: PlatformSprite = new PlatformSprite(this, this.gameWidth / 2, isFirst ? this.gameWidth * GameOptions.firstPlatformPosition : 0, this.gameWidth / 8, GameOptions.platformHeight);
this.platformGroup.add(platform);
platform.setPhysics();
platform.drawTexture(this.borderGraphics, this.spritePattern, this.eyes);
if (!isFirst) {
this.initPlatform(platform);
}
else {
platform.setTint(0x00ff00);
}
}
destroyPlatform(): void {
if (this.canDestroy) {
this.canDestroy = false;
let closestPlatform: Phaser.Physics.Arcade.Body = this.physics.closest(this.hero) as Phaser.Physics.Arcade.Body;
let platform: PlatformSprite = closestPlatform.gameObject as PlatformSprite;
platform.explodeAndDestroy(this.emitter);
this.initPlatform(platform);
}
}
initPlatform(platform: PlatformSprite): void {
platform.assignedVelocity = this.randomValue(GameOptions.platformHorizontalSpeedRange) * Phaser.Math.RND.sign();
platform.transformTo(this.gameWidth / 2, this.getLowestPlatform() + this.randomValue(GameOptions.platformVerticalDistanceRange), this.randomValue(GameOptions.platformLengthRange), GameOptions.platformHeight);
platform.drawTexture(this.borderGraphics, this.spritePattern, this.eyes);
}
getLowestPlatform(): number {
let lowestPlatform: number = 0;
let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
for (let platform of platforms) {
let y: number = platform.y;
lowestPlatform = Math.max(lowestPlatform, y);
}
return lowestPlatform;
}
getHighestPlatform(maxHeight: number): PlatformSprite {
let highestPlatform: PlatformSprite = this.platformGroup.getFirst();
let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
for (let platform of platforms) {
if ((platform.y > maxHeight) && (!highestPlatform || platform.y < highestPlatform.y)) {
highestPlatform = platform;
}
}
return highestPlatform;
}
randomValue(a: number[]): number {
return Phaser.Math.Between(a[0], a[1]);
}
handleCollision(body1: Phaser.GameObjects.GameObject, body2: Phaser.GameObjects.GameObject): void {
let hero: PlayerSprite = body1 as PlayerSprite;
let platform: PlatformSprite = body2 as PlatformSprite;
if (!platform.isHeroOnIt) {
if (!platform.isTinted) {
this.scene.start('PlayGame')
}
if (hero.x < platform.getBounds().left) {
hero.setVelocityY(-200);
hero.setVelocityX(-200);
hero.angle = -45;
}
if (hero.x > platform.getBounds().right) {
hero.setVelocityY(-200);
hero.setVelocityX(200);
hero.angle = 45;
}
platform.isHeroOnIt = true;
this.paintSafePlatforms();
this.canDestroy = true;
}
}
paintSafePlatforms(): void {
let floorPlatform: PlatformSprite = this.getHighestPlatform(0);
floorPlatform.setTint(0xff0000);
let targetPlatform: PlatformSprite = this.getHighestPlatform(floorPlatform.y);
targetPlatform.setTint(0x00ff00);
}
update(): void {
if (this.hero.angle == 0) {
this.physics.world.collide(this.hero, this.platformGroup, this.handleCollision, undefined, this);
}
let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
for (let platform of platforms) {
if (platform.y + this.gameHeight < this.hero.y) {
this.scene.start('PlayGame');
}
let distance: number = Math.max(0.2, 1 - ((Math.abs(this.gameWidth / 2 - platform.x) / (this.gameWidth / 2)))) * Math.PI / 2;
platform.body.setVelocityX(platform.assignedVelocity * distance);
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)) {
platform.assignedVelocity *= -1;
}
}
}
}
playerSprite.ts
Class to define the player Sprite.
export default class PlayerSprite extends Phaser.Physics.Arcade.Sprite {
canDestroyPlatform: Boolean = false;
constructor(scene: Phaser.Scene, x: number, y: number, key: string) {
super(scene, x, y, key);
scene.add.existing(this);
scene.physics.add.existing(this);
}
}
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.
import { GameOptions } from './gameOptions';
export default class PlatformSprite extends Phaser.GameObjects.RenderTexture {
isHeroOnIt: Boolean = false;
body: Phaser.Physics.Arcade.Body;
assignedVelocity: number = 0;
constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number) {
super(scene, x, y, width, height);
this.setOrigin(0.5);
scene.add.existing(this);
scene.physics.add.existing(this);
}
setPhysics(): void {
this.body.setImmovable(true);
this.body.setAllowGravity(false);
this.body.setFrictionX(1);
}
drawTexture(border: Phaser.GameObjects.Graphics, pattern: Phaser.GameObjects.TileSprite, eyes: Phaser.GameObjects.Sprite): void {
border.clear();
border.lineStyle(8, 0x000000, 1);
border.strokeRect(0, 0, this.displayWidth, this.displayHeight);
this.draw(pattern, this.displayWidth / 2, Phaser.Math.Between(0, GameOptions.platformHeight));
this.draw(eyes, this.displayWidth / 2, this.displayHeight / 2);
this.draw(border);
}
transformTo(x: number, y: number, width: number, height: number): void {
this.x = x;
this.y = y;
this.setSize(width, height);
this.body.setSize(width, height);
}
explodeAndDestroy(emitter: Phaser.GameObjects.Particles.ParticleEmitter): void {
let platformBounds: Phaser.Geom.Rectangle = this.getBounds();
emitter.setPosition(platformBounds.left, platformBounds.top);
emitter.active = true;
emitter.setEmitZone({
source: new Phaser.Geom.Rectangle(0, 0, platformBounds.width, platformBounds.height),
type: 'random',
quantity: 50
});
emitter.explode(50, this.x - this.displayWidth / 2, this.y - this.displayHeight / 2);
this.clearTint();
this.isHeroOnIt = false;
}
}
And the second porting of a JavaScript prototype to TypeScript has been made.
Now I am going to finish the development of this game, adding comments and features, meanwhile download the source code.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.