Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Fuse Ballz game, Game development, HTML5, Javascript and Phaser.

I recently played Fuse Ballz – available for both iOS and Android devices – by Ketchapp and I have to say that despite the easy and quick concept which is a Ketchapp trademark, it would be quite a challenge to prototype it with Phaser, mainly for two reasons: first, it’s a 3D game, and second the physics is not realistic, as balls bounce in a weird way.

The aim of the game is tof use balls together to get bigger balls, and blow them up when they grow too big.

The game starts with some colored balls on the stage, and the player controls another colored ball at the very bottom of the stage.

When two balls of the same color touch, they merge into a bigger ball. When a ball gets too big, it explodes leaving two little balls of the same color.

Have a look at the game:

Aim the ball at the bottom by moving the mouse/finger, click or tap to fire the ball.

Try to merge as many balls of the same color as possible, to watch them explode.

You will notice the physics is a bit weird, as balls react to collision in a strange – but useful for the sake of the gameplay – way.

The game is not in 3D, but if you play the original game, you will notice it’s just a 3D representation of a 2D environment, and last week I showed you how to build fake 3D games, so it won’t be a problem to add a third dimension later on.

The challenging part was to make balls grow – for some reason Matter messes up everything if you just change the radius of a body – and handle bounces by saving the velocities at the moment of the collision, perform the collision then apply modified velocities to give balls the strange movements you can see in the original game.

Unfortunately Matter does not handle continuous collision detection – aka “bullet bodies” if you are used to Box2D – so I couldn’t fire the ball at a high speed.

Moreover I did not like how Matter manages friction, I’ll have to find another way to make balls move in a more similar way to the original game.

Last but not least, sometimes balls stop exactly where new player ball spawns, and this does not happen in the original game so I’ll have to prevent it somehow.

Uh, and there isn’t object pooling. But hey, it works somehow.

I have to say, Matter is not the best physics engine to prototype this game, but believe me Arcade physics was even worse, since circle Vs circle collision does not work well when you have many balls on the stage.

Anyway, this is the source code, completely uncommented because it’s just a prototype, but it can help you a lot to see how Matter handles collisions and how you can change the way colliding bodies react:

