easy

Пинг-понг на JavaScript

Да, вдвоём тоже можно.

У нас очередной проект в кайф. В прошлый раз была змейка, теперь напишем игру в пинг-понг для двоих. Всё, что для этого понадобится, вы и так уже знаете. Если не знаете — почитайте про змейку на JavaScript, там много интересного.

Смысл игры — отбивать мячик, который летает по игровому полю. Для этого каждый участник двигает свою платформу, независимо от действий другого. Как только мячик вылетает за пределы поля — новая подача.

Логика проекта по шагам

  1. Берём пустую HTML-страницу и рисуем на ней игровое поле.Там же делаем отдельный скрипт для игры.
  2. В нём прописываем все переменные и константы, которые понадобятся.
  3. Описываем отдельно платформы и мячик.
  4. Делаем большой бесконечный цикл, где мы двигаем платформы и мячик.
  5. Не забываем следить за тем, какие клавиши нажаты, чтобы управлять платформой.

Страница с игровым полем

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

<!DOCTYPE html>
<html>
<head>
  <title>Пинг-понг на JavaScript</title>
  <!-- Задаём стили для всего документа -->
  <style>
    html,
    body {
      height: 100%;
      margin: 0;
    }
    body {
      background: black;
      display: flex;
      align-items: center;
      justify-content: center;
    }
  </style>
</head>
<body>
  <!-- Рисуем игровое поле -->
  <canvas width="750" height="585" id="game"></canvas>
  <script>
  </script>
</body>
</html>

Переменные и константы

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

// Обращаемся к игровому полю из документа
const canvas = document.getElementById('game');
// Делаем поле двухмерным
const context = canvas.getContext('2d');
// Размер игровой клетки
const grid = 15;
// Высота платформы
const paddleHeight = grid * 5; // 80
// Задаём максимальное расстояние, на которое может подняться платформа
const maxPaddleY = canvas.height - grid - paddleHeight;
// Скорость платформы
var paddleSpeed = 6;
// Скорость мяча
var ballSpeed = 5;

Платформы и мяч

Наша задача — описать отдельно каждую платформу для обоих игроков и мяч.

У нас есть переменная grid, которая отвечает за размер клетки на игровом поле. Мяч — это одна клетка, а платформа — 5 таких клеток подряд. На старте они обе стоят по краям, а мяч — в самом центре поля.

/ Описываем левую платформу
const leftPaddle = {
  // Ставим её по центру
  x: grid * 2,
  y: canvas.height / 2 - paddleHeight / 2,
  // Ширина — одна клетка
  width: grid,
  // Высоту берём из константы
  height: paddleHeight,
  // Платформа на старте никуда не движется
  dy: 0
};
// Описываем правую платформу 
const rightPaddle = {
  // Ставим по центру с правой стороны
  x: canvas.width - grid * 3,
  y: canvas.height / 2 - paddleHeight / 2,
  // Задаём такую же ширину и высоту
  width: grid,
  height: paddleHeight,
  // Правая платформа тоже пока никуда не двигается
  dy: 0
};
// Описываем мячик
const ball = {
  // Он появляется в самом центре поля
  x: canvas.width / 2,
  y: canvas.height / 2,
  // квадратный, размером с клетку
  width: grid,
  height: grid,
  // На старте мяч пока не забит, поэтому убираем признак того, что мяч нужно ввести в игру заново
  resetting: false,
  // Подаём мяч в правый верхний угол
  dx: ballSpeed,
  dy: -ballSpeed
};

Дополнительная функция: проверка на пересечение

Нам нужно будет знать, коснулся ли мяч платформы, чтобы отскочить, или нет. Для этого возьмём готовую функцию из интернета. Так как она написана под лицензией Creative Commons Attribution-ShareAlike license, то мы добавим в проект ссылку на оригинальную статью:

// Проверка на то, пересекаются два объекта с известными координатами или нет
// Подробнее тут: https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
function 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;
}

Главный цикл игры

В нём нам нужно:

  • очистить игровое поле от всего;
  • проследить за тем, чтобы ничего при движении не ушло за границы игрового поля;
  • перезапустить мяч из центра, если он всё-таки вылетел за платформу;
  • если игрок успел подставить платформу — сделать отскок мяча; отрисовать мяч, все стены и сетку посередине.

