Lecaw

Учимся создавать Web-сайты

Игра змейка на HTML5 Canvas и KineticJS

July 10, 2013 283hits

Прежде чем начать делать новую игру, Вы должны полностью понять концепцию игры. Необходимо иметь четкое представление о том, как работает игра, не обращая внимание на мелкие детали. На первом этапе, отвечаем на вопрос "что" мы хотим создать, следующий вопрос "как это сделать", но это уже на следующих этапах. Позвольте мне проиллюстрировать этот метод на примере популярной игры «Змейка». Раньше она была очень популярной, еще на консолях и старых мобильных телефонах.

 

Концепция игры

В самом начале игры, вы играете за маленькую змейку, которая растет на протяжении всей игры до бесконечности. Игрок может управлять направлением движения. Со временем, скорость змейки увеличивается. После того как вы "съели" рандомный блок, змейка увеличится на единицу. Чем больше длинна и скорость змейки, тем труднее будет играть.

Мы можем использовать технику раскадровки.

Согласно Википедии: "Раскадровка — последовательность рисунков, которые используются в качестве, вспомогательных элементов при создании фильмов, мультфильмов или рекламных роликов." В нашем случае вместо рисунков будем использовать блоки.

Ниже представлена наша раскадровка.

Чтобы вы все поняли, я добавлю описание для каждого экрана раскадровки:

Экран 1: Змейка (квадрат) ждет нажатия клавиши для начала движения. Круг обозначает еду.

Экран 2: После поедания - длина змейки увеличивается, а еда появляется в другом случайном месте.

Экран 3: Змейка может вновь войти в игровую зону через противоположный край игрового поля.

Экран 4: Змейка умирает, если укусит сама себя.

UML диаграммы состояний, могут помочь понять различные "состояния" змейки во время игры.

Согласно Википедии: "Диаграмма состояний - это, фактически, диаграмма состояний из теории автоматов cо стандартизированными условными обозначениями, которая может определять множество систем от компьютерных программ до бизнес-процессов".

Ниже представлена наша диаграмма состояний:

На схеме по краям представлены действия, а на овалах - состояния змейки. Игра начинается с маленькой змейки размером в один блок. Вы можете перемещать её в четырех направлениях с помощью клавиш со стрелками. При смене направления движения, определяется состояние, а затем "Решение", как поменять направление движения. Змейка ест пищу, когда сталкиваются с ней во время движения. Существует также "мертвые" зоны.

Есть много других вариантов UML диаграмм, которые могут помочь вам описать свой собственный игровой процесс. Эти диаграммы также очень полезны при работе в команде, так создать игру будет еще проще и быстрее, если каждый отвечает за отдельный этап игры.

Структура игры

Игровая площадка имеет размер в 200 на 200 пикселей, каждая клетка имеет размер 10 на 10. Для подготовки сетки у нас есть функция initGrid(). Ширина и высота змейки также равна - 10 на 10 пикселей. Для удобства программирования я взял высоту и ширину змейки равной размерам одной клетки.

function initGrid() {
   //*****Инициализация 
сетки
   ...
    cell = {"col":col*snakeHeight, "row":row*snakeWidth};
    ...
}

Мы используем виртуальную сетку. Во время инициализации игрового профиля, эта сетка помогает нам определить точки (ячейки ... если быть точнее), где мы можем расположить змейку и еду случайным образом. Генератор случайных чисел - randomCell (клетки) выдает случайные числа, которые мы используем в качестве индекса массива сетки и получаем координаты, которые хранятся в данном индексе. Вот функция генерации случайных чисел:

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

Math.random и Math.floor - это Javascript функции.

Следующий код демонстрирует использование сетки и функции генерации случайных чисел:

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 - это конструктор, который закрашивает начальный прямоугольник (змейку). Позже, при увеличении размеров змейки, мы добавим больше прямоугольников. Каждому прямоугольнику присваивается номер его положения в массиве, точно также как на сервисе по раскрутке сайтов. Пока имеется только один прямоугольник, он имеет положение 1.

snakePart.position = snakeParts;

snakeParts - это счетчик, который подсчитывает количество ячеек в массиве змейки. Фактически, если Вы сохраняете значение имени, то свойства для всех объектов snakePart и KineticJS присваиваются объектам в массиве.

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

Вы можете добавлять пользовательские свойства объекта Kinetic.Rect в динамический или любой другой объект, при помощи функции snakePart.position.

Вы наверняка спросите, почему нам необходимо задавать положение, когда можно присвоить KineticJS готовый массив? Чуть позже мы с этим разберемся.

Также необходимо установить два идентификатора, для упрощения управления змейкой, а именно движение её головы и хвоста. В самом начале игры голова и хвост это один и тот же прямоугольник.

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

Теперь мы закончили с настройкой змейки. Для создания еды, которая появляется в виде простого круга радиусом в 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'
});

