Do you like my tutorials?

Then consider supporting me on Ko-fi

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.