let game;
let gameOptions = {
    startingBalls: 5,
    ballColors: 6,
    ballSpeed: 60,
    growFactor: 25,
    explode: 4
}
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
        },
        physics: {
            default: "matter",
            matter: {
                gravity: {
                    y: 0
                },
                debug: true
            }
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    preload(){
        this.load.image("arrow", "arrow.png");
        this.load.spritesheet("balls", "balls.png", {
            frameWidth: 100,
            frameHeight: 100
        });
    }
    create(){
        this.ballsToReverse = [];
        this.angle = Math.PI / 2 * -1;
        for(let i = 0; i < gameOptions.startingBalls; i++){
            let posX = Phaser.Math.Between(50, game.config.width - 50);
            let posY = Phaser.Math.Between(50, game.config.height / 2);
            let frame = Phaser.Math.Between(0, gameOptions.ballColors - 1);
            this.addBall(posX, posY, frame, false);
        }
        this.addWall(game.config.width / 2, -10, game.config.width, 20);
        this.addWall(game.config.width / 2, game.config.height + 10, game.config.width, 20);
        this.addWall(-10, game.config.height / 2, 20, game.config.height);
        this.addWall(game.config.width + 10, game.config.height / 2, 20, game.config.height);
        this.playerBall = this.add.sprite(game.config.width / 2, game.config.height - 100, "balls");
        this.arrow = this.add.sprite(this.playerBall.x, this.playerBall.y - 150, "arrow");
        this.setBallAndArrow();
        this.input.on("pointerdown", this.launchBall, this);
        this.input.on("pointermove", this.moveArrow, this);
        this.matter.world.on("collisionstart", this.handleCollisionStart, this);
        this.matter.world.on("afterupdate", this.reverseMovement, this);
    }
    addWall(posX, posY, width, height){
        let wall = this.matter.add.rectangle(posX, posY, width, height, {
            isStatic: true,
        });
        wall.restitution = 1;
    }
    addBall(posX, posY, frame, isMoving){
        let ball = this.matter.add.sprite(posX, posY, "balls");
        ball.grown = 0;
        ball.setFrame(frame);
        ball.setBounce(0.4);
        ball.setCircle(50);
        ball.body.frictionAir = 0.02;
        if(isMoving){
            ball.setVelocity(gameOptions.ballSpeed * Math.cos(this.angle), gameOptions.ballSpeed * Math.sin(this.angle));
        }
    }
    setBallAndArrow(){
        this.playerBallMoving = false;
        this.playerBall.setVisible(true);
        this.playerBall.setFrame(Phaser.Math.Between(0, gameOptions.ballColors - 1));
        this.arrow.setVisible(true);
        this.arrow.x = this.playerBall.x;
        this.arrow.y = this.playerBall.y - 150;
        this.arrow.angle = -90;
        this.angle = Math.PI / 2 * -1;
    }
    moveArrow(pointer){
        this.angle = Phaser.Math.Clamp(Math.abs(Phaser.Math.Angle.Between(this.playerBall.x, this.playerBall.y, pointer.x, pointer.y)), 0.5, Math.PI - 0.5) * -1;
        this.arrow.x = this.playerBall.x + 150 * Math.cos(this.angle);
        this.arrow.y = this.playerBall.y + 150 * Math.sin(this.angle);
        this.arrow.rotation = this.angle;
    }
    reverseMovement(){
        while(this.ballsToReverse.length > 0){
            let ball = this.ballsToReverse.shift();
            ball.setVelocity(ball.mirrorMovement.body.velocity.x * -0.25, ball.mirrorMovement.body.velocity.y * -0.25);
        }
    }
    handleCollisionStart(event, bodyA, bodyB){
        if(bodyA.gameObject && bodyB.gameObject){
            if(bodyA.gameObject.frame.name == bodyB.gameObject.frame.name){
                if(bodyA.speed < bodyB.speed){
                    this.handleInclusion(bodyA.gameObject, bodyB.gameObject);
                }
                else{
                    this.handleInclusion(bodyB.gameObject, bodyA.gameObject);
                }
            }
            else{
                if(bodyA.speed < bodyB.speed){
                    this.handleBounce(bodyA.gameObject, bodyB.gameObject);
                }
                else{
                    this.handleBounce(bodyB.gameObject, bodyA.gameObject);
                }
            }
        }
    }
    handleBounce(bodyA, bodyB){
        bodyB.mirrorMovement = bodyA;
        this.ballsToReverse.push(bodyB);
    }
    handleInclusion(bodyA, bodyB){
        bodyA.grown ++;
        if(bodyA.grown == gameOptions.explode){
            for(let i = 0; i < 2; i++){
                let posX = Phaser.Math.Between(50, game.config.width - 50);
                let posY = Phaser.Math.Between(50, game.config.height / 2);
                let frame = bodyA.frame.name;
                this.addBall(posX, posY, frame, false);
            }
            bodyA.destroy();
        }
        else{
            let saveX = bodyA.x;
            let saveY = bodyA.y;
            bodyA.displayWidth = Math.max(bodyA.displayWidth, bodyB.displayWidth) + gameOptions.growFactor;
            bodyA.displayHeight = Math.max(bodyA.displayHeight, bodyB.displayHeight) + gameOptions.growFactor;
            bodyA.x = saveX;
            bodyA.y = saveY;
            bodyA.setVelocity(bodyB.body.velocity.x / 2, bodyB.body.velocity.y / 2);
        }
        bodyB.destroy();
    }
    launchBall(){
        if(!this.playerBallMoving){
            this.playerBallMoving = true;
            this.playerBall.setVisible(false);
            this.arrow.setVisible(false);
            this.addBall(this.playerBall.x, this.playerBall.y, this.playerBall.frame.name, true)
            this.time.addEvent({
                delay: 3000,
                callbackScope: this,
                callback: this.setBallAndArrow
            });
        }
    }
}

Although raw and with a lot of missing features, it’s interesting to see how this 160 lines script manages to create the feeling of the original game which, I repeat, is not that easy to prototype.

Next time I’ll add more features, maybe using another physics engine, meanwhile 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.