HTML5 platformer prototype inspired by iOS hit “Yeah Bunny” thanks to Phaser and Arcade physics now featuring WATER

Read all posts about "" game

Let’s be honest: the most loved/hated levels of Super Mario Bros franchise feature water. All platformers should featured at least a couple of levels with water.

And my “Yeah Bunny” prototype now supports water tiles. Now we have an
autorunner with jump, double jump, wall jump. wall slide, stop tile, trampoline tile and water.

Once the character is inside the water, physics changes and you will be able to swim.

This is the level I built with Tiled Map Editor:

Black tiles are walls, red tiles are “stop” tiles, yellow tiles are trampoline tiles and blue tiles are water.

Look at the game:

Click or tap to jump, double jump or wall jump.

The problem with Arcade physics is the engine does not support buoyancy, so we have to rewrite the way gravity and velocity are managed once the player hits the water.

Unlike other tiles, water tiles aren’t checked for collision, and we only know if the player is in the water with getTileAtWorldXY method.

Once in the water, physics changes, and that’s all. Look at the completely commented source code:

let game;
let gameOptions = {

    // hero gravity
    heroGravity: 900,

    // gravity when underwater
    underwaterGravity: 30,

    // hero friction when on wall
    heroGrip: 100,

    // hero horizontal speed
    heroSpeed: 200,

    // hero horizontal speed when underwater
    underwaterSpeed: 50,

    // hero jump force
    heroJump: 400,

    // hero jump force when underwater
    underwaterJump: 300,

    // hero double jump force
    heroDoubleJump: 300,

    // trampoline tile impulse
    trampolineImpulse: 500
}

// constants to make some numbers more readable
const STOP_TILE = 2;
const TRAMPOLINE_TILE = 3;
const WATER_TILE = 4;

window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        backgroundColor: 0x444444,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 640,
            height: 480
        },
        physics: {
            default: "arcade",
            arcade: {
                gravity: {
                    y: 0
                }
            }
        },
       scene: [preloadGame, playGame]
    }
    game = new Phaser.Game(gameConfig);
}
class preloadGame extends Phaser.Scene{
    constructor(){
        super("PreloadGame");
    }
    preload(){
        this.load.tilemapTiledJSON("level", "level.json");
        this.load.image("tile", "tile.png");
        this.load.image("hero", "hero.png");
    }
    create(){
        this.scene.start("PlayGame");
    }
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    create(){

        // creation of "level" tilemap
        this.map = this.make.tilemap({
            key: "level"
        });

        // add tiles to tilemap
        let tile = this.map.addTilesetImage("tileset01", "tile");

        // which layers should we render? That's right, "layer01"
        this.layer = this.map.createStaticLayer("layer01", tile);

        // which tiles will collide? Tiles from 1 to 3. Water won't be checked for collisions
        this.layer.setCollisionBetween(1, 3);

        // add the hero sprite and enable arcade physics for the hero
        this.hero = this.physics.add.sprite(260, 376, "hero");

        // set hero horizontal speed
        this.hero.body.velocity.x = gameOptions.heroSpeed;

        // hero can jump at the moment
        this.canJump = true;

        // hero cannot double jump
        this.canDoubleJump = false;

        // hero is not on the wall
        this.onWall = false;

        // hero is not underwater
        this.isUnderwater = false;

        // hero is on land
        this.isOnLand = true;

        // listener for hero input
        this.input.on("pointerdown", this.handleJump, this);

        // set workd bounds to allow camera to follow the hero
        this.cameras.main.setBounds(0, 0, 1920, 1440);

        // make the camera follow the hero
        this.cameras.main.startFollow(this.hero);
    }

    // method to make the hero jump
    handleJump(){

        // is the hero underwater?
        if(this.isUnderwater){

            // in this case, the hero can jump (let's say swim up) only if not already swimming up
            if(this.hero.body.velocity.y >= 0){

                // apply swim force
                this.hero.body.velocity.y = -gameOptions.underwaterJump;
            }
        }

        // hero is not underwater
        else{

            // hero can jump when:
            // canJump is true AND hero is on the ground (blocked.down)
            // OR
            // hero is on the wall
            if((this.canJump && this.hero.body.blocked.down) || this.onWall){

                // apply jump force
                this.hero.body.velocity.y = -gameOptions.heroJump;

                // is the hero on a wall?
                if(this.onWall){

                    // change horizontal velocity too. This way the hero will jump off the wall
                    this.setHeroXVelocity(true);
                }

                // hero can't jump anymore
                this.canJump = false;

                // hero is not on the wall anymore
                this.onWall = false;

                // hero can now double jump
                this.canDoubleJump = true;
            }
            else{

                // can the hero double jump?
                if(this.canDoubleJump){

                    // hero can't double jump anymore
                    this.canDoubleJump = false;

                    // apply double jump force
                    this.hero.body.velocity.y = -gameOptions.heroDoubleJump;
                }
            }
        }
    }

