16
\$\begingroup\$

Here's my first HTML5 game: a really simple snake. I've never made a game before and haven't had too much experience with JavaScript.

Fiddle

$(document).ready(function(){
    
    // SNAKE SETTINGS
    	var SQR_SIZE = 10;
    	var FRAMES = 100;
    	
    // SNAKE VARIABLES
    	var snakeSpeed = 50;
    	var moveCount = 0;
    	var snakeDirection = 38; //37 - left; 38 - up; 39 - right; 40 - down;
    	
    // OTHER VARS
    	var score = 0;
    	
    // TRAIL VARS
    	var xTrail = new Array();
    	var yTrail = new Array();
    	var snakeSize = 0;
    	
    // CREATE CANVAS
    	var c= document.getElementById("snakePlatform");
    	var ctx=c.getContext("2d");
    	var canvWidth = c.width;
    	var canvHeight = c.height;
    	
    	c.addEventListener('click',function(e){mouseHandle(e.offsetX,e.offsetY);},false);
    	
    // SNAKE POSITIONING;
    	var xSnake;
    	var ySnake;
    	var xpos;
    	var ypos;
    	resetPositions();
    	
    // FOOD POSITIONING
    	var xFood;
    	var yFood;
    	
    // BUTTON POSITIONS
    	var buttonPos = new Array();
    	
    // GAMESTATE
    	var gameState = 0;
    	var preState = gameState;
    
    // ----------- GAME PLAY -----------------------------------------------------------------
    
    	menuStart();
    	
    	var checkGs=self.setInterval(function(){checkGamestate(gameState)},20);
    	
    	function checkGamestate(s){
    		if(gameState != preState){
    			switch(s)
    			{
    				case 0:
    					menuStart();
    					preState = 0;
    					break;
    				case 1:
    					gameStart();
    					preState = 1;
    					break;
    				case 2:
    					pgStart();
    					preState = 2;
    					break;
    				default:
    			};
    		};
    	};
    	
    // ----------- GAME FUNCTIONS ------------------------------------------------------------
    	
    // GAME FLOW
    	function gameStart(){
    		var int=self.setInterval(function(){snakeAnimation()},snakeSpeed);
    	
    		function snakeAnimation(){
    			if(moveCount == 0){
    				clear();
    				drawFirst();
    				moveFood();
    				drawFood();
    				moveCount++;
    			}else{
    				clear();
    				drawScore();
    				setTrail();
    				moveSnake(snakeDirection);
    				drawSnake();
    				
    				if(wallCollision(xpos,ypos) || snakeCollision(xpos,ypos)){
    					resetGame();
    					int=window.clearInterval(int);
    					gameState=2;
    				};
    				
    				if(foodCollision(xpos,ypos)){
    					addTrail();
    					clearFood();
    					drawSnake();
    					moveFood();
    					score++;
    				};
    				
    				drawFood();
    				moveCount++;
    				drawScore();
    			};
    		};
    	};	
    	
    // SCORE FUNCTIONS
    	function drawScore(){
    		ctx.font="25px Arial";
    		ctx.fillStyle="rgba(0,0,0,0.2)";
    		ctx.textAlign="center";
    		ctx.fillText(score,canvWidth/2,canvHeight/2);
    	};
    
    // SNAKE FUNCTIONS
    	function drawSnake(){
    		drawFirst();
    		drawTrail();
    	};
    	
    	function drawFirst(){
    		ctx.clearRect(0,0,canvWidth,canvHeight);
    		ctx.fillStyle="rgba(41,99,12,1)";
    		ctx.fillRect(xpos,ypos,SQR_SIZE,SQR_SIZE);
    	};
    	
    	function moveSnake(d){
    		switch(d)
    		{
    		case 37:
    			xSnake--;
    			break;
    		case 38:
    			ySnake--;
    			break;
    		case 39:
    			xSnake++;
    			break;
    		case 40:
    			ySnake++;
    			break;
    		default:
    		};
    		xpos = xSnake*SQR_SIZE;
    		ypos = ySnake*SQR_SIZE;
    	};
    	
    	$(document).keydown(function(event){
    		if(event.which == 37 || event.which == 38 || event.which == 39 || event.which == 40){
    			if(((event.which%2) == 0 && (snakeDirection%2) != 0)){
    				snakeDirection = event.which;
    			}else if(((event.which%2) != 0 && (snakeDirection%2) == 0)){
    				snakeDirection = event.which;
    			};
    		};
    	});
    	
    	function resetPositions(){
    		xSnake = (canvWidth/SQR_SIZE)/2;
    		ySnake = (canvHeight/SQR_SIZE)/2;
    		xpos = xSnake*SQR_SIZE;
    		ypos = ySnake*SQR_SIZE;
    	};
    	
    // TRAIL FUNCTIONS
    	function addTrail(){
    		xTrail.push(xTrail[xTrail.length-1]);
    		yTrail.push(yTrail[yTrail.length-1]);
    	};
    	
    	function setTrail(){
    		var i=xTrail.length;
    		var xTemp;
    		var yTemp
    		while(i>0){
    			xTrail[i] = xTrail[i-1];
    			yTrail[i] = yTrail[i-1];
    			i--;
    		};
    		xTrail.pop();
    		yTrail.pop();
    		xTrail[0] = xpos;
    		yTrail[0] = ypos;
    	};
    	
    	function drawTrail(){
    		for(var a=0;a<xTrail.length;a++){
    			ctx.fillStyle="rgba(0,255,0,1)";
    			ctx.fillRect(xTrail[a],yTrail[a],SQR_SIZE,SQR_SIZE);
    		};
    	};
    	
    // FOOD FUNCTIONS
    	function clearFood(){
    		ctx.clearRect(xFood,yFood,SQR_SIZE,SQR_SIZE);
    	};
    	
    	function moveFood(){
    		do{
    			xFood = (Math.floor(Math.random()*((canvWidth/SQR_SIZE)-SQR_SIZE))+1)*SQR_SIZE;
    			yFood = (Math.floor(Math.random()*((canvHeight/SQR_SIZE)-SQR_SIZE))+1)*SQR_SIZE;
    		}
    		while (snakeCollision(xFood,yFood));
    	};
    	
    	function drawFood(){
    		ctx.fillStyle="rgba(255,0,0,1)";
    		ctx.fillRect(xFood,yFood,SQR_SIZE,SQR_SIZE);
    	};
    	
    // COLLISION CHECKS	
    	function wallCollision(xsource,ysource){
    		if(xsource == canvWidth || xsource == 0-SQR_SIZE){
    			return true;
    		}else if(ysource == canvHeight || ysource == 0-SQR_SIZE){
    			return true;
    		};
    	};
    	
    	function foodCollision(xsource,ysource){
    		if(xsource == xFood && ysource == yFood){
    			return true;
    		};
    	};
    	
    	function snakeCollision(xsource,ysource){
    		for(var i=0;i<xTrail.length;i++){
    			if(xsource == xTrail[i] && ysource == yTrail[i]){
    				return true;
    			};
    		};
    	};
    
    // RESET FUNCTIONS
    
    	function resetGame(){
    		resetPositions();
    		xTrail = [];
    		yTrail = [];
    		moveCount = 0;
    	};
    	
    // ----------- POST GAME FUNCTIONS -------------------------------------------------------
    
    // PG START
    	function pgStart(){
    		clear();
    		
    		ctx.font="25px Arial";
    		ctx.fillStyle="rgba(0,0,0,1)";
    		ctx.textAlign="center";
    		ctx.fillText('GAME OVER',canvWidth/2,canvHeight/2-30);
    		
    		ctx.font="25px Arial";
    		ctx.fillStyle="rgba(0,0,0,1)";
    		ctx.textAlign="center";
    		ctx.fillText('SCORE: '+score,canvWidth/2,canvHeight/2);
    		
    		drawButton(getCenterX(100),getCenterY(50)+35,100,50,"Re-Start",1);
    	};
    	
    // ----------- MENU FUNCTIONS ------------------------------------------------------------
    
    // MENU START
    	function menuStart(){
    		clear();
    		drawButton(getCenterX(100),getCenterY(50),100,50,"Start",0);
    	};
    	
    // CLEAR SCREEN
    	function clear(){
    		ctx.clearRect(0,0,canvWidth,canvHeight);
    	};
    	
    // DRAW BUTTON
    	function drawButton(x,y,width,height,string,event){
    		xCenterButton=x+(width/2);
    		yCenterButton=y+(height/2);
    		
    		ctx.fillStyle="rgba(0,0,0,1)";
        	ctx.fillRect(x-1,y-1,width+2,height+2);     
    
        	ctx.fillStyle="rgba(242,255,195,1)";
        	ctx.fillRect(x,y,width,height);
    		
    		ctx.font="25px Arial";
    		
    		fontSize = getFontSize();
    		centerNum = fontSize/4;
    		
    		ctx.fillStyle="rgba(0,0,0,1)";
    		ctx.textAlign="center";
    		ctx.fillText(string,xCenterButton,yCenterButton+centerNum);
    		
    		buttonPos.push([[x],[y],[x+width],[y+height],[event]]);
    	};
    
    // BUTTON EVENTS
    	function eventButton(d){
            var buttonInt = parseInt(d);
    		switch(buttonInt){
    		case 0: // STARTBUTTON
    			if(gameState == 0){
    				gameState = 1;
    			};
    			break;
    		case 1:
    			if(gameState == 2){
    				score = 0;
    				gameState = 1;
    			};
    			break;
    		default:
    			alert("Error: No button in place.");
    		};
    	};
    	
    // BUTTON CLICK
    		function mouseHandle(x,y){
    			for(var i=0; i<buttonPos.length; i++){
    				if(x>buttonPos[i][0] && x<buttonPos[i][2]){
    					if(y>buttonPos[i][1] && y<buttonPos[i][3]){
    						eventButton(buttonPos[i][4]);
    					};
    				};
    			};
    		};
    		
    // GET FONT SIZE
    	function getFontSize(){
    		fontSizeArray = new Array();
    		fontString = ctx.font;
    		fInstance = fontString.indexOf("px");
    		for(var i=0;i<fInstance;i++){
    			fontSizeArray[i] = fontString[i];
    		};
    		fontSize = fontSizeArray.join("");
    		return fontSize;
    	};
    	
    // CANVAS CENTER
    	function getCenterX(width){
    		canvCenter = canvWidth/2;
    		widthCenter = width/2;
    		x = canvCenter - widthCenter;
    		return x;
    	};
    	
    	function getCenterY(height){
    		canvCenter = canvHeight/2;
    		heightCenter = height/2;
    		y = canvCenter - heightCenter;
    		return y;
    	};
    });
