Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about vvvvvv game, Game development, HTML5 and Javascript.

One platform I really love is Terry Cavanagh‘s vvvvvv (six “v” in a row), in my opinion one of the most playable games of its genre since Jet Set Willy has been released.

It’s a platform game but you cannot jump: instead, you can switch the gravity which affects the player so you can walk both on the floor and on the ceiling.

To create this interesting kind of movement, I will start from the prototype I uploaded last week: Creation of an HTML5 tile based platform game with no engines behind: pure code!

It’s pure javascript, so you won’t need any gramework or game engine to have your game up and running.

The code is fully commented and new lines are highlighted, anyway we need two steps:

MAKING THE HERO SMALLER THAN A TILE

This is necessary when we want the hero to be able to easily move into one tile sized tunnels.

The main script does not change, we only have to adjust a bit collision detection when we check for right and bottom walls. Left and upper walls collision code remain intact because in our case the hero has the origin at (0,0).

Here is the script:

(function(){
	
	var canvas = document.getElementById("canvas");   // the canvas where game will be drawn
	var context = canvas.getContext("2d");            // canvas context
	var levelCols=11;							// level width, in tiles
	var levelRows=9;							// level height, in tiles
	var tileSize=32;							// tile size, in pixels
	var playerCol=5;                                  // player starting column
	var playerRow=4;                                  // player starting row
	var playerSize=24;                                // player size
	var sizeDiff=tileSize-playerSize;                 // difference between player and tile size
	var leftPressed=false;                            // are we pressing LEFT arrow key?
	var rightPressed=false;                           // are we pressing RIGHT arrow key?
	var upPressed=false;                              // are we pressing UP arrow key?
	var downPressed=false;                            // are we pressing DOWN arrow key?
	var movementSpeed=3;                              // the speed we are going to move, in pixels per frame
	var playerXSpeed=0;                               // player horizontal speed, in pixels per frame
	var playerYSpeed=0;                               // player vertical speed, in pixels per frame
	
	var level = [        						// the 11x9 level - 1=wall, 0=empty space
		[1,1,1,1,1,1,1,1,1,1,1],
		[1,1,0,0,0,0,0,0,0,1,1],
		[1,0,0,0,0,0,0,0,0,0,1],
		[1,0,0,0,1,0,1,0,0,0,1],
		[1,0,0,0,0,0,0,0,0,0,1],
		[1,0,0,0,1,0,1,0,0,0,1],
		[1,0,0,0,0,0,0,0,0,0,1],
		[1,1,0,0,0,0,0,0,0,1,1],
		[1,1,1,1,1,1,1,1,1,1,1]
	];

	var playerYPos=playerRow*tileSize+sizeDiff/2;	// converting Y player position from tiles to pixels
	var playerXPos=playerCol*tileSize+sizeDiff/2;    	// converting X player position from tiles to pixels
	
	canvas.width=640;                                 // canvas width. Won't work without it even if you style it from CSS
	canvas.height=480;                                // canvas height. Same as before

	// simple key listeners
	
	document.addEventListener("keydown", function(ev){
		switch(ev.keyCode){
			case 65:
				leftPressed=true;
				break;
			case 87:
				upPressed=true;
				break;
			case 68:
				rightPressed=true;
				break;
			case 83:
				downPressed=true;
				break;
		}
	}, false);

	document.addEventListener("keyup", function(ev){
		switch(ev.keyCode){
			case 65:
				leftPressed=false;
				break;
			case 87:
				upPressed=false;
				break;
			case 68:
				rightPressed=false;
				break;
			case 83:
				downPressed=false;
				break;
		}
	}, false);
	
	// function to display the level
	
	function renderLevel(){
		// clear the canvas
		context.clearRect(0, 0, canvas.width, canvas.height);
		// walls = red boxes
		context.fillStyle = "#ff0000";
		for(i=0;i<levelRows;i++){
			for(j=0;j<levelCols;j++){
				if(level[i][j]==1){
					context.fillRect(j*tileSize,i*tileSize,tileSize,tileSize);	
				}
			}
		}
		// player = green box
		context.fillStyle = "#00ff00";
		context.fillRect(playerXPos,playerYPos,playerSize,playerSize);
	}
	
	// this function will do its best to make stuff work at 60FPS - please notice I said "will do its best"
	
	window.requestAnimFrame = (function(callback) {
		return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
		function(callback) {
			window.setTimeout(callback, 1000/60);
		};
	})();
	
	// function to handle the game itself
	
	function updateGame() {
	
		// no friction or inertia at the moment, so at every frame initial speed is set to zero
		playerXSpeed=0;
		playerYSpeed=0;
		
		// updating speed according to key pressed
		if(rightPressed){
			playerXSpeed=movementSpeed
		}
		else{
			if(leftPressed){
				playerXSpeed=-movementSpeed;
			}
			else{
				if(upPressed){
					playerYSpeed=-movementSpeed;
				}
				else{
					if(downPressed){
						playerYSpeed=movementSpeed;
					}         
				}          
			}         
		}
		
		// updating player position
		
		playerXPos+=playerXSpeed;
		playerYPos+=playerYSpeed;
		
		// check for horizontal collisions
		
		var baseCol = Math.floor(playerXPos/tileSize);
		var baseRow = Math.floor(playerYPos/tileSize);
		var colOverlap = playerXPos%tileSize>sizeDiff;
		var rowOverlap = playerYPos%tileSize>sizeDiff;		
		
		if(playerXSpeed>0){
			if((level[baseRow][baseCol+1] && !level[baseRow][baseCol] && colOverlap) || (level[baseRow+1][baseCol+1] && !level[baseRow+1][baseCol] && rowOverlap && colOverlap)){
				playerXPos=baseCol*tileSize+sizeDiff;
			}	
		}
		
		if(playerXSpeed<0){
			if((!level[baseRow][baseCol+1] && level[baseRow][baseCol]) || (!level[baseRow+1][baseCol+1] && level[baseRow+1][baseCol] && rowOverlap)){
				playerXPos=(baseCol+1)*tileSize;
			}	
		}
		
		// check for vertical collisions
		
		var baseCol = Math.floor(playerXPos/tileSize);
		var baseRow = Math.floor(playerYPos/tileSize);
		var colOverlap = playerXPos%tileSize>sizeDiff;
		var rowOverlap = playerYPos%tileSize>sizeDiff;
				
		if(playerYSpeed>0){
			if((level[baseRow+1][baseCol] && !level[baseRow][baseCol] && rowOverlap) || (level[baseRow+1][baseCol+1] && !level[baseRow][baseCol+1] && colOverlap && rowOverlap)){
				playerYPos = baseRow*tileSize+sizeDiff;
			}	
		}
		
		if(playerYSpeed<0){
			if((!level[baseRow+1][baseCol] && level[baseRow][baseCol]) || (!level[baseRow+1][baseCol+1] && level[baseRow][baseCol+1] && colOverlap)){
				playerYPos = (baseRow+1)*tileSize;
			}		
		}
		
		// rendering level
		
		renderLevel();
		
		// update the game in about 1/60 seconds
		
		requestAnimFrame(function() {
			updateGame();
		});
	}

	updateGame();

})();