    // method to be executed at each frame
    update(){

        // check which tile the hero is on
        let tile = this.map.getTileAtWorldXY(this.hero.x, this.hero.y);

        // hero is underwater when over a water tile
        this.isUnderwater = tile != null && tile.index == WATER_TILE;

        // if the hero is underwater...
        if(this.isUnderwater){

            // if the hero is swimming up...
            if(this.hero.body.velocity.y < 0){

                // ... reduce swimming force
                this.hero.body.velocity.y *= 0.9;
            }

            // if the hero is drowning ...
            if(this.hero.body.velocity.y > 0){

                // ... reduce drowning force
                this.hero.body.velocity.y *= 0.97;
            }

            // if the hero is also on the land, this means the hero jumped in the water right now
            if(this.isOnLand){

                // reduce hero vertical velocity
                this.hero.body.velocity.y *= 0.5;

                // hero is no more on land
                this.isOnLand = false;
            }
        }

        // if the hero is not underwater...
        else{

            // the hero is on land
            this.isOnLand = true;
        }

        // apply the proper gravity according to hero being on land or underwater
        this.hero.body.gravity.y = this.isUnderwater ? gameOptions.underwaterGravity : gameOptions.heroGravity;

        // hero is not on wall
        this.onWall = false;

        // method to set hero velocity. Arguments are:
        // * move toward default direction
        // * should hero stop?
        // * is the hero underwater?
        this.setHeroXVelocity(true, false, this.isUnderwater);

        // handle collision between hero and tiles
        this.physics.world.collide(this.hero, this.layer, function(hero, layer){

            // should the hero stop?
            let shouldStop = false;

            // some temporary variables to determine if the hero is blocked only once
            let blockedDown = hero.body.blocked.down;
            let blockedLeft = hero.body.blocked.left;
            let blockedRight = hero.body.blocked.right;

            // if the hero hits something, no double jump is allowed
            this.canDoubleJump = false;

            // hero on the ground
            if(blockedDown){

                // hero can jump
                this.canJump = true;

                // if we are on tile 2 (stop tile)...
                if(layer.index == STOP_TILE){

                    // hero should stop
                    shouldStop = true;
                }

                // if we are on a trampoline and previous hero vertical velocity was greater than zero...
                if(layer.index == TRAMPOLINE_TILE && this.previousYVelocity > 0){

                    // trampoline jump!
                    hero.body.velocity.y = -gameOptions.trampolineImpulse;

                    // hero can double jump
                    this.canDoubleJump = true
                }

            }

            // hero on the ground and touching a wall on the right
            if(blockedRight){

                // horizontal flip hero sprite
                hero.flipX = true;
            }

            // hero on the ground and touching a wall on the right
            if(blockedLeft){

                // default orientation of hero sprite
                hero.flipX = false;
            }

            // hero NOT on the ground and touching a wall but not underwater
            if((blockedRight || blockedLeft) && !blockedDown && !this.isUnderwater){

                // hero on a wall
                hero.scene.onWall = true;

                // remove gravity
                hero.body.gravity.y = 0;

                // set new y velocity
                hero.body.velocity.y = gameOptions.heroGrip;
            }

            // adjust hero speed according to the direction the hero is moving
            this.setHeroXVelocity(!this.onWall || blockedDown, shouldStop, this.isUnderwater);
        }, null, this);

        // save current vertical velocity
        this.previousYVelocity = this.hero.body.velocity.y;

    }

    // method to set hero horizontal velocity
    setHeroXVelocity(defaultDirection, stopIt, underwater){

        // should the hero stop?
        if(stopIt){

            // ... then stop!
            this.hero.body.velocity.x = 0;
        }
        else{

            // set hero speed also checking if the hero is underwater or whether the hero looks left or right
            this.hero.body.velocity.x = (underwater ? gameOptions.underwaterSpeed : gameOptions.heroSpeed) * (this.hero.flipX ? -1 : 1) * (defaultDirection ? 1 : -1);
        }
    }
}

Water adds a lot of opportunities in level design, and next time I will try to prototype a level of an actual game using water in its levels, 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

215 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
// Stairs
// 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