Статьи

Учебник по HTML5 Avoider: несколько движущихся врагов

В первой части этой серии вы узнали основы использования JavaScript и элемента canvas для создания очень простой HTML5-игры для избегания. Но это слишком просто — единственный враг даже не двигается — нет проблем! В этом уроке вы узнаете, как создать бесконечный поток врагов, падающих с верхней части экрана.


В первой части руководства мы рассмотрели несколько концепций: рисование изображений на экране, взаимодействие между HTML и JavaScript, обнаружение действий мыши и оператор if . Вы можете скачать исходные файлы здесь, если вы хотите погрузиться в эту часть руководства, хотя я рекомендую прочитать все части по порядку.

HTML-страница нашей игры содержит элемент canvas, который при нажатии вызывает JavaScript-функцию drawAvatar() . Эта функция находится внутри отдельного файла с именем main.js и выполняет две функции:

  • Рисует копию avatar.png на холсте.
  • Устанавливает прослушиватель событий для вызова другой функции, называемой redrawAvatar() , всякий раз, когда мышь перемещается по холсту.

Функция redrawAvatar() также находится внутри main.js ; в отличие от drawAvatar() он принимает параметрmouseEvent — который автоматически передается ему слушателем события. Этот mouseEvent содержит информацию о положении мыши. Функция выполняет четыре вещи:

  • Очищает холст
  • Рисует копию avatar.png на холсте в позиции мыши.
  • Рисует копию enemy.png на холсте в указанной позиции.
  • Проверяет, находятся ли аватар и враг достаточно близко друг к другу, чтобы перекрываться, и отображает alert() если это так.

Все чисто? Если нет, попробуйте разминку.


Если прошло много времени с тех пор, как вы прочитали первую часть серии (или если вы просто хотите проверить, понимаете ли вы, что происходит), попробуйте эти небольшие упражнения. Они совершенно необязательны и отделены от самого учебника, поэтому я рекомендую работать с копией вашего проекта, а не с оригиналом. Вы можете выполнить все эти упражнения, используя только информацию из первой части серии.

Помните, что drawImage() работает как картофельная печать. Используйте его, чтобы создать непрерывное кольцо врагов по краю холста, например так:

Руководство по игре в HTML5

(Если вам надоест копировать и вставлять все эти утверждения, не стесняйтесь сделать его меньшим кольцом — вы также можете изменить размер холста, если хотите).

Сделайте так, чтобы предупреждение «вы ударили врага» появлялось всякий раз, когда аватар попадает на край кольца. (Чтобы проверить это, помните, что вы можете нажать Enter, чтобы отключить предупреждение; вам не нужно нажимать OK.)

Это предупреждение появится, когда вы попытаетесь переместить мышь из холста внутрь, что очень раздражает, если вы уже щелкнули по холсту. Сделайте возможным переход на холст, не вызывая оповещения, но попав внутрь, заставьте оповещение появляться всякий раз, когда аватар касается кольца врагов.


Мы собираемся заставить врага упасть с вершины экрана. Сейчас мы сосредоточимся на том, чтобы заставить его двигаться, а не на обнаружении столкновения, поэтому «закомментируйте» строки в redrawEnemy() которые имеют дело со столкновениями, вот так:

1
2
3
//if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
// alert(«You hit the enemy!»);
//}

Помните: две косые черты говорят браузеру «с этого момента игнорируйте все в этой строке». Ранее мы использовали это для создания комментариев — небольших напоминаний о том, что делают определенные фрагменты кода, — но здесь мы используем это для другой цели: остановка выполнения определенных фрагментов кода без их полного удаления. Это позволяет нам легко вернуть код позже.

На данный момент противник перерисовывается в той же позиции, когда мы перемещаем мышь:

1
gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, 150);

Помните ли вы Math.random() из первой части урока? Возвращает случайное число от нуля до единицы; умножив его на 300 (высота холста), мы получили бы число от 0 до 300. Что произойдет, если мы будем рисовать противника в случайной позиции y от 0 до 300 при каждом перемещении мыши? Давайте попробуем это; измените эту строку следующим образом:

1
gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, Math.random() * 300);

Попробуйте это здесь !

Похоже, что противник движется случайным образом вверх и вниз по определенной линии, но только при перемещении мыши. Конечно, это на самом деле не движется; он «телепортируется» из одной позиции в другую, но это создает иллюзию движения.

Мы бы получили лучшую иллюзию, если бы она двигалась только в одном направлении. Мы можем добиться этого, убедившись, что у-позиция противника только увеличивается и никогда не уменьшается.

Рассмотрим следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
function redrawAvatar(mouseEvent) {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
    var enemyImage = new Image();
    var enemyY = 0;
 
    avatarImage.src = «img/avatar.png»;
    enemyImage.src = «img/enemy.png»;
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY);
 
    enemyY = enemyY + 1;
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
     
    //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
    // alert(«You hit the enemy!»);
    //}
}

Видишь, что я делаю? Я установил enemyY значение 0 в верхней части функции, затем увеличил его на один пиксель, прежде чем рисовать врага. Таким образом, я стремлюсь увеличить у-позицию противника на один пиксель каждый раз, когда redrawAvatar() .

Однако в моей логике есть недостаток. Линия var enemyY = 0; будет сбрасывать enemyY переменную на 0 каждый раз, когда redrawAvatar() , это означает, что она всегда будет отображаться в позиции y, равной 1 (поскольку она будет увеличена в строке 12)