And this is the result

Move the hero with WASD keys and you will be able to easily walk in and out of the center of the stage, in the middle of the four blocks.

Now we have to add gravity.

ADDING AND FLIPPING PLAYER GRAVITY

In order to create gravity, we first need to disable UP and DOWN movement, and in this case we will assign UP arrow key the function to switch gravity.

Gravity acts as an acceleration on the player, and we will also set a maximun free fall speed for a gaming experience purpose. We don’t want the player to fall too fast, like it would happen in a real world.

Since the gravity wins on every other force, this time we’ll need to give vertical collisions a priority over horizontal collisions, so I am checking for horizontal collisions after I managed vertical collisions.

Here is the script:

(function(){
	
	var canvas = document.getElementById("canvas");   // the canvas where game will be drawn
	var context = canvas.getContext("2d");            // canvas context
	var levelCols=11;							// level width, in tiles
	var levelRows=9;							// level height, in tiles
	var tileSize=32;							// tile size, in pixels
	var playerCol=5;                                  // player starting column
	var playerRow=4;                                  // player starting row
	var playerSize=24;                                // player size
	var sizeDiff=tileSize-playerSize;                 // difference between player and tile size
	var leftPressed=false;                            // are we pressing LEFT arrow key?
	var rightPressed=false;                           // are we pressing RIGHT arrow key?
	var upPressed=false;                              // are we pressing UP arrow key?
	var downPressed=false;                            // are we pressing DOWN arrow key?
	var movementSpeed=3;                              // the speed we are going to move, in pixels per frame
	var playerXSpeed=0;                               // player horizontal speed, in pixels per frame
	var playerYSpeed=0;                               // player vertical speed, in pixels per frame
	var gravityForce=0.5;                             // gravity acceleration, in pixels per frame per frame
	var maxFallingSpeed=10;                           // maximum falling speed
	
	var level = [        						// the 11x9 level - 1=wall, 0=empty space
		[1,1,1,1,1,1,1,1,1,1,1],
		[1,1,0,0,0,0,0,0,0,1,1],
		[1,0,0,0,0,0,0,0,0,0,1],
		[1,0,0,0,1,0,1,0,0,0,1],
		[1,0,0,0,0,0,0,0,0,0,1],
		[1,0,0,0,1,0,1,0,0,0,1],
		[1,0,0,0,0,0,0,0,0,0,1],
		[1,1,0,0,0,0,0,0,0,1,1],
		[1,1,1,1,1,1,1,1,1,1,1]
	];

	var playerYPos=playerRow*tileSize+sizeDiff/2;	// converting Y player position from tiles to pixels
	var playerXPos=playerCol*tileSize+sizeDiff/2;    	// converting X player position from tiles to pixels
	
	canvas.width=640;                                 // canvas width. Won't work without it even if you style it from CSS
	canvas.height=480;                                // canvas height. Same as before

	// simple key listeners
	
	document.addEventListener("keydown", function(ev){
		switch(ev.keyCode){
			case 65:
				leftPressed=true;
				break;
			case 87:
				// this time we reverse gravity if the player is on the floor
				upPressed=true;
				if(playerYSpeed==0){
					gravityForce*=-1;
				}
				break;
			case 68:
				rightPressed=true;
				break;
			case 83:
				downPressed=true;
				break;
		}
	}, false);

	document.addEventListener("keyup", function(ev){
		switch(ev.keyCode){
			case 65:
				leftPressed=false;
				break;
			case 87:
				upPressed=false;
				break;
			case 68:
				rightPressed=false;
				break;
			case 83:
				downPressed=false;
				break;
		}
	}, false);
	
	// function to display the level
	
	function renderLevel(){
		// clear the canvas
		context.clearRect(0, 0, canvas.width, canvas.height);
		// walls = red boxes
		context.fillStyle = "#ff0000";
		for(i=0;i<levelRows;i++){
			for(j=0;j<levelCols;j++){
				if(level[i][j]==1){
					context.fillRect(j*tileSize,i*tileSize,tileSize,tileSize);	
				}
			}
		}
		// player = green box
		context.fillStyle = "#00ff00";
		context.fillRect(playerXPos,playerYPos,playerSize,playerSize);
	}
	
	// this function will do its best to make stuff work at 60FPS - please notice I said "will do its best"
	
	window.requestAnimFrame = (function(callback) {
		return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
		function(callback) {
			window.setTimeout(callback, 1000/60);
		};
	})();
	
	// function to handle the game itself
	
	function updateGame() {
	
		// no friction or inertia at the moment, so at every frame initial speed is set to zero
		playerXSpeed=0;
		playerYSpeed+=gravityForce;
		
		// limiting max vertical speed
		
		playerYSpeed=Math.min(playerYSpeed,maxFallingSpeed);
		playerYSpeed=Math.max(playerYSpeed,-maxFallingSpeed);
		
		// updating speed according to key pressed
		if(rightPressed){
			playerXSpeed=movementSpeed
		}
		else{
			if(leftPressed){
				playerXSpeed=-movementSpeed;
			}
			else{
				if(upPressed){
					/*playerYSpeed=-movementSpeed;*/
				}
				else{
					if(downPressed){
						/*playerYSpeed=movementSpeed;*/
					}         
				}          
			}         
		}
		
		// updating player position
		
		playerXPos+=playerXSpeed;
		playerYPos+=playerYSpeed;
		
		// check for vertical collisions, got to do it first this time
		
		var baseCol = Math.floor(playerXPos/tileSize);
		var baseRow = Math.floor(playerYPos/tileSize);
		var colOverlap = playerXPos%tileSize>sizeDiff;
		var rowOverlap = playerYPos%tileSize>sizeDiff;
				
		if(playerYSpeed>0){
			if((level[baseRow+1][baseCol] && !level[baseRow][baseCol] && rowOverlap) || (level[baseRow+1][baseCol+1] && !level[baseRow][baseCol+1] && colOverlap && rowOverlap)){
				playerYPos = baseRow*tileSize+sizeDiff;
				playerYSpeed=0;
			}	
		}
		
		if(playerYSpeed<0){
			if((!level[baseRow+1][baseCol] && level[baseRow][baseCol]) || (!level[baseRow+1][baseCol+1] && level[baseRow][baseCol+1] && colOverlap)){
				playerYPos = (baseRow+1)*tileSize;
				playerYSpeed=0;
			}		
		}
		
		// check for horizontal collisions
		
		var baseCol = Math.floor(playerXPos/tileSize);
		var baseRow = Math.floor(playerYPos/tileSize);
		var colOverlap = playerXPos%tileSize>sizeDiff;
		var rowOverlap = playerYPos%tileSize>sizeDiff;		
		
		if(playerXSpeed>0){
			if((level[baseRow][baseCol+1] && !level[baseRow][baseCol] && colOverlap) || (level[baseRow+1][baseCol+1] && !level[baseRow+1][baseCol] && rowOverlap && colOverlap)){
				playerXPos=baseCol*tileSize+sizeDiff;
			}	
		}
		
		if(playerXSpeed<0){
			if((!level[baseRow][baseCol+1] && level[baseRow][baseCol]) || (!level[baseRow+1][baseCol+1] && level[baseRow+1][baseCol] && rowOverlap)){
				playerXPos=(baseCol+1)*tileSize;
			}	
		}
		
		// rendering level
		
		renderLevel();
		
		// update the game in about 1/60 seconds
		
		requestAnimFrame(function() {
			updateGame();
		});
	}

	updateGame();

})();

And this is the result:

Move with A and D keys, switch gravity with W.

Download the source code of the entire project.

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