Пинг-понг на JavaScript
Пинг-понг на JavaScript
Пинг-понг против компьютера на JavaScript Непобедимый пинг-понг на JavaScript Добавляем секретный уровень в пинг-понг на 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-файл и откры­ва­ем в браузере:

Что дальше

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

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

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