Kinetic.Circle будет создавать еду для нашей змейки. Круг помещается ровно в центре клетки 10 на 10, радиус окружности равен 5.

После создания основных форм для нашей игры и позиционирования их на игровой области/сетке мы должны добавить формы для Kinetic.Layer, затем добавить данный слой к Kinetic.Stage.

// Добавляем формы к слою (змейка и еда)
 var layer = new Kinetic.Layer();
 layer.add(snakePart);
 layer.add(food);
 // добавить слой на 1 этап 
 stage.add(layer);

 

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

Свойство  'container' должно идентифицировать положение змейки на HTML5 Canvas.

Начальный экран будет выглядеть примерно так:

Основной цикл игры

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

SetInterval является функцией Javascript, она вызывает функции и представляет их в виде аргумента через заданные интервалы времени. В нашем случае gameLoop() это функция, которая управляет всей игрой. Вот код:

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

Поведение gameLoop() довольно очевидно. Функция заставляет двигаться змейку в соответствии с нажатой клавишей. Если змейка задевает себя, игроку выдается сообщение - Game Over, а также останавливает Javascript функцию clearInterval().

Для захвата клавиш со стрелками я использовал события обработчика KeyDown, который реагирует на нажатия. Он задает переменную 'where', если используются gameLoop() передающий код фактического перемещения функции move().

$( document ).ready(function() {
   $(document).keydown(function(e) {
      switch(e.keyCode) {
         // Игрок нажимает кнопку вверх
         case Up_Arrow:
            where = Up_Arrow;
         break;
         // Игрок нажимает кнопку вниз
         case Down_Arrow:
            where = Down_Arrow;
         break;
         // Игрок нажимает кнопку направо
         case Right_Arrow:
            where = Right_Arrow;
         break;
         // Игрок нажимает кнопку налево
         case Left_Arrow:
            where = Left_Arrow;
         break;
      }
   });
});

Как Вы уже догадались, функция move() является главной в этой игре, а функция Kinetic.Animation отвечает за перемещение объектов. Все, что Вам нужно сделать, это задать новые координаты для объектов, используя метод start(). Чтобы предотвратить запуск анимации бесконечными запросами используйте метод stop() сразу после метода start(). Попробуйте самостоятельно использовать данный метод.

function move(direction) {
   //Подсказка: движение хвоста змейки
   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:
        ...
        ...

Перемещение змейки

Змейка разделена на 10 квадратов по 10 пикселей. Метод перемещения змейки очень прост. Мы выбираем хвост и помещаем его перед головой кроме тех случаев, когда змейка состоит из одного квадрата. Голова и хвост указывают на неправильные части. Требуется пересчет числа позиций и указателей. Для этого используем функцию rePosition(). Как это работает показано в схеме ниже:

Рост змейки

Змейка растет, когда её голова находится на блоке с едой. Как только это условие будет выполнено, пища становится частью змейки. Решение принимается внутри функции snakeEatsFood(). Данный алгоритм широко используется у разработчиков и известен как Bounding Box Collision Detection для 2D объектов.

При росте, голова перемещается на один шаг вперед, оставив пустую ячейку позади. Далее в пустой ячейке создается пустой прямоугольник, тем самым создается эффект роста змейки.

//Рост змейки после поедания
function growSnake(direction) {
   switch(direction) {
      case Down_Arrow:
         var x, y;
         x = snakeHead.getX();
         y = snakeHead.getY()-10;
         resetPositions(createSnakePart(x,y));
      break;
    ...
    ...

Расположение еды

Для случайной генерации еды на игровой области, мы используем функцию relocateFood(). После создания нового массива сетки, функция генерации случайных чисел генерирует номер, который используется в качестве индекса, в итоге мы получим координаты расположения еды.

Поля игровой области

Мы проверяем находится ли голова вне границы игрового поля. Если это так, то мы присваиваем ей новые координаты с противоположной стороны. Приведенный ниже код работает, когда змейка движется вниз:

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

Конец игры, в случае проигрыша или выйгрыша

Игра заканчивается в случае, если змейка натыкается на себя или все игровое поле занято змейкой. После каждого хода, функция checkGameStatus() вызывает метод gameLoop() для проверки пересечения с телом змейки. Логика довольно проста. Здесь так же используется алгоритм Bounding Box Collision Detection для 2D объектов. Если координаты головы совпадут с любыми координатами тела змейки, то она мертва - конец игры!

Сегодня мы подготовили хороший урок по использованию KineticJS и HTML5. Надеюсь Вам понравился наш урок.

ДЕМО

СКАЧАТЬ

 

Дополнительная информация

Роман Воеводин

Воеводин Роман - дизайнер, модератор, соучредитель и член команды Lecaw.

Эл. почта
RATTING:
(1 Голосовать)

Оставить комментарий