使用HTML5 Canvas和KineticJS的Snake游戏

Snake game using HTML5 Canvas and KineticJS
Snake game using HTML5 Canvas and KineticJS

Snake game using HTML5 Canvas and KineticJS Snake game. In the beginning, before start making a new game, you need to prepare the concept (logic) of the game. You need to have a clear idea about it. You need to develop a high level of understanding, without going into the fine details. In the first phase, we find the answer "what" we want to create, and we reserve the answers to "how to do" for the upcoming stages. Let me illustrate this method with an example. Let’s develop the popular game "Snake". This game was popular long ago, even on consoles and old cell phones with text user interfaces.

使用HTML5 Canvas和KineticJS Snake游戏的Snake游戏。 首先,在开始制作新游戏之前,您需要准备游戏的概念(逻辑)。 您需要对此有一个清晰的想法。 您需要发展高层次的理解,而无需深入细节。 在第一阶段,我们找到我们要创建的答案“ what”,并为接下来的阶段保留“ how do”的答案。 让我用一个例子来说明这种方法。 让我们来开发流行的游戏“蛇”。 这款游戏很久以前就很流行,甚至在带有文本用户界面的游戏机和旧手机上也是如此。

Classic Snake Game

Classic Snake Game

游戏概念 (Concept of the game)

Initially a small sized snake appears on the screen which keeps running for the rest of the game like endless runner. Player can change the direction only. Speed of the snake increases with time. Length of the snake also increases after eating randomly appearing food. Increasing length and speed of the snake adds difficulty to the game over time.

最初,一条小蛇出现在屏幕上,并像无尽的跑步者一样在游戏的其余部分继续运行。 玩家只能更改方向。 蛇的速度随时间增加。 吃了随机出现的食物后,蛇的长度也会增加。 随着时间的推移,蛇的长度和速度的增加会增加游戏的难度。

We can use Storyboard technique to graphically represent our idea.

我们可以使用情节提要技术以图形方式表示我们的想法。

According to Wikipedia: "Storyboards are graphic organizers in the form of illustrations or images displayed in sequence for the purpose of pre-visualizing a motion picture, animation, motion graphic or interactive media sequence."

根据Wikipedia的说法:“故事板是按顺序显示的插图或图像形式的图形组织者,目的是预先可视化电影,动画,运动图形或交互式媒体序列。”

Here is our Storyboard:

这是我们的情节提要:

Snake Storyboard

Snake Storyboard

For your convenience I am adding the description of each Storyboard screen:

为了方便起见,我添加了每个Storyboard屏幕的描述:

Screen 1: Snake (square) waiting for a key-press to start moving. Circle is Shown as food item.

屏幕1:蛇(方形)正在等待按键开始移动。 圆圈显示为食物。

Screen 2: After eating (hitting) food item the snake’s length increases and food item appears at another random location.

屏幕2:吃(击中)食物后,蛇的长度增加,食物出现在另一个随机位置。

Screen 3: Snake can re-enter the playing area from the opposite edge after leaving it from an edge.

屏幕3:从边缘离开后,Snake可以从另一边缘重新进入游戏区域。

