Talking about Magick game, Game development, HTML5, Javascript, Phaser and TypeScript.
Due to the 32 bit apocalypse back in 2017 when Apple launched iOS 11 terminating 32 bit app support, you can’t play Magick game anymore (here is the dead link) but believe me, the game was fun, and there is a tutorial series about it.
Time to update previous step to Phaser 3.60, rewritting it in TypeScript, in order to bring it up to date and prepare it for future updates.
This is what I built:
The player walks and climbs on his own, you can only click anywhere on an empty spot to summon a block, but you can summon only one blockat once.
You can also change player direction by tapping on it.
The gameplay is simple, but with the proper level design you can turn this prototype into a fun game.
The source code is pretty straightforward and consists in one HTML file, one CSS file and six TypeScript files.
index.html
The web page which hosts the game, to be run inside thegame element.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="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.
* {
padding : 0;
margin : 0;
}
body {
background-color: #000000;
}
canvas {
touch-action : none;
-ms-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. I also grouped the variables to keep them more organized.
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay
export const GameOptions = {
player : {
speed : 120,
jumpSpeed : {
x : 30,
y : -100
},
gravity : 400,
triggerRadius : 32
},
tileSize : 32
}
main.ts
This is where the game is created, with all Phaser related options.
// MAIN GAME FILE
// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame } from './playGame';
// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
mode : Phaser.Scale.FIT,
autoCenter : Phaser.Scale.CENTER_BOTH,
parent : 'thegame',
width : 800,
height : 320
}
// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
type : Phaser.AUTO,
backgroundColor : 0x000000,
scale : scaleObject,
scene : [PreloadAssets, PlayGame],
physics : {
default : 'arcade',
arcade : {
gravity : {
y : 0
}
}
}
}
// the game itself
new Phaser.Game(configObject);
preloadAssets.ts
Here we preload all assets to be used in the game.
// CLASS TO PRELOAD ASSETS
// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {
// constructor
constructor() {
super({
key : 'PreloadAssets'
});
}
// method to be execute during class preloading
preload() : void {
this.load.image('tiles', 'assets/sprites/tiles.png');
this.load.tilemapTiledJSON('map', 'assets/maps/map.json');
this.load.image('player', 'assets/sprites/player.png');
}
// method to be called once the instance has been created
create() : void {
// call PlayGame class
this.scene.start('PlayGame');
}
}
playGame.ts
Main game file, all game logic is stored here.
// THE GAME ITSELF
import { GameOptions } from './gameOptions';
import { Player } from './player';
import { MapUtils } from './mapUtils';
// this class extends Scene class
export class PlayGame extends Phaser.Scene {
constructor() {
super({
key : 'PlayGame'
});
}
map : Phaser.Tilemaps.Tilemap;
player : Player;
levelLayer : Phaser.Tilemaps.TilemapLayer;
// method to be executed when the scene has been created
create() : void {
this.map = this.make.tilemap({
key : 'map'
});
let tileSet : Phaser.Tilemaps.Tileset = this.map.addTilesetImage('tileset01', 'tiles') as Phaser.Tilemaps.Tileset;
this.levelLayer = this.map.createLayer('myLevel', tileSet) as Phaser.Tilemaps.TilemapLayer;
this.map.setCollisionBetween(1, 2);
this.player = new Player(this, 48, 226);
this.input.on('pointerdown', this.handleInput, this);
}
handleInput(pointer : Phaser.Input.Pointer) : void {
if (Phaser.Math.Distance.Between(pointer.x, pointer.y, this.player.x, this.player.y) < GameOptions.player.triggerRadius) {
this.player.changeDirection();
}
else {
MapUtils.addTile(this.map, pointer.x, pointer.y);
}
}
movePlayer() : void {
this.player.move(this.map);
}
update() : void {
this.player.stopMoving();
this.physics.world.collide(this.player, this.levelLayer, this.movePlayer, undefined, this);
}
}
player.ts
The player, a custom class which extends Phaser.Physics.Arcade.Sprite object.
// PLAYER CLASS EXTENDS PHASER.PHYSICS.ARCADE.SPRITE
import { GameOptions } from './gameOptions';
export class Player extends Phaser.Physics.Arcade.Sprite {
isJumping : boolean;
direction : number;
body : Phaser.Physics.Arcade.Body;
constructor(scene : Phaser.Scene, posX : number, posY : number) {
super(scene, posX, posY, 'player');
scene.add.existing(this);
scene.physics.add.existing(this);
this.isJumping = false;
this.direction = 1;
this.body.gravity.y = GameOptions.player.gravity;
}
move(map : Phaser.Tilemaps.Tilemap) : void {
if (this.body.blocked.down) {
this.body.setVelocityX(GameOptions.player.speed * this.direction);
this.isJumping = false;
}
if (this.body.blocked.right && this.direction == 1) {
if ((!map.getTileAtWorldXY(this.x + GameOptions.tileSize, this.y - GameOptions.tileSize) && !map.getTileAtWorldXY(this.x, this.y - GameOptions.tileSize)) || this.isJumping) {
this.jump();
}
else {
this.direction *= -1;
}
}
if(this.body.blocked.left && this.direction == -1) {
if((!map.getTileAtWorldXY(this.x - GameOptions.tileSize, this.y - GameOptions.tileSize) && !map.getTileAtWorldXY(this.x, this.y - GameOptions.tileSize)) || this.isJumping) {
this.jump();
}
else {
this.direction *= -1;
}
}
}
changeDirection() : void {
this.direction *= -1;
}
stopMoving() : void {
this.setVelocityX(0);
}
jump() : void {
this.body.setVelocity(GameOptions.player.jumpSpeed.x * this.direction, GameOptions.player.jumpSpeed.y);
this.isJumping = true;
}
}
mapUtils.ts
A simple class to store map utilities.
export class MapUtils {
static tilePoint : Phaser.Math.Vector2 | null = null;
static addTile(map: Phaser.Tilemaps.Tilemap, posX: number, posY: number) {
if (!map.getTileAtWorldXY(posX, posY)) {
if (this.tilePoint != null) {
map.removeTileAtWorldXY(this.tilePoint.x, this.tilePoint.y);
}
map.putTileAtWorldXY(2, posX, posY);
this.tilePoint = new Phaser.Math.Vector2(posX, posY);
}
}
}
And in a few lines I managed old Magick spirit to live once again. During next steps I will show you how to progress through levels, 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.