Статьи

Разработайте свою первую игру на холсте от начала до конца

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

С разметкой не происходит никаких причудливых вещей — только 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 основан на прототипах, мы можем создать несколько экземпляров блоков из одного определения базового блока.


Теперь, когда у нас есть готовый список объектов, не составит труда создать для них оболочки.

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 инициализирует игровые компоненты (например, 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’ ‘TypeError, поэтому давайте добавим метод 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 &amp;&amp;
            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 &amp;&amp;
            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 &amp;&amp; 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 &amp;&amp; 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 для его изменения.

Экран заголовка будет помещен в общий элемент 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>
      
    <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Функция является дочерним-функция (метод) функции 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);
        }
    }
}

В нашем обновленном игровом цикле отсутствует подпрограмма, которая обрабатывает сообщение о повышении уровня.
У нас уже есть переменная, 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();
    }
};

Свойство targetValueand 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методу, который установит свойство valueand 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();

Базовый счетный объект уже определяет несколько свойств, но, поскольку не все наследники имеют одинаковые точные значения свойств, 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);
}

Визуализация текста на холсте может быть достигнута с помощью 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();

resetМетод объекта Score должен вызываться только в начале новой игры , как счет должен быть принят вместе с уровнями игрока завершает — установка valueи targetValueсвойства к нулю является достаточным.

1
2
3
4
Score.prototype.reset = function() 
{
    this.value = this.targetValue = 0;
}

Фазирование всего рисунка объекта партитуры состоит не более чем из двух частей текста на холсте; «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 .

Что бы вы ни делали, получайте от этого удовольствие!