Talking about Perfect Square! game, Game development, HTML5, Javascript and Phaser.
A couple of years ago I covered Perfect Square! game with a tutorial series.
It may seem a physics game where you have to grow a square in a way it will perfectly fit in a hole, but I was able to create a fully working prototype using only tweens.
Have a look at the game, there are in-game instructions too!
Did you manage to advance levels? Good. All the physics stuff you see in the game is driven by tweens. The square floats, falls and bounces on the ground only thanks to tweens.
At the moment I updated the code to Phaser 3, finding these two issues:
1 – for some reason, in Phaser 3.15.* I wasn’t able to make cameras.main.setBackgroundColor
work.
2 – I had some troubles in changing the text on the falling square, it seems I wasn’t able to keep the origin – or anchor point – in place.
The rest of the code works like a charm, have a look at the uncommented code but a commented version is coming soon:
let game;
let saveData;
let gameOptions = {
bgColors: [0x62bd18, 0xff5300, 0xd21034, 0xff475c, 0x8f16b2, 0x588c7e, 0x8c4646],
holeWidthRange: [80, 260],
wallRange: [10, 50],
growTime: 1500,
localStorageName: "squaregamephaser3"
}
const IDLE = 0;
const WAITING = 1;
const GROWING = 2;
window.onload = function() {
let width = 640;
let height = 960;
let windowRatio = window.innerWidth / window.innerHeight;
if(windowRatio < width / height){
height = width / windowRatio;
}
var gameConfig = {
width: width,
height: height,
scene: playGame,
backgroundColor: 0x444444
}
game = new Phaser.Game(gameConfig);
window.focus()
resize();
window.addEventListener("resize", resize, false);
}
class playGame extends Phaser.Scene{
constructor(){
super("PlayGame");
}
preload(){
this.load.image("base", "base.png");
this.load.image("square", "square.png");
this.load.image("top", "top.png");
this.load.bitmapFont("font", "font.png", "font.fnt");
}
create(){
saveData = localStorage.getItem(gameOptions.localStorageName) == null ? {
level: 1
} : JSON.parse(localStorage.getItem(gameOptions.localStorageName));
let tintColor = Phaser.Utils. Array.GetRandom(gameOptions.bgColors);
this.cameras.main.setBackgroundColor(tintColor);
this.leftSquare = this.add.sprite(0, game.config.height, "base");
this.leftSquare.setOrigin(1, 1);
this.rightSquare = this.add.sprite(game.config.width, game.config.height, "base");
this.rightSquare.setOrigin(0, 1);
this.leftWall = this.add.sprite(0, game.config.height - this.leftSquare.height, "top");
this.leftWall.setOrigin(1, 1);
this.rightWall = this.add.sprite(game.config.width, game.config.height - this.rightSquare.height, "top");
this.rightWall.setOrigin(0, 1);
this.square = this.add.sprite(game.config.width / 2, -400, "square");
this.square.successful = 0;
this.square.setScale(0.2);
this.squareText = this.add.bitmapText(game.config.width / 2, -400, "font", (saveData.level - this.square.successful).toString(), 120);
this.squareText.setOrigin(0.5);
this.squareText.setScale(0.4);
this.squareText.setTint(tintColor);
this.levelText = this.add.bitmapText(game.config.width / 2, 0, "font", "level " + saveData.level, 60);
this.levelText.setOrigin(0.5, 0);
this.updateLevel();
this.input.on("pointerdown", this.grow, this);
this.input.on("pointerup", this.stop, this);
this.gameMode = IDLE;
}
updateLevel(){
let holeWidth = Phaser.Math.Between(gameOptions.holeWidthRange[0], gameOptions.holeWidthRange[1]);
let wallWidth = Phaser.Math.Between(gameOptions.wallRange[0], gameOptions.wallRange[1]);
this.placeWall(this.leftSquare, (game.config.width - holeWidth) / 2);
this.placeWall(this.rightSquare, (game.config.width + holeWidth) / 2);
this.placeWall(this.leftWall, (game.config.width - holeWidth) / 2 - wallWidth);
this.placeWall(this.rightWall, (game.config.width + holeWidth) / 2 + wallWidth);
let squareTween = this.tweens.add({
targets: [this.square, this.squareText],
y: 150,
scaleX: 0.2,
scaleY: 0.2,
angle: 50,
duration: 500,
ease: "Cubic.easeOut",
callbackScope: this,
onComplete: function(){
this.rotateTween = this.tweens.add({
targets: [this.square, this.squareText],
angle: 40,
duration: 300,
yoyo: true,
repeat: -1
});
if(this.square.successful == 0){
this.addInfo(holeWidth, wallWidth);
}
this.gameMode = WAITING;
}
})
}
placeWall(target, posX){
this.tweens.add({
targets: target,
x: posX,
duration: 500,
ease: "Cubic.easeOut"
});
}
grow(){
if(this.gameMode == WAITING){
this.gameMode = GROWING;
if(this.square.successful == 0){
this.infoGroup.toggleVisible();
}
this.growTween = this.tweens.add({
targets: [this.square, this.squareText],
scaleX: 1,
scaleY: 1,
duration: gameOptions.growTime
});
}
}
stop(){
if(this.gameMode == GROWING){
this.gameMode = IDLE;
this.growTween.stop();
this.rotateTween.stop();
this.rotateTween = this.tweens.add({
targets: [this.square, this.squareText],
angle: 0,
duration:300,
ease: "Cubic.easeOut",
callbackScope: this,
onComplete: function(){
if(this.square.displayWidth <= this.rightSquare.x - this.leftSquare.x){
this.tweens.add({
targets: [this.square, this.squareText],
y: game.config.height + this.square.displayWidth,
duration:600,
ease: "Cubic.easeIn",
callbackScope: this,
onComplete: function(){
this.levelText.text = "Oh no!!!";
this.gameOver();
}
})
}
else{
if(this.square.displayWidth <= this.rightWall.x - this.leftWall.x){
this.fallAndBounce(true);
}
else{
this.fallAndBounce(false);
}
}
}
});
}
}
fallAndBounce(success){
let destY = game.config.height - this.leftSquare.displayHeight - this.square.displayHeight / 2;
let message = "Yeah!!!!";
if(success){
this.square.successful ++;
}
else{
destY = game.config.height - this.leftSquare.displayHeight - this.leftWall.displayHeight - this.square.displayHeight / 2;
message = "Oh no!!!!";
}
this.tweens.add({
targets: [this.square, this.squareText],
y: destY,
duration:600,
ease: "Bounce.easeOut",
callbackScope: this,
onComplete: function(){
this.levelText.text = message;
if(!success){
this.gameOver();
}
else{
this.time.addEvent({
delay: 1000,
callback: function(){
if(this.square.successful == saveData.level){
saveData.level ++;
localStorage.setItem(gameOptions.localStorageName, JSON.stringify({
level: saveData.level
}));
this.scene.start("PlayGame");
}
else{
this.squareText.text = saveData.level - this.square.successful;
this.squareText.setOrigin(1, 1)
this.levelText.text = "level " + saveData.level;
this.updateLevel();
}
},
callbackScope: this
});
}
}
})
}
addInfo(holeWidth, wallWidth){
this.infoGroup = this.add.group();
let targetSquare = this.add.sprite(game.config.width / 2, game.config.height - this.leftSquare.displayHeight, "square");
targetSquare.displayWidth = holeWidth + wallWidth;
targetSquare.displayHeight = holeWidth + wallWidth;
targetSquare.alpha = 0.3;
targetSquare.setOrigin(0.5, 1);
this.infoGroup.add(targetSquare);
let targetText = this.add.bitmapText(game.config.width / 2, targetSquare.y - targetSquare.displayHeight - 20, "font", "land here", 48);
targetText.setOrigin(0.5, 1);
this.infoGroup.add(targetText);
let holdText = this.add.bitmapText(game.config.width / 2, 250, "font", "tap and hold to grow", 40);
holdText.setOrigin(0.5, 0);
this.infoGroup.add(holdText);
let releaseText = this.add.bitmapText(game.config.width / 2, 300, "font", "release to drop", 40);
releaseText.setOrigin(0.5, 0);
this.infoGroup.add(releaseText);
}
gameOver(){
this.time.addEvent({
delay: 1000,
callback: function(){
this.scene.start("PlayGame");
},
callbackScope: this
});
}
}
function resize() {
let canvas = document.querySelector("canvas");
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
let windowRatio = windowWidth / windowHeight;
let gameRatio = game.config.width / game.config.height;
if(windowRatio < gameRatio){
canvas.style.width = windowWidth + "px";
canvas.style.height = (windowWidth / gameRatio) + "px";
}
else{
canvas.style.width = (windowHeight * gameRatio) + "px";
canvas.style.height = windowHeight + "px";
}
}
Which level did you reach? Give me feedback or download the source code and play with it.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.