Talking about Mini Archer game, Game development, HTML5, Javascript, Phaser and TypeScript.
Welcome to the the 6th part of the Mini Archer tutorial series. This time I won’t be adding new features, splitting the existing code into classes.
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.
Was it necessary to split an already working code into classes? Of course!
Classes contribute to the readability and maintainability of your code. They provide a clear structure and hierarchy, making it easier for other programmers (including your future self) to understand the codebase, navigate through the project, and make changes without introducing errors.
As you can see, the game is working in the same way as previous step:
Tap to shoot the arrow and try to hit the target.
Here you have the completely commented source code.
We have one HTML file, one CSS file and ten 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: #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. I also grouped the variables to keep them more organized.
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay
export const GameOptions = {
terrain : {
// terrain start, in screen height ratio, where 0 = top, 1 = bottom
start : 0.6,
// vertical offset where to start placing stuff, in pixels
stuffOffset : 38
},
// girl x position, in screen width ratio, where 0 = left, 1 = right
girlPosition : 0.15,
target : {
// target position range, in screen width ratio, where 0 = left, 1 = right
positionRange : {
from : 0.6,
to : 0.9
},
// target height range, in pixels
heightRange : {
from : 200,
to : 450
}
},
targetRings : {
// number of rings
amount : 5,
// ring ratio, to make target look oval, this is the ratio of width compared to height
ratio : 0.8,
// ring colors, from external to internal
color : [0xffffff, 0x5cb6f8, 0xe34d46, 0xf2aa3c, 0x95a53c],
// ring radii, from external to internal, in pixels
radius : [50, 40, 40, 30, 20],
// tolerance of ring radius, can be up to this ratio bigger or smaller
radiusTolerance : 0.5
},
rainbow : {
// rainbow rings width, in pixels
width : 5,
// rainbow colors
colors : [0xe8512e, 0xfbb904, 0xffef02, 0x65b33b, 0x00aae5, 0x3c4395, 0x6c4795]
},
arrow : {
// arrow rotation speed, in degrees per second
rotationSpeed : 180,
// flying speed
flyingSpeed: 1000
}
}
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. Animations are also defined here.
// 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.image('cloud', 'assets/sprites/cloud.png');
this.load.image('arrow', 'assets/sprites/arrow.png');
this.load.image('mask', 'assets/sprites/mask.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 {
// 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
});
// 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 { Rainbow } from './rainbow';
import { Arrow } from './arrow';
import { Girl } from './girl';
import { Target } from './target';
import { Terrain } from './terrain';
// game states
enum GameState {
Idle,
Aiming,
Firing
}
// this class extends Scene class
export class PlayGame extends Phaser.Scene {
constructor() {
super({
key : 'PlayGame'
});
}
// current game state
gameState : GameState;
// the girl
girl : Girl;
// the terrain
terrain : Terrain;
// the rainbow
rainbow : Rainbow;
// the arrow
arrow : Arrow;
// the target
target : Target;
// array to hold all stuff to be scrolled
stuffToScroll : any[];
// method to be executed when the scene has been created
create() : void {
// at the beginning, the game is in idle state
this.gameState = GameState.Idle;
// add the terrain
this.terrain = new Terrain(this);
// add the target
this.target = new Target(this);
// add rainbow
this.rainbow = new Rainbow(this);
// add the arrow
this.arrow = new Arrow(this);
// add clouds, after the arrow for a matter of z-indexing
this.rainbow.addClouds();
// add the girl
this.girl = new Girl(this);
// tween the target to a random position
this.tweenTarget(this.getRandomPosition());
// populate the array with all stuff to be scrolled
this.stuffToScroll = this.arrow.getChildren();
this.stuffToScroll = this.stuffToScroll.concat(this.target.getChildren());
this.stuffToScroll = this.stuffToScroll.concat(this.terrain.getChildren());
// listeners
this.input.on('pointerdown', this.handlePointer, this);
this.rainbow.on('appeared', this.rainbowAppeared, this);
this.arrow.on('flown', this.arrowFlown, this);
}
// method to handle pointer
handlePointer() : void {
// is the girl aiming?
if (this.gameState == GameState.Aiming) {
// now the girl is firing
this.gameState = GameState.Firing;
// default distance the arrow will travel
let distance : number = this.game.config.width as number * 2;
// if the arrow hits the target...
if (this.target.hitByArrow(this.rainbow.center.x, this.rainbow.center.y, this.arrow.arrow.rotation)) {
// adjust the distance to make the arrow stop in the center of the target
distance = (this.target.rings[0].x - this.rainbow.center.x - this.arrow.arrow.displayWidth) / Math.cos(this.arrow.arrow.rotation);
// place the mask behind target horizontal center and make it big enough
this.arrow.arrowMask.x = this.target.rings[0].x;
this.arrow.arrowMask.y = this.target.rings[0].y;
}
// shoot the arrow!
this.arrow.shoot(distance);
}
}
// simple metod to get a random target position
getRandomPosition() : number {
return Math.round(Phaser.Math.FloatBetween(GameOptions.target.positionRange.from, GameOptions.target.positionRange.to) * (this.game.config.width as number));
}
// method to draw the rainbow
drawRainbow() : void {
this.rainbow.appear(this.girl.body.x, this.girl.body.getBounds().centerY, this.rainbow.clouds[1].x, this.rainbow.clouds[1].posY);
}
// method to be called once the rainbow appeared
rainbowAppeared() : void {
// the girl is aiming
this.gameState = GameState.Aiming;
// aim the arrow
this.arrow.prepareToAim(this.rainbow);
}
// method to be called once the arrow flown
arrowFlown() : void {
// make rainbow disappear
this.rainbow.disappear();
// tween the new target
this.tweenTarget(this.getRandomPosition());
}
// method to tween the target to posX
// posX : the x position
tweenTarget(posX : number) : void {
// 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;
// move rainbow cloud
this.rainbow.moveCloud(this.girl.body.x - 50, this.girl.body.getBounds().top, deltaX * 3);
// play girl's "run" animation
this.girl.body.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
this.stuffToScroll.forEach((item : any) => {
item.x -= delta * deltaX;
});
// adjust terrain position
this.terrain.adjustPosition();
// adjust target position
this.target.adjustPosition(totalTravelled);
},
// method to be called when the tween completes
onComplete : () => {
// play girl's "idle" animation
this.girl.body.anims.play('idle');
// draw the rainbow
this.drawRainbow();
}
})
}
// method to be executed at each frame
// time : time passed since the beginning
// deltaTime : time passed since last frame
update(time : number, deltaTime : number) : void {
// is the player aiming?
if (this.gameState == GameState.Aiming) {
// let the arrow aim
this.arrow.aim(deltaTime, this.rainbow);
}
}
}
arrow.ts
The arrow, a Group with two Sprite objects, one for the arrow itself and one for the mask to simulate the arrow to stick into the target.
// ARROW CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP
import { GameObjects } from 'phaser';
import { GameOptions } from './gameOptions';
import { Rainbow } from './rainbow';
export class Arrow extends Phaser.GameObjects.Group {
// a simple multiplier to make the arrow move clockwise or counter clockwise
mult : number;
// the arrow itself
arrow : Phaser.GameObjects.Sprite;
// arrow mask to simulate the arrow to stick in the target
arrowMask : GameObjects.Sprite;
constructor(scene : Phaser.Scene) {
super(scene);
scene.add.existing(this);
// add the arrow
this.arrow = scene.add.sprite(0, 0, 'arrow');
this.arrow.setOrigin(0, 0.5);
this.arrow.setVisible(false);
this.mult = 1;
// add arrow to the group
this.add(this.arrow);
// create a mask to simulate arrow sticking in the target
this.arrowMask = scene.add.sprite(0, 0, 'mask');
this.arrowMask.setOrigin(0, 0.5);
this.arrowMask.setVisible(false);
this.arrowMask.setDisplaySize(512, 512);
// add arrow mask to the group
this.add(this.arrowMask);
// set the mask as a bitmap mask
let bitmapMask : Phaser.Display.Masks.BitmapMask = this.arrowMask.createBitmapMask();
bitmapMask.invertAlpha = true;
this.arrow.setMask(bitmapMask);
}
// method to prepare to aim
// rainbow : the rainbow
prepareToAim(rainbow : Rainbow) {
// place the arrow mask outside the screen
this.arrowMask.setX(this.scene.game.config.width as number);
// set the arrow visible
this.arrow.setVisible(true);
// place the arrow according to rainbow radius
this.arrow.setPosition(rainbow.center.x + rainbow.radius - 30, rainbow.center.y);
// rotate the arrow according to rainbow radius
this.arrow.setAngle(Phaser.Math.RadToDeg(rainbow.startAngle));
}
// method to aim
// deltaTime : time passed since previous frame, in milliseconds
// rainbow : the raimbow
aim(deltaTime : number, rainbow : Rainbow) : void {
// rotate the arrow according to arrow speed
this.arrow.angle += GameOptions.arrow.rotationSpeed * deltaTime / 1000 * this.mult;
// did the arrow reach the end of the rainbow?
if (this.arrow.angle > Phaser.Math.RadToDeg(rainbow.startAngle + rainbow.length)) {
// don't let it go further
this.arrow.angle = Phaser.Math.RadToDeg(rainbow.startAngle + rainbow.length);
// invert arrow rotation direction
this.mult *= -1;
}
// did the arrow reach the beginning of the rainbow?
if (this.arrow.angle < Phaser.Math.RadToDeg(rainbow.startAngle)) {
// don't let the arrow go further
this.arrow.angle = Phaser.Math.RadToDeg(rainbow.startAngle);
// invert arrow rotation direction
this.mult *= -1;
}
// set arrow position according to its rotation
this.arrow.setPosition(rainbow.center.x + (rainbow.radius - 30) * Math.cos(this.arrow.rotation), rainbow.center.y + (rainbow.radius - 30) * Math.sin(this.arrow.rotation));
}
// method to shoot
// distance : distance the arrow must travel
shoot(distance : number) : void {
// add the tween to shoot the arrow
this.scene.tweens.add({
// target: the arrow
targets: this.arrow,
// arrow destination, determine with trigonometry
x : this.arrow.x + distance * Math.cos(this.arrow.rotation),
y : this.arrow.y + distance * Math.sin(this.arrow.rotation),
// tween duration, according to distance
duration : distance / GameOptions.arrow.flyingSpeed * 1000,
// tween callback scope
callbackScope : this,
// function to execute when the tween is complete
onComplete : () => {
// add a timer event to wait one second
this.scene.time.addEvent({
// amount of milliseconds to wait
delay : 1000,
// timer callback scope
callbackScope : this,
// function to execute when the timer is complete
callback : () => {
// emit "flown" event
this.emit('flown');
}
})
}
})
}
}
cloud.ts
The cloud, a Sprite.
// CLOUD CLASS EXTENDS PHASER.GAMEOBJECTS.SPRITE
export class Cloud extends Phaser.GameObjects.Sprite {
// we need to keep track of y position to allow floating movement
posY : number;
constructor(scene : Phaser.Scene) {
super(scene, 0, 0, 'cloud');
scene.add.existing(this);
// add a tween from 0 to 1
this.scene.tweens.addCounter({
// start value
from : 0,
// end value
to : 1,
// tween duration, in milliseconds
duration : 1000,
// callback scope
callbackScope : this,
// function to be executed at each update
onUpdate : (tween : Phaser.Tweens.Tween) => {
// make cloud float using a cosine function
this.setY(this.posY + 5 * Math.cos(Math.PI * tween.getValue()));
},
// run the tween in reverse as well
yoyo : true,
// execute the tween forever
repeat : -1
});
}
// method to move a cloud along a path
// posX : new x position
// posY : new y position
// duration : duration, in milliseconds, of the movement
moveAlongPath(posX : number, posY : number, duration : number) : void {
// object which will follow a path
let follower : any = {
t: 0,
vec: new Phaser.Math.Vector2()
};
// define cloud movement line
let movementLine : Phaser.Curves.Line = new Phaser.Curves.Line([this.x, this.posY, posX, posY]);
// add a path
var path : Phaser.Curves.Path = this.scene.add.path(0, 0);
// add movement line to path
path.add(movementLine);
// add a tween
this.scene.tweens.add({
// tween target
targets : follower,
// bring t property of the target to 1
t : 1,
// duration, in milliseconds
duration : duration,
// callback scope
callbackScope : this,
// function to be executed at each update
onUpdate : () => {
// get the point along the path at time t, where 0 = the beginning, 1 = the end
var point = path.getPoint(follower.t, follower.vec)
// set new cloud x position
this.setX(point.x)
// set new cloud Y property
this.posY = point.y;
}
});
}
}
girl.ts
The running girl, a Group with Sprite objects representing the girl herself and her shadow.
// GIRL CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP
import { GameOptions } from './gameOptions';
export class Girl extends Phaser.GameObjects.Group {
// the girl itself
body : Phaser.GameObjects.Sprite;
// girl shadow
shadow : Phaser.GameObjects.Sprite;
constructor(scene : Phaser.Scene) {
super(scene);
scene.add.existing(this);
// determine girl x and y position
let girlPositionY : number = this.scene.game.config.height as number * GameOptions.terrain.start + GameOptions.terrain.stuffOffset;
let girlPositionX : number = this.scene.game.config.width as number * GameOptions.girlPosition;
// add girl shadow
this.shadow = this.scene.add.sprite(girlPositionX + 5, girlPositionY, 'circle');
this.shadow.setTint(0x000000);
this.shadow.setAlpha(0.2);
this.shadow.setDisplaySize(60, 20);
// add shadow to group
this.add(this.shadow);
// add girl
this.body = this.scene.add.sprite(girlPositionX, girlPositionY, 'girl');
this.body.setOrigin(0.5, 1);
this.body.anims.play('idle');
// add girl to group
this.add(this.body);
}
}
rainbow.ts
The rainbow, a Group with a Graphics object to draw the rainbow itself and two Cloud objects.
// RAINBOW CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP
import { GameOptions } from './gameOptions';
import {Cloud} from './cloud';
// rainbow ends
enum RainbowEnd {
Lower,
Upper
}
export class Rainbow extends Phaser.GameObjects.Group {
// the Graphics ojbect representing the rainbow itself
graphics : Phaser.GameObjects.Graphics;
// rainbow start angle
startAngle : number;
// rainbow radius
radius : number;
// rainbow length, in radians
length : number;
// rainbow center
center : Phaser.Geom.Point;
// array where to store the clouds
clouds : Cloud[];
constructor(scene : Phaser.Scene) {
super(scene);
scene.add.existing(this);
// add a new graphics
this.graphics = this.scene.add.graphics();
// add the graphics to the group
this.add(this.graphics);
}
// method to add the clouds
addClouds() : void {
// populate clouds array
this.clouds = [new Cloud(this.scene), new Cloud(this.scene)];
this.add(this.clouds[RainbowEnd.Lower]);
this.add(this.clouds[RainbowEnd.Upper]);
}
// method to make the rainbow appear
// centerX : x coordinate of rainbow center
// centerY : y coordinate of rainbow center
// arcX : x coordinate of the beginning of the arc
// arcY : y coordinate of the beginning of the arc
appear(centerX : number, centerY : number, arcX : number, arcY : number) : void {
// make lower cloud visible
this.clouds[RainbowEnd.Lower].setVisible(true);
// set rainbow center
this.center = new Phaser.Geom.Point(centerX, centerY);
// make a line representing rainbow radius
let rainbowRadius : Phaser.Geom.Line = new Phaser.Geom.Line(centerX, centerY, arcX, arcY);
// get radius length
this.radius = Phaser.Geom.Line.Length(rainbowRadius) - GameOptions.rainbow.colors.length / 2 * GameOptions.rainbow.width;
// get radius angle, which is rainbow start angle
this.startAngle = Phaser.Geom.Line.Angle(rainbowRadius);
// get a random rainbow arc length
this.length = Math.PI / 4 * 3 + Phaser.Math.FloatBetween(0, Math.PI / 4);
// generic tween of a value from 0 to 1, to make rainbow appear
this.scene.tweens.addCounter({
from : 0,
to : 1,
// tween duration
duration : 200,
// tween callback scope
callbackScope : this,
// method to be called at each tween update
onUpdate : (tween : Phaser.Tweens.Tween) => {
// draw the rainbow
this.drawRainbow(this.length * tween.getValue());
},
// method to be called when the tween completes
onComplete : () => {
// emit "appeared" event
this.emit('appeared');
}
})
}
// method to make the rainbow disappear
disappear() : void {
// generic tween of a value from 0 to 1, to make rainbow disappear
this.scene.tweens.addCounter({
from : 0,
to : 1,
// tween duration
duration : 200,
// tween callback scope
callbackScope : this,
// function to be called at each tween update
onUpdate : (tween : Phaser.Tweens.Tween) => {
// draw the rainbow
this.drawRainbow(this.length - this.length * tween.getValue());
},
// function to be called when the tween ends
onComplete : () => {
// make lower cloud disappear
this.clouds[RainbowEnd.Lower].setVisible(false)
}
})
}
// method to draw the rainbow
// angle : rainbow angle
drawRainbow(angle : number) : void {
// clear rainbow graphics
this.graphics.clear();
// loop through all rainbow colors
GameOptions.rainbow.colors.forEach((item : number, index : number) => {
// set line style
this.graphics.lineStyle(GameOptions.rainbow.width, item, 1);
// draw the arc
this.graphics.beginPath();
this.graphics.arc(this.center.x, this.center.y, this.radius + index * GameOptions.rainbow.width, this.startAngle, this.startAngle + angle, false);
this.graphics.strokePath();
// set lower cloud posY attribute
this.clouds[RainbowEnd.Lower].posY = this.center.y + (this.radius + (index * GameOptions.rainbow.width) / 2) * Math.sin(this.startAngle + angle);
// set lower cloud x position
this.clouds[RainbowEnd.Lower].setX(this.center.x + (this.radius + (index * GameOptions.rainbow.width) / 2) * Math.cos(this.startAngle + angle));
});
}
// method to move the cloud
// startX : cloud x position
// startY : cloud y position
// duration : movement duration, in milliseconds.
moveCloud(startX : number, startY : number, duration : number) : void {
// next cloud X position
let nextCloudX : number = startX + Phaser.Math.Between(0, 100);
// next cloud y position
let nextCloudY : number = startY - Phaser.Math.Between(50, 100);
// move the cloud along the path
this.clouds[RainbowEnd.Upper].moveAlongPath(nextCloudX, nextCloudY, duration);
}
}
target.ts
The target, a Group with some Sprite objects representing the circles building the target itself, the pole, the pole top, the pole shadow and the target shadow.
// TARGET CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP
import { GameOptions } from './gameOptions';
export class Target extends Phaser.GameObjects.Group {
// target shadow
shadow : Phaser.GameObjects.Sprite;
// pole shadow
poleShadow : Phaser.GameObjects.Sprite;
// the pole
pole : Phaser.GameObjects.TileSprite;
// pole top
poleTop : Phaser.GameObjects.Sprite;
// array with all rings
rings : Phaser.GameObjects.Sprite[];
// array with all rings radii
ringRadii : number[];
constructor(scene : Phaser.Scene) {
super(scene);
scene.add.existing(this);
// x position where to start placing stuff
let stuffStartX : number = this.scene.game.config.height as number * GameOptions.terrain.start + GameOptions.terrain.stuffOffset;
// add a circle which represents target shadow
this.shadow = this.scene.add.sprite(0, 0, 'circle');
this.shadow.setTint(0x676767);
// add the shadow to the group
this.add(this.shadow);
// add a circle representing pole shadow
this.poleShadow = this.scene.add.sprite(0, stuffStartX, 'circle');
this.poleShadow.setTint(0x000000);
this.poleShadow.setAlpha(0.2);
this.poleShadow.setDisplaySize(90, 20);
// add pole shadow to the group
this.add(this.poleShadow);
// add pole
this.pole = this.scene.add.tileSprite(0, stuffStartX, 32, 0, 'pole');
this.pole.setOrigin(0.5, 1);
// add the pole to the group
this.add(this.pole);
// add pole top
this.poleTop = this.scene.add.sprite(0, 0, 'poletop');
this.poleTop.setOrigin(0.5, 1);
// add pole top to the group
this.add(this.poleTop);
// add circles which represent the various target circles
this.rings = [];
for (let i : number = 0; i < GameOptions.targetRings.amount; i ++) {
this.rings[i] = this.scene.add.sprite(0, 0, 'circle');
this.add(this.rings[i]);
}
// place the target
this.place(this.scene.game.config.width as number * 2);
}
// method to place the garget
// posX : x position
place(posX : number) : void {
// array where to store radii values
this.ringRadii = [];
// determine radii values according to default radius size and tolerance
for (let i : number = 0; i < GameOptions.targetRings.amount; i ++) {
this.ringRadii[i] = Math.round(GameOptions.targetRings.radius[i] + (GameOptions.targetRings.radius[i] * Phaser.Math.FloatBetween(0, GameOptions.targetRings.radiusTolerance) * Phaser.Math.RND.sign()));
}
// get the sum of all radii, this will be the size of the target
let radiiSum : number = this.ringRadii.reduce((sum, value) => sum + value, 0);
// determine target height
let targetHeight : number = this.pole.y - Phaser.Math.Between(GameOptions.target.heightRange.from, GameOptions.target.heightRange.to)
// set pole shadow x poisition
this.poleShadow.setX(posX);
// set pole x position
this.pole.setX(posX);
// set pole height
this.pole.height = this.pole.y - targetHeight;
// set pole top position
this.poleTop.setPosition(posX, this.pole.y - this.pole.displayHeight - radiiSum / 2 + 10);
// set shadow size
this.shadow.setDisplaySize(radiiSum * GameOptions.targetRings.ratio, radiiSum);
// set target shadow position
this.shadow.setPosition(posX + 5, targetHeight);
// loop through all rings
for (let i : number = 0; i < GameOptions.targetRings.amount; i ++) {
// set ring position
this.rings[i].setPosition(posX, targetHeight);
// set ring tint
this.rings[i].setTint(GameOptions.targetRings.color[i]);
// set ring diplay size
this.rings[i].setDisplaySize(radiiSum * GameOptions.targetRings.ratio, radiiSum);
// decrease radiiSum to get the radius of next ring
radiiSum -= this.ringRadii[i];
}
}
// method to check if the target has been hit by the arrow
// startX : x position of rainbow center
// startY : y position of rainbow center
// arrowAngle : arrow angle
hitByArrow(startX : number, startY : number, arrowAngle : number) : boolean {
// get radii sum, which is the height of the target
let radiiSum : number = this.ringRadii.reduce((sum, value) => sum + value, 0);
// we define a target line going from the center of the rainbow to the top edge of the target
let topTargetLine : Phaser.Geom.Line = new Phaser.Geom.Line(startX, startY, this.rings[0].x, this.rings[0].y - radiiSum / 2);
// we define a target line going from the center of the rainbow to the bottom edge of the target
let bottomTargetLine : Phaser.Geom.Line = new Phaser.Geom.Line(startX, startY, this.rings[0].x, this.rings[0].y + radiiSum / 2);
// get the angle of the top target line and normalize it
let topAngle : number = Phaser.Geom.Line.Angle(topTargetLine);
let topNormalizedAngle : number = Phaser.Math.Angle.Normalize(topAngle);
// get the angle of the bottom target line and normalize it
let bottomAngle : number = Phaser.Geom.Line.Angle(bottomTargetLine);
let bottomNormalizedAngle : number = Phaser.Math.Angle.Normalize(bottomAngle);
// get the normalized arrow angle
let arrowNormalizedAngle : number = Phaser.Math.Angle.Normalize(arrowAngle);
// return true if arrow angle is between top and bottom target angle
return arrowNormalizedAngle >= topNormalizedAngle && arrowNormalizedAngle <= bottomNormalizedAngle;
}
// adjust target position
// deltaX : distance alreay travelled by the target
adjustPosition(deltaX : number) : void {
// if the target left the canvas from the left side...
if (this.shadow.getBounds().right < 0) {
// reposition it on the right side
this.place(this.scene.game.config.width as number * 2 - deltaX);
}
}
}
terrain.ts
The scrolling terrain, a Group with two TileSprite objects, one for the grass and one for the dirt.
// TERRAIN CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP
import { GameOptions } from './gameOptions';
export class Terrain extends Phaser.GameObjects.Group {
// the grass, the upper part of the terrain
grass : Phaser.GameObjects.TileSprite;
// the dirt, the lower part of the terrain
dirt : Phaser.GameObjects.TileSprite;
constructor(scene : Phaser.Scene) {
super(scene);
scene.add.existing(this);
// determine terrain starting y position
let terrainStartY : number = this.scene.game.config.height as number * GameOptions.terrain.start
// add grass tilesprite
this.grass = this.scene.add.tileSprite(0, terrainStartY, this.scene.game.config.width as number + 256, 256, 'grasstile');
this.grass.setOrigin(0, 0);
// add the grass to group
this.add(this.grass)
// determine dirt starting y position
let dirtStartY : number = terrainStartY + 256;
// add dirt tilesprite
this.dirt = this.scene.add.tileSprite(0, dirtStartY, this.grass.width, this.scene.game.config.height as number - dirtStartY, 'dirttile');
this.dirt.setOrigin(0, 0);
// add the dirt to the group
this.add(this.dirt)
}
// method to adjust terrain position
adjustPosition() : void {
// adjust the seamless grass when it goes too much outside the screen
if (this.grass.x < -256) {
this.grass.x += 256;
}
// adjust the seamless dirt when it goes too much outside the screen
if (this.dirt.x < -256) {
this.dirt.x += 256;
}
}
}
And that’s it! Now your code is more readable and more reusable. Download the source code of the entire project.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.