Screen 4: Snake dies after biting itself :(

屏幕4:蛇咬自己后死:(

Here, UML Statechart diagram may also help to understand different "states" of the snake during a game.

在这里,UML状态图还可以帮助您理解游戏过程中蛇的不同“状态”。

According to Wikipedia: "A state diagram is a type of diagram used in computer science and related fields to describe the behavior of systems. State diagrams require that the system described is composed of a finite number of states; sometimes, this is indeed the case, while at other times this is a reasonable abstraction. Many forms of state diagrams exist, which differ slightly and have different semantics."

根据Wikipedia的说法:“状态图是计算机科学和相关领域中用于描述系统行为的一种图。状态图要求所描述的系统由有限数量的状态组成;有时确实如此,而在其他时候,这是一个合理的抽象。存在许多形式的状态图,它们略有不同并且具有不同的语义。”

Here is our Statechart diagram:

这是我们的状态图图:

Snake Storyboard

Snake Storyboard

In the diagram, edges represent the actions and the ovals represent the states a snake is in after the specific action. Game starts with an idle snake. You can move snake in all four directions using arrow keys. While moving in any direction when a key is pressed, the snake goes to "Deciding" state to determine which key was pressed and then again goes to the respective direction. Snake eats food when encountered while moving. There is also a "dead" state if snake hits itself while moving.

在图中,边缘代表动作,椭圆形代表特定动作后蛇所处的状态。 游戏从闲置的蛇开始。 您可以使用箭头键在所有四个方向上移动蛇。 当按下某个键时,蛇按任何方向移动时,蛇会进入“正在决定”状态,以确定按下了哪个键,然后再次转到相应的方向。 蛇在走动时会吃东西。 如果蛇在移动时撞到自己,也会有一个“死”状态。

You may also want to add more state diagrams for prominent objects to clarify. There are other UML diagrams which may help you describe your game project. These diagrams are not only helpful for yourself but also helps if you are working in a team making team communication easy and unambiguous.

您可能还需要为突出的对象添加更多状态图以进行说明。 还有其他UML图可以帮助您描述游戏项目。 这些图不仅对您自己有帮助,而且在团队中工作时也非常有帮助,使团队沟通变得轻松而明确。

游戏结构 (Structure of the game)

Snake Storyboard

Snake Storyboard

The play area in virtually divided into a 200?200-pixel grid having 10?10-pixel cells. The initGrid() function prepares the grid. As you can guess from the code that the snake’s width and height is also 10?10 pixels. So as a programming trick, I used the height and width of the snake to represent the dimensions of a single cell.

游乐区实际上被划分为一个200至200像素的网格,其中包含10至10像素的单元。 initGrid()函数准备网格。 从代码中可以猜到,蛇的宽度和高度也是10到10像素。 因此,作为编程技巧,我使用了蛇的高度和宽度来表示单个单元格的尺寸。


function initGrid() {
   //*****Initialize Grid
   ...
    cell = {"col":col*snakeHeight, "row":row*snakeWidth};
    ...
}

function initGrid() {
   //*****Initialize Grid
   ...
    cell = {"col":col*snakeHeight, "row":row*snakeWidth};
    ...
}

You are right if you are thinking about the usage of this virtual grid. In fact, during the initialization of the game structure, this grid helps us to identify the places (cells…to be precise) where we can put the snake and food randomly. A random number generator function randomCell(cells) gives us a random number which we use as an index to the grid array and get the coordinates stored against that specific index. Here is the random number generator function…

如果您正在考虑使用此虚拟网格,那是对的。 实际上,在游戏结构初始化期间,此网格可帮助我们确定可以随机放置蛇和食物的位置(准确地说是单元格)。 随机数生成器函数randomCell(cells)给我们一个随机数,我们将其用作栅格数组的索引,并获取针对该特定索引存储的坐标。 这是随机数生成器函数…


function randomCell(cells) {
   return Math.floor((Math.random()*cells));
}

function randomCell(cells) {
   return Math.floor((Math.random()*cells));
}

Math.random and Math.floor are both Javascript functions.

Math.random和Math.floor都是Javascript函数。

The following code shows the usage of grid and random number generator function…

以下代码显示了网格和随机数生成器函数的用法…


var initSnakePosition = randomCell(grid.length - 1); //pass number of cells
var snakePart = new Kinetic.Rect({
   name: 'snake',
   x: grid[initSnakePosition].col,
   y: grid[initSnakePosition].row,
   width: snakeWidth,
   height: snakeHeight,
   fill: 'black',
});

var initSnakePosition = randomCell(grid.length - 1); //pass number of cells
var snakePart = new Kinetic.Rect({
   name: 'snake',
   x: grid[initSnakePosition].col,
   y: grid[initSnakePosition].row,
   width: snakeWidth,
   height: snakeHeight,
   fill: 'black',
});

Kinetic.Rect constructor constructs the initial single rectangle to represent the snake. Later, when the snake would grow after eating the food, we would be adding more rectangles. Each rectangle is assigned a number to represent its actual position in the snake array. As there is only one rectangle at the moment, we assign it position 1.

Kinetic.Rect构造函数构造初始的单个矩形来表示蛇。 稍后,当蛇在吃完食物后长大时,我们将添加更多的矩形。 每个矩形都分配有一个数字,以表示其在蛇形数组中的实际位置。 由于目前只有一个矩形,因此我们将其指定为位置1。


snakePart.position = snakeParts;

snakePart.position = snakeParts;

snakeParts is a counter which keeps counting the number of parts in the snake array. You might be wondering that we have not created any array of snakePart objects but we are talking about array? In fact if you keep the value of name: property same for all the snakePart objects, KineticJS would return all those objects as an array if you ask like this…

snakeParts是一个计数器,用于不断计数蛇阵列中的零件数。 您可能想知道我们没有创建任何蛇形对象数​​组,但是我们在谈论数组吗? 实际上,如果您对所有snakePart对象都保留name:property的值相同,那么如果您这样询问,KineticJS会将所有这些对象作为数组返回……


var snakePartsArray = stage.get('.snake');

var snakePartsArray = stage.get('.snake');

You will see the usage of this feature in action later in the code.

您将在代码后面的部分中看到此功能的用法。

snakePart.position shows how you can add custom properties to Kinetic.Rect object dynamically, or to any other object.

snakePart.position显示了如何动态地向Kinetic.Rect对象或任何其他对象添加自定义属性。

Why we need the position when KineticJS can return indexed array? Please don’t bother yourself with this question at the moment, you will find the answer if you keep reading. :)

为什么我们需要KineticJS可以返回索引数组的位置? 现在请不要为这个问题而烦恼,如果您继续阅读,将会找到答案。 :)

