Phaser tutorial: how to create an HTML5 survival horror game in 6 easy steps

Read all posts about "" game

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.

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