Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Survival Horror game, Game development, HTML5, Javascript and Phaser.

As promised, here we go with the full tutorial to create the survival horror engine I showed you in the post A quick HTML5 survival horror prototype made with Phaser.

The concept is the same as already explained in the original AS2 post Create a survival horror game in Flash tutorial and its AS3 version: Create a survival horror game in Flash – AS3 version.

With Phaser everything is built upon bitmapData and its getPixel32 method.

Let’s split the prototype in 6 steps

1 – PLACING GRAPHIC ASSETS ON STAGE

Here we just place the floor texture, the player (a green square) and the walls which is a PNG image with transparency. Using a black color to draw walls while leaving empty space transparent gives the best results.

window.onload = function() {
	
	var game = new Phaser.Game(640,480,Phaser.CANVAS,"",{preload:onPreload, create:onCreate, update:onUpdate});                
	
	var player;
	var wallsBitmap;
	var floor;

	function onPreload() {
		game.load.image("floor","floor.png");
		game.load.image("walls","walls.png");
		game.load.image("player","player.png");
	}

	function goFullScreen(){
		game.scale.pageAlignHorizontally = true;
		game.scale.pageAlignVertically = true;
		game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
		game.scale.setScreenSize(true);
	}

	function onCreate() {
		goFullScreen();
		floor = game.add.sprite(0,0,"floor");
		wallsBitmap = game.make.bitmapData(640,480);
		wallsBitmap.draw("walls");
		wallsBitmap.update();
		game.add.sprite(0,0,wallsBitmap);
		player = game.add.sprite(80,80,"player");
		player.anchor.setTo(0.5,0.5);
	}

	function onUpdate() {
		// do something
	}	
}

And this is the result:

Don’t try to interact with it because it’s only a canvas with three images.

2 – MOVING THE CHARACTER WITH ARROW KEYS

As you will see in the highlighted lines, I am creating a cursor key listener and assign each key press an horizontal and vertical speed. If one and only one arrow key is pressed, the player moves.

window.onload = function() {
	
	var game = new Phaser.Game(640,480,Phaser.CANVAS,"",{preload:onPreload, create:onCreate, update:onUpdate});                
	
	var player;
	var wallsBitmap;
	var floor;

	function onPreload() {
		game.load.image("floor","floor.png");
		game.load.image("walls","walls.png");
		game.load.image("player","player.png");
	}

	function goFullScreen(){
		game.scale.pageAlignHorizontally = true;
		game.scale.pageAlignVertically = true;
		game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
		game.scale.setScreenSize(true);
	}

	function onCreate() {
		goFullScreen();
		floor = game.add.sprite(0,0,"floor");
		wallsBitmap = game.make.bitmapData(640,480);
		wallsBitmap.draw("walls");
		wallsBitmap.update();
		game.add.sprite(0,0,wallsBitmap);
		player = game.add.sprite(80,80,"player");
		player.anchor.setTo(0.5,0.5);
		cursors = game.input.keyboard.createCursorKeys();
	}

	function onUpdate() {
		var xSpeed = 0;
		var ySpeed = 0;
		if(cursors.up.isDown){
			ySpeed -=1;
		}
		if(cursors.down.isDown){
			ySpeed +=1;
		}
		if(cursors.left.isDown){
			xSpeed -=1;
		}
		if(cursors.right.isDown){
			xSpeed +=1;
		}
		if(Math.abs(xSpeed)+Math.abs(ySpeed)<2 && Math.abs(xSpeed)+Math.abs(ySpeed)>0){
			player.x+=xSpeed;
			player.y+=ySpeed;
		}
	}	
}

Here is the result: try to move the player with arrow keys.

Unfortunately, walls does not stop the player, so it’s time to add the first hit test using getPixel32 method

3 – PREVENTING THE PLAYER TO WALK OVER WALLS

To prevent the player to walk over walls, we simply check if the player is about to hit the wall with one of its vertices. We simply need to check the color of walls image at green square vertices coordinates.