Two more identifications are required to make the job more easy to manage the snake actions and movements, snake head and tail. There is only one snake-part (rectangle) to begin with therefore both head and tail pointers point to the same rectangle.

为了使这项工作更容易管理蛇的动作和动作(蛇的头和尾),还需要两个标识。 首先只有一个蛇形部分(矩形),因此头和尾指针都指向同一矩形。


var snakeTail;
var snakeHead;
...
snakeHead = snakePart;
snakeTail = snakePart;

var snakeTail;
var snakeHead;
...
snakeHead = snakePart;
snakeTail = snakePart;

We are done with setting up the snake. To construct the food which is a simple circle of radius 5 see the following code…

我们完成了设置蛇的工作。 要构造半径为5的简单圆的食物,请参见以下代码…


var randomFoodCell = randomCell(grid.length - 1);
 var food = new Kinetic.Circle({
    id: 'food',
    x:grid[randomFoodCell].col+5,
    y:grid[randomFoodCell].row+5,
    radius: 5,
    fill: 'black'
});

var randomFoodCell = randomCell(grid.length - 1);
 var food = new Kinetic.Circle({
    id: 'food',
    x:grid[randomFoodCell].col+5,
    y:grid[randomFoodCell].row+5,
    radius: 5,
    fill: 'black'
});

Kinetic.Circle constructs the food for our snake game. Here adding +5 to x and y coordinates to place the circle exactly in the centre of a 10?10 cell provided that the radius of the circle is 5.

Kinetic.Circle为我们的蛇游戏构造食物。 这里,在x和y坐标上加上+5,可以将圆精确地放置在10?10像元的中心,前提是圆的半径为5。

After we are done with the creation of basic shapes for our game and their positions on the game area/grid we need to add those shapes to a Kinetic.Layer and then add that layer to the Kinetic.Stage.

