В этом уроке я создам в небольшую игру типа Змейки, а параллельно подробно, шаг за шагом, расскажу вам как начать работу с Crafty - популярном движке для браузерных игр.
Crafty - игровой движок, написанный на JavaScript. Его ключевой особенностью является большое колличество компонентов, которые можно добавлять к объектам в игре. Например , если вы хотите создать персонажа, который может двигаться по карте, - просто добавьте компонент 2D. Если вы хотите управлять столкновения персонажа с окружающими предметами, - просто добавьте компонент Collision (столкновения). Компонент может сам содержать другие компоненты, например, игрок может управлять своим персонажем с помощью клавиатуры и мыши. При этом мы создадим компонент Playable, содержащий компоненты Mouse(мышь) и Keyboard (клавиатура):
Первое, с чего мы начнем - это создание простой HTML страницы, к которой подключим скрипт Crafty crafty-min.js
и файл game.js
, в который мы будем записывать код нашей игры:
<script src="js/crafty-min.js"></script>
<script src="js/game.js"></script>
Далее мы должны инициализировать Crafty, задав при этом размеры холста. Делается это так:
// Начнем работу только когда вся страница загрузится
window.onload = function () {
// Инициализация Crafty
Crafty.init(400, 400); // создаем игровое поле шириной 400px и высотой 400px
};
Далее, нужно указать Crafty, что мы хотим использовать Canvas:
// Инициализация Crafty
Crafty.init(400, 400);
// Инициализация Canvas
Crafty.canvas.init();
В нашей игре будут две сцены : main (главная) на которой будет происходить игровой процесс и menu - сцена с экраном приветствия. Сейчас мы создадим сцену "main" :
// Создание главной сцены
Crafty.scene("main", function() {
});
Когда наша сцена инициализирована, мы добавляем туда фоновое изображение, которое будет представлять собой игровое поле с Crafty.e(). Crafty.e() используется для создания элемента, задействованного в игре, или компонентов этого элемента согласно параметрам. :
// Создание главной сцены
Crafty.scene("main", function() {
// Добавим на карту фоновое изображение
Crafty.e("2D, Canvas, Image").image("img/map.png");
});
Здесь мы используем компоненты:
- Image - чтобы получить доступ к функционалу, работающему с картинками
- 2D - чтобы позиционировать изображение по осям x и y.
- Canvas - для рисования на холсте
<canvas>
Теперь, когда наша сцена готова, мы продолжим работу как только наши изображения загрузятся:
// Загрузка ассетов
Crafty.load(["img/map.png"], function () {
// Запуск главной сцены
Crafty.scene("main");
});
Crafty.load() отвечает за предварительную массива изображений, потом выполняет кол-бэк: происходит запуск главной сцены.
Теперь мы создадим змею (которая на самом деле состоит из пустых панцирей черепхи) ).
Сначала необходимо обозначить спрайт панциря в Crafty ::
// Картинки объединены в спрайт img/items.png
с кадрами размером 32x32 px
Crafty.sprite(32, "img/items.png", {
apple: [0, 0], // яблоко располагается в кадре с координатами (0,0)
fruits: [1, 0], // фрукты - в кадре с координатами (0,0)
egg: [2, 0],
shell: [3, 0],
flask: [4, 0]
});
Не забудьте добавить наш спрайт в загрузчик
Crafty.load(["img/map.png", "img/items.png"], function () {
Crafty.scene("main");
});
Теперь мы можем создать наш компонент Snake (Змея) через Crafty.c().
// Создание компонента Snake
Crafty.c("Snake", {
init: function() {
// Добавление компонентов:
// - 2D размещение
// - для метода отображения Canvas
// - файл спрайта
this.addComponent("2D, Canvas, shell");
// Позиционирование змеи на холсте
this.attr({
x: 100,
y: 200
});
}
});
Первый параметр Crafty.c () - имя компонента, второй сам компонент.
Когда компонент создан, автоматические активируется функция init() для его компонентов.
addComponent()позволяет добавить другие компоненты к элементу.
Отметим здесь, что мы добавили компонент shell, который был создан Crafty.sprite()
, он обеспечивает доступ к спрайту со змеей.
attr() позволяет изменять определенные атрибуты вашего объекта, такие как положение (x, y) вращение или размер (ширина, высота).
Добавим змею в главную сцену:
Crafty.scene("main", function() {
Crafty.e("2D, Canvas, Image").image("img/map.png");
Crafty.e("Snake");
});
В этом шаге мы добавим обработку столкновений и зададим клавиши для управления на змеей.
Теперь создаем компонент Wall путем добавления к нему Collision - компонент для обнаружения столкновения между двумя любыми областями.
// Элемент стены
Crafty.c("Wall", {
init: function() {
this.addComponent("2D, Canvas, Collision");
}
});
Теперь создайте компонент Walls, который будет содержать наши четыре стены.
//Компонент Walls (стена) содержит 4 стены, которые определяют игровое поле
Crafty.c("Walls", {
init: function() {
// Создание стены на севере
Crafty.e("Wall")
.attr({x: 16, y: 16, w: 368, h: 16}) // Позиционирование стены
.collision(new Crafty.polygon([0,0], [368,0], [368, 16], [0, 16])); // Область столкновения
// Стена на востоке
Crafty.e("Wall")
.attr({x: 368, y: 16, w: 16, h: 336})
.collision(new Crafty.polygon([0,0], [16,0], [16, 336], [0, 336]));
// Стена на юге
Crafty.e("Wall")
.attr({x: 16, y: 336, w: 368, h: 16})
.collision(new Crafty.polygon([0,0], [368,0], [368, 16], [0, 16]));
// Стена на западе
Crafty.e("Wall")
.attr({x: 16, y: 16, w: 16, h: 336})
.collision(new Crafty.polygon([0,0], [16,0], [16, 336], [0, 336]));
}
});
Здесь мы задаем высоту и позицию для каждой стены с помощью attr()
, потом определяет область столкновения (Hitbox) с помощью .collision().
Чтобы обозначить область столкновения (Hitbox), мы используем new Crafty.polygon() с указанием координат вершин этой области.
Теперь мы можем добавить стены в нашу главную сцену:
Crafty.scene("main", function() {
Crafty.e("2D, Canvas, Image").image("assets/map.png");
Crafty.e("Walls");
Crafty.e("Snake");
});
Теперь нам нужно протестировать столкновения путем добавления комбинаций клавиш на клавиатуре для управления змеей.
Crafty.c("Snake", {
init: function() {
// ... Код инициализации
// Текущее направление Змеи
this.currentDirection = "e";
// Перемещаем змею каждый кадр
this.bind("EnterFrame", function() {
this.move(this.currentDirection, 1.2);
});
// Изменение направления при нажатии клавиш со стрелками
this.bind('KeyDown', function(e) {
this.currentDirection = {
38: "n", // вверх
39: "e", // направо
40: "s", // вниз
37: "w" // налево
}[e.keyCode] || this.currentDirection;
});
// Если змея ударяется о стену, перезапускаем игру
this.onHit("Wall", function(){
Crafty.scene("main");
});
}
});
bind() позволяет подключать событие. Здесь мы подключаем EnterFrame , который срабатывает каждый кадр (т.е. 60 раз в секунду).
move() происходит от компонента "2D" и позволяет переместить ваш элемент с определенного пикселя в заданном направлении (север, юг…).
Событие "KeyDown" происходит от компонента "Keyboard" и запускается при нажатии клавиши. В настройках колбэка этого события содержиться вся информация о нем. Нас интересует код клавиши: e.keyCode. Зная, какую клавишу мы нажали, можем изменить направление движения нашей змеюки.
onHit() обнаруживает столкновение.
Здесь мы хотим обнаружить натолкнулась ли змея на стену и, если да, то начинаем игру сначала.
Теперь, когда наша змея перемещается, мы добавляем фрукты, которые нужно собрать, чтобы заработать очки.
Создайте новый компонент Food (Еда)
// Компонент Food
Crafty.c("Food", {
init: function() {
this.addComponent("2D, Canvas, fruits, Collision");
this.attr({
w: 32,
h: 32,
// Фрукты появляются в случайном месте на карте
x: Crafty.math.randomInt(32, 336),
y: Crafty.math.randomInt(32, 304)
});
}
});
Crafty.math.randomInt() генерирует случайное целое число в заданном диапазоне. Это позволяет нам позиционировать плод случайно на карте.
Теперь мы можем добавить первые плоды на главную сцену.
Crafty.scene("main", function() {
Crafty.e("2D, Canvas, Image").image("assets/map.png");
Crafty.e("Walls");
Crafty.e("Snake");
Crafty.e("Food");
});
Теперь нам нужно увеличить скорость змеи после поедания фруктов.
// Создание компонента Snake
Crafty.c("Snake", {
init: function() {
// Код сокращен для удобства чтения ...
// Скорость перемещения змеи
this.speed = 1;
// Перемещаем змею каждый кадр
this.bind("EnterFrame", function() {
this.move(this.currentDirection, this.speed);
});
// Если съели фрукты
this.onHit("Food", function(collisions){
// Удаляем старые фрукты
collisions[0].obj.destroy();
// Создаем новые фрукты
Crafty.e("Food");
// Увеличиваем скорость
this.speed += 0.125;
});
}
});
onHit() предоставляет широкий спектр различных объектов, на которые наткнулась наша змея во время движения. В нашем случае нас интересует самое первое столкновение: collisions[0]. Когда змея натолкнется на фрукты, мы заставим их исчезнуть: collisions[0].obj.destroy();
. Далее мы создаем новые плоды и увеличиваем скорость змеи.
Теперь мы должны удлиннить тело змеи когда она ест фрукты. Для этого нам придется реорганизовать наш компонент Snake и создать новый для каждой части его тела SnakePart. Начинаем с того, что создаем компонент SnakePart используя части Snake.
// Создание составных частей змеи
Crafty.c("SnakePart", {
init: function() {
this.addComponent("2D, Canvas, shell, Collision");
},
// Голова змеи
head: function(snake) {
// Позиция по умолчанию
this.attr({ x: 100, y: 200 });
this.speed = 1;
this.direction = "e";
this.bind("EnterFrame", function() {
this.move(this.direction, this.speed);
});
// Если змея натолкнулась на стену, перезапускаем игру
this.onHit("Wall", function(){
Crafty.scene("main");
});
// Если вы натолкнулись на фрукты
this.onHit("Food", function(collision) {
// Уничтожение фруктов
collision[0].obj.destroy();
// Создание новых фруктов
Crafty.e("Food");
// Увеличиваем скорость
this.speed += 0.075;
});
return this;
}
});
У нас будет два типа "SnakePart" - голова (head) и тело (body), которое мы создадим позже. Части, которые являются общими для головы и змеи могут остаться в init(), остальное добавляем в голову (head), которая является первым, управляющим, звеном цепочки. Позже компонент "Snake" будет использован повторно:
// Создание компонента Snake
Crafty.c("Snake", {
init: function() {
this.addComponent("2D, Canvas, Keyboard");
// Голова змеи
this.head = Crafty.e("SnakePart").head(this);
// Хвост змеи
this.tail = this.head;
// Изменение направления при нажатии клавиш со стрелками
this.bind('KeyDown', function(e) {
this.head.direction = {
38: "n",
39: "e",
40: "s",
37: "w"
}[e.keyCode] || this.head.direction;
});
}
});
Мы видим, что когда змея создается, она состоит из одного элемента, который является и ее головой, и ее хвостом. Голова задает направление движения змеи.
Теперь мы хотим:
- Добавить звенья змеи, когда она ест фрукты.
- Сделать так, чтобы каждое последующее звено шло за предыдущим.
// Создание составных частей змеи
Crafty.c("SnakePart", {
init: function() {
this.addComponent("2D, Canvas, shell, Collision");
// Массив с координатами точек, через которые прошла змея
this.steps = [];
// Сохранять будем только 10 последних значений
this.bind("EnterFrame", function() {
// Сохранение происходит каждый кадр
this.steps.push({ x: this.x, y: this.y });
// Если у нас более 10 значений, то
if(this.steps.length > 10) {
// удаляем устаревшие данные
this.steps.shift();
}
});
},
// Голова змеи
head: function(snake) {
// Код сокращен для удобства чтения ...
// Когда змея съедает фрукт
this.onHit("Food", function(collision) {
// Код сокращен для удобства чтения ...
// она растет
snake.tail.append(snake);
});
return this;
},
// Тело змеи
body: function(snake, parent) {
// Позиция по умолчанию
this.attr(parent.steps[0]);
// каждая часть тела змеи будет следовать за предыдущей
this.bind("EnterFrame", function() {
this.attr(parent.steps[0]);
});
return this;
},
append: function(snake) {
snake.tail = Crafty.e("SnakePart").body(snake, this);
}
})
Чтобы разные звенья следовали друг за другом, нужно сохранить последние позиции предыдущего звена. Эта часть является общей для головы и тела змеи, поэтому мы можем поместить ее в init().
Уникальная особенность одной части тела змеи заключается в ее следовании за предыдущей частью, а значит, в событии "EnterFrame" нам нужно заменить текущую позицию на прошлую позицией предыдущего звена. Далее задаем команду append() которая добавляет звено в цепочку.
Игра начинает приобретать форму, теперь нужно добавить домашний экран и очки. Сначала обозначим, что очки – это изменяющаяся величина:
window.onload = function () {
// Ваш текущий счет
var currentScore = 0;
// ..
});
Затем сделайте компонент Score.
// Создание компонента Score
Crafty.c("Score", {
init: function() {
this.addComponent("2D, DOM, Text");
this.attr({ x: 40, y: 40, w: 200 });
// Параметры CSS как в JQuery
this.css({ font: '16px Verdana', color: "white" });
// Сброс счета
currentScore = 0;
},
// Увеличиваем и показываем новый счет
increment: function(by) {
currentScore += by;
this.display();
return this;
},
display: function() {
// Показываем счет на экране
this.text("Score: "+currentScore);
return this;
}
});
Для отображения счета мы используем DOM модель и компонент текст чтобы воспользоваться форматированием с помощью CSS. Хочу заметить, что тоже самое можете сделать с текстом при помощи Canvas.
Puis on crée une entité Score dans le serpent :
Crafty.c("Snake", {
init: function() {
// Код сокращен для удобства чтения ...
// Создание и отображение заработанных очков
this.score = Crafty.e("Score").display();
}
});
Остается только увеличить счет, когда змея хватает плоды:
// Код сокращен для удобства чтения ...
Crafty.c("SnakePart", {
head: function(snake) {
this.onHit("Food", function(collision) {
// Счет увеличивается в соответствии с текущей скоростью
snake.score.increment(this.speed*1000);
});
}
});
Остается создать сцену menu:
Crafty.scene("menu", function() {
// Если счет не нулевой, покажем его
if(currentScore !== 0) {
Crafty.e("2D, DOM, Text")
.attr({ x: 40, y: 40, w: 200 })
.css({ font: '16px Verdana', color: "black" })
.text("Вы набрали: "+currentScore+" очков");
}
// Инструкции, чтобы начать игру
Crafty.e("2D, DOM, Text, Keyboard")
.attr({ x: 40, y: 80, w: 200 })
.css({ font: '16px Verdana', color: "black" })
// Инструкции
.text("Нажмите клавишу со стрелками для старта")
// если нажата одна из указанных клавиш, игра запускается
.bind('KeyUp', function(e) {
if(e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40) {
Crafty.scene("main");
}
});
});
В этой сцене мы показывать счет, если таковой имеется, и инструкцию как начать игру. Если игрок нажимает на одну из кнопок со стрелками, игра запускается. Теперь нужно заменить запуск главной сцены на сцену меню с двумя опциями:
- Загрузка игры
- Окончание игры
Crafty.load(["img/map.png", "img/items.png"], function () {
Crafty.scene("menu");
});
// Если змея ударяется стену, перезапускаем игру
this.onHit("Wall", function(){
Crafty.scene("menu");
});
Оттуда множество вариантов, доступных нам чтобы улучшить игру. Лично я вижу 4, которые, кажется важными:
- Сбалансировать скорость движения змеи
- Уменьшить HitBox с помощью звена змеи
- Добавить 2 звена по умолчанию
- Блокировать движение в обратную сторону
Первый пункт довольно простой, я предлагаю следующее:
this.speed = 1;
// Если вы слопали фрукты
this.onHit("Food", function(collision) {
// Код сокращен для удобства чтения ...
// Увеличить скорость
this.speed += 0.075;
});
Дальше мы будем уменьшать HitBox звеньями змеи:
Crafty.c("SnakePart", {
init: function() {
// Код сокращен для удобства чтения ...
this.collision(new Crafty.polygon([8,8], [24,8], [24, 24], [8, 24]))
}
});
Затем добавим 2 звена по умолчанию когда змея создается:
Crafty.c("Snake", {
init: function() {
// Код сокращен для удобства чтения ...
// голова змеи
this.head = Crafty.e("SnakePart").head(this);
// Хвост змеи
this.tail = this.head;
// Потом добавляем 2 звена по умолчанию, когда змея растет:
this.tail.append(this, true);
this.tail.append(this, true);
}
});
В конце блокируем противоположное направление движения:
// Изменение направления движения при нажатии клавиш со стрелками
this.bind('KeyDown', function(e) {
// Сохранение предыдущей позиции
var oldDirection = this.head.direction;
// Сохранение новой позиции
var newDirection = {
38: "n",
39: "e",
40: "s",
37: "w"
}[e.keyCode] || this.head.direction;
// Если новое направление не противоположно первоначальному,
// направление движения нашей змеи изменяется.
if(newDirection !== {"n":"s", "s":"n", "e":"w", "w":"e"}[oldDirection]) {
this.head.direction = newDirection;
}
});
На этом все. Здесь вы можете увидеть демо-версию нашей браузерной игры на HTML5 "Змейка". Сама же тема создания игр на HTML5 настолько интересна, что я еще не раз вернусь к ней на страницах этого блога.