Принимаете ли вы участие в обсуждении или читаете статьи о нем, вы должны знать, что, хотя это все еще спецификация, HTML5 готов к использованию … прямо сейчас.
Он не только улучшает семантическую структуру Интернета, но и воплощает в жизнь новые API JavaScript, чтобы упростить внедрение видео , аудио и даже технологий геолокации (что особенно интересно для мобильных веб-приложений).
Это не то, где API-интерфейсы останавливаются. Они также могут быть использованы для создания игр, используя Canvas 2D API . И это именно то, что мы собираемся сделать сегодня.
Концепция
Предварительное изображение этой статьи может указывать на то, что мы собираемся построить, но оно все еще довольно расплывчато, верно?
Чтобы продемонстрировать потенциал Canvas 2D API, мы создадим игру, которая создавалась во Flash много раз прежде: игру «лови падающие предметы».
Как вы, наверное, знаете, большинство из этих игр всегда связаны с определенной темой, например, ловить фрукты в корзине, избегая гнилых яблок, или ловить золотые монеты, избегая бесполезных камней.
Для простоты и того, что большинство из вас, имеющих опыт работы в веб-дизайне и разработке, уже знают, что такое RGB , мы будем ловить падающие блоки определенного цвета: красного , зеленого или синего .
Мы также реализуем несколько уникальную функцию: цвет корзины указывает, какие блоки вы должны поймать. Например, если корзина синего цвета, вы должны ловить только синие блоки.
Теперь, когда у нас есть готовая концепция, мы должны придумать имя, чтобы связать все это вместе. На этом этапе вы можете использовать свои творческие способности и имена для мозгового штурма, такие как «Zealof», «Xymnia» и «Doogle», но лучше придумать имя, связанное с самой игрой.
Так как основной смысл игры — «ловить» «цветные» блоки RGB, хорошим названием может быть что-то вроде «RGBlock» или «CatchBlock». Я решил назвать игру «RGBCatcher», ссылаясь на возможное название должности для игрока в игре.
Простой эскиз
Независимо от того, находитесь ли вы на начальных этапах создания игры или разработки своего нового веб-сайта с портфолио, простой эскиз очень полезен для быстрого захвата грубых сторон идеи для последующего использования.
Задолго до того, как я начал писать одну строчку кода, я сначала сделал набросок того, как все должно выглядеть.
Я окружил объекты стрелками, чтобы указать их возможности перемещения, а также добавил цветные точки, чтобы помочь мне помнить, что цвет корзины и блоков не должен быть статичным.
У всего этого также должен быть какой-то интерфейс, с помощью которого игрок может считывать свое здоровье (HP) и очки (PT). Хотя я сделал оригинальный набросок простым карандашом на бумаге, он выглядел примерно так, как показано на рисунке ниже.
Не переусердствуйте с вашими набросками; Вы должны использовать его как инструмент, чтобы уловить грубые грани идеи, а не рисовать или описывать словами точно каждый шаг того, как все должно выглядеть или работать. Вот для чего макеты и прототипы.
Начиная
Прежде чем мы начнем писать единственную строку JavaScript, мы должны сначала подготовить наш HTML5-тег canvas. canvas
— это тег HTML5, который взаимодействует с Canvas 2D API. Правильно: сотрудничает .
Это распространенное заблуждение, что новая функциональность HTML5 не требует JavaScript для запуска. HTML5, как и предыдущие версии, является языком разметки и не способен сделать Интернет динамичным сам по себе.
Организация нашего проекта
Недостаток организации определенно запрещен, поэтому давайте создадим новый каталог и назовем его точно таким же, как и название игры — в нем будут храниться все необходимые файлы для нашего проекта.
В этом каталоге вы должны создать дочерний каталог с именем «js», в котором будет находиться файл «rgbcatcher.js». Мы также напишем JavaScript позже, поэтому разумно было бы иметь текстовый редактор с подсветкой как минимум синтаксиса и поддержкой номера строки для вашего удобства.
В корне каталога ‘RGBCatcher’ создайте текстовый файл с именем ‘index’ с обычным расширением HTML и откройте его в своем любимом текстовом редакторе — index.html будет служить отправной точкой для нашей игры.
HTML
С разметкой не происходит никаких причудливых вещей — только doctype HTML5, ссылка на rgbcatcher.js и тег canvas с очень ограниченным применением стиля должны сделать свое дело.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<!DOCTYPE html>
<html>
<head>
<title>RGBCatcher</title>
<script src=»js/rgbcatcher.js»></script>
<style type=»text/stylesheet»>
body, html {
margin: 0px;
padding: 0px;
}
#canvas {
border: 1px solid #eee;
margin: 10px;
}
</style>
</head>
<body>
<canvas id=»canvas» width=»398″ height=»198″></canvas>
</body>
</html>
|
Как видите, тип документа HTML5 значительно проще по сравнению с большинством текущих типов документов основного потока. Помимо прочего, этот новый тип документа позволяет быстрее создавать экземпляр HTML-документа, соответствующего стандартам.
JavaScript
В эскизе каждый экземпляр объекта имеет крест. Помня об этом: мы можем легко составить список определений функций, которые нам понадобятся:
- Индикатор здоровья (исчисляемый)
- Счетчик очков (счетный)
- Блок (подвижный)
- Корзина (контролируемая игроком, также подвижная)
Так почему же в списке только один блок, а на эскизе мы видим пять? Хотя JavaScript основан на прототипах, мы можем создать несколько экземпляров блоков из одного определения базового блока.
Объектные Обертки
Теперь, когда у нас есть готовый список объектов, не составит труда создать для них оболочки.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
var Basket = function()
{
}
var Block = function()
{
}
var Health = function()
{
}
var Score = function()
{
}
|
Подожди — оба блока и объект корзины имеют тип «подвижный» (их координаты могут меняться), поэтому не будет ли полезно иметь базовый подвижный объект, от которого впоследствии могут наследовать объект блока и корзины?
1
2
3
|
var Movable = function()
{
}
|
Объект «блок» и «корзина» — не единственные объекты, которые имеют определенную общую функциональность — индикатор состояния и счетчик оценок предназначены для отслеживания определенного значения: показателя и состояния, соответственно.
1
2
3
|
var Countable = function()
{
}
|
Вот и все. Несмотря на то, что они не имеют никакой функциональности, наши оболочки готовы!
Глобальные определения
Прежде чем идти дальше, нам нужно две вещи: несколько глобальных переменных, которые доступны из любой функции, требующей этого, и глобальная оболочка. Эта глобальная оболочка выполняет функции основного обработчика нашей игры.
Вообще, глобальные определения — это плохо, потому что JavaScript имеет только одно пространство имен. Это облегчает столкновение имен переменных и функций между различными сценариями на одной странице, но, поскольку эта игра должна работать в автономном режиме, нам не следует об этом беспокоиться.
Все глобальные переменные будут помещены в верхнюю часть файла rgbcatcher.js. Глобальная оболочка RGBCatcher будет размещена внизу.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
// This will hold the DOM object of our canvas element
var canvas;
// This will hold the Canvas 2D API object
var context;
// This object will hold pressed keys to check against
var keyOn = [];
// [object definitions]
RGBCatcher = new function()
{
// This is the object which holds our game-colors, the ‘this’ keyword is used to make it accessible from outside this function (aka a public property)
this.colors = [
‘#f00’, // Red
‘#0f0’, // Green
‘#00f’, // Blue
];
}
|
Мы также определяем новую глобальную функцию rand
, которая облегчит нам захват случайного числа в определенном диапазоне. Это, например, будет использоваться для выбора случайного цвета из массива RGBCatcher.colors
.
1
2
3
4
|
function rand(min, max)
{
return Math.random() * (max-min) + min;
}
|
Подвижная базовая функция
Поскольку и корзина, и объект блока являются подвижными, что означает, что их координаты не являются постоянными, мы сначала напишем базовый объект, от которого они могут наследовать.
Подвижный базовый объект должен содержать только функциональные возможности, которые будут присутствовать как в определении блока, так и в определении функции корзины.
Конструктор
Конструктор — это подпрограмма, которая вызывается, как только создается новый объект и до того, как ему будет присвоен прототип. Это делает конструктор особенно полезным для инициализации специфических для объекта переменных, которые затем доступны в области видимости объекта.
Поскольку объект корзина и блок будут иметь разные свойства, мы напишем конструктор, который принимает массив, а затем использует этот массив для установки переменных объекта.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
// Add a parameter called ‘data’ so we can access the contents of an argument used at the instantiation of the Player object in the constructor
var Movable = function(data)
{
if (data === undefined)
return;
for (var i = 0; i < data.length; i++)
{
var setting = data[i];
// By accessing ‘this’ (which refers to this very instance) as an array, we can set a new object-specific variable with the name of ‘setting’ to ‘setting’ its value
this[setting[0]] = setting[1];
}
// When this object is succesfully instantiated;
this.alive = true;
}
|
Обновление подвижного объекта
Процесс обновления подвижного объекта, который состоит из фазы перемещения и рисования, должен происходить только тогда, когда подвижный объект еще жив.
Поскольку фактическое перемещение и процесс рисования подвижного объекта, вероятно, зависит от его наследника, базовый подвижный объект не должен определять их, если все наследники не используют один и тот же метод обновления или рисования.
01
02
03
04
05
06
07
08
09
10
|
Movable.prototype = {
update: function()
{
if (this.alive)
{
this.move();
this.draw();
}
}
};
|
Корзина
Теперь, когда мы определили подвижный объект, нам нужно только написать дополнительный метод move
и draw
для объекта корзины, который наследуется от него.
Базовые переменные корзины
Прежде чем мы начнем с методов объекта корзины, мы должны сначала определить несколько переменных, конкретно связанных с этим объектом. Эти переменные будут храниться в оболочке RGBCatcher.
Переменная basket
будет содержать экземпляр объекта basket. Переменная basketData
— это массив, который содержит данные о корзине, такие как ее высота и скорость горизонтального перемещения.
1
2
3
4
5
6
7
8
9
|
var basket;
var basketData = [
[‘width’, 30], // Width in pixels of the basket
[‘height’, 10], // Height in pixels of the basket
[‘xSpeed’, 1.1], // Horizontal movement speed in pixels
[‘color’, undefined], // The color of the basket
[‘oldColor’, undefined] // The old color of the basket, we can check against to prevent having the same basket color twice
];
|
Конструктор
Поскольку у подвижной базы уже есть конструктор и метод обновления, необходимые для функции корзины, мы не можем реализовать ее.
1
2
3
4
5
|
var Basket = function(data)
{
Movable.call(this, data);
}
Basket.prototype = new Movable();
|
Сброс корзины
Мы хотим расположить корзину по центру внизу экрана и изменить цвет корзины, как только начнется новая игра или уровень.
Разделив ширину нашей корзины на два и вычтя это значение из ширины холста, также разделенной на два, в результате мы получим идеально центрированную корзину.
Чтобы расположить корзину внизу экрана, мы устанавливаем координату y
на высоту корзины, вычтенную из высоты холста.
1
2
3
4
5
|
Basket.prototype.reset = function()
{
this.x = canvas.width / 2 — this.width / 2;
this.y = canvas.height — this.height;
}
|
Сбросить цвет корзины немного сложнее.
Мы используем цикл while, который будет случайным образом выбирать цвет из общедоступного массива RGBCatcher.color
на каждом вращении.
Как только выбран цвет, который не равен старому цвету корзины, цикл прекращает вращаться и oldColor
новый oldColor
.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
Basket.prototype.reset = function()
{
// Reset the position
this.x = canvas.width / 2 — this.width / 2;
this.y = canvas.height — this.height;
// Reset the color
while (this.color == this.oldColor)
this.color = RGBCatcher.colors[Math.round(rand(0, (RGBCatcher.colors.length-1)))];
// Change the old color to the current color (so that the while loop will stil work the next time this method is called)
this.oldColor = this.color;
}
|
Обновление корзины
Поскольку подвижный объект уже предоставляет функциональность для метода update
, нам нужно только определить конкретные методы для перемещения и рисования корзины.
Метод, который будет перемещать корзину, будет, в зависимости от ввода пользователя, добавлять или вычитать скорость горизонтального перемещения ( basket.xSpeed
) из текущей координаты x корзины.
Метод move также отвечает за перемещение корзины обратно в ее последнее возможное положение, если корзина выходит из элемента canvas из его окна просмотра — это требует от нас реализации очень простой процедуры обнаружения столкновений.
Обнаружение столкновений никогда не должно быть реализовано, просто проверяя, соответствует ли координата объекта определенному выражению. Это может работать для графики размером 1 на 1 пиксель, но графическое представление чего-либо большего, чем это, будет полностью отключено, поскольку также следует учитывать ширину и высоту.
Вот почему большую часть времени вам нужно будет вычесть или добавить форму, ее ширину или высоту, соответственно к координате x или y, чтобы соответствовать графическому представлению столкновения.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
Basket.prototype.move = function()
{
// 37 is the keycode representation of a left keypress
if (keyOn[37])
this.x -= this.xSpeed;
// 39 is the keycode representation of a right keypress
if (keyOn[39])
this.x += this.xSpeed;
// If the x coordinate is lower than 0, which is less than the outer left position of the canvas, move it back to the outer left position of the canvas
if (this.x > 0)
this.x = 0;
// If the x coordinate plus the basket’s width is greater than the canvas’s width, move it back to the outer right position of the canvas
if (this.x + this.width < canvas.width)
this.x = canvas.width — this.width;
}
|
Теперь, когда пользователь может перемещать свою корзину, единственное, что нам еще нужно сделать, — это написать метод, который имеет возможность рисовать корзину на экране — это то место, где подключается Canvas 2D API.
Вы можете подумать, что использование этого API чрезвычайно сложно, но на самом деле это не так! Canvas 2D API разработан так, чтобы быть максимально простым, но при этом максимально функциональным.
Нам нужны только две зависимости Canvas 2D API: свойство fillRect
метод fillRect
.
Свойство fillStyle
используется для определения цвета заливки для фигур.
Значение должно соответствовать спецификации CSS3 для цветов — это означает, что вам разрешено использовать «черный», но также его HEX-эквивалент «# 000», но также работает с градиентами и узорами, но мы будем использовать простой цвет заливки.
Метод fillRect
рисует заполненный прямоугольник, используя fillStyle
качестве цвета заливки, в позиции (x, y) и определенной ширине и высоте в пикселях.
Обратите внимание, что требуемые параметры fillRect
могут быть найдены в качестве свойств в экземпляре объекта корзины, поэтому мы можем просто использовать ключевое слово this
в сочетании с именем параметра.
1
2
3
4
5
6
7
8
9
|
Basket.prototype.draw = function()
{
// The Basket object’s ‘color’ atribute holds the color which our basket needs to be
context.fillStyle = this.color;
// The C2A fillRect method draws a filled rectangle (with fillStyle as its fill color) at position (x, y) with a set height and width.
// All these arguments can be found in the atributes of our basket object
context.fillRect(this.x, this.y, this.width, this.height);
}
|
Окончательное определение функции
Это оно; у нас есть весь объект корзины и работает.
Ну, я бы точно не назвал его запущенным, поэтому давайте настроим игровой цикл и другие различные основные элементы нашей игры, чтобы мы могли использовать эту корзину!
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
var Basket = function(data)
{
Movable.call(this, data);
}
Basket.prototype = new Movable();
Basket.prototype.reset = function()
{
// Reset the position
this.x = canvas.width / 2 — this.width / 2;
this.y = canvas.height — this.height;
// Reset the color
while (this.color == this.oldColor)
this.color = RGBCatcher.colors[Math.round(rand(0, (RGBCatcher.colors.length-1)))];
// Change the old color to the current color (so that the while loop will stil work the next time this method is called)
this.oldColor = this.color;
}
Basket.prototype.move = function()
{
// 37 is the keycode representation of a left keypress
if (keyOn[37])
this.x -= this.xSpeed;
// 39 is the keycode representation of a right keypress
if (keyOn[39])
this.x += this.xSpeed;
// If the x coordinate is lower than 0, which is less than the outer left position of the canvas, move it back to the outer left position of the canvas
if (this.x < 0)
this.x = 0;
// If the x coordinate plus the basket’s width is greater than the canvas’s width, move it back to the outer right position of the canvas
if (this.x + this.width < canvas.width)
this.x = canvas.width — this.width;
}
Basket.prototype.draw = function()
{
// The Basket object’s ‘color’ atribute holds the color which our basket needs to be
context.fillStyle = this.color;
// The C2A fillRect method draws a filled rectangle (with fillStyle as its fill color) at position (x, y) with a set height and width.
// All these arguments can be found in the atributes of our basket object
context.fillRect(this.x, this.y, this.width, this.height);
}
|
Упаковщик RGBCatcher
Чтобы на самом деле реализовать функции и прототип, который мы только что написали, они должны быть созданы и вызваны где-то. Вот что будет обрабатывать основной обработчик нашей игры: создание экземпляров и вызов правильных вещей в нужное время.
Метод бега
Открытый метод запуска оболочки RGBCatcher инициализирует игровые компоненты (например, Canvas 2D API, экземпляр объекта корзины и т. Д.).
Он также настроит глобальные прослушиватели событий keyup
и keydown
чтобы текущие нажатия клавиш можно было легко извлечь из глобального массива keyOn
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
RGBCatcher = new function()
{
// Public
this.colors = [
‘#f00’,
‘#0f0’,
‘#00f’,
];
// Private
var basketData = [
[‘width’, 30],
[‘height’, 10],
[‘xSpeed’, 1.1],
[‘color’, ‘#f00’],
[‘oldColor’, ‘#f00’]
];
var basket;
this.run = function()
{
// Set the global ‘canvas’ object to the #canvas DOM object to be able to access its width, height and other attributes are
canvas = document.getElementById(‘canvas’);
// This is where its all about;
context = canvas.getContext(‘2d’);
// Add an eventListener for the global keydown event
document.addEventListener(‘keydown’, function(event)
{
// Add the keyCode of this event to the global keyOn Array
// We can then easily check if a specific key is pressed by simply checking whether its keycode is set to true
keyOn[event.keyCode] = true;
}, false);
// Add another eventListener for the global keyup event
document.addEventListener(‘keyup’, function(event)
{
// Set the keyCode of this event to false, to avoid an inifinite keydown appearance
keyOn[event.keyCode] = false;
}, false);
// Instantiate the basket object and feed it the required basketData
basket = new Basket(basketData);
// At the start of a new game, this method is called to set dynamic variables to their default values
resetGame();
}
}
|
Сброс игры
Поскольку до сих пор у нас есть только один объект, связанный с игрой, который нужно сбросить, это объект корзины, нам нужно только вызвать методы сброса этого объекта для сброса игры.
01
02
03
04
05
06
07
08
09
10
|
RGBCatcher = new function()
{
// […]
function resetGame()
{
basket.resetPosition();
basket.resetColor();
}
}
|
Обратите внимание, что этот метод следует вызывать только в начале новой игры. Поскольку у нас пока нет никакого управления уровнями, сейчас оно вызывается из функции run
RGBCatcher.
Игровой цикл
Игровой цикл отвечает за обновление и перерисовку всех объектов игры до тех пор, пока не произойдет конкретное событие, например, когда у пользователя закончится здоровье.
Игровые циклы бывают разных форматов и размеров, но общий порядок игрового цикла заключается в проверке ввода, обновлении и перемещении игровых объектов, очистке экрана и перерисовке всех игровых объектов на экране.
Просто вызывая метод update
объекта корзины, мы уже обрабатываем большинство шагов, найденных в игровом цикле.
Единственная дополнительная строка кода для записи — это строка, которая очищает экран. Поскольку наш экран является элементом canvas, для этого требуется API Canvas 2D.
Очистка холста с помощью Canvas 2D API выполняется с помощью метода clearRect
. Он принимает четыре параметра: координаты x и y, с которых начинается очистка, а также ширину и высоту очистки.
Поскольку нам нужно очистить прямоугольник, который перекрывает весь холст, мы начинаем с точки (0, 0) и устанавливаем высоту и ширину соответственно высоте и ширине элемента холста.
01
02
03
04
05
06
07
08
09
10
|
RGBCatcher = new function()
{
// […]
function gameLoop()
{
// Clear the entire canvas
context.clearRect(0, 0, canvas.width, canvas.height);
}
}
|
Теперь, когда наш игровой цикл очищает холст при каждом вращении, также называемом фреймом, нам нужно только вызвать метод update объекта корзины, поскольку он обрабатывает процессы перемещения, обновления и перерисовки самостоятельно.
01
02
03
04
05
06
07
08
09
10
11
|
RGBCatcher = new function()
{
// […]
function gameLoop()
{
context.clearRect(0, 0, canvas.width, canvas.height);
basket.update();
}
}
|
Точка входа
Единственное, чего еще не хватает для выполнения первого тестового запуска нашей игры, это точка входа в игровой цикл.
Единственная цель точки входа — запустить игровой цикл и определить интервал для каждого цикла его вращения — setInterval
— это именно то, что нам нужно.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
RGBCatcher = new function()
{
// […]
// The variable associated with the setInterval ID
var interval;
this.run = function()
{
// […]
// Set the interval variable to the interval its ID so we can easily abort the game loop later
// The speed of the interval equals 30 frames per second, which should be enough to keep things running smoothly
interval = setInterval(gameLoop, 30/1000);
}
}
|
Как вы могли заметить, после входа наш игровой цикл работает бесконечно. Мы обязательно изменим это позже, но пока мы все еще находимся на ранних стадиях процесса разработки.
Давайте попробуем!
Теперь, когда определены мастер-оболочка RGBCatcher и объект корзины, мы можем наконец протестировать нашу преждевременную игру!
Поскольку наша игра зависит от элемента canvas, мы должны подождать, пока он не будет отрендерен, прежде чем вызывать RGBCatcher для его открытого метода run
.
Это можно сделать, включив файл JavaScript после разметки элемента canvas, но если вы хотите сохранить свой JavaScript в элементе head, просто присоедините метод run
к window
его событием onload
.
1
2
3
4
|
window.onload = function()
{
RGBCatcher.run();
}
|
Запустите файл index.html, расположенный в каталоге RGBCatcher, и попробуйте переместить корзину, используя клавиши со стрелками влево и вправо.
Кроме того, попробуйте обновить страницу несколько раз, чтобы проверить, действительно ли цвет корзины меняется. Не беспокойтесь, если вы видите повторяющийся цвет — старый цвет не сохраняется при обновлении страницы.
Очевидно, это пока не очень впечатляюще, так что не стесняйтесь продолжать, потому что мы собираемся добавить несколько падающих блоков.
Блок
В игру «лови падающие предметы» не стоит играть без падающих предметов, поэтому давайте продолжим и заставим эти блоки упасть!
Мы начнем с написания кода для блочного объекта, и через мгновение вы увидите красные, зеленые и синие блоки, падающие на ваш экран.
Блокировать определенные переменные
Точно так же, как объект корзины, объекту блока нужен массив, заполненный данными для хранения определенных свойств блока, таких как его ширина и скорость вертикального перемещения.
Этот массив данных помещается в оболочку RGBCatcher.
1
2
3
4
5
6
|
var blockData = [
[‘width’, 10], // Width in pixels of this block
[‘height’, 10], // Height in pixels of this block
[‘ySpeed’, 2], // Vertical movement speed in pixels
[‘color’, undefined] // The color of this block
];
|
Так почему же, в отличие от переменной basket
, нет block
переменной, которая будет содержать экземпляр объекта блока?
Потому что уровень должен содержать более одного блока, чтобы поймать.
Фактически, мы будем использовать своего рода менеджер блоков, который позаботится об управлении блоками — об этом позже.
Конструктор
Конструктор нашего блочного объекта точно такой же, как конструктор объекта корзины.
1
2
3
4
5
|
var Block = function(data)
{
Movable.call(this, data);
}
Block.prototype = new Movable();
|
Заметьте, как бы у нас было несколько строк дублирующего кода, если бы мы не использовали определение базового подвижного объекта?
Сброс блока
Поскольку мы хотим предотвратить падение всех блоков в одной и той же позиции, мы используем функцию rand
чтобы поместить блок в случайную горизонтальную позицию.
Вместо использования метода resetPosition
, мы будем вызывать метод, который достигает случайного позиционирования initPosition
поскольку этот метод предназначен для инициализации позиции, а не для ее сброса.
Инициализация? Разве конструктор не должен этого делать? Это так, поэтому мы будем вызывать метод initPosition
только один раз в конструкторе.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
var Block = function(data)
{
Movable.call(this, data);
this.initPosition();
}
Block.prototype = new Movable();
Block.prototype.initPosition = function()
{
// Only allow to set the position of this block once
if (this.x !== undefined || this.y !== undefined)
return;
// By picking a rounded number between 0 and the canvas.width subtracted by the block’s width, we have a position for this block which is still inside the block’s viewport
this.x = Math.round(rand(0, canvas.width — this.width));
// By setting the vertical position of the block to 0 subtracted by the block’s height, the block will look like it slides into the canvas’s viewport
this.y = 0 — this.height;
}
|
Блок теперь будет автоматически инициализировать свою позицию при создании, но он еще не инициализирует свой цвет.
Во-первых, мы должны изменить конструктор функции блока так, чтобы рядом с initPosition
, был сделан вызов initColor
при initColor
экземпляра.
1
2
3
4
5
6
7
|
var Block = function(data)
{
Movable.call(this, data);
this.initPosition();
this.initColor();
}
|
На данный момент, это будет бросать «Объект # initColor
к прототипу.
1
2
3
4
5
6
7
|
Block.prototype.initColor = function()
{
if (this.color !== undefined)
return;
this.color = RGBCatcher.colors[Math.round(rand(0, (RGBCatcher.colors.length-1)))];
}
|
Обновление блока
Подвижный прототип уже предоставляет функциональность для коллективного метода обновления, так что, опять же, это то, о чем мы не должны беспокоиться.
Поскольку для падения блока входных данных не требуется, метод, который заставляет блок двигаться, очень прямолинеен: продолжайте добавлять скорость вертикального падения к текущей вертикальной позиции.
1
2
3
4
5
|
Block.prototype.move = function()
{
// Add the vertical speed to the block’s current position to move it
this.y += this.ySpeed;
}
|
Единственное, что осталось написать для нашего блочного объекта — это метод draw, который является еще одной задачей для Canvas 2D API.
Чтобы нарисовать наш блок, мы должны сначала установить цвет заливки, а затем мы должны нарисовать прямоугольник с параметрами, выбранными из свойств блока.
Звучит семейно? Должно. Мы сделали точно такой же трюк с методом draw
объекта корзины, помните?
Поскольку объект корзина и блок оба являются подвижными, мы просто переместим метод draw
корзины в подвижный прототип, чтобы все наследники совместно использовали этот метод draw
.
1
2
3
4
5
6
7
8
9
|
Movable.prototype = {
// […]
draw: function()
{
context.fillStyle = this.color;
context.fillRect(this.x, this.y, this.width, this.height);
}
}
|
Окончательное определение функции
Эти блоки не будут падать сами по себе, поэтому давайте продолжим и добавим обработку блоков в оболочку RGBCatcher.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
var Block = function(data)
{
Movable.call(this, data);
this.initPosition();
this.initColor();
}
Block.prototype = new Movable();
Block.prototype.initPosition = function()
{
// Only allow to set the position of this block once
if (this.x !== undefined || this.y !== undefined)
return;
// By picking a rounded number between 0 and the canvas.width subtracted by the block’s width, we have a position for this block which is still inside the block’s viewport
this.x = Math.round(rand(0, canvas.width — this.width));
// By setting the vertical position of the block to 0 subtracted by the block’s height, the block will look like it slides into the canvas’s viewport
this.y = 0 — this.height;
}
Block.prototype.initColor = function()
{
if (this.color !== undefined)
return;
this.color = RGBCatcher.colors[Math.round(rand(0, (RGBCatcher.colors.length-1)))];
}
Block.prototype.move = function()
{
// Add the vertical speed to the block’s current position to move it
this.y += this.ySpeed;
}
|
Обработка блоков
Хотя корзина может перемещаться без помощи обработчика, управляемого компьютером, блокировать ее нельзя.
Для этого требуется руководитель или, вернее, менеджер, который, помимо прочего, способен удалять блоки с экрана, как только они падают на землю. Этот супервизор найдет свое место в главной оболочке RGBCatcher.
- Инициализировать : Инициализация переменных, относящихся конкретно к блокам, например, переменная, которая указывает, сколько блоков еще на экране.
- Сброс : сброс всех инициализированных переменных до значений по умолчанию.
- Обновление блоков : как только инициализация завершена, начинается игровой цикл, и мы начнем с циклического прохождения всех текущих доступных блоков и их обновления.
- Проверка на столкновение : для каждого блока мы должны проверить, находится ли он в данный момент в столкновении или нет, и выполнить определенные действия для определенных столкновений.
- Если необходимо, добавьте блоки : когда все блоки были успешно обновлены и столкновения были обработаны, новые блоки должны появляться, если это требуется.
Блок обработки определенных переменных
Нам нужно еще пять переменных для обработки блоков. Все они входят в определение функции RGBCatcher как частные свойства.
01
02
03
04
05
06
07
08
09
10
|
// The amount of blocks there should be per level (example: level 3 equals has (3*4) 12 blocks to process)
var blocksPerLevel = 4;
// The time in seconds there should be between the fall of a new block.
var blocksSpawnSec;
// The amount of blocks already spawned
var blocksSpawned;
// The amount of blocks currently on the canvas
var blocksOnScreen;
// The array which holds the blocks to process
var blocks = [];
|
Сброс настроек
Оболочка объекта RGBCatcher уже имеет метод resetGame
. Теперь нам нужно только добавить значения по умолчанию для переменных, определенных выше, чтобы они сбрасывались в начале новой игры.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
RGBCatcher = new function()
{
// […]
function resetGame()
{
basket.resetPosition();
basket.resetColor();
blocksSpawnSec = 2.5;
blocksSpawned = 0;
blocksOnScreen = 0;
blocks = [];
}
}
|
Обновить блоки
Фаза обновления блоков начинается с циклического прохождения всех блоков и их обновления. Он также отвечает за начало следующей фазы управления блоками, которая состоит в вызове метода для обработки обнаружения столкновений.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
RGBCatcher = new function()
// […]
function updateBlocks()
{
for (var i = 0; i < blocks.length; i++)
{
// Assign a local copy
var block = blocks[i];
block.update();
checkCollision(block);
}
}
}
|
Теперь мы куда-то добираемся. Каждый блок в переменной blocks
обновляется с использованием метода update
объекта блока.
Когда блок был обновлен, он будет checkCollision
методу checkCollision
который затем будет обрабатывать обнаружение столкновений.
Проверьте на столкновение
Обнаружение столкновений — одна из самых сложных частей разработки игр. Недооценка может привести к неожиданным результатам.
Вы когда-нибудь сталкивались с игрой, в которой вы могли проходить сквозь стены и стрелять в своих врагов, не целившись в них? Виноваты разработчики, которые работали над обнаружением столкновений!
Эти игры, вероятно, были 3D, но, к счастью для нас, нам нужно беспокоиться только о двух возможных измерениях, сталкивающихся друг с другом, так что практически есть целое измерение, с которым можно ошибиться!
В нашей игре RGBCatcher есть всего четыре столкновения, о которых нужно беспокоиться, и мы уже имели дело с первыми двумя — помните возможное столкновение между корзиной и правыми или левыми границами экрана?
Последние два столкновения связаны с падающими блоками и с тем, попадают ли они в корзину или на землю.
Прежде чем мы проверим, есть ли фактическое столкновение с землей или корзиной, мы проверим, прошел ли блок или находится в настоящее время на линии y, на которой находится корзина.
Как вы можете видеть на рисунке ниже, основывать обнаружение столкновений исключительно на объектах, их координаты — это плохо, поскольку визуальное представление блока уже попало в корзину до того, как координаты соответствуют столкновению.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
RGBCatcher = new function()
{
// […]
function checkCollision(block)
{
// If the block is not defined or not alive, return
if (block === undefined || block.alive === false)
return;
// If the block hasn’t passed the vertical line the basket resides on, there’s no way we are dealing with a collision yet
if (block.y + block.height < basket.y)
return;
}
}
|
Когда мы уверены, что блок визуально имеет ту же или более высокую координату y, чем корзина, мы можем безопасно проверить, находится ли координата x блока в правильном диапазоне .
Кроме координаты y, не существует единственного места, в котором блок может горизонтально столкнуться с корзиной. Вот почему нам нужно проверить, находится ли блок в диапазоне общей ширины корзины, чтобы определить, имеем ли мы столкновение или нет.
Диапазон корзины не может быть описан только одним статическим значением, поэтому мы будем использовать очень простой алгоритм.
Этот алгоритм будет определять, является ли позиция x блока больше или равна позиции x корзины и является ли позиция x блока плюс его ширина ниже, чем позиция x
корзины плюс ее ширина.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
RGBCatcher = new function()
{
// […]
function checkCollision(block)
{
// If the block is not defined or not alive, return
if (block === undefined || block.alive === false)
return;
// If the block hasn’t passed the vertical line the basket resides on, we’re not dealing with a collision (yet)
if (block.y + block.height < basket.y)
return;
// If the block’s x coordinate is in the range of the basket’s width, then we’ve got a collision
if (block.x >= basket.x &&
block.x + block.width <= basket.x + basket.width)
{
}
// If it’s not, the block has missed the basket and will thus, eventually, collide with the ground.
else
{
}
}
}
|
Редактирование и добавление некоторых игровых переменных
Прежде чем мы продолжим, мы должны сначала добавить и отредактировать несколько переменных, найденных в области оболочки RGBCatcher.
Переменная для редактирования является переменной blockData
. Мы добавим свойство блока под названием сила — это значение будет применено к здоровью пользователя, если пользователь пропустит правильно окрашенный блок или если он поймает неправильно окрашенный блок.
С другой стороны, количество силы будет добавлено к счету пользователя, если пользователь поймает блок, который имеет тот же цвет, что и корзина.
Две другие переменные, которые нужно добавить, называются health
и score
. Эти переменные будут содержать объекты, представляющие графическую панель состояния и счетчик баллов позже, но на данный момент они представляют собой простые целочисленные значения.
01
02
03
04
05
06
07
08
09
10
11
12
|
// This is a percentage which starts at 100%
var health = 100;
// This is just a regular integer value
var score = 0;
var blockData = [
[‘width’, 10],
[‘height’, 10],
[ 'ySpeed' , 2], [ 'color' , undefined], [ 'strength' , 30] // The strength of this block *new* ];
|
Ловить блок
Поскольку цель нашей игры требует, чтобы пользователь ловил только блоки, которые имеют тот же цвет, что и корзина, сначала мы сравним цвет блока с цветом корзины.
Если они одинаковы, у нас есть правильный улов, и счет должен увеличиваться с силой блока. С другой стороны, если неправильно окрашенный блок уменьшит здоровье пользователя на силу блока.
В обоих случаях блок должен исчезнуть с экрана, а blocksOnScreen
переменная должна уменьшиться на единицу
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
RGBCatcher = new function () {
// [...] // By passing a reference of the block object to the function, we can use the current very block to perform our collision detection function checkCollision(block) {
// [...] // If the block's x-coordinate is in the range of the basket's width, then we've got a collision if (block.x >= basket.x && block.x + block.width <= basket.x + basket.width) {
// Whether it's a correctly colored block or not, the current block should disappear and the amount of blocks on the screen should decrease with one if (block.alive == true ) {
block.alive = false ; blocksOnScreen--; }
// If the block's color matches the basket's current color, we've got a correct catch if (block.color === basket.color) // So give the player some points score += block.strength; else
// Otherwise, inflict damage to the health of the player health -= block.strength; }
// If it's not, the block has missed the basket and will thus, eventually, collide with the ground else
{
}
}
}
|
Отсутствует блок
Если пользователь пропускает правильно окрашенный блок, сила блока должна быть нанесена здоровью игрока. Тем не менее, нет ничего плохого в том, чтобы пропустить неправильно окрашенный блок, и если это произойдет, игра должна просто продолжиться без дальнейших действий.
В отличие от захвата блока, блок не должен сразу исчезать с экрана. Сначала он должен упасть в окно просмотра холста, а затем удалить его из blocks
массива.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
RGBCatcher = new function {
// [...] function checkCollision() {
// [...] // If it's not, the block has missed the basket and will thus, eventually, collide with the ground else
{
// The player missed a correctly colored block and no damage has been inflicted yet if (block.color === basket.color && block.strength > 0) {
// So lets inflict damage to the health of the player health -= block.strength; // To prevent this block from inflicting damage again, we set its strength to 0 block.strength = 0; }
// If the block's y coordinate is greater than the canvas's height, it has disappeared from the viewport and can be removed if (block.alive === true && block.y > canvas.height) {
block.alive = false ; blocksOnScreen--; }
}
}
}
|
Добавьте блоки, если требуется
Так как же должен addBlock
работать наш метод?
Сначала он проверяет, blocksPerLevel * level
равно ли количество требуемых блоков для текущего уровня ( ) количеству уже созданных blocksSpawned
блоков ( ).
Если этого не произойдет, будет создан новый объект блока, он будет добавлен в blocks
массив, blocksSpawned
а blocksOnScreen
переменные и будут увеличены на единицу.
Если это так, выполняется выражение, чтобы определить, есть ли еще блоки на экране ( blocksOnScreen
), которые должны сначала приземлиться где-нибудь.
Если это не так, и blocksOnScreen == 0
мы можем с уверенностью сказать, что уровень был пройден.
addBlock
Метод возвращает истину , если все еще существует блоки для обработки и ложного , если правильное количество блоков было порождали и удаляется с экрана.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
RGBCatcher = new function // [...] function addBlock()
{
if (blocksSpawned != blocksPerLevel * level) {
// Add a new block the the blocks array blocks[blocks.length] = new Block(blockData); // Both increase the amount of blocks on the screen and the amount of spawned blocks blocksSpawned++; blocksOnScreen++; }
else
{
// Check whether all blocks have been processed if (blocksOnScreen == 0) return false;
}
// Return true if there's still something to work with return true;
}
}
|
Информационный экран
Мы могли бы реализовать только что написанные функции в gameloop и начать играть немедленно, но разве не было бы намного приятнее иметь титульный экран, который ожидает ответа пользователя, а не сразу начинает падать с блоков?
Давайте напишем функцию, которая отображает причудливый экран заголовка и ждет ввода от пользователя — пробел звучит уместно.
Мы могли бы использовать Canvas 2D API для отображения титульного экрана, но почему бы просто не использовать HTML-тег и использовать JavaScript для его изменения.
Обновление HTML
Экран заголовка будет помещен в общий элемент div, помеченный идентификатором информации, поскольку он также будет функционировать как информационный экран, а не только для отображения экрана заголовка.
С размерами 200px * 26px мы используем абсолютную позицию и просто вычисляем «верхнее» и «левое» поле информационного элемента, чтобы красиво расположить его над элементом canvas.
Для верхней позиции мы добавляем верхнее поле #canvas
элемента к общей высоте минус высота информационного элемента и делим его на два.
Мы делаем то же самое для левой позиции, за исключением того, что вместо того , чтобы использовать высоту, мы используем ширину как #canvas
и в #info
элементе , чтобы вычислить правильное положение.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
<!DOCTYPE html>
<html>
<head>
< title >RGBCatcher</ title > < script src = "http://tutsplus.s3.amazonaws.com/tutspremium/web-development/172_game/js/js.js" ></ script > <style>
body, html, p { margin: 0px;
padding: 0px;
}
#canvas { border: 1px solid #eee; margin: 10px;
}
#info { position: absolute;
top: 92px; /* (10px top margin + 200px of total height for the #canvas element - 26px of total height for the #info element) / 2 */ left: 105px; /* (10px left margin + 400px of total width for the #canvas element - 200px of total width for the #info element / 2 */ text-align: center;
font: 10px sans-serif; background-color: #fff;
width: 200px;
height: 26px; }
/* Some styling for the title screen */ span.red { color: #f00; }
span.green { color: #0f0; }
span.blue { color: #00f; }
span.red, span.green, span.blue { font-weight: bold;
}
</style>
</head>
<body>
< canvas id = "canvas" width = "398" height = "198" ></ canvas > < div id = "info" ></ div > </body>
</html>
|
Теперь мы снова отсортировали HTML и CSS, и мы можем вернуться к гораздо более интересному процессу написания кода, отображающего фактический титульный экран.
Функция titleScreen
titleScreen
Функция является дочерним-функция (метод) функции RGBCatcher обертки.
Прежде чем мы это напишем, нам нужно добавить две частные переменные — info
и infoScreenChange
.
info
содержит объект DOM, который дает нам необходимые инструменты для изменения элемента информации. Логическое значение, которое по умолчанию истины, помогает нам определить , должен ли экран информации обновляться или нет.infoScreenChange
1
2
3
4
|
// A DOM Element of the info screen var info; // Should the info screen be changed? var infoScreenChange = true ; |
Подводя итог: если информационный экран еще не обновлен ( infoScreenChange === true
), сделайте это.
Затем просто дождитесь нажатия клавиши пробела ( keyOn[32]
), чтобы скрыть информационный экран, перезагрузить игру и войти в игровой цикл.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
RGBCatcher = new function () {
// [...] function titleScreen() {
// Should the info screen be updated? if (infoScreenChange) {
// Set the HTML value of the info DOM object so it displays a fancy titlescreen info.innerHTML = '<p><span class="red">R</span><span class="green">G</span><span class="blue">B</span>Catcher</p> <p>Press spacebar to start</p>' ; // Only update the info screen once infoScreenChange = false ; }
// 32 is the key code representation of a press on the spacebar if (keyOn[32]) {
// Set the infoScreenChange variable to its default value again infoScreenChange = true ; // Set the CSS 'display' rule of the info element to none so it disappears info.style.display = 'none' ; // The player wants to start playing so the current 'titleScreen loop' will be cleared clearInterval(interval); // Reset the game resetGame();
// And enter the game loop at 30 frames per second interval = setInterval(gameLoop, 30/1000) }
}
}
|
Должны быть сделаны некоторые изменения в run
методе установки локальной info
переменной и подключения setInterval
к экрану заголовка вместо игрового цикла.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
RGBCatcher = new function () {
// [...] this .run = function () {
// Set the global 'canvas' object to the #canvas DOM object to be able to access its width, height and other attributes are canvas = document.getElementById( 'canvas' ); // Set the local 'info' object to the #info DOM object info = document.getElementById( 'info' ); // This is where it's all about; getting a new instance of the C2A object — pretty simple huh? context = canvas.getContext('2d '); // Add an eventListener for the global keydown event document.addEventListener(' keydown ', function(event) {
// Add the keyCode of this event to the global keyOn Array // We can then easily check if a specific key is pressed by simply checking whether its keycode is set to true keyOn[event.keyCode] = true; }, false);
// Add another eventListener for the global keyup event document.addEventListener(' keyup', function (event) {
// Set the keyCode of this event to false, to avoid an inifinite keydown appearance keyOn[event.keyCode] = false ; }, false);
// Instantiate the basket object and feed it the required basketData basket = new Basket(basketData); // Go to the title screen at 30 frames per second interval = setInterval(titleScreen, 30/1000); }
}
|
Не стесняйтесь попробовать.
Хотя падающих блоков еще нет, у нас теперь есть титульный экран, который отображается перед началом игры и исчезает, как только пользователь нажимает клавишу пробела.
Управление уровнем
Теперь у нас есть титульный экран, пора выпустить эти блоки.
Чуть назад вы могли заметить еще не определенную локальную level
переменную в приватном addBlock
методе RGBCatcher . Эта переменная является реквизитом процедуры управления уровнем.
Давайте определим это вместе с новой переменной с именем levelUp
. С помощью этого логического значения мы можем определить, прошел ли пользователь недавно уровень или нет.
1
2
3
4
|
// The level the player is currently at var level; // Has the player recently leveled up? var levelUp = false ; |
Оглядываясь назад на схему управления блоками, мы видим цикл обновления, проверки коллизий и добавления блоков, если это требуется. Поскольку в нашей игре уже есть вращающийся цикл, который является игровым циклом, повторяющийся паттерн входит туда.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
RGBCatcher = new function () {
// [...] function gameLoop()
{
context.clearRect(0, 0, canvas.width, canvas.height); basket.update(); updateBlocks(); }
}
|
Но какие блоки будут обновлены, если не будут сделаны вызовы addBlock
функции?
Добавление функции таймера
Когда новый блок должен упасть, зависит от blocksSpawnSec
переменной. Мы добавим простой таймер для проверки, чтобы мы могли определить, следует ли нам вызывать addBlock
функцию или нет.
Переменные, необходимые для достижения этого, называются frameTime
и startTime
.
1
2
|
var startTime = 0; var frameTime = 0; |
frameTime
будет установлен на каждом вращении игрового цикла, тогда как startTime
должен быть установлен только в начале нового таймера. Этот первый таймер должен начать работать в начале новой игры.
У нас уже есть функция с именем resetGame
, которая вызывается в начале новой игры, поэтому мы определяем startTime
переменную там. Пока мы на этом, мы могли бы также добавить сброс для level
переменной.
Обратите внимание, что функция, которую мы используем для получения текущего времени, getTime
возвращает целочисленное значение, представляющее текущее время в миллисекундах с 1970 года.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
RGBCatcher = new function () {
// [...] function resetGame()
{
basket.reset(); health = 100; score = 0; blocksSpawnSec = 2; blocksSpawned = 0; blocksOnScreen = 0; blocks = []; level = 1; startTime = new Date().getTime(); }
}
|
Теперь у нас есть начальное startTime
значение, мы можем легко узнать, сколько миллисекунд прошло, просто извлекая это значение из текущего frameTime
.
Поэтому после вызова updateBlocks
метода мы проверяем, больше ли текущее время кадра, чем время начала плюс секунды, после которых должен появляться новый блок. Если это так, мы сбрасываем таймер и вызываем addBlock
функцию — в зависимости от ее возвращаемого значения будут выполнены соответствующие действия.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
RGBCatcher = new function () {
// [...] function gameLoop()
{
frameTime = new Date().getTime(); context.clearRect(0, 0, canvas.width, canvas.height); basket.update(); updateBlocks(); // blocksSpawnSec * 1000 because getTime() returns a value in miliseconds if (frameTime >= startTime + (blocksSpawnSec*1000)) {
// If all blocks have been processed if (addBlock() === false ) {
}
// Reset the timer startTime = frameTime; }
}
}
|
Поднимаясь на уровень
Если addBlock
функция возвращается false
, мы можем сказать, что текущий уровень завершен, поэтому мы устанавливаем для levelUp
переменной значение true.
Мы также увеличиваем сложность игры, немного уменьшая время между блоками fall ( blocksSpawnSec
) и немного увеличивая скорость падения блоков ( blockData['ySpeed']
). Эти входные и выходные значения являются частью игрового процесса — не стесняйтесь настраивать их позже, если вы считаете, что они слишком низкие или слишком высокие.
Когда уровень пройден, несколько переменных должны быть сброшены. Эта процедура не совсем совпадает с процедурой сброса в игре, поэтому мы напишем следующую подходящую функцию.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
RGBCatcher = new function () {
// [...] function gameLoop()
{
frameTime = new Date().getTime(); context.clearRect(0, 0, canvas.width, canvas.height); basket.update(); updateBlocks(); // blocksSpawnSec * 1000 because the timer values are in miliseconds if (frameTime >= startTime + (blocksSpawnSec*1000)) {
// If all blocks have been added if (addBlock() === false ) {
// The player should go up a level levelUp = true ; level++; // Increase difficulty blocksSpawnSec *= 0.99; blockData[ 'ySpeed' ] *= 1.01; basketData[ 'xSpeed' ] *= 1.02; // Reset level specific variables resetLevel(); }
// The timer is finished, reset it startTime = frameTime; }
}
}
|
Сброс уровня
Учитывая концепцию нашей игры, цвет и положение корзины должны быть сброшены в начале нового уровня. Чтобы наградить игрока за прохождение уровня, мы сбросим его здоровье.
Очевидно, что blocksSpawned
, blocksOnScreen
и blocks
переменная также должна быть сброшена.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
RGBCatcher = new function () {
// [...] function resetLevel() {
basket.reset(); health = 100; blocksSpawned = 0; blocksOnScreen = 0; blocks = []; }
}
|
Игра закончена
Теперь у нас есть resetLevel
реализация, мы можем продолжить работу над игровым циклом.
Чего еще не хватает, так это проверки работоспособности пользователя, чтобы определить, достаточно ли у него здоровья, чтобы продолжить, и процедуры, которая позволяет пользователю узнать, что начинается новый уровень.
Первое, что относительно просто, так что давайте начнем с этого. По сути, нам нужно только проверить, если здоровье игрока ниже 1.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
RGBCatcher = new function () {
// [...] function gameLoop()
{
frameTime = new Date().getTime(); if (health < 1) {
basket.alive = false ; // Game over }
// [...] }
}
|
Итак, как должна быть реализована игровая процедура? В тот момент, когда у пользователя осталось менее 1/100 HP, игра должна закончиться, и должно появиться сообщение об окончании игры.
Это лучше всего сделать, выйдя из текущего игрового цикла и войдя в новый игровой цикл, который будет мигать сообщение в течение определенного времени, три секунды звучат нормально, после чего снова отображается экран заголовка.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
RGBCatcher = new function () {
// [...] function gameLoop()
{
frameTime = new Date().getTime(); if (health < 1) {
basket.alive = false ; // Abort the game loop and set a new loop for the game over screen clearInterval(interval); interval = setInterval(gameOverScreen, 30/1000); return;
}
// [...] }
}
|
gameOverScreen
Должна быть определена новая функция, которая сначала очищает весь холст, отображает сообщение об игре и по истечении трех секунд возвращается к экрану заголовка.
Сообщение об игре будет отображено на информационном экране. Отображение сообщения всего за три секунды легко достигается с помощью функции таймера, с которой вы должны быть знакомы.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
RGBCatcher = new function () {
// [...] function gameOverScreen() {
frameTime = new Date().getTime(); // Should the info screen be changed? if (infoScreenChange) {
// First clear the canvas with the basket and blocks from the background context.clearRect(0, 0, canvas.width, canvas.height); // Change the text of the info screen and show it info.innerHTML = '<p>Game over!</p>' ; info.style.display = 'block' ; // Do not update the info screen again infoScreenChange = false ; }
// If three seconds have passed if (frameTime > startTime + (3*1000)) {
// A new info screen should be pushed next time infoScreenChange = true ; // Reset the timer startTime = frameTime; // Quit this loop and set a new the loop for the title screen clearInterval(interval); interval = setInterval(titleScreen, 30/1000); }
}
}
|
Будьте готовы к уровню 2!
В нашем обновленном игровом цикле отсутствует подпрограмма, которая обрабатывает сообщение о повышении уровня.
У нас уже есть переменная, levelUp
которая помогает нам определить, должно ли сообщение отображаться или нет.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
RGBCatcher = new function () {
// [...] function gameLoop()
{
frameTime = new Date().getTime(); if (health < 1) {
basket.alive = false ; // Abort the game loop and set a new loop for the game over screen clearInterval(interval); interval = setInterval(gameOverScreen, 30/1000); return;
}
if (levelUp) {
return;
}
// [...] }
}
|
Поэтому мы просто копируем gameOverScreen
код в оператор if, меняем сообщение, которое будет мигать, и действия, выполняемые после достижения отметки в три секунды.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
RGBCatcher = new function () {
// [...] function gameLoop()
{
frameTime = new Date().getTime(); if (health < 1) {
basket.alive = false ; // Abort the game loop and set a new loop for the game over screen clearInterval(interval); interval = setInterval(gameOverScreen, 30/1000); return;
}
if (levelUp) {
if (infoScreenChange) {
// First clear the canvas with the basket and blocks from the background context.clearRect(0, 0, canvas.width, canvas.height); // Change the text of the info screen and show it info.innerHTML = '<p>Level ' + (level-1) + ' cleared!</p><p>Get ready for level ' + level + '!</p>' ; info.style.display = 'block' ; // Do not update the info screen again infoScreenChange = false ; }
// If three seconds have passed if (frameTime > startTime + (3*1000)) {
// Flashing of the message has been completed levelUp = false ; // Hide the info screen and force an update next time info.style.display = 'none' ; infoScreenChange = true ; // Set a new timer startTime = frameTime; }
return;
}
// [...] }
}
|
Не стесняйтесь дать игре снова. Это в значительной степени функционально, за исключением того, что все еще отсутствуют две вещи: счетчик очков и индикатор здоровья. Мы делаем успехи, сейчас!
Счетная базовая функция
Счетчик очков и индикатор здоровья имеют одну общую черту: они учитывают определенное значение. Поэтому для них будет хорошей идеей иметь объект, от которого они могут наследовать: счетный базовый объект — так же, как мы сделали с определением подвижной базы для объекта корзины и блока.
Начиная с того факта, что как объект здоровья, так и объект оценки должны предоставить нам метод для изменения их значения, у них также должен быть метод, который возвращает текущее значение экземпляра объекта, и они оба будут иметь процедуру обновления и перемещения.
Конструктор
Конструктор счетной базы должен определять только те свойства, которые являются общими для всех наследников.
Это x
и y
для позиционирования, value
свойство, которое содержит значение исчисляемого (например, количество здоровья), targetValue
свойства и speed
свойства.
speed
И targetValue
свойства будут использоваться для графического анимации, но мы вернемся к этому позже.
01
02
03
04
05
06
07
08
09
10
11
12
|
var Countable = function () {
this.x = 0;
this.y = 0;
this .speed = 2; this .value = 0; this .targetValue = 0; }
Countable.prototype = { };
|
reset
Метод должен быть объявлен с помощью наследодателя , который установит свойство объекта пользовательских значения для инициализации.
Обновление счетного объекта
Метод обновления счетного прототипа идентичен методу обновления подвижного прототипа, за исключением того, что он не требует проверки «жив».
1
2
3
4
5
6
7
|
Countable.prototype = { update: function () {
this .move(); this.draw();
}
};
|
Установка значения счетного объекта
Свойство targetValue
and value
будет использоваться для выполнения основной графической анимации, в которой индикатор работоспособности или счетчик оценок перемещаются из своего старого value
в новое targetValue
.
Это достигается не напрямую установкой value
свойства, а использованием подстанции, targetValue
которая называется, которую мы можем использовать для настройки анимации.
1
2
3
4
5
6
7
8
|
Countable.prototype = { // [...] set: function (amount) {
this .targetValue += amount; }
};
|
Добавляя желаемое количество изменений к целевому значению напрямую, мы также можем использовать отрицательное значение. Это дает нам возможность вычитать, но также добавлять определенное количество к целевому значению.
Перемещение счетного объекта
Процесс перемещения счетного объекта не связан с изменением его координат, как мы это делали с move
рутиной блока и корзины , а с изменением тока, value
так что в конечном итоге он будет таким же, как targetValue
.
Практически это приведет к тому, что value
свойство будет слегка увеличиваться или уменьшаться, пока оно не станет равным значению targetValue
.
Поскольку это value
свойство будет использоваться для рисования счета или индикатора состояния, изменение его во времени вместо установки непосредственно приводит к анимации.
Сначала мы должны определить, равно ли это значение целевому значению. Если это так, ничего не должно случиться. Если это не так, скорость должна быть добавлена ( value < targetValue
) или вычтена ( value > targetValue
) из фактического значения.
Как только разница между целевым и фактическим значением ниже, чем скорость анимации, значение должно быть установлено на целевое значение, так как сложение или вычитание скорости из значения будет более чем достаточным.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
Countable.prototype = { // [...] move: function () {
// If the difference between the target and actual value is lower than the animation speed, set the value to the target value if (Math.abs( this .value - this .targetValue) < this .speed) this .value = this .targetValue; else if ( this .targetValue > this .value) this .value += this .speed; else
this .value -= this .speed; }
};
|
Окончательное определение функции
Теперь, когда у нас есть базовый Countable определение функции, наследник нужно только определить reset
и draw
метод … и он готов к работе! Насколько это удобно?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
var Countable = function () {
this.x = 0;
this.y = 0;
this .speed = 2; this .value = 0; this .targetValue = 0; }
Countable.prototype = { update: function () {
this .move(); this.draw();
},
change: function (amount) {
this .targetValue += amount; },
move: function () {
// If the difference between the target and actual value is lower than the animation speed, set the value to the target value if (Math.abs( this .value - this .targetValue) < this .speed) this .value = this .targetValue; else if ( this .targetValue > this .value) this .value += this .speed; else
this .value -= this .speed; }
};
|
Объект Здоровья
Очевидно, что объект здоровья отвечает за отслеживание точек здоровья пользователя и выводит панель состояния на экран.
Как счетная функция базы уже поставляет нам с update
, change
и get
методом, нам нужно только определить reset
и draw
метод.
Конструктор
Хотя свойства value
и targetValue
объекта этого объекта должны быть сброшены в reset
методе, конструктор определяет static x
и y
свойство.
Если мы оглянемся назад на эскиз, то увидим, что мы планировали расположить индикатор работоспособности в верхнем правом углу с некоторым дополнительным полем от границ холста.
Это достигается путем задания для x
свойства ширины холста, вычитаемой из ширины индикатора работоспособности (52 пикселя) и требуемого правого поля (10 пикселов). Во-вторых, мы установим для y
свойства значение 10, которое является не более чем верхним полем.
Мы также делаем вызов еще не определенному reset
методу, который установит свойство value
and targetValue
при создании экземпляра.
01
02
03
04
05
06
07
08
09
10
|
var Health = function () {
Countable.call( this ); this .x = canvas.width - 52 - 10; this .y = 10; this .reset(); }
Health.prototype = new Countable(); |
Сброс объекта Health
Базовый счетный объект уже определяет несколько свойств, но, поскольку не все наследники имеют одинаковые точные значения свойств, reset
метод его наследника установит для этих свойств правильные значения.
Поскольку мы хотим начать игру с анимацией заполнения полосы здоровья, мы установим начальное значение здоровья равным 1, а целевое значение равным 100 вместо того, чтобы оставить для них значения по умолчанию.
1
2
3
4
5
6
|
Health.prototype.reset = function () {
// If we would leave it at a default of 0, the game would immediately end as it equals a loss of the game this .value = 1; this .targetValue = 100; }
|
Рисование объекта Здоровье
Панель состояния состоит из трех сотрудничающих рисунков; контейнер, фактическая панель состояния, ширина которой колеблется, и небольшой фрагмент текста для интерфейса.
Контейнер
Контейнер довольно просто нарисовать, хотя он представляет новый метод Canvas 2D API: strokeRect
метод; метод для рисования прямоугольника с границей, определенной текущим стилем обводки.
strokeRect
требует точно такие же параметры как fillRect
: x, y, ширина и высота. Чтобы определить цвет обводки, используется его strokeStyle
свойство Canvas 2D API . Мы оставим значение по умолчанию черного цвета.
Как и fillRect
метод, цвет заливки можно определить, установив fillStyle
свойство.
1
2
3
4
5
6
|
Health.prototype.draw = function () {
// The container context.fillStyle = '#fff' ; context.strokeRect( this .x, this .y, 50 + 2, 5 + 2); }
|
Обратите внимание, что мы добавляем два пикселя к ширине и высоте контейнера. Это делается для того, чтобы на самом деле сделать контейнер размером 50х5 пикселей, так как Canvas 2D API помещает границу внутри определенной ширины и высоты.
Бар здоровья
Анимация, ее логика уже обрабатывается унаследованным move
методом счетного объекта. В сочетании с изменяющимся цветом, это придаст изменениям, внесенным в процент здоровья, хороший переход.
Установить Canvas 2D API для его fillStyle
свойства в соответствии с процентом работоспособности невероятно просто при использовании нескольких операторов if.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
Health.prototype.draw = function () {
// The container context.fillStyle = '#fff' ; context.strokeRect( this .x, this .y, 50 + 2, 5 + 2); // The bar if ( this .value >= 50) context.fillStyle = '#00ff00' ; else if ( this .value >= 25) context.fillStyle = '#fa6600' ; else if ( this .value >= 0) context.fillStyle = '#ff0000' ; }
|
Процесс рисования панели здоровья не намного сложнее; нам нужно только нарисовать заполненный прямоугольник ( fillRect
) в одной и той же позиции контейнера, за исключением того, что мы добавляем один пиксель как к ширине, так и к высоте, чтобы расположить его в границах контейнера.
Ширина индикатора работоспособности просто определяется путем умножения текущего процента здоровья ( value
) на коэффициент масштабирования контейнера, его пустого пространства (50) и возможного максимального количества здоровья (100). 5 пикселей должно быть достаточно в качестве его высоты.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
Health.prototype.draw = function () {
// The container context.fillStyle = '#fff' ; context.strokeRect( this .x, this .y, 50 + 2, 5 + 2); // The bar if ( this .value >= 50) context.fillStyle = '#00ff00' ; else if ( this .value >= 25) context.fillStyle = '#fa6600' ; else if ( this .value >= 0) context.fillStyle = '#ff0000' ; context.fillRect( this .x + 1, this .y + 1, this .value * (50/100), 5); }
|
Текст «HP»
Визуализация текста на холсте может быть достигнута с помощью fillText
или strokeText
метода Canvas 2D API. Определение шрифта может быть установлено с помощью font
свойства (по умолчанию «10px sans-serif»).
Так же , как прямоугольники, то fillStyle
и strokeStyle
свойства используются для , соответственно , установить заливки или цвет границы. Кроме этого, есть также свойства для настройки выравнивания (по textAlign
умолчанию «начало») и базовой линии (по textBaseline
умолчанию «алфавитно») текста.
Мы будем использовать fillText
метод, так как не хотим, чтобы вокруг нашего текста была рамка. fillText
требуются следующие параметры: часть текста для печати и позиция x и y.
Чтобы упростить процесс размещения текста в нужном месте, мы установим для текста его базовую линию «top». Перед прорисовкой текста все еще есть некоторые отступы, и поэтому мы вычтем 3 пикселя из положения y, чтобы выровнять его по строке состояния.
1
2
3
4
5
6
7
8
9
|
Health.prototype.draw = function () {
// [...] // The text context.fillStyle = '#000' ; context.textBaseline = 'top' ; context.fillText( 'HP' , this .x - 25, this .y - 3); }
|
Окончательное определение функции
И с этим мы завершили наш объект здоровья.
Последний элемент, который мы должны собрать, прежде чем мы сможем назвать его концом, — это объект оценки.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
var Health = function () {
Countable.call( this ); this .x = canvas.width - 52 - 10; this .y = 10; }
Health.prototype = new Countable(); Health.prototype.reset = function () {
// If we would leave it at a default of 0, the game would immediately end as it equals a loss of the game this .value = 1; this .targetValue = 100; }
Health.prototype.draw = function () {
// The container context.fillStyle = '#fff' ; context.strokeRect( this .x, this .y, 50 + 2, 5 + 2); // The bar if ( this .value >= 50) context.fillStyle = '#00ff00' ; else if ( this .value >= 25) context.fillStyle = '#fa6600' ; else if ( this .value >= 0) context.fillStyle = '#ff0000' ; context.rect( this .x + 1, this .y + 1, this .value * (50/100), 5); // The text context.fillStyle = '#000' ; context.textBaseline = 'top' ; context.fillText( 'HP' , this .x - 25, this .y - 3); }
|
Объект счета
Объект Score наследуется от счетного базового объекта, что, опять же, дает нам преимущество, заключающееся в том, что у нас есть несколько методов для записи. Это означает, что нам нужно написать только два метода.
Один для рисования текста «PT» (сокращение для точек) и тока value
на холсте, а другой для сброса свойств объекта, чтобы игрок начал заново в начале нового уровня.
Конструктор
Поскольку положение счетчика очков является статическим и не требует сброса в начале новой игры, его можно определить в конструкторе.
В то время как позиция x должна быть такой же, как и у индикатора здоровья, чтобы правильно выровнять интерфейс, позиция y должна быть немного ниже. Это сводится к 10 пикселям для верхнего поля, дополнительным 7 пикселям для полосы здоровья и ее высоте и еще 5 пикселям для поля между строкой здоровья и счетчиком очков.
1
2
3
4
5
6
7
8
|
var Score = function () {
Countable.call( this ); this .x = canvas.width - 50 - 10; this .y = 10 + 7 + 5; }
Score.prototype = new Countable(); |
Сброс объекта Score
reset
Метод объекта Score должен вызываться только в начале новой игры , как счет должен быть принят вместе с уровнями игрока завершает — установка value
и targetValue
свойства к нулю является достаточным.
1
2
3
4
|
Score.prototype.reset = function () {
this .value = this .targetValue = 0; }
|
Рисование объекта Score
Фазирование всего рисунка объекта партитуры состоит не более чем из двух частей текста на холсте; «PT» и текущий счет.
Текст «PT» можно расположить так же, как мы это делали с текстом «HP»; немного изменив уже определенное свойство x, чтобы расположить его с пробелами от фактического счетчика очков.
1
2
3
4
5
6
7
|
Score.prototype.draw = function () {
context.textBaseline = 'top' ; context.fillStyle = '#000' ; context.fillText( this .value, this .x, this .y); context.fillText( 'PT' , this .x - 25, this .y); }
|
Окончательное определение функции
Это то, что для объекта оценки.
Давайте перейдем к финальной стадии и в действительности добавим объект здоровья и очков в нашу игру.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
var Score = function () {
Countable.call( this ); this .x = canvas.width - 52 - 10; this .y = 10 + 7 + 5; }
Score.prototype = new Countable(); Score.prototype.reset = function () {
this .value = this .targetValue = 0; }
Score.prototype.draw = function () {
context.textBaseline = 'top' ; context.fillStyle = '#000' ; context.fillText( this .value, this .x, this .y); context.fillText( 'PT' , this .x - 25, this .y); }
|
Собираем все вместе
Чтобы фактически интегрировать счетчик здоровья и очков в нашу игру, мы должны обновить мастер-упаковщик функций RGBCatcher.
Начиная с run
метода, мы должны добавить в него две строки для создания нового объекта счетчика здоровья и очков.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
RGBCatcher = new function () {
// [...] this .run = function () {
// [...] basket = new Basket(basketData); health = new Health(); score = new Score();
// [...] }
}
|
В resetGame
и resetLevel
методе линия , которые устанавливают переменные здоровья и оценки к нулю, когда они были еще просто целые значениями, должна быть заменена на призыв к здоровью и оценка объекта их reset
методу.
1
2
|
score.reset(); // old definition: score = 0; health.reset(); // old definition: health = 100; |
gameLoop
Метод его обработки , чтобы определить , по- прежнему имеет ли пользователь достаточно здоровья , чтобы продолжать играть, по- прежнему работает с health
переменной , как если бы это было целое ( health < 1
).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
RGBCatcher = new function () {
// [...] function gameLoop()
{
// [...] if (health.value < 1) {
clearInterval(interval); interval = setInterval(gameOver, 1000/targetFPS); }
// [...] }
}
|
К gameLoop
методу также должен быть добавлен вызов здоровья и оценка объектов их update
методами. Это хорошо вписывается в вызов update
метода объекта корзины .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
RGBCatcher = new function () {
// [...] function gameLoop()
{
// [...] basket.update(); health.update(); score.update(); // [...] }
}
|
Последнее, что мы должны изменить, это вызовы увеличения и уменьшения старых значений здоровья и оценки целых чисел в checkCollision
функции в оболочке RGBCatcher.
Эти две переменные больше не являются целыми числами, и поэтому мы должны использовать объект «здоровье» и «оценка» их change
методом, чтобы соответственно нанести урон или добавить очки к оценке.
1
2
3
4
5
6
7
8
|
score += block.strength;; // becomes score.change(block.strength); // and
health -= block.strength; // becomes health.change(- block.strength); |
И мы сделали!
Вывод
Используя здравый смысл и разделив элементы игры на отдельные задачи, не так уж сложно взломать основную игру с использованием технологий HTML5.
Не стесняйтесь и продолжайте исследовать удивительный мир Canvas 2D API. Например, вы можете попытаться изменить фазу рисования корзины так, чтобы она фактически имела разрыв.
Вы также можете подумать об улучшении игры, добавив блоки, которые улучшат здоровье пользователя или добавят максимальные результаты, используя функциональность локального хранилища HTML5 .
Что бы вы ни делали, получайте от этого удовольствие!