window.onload = function() {
	
	var game = new Phaser.Game(640,480,Phaser.CANVAS,"",{preload:onPreload, create:onCreate, update:onUpdate});                
	
	var player;
	var wallsBitmap;
	var floor;

	function onPreload() {
		game.load.image("floor","floor.png");
		game.load.image("walls","walls.png");
		game.load.image("player","player.png");
	}

	function goFullScreen(){
		game.scale.pageAlignHorizontally = true;
		game.scale.pageAlignVertically = true;
		game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
		game.scale.setScreenSize(true);
	}

	function onCreate() {
		goFullScreen();
		floor = game.add.sprite(0,0,"floor");
		wallsBitmap = game.make.bitmapData(640,480);
		wallsBitmap.draw("walls");
		wallsBitmap.update();
		game.add.sprite(0,0,wallsBitmap);
		player = game.add.sprite(80,80,"player");
		player.anchor.setTo(0.5,0.5);
		cursors = game.input.keyboard.createCursorKeys();
	}

	function onUpdate() {
		var xSpeed = 0;
		var ySpeed = 0;
		if(cursors.up.isDown){
			ySpeed -=1;
		}
		if(cursors.down.isDown){
			ySpeed +=1;
		}
		if(cursors.left.isDown){
			xSpeed -=1;
		}
		if(cursors.right.isDown){
			xSpeed +=1;
		}
		if(Math.abs(xSpeed)+Math.abs(ySpeed)<2 && Math.abs(xSpeed)+Math.abs(ySpeed)>0){
			var color = wallsBitmap.getPixel32(player.x+xSpeed+player.width/2,player.y+ySpeed+player.height/2);
			color+= wallsBitmap.getPixel32(player.x+xSpeed-player.width/2,player.y+ySpeed+player.height/2);
			color+=wallsBitmap.getPixel32(player.x+xSpeed-player.width/2,player.y+ySpeed-player.height/2)
			color+=wallsBitmap.getPixel32(player.x+xSpeed+player.width/2,player.y+ySpeed-player.height/2)
			if(color==0){
				player.x+=xSpeed;
				player.y+=ySpeed;
			}
		}
	}	
}

And here it is the result: try to walk over the walls

You can’t. There are several, more optimized ways to do this, but since the main topic of the tutorial is getPixel32 method, I used it for player movement too.

From now on, we’ll be focus on the torchlight.

4 – WALLS VERSUS RAYCAST

This is the concept: starting from player origin, we emit a series of rays at a given angle interval, checking at every pixel if a ray hits the wall and stopping it accordingly. This will represent the torchlight.

window.onload = function() {
	
	var game = new Phaser.Game(640,480,Phaser.CANVAS,"",{preload:onPreload, create:onCreate, update:onUpdate});                
	
	var player;
	var wallsBitmap;
	var floor;
	var lightAngle = Math.PI/4;
	var numberOfRays = 20;
	var rayLength = 100;

	function onPreload() {
		game.load.image("floor","floor.png");
		game.load.image("walls","walls.png");
		game.load.image("player","player.png");
	}

	function goFullScreen(){
		game.scale.pageAlignHorizontally = true;
		game.scale.pageAlignVertically = true;
		game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
		game.scale.setScreenSize(true);
	}

	function onCreate() {
		goFullScreen();
		floor = game.add.sprite(0,0,"floor");
		wallsBitmap = game.make.bitmapData(640,480);
		wallsBitmap.draw("walls");
		wallsBitmap.update();
		game.add.sprite(0,0,wallsBitmap);
		player = game.add.sprite(80,80,"player");
		player.anchor.setTo(0.5,0.5);
		cursors = game.input.keyboard.createCursorKeys();
		maskGraphics = this.game.add.graphics(0, 0);
	}

	function onUpdate() {
		var xSpeed = 0;
		var ySpeed = 0;
		if(cursors.up.isDown){
			ySpeed -=1;
		}
		if(cursors.down.isDown){
			ySpeed +=1;
		}
		if(cursors.left.isDown){
			xSpeed -=1;
		}
		if(cursors.right.isDown){
			xSpeed +=1;
		}
		if(Math.abs(xSpeed)+Math.abs(ySpeed)<2 && Math.abs(xSpeed)+Math.abs(ySpeed)>0){
			var color = wallsBitmap.getPixel32(player.x+xSpeed+player.width/2,player.y+ySpeed+player.height/2);
			color+= wallsBitmap.getPixel32(player.x+xSpeed-player.width/2,player.y+ySpeed+player.height/2);
			color+=wallsBitmap.getPixel32(player.x+xSpeed-player.width/2,player.y+ySpeed-player.height/2)
			color+=wallsBitmap.getPixel32(player.x+xSpeed+player.width/2,player.y+ySpeed-player.height/2)
			if(color==0){
				player.x+=xSpeed;
				player.y+=ySpeed;
			}		
		}
		var mouseAngle = Math.atan2(player.y-game.input.y,player.x-game.input.x);
		maskGraphics.clear();
		maskGraphics.lineStyle(2, 0xffffff, 1);
		for(var i = 0; i<numberOfRays; i++){
			maskGraphics.moveTo(player.x,player.y);	
			var rayAngle = mouseAngle-(lightAngle/2)+(lightAngle/numberOfRays)*i
			var lastX = player.x;
			var lastY = player.y;
			for(var j= 1; j<=rayLength;j+=1){
          		var landingX = Math.round(player.x-(2*j)*Math.cos(rayAngle));
          		var landingY = Math.round(player.y-(2*j)*Math.sin(rayAngle));
          		if(wallsBitmap.getPixel32(landingX,landingY)==0){
					lastX = landingX;
					lastY = landingY;	
				}
				else{
					maskGraphics.lineTo(lastX,lastY);
					break;
				}
			}
			maskGraphics.lineTo(lastX,lastY);
		}	
	}	
}

