Вначале, прежде чем приступить к созданию новой игры, вам необходимо подготовить концепцию (логику) игры. Вы должны иметь четкое представление об этом. Вам нужно развить высокий уровень понимания, не вдаваясь в мелкие детали. На первом этапе мы находим ответ «что» мы хотим создать, и мы оставляем за собой ответы «как это сделать» для последующих этапов. Позвольте мне проиллюстрировать этот метод на примере. Давайте разработаем популярную игру «Змея». Эта игра была популярна давно, даже на консолях и старых сотовых телефонах с текстовыми пользовательскими интерфейсами.
Концепция игры
Первоначально на экране появляется змея небольшого размера, которая продолжает работать до конца игры, как бесконечный раннер. Игрок может изменить только направление. Скорость змеи увеличивается со временем. Длина змеи также увеличивается после употребления случайно появляющейся пищи. Увеличение длины и скорости змеи со временем усложняет игру.
Мы можем использовать технику раскадровки для графического представления нашей идеи.
Согласно Википедии: «Раскадровки — это графические организаторы в виде иллюстраций или изображений, отображаемых последовательно с целью предварительной визуализации движущегося изображения, анимации, анимационной графики или последовательности интерактивных медиа».
Вот наш раскадровка:
Для вашего удобства я добавляю описание каждого экрана раскадровки:
Экран 1: Змея (квадрат), ожидающая нажатия клавиши, чтобы начать движение. Круг показан как элемент питания.
Экран 2: После еды (удара) предмета питания длина змеи увеличивается, и предмет пищи появляется в другом случайном месте.
Экран 3: Змея может повторно войти в игровую зону с противоположного края после того, как покинула ее с края.
Экран 4: Змея умирает после укуса себя
Здесь диаграмма UML Statechart может также помочь понять различные «состояния» змеи во время игры.
Согласно Википедии: «Диаграмма состояний — это тип диаграммы, используемой в информатике и смежных областях для описания поведения систем. Диаграммы состояний требуют, чтобы описанная система состояла из конечного числа состояний; иногда это действительно так в то время как в других случаях это разумная абстракция. Существует множество форм диаграмм состояний, которые немного отличаются и имеют разную семантику ».
Вот наша диаграмма Statechart:
На диаграмме ребра представляют действия, а овалы — состояния, в которых находится змея после определенного действия. Игра начинается с пустой змеи. Вы можете перемещать змею во всех четырех направлениях, используя клавиши со стрелками. При перемещении в любом направлении при нажатии клавиши змея переходит в состояние «Выбор», чтобы определить, какая клавиша была нажата, а затем снова перемещается в соответствующем направлении. Змея ест пищу, когда сталкивается во время движения. Существует также «мертвое» состояние, если змея поражает себя во время движения.
Вы также можете добавить больше диаграмм состояния для заметных объектов, чтобы уточнить. Существуют и другие UML-диаграммы, которые могут помочь вам описать ваш игровой проект. Эти диаграммы не только полезны для вас, но и помогают, если вы работаете в команде, что делает общение в команде простым и однозначным.
Структура игры
Игровая зона практически разделена на сетку 200–200 пикселей, имеющую ячейки 10–10 пикселей. Функция initGrid () подготавливает сетку. Как вы можете догадаться из кода, ширина и высота змеи также составляют 10-10 пикселей. Так что, как трюк программирования, я использовал высоту и ширину змеи, чтобы представить размеры одной ячейки.
function initGrid() { //*****Initialize Grid ... 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, но мы говорим о массиве? Фактически, если вы сохраните значение name: property одинаковым для всех объектов snakePart, KineticJS вернет все эти объекты в виде массива, если вы спросите так:
var snakePartsArray = stage.get('.snake');
Вы увидите использование этой функции в действии позже в коде.
snakePart.position показывает, как можно динамически добавлять пользовательские свойства в объект Kinetic.Rect или в любой другой объект.
Зачем нам нужна позиция, когда 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 создает еду для нашей игры змеи. Здесь добавляем +5 к координатам x и y, чтобы поместить круг точно в центр ячейки 10 × 10, при условии, что радиус круга равен 5.
После того, как мы закончили с созданием основных фигур для нашей игры и их позиций в игровой области / сетке, нам нужно добавить эти формы в 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);
объект ‘stage’, использованный в коде выше, уже был создан в начале с использованием следующего фрагмента кода…
//Stage var stageWidth = 200; var stageHeight = 200; var stage = new Kinetic.Stage({ container: 'container', width: stageWidth, height: stageHeight });
Свойство контейнера для Stage должно знать идентификатор div, в котором мы хотим показать наш 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 ().
Для захвата клавиш со стрелками я использовал обработчик событий нажатия клавиш jQuery, чтобы реагировать на нажатые клавиши. Он устанавливает переменную ‘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; } }); });
Как вы уже могли догадаться, функция move () является настоящим мозгом этой игры, а Kinetic.Animation управляет фактическим движением объектов. Все, что вам нужно сделать, это установить новые местоположения для желаемых объектов, а затем вызвать метод start (). Чтобы анимация не запускалась бесконечно, вызывайте метод stop () сразу после start (). Попробуйте удалить метод 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: ... ...
Переместить змею
Змея разделена на маленькие квадраты размером 10 × 10 пикселей. Квадрат спереди — голова, а сзади — квадрат. Техника перемещения змеи проста. Мы выбираем хвост и помещаем его перед головой, кроме случаев, когда есть только одна часть змеи. Номер позиции, назначенный каждой части змеи, теперь не соответствует порядку. Указатели головы и хвоста указывают на неправильные части. Нам нужно переместить указатели и номера позиций. Это делается путем вызова rePosition (). Это работает, как показано на рисунке ниже …
Вырасти змею, когда она ест пищу
Змея ест пищу, когда голова змеи находится на еде. Как только это условие выполнено, еда перемещается в какую-то другую ячейку сетки. Решение принимается внутри функции 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; ... ...
resetPositions() is almost identical to rePosition(), see the detils under «Move the snake» heading.
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.
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);
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!
Live Demo
download in package
Today we prepared another one good tutorial using KineticJS and HTML5. I hope you enjoyed our lesson. Good luck and welcome back.