Нам нужно только установить его в 0 раз. Что если мы сделаем это в drawEnemy() ? В конце концов, эта функция запускается только один раз:

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
function drawAvatar() {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
    var enemyY = 0;
     
    avatarImage.src = «img/avatar.png»;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, Math.random() * 100, Math.random() * 100);
     
    gameCanvas.addEventListener(«mousemove», redrawAvatar);
}
 
function redrawAvatar(mouseEvent) {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
    var enemyImage = new Image();
 
    avatarImage.src = «img/avatar.png»;
    enemyImage.src = «img/enemy.png»;
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY);
 
    enemyY = enemyY + 1;
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
     
    //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
    // alert(«You hit the enemy!»);
    //}
}

Попробуйте это здесь !

К сожалению, это не работает вообще. Проблема заключается в концепции, называемой областью действия . Если вы используете ключевое слово var для определения переменной внутри функции, тогда переменная будет доступна только внутри этой функции. Это означает, что наша redrawAvatar() не может получить доступ к той же переменной enemyY которая была определена в drawAvatar() .

Однако, если мы определяем переменную вне всех функций, она может быть доступна любой из них! Итак, попробуйте это:

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
var enemyY = 0;
 
function drawAvatar() {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
     
    avatarImage.src = «img/avatar.png»;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, Math.random() * 100, Math.random() * 100);
     
    gameCanvas.addEventListener(«mousemove», redrawAvatar);
}
 
function redrawAvatar(mouseEvent) {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
    var enemyImage = new Image();
 
    avatarImage.src = «img/avatar.png»;
    enemyImage.src = «img/enemy.png»;
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY);
 
    enemyY = enemyY + 1;
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
     
    //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
    // alert(«You hit the enemy!»);
    //}
}

Попробуйте это здесь !

Это работает — враг скользит вниз по экрану. Тем не менее, это происходит только тогда, когда мы перемещаем мышь. Это интересная игровая механика, но это не то, к чему я стремился.


Было бы намного лучше, если бы противник двигался сам по себе — это значит, что он движется независимо от того, двигает игрок мышью или нет. Мы можем сделать это, запустив его движение (его «телепортацию»), основанное на времени, а не на движении мыши.

Мы можем сделать это с помощью функции setInterval() . Это работает так:

1
setInterval(functionName, period);

Здесь functionName — это имя функции, которую мы хотим запускать снова и снова, а period — количество времени (в миллисекундах), которое мы хотим передать между каждым вызовом этой функции.

Посмотрим, как это выглядит:

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 enemyY = 0;
 
function drawAvatar() {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
     
    avatarImage.src = «img/avatar.png»;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, Math.random() * 100, Math.random() * 100);
     
    gameCanvas.addEventListener(«mousemove», redrawAvatar);
    setInterval(redrawEnemy, 1000);
}
 
function redrawAvatar(mouseEvent) {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
 
    avatarImage.src = «img/avatar.png»;
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY);
     
    //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
    // alert(«You hit the enemy!»);
    //}
}
 
function redrawEnemy() {
    var enemyImage = new Image();
    enemyImage.src = «img/enemy.png»;
     
    enemyY = enemyY + 1;
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
}

Я переместил весь код, касающийся перемещения и рисования врага, в новую redrawEnemy() , и я установил, что он будет вызываться каждые 1000 миллисекунд (каждую секунду) с помощью вызова setInterval() в drawAvatar() . (В отличие от использования прослушивателя событий, никакие параметры автоматически не передаются в redrawEnemy() когда мы вызываем его из setInterval() .)

Попробуйте это здесь ! Нажмите на холст, затем не двигайте мышью.

Руководство по игре в HTML5

Есть несколько вещей не так с этим:

  • Враг оставляет след — это потому, что холст не очищается в redrawEnemy() .
  • Враг движется очень медленно — возможно, 1000 миллисекунд слишком долго ждать.
  • Когда аватар перемещается, враг исчезает — это потому, что враг обращается только в redrawEnemy() ; в redrawAvatar() холст очищается, а аватар перерисовывается, но не враг.

Давайте исправим это по одному. Сначала мы очистим холст в redrawEnemy() :

1
2
3
4
5
6
7
8
9
function redrawEnemy() {
    var enemyImage = new Image();
    enemyImage.src = «img/enemy.png»;
     
    gameCanvas.width = 400;
     
    enemyY = enemyY + 1;
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
}

Попробуйте это здесь !

Гектометр Теперь аватар исчезает всякий раз, когда враг втягивается, и наоборот. Конечно, это имеет смысл; мы redrawEnemy() холст в redrawEnemy() и redrawAvatar() , но никогда не рисуем одновременно врага и аватара.

Что, если мы переместили врага в redrawEnemy() — увеличив значение enemyY — но фактически нарисовали его в redrawAvatar() ?

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 enemyY = 0;
 
function drawAvatar() {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
     
    avatarImage.src = «img/avatar.png»;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, Math.random() * 100, Math.random() * 100);
     
    gameCanvas.addEventListener(«mousemove», redrawAvatar);
    setInterval(redrawEnemy, 1000);
}
 
function redrawAvatar(mouseEvent) {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
    var enemyImage = new Image();
    enemyImage.src = «img/enemy.png»;
     
    avatarImage.src = «img/avatar.png»;
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
 
    //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
    // alert(«You hit the enemy!»);
    //}
}
 
function redrawEnemy() {
    enemyY = enemyY + 1;
}

Попробуйте это здесь !

Это вроде работает, но мы вернулись к той проблеме, когда враг двигается только тогда, когда вы двигаете мышь. Однако на этот раз все немного по-другому; чтобы сделать это более очевидным, мы можем увеличить скорость противника за счет сокращения period . Установите значение 25 (это redrawEnemy() секунды, то есть redrawEnemy() будет запускаться 40 раз в секунду):

