Talking about Mini Archer game, Game development, HTML5, Javascript, Phaser and TypeScript.
Here we go with the second step of Mini Archer series. We already have an endless terrain with infinite randomly generated targets, now it’s time to add a running archer, a girl in this case.
First, let me thank Bayat Games for terrain graphics and pzUH for the cute little girl.
All posts in this tutorial series:
Step 1: Creation of an endless terrain with infinite randonly generated targets.
Step 2: Adding a running character with more animations.
Step 3: Adding a bow using a Graphics GameObject.
Step 4: Adding the arrow.
Step 5: Firing the arrow.
Step 6: Splitting the code into classes.
Adding a running girl wasn’t hard, I just had to define two animations: the idle animation when the girl is not running, and the running animation to be played, of course, when the girl is runnng.
Then, it was just a matter of syncing animations with terrain tweens. So the girl actually does not run, it’s the entire environment to move towards her. Have a look:
There is room for a lot of customization thanks to gameOptions.ts file which holds a lot of options.
Let’s have a look at the commented source code: we have one HTML file, one CSS file and four TypeScript files. Phaser version used here is 3.60, beta 13.
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: #011025;
}
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.
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay
export const GameOptions = {
// terrain start, in screen height ratio, where 0 = top, 1 = bottom
terrainStart : 0.6,
// girl x position, in screen width ratio, where 0 = left, 1 = right
girlPosition : 0.15,
// target position range, in screen width ratio, where 0 = left, 1 = right
targetPositionRange : {
from : 0.5,
to : 0.9
},
// target height range, in pixels
targetHeightRange : {
from : 150,
to : 350
},
// number of rings
rings : 5,
// ring ratio, to make target look oval, this is the ratio of width compared to height
ringRatio : 0.8,
// ring colors, from external to internal
ringColor : [0xffffff, 0x5cb6f8, 0xe34d46, 0xf2aa3c, 0x95a53c],
// ring radii, from external to internal, in pixels
ringRadius : [45, 35, 35, 25, 15],
// tolerance of ring radius, can be up to this ratio bigger or smaller
ringRadiusTolerance : 0.5
}
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 : 540,
height : 960
}
// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
type : Phaser.AUTO,
backgroundColor : 0x5df4f0,
scale : scaleObject,
scene : [PreloadAssets, PlayGame]
}
// 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 is how we preload a bitmap font
this.load.image('circle', 'assets/sprites/circle.png');
this.load.image('grasstile', 'assets/sprites/grasstile.png');
this.load.image('dirttile', 'assets/sprites/dirttile.png');
this.load.image('pole', 'assets/sprites/pole.png');
this.load.image('poletop', 'assets/sprites/poletop.png');
this.load.spritesheet('idlegirl', 'assets/sprites/idlegirl.png', {
frameWidth : 119,
frameHeight : 130
});
this.load.spritesheet('runninggirl', 'assets/sprites/runninggirl.png', {
frameWidth : 119,
frameHeight : 130
});
}
// 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';
// this class extends Scene class
export class PlayGame extends Phaser.Scene {
constructor() {
super({
key : 'PlayGame'
});
}
// terrain
terrain : Phaser.GameObjects.TileSprite;
// dirt below the terrain
dirt : Phaser.GameObjects.TileSprite;
// pole
pole : Phaser.GameObjects.TileSprite;
// topmost part of the pole
poleTop : Phaser.GameObjects.Sprite;
// pole shadow
poleShadow : Phaser.GameObjects.Sprite;
// target shadow
targetShadow : Phaser.GameObjects.Sprite;
// target rigns
targetRings : Phaser.GameObjects.Sprite[];
// girl
girl : Phaser.GameObjects.Sprite;
// method to be executed when the scene has been created
create() : void {
// define idle animation
this.anims.create({
key : 'idle',
frames: this.anims.generateFrameNumbers('idlegirl', {
start: 0,
end: 15
}),
frameRate: 15,
repeat: -1
});
// define running animation
this.anims.create({
key : 'run',
frames: this.anims.generateFrameNumbers('runninggirl', {
start: 0,
end: 19
}),
frameRate: 15,
repeat: -1
});
// add terrain
let terrainStartY : number = this.game.config.height as number * GameOptions.terrainStart;
this.terrain = this.add.tileSprite(0, terrainStartY, this.game.config.width as number + 256, 256, 'grasstile');
this.terrain.setOrigin(0, 0);
// add dirt, the graphics below the terrain
let dirtStartY : number = terrainStartY + 256;
this.dirt = this.add.tileSprite(0, dirtStartY, this.terrain.width, this.game.config.height as number - dirtStartY, 'dirttile');
this.dirt.setOrigin(0, 0);
// add a circle which represents target shadow
this.targetShadow = this.add.sprite(0, 0, 'circle');
this.targetShadow.setTint(0x676767);
// add pole shadow
let poleYPos : number = terrainStartY + 38;
this.poleShadow = this.add.sprite(0, poleYPos, 'circle');
this.poleShadow.setTint(0x000000);
this.poleShadow.setAlpha(0.2);
this.poleShadow.setDisplaySize(90, 20);
// add pole
this.pole = this.add.tileSprite(0, poleYPos, 32, 0, 'pole');
this.pole.setOrigin(0.5, 1);
// add pole top
this.poleTop = this.add.sprite(0, 0, 'poletop');
this.poleTop.setOrigin(0.5, 1);
// add circles which represent the various target circles
this.targetRings = [];
for (let i : number = 0; i < GameOptions.rings; i ++) {
this.targetRings[i] = this.add.sprite(0, 0, 'circle');
}
// girl start position
let girlXPos : number = this.game.config.width as number * GameOptions.girlPosition;
// add girl shadow
let girlShadow : Phaser.GameObjects.Sprite = this.add.sprite(girlXPos + 5, poleYPos, 'circle');
girlShadow.setTint(0x000000);
girlShadow.setAlpha(0.2);
girlShadow.setDisplaySize(60, 20);
// add girl
this.girl = this.add.sprite(girlXPos, poleYPos, 'girl');
this.girl.setOrigin(0.5, 1);
this.girl.anims.play('idle');
// place a random target at current position
this.placeTarget(this.game.config.width as number * 2, this.pole.y);
// tween the target to a random position
this.tweenTarget(this.getRandomPosition());
}
// simple metod to get a random target position
getRandomPosition() : number {
return Math.round(Phaser.Math.FloatBetween(GameOptions.targetPositionRange.from, GameOptions.targetPositionRange.to) * (this.game.config.width as number));
}
// method to place the target at (posX, posY)
placeTarget(posX : number, posY : number) : void {
// array where to store radii values
let ringRadii : number[] = [];
// determine radii values according to default radius size and tolerance
for (let i : number = 0; i < GameOptions.rings; i ++) {
ringRadii[i] = Math.round(GameOptions.ringRadius[i] + (GameOptions.ringRadius[i] * Phaser.Math.FloatBetween(0, GameOptions.ringRadiusTolerance) * Phaser.Math.RND.sign()));
}
// get the sum of all radii, this will be the size of the target
let radiiSum : number = ringRadii.reduce((sum, value) => sum + value, 0);
// determine target height
let targetHeight : number = posY - Phaser.Math.Between(GameOptions.targetHeightRange.from, GameOptions.targetHeightRange.to)
// set pole shadow x poisition
this.poleShadow.setX(posX);
// set pole x position
this.pole.setX(posX);
// set pole height
this.pole.height = posY - targetHeight;
// set pole top position
this.poleTop.setPosition(posX, this.pole.y - this.pole.displayHeight - radiiSum / 2 + 10);
// set shadow size
this.targetShadow.setDisplaySize(radiiSum * GameOptions.ringRatio, radiiSum);
// set target shadow position
this.targetShadow.setPosition(posX + 5, targetHeight);
// loop through all rings
for (let i : number = 0; i < GameOptions.rings; i ++) {
// set ring position
this.targetRings[i].setPosition(posX, targetHeight);
// set ring tint
this.targetRings[i].setTint(GameOptions.ringColor[i]);
// set ring diplay size
this.targetRings[i].setDisplaySize(radiiSum * GameOptions.ringRatio, radiiSum);
// decrease radiiSum to get the radius of next ring
radiiSum -= ringRadii[i];
}
}
// method to tween the target to posX
tweenTarget(posX : number) : void {
// array with all target related stuff to move
let stuffToMove : any[] = [this.pole, this.poleTop, this.poleShadow, this.targetShadow, this.terrain, this.dirt].concat(this.targetRings);
// delta X between current target position and destination position
let deltaX : number = this.game.config.width as number * 2 - posX;
// variable to save previous value
let previousValue : number = 0;
// variable to save the amount of pixels already travelled
let totalTravelled : number = 0;
// play girl's "run" animation
this.girl.anims.play('run');
// tween a number from 0 to 1
this.tweens.addCounter({
from : 0,
to : 1,
// tween duration according to deltaX
duration : deltaX * 3,
// tween callback scope
callbackScope : this,
// method to be called at each tween update
onUpdate : (tween : Phaser.Tweens.Tween) => {
// delta between previous and current value
let delta : number = tween.getValue() - previousValue;
// update previous value to current value
previousValue = tween.getValue();
// determine the amount of pixels travelled
totalTravelled += delta * deltaX;
// move all stuff
stuffToMove.forEach((item : any) => {
item.x -= delta * deltaX;
})
// adjust the seamless terrain when it goes too much outside the screen
if (this.terrain.x < -256) {
this.terrain.x += 256;
}
// adjust the seamless dirt when it goes too much outside the screen
if (this.dirt.x < -256) {
this.dirt.x += 256;
}
// if the target left the canvas from the left side...
if (this.targetShadow.getBounds().right < 0) {
// reposition it on the right side
this.placeTarget(this.game.config.width as number * 2 - totalTravelled, this.pole.y);
}
},
// method to be called when the tween completes
onComplete : () => {
// play girl's "idle" animation
this.girl.anims.play('idle');
// add a time event
this.time.addEvent({
// wait 1 second
delay : 2000,
// tween callback scope
callbackScope : this,
// callback function
callback : () => {
// tween the new target
this.tweenTarget(this.getRandomPosition());
}
});
}
})
}
}
Adding the running girl was easy, but since the sprite sheet does not feature animations throwing arrows with a bow, I’ll tweak the gameplay a bit and turn the little archer into a little sorceress throwing magic bolts. Will it work? We’ll see, meanwhile download the source code of the full project.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.