Build a HTML5 game like “Fuse Ballz” with Phaser using Matter physics and changing the way collisions work

Read all posts about "" game

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 &amp;&amp; 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.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers