📜 ⬆️ ⬇️

We write a snake game using JavaScript + Canvas

Good day, friends. Now I will try to show you how to write a game Snake. Of course, not the fastest way and not the smallest in terms of the number of lines of code, but in my opinion the most understandable for novice developers like me. The article is written for people who want to get acquainted with the canvas element and its simple methods for working with 2D graphics.
image
Let's write a snake in the "old" form, without particularly beautiful graphics - in the form of cubes. But it will only simplify the understanding of the development. Well, let's go!

Training


Perhaps, it is worth starting with preparing for creating a game and writing code. We will use the simple Sublime Text editor. However, it does not matter. We do everything in one document to make it faster.

First of all, let's write the code for embedding the canvas in the document. Let me remind you that canvas is supported only in HTML5.

<!--      ,     . --> <canvas id="gP">HTML5  </canvas> 

Preparation is complete, now we can start creating the game itself.
')

Getting started


To begin with, I would like to explain to you in general how the snake will work, so it will be much clearer. Our snake is an array. An array of elements, elements are its parts into which it is divided. These are just squares that have X and Y coordinates. As you know, X is horizontal, Y is vertical. In its usual form, we imagine a coordinate plane like this:

image

It is absolutely correct, there is no doubt about it, but on a computer monitor (in particular, canvas) it looks different, like this:

image

You need to know this if you first encountered canvas. When I ran into this, at first I didn’t understand where the point was (0,0), since I quickly figured it out. I hope you have no problems.

Let's return to the snake elements, its parts. Imagine that each element has its own coordinates, but the same height and the same width. These are squares, no more. And now let's imagine that here we drew a snake, all the squares, they go one after the other, the same ones. And here we decided that we need to move the snake to the right. What would you do?

Some people would say that we need to move the first element to the right, then the second, then the third, and so on. But for me this option is not correct, because if suddenly the snake is huge and the computer is weak, we can notice that the snake is sometimes torn, which generally should not be. And in general, this method requires too many teams, when you can get by with a much smaller number without losing quality. And now my way: We take the last element of the snake, and put it in the beginning, changing its coordinates so that it is after the head. Now this element is the head. Just! And yes, the effect of the movement will be present, and on the computer it will not be noticeable at all, as we hid the tail, and then it was placed at the beginning. this is exactly what we will do during the creation of the snake movement.

Here we start exactly


First, we need to declare some variables that we will use in the future.

 //  . function rand (min, max) {k = Math.floor(Math.random() * (max - min) + min); return (Math.round( k / s) * s);} //    . function newA () {a = [rand(0, innerWidth),rand(0, innerHeight)];} //       . function newB () {sBody = [{x: 0,y: 0}];} var gP = document.getElementById('gP'), // canvas. // "" (    canvas). g = gP.getContext('2d'), sBody = null, // ,    . d = 1, //  1 - d, 2 -  3 - , 4 - . a = null, //, , 0  - x, 1  - y. s = 30; newB(); newA(); // . 

And so, in general, we need to slightly explain why we need in the function of returning a random number, multiply and divide by the variable s, which stores nothing but width as part of the height of the snake elements. In fact, this is necessary so that there are no displacements during the movement, since we have an element width of 30, then if we want to move it without gaps, then all coordinates should be divided by 30 without a residue. That is why I divide by the number, round, and then multiply. Thus, the number is returned so that it can be divided without balance by 30.

You could argue by saying that you could just make a canvas a width and height multiple of 30. But in fact, this is not the best option. Since I personally used to use the entire width of the screen. And if the width = 320, then I would have to take as much as 20 pixels from the user, which could cause discomfort. That is why in our snake all the coordinates of the objects are divided by 30, so that there are no unexpected moments. It would be even more correct to render this as a separate function, since it is often used in code. But I came to this conclusion late. (But maybe it is not even necessary).

Now let's make the picture have a clear display quality. In the course of the article, I will finish writing the code, in some places going back to the beginning, so that you have the full picture of the code with you.

 gP.width = innerWidth; //  ,    . gP.height = innerHeight; //  ,    . 

If anything, the variables innerWidth and innerHeight are stored in the global namespace.
Therefore, they can be addressed in this way. True, I do not know whether to do so.

Well, now we are starting to write snake code.