#snakePlatform{
    border:1px solid #000;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<canvas id="snakePlatform" width="400" height="400"></canvas></body>

\$\endgroup\$
17
\$\begingroup\$

It's pretty good for a first game. There are a few things I would change:

1. Dependencies

You use jQuery for two purposes in your code: $(document).ready to trigger the game setup, and $(document).keydown to catch the keyboard events.

However, there really isn't any need to use jQuery for either of these things, so all it does is add to the load time of the page.

You can get rid of the $(document).ready completely if you move the JavaScript down to the bottom of the page (instead of inside the <head>).

You can replace $(document).keydown(function(event){ with document.addEventListener('keydown',function(event){.

2. Objects Namespaces

If you create a single component that uses a lot of JavaScript code, it is often considered best practice to put the code into an object. The biggest benefits of doing this are:

  1. You don't have to worry about other things on the same page conflicting with this code because you (or someone else) gave them the same names. This is particularly important given that some of your variable names c, ctx, etc. are fairly generic and likely to be used again.
  2. You can reuse the same code again and again to create multiple objects on the page without worrying about them interfering with each other. For example, you could create two snake games on a page by doing this:

    <canvas id="snakePlatform1" width="400" height="400"></canvas>
    <canvas id="snakePlatform2" width="400" height="400"></canvas>
    <script>
        var snake1 = new Snake('snakePlatform1');
        var snake2 = new Snake('snakePlatform2');
    </script>
    

    Important Note: The code, as it exists now, will not work with more than one snake because it binds the keydown event to the document and this would cause the keystrokes to be sent to both games at once. In order to use more than one, you will need to figure out how to "focus" one or the other snake game at a time for keyboard input. Explaining how to do this is beyond the scope of this answer.

  3. You can expose some of the functionality of the snake game to outside code in a consistent and documented way. For example, you could provide a getScore() method that returns the current score (or a snake.scored event that fires whenever the score changes), a turn(direction) method that allows outside control of the snake, etc. These would be accessed in the example above by calling snake1.getScore(), snake2.turn('DOWN'), etc. You might subscribe to the event using (a jQuery example) $('#snakePlatform1').on('snake.scored', function(score) {...}). The possibilities are endless.

It is also best to put this object inside a namespace. For example, instead of calling new Snake('snakePlatform'), you would call new Dominic.Games.Snake('snakePlatform'). This makes it easier to track and maintain code in large projects. While this project is fairly simple and may not need namespaces, it's good to get in the habit of using them unless there is a very good reason not to do so.

3. Enums

Although JavaScript doesn't really have the concept of enum like many other languages, it is often useful to define helper objects that act as enums. Here are the two enums that I would define for this game:

var directions = {
    LEFT: 37,
    UP: 38,
    RIGHT: 39,
    DOWN: 40,
};

var states = {
    READY: 0,
    RUNNING: 1,
    OVER: 2,
};

This makes it much easier to see what parts of the code are doing, and therefore makes the code much easier to maintain. For instance, if you don't touch the code for a long time, then come back to it to make some changes, you don't have to relearn what the different numbers you used stand for.


I made a jsFiddle with these changes in it at http://jsfiddle.net/PXMAh/. Each of the sections above is done as a version of the fiddle, so you can add the version number at the end of the URL (last one is ...PXMAh/2/, back to .../1/ or .../0/) to see the changes.

\$\endgroup\$
  • \$\begingroup\$ Thank you for such an in depth and informative answer! Very much appreciated! Also, could I give the canvas a tabIndex and use document.getElementById(id).focus(); to separate each instance of the game? \$\endgroup\$ – Dominic Sore Oct 1 '13 at 15:02
6
\$\begingroup\$

You did really well with readability and documentation. On the design side, I would personally have the score off to the side and not in the middle, although it isn't that big of a deal. I would also consider slowing down the snake a little bit. I noticed that you use a lot of code for adding length to your snake. If you want to, I would suggest doing this instead of using xTrail and yTrail:

var body = [
    {
        "x": 6,
        "y": 0
    },
    {
        "x": 5,
        "y": 0
    },
    {
        "x": 4,
        "y": 0
    },
    {
        "x": 3,
        "y": 0
    },
    {
        "x": 2,
        "y": 0
    },
    {
        "x": 1,
        "y": 0
    }
];

I used a grid system to go along with the array of body segment objects (row 1, column 1 = 1x1). To add/remove segments during movement, I used .unshift() and .pop().

To draw the segments onto the board, I used a for loop and body[i].x or body[i].y.

\$\endgroup\$
  • \$\begingroup\$ Hi! Welcome to Code Review. Good job on your first answer! I hope to see more of these great answers! \$\endgroup\$ – TheCoffeeCup Jun 23 '16 at 20:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.