1
setInterval(redrawEnemy, 25);

Попробуйте это здесь !

Сравните это с более ранней версией, где враг двигался только когда мышь двигалась. Увидеть разницу? В новой позиции противника постоянно меняются, но это происходит «за кадром»; фактическое изображение врага движется только при движении мыши. Если вы подождете секунду или около того, прежде чем двигать мышь, изображение врага спрыгнет с экрана, чтобы догнать его реальное положение.

Отделение фактической позиции врага от позиции изображения врага , как это позволит нам решить нашу проблему.

Прежде чем мы продолжим, вас смущают названия функций? Я. redrawEnemy() фактически не рисует врага вообще. Давайте переименуем их в нечто, что будет легче отслеживать.

  • drawAvatar() запускается, когда мы запускаем игру, и он все настраивает, поэтому давайте переименуем его в setUpGame()
  • redrawAvatar() запускается при каждом перемещении мыши, поэтому давайте переименуем его в handleMouseMovement()
  • redrawEnemy() запускается каждую долю секунды; как будто есть часы, которые тикают 40 раз в секунду, и каждый тик вызывает функцию. Итак, давайте переименуем его в handleTick()
  • Не забудьте также переименовать все ссылки на функции в слушателе событий и в 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
var enemyY = 0;
 
function setUpGame() {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
     
    avatarImage.src = «img/avatar.png»;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, Math.random() * 100, Math.random() * 100);
     
    gameCanvas.addEventListener(«mousemove», handleMouseMovement);
    setInterval(handleTick, 25);
}
 
function handleMouseMovement(mouseEvent) {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
    var enemyImage = new Image();
    enemyImage.src = «img/enemy.png»;
     
    avatarImage.src = «img/avatar.png»;
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
 
    //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
    // alert(«You hit the enemy!»);
    //}
}
 
function handleTick() {
    enemyY = enemyY + 1;
}

(Вам также необходимо изменить HTML-страницу, чтобы атрибут onclick холста был "setUpGame();" а не "drawAvatar();" .

Я думаю, что это помогает увидеть, что происходит:

  • Когда мышь перемещается, мы перемещаем положение аватара, рисуем аватар в его текущем положении и привлекаем противника в его текущем положении.
  • Когда часы тикают, мы перемещаем позицию противника.
  • Нам нужно нарисовать врага и аватара одновременно (т.е. в одной функции).
  • Если мы рисуем противника только тогда, когда движется мышь, то движение противника не является плавным.

Это упрощает поиск возможного решения:

  • Когда мышь перемещается, перемещайте положение аватара.
  • Когда часы тикают, переместите позицию противника, нарисуйте аватар в его текущем положении и переведите противника в его текущее положение.

Давайте реализуем это. Все, что нам нужно сделать, это переместить код рисования из handleMouseMovement() в handleTick() , верно? Как это:

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 enemyY = 0;
 
function setUpGame() {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
     
    avatarImage.src = «img/avatar.png»;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, Math.random() * 100, Math.random() * 100);
     
    gameCanvas.addEventListener(«mousemove», handleMouseMovement);
    setInterval(handleTick, 25);
}
 
function handleMouseMovement(mouseEvent) {
    //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
    // alert(«You hit the enemy!»);
    //}
}
 
function handleTick() {
    var avatarImage = new Image();
    var enemyImage = new Image();
 
    enemyY = enemyY + 1;
 
    enemyImage.src = «img/enemy.png»;
    avatarImage.src = «img/avatar.png»;
 
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, mouseEvent.offsetX, mouseEvent.offsetY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
}

Хм. Это не правильно. У нас ничего не осталось в handleMouseMovement() . Ах, но это потому, что мы не отделили положение изображения аватара от фактического положения аватара, как мы сделали с врагом. Итак, давайте сделаем это:

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
var enemyY = 0;
var avatarX = 0;
var avatarY = 0;
 
function setUpGame() {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
     
    avatarImage.src = «img/avatar.png»;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, Math.random() * 100, Math.random() * 100);
     
    gameCanvas.addEventListener(«mousemove», handleMouseMovement);
    setInterval(handleTick, 25);
}
 
function handleMouseMovement(mouseEvent) {
    avatarX = mouseEvent.offsetX;
    avatarY = mouseEvent.offsetY;
     
    //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
    // alert(«You hit the enemy!»);
    //}
}
 
function handleTick() {
    var gameCanvas = document.getElementById(«gameCanvas»);
    var avatarImage = new Image();
    var enemyImage = new Image();
 
    enemyY = enemyY + 1;
 
    enemyImage.src = «img/enemy.png»;
    avatarImage.src = «img/avatar.png»;
 
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, avatarX, avatarY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
}

Мы должны создать новые переменные для хранения фактических x- и y-позиций аватара и определить эти переменные вне любой функции, чтобы мы могли получить к ним доступ из любого места.

Попробуйте это здесь !

Это работает! (Если это немного отрывисто, попробуйте закрыть некоторые вкладки или перезапустить Chrome; это сработало для меня.) Теперь у нас есть движущийся враг. Потребовалось время, чтобы добраться туда, но фактический код, который мы получили, не слишком сложен, я надеюсь, вы согласитесь.

Если вы хотите испытать трудности, попробуйте снова ввести окно оповещения об обнаружении столкновений. Не волнуйтесь, если у вас есть проблемы; мы пройдем через это немного позже.

А пока мы рассмотрим проблему, с которой вы, вероятно, еще не сталкивались …


