В первой части этой серии вы узнали основы использования 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()
работает как картофельная печать. Используйте его, чтобы создать непрерывное кольцо врагов по краю холста, например так:
(Если вам надоест копировать и вставлять все эти утверждения, не стесняйтесь сделать его меньшим кольцом — вы также можете изменить размер холста, если хотите).
средний
Сделайте так, чтобы предупреждение «вы ударили врага» появлялось всякий раз, когда аватар попадает на край кольца. (Чтобы проверить это, помните, что вы можете нажать 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()
.)
Попробуйте это здесь ! Нажмите на холст, затем не двигайте мышью.
Есть несколько вещей не так с этим:
- Враг оставляет след — это потому, что холст не очищается в
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);
}
|
Просто!
Вы можете разместить их на разной высоте, например так:
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);
}
|
Это немного грязно, хотя. Вместо этого, как насчет создания переменной 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);
}
|
Сделать десять врагов
О, это будет утомительно, верно? Поддержание всех этих врагов и добавление трех строк кода для каждого. Тьфу.
Позвольте мне представить массивы . Взгляните на это:
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
теперь снова верно, и начинает перемещать всех врагов.
Все работает так же, как и раньше.
Самое большое преимущество в том, как легко добавить еще пять врагов. Нам нужно всего лишь изменить четыре строки кода:
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; }
}
|
Просто!
Сделать пятнадцать врагов
Вот еще одно упражнение для вас: измените код так, чтобы он создал пятнадцать врагов. Опять же, вам нужно всего лишь изменить четыре строки кода. Посмотрите на мой код, если вы не уверены:
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!" ); }
|
Он в основном проверяет, перекрываются ли два прямоугольника — один, который движется вместе с курсором, а другой — неподвижно. Но из этого кода трудно понять, поэтому давайте взглянем по-новому.
Во-первых, давайте посмотрим на это с точки зрения горизонтального перекрытия:
Здесь аватар и враг не пересекаются. У нас есть:avatarX < avatarX + 30 < enemyX < enemyX + 30
Здесь, аватар и враг являются перекрытие. У нас есть:avatarX < enemyX < avatarX + 30 < enemyX + 30
Все еще перекрываются. У нас есть:enemyX < avatarX < enemyX + 30 < avatarX + 30
Больше не перекрывается. У нас есть: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); }
|
Работает отлично; враг все еще движется, и обнаружение столкновений все еще работает. Это точно так же, как если бы мы начали код с:
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); |
Гектометр Это создает новых врагов в тех же позициях с такой высокой скоростью, что они перекрывают друг друга. (Тот, что вверху экрана, кажется, выше всех остальных, потому что это последний, который будет нарисован.)
Попробуем исправить это, создав врагов в случайных стартовых 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); |
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); }
|
Намного лучше! Не стесняйтесь экспериментировать с этим условием, чтобы найти значение, которое работает для вас.
Завершение
Вот именно для этой части серии. Теперь мы создали рудиментарную игру — не отточенную игру, не особенно веселую игру, но, тем не менее, игру.
Если вы хотите испытать себя перед следующей частью, попробуйте внести следующие изменения:
- Легко: враги «попадают» на верх холста; вместо этого заставьте их скользить.
- Легко: некоторые враги созданы частично за пределами холста; прекрати это
- Средняя: все враги движутся с одинаковой скоростью, что довольно скучно; позволяют им иметь разные скорости.
- Сложно: аватар может сойти с правого края холста, что делает невозможным прикосновение к нему врагов (при условии, что вы выполнили второй легкий вызов). Убедитесь, что аватар остается внутри границ.
Наслаждайтесь!