Talking about Yeah Bunny game, Game development, HTML5, Javascript and Phaser.
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.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.