Как я упоминал в первой части этой серии, если вы поместите свою игру на веб-сервер таким, какой он есть сейчас, он не будет работать правильно, даже если они будут работать нормально при запуске с вашего компьютера. Мои демонстрации работают, потому что я сделал небольшое изменение в коде; вот как игра работает без этого кода:

Попробуйте это здесь .

В чем дело? Ну, это связано с изображениями. Каждый раз, когда тикают часы, мы создаем новое изображение и устанавливаем его источник так, чтобы он указывал на фактический файл изображения. Это не вызывает проблем, когда файл изображения находится на вашем жестком диске, но когда он находится в Интернете, страница может попытаться загрузить изображение перед его рисованием, что приводит к мерцанию, которое мы видим в демонстрационной версии.

Возможно, вы уже можете угадать решение, основанное на том, что мы уже сделали в этом руководстве. Как и в случае с позициями врага и аватара, мы можем перемещать изображения врага и аватара за пределы функций и использовать их снова и снова, без необходимости определять их и каждый раз устанавливать их значения в функции handleTick() .

Взглянем:

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
var enemyY = 0;
var avatarX = 0;
var avatarY = 0;
var avatarImage;
var enemyImage;
 
function setUpGame() {
    var gameCanvas = document.getElementById(«gameCanvas»);
    avatarImage = new Image();
    enemyImage = new Image();
    enemyImage.src = «img/enemy.png»;
    avatarImage.src = «img/avatar.png»;
     
    gameCanvas.getContext(«2d»).drawImage(avatarImage, Math.random() * 100, Math.random() * 100);
     
    gameCanvas.addEventListener(«mousemove», handleMouseMovement);
    setInterval(handleTick, 25);
}
 
function handleMouseMovement(mouseEvent) {
    avatarX = mouseEvent.offsetX;
    avatarY = mouseEvent.offsetY;
     
    //if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
    // alert(«You hit the enemy!»);
    //}
}
 
function handleTick() {
    var gameCanvas = document.getElementById(«gameCanvas»);
 
    enemyY = enemyY + 1;
 
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, avatarX, avatarY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
}

Попробуйте это здесь — не мерцание!

Если вам интересно: мы могли бы также переместить строки 9-12 за пределы функций. Я решил поместить их в setUpGame() просто потому, что они, похоже, больше о настройке игры.


На самом деле очень легко заставить другого врага появляться на экране. Помните, что изображения похожи на картофельные марки; это означает, что ничто не мешает нам рисовать вражеское изображение на холсте в двух разных местах в пределах одного тика:

01
02
03
04
05
06
07
08
09
10
function handleTick() {
    var gameCanvas = document.getElementById(«gameCanvas»);
 
    enemyY = enemyY + 1;
 
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, avatarX, avatarY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 100, enemyY);
}
Руководство по игре в HTML5
Нажмите, чтобы попробовать.

Просто!

Вы можете разместить их на разной высоте, например так:

01
02
03
04
05
06
07
08
09
10
function handleTick() {
    var gameCanvas = document.getElementById(«gameCanvas»);
 
    enemyY = enemyY + 1;
 
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, avatarX, avatarY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 100, enemyY — 50);
}
Руководство по игре в HTML5
Нажмите, чтобы попробовать.

Это немного грязно, хотя. Вместо этого, как насчет создания переменной enemyY2 ?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
var enemyY = 0;
var enemyY2 = -50;
var avatarX = 0;
var avatarY = 0;
var avatarImage;
var enemyImage;
 
//…
 
function handleTick() {
    var gameCanvas = document.getElementById(«gameCanvas»);
 
    enemyY = enemyY + 1;
    enemyY2 = enemyY2 + 1;
     
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, avatarX, avatarY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 100, enemyY2);
}

Теперь вы можете установить начальные позиции enemyY и enemyY2 как хотите, без необходимости изменять код в handleTick() .


Попробуйте расширить то, что мы только что сделали, чтобы у нас было пять врагов с разными начальными точками. Посмотрите на мой код, если вам нужно. Вот подсказка: вам нужно только добавить код вне функций и внутри функции handleTick() — не нужно прикасаться к setUpGame() или handleMouseMovement() .

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
var enemyY = 0;
var enemyY2 = -50;
var enemyY3 = -75;
var enemyY4 = -120;
var enemyY5 = -250;
var avatarX = 0;
var avatarY = 0;
var avatarImage;
var enemyImage;
 
//…
 
function handleTick() {
    var gameCanvas = document.getElementById(«gameCanvas»);
 
    enemyY = enemyY + 1;
    enemyY2 = enemyY2 + 1;
    enemyY3 = enemyY3 + 1;
    enemyY4 = enemyY4 + 1;
    enemyY5 = enemyY5 + 1;
     
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, avatarX, avatarY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 130, enemyY2);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 300, enemyY3);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 50, enemyY4);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 190, enemyY5);
}
Руководство по игре в HTML5
Нажмите, чтобы попробовать.

О, это будет утомительно, верно? Поддержание всех этих врагов и добавление трех строк кода для каждого. Тьфу.

Позвольте мне представить массивы . Взгляните на это:

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
var enemyYPositions = [0, -50, -75, -120, -250];
var avatarX = 0;
var avatarY = 0;
var avatarImage;
var enemyImage;
 
//…
 
function handleTick() {
    var gameCanvas = document.getElementById(«gameCanvas»);
 
    enemyYPositions[0] = enemyYPositions[0] + 1;
    enemyYPositions[1] = enemyYPositions[1] + 1;
    enemyYPositions[2] = enemyYPositions[2] + 1;
    enemyYPositions[3] = enemyYPositions[3] + 1;
    enemyYPositions[4] = enemyYPositions[4] + 1;
     
    gameCanvas.width = 400;
    gameCanvas.getContext(«2d»).drawImage(avatarImage, avatarX, avatarY);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyYPositions[0]);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 130, enemyYPositions[1]);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 300, enemyYPositions[2]);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 50, enemyYPositions[3]);
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 190, enemyYPositions[4]);
}

