В нашем сегодняшнем уроке нам предстоит спасти отважных искателей приключений, спрятавшихся от дождя из фруктового мороженого в палатке. И поможет нам в этом HTML5 и JavaScript.
Мы будем использовать фреймворк BlocksJS, чтобы упростить некоторые вещи. скачайте его с официального сайта и сохраните на своем компьютере.
Теперь создадим HTML-файл, в котором внутри тега head разместим CSS файл BlocksJS, который мы только что загрузили. На момент написания статьи это была версия 0.5.11.
<link rel="stylesheet" href="css/blocksjs-0.5.11.min.css">
Внутри тега body нашего HTML файла добавим элемент-контейнер div с классом "BlocksGame" и подключим файл BlocksJS, который был в скаченном архиве.
<div class="BlocksGame"></div>
<script src="js/blocksjs-0.5.11.min.js"></script>
Наконец мы создаем объект BLOCKS.game
с помощью JavaScript. С помощью свойств width
и height
зададим размер нашей игры равным 800 на 600 пикселей.
var game = BLOCKS.game({
width: 800,
height: 600
});
Вторжение начинается
Давайте начнем с добавления на сцену фона с травой и облаками.
Для этого мы создаем BLOCKS.slice
записав в src
путь к файлу с фоном.
bg = BLOCKS.slice({
src: "sweet-drop-bg.png"
});
Что такое слайс BlocksJS?
По сути своей, слайс похож на спрайт. Слайс – это объект отображения, у которого есть положение и размеры. Это может быть просто форма, но чаще всего это изображение. Оно может содержать несколько фреймов для анимации.
Анимации слайсов можно проигрывать, останавливать, ставить на паузу, перезапускать и т.д. У них также есть свойства, позволяющие установить автоматическое повторное и циклическое проигрывание.
Мы хотим, чтобы фон располагался в нижнем слое, поэтому мы привязываем свойство слайса layer
к игровому слою с индексом 0
. Наконец, мы добавляем слайд в сцену, чтобы сделать фон частью игрового цикла.
bg.layer = game.layers[0];
game.stage.addView(bg);
Давайте создадим функцию dropPopsicle
, которая заставляет наше фруктовое мороженое падать с неба на землю.
var dropPopsicle = function () {
Подобно тому, как мы создали фон, мы собираемся создать наше первое эскимо. Единственное отличие от фона будет в том, что наше эскимо имеет два состояния: одно, когда оно падает, и другое, когда оно разбается о землю.
Чтобы создать эскимо, мы создаем BLOCKS.block
вместо BLOCKS.slice
.Блок выполняет все функции слайса, но поддерживает несколько состояний. В принципе, блок может состоять из слайсов. Так как наше эскимо имеет два различных состояния, мы можем рассматривать его как единый блок, и когда он падает на землю, мы просто меняем его изображение. Мы определяем два слайса блока с помощью имени файла, приписанного свойству src
и имени слайса, приписанного свойству name
.
var popsicle = BLOCKS.block({
slices: [{
name: "falling",
src: "creamsicle.png"
}, {
name: "crashed",
src: "creamsicle-broken.png"
}]
});
Что такое блок BlocksJS?
Термин блок произошел от названия BlocksJS. Преимущество блока состоит в его способности плавно переключаться между слайсами (изображениями или анимациями) без изменений таких свойств, как положение, прозрачность, раз мер и т.д. Например, если у нас есть астронавт летящий сквозь пространство, при клике на которое создается силовое поле. При этом мы скрываем нашего астронавта, а затем создаем нового с силовым полем, в том же месте, скоростью и т.д. Мы можем просто приказать блоку астронавта переключиться на слайс силового поля, и все свойства анимации будут автоматически присвоены этой версии астронавта.
Кроме того блок наследует свойства частиц и вид, тоесть он поддерживает такие функции, как обнаружение столкновения, контроль анимации (воспроизведение, остановка и пауза) и др.
Вы можете посмотреть код блока в скаченном авхиве в папке "src/js"
.
Мы хотим, чтобы эскимо появлялось над фоновым слоем (слой 0
), поэтому мы привязываем его свойство layer
к игровому слою с индексом 2
. Как и раньше, мы добавляем блок в игровую сцену, чтобы сделать эскимо частью игрового цикла.
popsicle.layer = game.layers[2];
game.stage.addView(popsicle);
Теперь у нас есть функция, с помощью которой мы можем порождать новые экземпляры фруктового мороженого, поэтому ничто не мешает нам устроить сладкий дождь. Когда мы создаем новое мороженое, мы не хотим чтобы оно падало с того же места, что и предыдущее, поэтому мы используем Math.random
, который будет возвращать случайное число между 0
и 1
(а на самом деле 0.999999...
). Умножив наше случайное число на ширину экрана, мы получим случайную координату x на экране. А поскольку мы не хотим, чтобы эскимо частично выходило за правую границу экрана, мы умножаем на ширину игрового окна минус ширина эскимо.
Начальное положение эскимо равно его высоте со знаком "минус". Это поместит эскимо в непосредственной близости от верхней части экрана.
popsicle.x = Math.random() * (game.width - popsicle.width);
popsicle.y = -popsicle.height;
Следующее, что нам предстоит сделать в dropPopsicle
- это анимация эскимо при падении на землю.
Для анимации эскимо мы добавляем движок y
, используя метод addMotor
. Благодаря этому эскимо переместится по оси y, имитируя падение с неба. Мы устанавливаем значение свойства object
равным переменной нашего эскимо, свойства duration
- равным 3000мс (3с), а свойства amount
- равным высоте игрового окна с учетом поля хотя бы в несколько миллиметров, чтобы анимация не запускалась у самой границы экрана. Мы также устанавливаем значение свойства easing
равным "easeIn"
, чтобы имитировать гравитацию, замедляя анимацию в самом начале.
Значение свойства callback
метода addMotor
запустит соответствующую функцию, как только анимация закончится. Внутри это анонимной функции мы переключаем эскимо на слайс "crashed"
, который выведет на экран изображение мороженого, размазанного по земле.
game.addMotor("y", {
object: popsicle,
duration: 3000,
amount: game.height - 20,
easing: "easeIn",
callback: function () {
popsicle.setSlice("crashed");
game.addTicker(melt, 2000);
}
}
Что такое движок в BlocksJS?
Движок BlocksJS добавляет поведение к изображению (например, блок или слайс). Присоединив движок, мы можем затвинить такие числовые свойства, как положение или вращение. Движок – это не просто твин, т.к. с его помощью можно перемещать изображение мышкой или контролировать несколько свойств одновременно – например, движение изображения по диагонали, при котором координаты x и y меняются с разной скоростью.
Вы можете посмотреть код блока в скаченном авхиве в папке "src/js"
.
Внутри функции обратного вызова мы создаем тикер, который вызовет через пару секунд функцию melt
, чтобы убрать из игры разрушенное эскимо.
Что такое тикер в BlocksJS?
Тикер BlocksJS похож на функцию отложенного запуска кода в JavaScript - setTimeout
, которая вызывает функцию по истечении установленного периода времени. BlocksJS, однако, имеет некоторые преимущества по сравнению с setTimeout
, поскольку он работает с игровыми часами таким образом, что, если игра приостанавливается тикер также останавливается. Мы также можем точно установить время запуска таймера внутри игрового цикла, которое следует за событием preupdate
и предшествует обновлению игры.
Под жарким полуденным солнцем эскимо не останется на земле долго. Через пару секунд после столкновения каждый экземпляр эскимо вызывает функцию melt
.
Чтобы затвинить свойства эскимо для имитации таяния, мы добавим движок к каждому свойству:
- Движок
alpha
изменит прозрачность эскимо с opaque на transparent.
- Движок
y
переместит эскимо вниз на одну высоту мороженого.
- Движок
cropHeight
обрежет нижнюю часть эскимо, имитируя таяние.
Мы установим easing
значение "easeIn"
, как мы это уже делали с эскимо, чтобы замедлить начало анимации.
var melt = function () {
game.addMotor("alpha", {
object: popsicle,
duration: 800,
amount: -1,
easing: "easeIn",
callback: function () {
game.stage.removeView(popsicle);
popsicle.destroy();
popsicle = null;
}
});
game.addMotor("y", {
object: popsicle,
duration: 1000,
amount: popsicle.height,
easing: "easeIn"
});
popsicle.cropHeight = popsicle.height;
game.addMotor("cropHeight", {
object: popsicle,
duration: 1000,
amount: -popsicle.height,
easing: "easeIn"
});
После того, как эскимо растает, мы удалим его со сцены и из памяти. Для этого мы добавляем функцию обратного вызова одного из двигателей. Разрушение эскимо включает в себя удаление блока на сцене с помощью destroy
метода и удалением всех ссылок на экземпляр мороженого, установив его null
.
Вызываем подкрепление
Мы сможем захватить мир намного быстрее, если у нас будет несколько видов мороженого:
Для поддержки дополнительных фруктовых мороженых мы определяем спецификации эскимо как мы сделали это сделали с creamsicle
, но на этот раз мы размещаем определения в массиве spec.popsicles
. Это дает доступ к дополнительным фруктовым мороженым по имени "bombpop", "popsicle" и "pushUp".
spec.popsicles = [{
name: "creamsicle",
slices: [{
name: "falling",
src: "creamsicle.png"
}, {
name: "crashed",
src: "creamsicle-broken.png"
}]
}, {
name: "bombpop",
slices: [{
name: "falling",
src: "bombpop.png"
}, {
name: "crashed",
src: "bombpop-broken.png"
}]
}, {
name: "popsicle",
slices: [{
name: "falling",
src: "popsicle.png"
}, {
name: "crashed",
src: "popsicle-broken.png"
}]
}, {
name: "pushUp",
slices: [{
name: "falling",
src: "push-up.png"
}, {
name: "crashed",
src: "push-up-broken.png"
}]
}];
Чтобы в полной мере получить доступ к нашему сладкому арсеналу, мы снова обратимся к Math.random
. Мы умножаем длину массиваspec.popsicles
, применяем к этому свойству Math.floor
и получаем случайный индекс (i.e. 0
, 1
, 2
or 3
). Следующий код создает случайное эскимо из массива, который мы только что создали.
var popsicle = BLOCKS.block(spec.popsicles[Math.floor(Math.random() * spec.popsicles.length)]);
Нам нужно, чтобы эскимо упало. Мы делаем это, вызывая функцию dropPopsicle
.
dropPopsicle();
Чтобы сохранить этот дождь из фруктового мороженого мы должны добавить тикер в конце функции dropPopsicle
. Это будет гарантировать, что дождь из фруктового мороженого будет капать и капать.Второй параметр addTicker
- метод задержки перед вызовом функции. В нашей демо-версии мы будем вызывать дождь, сбрасывая эскимо два раза в секунду илbи один раз в 500 миллисекунд.
game.addTicker(dropPopsicle, 500);
В следующем примере фруктовое мороженое падает повсюду случайным образом
Нужна целая деревня
Нам нужно что-то, что можно разрушить. Давайте создадим несколько построек.
Сначала мы создаем новый блок внутри функции, называемой spawnStructure
. Так как наша структура имеет три различных состояния мы определяем три различных срезов в slices
массив нового блока. Каждое определение среза включает имя среза ("inactive"
, "active"
and "broken"
) и имя файла изображения среза через его src
свойство.
spawnStructure = function () {
structure = BLOCKS.block({
name: "tent",
slices: [{
name: "inactive",
src: "tent.png"
}, {
name: "active",
src: "tent-hit.png"
}, {
name: "broken",
src: "tent-broken.png"
}]
});
Давайте поставим палатку на слое с индексом 1
, так чтобы она была над фоном (слой 0
) и за фруктовым мороженоным (слой 2
), а после добавим палатку на игровую сцену через метод addView
.
structure.layer = game.layers[1];
game.stage.addView(structure);
Расположим координату x новой структуры случайным образом так же, как мы до этого делали с эскимо. Мы устанавливаем координату y на уровне земли. Мы также инициализируем свойство numHits
, которое мы будем использовать в будущем, чтобы подсчитать, сколько раз эскимо столкнется со структурой.
structure.x = Math.random() * (game.width - structure.width * 2) + structure.width / 2;
structure.y = game.height - 20;
structure.numHits = 0;
Вместо того, чтобы новая структура просто появлялась ниоткуда, сделаем для нее красивую анимацию.
Мы можем анимировать появление структуры путем добавления пара движков:
- Движок
y
будет смещать структуру по оси у. Мы устанавливаем свойство duration
равным 500 миллисекундам и свойство amount
равной высоте конструкции, взфтой со знаком "минус". Свойство easing
равное "easeOut"
замедлит анимацию ближе к концу.
- Движок
cropHeight
обрежет палатку, чтобы сымитировать ее падение на землю. Мы устанавливаем такие же значения свойства, как и для движка y
, за тем исключением, что в данном случае мы используем положительное значение высоты, т.к. мы хотим, чтобы cropHeight
to увеличивался при анимации.
game.addMotor("y", {
object: structure,
duration: 500,
amount: -structure.height,
easing: "easeOut"
});
structure.cropHeight = 0;
game.addMotor("cropHeight", {
object: structure,
duration: 500,
amount: structure.height,
easing: "easeOut"
});
structures.push(structure);
Определение столкновений
Теперь, когда у нас есть структуры в игре, давайте их уничтожим!
Для определения приземлилось ли эскимо на одну из структур мы будем использовать обнаружение столкновения. Есть много различных методов обнаружения столкновения; в нашем уроке мы будем использовать технику прямоугольник-прямоугольник. Мы будем сравнивать рамку (прямоугольник с той же позицией и шириной и высототой) эскимо и ограничительной рамкой структуры. Если имеется наложение двух прямоугольников, то мы считаем, что произошло столкновение.
Чтобы не нагружать систему, мы проверим это только один раз, когда эскимо упало на землю. Этот тест на столкновение нужно будет сделать для всех структур в простом цикле for
:
for (i = 0; i < structures.length; i += 1) {
if (popsicle.isRectInside(structures[i])) {
...
}
}
Используем метод BlocksJS isRectInside
, который доступен для любого вида (например, блок или фрагмент), чтобы сравнить ограничительную рамку эскимо с ограничительной рамки каждой структуры.
Как BlocksJS обрабатывает прямоугольное обнаружение столкновений?
BlocksJS поддерживает несколько способов обнаружения столкновений. Для определения два прямоугольника наложения можно использовать метод isRectInsideRect
в панели инструментов. Этот метод также доступен на любом виде (например, блок или slices).
BLOCKS.toolbox.isRectInsideRect = function (rect1, rect2) {
return (rect1.x + rect1.width > rect2.x &&
rect1.x < rect2.x + rect2.width &&
rect1.y + rect1.height > rect2.y &&
rect1.y < rect2.y + rect2.height);
};
Функция принимает два прямоугольника в качестве аргументов, а затем сравнивает их используя положение (х, у) и размеры (ширину и высоту), чтобы определить, есть ли столкновение. Если столкновение было обнаружено функция вернет true
.
Когда обнаружено столкновение, мы увеличиваем свойство numHits
у структуры, с которой мы столкнулись.
structures[i].numHits += 1;
Первое столкновение изменяет цвет структуры на мгновение, второе - разрушает его.
При первом столкновении со структурой (т.е., когда свойство numHits
равно 1
) нам нужно переключить слайс структуры в "активное "
состояние. Это приведет к изменению изображения структуры (красный шатер).
Мы также добавили тикер, который вызовет resetStructure
через 2500 миллисекунд, чтобы вернуть палатке ее первоначальный цвет.
if (structures[i].numHits === 2) {
structures[i].setSlice("active");
game.addTicker(resetStructure, 2500, structures[i]);
...
Функция resetStructure
будет переключать состояние структуры с "active" (красная палатка) на "inactive" (коричневая палатка). Но нам надо быть осторожными; к тому времени, как запустится функция, еще одно эскимо может столкнуться со структурой и разрушить ее. Если это случится, структура окажется в разрушенном состоянии, а мы не хотим, чтобы она переключилась обратно в свое активное состояние. К счастью, мы можем определить текущее состояние структуры, используя метод блока getSlice
.Он возвращает активный слайс блока. Мы можем прочитать свойство name
этого активного слайса, чтобы определить, какой слайс отображается в данное время. Таким образом, мы всего лишь перезапускаем структуру, если свойство слайса name
установлено на"active"
.
resetStructure = function (structure) {
if (structure.getSlice().name === "active") {
structure.setSlice("inactive");
}
};
Если в нашу палатку попали дважды (т.е. numHits=2
), то мы устанавливаем кусочек структуры "broken"
. Это изменит изображение палатки и она будет выглядеть как сломанная.
Мы также добавили несколько тикеров, которые разрушают структуру со второго раза, а затем создают новую.
} else if (structures[i].numHits === 2) {
structures[i].setSlice("broken");
game.addTicker(destroyStructure, 3500, structures[i]);
game.addTicker(spawnStructure, 3500);
}
Также, как мы уничтожили мороженое, мы удаляем блок структуры со сцены, вызываем метод destroy
и удаляем структуру из массива.
destroyStructure = function (structure) {
var i;
for (i = 0; i < structures.length; i += 1) {
if (structure === structures[i]) {
structures.splice(i, 1);
break;
}
}
game.stage.removeView(structure);
structure.destroy();
structure = null;
},
Вот как выглядит процесс разрушения:
Герой в каждом из нас
Мы слишком долго наблюдали за уничтожение нашей палатки. Пора дать отпор!
Чтобы остановить вторжение инопланетного эскимо нам нужно нажать те фруктовое мороженое в пух и прах. Добавление слушателя событий для controller
объекта игры позволит нам вызвать функцию, когда пользователь нажимает мышью, либо прикасается к экрану, на сенсорном устройстве. Оба события абстрагируются в "водопроводной" события. Мы добавляем строку "tap" и функцию gameTapped
в качестве параметров controller
объекта addEventListener
. Это потребует gameTapped
функцию когда происходит событие "tap".
game.controller.addEventListener("tap", gameTapped);
Как BlocksJS обеспечивает взаимодействие пользователя?
У BlocksJS есть объект контроллера, который автоматически добавляет слушатели пользовательского взаимодействия в игру. Вместо того, чтобы добавлять слушатели событий для мыши и тач-девайсов, мы делаем это только для тэпа. Объект контроллера обеспечивает эту функцию, реагируя на события мыши и тач-скрина. Зафиксировав одно из этих событий, контроллер запускает событие тэп из контроллера объекта.
element.addEventListener("touchstart", onTouchEvent, true);
element.addEventListener("mousedown", onMouseEvent, true);
Кроме того, контроллер запускает события мыши и сенсорные события отдельно в случае, если вы хотите иметь на них разную реакцию. Например, вы можете отслеживать только нажатие кнопки мыши на элементе, добавив "MouseDown" в качестве первого аргумента метода addEventListener
.
game.controller.addEventListener("mouseDown", onMouseDown);
Еще одна функция объекта контроллера – сообщать положение клика мыши или тач-события относительно самой игры, а не окна, в котором она открыта. Это достигается с помощью функции getElementPos
, которая применяется к родительским элементам и регулирует положение игрового элемента.
Вы можете просмотреть код контроллера в папке "src/js" Github.
Раньше для определения столкновения мороженого со структурой мы определяли оба объекта как прямоугульники. Чтобы зафиксировать столкновение эскимо и тэпа, мы определим первый объект как прямоугольник, а второй – как точку. В отличие от структуры, у тэпа есть только положение, без ширины и высоты.
Как BlocksJS обнаруживает столкновения?
BlocksJS поддерживает несколько различных типов обнаружения столкновений. Чтобы определить, находится ли точка внутри прямоугольника, мы можем использовать метод isPointInsideRect
в панели инструментов. Этот метод доступен при любом режиме просмотра (например, блок или слайсы).
BLOCKS.toolbox.isPointInsideRect = function (point, rect) {
return (point.x > rect.x &&
point.x < rect.x + rect.width &&
point.y > rect.y &&
point.y < rect.y + rect.height);
};
Функция использует точку и прямоугольник как аргументы и определяет, находится ли точка внутри границ прямоугольника. В случае столкновения функция вернет true
.
Функция gameTapped
вызывается при каждом тэпе. Она имеет только один аргумент: объект point
, который включает в себя положение этого тэпа по осям Х и Y относительно игры.
Чтобы проверить, щелкнул ли пользователь по эскимо, мы зацикливаем массив мороженого и вызываем метод isPointInside
каждого эскимо. Если метод возвращает true
, то мы щелкнули по эскимо.
gameTapped = function (point) {
var i;
for (i = 0; i < popsicles.length; i += 1) {
if (popsicles[i].isPointInside(point)) {
...
}
}
},
Если мы щелкнем по эскимо, то сможем отомстить тремя способами.
Сначала мы остановим падение эскимо. Мы делаем это с помощью метода removeMotors
, который будет удалять добавленный нами движок y
, заставляющий эскимо двигаться.
Далее мы собираемся поработать над исчезновением мороженого. Для этого мы добавляем новый движок "alpha"
. По умолчанию вид начинается с альфа-значением 1
(полностью непрозрачный). Если мы установим свойство amount
нашего движка на значение -1
, движок "alpha"
изменит это значение с 1
на 0
(полностью прозрачный).
Наконец мы добавляем тикер, который будет вызывать функцию destroyPopsicle
после задержки 500 мс.
popsicles[i].removeMotors();
game.addMotor("alpha", {
object: popsicles[i],
duration: 500,
amount: -1
});
game.addTicker(destroyPopsicle, 500, popsicles[i]);
Поиграйте в нашу игру и уничтожьте мороженое, кликая на него, чтобы спасти палатку и тех, кто в ней!
Наш урок закончен, но я призываю вас не останавливаться, а продолжить эту игру дальше. Например, фруктовое мороженое может падать все быстрее и быстрее, палатка может быть прокачена до бункера, так что потребуется больше фмороженго, чтобы уничтожить ее. Весь код и изображения доступны для скачивания. Дерзайте!