在为游戏创建基本形状及其在游戏区域/网格中的位置之后,我们需要将这些形状添加到Kinetic.Layer,然后将该层添加到Kinetic.Stage。


// add the shapes (sanke and food) to the layer
 var layer = new Kinetic.Layer();
 layer.add(snakePart);
 layer.add(food);
 // add the layer to the stage
 stage.add(layer);

// add the shapes (sanke and food) to the layer
 var layer = new Kinetic.Layer();
 layer.add(snakePart);
 layer.add(food);
 // add the layer to the stage
 stage.add(layer);

the ‘stage’ object used in the cod above has already been created in the beginning using the following code snippet…

上面鳕鱼中使用的“ stage”对象已经使用以下代码段在开始时创建了……


//Stage
 var stageWidth = 200;
 var stageHeight = 200;
 var stage = new Kinetic.Stage({
 container: 'container',
 width: stageWidth,
 height: stageHeight
 });

//Stage
 var stageWidth = 200;
 var stageHeight = 200;
 var stage = new Kinetic.Stage({
 container: 'container',
 width: stageWidth,
 height: stageHeight
 });

container property of Stage needs to know the id of the div where we want to show our HTML5 Canvas.

Stage的container属性需要知道要在其中显示HTML5 Canvas的div的ID。

Initial screen of the game looks like this once our structure is complete…

一旦我们的结构完成,游戏的初始屏幕将如下所示:

Initial screen

Initial screen

After setting up the environment/structure let’s deal with the user stories / use cases one by one.

设置环境/结构后,让我们逐一处理用户案例/用例。

在设定的间隔后执行主游戏循环 (The main game loop executing after a set interval)


var gameInterval = self.setInterval(function(){gameLoop()},70);

var gameInterval = self.setInterval(function(){gameLoop()},70);

setInterval is a Javascript function which makes a function called asynchronously that is passed as an argument, after the set intervals. In our case gameLoop() is the function which drives the whole game. Have a look at it…

setInterval是一个Javascript函数,该函数使函数异步调用,并在设置间隔后作为参数传递。 在我们的例子中,gameLoop()是驱动整个游戏的函数。 看看它…


function gameLoop() {
   if(checkGameStatus())
      move(where);
   else {
      clearInterval(gameInterval);//Stop calling gameLoop()
      alert('Game Over!');
   }
}

function gameLoop() {
   if(checkGameStatus())
      move(where);
   else {
      clearInterval(gameInterval);//Stop calling gameLoop()
      alert('Game Over!');
   }
}

Well, the behaviour of gameLoop() is pretty obvious. It moves the snake according to the arrow key pressed. And if snake hits himself then display a Game Over message to the player and also stop the asynchronous calls by calling a Javascript clearInterval() method.

好吧,gameLoop()的行为非常明显。 它根据按下的箭头键移动蛇。 如果蛇打自己,则向玩家显示游戏结束消息,并通过调用Javascript clearInterval()方法停止异步调用。

To capture the arrow keys I have used the jQuery’s keydown event handler to respond to the keys pressed. It sets a variable ‘where’ that is eventually used by gameLoop() to pass the code to actual move() function.

为了捕获箭头键,我使用了jQuery的keydown事件处理程序来响应按下的键。 它设置了一个变量“ where”,gameLoop()最终使用该变量将代码传递给实际的move()函数。


$( document ).ready(function() {
   $(document).keydown(function(e) {
      switch(e.keyCode) {
         // User pressed "up" arrow
         case Up_Arrow:
            where = Up_Arrow;
         break;
         // User pressed "down" arrow
         case Down_Arrow:
            where = Down_Arrow;
         break;
         // User pressed "right" arrow
         case Right_Arrow:
            where = Right_Arrow;
         break;
         // User pressed "left" arrow
         case Left_Arrow:
            where = Left_Arrow;
         break;
      }
   });
});