Это дает нам тот же результат, что и раньше, но:

  • Все y позиции врагов определены в одной строке, и
  • Мы получаем простой способ ссылаться на любую из этих позиций: enemyYPosition[ enemyNumber ] .

Этот тип переменной называется массивом ; это способ хранения списка значений (или даже других переменных), который позволяет нам извлекать любой элемент из этого списка, используя число. Обратите внимание, что первый элемент массива имеет номер 0, второе значение — номер 1 и т. Д. По этой причине мы называем массивы «на основе нуля».

Теперь взгляните на этот раздел кода:

1
2
3
4
5
enemyYPositions[0] = enemyYPositions[0] + 1;
enemyYPositions[1] = enemyYPositions[1] + 1;
enemyYPositions[2] = enemyYPositions[2] + 1;
enemyYPositions[3] = enemyYPositions[3] + 1;
enemyYPositions[4] = enemyYPositions[4] + 1;

Мы делаем одно и то же снова и снова для разных элементов массива. Каждая строка кода такая же, как и все остальные, за исключением того, что число в квадратных скобках изменяется. Это здорово, потому что мы можем написать код, который говорит: «делай одно и то же пять раз, но каждый раз меняя одно число». Например:

1
2
3
4
5
var currentNumber = 0;
while (currentNumber < 5) {
    alert(currentNumber);
    currentNumber = currentNumber + 1;
}

Если вы поместите это в свой JS-файл (например, в setUpGame() ), на странице отобразится пять окон с предупреждениями: первый из них скажет «0»; второй скажет «1»; и так далее до «4». Другими словами, это эквивалентно выполнению этого:

01
02
03
04
05
06
07
08
09
10
11
var currentNumber = 0;
alert(currentNumber);
currentNumber = currentNumber + 1;
alert(currentNumber);
currentNumber = currentNumber + 1;
alert(currentNumber);
currentNumber = currentNumber + 1;
alert(currentNumber);
currentNumber = currentNumber + 1;
alert(currentNumber);
currentNumber = currentNumber + 1;

Это потому, что оператор while действует как повторяющийся оператор if . Помните, if работает так:

1
2
3
if (condition) {
    outcome;
}

«Если condition истинно, то запустить outcome ».

while работает так:

1
2
3
while (condition) {
    outcome;
}

«Пока condition остается верным, продолжай работать outcome ».

Это тонкая разница, но очень важная. Блок if будет запущен только один раз, если condition истинно; блок while будет работать снова и снова, пока условие не прекратится, condition перестанет быть истинным.

По этой причине outcome — код, который запускается внутри блока while — обычно содержит некоторый код, который в конечном итоге приводит к тому, что condition перестает быть истинным; если это не так, код будет повторяться вечно. В нашем примере окна оповещения мы увеличивали значение currentNumber до currentNumber пор, пока значение « currentNumber < 5 » больше не было истинным (когда currentNumber достигло 5, оно было не меньше 5, поэтому мы никогда не видим окно оповещения, содержащее число 5 ).

Запуск кода снова и снова, такой как это, называется «зацикливанием», а блок while называется «циклом». Давайте теперь возьмем этот код:

1
2
3
4
5
enemyYPositions[0] = enemyYPositions[0] + 1;
enemyYPositions[1] = enemyYPositions[1] + 1;
enemyYPositions[2] = enemyYPositions[2] + 1;
enemyYPositions[3] = enemyYPositions[3] + 1;
enemyYPositions[4] = enemyYPositions[4] + 1;

… и поместите это в цикл:

1
2
3
4
var currentEnemyNumber = 0;
while (currentEnemyNumber < 5) {
    enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1;
}

Большой! Или это?

На самом деле, это не совсем правильно: мы не меняем значение currentEnemyNumber . Это означает, что мы просто будем увеличивать значение enemyYPositions[0] снова и снова, навсегда, никогда не меняя y-позиции других врагов и не выходя из цикла.

Итак, нам нужно сделать это:

1
2
3
4
5
var currentEnemyNumber = 0;
while (currentEnemyNumber < 5) {
    enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1;
    currentEnemyNumber = currentEnemyNumber + 1;
}

Теперь это здорово.

Можем ли мы применить то же мышление к нашему другому повторяющемуся коду? Я имею в виду это:

1
2
3
4
5
gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyYPositions[0]);
gameCanvas.getContext(«2d»).drawImage(enemyImage, 130, enemyYPositions[1]);
gameCanvas.getContext(«2d»).drawImage(enemyImage, 300, enemyYPositions[2]);
gameCanvas.getContext(«2d»).drawImage(enemyImage, 50, enemyYPositions[3]);
gameCanvas.getContext(«2d»).drawImage(enemyImage, 190, enemyYPositions[4]);

К сожалению, что-то вроде этого не будет работать:

1
2
3
4
5
var currentEnemyNumber = 0;
while (currentEnemyNumber < 5) {
    gameCanvas.getContext(«2d»).drawImage(enemyImage, 250, enemyYPositions[currentEnemyNumber]);
    currentEnemyNumber = currentEnemyNumber + 1;
}

… потому что не все враги имеют x-позицию 250. Но мы можем заставить ее работать, если переместим все x-позиции врагов в другой массив:

01
02
03
04
05
06
07
08
09
10
var enemyXPositions = [250, 130, 300, 50, 190];
var enemyYPositions = [0, -50, -75, -120, -250];
 