In order to have movement, we need animation, we will use the setInterval function, the second parameter of which will be the number 60. A little more, 75 for an example, but I like 60. The function is only for every 60 ms. draws a snake "again." Further writing of the code is only this interval.

I will show in general a simple drawing of our snake, so far without movement.

 setInterval(function(){ g.clearRect(0,0,gP.width,gP.height); // . g.fillStyle = "red"; //     . g.fillRect(...a, s, s); //    30x30   a[0]  a[1]. g.fillStyle = "#000"; //     . }, 60); 

To check that our snake does not collide with itself, we need to do some checking for each element, except the last one. We will check whether the coordinates of the last element (head) of the snake are equal to any of ... That is, simply speaking: whether a collision occurred. This line of code was a single line, but you made it understandable. I remind you that all this is added to the interval function.

 sBody.forEach(function(el, i){ //  ,      ,     . if (a[0] + s >= gP.width || a[1] + s >= gP.height) newA(); //  . var last = sBody.length - 1; if ( el.x == sBody[last].x && el.y == sBody[last].y && i < last) { sBody.splice(0,last); //  . sBody = [{x:0,y:0}]; //  . d = 1; //    . } }); //+ //     . var m = sBody[0], f = {x: mx,y: my}, l = sBody[sBody.length - 1]; /*       . ,      ,     .       ( -  1, - ),     . ,   . */ //  ,    Y,   X  + s. if (d == 1) fx = lx + s, fy = Math.round(ly / s) * s; //   ,   X,   Y  + s. if (d == 2) fy = ly + s, fx = Math.round(lx / s) * s; //  ,   Y,   X  -s. if (d == 3) fx = lx - s, fy = Math.round(ly / s) * s; //  ,   X,   Y  -s. if (d == 4) fy = ly - s, fx = Math.round(lx / s) * s; 

And now, you probably noticed that while we change the coordinates, we always “save” something, first dividing, and then rounding and multiplying by the number s. This is all the same way that a snake aligns with an apple. The movement in this case is strict, simple, and therefore the apple snake can strictly follow certain rules, which are specified at the very beginning of the interval. And if the coordinates of the snake's head even shifted by 1px, the apple could not be eaten. And yes, this is a simple option, so everything is so very limited.

Well, we still have something to do? Right, remove the tail from the array (the first element), add a new element to the very end and draw the whole snake. Let's do this by adding such lines of code to the end of the interval.

 sBody.push(f); //      . sBody.splice(0,1); // . //   . sBody.forEach(function(pob, i){ //   ,      X ,   ,     if (d == 1) if (pob.x > Math.round(gP.width / s) * s) pob.x = 0; //   ,      X ,   ,    . if (d == 2) if (pob.y > Math.round(gP.height / s) * s) pob.y = 0; //   ,    X  ,         ( ). if (d == 3) if (pob.x < 0) pob.x = Math.round(gP.width / s) * s; //   ,    Y  ,         ( ). if (d == 4) if (pob.y < 0) pob.y = Math.round(gP.height / s) * s; //     ,    . if (pob.x == a[0] && pob.y == a[1]) newA(), sBody.unshift({x: fx - s, y:ly}) //    . g.fillRect(pob.x, pob.y, s, s); }); 

In addition to rendering a snake, I added code that makes it feel like the end of the screen is its beginning. And if the snake goes beyond the boundaries, then it goes out of the beginning, to perish.
You can replace the zeroing of coordinates, for example, by dropping the game, if everything is very tough. But I like more so. And now, it remains only by pressing the buttons to change the direction of the snake. Makes in seconds. You just need to write this code immediately after setInterval. Like that:

 //setInerval(...); onkeydown = function (e) { var k = e.keyCode; if ([38,39,40,37].indexOf(k) >= 0) e.preventDefault(); if (k == 39 && d != 3) d = 1; // if (k == 40 && d != 4) d = 2; // if (k == 37 && d != 1) d = 3; // if (k == 38 && d != 2) d = 4; // }; 

Here we made it so that if the snake moves to the right, then it cannot change direction to the left. This is logical, in fact.

That's all friends. My first article written by a newbie for newbies. I hope everything was clear and it was useful to someone. Snake can be improved by adding for example, the scorer, records, additional chips, but these are all additions that you can do yourself. That's all, good luck to all!

Watch the demo (CodePen):

Snake game.

Source: https://habr.com/ru/post/334434/


All Articles