$( document ).ready(function() {
   $(document).keydown(function(e) {
      switch(e.keyCode) {
         // User pressed "up" arrow
         case Up_Arrow:
            where = Up_Arrow;
         break;
         // User pressed "down" arrow
         case Down_Arrow:
            where = Down_Arrow;
         break;
         // User pressed "right" arrow
         case Right_Arrow:
            where = Right_Arrow;
         break;
         // User pressed "left" arrow
         case Left_Arrow:
            where = Left_Arrow;
         break;
      }
   });
});

As you might have guessed already that move() function is actual brain of this game and Kinetic.Animation handles the actual movement of the objects. All you have to do is to set new locations for your desired objects and then call start() method. To prevent the animation from running infinitely call stop() method immediately after start(). Try removing the stop() method and see what happens yourself.

您可能已经猜到move()函数是该游戏的实际大脑,而Kinetic.Animation处理对象的实际运动。 您要做的就是为所需的对象设置新的位置,然后调用start()方法。 为防止动画无限运行,请在start()之后立即调用stop()方法。 尝试删除stop()方法,看看自己会发生什么。


function move(direction) {
   //Super hint: only move the tail
   var foodHit = false;
   switch(direction) {
      case Down_Arrow:
         foodHit = snakeEatsFood(direction);
         var anim2 = new Kinetic.Animation(function(frame) {
            if(foodHit) {
               snakeHead.setY(snakeHead.getY()+10);
               growSnake(direction);
               if(snakeHead.getY() == stageHeight)
                  snakeHead.setY(0);
               relocateFood();
            } else {
               snakeTail.setY(snakeHead.getY()+10);
               snakeTail.setX(snakeHead.getX());
               if(snakeTail.getY() == stageHeight)
                  snakeTail.setY(0);
               rePosition();
            }
          }, layer);
         anim2.start();
         anim2.stop();
          break;
      case Up_Arrow:
        ...
        ...

function move(direction) {
   //Super hint: only move the tail
   var foodHit = false;
   switch(direction) {
      case Down_Arrow:
         foodHit = snakeEatsFood(direction);
         var anim2 = new Kinetic.Animation(function(frame) {
            if(foodHit) {
               snakeHead.setY(snakeHead.getY()+10);
               growSnake(direction);
               if(snakeHead.getY() == stageHeight)
                  snakeHead.setY(0);
               relocateFood();
            } else {
               snakeTail.setY(snakeHead.getY()+10);
               snakeTail.setX(snakeHead.getX());
               if(snakeTail.getY() == stageHeight)
                  snakeTail.setY(0);
               rePosition();
            }
          }, layer);
         anim2.start();
         anim2.stop();
          break;
      case Up_Arrow:
        ...
        ...

移动蛇 (Move the snake)

Move the snake

Move the snake

Snake is divided into small 10?10-pixels squares. The square on the front is head and the back most square is tail. The technique to move the snake is simple. We pick the tail and put it before the head except when there is only one snake part. The position number assigned to each part of the snake is out of sequence now. Head and tail pointers are pointing towards wrong parts. We need to reposition the pointers and position numbers. It is done by calling rePosition(). It works as shown in the diagram below…

蛇被分成10到10像素的小方块。 前面的正方形是头部,后面的正方形是尾巴。 移动蛇的技术很简单。 我们选择尾巴并将其放在头部之前,除非只有一条蛇部分。 分配给蛇的每个部分的位置编号现在不正确。 头和尾指针指向错误的部分。 我们需要重新定位指针和位置编号。 这是通过调用rePosition()完​​成的。 它的工作原理如下图所示……

Snake moves and reposition

Snake moves and reposition

吃蛇时长蛇 (Grow the snake when it eats food)

Place for new snake part

Place for new snake part

Snake eats food when snake’s head is on the food. Once this condition is met, food is relocated to some other cell of the grid. The decision is made inside snakeEatsFood() function. The algorithm used is commonly known as Bounding Box Collision Detection for 2D objects.

蛇在食物上时,蛇会吃食物。 一旦满足此条件,就将食物重新放置到网格的其他某个单元中。 该决定是在snakeEatsFood()函数内部做出的。 所使用的算法通常称为2D对象的边界框碰撞检测。

To grow the snake, head moves one step ahead by leaving an empty cell behind. A new rectangle is created at that empty cell to give the impression of the growth of the snake.

为了长出这条蛇,头部向后移动了一个空单元,向前移动了一步。 在该空单元格处会创建一个新的矩形,以给人蛇的成长印象。


//Grow snake length after eating food
function growSnake(direction) {
   switch(direction) {
      case Down_Arrow:
         var x, y;
         x = snakeHead.getX();
         y = snakeHead.getY()-10;
         resetPositions(createSnakePart(x,y));
      break;
    ...
    ...

//Grow snake length after eating food
function growSnake(direction) {
   switch(direction) {
      case Down_Arrow:
         var x, y;
         x = snakeHead.getX();
         y = snakeHead.getY()-10;
         resetPositions(createSnakePart(x,y));
      break;
    ...
    ...

resetPositions() is almost identical to rePosition(), see the detils under "Move the snake" heading.

resetPositions()与rePosition()几乎相同,请参见“ Move the snake”标题下的detils。

将食物分配到新位置 (Assign the food a new location)

Once the snake eats the food, the food is assigned a new location on the grid. relocateFood() performs this function. It prepares a new grid skipping all the positions occupied by the snake. After creating a new grid array, random number generator function generates a number which is used as an index to the grid and eventually we get the coordinates where we can place the food without overlapping the snake.

蛇吃完食物后,将在网格上为食物分配新位置。 relocateFood()执行此功能。 它准备一个新的网格,跳过蛇占据的所有位置。 创建新的网格数组后,随机数生成器函数将生成一个数字,该数字用作网格的索引,最终我们获得可在不重叠蛇的情况下放置食物的坐标。

当蛇碰到并通过一条边缘时,从相反的边缘重新进入 (Re-enter the snake from the opposite edge when it meets and passes an edge)

This is really simple. We let snake finish its move, then we check if the head is out of boundary. If it is, we assign it new coordinates to make it appear from the opposite end. The code given below works when snake is moving down, for example…

这真的很简单。 我们让蛇完成移动,然后检查头部是否在边界之外。 如果是的话,我们给它分配新的坐标以使其从另一端出现。 例如,下面给出的代码在蛇向下移动时起作用。


if(snakeHead.getY() == stageHeight)
    snakeHead.setY(0);

if(snakeHead.getY() == stageHeight)
    snakeHead.setY(0);

当蛇击中自己或占据整个地面时结束游戏 (End the game when snake hits himself or it occupies the whole ground)

After each move, checkGameStatus() is called by gameLoop() to check if snake has hit himself or not. Logic is fairly simple. The same Bounding Box Collision Detection method for 2D objects is used here. If coordinates of head matches the coordinates of any other part of the snake, the snake is dead – GAME END!

每次移动后,gameLoop()都会调用checkGameStatus()来检查蛇是否被自己击中。 逻辑很简单。 此处,对2D对象使用相同的边界框碰撞检测方法。 如果头部坐标与蛇的任何其他部分的坐标匹配,则蛇已死-GAME END!

现场演示

[sociallocker]

[社交储物柜]

打包下载

[/sociallocker]

[/ sociallocker]

Today we prepared another one good tutorial using KineticJS and HTML5. I hope you enjoyed our lesson. Good luck and welcome back.

今天,我们使用KineticJS和HTML5准备了另一篇很好的教程。 希望您喜欢我们的课程。 祝你好运,欢迎回来。

翻译自: https://www.script-tutorials.com/snake-game-using-html5-canvas-and-kineticjs/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值