Инструкции по выполнению шага
Теперь, когда функция отрисовки игрового поля вместе с описанием объектов мячика и ракетки у нас имеется, можно заняться описанием логики игры. Каждый момент времени в игре пинг-понга необходимо проделывать следующие вещи:
- очистить игровое поле от всего;
- проследить за тем, чтобы ничего при движении не ушло за границы игрового поля;
- перезапустить мяч из центра, если он всё-таки вылетел за платформу;
- если игрок успел подставить платформу — сделать отскок мяча;
- отрисовать игровое поле.
Всю механику игру опишем в отдельном скрипте JavaScript engine.js
. Не забудем указать новоиспечённый файл в главном файле игры – index.html
:
<!-- Начало места, которое мы изменяем -->
<img src="assets/background.jpg" id="background"></img>
<script src="js/render.js"></script>
<script src="js/engine.js"></script>
</body>
</html>
<!-- Конец редактирования -->
Вот так будет выглядеть наша папка проекта после создания файла engine.js
:
ping-pong
├── index.html
├── assets
│ ├── ball.png
│ ├── paddle.png
│ ├── background.jpg
│ └── style.css
└── js
├── render.js
└── engine.js
Обнаружение столкновения
Центральной механикой игры является движение мячика и его взаимодействие с другими объектами. Поэтому прежде всего нам понадобится функция, которая будет проверять столкновения мячика с платформами. Для этого возьмём готовую функцию из интернета. Так как она написана под лицензией Creative Commons Attribution-ShareAlike license, то мы добавим в проект ссылку на оригинальную статью:
Следующую часть кода добавляем в начало файла engine.js
.
// Проверка на то, пересекаются два объекта с известными координатами или нет
// Подробнее тут: https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
const collides = (obj1, obj2) => {
return (
obj1.x < obj2.x + obj2.width &&
obj1.x + obj1.width > obj2.x &&
obj1.y < obj2.y + obj2.height &&
obj1.y + obj1.height > obj2.y
);
};
Давайте взглянем на основы canvas
, используемые в программировании игр. Холст представляет собой сетку из пикселей. Когда мы используем холст для рисования 2D-изображений, каждый пиксель имеет местоположение x
и y
, которое описывает, где он находится на холсте:
Верхнеуровнево функция collides
проверяет наличие пересечения между двумя фигурами как на рисунке ниже:
Создание основного игрового цикла
Теперь можно создать игровой цикл, в котором будем обрабатывать все события игры:
Следующую часть кода добавляем в конец файла engine.js
.
// Главный цикл игры
const loop = () => {
// Логика
// ...
// Отрисовываем новый кадр
redraw();
// Логирование
console.log('!');
// Рекурсивный вызов игрового движка
requestAnimationFrame(loop);
};
Цикл представим в виде функции без аргументов, которая вызывает в конце саму себя с ограничением на частоту вызова при помощи метода requestAnimationFrame
. Таким образом, функция будет бесконечно вызывать саму себя снова и снова. Альтернативно можно было написать бесконечный цикл while. Логирование можно выполнить с помощью команды console.log(TEXT)
. Это позволит производить отладку программы более эффективно.
Проверим работоспособность игрового цикла. Добавим в конце файла engine.js
следующие строчки:
Следующую часть кода добавляем в конец файла engine.js
.
// Запускаем игру
requestAnimationFrame(loop);
После чего откроем главную страницу игры и откройте режим разработчика (в Google Chrome и Firefox это можно сделать при помощи горячей клавиши F12
). Затем выберите вкладку Console
или Консоль
.
Если вы всё сделали правильно, то на экране должен появиться восклицательный знак и постоянно растущее рядом с ним число:
Движение мячика
Когда работоспособность рекурсивного цикла была успешно проверена, можем перейти к написанию логики игры. Что нам нужно сделать в следующий момент игры? Сместить движущиеся предметы на следующий шаг и проверить, не вылезли ли они за границы поля. Только после этого можно вызывать написанную на Шаге 3 функцию отрисовки redraw()
. Код обновления положения объектов выглядит следующим образом:
Следующая часть кода изменяет функцию loop
в engine.js
.
const loop = () => {
// Если платформы на предыдущем шаге куда-то двигались — пусть продолжают двигаться
rightPaddle.y += rightPaddle.dy;
// Если правая платформа пытается вылезти за игровое поле вниз,
if (rightPaddle.y < grid) {
// то оставляем её на месте
rightPaddle.y = grid;
}
// Проверяем то же самое сверху
else if (rightPaddle.y > RightmaxPaddleY) {
rightPaddle.y = RightmaxPaddleY;
}
// Если мяч на предыдущем шаге куда-то двигался — пусть продолжает двигаться
ball.x += ball.dx;
ball.y += ball.dy;
// Если мяч касается стены снизу — меняем направление по оси У на противоположное
if (ball.y < grid) {
ball.y = grid;
ball.dy *= -1;
}
// Делаем то же самое, если мяч касается стены сверху
else if (ball.y + grid > canvas.height - grid) {
ball.y = canvas.height - grid * 2;
ball.dy *= -1;
}
// Отрисовываем новый кадр
redraw();
// Рекурсивный вызов игрового движка
requestAnimationFrame(loop);
};
Возвращаем мяч в игру, если он улетел
Всё, что нам для этого понадобится, — проверить, выходят ли координаты мяча за координаты границ поля. Если да — ставим мяч по центру и даём игроку секунду на подготовку.
Следующая часть кода изменяет функцию loop
в engine.js
.
// Главный цикл игры
const loop = () => {
// Если платформы на предыдущем шаге куда-то двигались — пусть продолжают двигаться
rightPaddle.y += rightPaddle.dy;
// Если правая платформа пытается вылезти за игровое поле вниз,
if (rightPaddle.y < grid) {
// то оставляем её на месте
rightPaddle.y = grid;
}
// Проверяем то же самое сверху
else if (rightPaddle.y > RightmaxPaddleY) {
rightPaddle.y = RightmaxPaddleY;
}
// Если мяч на предыдущем шаге куда-то двигался — пусть продолжает двигаться
ball.x += ball.dx;
ball.y += ball.dy;
// Если мяч касается стены снизу — меняем направление по оси У на противоположное
if (ball.y < grid) {
ball.y = grid;
ball.dy *= -1;
}
// Делаем то же самое, если мяч касается стены сверху
else if (ball.y + grid > canvas.height - grid) {
ball.y = canvas.height - grid * 2;
ball.dy *= -1;
}
// Если мяч улетел за игровое поле влево или вправо — перезапускаем его
if ((ball.x < 0 || ball.x > canvas.width) && !ball.resetting) {
// Помечаем, что мяч перезапущен, чтобы не зациклиться
ball.resetting = true;
// Даём секунду на подготовку игрокам
setTimeout(() => {
// Всё, мяч в игре
ball.resetting = false;
// Снова запускаем его из центра
ball.x = canvas.width / 2;
ball.y = canvas.height / 2;
rightPaddle.x = canvas.width - grid * 3;
rightPaddle.y = canvas.height / 2 - paddleHeight / 2;
}, 1000);
}
// Отрисовываем новый кадр
redraw();
// Рекурсивный вызов игрового движка
requestAnimationFrame(loop);
};
Как можно догадаться, метод, который даёт игроку секунду на подготовку называется setTimeout(callback, timeOut)
. Как только пройдёт timeOut миллисекунд (1000 мс = 1с), то вызывается так называемая функция обратного вызова или callback. В данном случае, мы задаёс её как анонимную стрелочную функцию без имени.
Если игрок успел отбить мяч
Используем функцию collides, о которой мы говорили раньше, — она проверяет, есть на пути мяча препятствие или нет. Если есть — меняем направление движения мячика:
// Главный цикл игры
const loop = () => {
// Если платформы на предыдущем шаге куда-то двигались — пусть продолжают двигаться
rightPaddle.y += rightPaddle.dy;
// Если правая платформа пытается вылезти за игровое поле вниз,
if (rightPaddle.y < grid) {
// то оставляем её на месте
rightPaddle.y = grid;
}
// Проверяем то же самое сверху
else if (rightPaddle.y > RightmaxPaddleY) {
rightPaddle.y = RightmaxPaddleY;
}
// Если мяч на предыдущем шаге куда-то двигался — пусть продолжает двигаться
ball.x += ball.dx;
ball.y += ball.dy;
// Если мяч касается стены снизу — меняем направление по оси У на противоположное
if (ball.y < grid) {
ball.y = grid;
ball.dy *= -1;
}
// Делаем то же самое, если мяч касается стены сверху
else if (ball.y + grid > canvas.height - grid) {
ball.y = canvas.height - grid * 2;
ball.dy *= -1;
}
// Если мяч улетел за игровое поле влево или вправо — перезапускаем его
if ((ball.x < 0 || ball.x > canvas.width) && !ball.resetting) {
// Поме чаем, что мяч перезапущен, чтобы не зациклиться
ball.resetting = true;
// Даём секунду на подготовку игрокам
setTimeout(() => {
// Всё, мяч в игре
ball.resetting = false;
// Снова запускаем его из центра
ball.x = canvas.width / 2;
ball.y = canvas.height / 2;
rightPaddle.x = canvas.width - grid * 3;
rightPaddle.y = canvas.height / 2 - paddleHeight / 2;
}, 1000);
}
// Если мяч коснулся левой платформы,
if (collides(ball, leftPaddle)) {
// то отправляем его в обратном направлении
ball.dx *= -1;
// Увеличиваем координаты мяча на ширину платформы, чтобы не засчитался новый отскок
ball.x = leftPaddle.x + leftPaddle.width;
}
// Проверяем и делаем то же самое для правой платформы
else if (collides(ball, rightPaddle)) {
ball.dx *= -1;
ball.x = rightPaddle.x - ball.width;
}
// Отрисовываем новый кадр
redraw();
// Рекурсивный вызов игрового движка
requestAnimationFrame(loop);
};
Итоговый результат выполнения шага можно скачать тут.