Сначала создадим сам цикл, а потом наполним его всем остальным:

 // Главный цикл игры
 function loop() {
   // Очищаем игровое поле
   requestAnimationFrame(loop);
   context.clearRect(0,0,canvas.width,canvas.height);
 // Тут будет остальное
 }

Следим за границами

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

// Если платформы на предыдущем шаге куда-то двигались — пусть продолжают двигаться
leftPaddle.y += leftPaddle.dy;
rightPaddle.y += rightPaddle.dy;
// Если левая платформа пытается вылезти за игровое поле вниз,
if (leftPaddle.y < grid) {
  // то оставляем её на месте
  leftPaddle.y = grid;
}
// Проверяем то же самое сверху
else if (leftPaddle.y > maxPaddleY) {
  leftPaddle.y = maxPaddleY;
}
// Если правая платформа пытается вылезти за игровое поле вниз,
if (rightPaddle.y < grid) {
  // то оставляем её на месте
  rightPaddle.y = grid;
}
// Проверяем то же самое сверху
else if (rightPaddle.y > maxPaddleY) {
  rightPaddle.y = maxPaddleY;
}
// Рисуем платформы белым цветом
context.fillStyle = 'white';
// Каждая платформа — прямоугольник
context.fillRect(leftPaddle.x, leftPaddle.y, leftPaddle.width, leftPaddle.height);
context.fillRect(rightPaddle.x, rightPaddle.y, rightPaddle.width, rightPaddle.height);
// Если мяч на предыдущем шаге куда-то двигался — пусть продолжает двигаться
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;
  }, 1000);
}

Если игрок успел отбить мяч

Используем функцию collides, о которой мы говорили раньше, — она проверяет, есть на пути мяча препятствие или нет. Если есть — меняем направление движения мячика:

// Если мяч коснулся левой платформы,
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;
}

Рисуем всё остальное

Самый простой элемент игры:

// Рисуем мяч
context.fillRect(ball.x, ball.y, ball.width, ball.height);
// Рисуем стены
context.fillStyle = 'lightgrey';
context.fillRect(0, 0, canvas.width, grid);
context.fillRect(0, canvas.height - grid, canvas.width, canvas.height);
// Рисуем сетку посередине
for (let i = grid; i < canvas.height - grid; i += grid * 2) {
  context.fillRect(canvas.width / 2 - grid / 2, i, grid, grid);
}

Последнее: нажатия на клавиши управления

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

Смысл в том, что платформы движутся только когда игрок зажимает клавишу. Как только он её отпускает — платформа останавливается. Именно поэтому мы сделаем два обработчика: один будет следить за нажатыми клавишами, а второй — за отпущенными.

// Отслеживаем нажатия клавиш
document.addEventListener('keydown', function (e) {
  // Если нажата клавиша вверх,
  if (e.which === 38) {
    // то двигаем правую платформу вверх
    rightPaddle.dy = -paddleSpeed;
  }
  // Если нажата клавиша вниз,
  else if (e.which === 40) {
    // то двигаем правую платформу вниз
    rightPaddle.dy = paddleSpeed;
  }
  // Если нажата клавиша W, 
  if (e.which === 87) {
    // то двигаем левую платформу вверх
    leftPaddle.dy = -paddleSpeed;
  }
  // Если нажата клавиша S,
  else if (e.which === 83) {
    // то двигаем левую платформу вниз
    leftPaddle.dy = paddleSpeed;
  }
});
// А теперь следим за тем, когда кто-то отпустит клавишу, чтобы остановить движение платформы
document.addEventListener('keyup', function (e) {
  // Если это стрелка вверх или вниз,
  if (e.which === 38 || e.which === 40) {
    // останавливаем правую платформу
    rightPaddle.dy = 0;
  }
  // А если это W или S, 
  if (e.which === 83 || e.which === 87) {
    // останавливаем левую платформу
    leftPaddle.dy = 0;
  }
});

Запускаем игру

Для запуска добавляем это в самый конец скрипта:

// Запускаем игру
requestAnimationFrame(loop);

Сохраняем всё как HTML-файл и открываем в браузере:

Пинг-понг на JavaScript

Что дальше

Эту игру можно улучшать бесконечно:

  • добавить счётчик очков,
  • нарисовать платформы разным цветом,
  • увеличить размер игрового поля,
  • добавить таблицу рекордов,
  • и вообще прикрутить к ней все правила настольного тенниса.

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

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
easy