The result is here. Try to move the torchlight with the mouse.

Now it’s time to turn a series of rays into a filled area

5 – RAYCAST TO AREA

In this fifth part I only made some changes to turn a series of rays into an area, nothing interesting from a programming point of view

window.onload = function() {
	
	var game = new Phaser.Game(640,480,Phaser.CANVAS,"",{preload:onPreload, create:onCreate, update:onUpdate});                
	
	var player;
	var wallsBitmap;
	var floor;
	var lightAngle = Math.PI/4;
	var numberOfRays = 20;
	var rayLength = 100;

	function onPreload() {
		game.load.image("floor","floor.png");
		game.load.image("walls","walls.png");
		game.load.image("player","player.png");
	}

	function goFullScreen(){
		game.scale.pageAlignHorizontally = true;
		game.scale.pageAlignVertically = true;
		game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
		game.scale.setScreenSize(true);
	}

	function onCreate() {
		goFullScreen();
		floor = game.add.sprite(0,0,"floor");
		wallsBitmap = game.make.bitmapData(640,480);
		wallsBitmap.draw("walls");
		wallsBitmap.update();
		game.add.sprite(0,0,wallsBitmap);
		player = game.add.sprite(80,80,"player");
		player.anchor.setTo(0.5,0.5);
		cursors = game.input.keyboard.createCursorKeys();
		maskGraphics = this.game.add.graphics(0, 0);
	}

	function onUpdate() {
		var xSpeed = 0;
		var ySpeed = 0;
		if(cursors.up.isDown){
			ySpeed -=1;
		}
		if(cursors.down.isDown){
			ySpeed +=1;
		}
		if(cursors.left.isDown){
			xSpeed -=1;
		}
		if(cursors.right.isDown){
			xSpeed +=1;
		}
		if(Math.abs(xSpeed)+Math.abs(ySpeed)<2 && Math.abs(xSpeed)+Math.abs(ySpeed)>0){
			var color = wallsBitmap.getPixel32(player.x+xSpeed+player.width/2,player.y+ySpeed+player.height/2);
			color+= wallsBitmap.getPixel32(player.x+xSpeed-player.width/2,player.y+ySpeed+player.height/2);
			color+=wallsBitmap.getPixel32(player.x+xSpeed-player.width/2,player.y+ySpeed-player.height/2)
			color+=wallsBitmap.getPixel32(player.x+xSpeed+player.width/2,player.y+ySpeed-player.height/2)
			if(color==0){
				player.x+=xSpeed;
				player.y+=ySpeed;
			}		
		}
		var mouseAngle = Math.atan2(player.y-game.input.y,player.x-game.input.x);
		maskGraphics.clear();
		maskGraphics.lineStyle(2, 0xffffff, 1);
		maskGraphics.beginFill(0xffff00);
		maskGraphics.moveTo(player.x,player.y);	
		for(var i = 0; i<numberOfRays; i++){	
			var rayAngle = mouseAngle-(lightAngle/2)+(lightAngle/numberOfRays)*i
			var lastX = player.x;
			var lastY = player.y;
			for(var j= 1; j<=rayLength;j+=1){
          		var landingX = Math.round(player.x-(2*j)*Math.cos(rayAngle));
          		var landingY = Math.round(player.y-(2*j)*Math.sin(rayAngle));
          		if(wallsBitmap.getPixel32(landingX,landingY)==0){
					lastX = landingX;
					lastY = landingY;	
				}
				else{
					maskGraphics.lineTo(lastX,lastY);
					break;
				}
			}
			maskGraphics.lineTo(lastX,lastY);
		}
		maskGraphics.lineTo(player.x,player.y); 
     	maskGraphics.endFill();	
	}	
}