//…
 
var currentEnemyNumber = 0;
while (currentEnemyNumber < 5) {
    gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]);
    currentEnemyNumber = currentEnemyNumber + 1;
}

Это дает дополнительное преимущество, заключающееся в том, что все x- и y-позиции врагов находятся в одном аккуратном месте, а не разбросаны по нескольким линиям.

Давайте посмотрим на код в контексте:

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
var enemyYPositions = [0, -50, -75, -120, -250];
var enemyXPositions = [250, 130, 300, 50, 190];
var avatarX = 0;
var avatarY = 0;
var avatarImage;
var enemyImage;
 
//…
 
function handleTick() {
    var gameCanvas = document.getElementById("gameCanvas");
    var currentEnemyNumber = 0;
 
    while (currentEnemyNumber < 5) {
        enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1;
        currentEnemyNumber = currentEnemyNumber + 1;
    }
     
    gameCanvas.width = 400; //this erases the contents of the canvas
    gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY);
     
    currentEnemyNumber = 0;
    while (currentEnemyNumber < 5) {
        gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]);
        currentEnemyNumber = currentEnemyNumber + 1;
    }
}

Обратите внимание, что в строке 22 выше я сбросил currentEnemyNumberна 0; если бы я этого не сделал, цикл рисования изображений противника не запустился бы ни разу, как conditionбыло бы неверно из предыдущего цикла, который перемещал врагов. Также обратите внимание, что когда я делаю это, петля, которая привлекает врагов, не сразу обнаруживает, что conditionтеперь снова верно, и начинает перемещать всех врагов.

Все работает так же, как и раньше.

Руководство по игре в HTML5
Нажмите, чтобы попробовать.

Самое большое преимущество в том, как легко добавить еще пять врагов. Нам нужно всего лишь изменить четыре строки кода:

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
var enemyYPositions = [0, -50, -75, -120, -250, -280, -305, -330, -340, -400];
var enemyXPositions = [250, 130, 300, 50, 190, 200, 220, 60, 100, 110];
var avatarX = 0;
var avatarY = 0;
var avatarImage;
var enemyImage;
 
//…
 
function handleTick() {
    var gameCanvas = document.getElementById("gameCanvas");
    var currentEnemyNumber = 0;
 
    while (currentEnemyNumber < 10) {
        enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1;
        currentEnemyNumber = currentEnemyNumber + 1;
    }
     
    gameCanvas.width = 400; //this erases the contents of the canvas
    gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY);
     
    currentEnemyNumber = 0;
    while (currentEnemyNumber < 10) {
        gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]);
        currentEnemyNumber = currentEnemyNumber + 1;
    }
}
Руководство по игре в HTML5
Нажмите, чтобы попробовать.

Просто!


Вот еще одно упражнение для вас: измените код так, чтобы он создал пятнадцать врагов. Опять же, вам нужно всего лишь изменить четыре строки кода. Посмотрите на мой код, если вы не уверены:

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
var enemyYPositions = [0, -50, -75, -120, -250, -280, -305, -330, -340, -400, -425, -450, -500, -520, -550];
var enemyXPositions = [250, 130, 300, 50, 190, 200, 220, 60, 100, 110, 30, 300, 150, 190, 90];
var avatarX = 0;
var avatarY = 0;
var avatarImage;
var enemyImage;
 
//…
 
function handleTick() {
    var gameCanvas = document.getElementById("gameCanvas");
    var currentEnemyNumber = 0;
 
    while (currentEnemyNumber < 15) {
        enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1;
        currentEnemyNumber = currentEnemyNumber + 1;
    }
     
    gameCanvas.width = 400; //this erases the contents of the canvas
    gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY);
     
    currentEnemyNumber = 0;
    while (currentEnemyNumber < 15) {
        gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]);
        currentEnemyNumber = currentEnemyNumber + 1;
    }
}

Очевидно, мы могли бы продолжать в том же духе. Но меня уже раздражает изменение строк 14 и 23 выше, и я забыл это сделать пару раз.

К счастью, мы можем это каким-то образом автоматизировать. Число — 5, 10, 15 или любое другое — равно количеству элементов в массиве enemyXPositions[]или enemyYPositions[]. Мы называем это длиной массива и можем извлечь ее из любого массива, используя .lengthсвойство — например, так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
function handleTick() {
    var gameCanvas = document.getElementById("gameCanvas");
    var currentEnemyNumber = 0;
 
    while (currentEnemyNumber < enemyXPositions.length) {
        enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1;
        currentEnemyNumber = currentEnemyNumber + 1;
    }
     
    gameCanvas.width = 400; //this erases the contents of the canvas
    gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY);
     
    currentEnemyNumber = 0;
    while (currentEnemyNumber < enemyXPositions.length) {
        gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]);
        currentEnemyNumber = currentEnemyNumber + 1;
    }
}

Или, чтобы быть немного аккуратнее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
function handleTick() {
    var gameCanvas = document.getElementById("gameCanvas");
    var currentEnemyNumber = 0;
    var numberOfEnemies = enemyXPositions.length;
 
    while (currentEnemyNumber < numberOfEnemies) {
        enemyYPositions[currentEnemyNumber] = enemyYPositions[currentEnemyNumber] + 1;
        currentEnemyNumber = currentEnemyNumber + 1;
    }
     
    gameCanvas.width = 400; //this erases the contents of the canvas
    gameCanvas.getContext("2d").drawImage(avatarImage, avatarX, avatarY);
     
    currentEnemyNumber = 0;
    while (currentEnemyNumber < numberOfEnemies) {
        gameCanvas.getContext("2d").drawImage(enemyImage, enemyXPositions[currentEnemyNumber], enemyYPositions[currentEnemyNumber]);
        currentEnemyNumber = currentEnemyNumber + 1;
    }
}

