Talking about Bouncy Light game, 3D, Game development, HTML5, Javascript and Phaser.
Did you ever dream a Phaser version capable of rendering in 3D? There’s a official library, available to Phaser Patreon bakers, which helps you to add the third dimension to your games by integrating three.js in a quick and intuitive way.
If you plan to use what you are going to see in this example, please consider supporting Phaser on Patreon.
By the way, most loyal readers should already have seen an example on this blog when I wrote how to fake a 3D effect whit Phaser 3D.
This time we’ll see how to turn a plain and flat 2D game into a shiny 3D game using Phaser 3D without changing any single line of the original 2D prototype.
We are going to convert the first step of Bouncy Light prototype, which is an easy and quick game which uses Arcade physics.
The trick is to keep the original game mechanics, and use three.js and Phaser 3D library only to draw a 3D scene which is updated according to what happens in its 2D counterpart.
It’s not a real 3D world, but a 3D representation of what is happening in the 2D world.
Look:
Just click/tap and hold to make ball move, release to stop it.
Obviously the ball does not move, it’s the entire environment which moves towards the ball, but most of all there is a 3D representation of the 2D world.
I left both worlds to let you see how 2D world affects 3D world, but to have a complete 3D game you just have to hide the 2D sprites.
This way you will be able to give a new twist to your existing simple 2D games, have a look at the completely commented source code:
let game;
let gameOptions = {
// ball gravity
ballGravity: 1200,
// bounce velocity when the ball hits a platform
bounceVelocity: 800,
// ball start x position, 0 = left; 1 = right
ballStartXPosition: 0.2,
// amount of platforms to be created and recycled
platformAmount: 10,
// platform speed, in pixels per second
platformSpeed: 650,
// min and max distance range between platforms
platformDistanceRange: [250, 450],
// min and max platform height, , 0 = top of the screen; 1 = bottom of the screen
platformHeightRange: [0.5, 0.8],
// min and max platform length
platformLengthRange: [40, 160],
// local storage name where to save best scores
localStorageName: "bestballscore3d",
// game scale between 2D and 3D
gameScale: 0.1
}
window.onload = function() {
let gameConfig = {
type: Phaser.AUTO,
backgroundColor:0x87ceeb,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
parent: "thegame",
width: 750,
height: 500
},
physics: {
default: "arcade"
},
scene: playGame
}
game = new Phaser.Game(gameConfig);
window.focus();
}
class playGame extends Phaser.Scene{
constructor(){
super("PlayGame");
}
preload(){
this.load.image("ground", "ground.png");
this.load.image("ball", "ball.png");
}
create() {
// method to create the 3D world
this.create3DWorld();
// method to add the 2D ball
this.add2DBall();
// method to add the 3D ball
this.add3DBall();
// method to add platforms
this.addPlatforms();
// method to add score
this.addScore();
// method to add game listeners
this.addListeners();
}
// method to create the 3D world
create3DWorld(){
// 3D world creation
this.phaser3D = new Phaser3D(this, {
// camera fov, learn more at https://threejsfundamentals.org/threejs/lessons/threejs-cameras.html
fov: 25,
// camera x, y and z position
x: 50,
y: 110,
z: 110
});
// point the camera at a x, y, z coordinate
this.phaser3D.camera.lookAt(50, 20, 0);
// enable shadows
this.phaser3D.enableShadows();
// enable gamma correction, learn more at https://en.wikipedia.org/wiki/Gamma_correction
this.phaser3D.enableGamma();
// add a soft, white ambient light
this.phaser3D.add.ambientLight({
color: 0xffffff,
intensity: 0.4
});
// add a bright, white spotlight, learn more at https://threejs.org/docs/#api/en/lights/SpotLight
let spotlight = this.phaser3D.add.spotLight({
color: 0xffffff,
intensity: 1,
angle: 0.4,
decay: 0.1,
x: 0,
y: 250,
z: 80
});
// enable the spotlight to cast shadow
this.phaser3D.setShadow(spotlight);
}
// method to create the 2D ball
add2DBall(){
// this is just the good old Arcade physics body creation
this.ball = this.physics.add.sprite(game.config.width * gameOptions.ballStartXPosition, 0, "ball");
// set ball gravity
this.ball.body.gravity.y = gameOptions.ballGravity;
// we are only checking for collisions on the bottom of the ball
this.ball.body.checkCollision.down = true;
this.ball.body.checkCollision.up = false;
this.ball.body.checkCollision.left = false;
this.ball.body.checkCollision.right = false;
// modify a bit the collision shape to make the game more kind with players
this.ball.setSize(30, 50, true);
}
// method to create the 3D ball
add3DBall(){
// create a red sphere
this.ball3D = this.phaser3D.add.sphere({
radius: this.ball.displayWidth / 2 * gameOptions.gameScale,
widthSegments: 64,
heightSegments: 64,
color: 0xff0000,
x: 0,
y: 0,
z: 0
});
// set the ball to cast a shadow
this.phaser3D.castShadow(this.ball3D);
}
// method to add platforms
addPlatforms(){
// creation of a physics group containing all platforms
this.platformGroup = this.physics.add.group();
// let's proceed with the creation
for(let i = 0; i < gameOptions.platformAmount; i++){
this.add2DPlatform();
}
}
// method to set a random platform X position
setPlatformX(){
return this.getRightmostPlatform() + Phaser.Math.Between(gameOptions.platformDistanceRange[0], gameOptions.platformDistanceRange[1]);
}
// method to set a random platform Y position
setPlatformY(){
return Phaser.Math.Between(game.config.height * gameOptions.platformHeightRange[0], game.config.height * gameOptions.platformHeightRange[1]);
}
add2DPlatform(){
// st platform X position
let platformX = (this.getRightmostPlatform() == 0) ? this.ball.x : this.setPlatformX();
// create 2D platform
let platform = this.platformGroup.create(platformX, this.setPlatformY(), "ground");
// set platform registration point
platform.setOrigin(0.5, 1);
// platform won't move no matter how many hits it gets
platform.setImmovable(true);
// set a random platform width
platform.displayWidth = Phaser.Math.Between(gameOptions.platformLengthRange[0], gameOptions.platformLengthRange[1]);
// add 3D platform as a 2D platform property
platform.platform3D = this.add3DPlatform(platform);
}
// method to add a 3D platform, the argument is the 2D platform
add3DPlatform(platform2D){
// create a green box
let platform3D = this.phaser3D.add.box({
width: 1,
height: 50,
depth: 20,
color: 0x40ff80,
x: 0,
y: (game.config.height - platform2D.y) * gameOptions.gameScale - 25,
z: 0
});
// platform will receive shadows
this.phaser3D.receiveShadow(platform3D);
// scale the 3D platform to make it match 2D platform size
platform3D.scale.x = platform2D.displayWidth * gameOptions.gameScale;
return platform3D;
}
// method to add the score, just a dynamic text
addScore(){
this.score = 0;
this.topScore = localStorage.getItem(gameOptions.localStorageName) == null ? 0 : localStorage.getItem(gameOptions.localStorageName);
this.scoreText = this.add.text(10, 10, 0);
}
// method to update the score
updateScore(inc){
this.score += inc;
this.scoreText.text = "Score: " + this.score + "\nBest: " + this.topScore;
}
// listeners to make platforms move and stop
addListeners(){
this.input.on("pointerdown", function(){
this.platformGroup.setVelocityX(-gameOptions.platformSpeed);
}, this);
this.input.on("pointerup", function(){
this.platformGroup.setVelocityX(0);
}, this);
}
// method to get the rightmost platform
getRightmostPlatform(){
let rightmostPlatform = 0;
this.platformGroup.getChildren().forEach(function(platform){
rightmostPlatform = Math.max(rightmostPlatform, platform.x);
});
return rightmostPlatform;
}
// method to be executed at each frame
update(){
// collision management ball Vs platforms
this.physics.world.collide(this.platformGroup, this.ball, function(){
// bounce back the ball
this.ball.body.velocity.y = -gameOptions.bounceVelocity;
}, null, this);
// loop through all platforms
this.platformGroup.getChildren().forEach(function(platform){
// if the platform leaves the screen to the left...
if(platform.getBounds().right < -100){
// increase the score
this.updateScore(1);
// recycle the platform moving it to a new position
platform.x = this.setPlatformX();
platform.y = this.setPlatformY();
// set new platform width
platform.displayWidth = Phaser.Math.Between(gameOptions.platformLengthRange[0], gameOptions.platformLengthRange[1]);
// adjust 3D platform scale and y position
platform.platform3D.scale.x = platform.displayWidth * gameOptions.gameScale;
platform.platform3D.position.y = (game.config.height - platform.y) * gameOptions.gameScale - 25;
}
// adjust 3D platform x position
platform.platform3D.position.x = platform.x * gameOptions.gameScale;
}, this);
// if 2D ball falls down the screen...
if(this.ball.y > game.config.height){
// manage best score
localStorage.setItem(gameOptions.localStorageName, Math.max(this.score, this.topScore));
// restart the game
this.scene.start("PlayGame");
}
// adjust 3D ball position
this.ball3D.position.y = (game.config.height - this.ball.y) * gameOptions.gameScale;
this.ball3D.position.x = this.ball.x * gameOptions.gameScale;
}
}
If you look at the original source code, you will notice the 2D management did not change, and I just added the routine to move 3D bodies to match 2D bodies movements.
Do you plan to convert some of your games from 2D to 3D? Then consider becoming a Phaser Patron, you will get Phaser 3D, a lot of examples and you will also support future Phaser development. Download the entire project to see how easy it was for me to convert the game from 2D to 3D.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.