17
\$\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>

Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Sep 27, 2013 at 22:36
\$\endgroup\$
0

2 Answers 2

18
\$\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.

answered Sep 30, 2013 at 19:09
\$\endgroup\$
1
  • \$\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\$ Commented Oct 1, 2013 at 15:02
7
\$\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.

TheCoffeeCup
9,5264 gold badges38 silver badges96 bronze badges
answered Jun 23, 2016 at 17:49
\$\endgroup\$
1
  • \$\begingroup\$ Hi! Welcome to Code Review. Good job on your first answer! I hope to see more of these great answers! \$\endgroup\$ Commented Jun 23, 2016 at 20:57

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.