Теперь вы можете сделать так много врагов , как вы хотите, просто добавляя новые номера к enemyXPositions[]и enemyYPositions[]массивам.


Помните, как работало обнаружение столкновений? У нас был код handleMouseMovement()(хотя и закомментированный) некоторое время:

1
2
3
if (mouseEvent.offsetX > 220 && mouseEvent.offsetX < 280 && mouseEvent.offsetY > 117 && mouseEvent.offsetY < 180) {
    alert("You hit the enemy!");
}

Он в основном проверяет, перекрываются ли два прямоугольника — один, который движется вместе с курсором, а другой — неподвижно. Но из этого кода трудно понять, поэтому давайте взглянем по-новому.

Во-первых, давайте посмотрим на это с точки зрения горизонтального перекрытия:

Руководство по игре в HTML5

Здесь аватар и враг не пересекаются. У нас есть:avatarX < avatarX + 30 < enemyX < enemyX + 30

Руководство по игре в HTML5

Здесь, аватар и враг являются перекрытие. У нас есть:avatarX < enemyX < avatarX + 30 < enemyX + 30

Руководство по игре в HTML5

Все еще перекрываются. У нас есть:enemyX < avatarX < enemyX + 30 < avatarX + 30

Руководство по игре в HTML5

Больше не перекрывается. У нас есть:enemyX < enemyX + 30 < avatarX < avatarX + 30

Взяв все это вместе, мы можем видеть, что враг и аватар пересекаются по горизонтали, если:

avatarX < enemyX и enemyX < avatarX + 30

…или:

enemyX < avatarX и avatarX < enemyX + 30

Другими словами, для горизонтального перекрытия это условие должно выполняться:

(avatarX < enemyX && enemyX < avatarX + 30) || (enemyX < avatarX && avatarX < enemyX + 30)

(Помните, &&означает «и»; ||означает «или».)

Нетрудно придумать аналогичное условие для вертикального перекрытия:

(avatarY < enemyY && enemyY < avatarY + 33) || (enemyY < avatarY && avatarY < enemyY + 30)

(Мы используем 33 здесь, потому что аватар 33 пикселя в высоту, но только 30 пикселей в ширину.)

Чтобы враг и аватар перекрывались, они должны перекрываться как по горизонтали, так и по вертикали — поэтому мы используем &&оператор для объединения этих двух условий:

( (avatarX < enemyX && enemyX < avatarX + 30) || (enemyX < avatarX && avatarX < enemyX + 30) ) && ( (avatarY < enemyY && enemyY < avatarY + 33) || (enemyY < avatarY && avatarY < enemyY + 30) )

Это долгое состояние! Но это на самом деле довольно просто, теперь, когда мы разбили его. Давайте поместим это в наш код. Сначала удалите старый код обнаружения столкновений из handleMouseMovement(); мы вставим это handleTick()после того, как переместим и перерисоваем врага и аватара, чтобы оно казалось более справедливым.

Для начала мы просто проверим наличие столкновения между аватаром и первым врагом:

1
2
3
4
5
6
7
8
function handleTick() {
 
    //…
     
    if ( ( (avatarX < enemyXPositions[0] && enemyXPositions[0] < avatarX + 30) || (enemyXPositions[0] < avatarX && avatarX < enemyXPositions[0] + 30) ) && ( (avatarY < enemyYPositions[0] && enemyYPositions[0] < avatarY + 33) || (enemyYPositions[0] < avatarY && avatarY < enemyYPositions[0] + 30) ) ) {
        alert("You hit the first enemy!");
    }
}

Это выглядит грязно (поэтому вы, вероятно, должны добавить комментарий, чтобы напомнить себе о том, как это работает позже), но в нем есть все, что нам нужно. Это работает?

Попробуйте это здесь !

Это работает! Теперь мы должны заставить это работать для всех врагов — и, конечно, мы можем использовать цикл, чтобы сделать это:

1
2
3
4
5
6
7
currentEnemyNumber = 0;
while (currentEnemyNumber < numberOfEnemies) {
    if ( ( (avatarX < enemyXPositions[currentEnemyNumber] && enemyXPositions[currentEnemyNumber] < avatarX + 30) || (enemyXPositions[currentEnemyNumber] < avatarX && avatarX < enemyXPositions[currentEnemyNumber] + 30) ) && ( (avatarY < enemyYPositions[currentEnemyNumber] && enemyYPositions[currentEnemyNumber] < avatarY + 33) || (enemyYPositions[currentEnemyNumber] < avatarY && avatarY < enemyYPositions[currentEnemyNumber] + 30) ) ) {
        alert("You hit an enemy!");
    }
    currentEnemyNumber = currentEnemyNumber + 1;
}

Обратите внимание, что это происходит внутри handleTick(), в конце, поэтому мы должны сбросить currentEnemyNumberна 0. Нам также нужно изменить текст окна предупреждения, так как это может быть не первый враг, который вызывает предупреждение.

Попробуйте это здесь !

Хорошо, это действительно превращается в игру! Ладно, конечно, окно оповещения немного раздражает, но пока оно служит нашим целям.

Есть еще одно большое дополнение, которое я хотел бы, чтобы мы сделали в этой части …


Мы можем добавлять все больше и больше вражеских позиций в наши массивы — сто, тысяча, что угодно — но в итоге запас закончится, и у игрока больше не будет врагов, чтобы увернуться. Нам нужно иметь возможность создавать новых врагов и добавлять их позиции в массивы во время игры .