Now the torchlight moves filling an area rather than just shooting a bunch of rays

Now, it’s time to turn a yellow area into an actual flashlight

6 – MASKING THE FLOOR

To create the flashlight effect, you just need to use torchlight bitmap as a mask for floor image. Randomly change floor alpha to create a flicker effect

window.onload = function() {
	
	var game = new Phaser.Game(640,480,Phaser.CANVAS,"",{preload:onPreload, create:onCreate, update:onUpdate});                
	
	var player;
	var wallsBitmap;
	var floor;
	var lightAngle = Math.PI/4;
	var numberOfRays = 20;
	var rayLength = 100;

	function onPreload() {
		game.load.image("floor","floor.png");
		game.load.image("walls","walls.png");
		game.load.image("player","player.png");
	}

	function goFullScreen(){
		game.scale.pageAlignHorizontally = true;
		game.scale.pageAlignVertically = true;
		game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
		game.scale.setScreenSize(true);
	}

	function onCreate() {
		goFullScreen();
		floor = game.add.sprite(0,0,"floor");
		wallsBitmap = game.make.bitmapData(640,480);
		wallsBitmap.draw("walls");
		wallsBitmap.update();
		game.add.sprite(0,0,wallsBitmap);
		player = game.add.sprite(80,80,"player");
		player.anchor.setTo(0.5,0.5);
		cursors = game.input.keyboard.createCursorKeys();
		maskGraphics = this.game.add.graphics(0, 0);
		floor.mask=maskGraphics
	}

	function onUpdate() {
		var xSpeed = 0;
		var ySpeed = 0;
		if(cursors.up.isDown){
			ySpeed -=1;
		}
		if(cursors.down.isDown){
			ySpeed +=1;
		}
		if(cursors.left.isDown){
			xSpeed -=1;
		}
		if(cursors.right.isDown){
			xSpeed +=1;
		}
		if(Math.abs(xSpeed)+Math.abs(ySpeed)<2 && Math.abs(xSpeed)+Math.abs(ySpeed)>0){
			var color = wallsBitmap.getPixel32(player.x+xSpeed+player.width/2,player.y+ySpeed+player.height/2);
			color+= wallsBitmap.getPixel32(player.x+xSpeed-player.width/2,player.y+ySpeed+player.height/2);
			color+=wallsBitmap.getPixel32(player.x+xSpeed-player.width/2,player.y+ySpeed-player.height/2)
			color+=wallsBitmap.getPixel32(player.x+xSpeed+player.width/2,player.y+ySpeed-player.height/2)
			if(color==0){
				player.x+=xSpeed;
				player.y+=ySpeed;
			}		
		}
		var mouseAngle = Math.atan2(player.y-game.input.y,player.x-game.input.x);
		maskGraphics.clear();
		maskGraphics.lineStyle(2, 0xffffff, 1);
		maskGraphics.beginFill(0xffff00);
		maskGraphics.moveTo(player.x,player.y);	
		for(var i = 0; i<numberOfRays; i++){	
			var rayAngle = mouseAngle-(lightAngle/2)+(lightAngle/numberOfRays)*i
			var lastX = player.x;
			var lastY = player.y;
			for(var j= 1; j<=rayLength;j+=1){
          		var landingX = Math.round(player.x-(2*j)*Math.cos(rayAngle));
          		var landingY = Math.round(player.y-(2*j)*Math.sin(rayAngle));
          		if(wallsBitmap.getPixel32(landingX,landingY)==0){
					lastX = landingX;
					lastY = landingY;	
				}
				else{
					maskGraphics.lineTo(lastX,lastY);
					break;
				}
			}
			maskGraphics.lineTo(lastX,lastY);
		}
		maskGraphics.lineTo(player.x,player.y); 
     	maskGraphics.endFill();
		floor.alpha = 0.5+Math.random()*0.5;	
	}	
}

And this is the final result. See it by yourself

That was incredibly easy, wasn’t it? That’s the magic of Phaser. I plan to extend the prototype adding enemies, what do you think about?

Meanwhile, download the source code and give me feedback.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.