Talking about Spinny Gun game, Game development, HTML5, Javascript and Phaser.
Ketchapp did it again. Spinny Gun available for both iOS and Android is the 10,000th hyper-casual game made by this studio which becomes a smashing hit.
«Shoot as many targets as you can in this new addictive game!» the description says, and actually that’s all! There is a spinning gun and a series of targets moving around a circular path.
So we have objects moving along a path and rotating accordingly, bullets fired from a spinning gun and a collision routine checking for bullets trajectory. Which physics engine should we choose? No one.
Thanks to Phaser 3 paths, we can draw a path just like we are used to draw on graphic canvas with lines, curves and arcs, then make sprites follow such path tweening their positions with all the kind of controls we expect to be capable of when tweening a sprite: easing, speed, repeating, yoyo effects and more.
As for the collision system, we are only using trigonometry to determine the line of fire and check for intersection with targets bounding box.
Have a look at the example:
Tap or click to fire, try to hit the targets.
The source code, completely commented, allows a lot of customization thanks to gameOptions
object.
let game;
let gameOptions = {
// width of the path, in pixels
pathWidth: 500,
// height of the path, in pixels
pathHeight: 800,
// radius of path curves, in pixels
curveRadius: 50,
// amount of targets in game
targets: 5,
// min and max milliseconds needed by the targets
// to run all the way around the path
targetSpeed: {
min: 6000,
max: 10000
},
// min and max target size, in pixels
targetSize: {
min: 100,
max: 200
},
// milliseconds needed by the gun to rotate by 360 degrees
gunSpeed: 5000,
// multiplier to be applied to gun rotation speed each time
// the gun fires
gunThrust: 2,
// maximum gun speed multiplier.
// If gunSpeed is 5000 and maxGunSpeedMultiplier is 15,
// maximum gun rotation will allow to rotate by 360 degrees
// in 5000/15 seconds
maxGunSpeedMultiplier: 15,
// gunFriction will reduce gun rotating speed each time the gun
// completes a 360 degrees rotation
gunFriction: 0.9
}
window.onload = function() {
let gameConfig = {
type: Phaser.AUTO,
backgroundColor: 0x222222,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
parent: "thegame",
width: 750,
height: 1334
},
scene: playGame
}
game = new Phaser.Game(gameConfig);
window.focus();
}
class playGame extends Phaser.Scene{
constructor(){
super("PlayGame");
}
preload(){
this.load.image("tile", "tile.png");
this.load.image("gun", "gun.png");
this.load.image("fireline", "fireline.png");
}
create(){
// determine the offset to make path always stand in the center of the stage
let offset = new Phaser.Math.Vector2((game.config.width - gameOptions.pathWidth) / 2, (game.config.height - gameOptions.pathHeight) / 2);
// building a path using lines and ellipses. Ellipses are used to create
// circle arcs and build the curves
this.path = new Phaser.Curves.Path(offset.x + gameOptions.curveRadius, offset.y);
this.path.lineTo(offset.x + gameOptions.pathWidth - gameOptions.curveRadius, offset.y);
this.path.ellipseTo(-gameOptions.curveRadius, -gameOptions.curveRadius, 90, 180, false, 0);
this.path.lineTo(offset.x + gameOptions.pathWidth, offset.y + gameOptions.pathHeight - gameOptions.curveRadius);
this.path.ellipseTo(-gameOptions.curveRadius, -gameOptions.curveRadius, 180, 270, false, 0);
this.path.lineTo(offset.x + gameOptions.curveRadius, offset.y + gameOptions.pathHeight);
this.path.ellipseTo(-gameOptions.curveRadius, -gameOptions.curveRadius, 270, 0, false, 0);
this.path.lineTo(offset.x, offset.y + gameOptions.curveRadius);
this.path.ellipseTo(-gameOptions.curveRadius, -gameOptions.curveRadius, 0, 90, false, 0);
// drawing the path
this.graphics = this.add.graphics();
this.graphics.lineStyle(4, 0xffff00, 1);
this.path.draw(this.graphics);
// fireLine is the bullet trajectory
this.fireLine = this.add.sprite(game.config.width / 2, game.config.height / 2, "fireline");
this.fireLine.setOrigin(0, 0.5);
this.fireLine.displayWidth = 700;
this.fireLine.displayHeight = 8;
this.fireLine.visible = false;
// the rotating gun
this.gun = this.add.sprite(game.config.width / 2, game.config.height / 2, "gun");
// the group of targets
this.targets = this.add.group();
for(let i = 0; i < gameOptions.targets; i++){
// target aren't sprites but followers!!!!
let target = this.add.follower(this.path, offset.x + gameOptions.curveRadius, offset.y, "tile");
target.alpha = 0.8;
target.displayWidth = Phaser.Math.RND.between(gameOptions.targetSize.min, gameOptions.targetSize.max)
this.targets.add(target);
// the core of the script: targets run along the path starting from a random position
target.startFollow({
duration: Phaser.Math.RND.between(gameOptions.targetSpeed.min, gameOptions.targetSpeed.max),
repeat: -1,
rotateToPath: true,
verticalAdjust: true,
startAt: Phaser.Math.RND.frac()
});
}
// tween to rotate the gun
this.gunTween = this.tweens.add({
targets: [this.gun],
angle: 360,
duration: gameOptions.gunSpeed,
repeat: -1,
callbackScope: this,
onRepeat: function(){
// each round, gun angular speed decreases
this.gunTween.timeScale = Math.max(1, this.gunTween.timeScale * gameOptions.gunFriction);
}
});
// waiting for user input
this.input.on("pointerdown", function(pointer){
// we say we can fire when the fire line is not visible
if(!this.fireLine.visible){
this.fireLine.visible = true;
this.fireLine.angle = this.gun.angle;
// gun angular speed increases
this.gunTween.timeScale = Math.min(gameOptions.maxGunSpeedMultiplier, this.gunTween.timeScale * gameOptions.gunThrust);
// fire line disappears after 50 milliseconds
this.time.addEvent({
delay: 50,
callbackScope: this,
callback: function(){
this.fireLine.visible = false;
}
});
// calculate the line of fire starting from sprite angle
let radians = Phaser.Math.DegToRad(this.fireLine.angle);
let fireStartX = game.config.width / 2;
let fireStartY = game.config.height / 2;
let fireEndX = fireStartX + game.config.height / 2 * Math.cos(radians);
let fireEndY = fireStartY + game.config.height / 2 * Math.sin(radians);
let lineOfFire = new Phaser.Geom.Line(fireStartX, fireStartY, fireEndX, fireEndY);
// loop through all targets
this.targets.getChildren().forEach(function(target){
if(target.visible){
// get target bounding box
let bounds = target.getBounds();
// check if the line intersect the bounding box
if(Phaser.Geom.Intersects.LineToRectangle(lineOfFire, bounds)){
// target HIT!!!! hide it for 3 seconds
target.visible = false;
this.time.addEvent({
delay: 3000,
callback: function(){
target.visible = true;
}
});
}
}
}.bind(this))
}
}, this);
}
};
We were able, once more, to build a fully featured prototype of a hyper casual game in less than 100 lines of code thanks to Phaser. 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.