Когда мы хотим изменить значение определенного элемента внутри enemyXPositions, это легко: мы просто пишем enemyXPositions[3] = 100или что-то еще. Но как мы можем добавить что-то в массив? Запись enemyXPositions = [100](или что-то еще) просто заменит массив новым, содержащим только один элемент.

Ответ в функции массивов .push(); это позволяет нам добавлять элемент в массив без создания нового. Чтобы продемонстрировать это, давайте удалим все элементы из наших массивов, а затем .push()добавим новые:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
var enemyYPositions = []; //empty square brackets means new empty array
var enemyXPositions = [];
var avatarX = 0;
var avatarY = 0;
var avatarImage;
var enemyImage;
 
function setUpGame() {
    var gameCanvas = document.getElementById("gameCanvas");
    avatarImage = new Image();
    enemyImage = new Image();
    enemyImage.src = "img/enemy.png";
    avatarImage.src = "img/avatar.png";
     
    enemyYPositions.push(0);
    enemyXPositions.push(250);
     
    gameCanvas.getContext("2d").drawImage(avatarImage, Math.random() * 100, Math.random() * 100);
     
    gameCanvas.addEventListener("mousemove", handleMouseMovement);
    setInterval(handleTick, 25);
}
Руководство по игре в HTML5
Нажмите, чтобы попробовать.

Работает отлично; враг все еще движется, и обнаружение столкновений все еще работает. Это точно так же, как если бы мы начали код с:

1
2
var enemyYPositions = [0];
var enemyXPositions = [250];

Так что же произойдет, если мы поместим новые значения в массивы внутри handleTick(), а не внутри setUpGame()?

1
2
3
4
5
6
7
function handleTick() {
    var gameCanvas = document.getElementById("gameCanvas");
    var currentEnemyNumber = 0;
    var numberOfEnemies = enemyXPositions.length;
     
    enemyYPositions.push(0);
    enemyXPositions.push(250);
Руководство по игре в HTML5
Нажмите, чтобы попробовать.

Гектометр Это создает новых врагов в тех же позициях с такой высокой скоростью, что они перекрывают друг друга. (Тот, что вверху экрана, кажется, выше всех остальных, потому что это последний, который будет нарисован.)

Попробуем исправить это, создав врагов в случайных стартовых x-позициях. Помните, что Math.random()мы получаем случайное число от 0 до 1, поэтому для получения случайного числа от 0 до 400 — ширина холста — мы можем использовать Math.random() * 400:

1
2
3
4
5
6
7
function handleTick() {
    var gameCanvas = document.getElementById("gameCanvas");
    var currentEnemyNumber = 0;
    var numberOfEnemies = enemyXPositions.length;
     
    enemyYPositions.push(0);
    enemyXPositions.push(Math.random() * 400);
Руководство по игре в HTML5
Нажмите, чтобы попробовать.

Argh!

Хорошо, так что, может быть, они все еще идут слишком быстро …

Прямо сейчас, враги создаются со скоростью одного нового врага за такт — и, поскольку такт составляет 25 мс, есть 40 тактов в секунду, и, следовательно, 40 новых врагов в секунду.

Давайте уменьшим это до чего-то более управляемого: около двух врагов в секунду.

Поскольку это 1/20 нашей текущей ставки, мы могли бы добиться этого, отслеживая количество пройденных тиков и создавая новый на тике № 20, тике № 40, тике № 60 и т. Д. Но я думаю, что будет веселее, если вместо этого у нас будет шанс 1/20 создания нового врага на любом данном тике. Таким образом, иногда мы создаем более двух новых врагов за одну секунду, а иногда мы создаем меньше, но это в среднем составляет два в секунду. Неопределенность может сделать игру немного более захватывающей (хотя, возможно, «захватывающая» — плохой выбор слов на этой ранней стадии разработки игры …).

Как мы можем это сделать? Итак, мы знаем, что Math.random()создаем случайное число от 0 до 1. Так как эти случайные числа равномерно распределены между 0 и 1, это означает, что сгенерированное число составляет 1/20, а где-то между 0 и … ну, 1 / 20.

Другими словами, шанс Math.random() < 1/20быть правдой составляет 1/20.

Итак, давайте изменим наш код, чтобы использовать этот факт:

01
02
03
04
05
06
07
08
09
10
function handleTick() {
    var gameCanvas = document.getElementById("gameCanvas");
    var currentEnemyNumber = 0;
    var numberOfEnemies = enemyXPositions.length;
     
    if (Math.random() < 1/20)
    {
        enemyYPositions.push(0);
        enemyXPositions.push(Math.random() * 400);
    }
Руководство по игре в HTML5
Нажмите, чтобы попробовать.

Намного лучше! Не стесняйтесь экспериментировать с этим условием, чтобы найти значение, которое работает для вас.


Вот именно для этой части серии. Теперь мы создали рудиментарную игру — не отточенную игру, не особенно веселую игру, но, тем не менее, игру.

Если вы хотите испытать себя перед следующей частью, попробуйте внести следующие изменения:

  • Легко: враги «попадают» на верх холста; вместо этого заставьте их скользить.
  • Легко: некоторые враги созданы частично за пределами холста; прекрати это
  • Средняя: все враги движутся с одинаковой скоростью, что довольно скучно; позволяют им иметь разные скорости.
  • Сложно: аватар может сойти с правого края холста, что делает невозможным прикосновение к нему врагов (при условии, что вы выполнили второй легкий вызов). Убедитесь, что аватар остается внутри границ.

Наслаждайтесь!