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.
$(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>
2 Answers 2
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:
- 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. 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.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 asnake.scored
event that fires whenever the score changes), aturn(direction)
method that allows outside control of the snake, etc. These would be accessed in the example above by callingsnake1.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.
-
\$\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– Dominic Sore2013年10月01日 15:02:17 +00:00Commented Oct 1, 2013 at 15:02
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
.
-
\$\begingroup\$ Hi! Welcome to Code Review. Good job on your first answer! I hope to see more of these great answers! \$\endgroup\$TheCoffeeCup– TheCoffeeCup2016年06月23日 20:57:58 +00:00Commented Jun 23, 2016 at 20:57
Explore related questions
See similar questions with these tags.