easy

Пинг-понг против компьютера на JavaScript

Лёгкая версия

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

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

Что нам понадобится

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

<!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>
    // Обращаемся к игровому полю из документа
    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;
    // Описываем левую платформу
    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
    };
    // Проверка на то, пересекаются два объекта с известными координатами или нет
    // Подробнее тут: 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);
      }
      // Если мяч коснулся левой платформы,
      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('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);
  </script>
</body>
</html>

Новая логика игры

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

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

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

Начнём с мяча.

Замедляем мяч

Это самое простое изменение — достаточно поменять одну цифру в коде, как игра становится более понятной и управляемой:

// Скорость мяча
var ballSpeed = 4;

Если 4 для вас всё равно слишком быстро — поставьте 3, а если хотите занять детей — то 2.

Увеличиваем левую платформу

Находим код, который отвечает за левую платформу. За размер отвечает параметр высоты. Просто умножаем её на 2:

// Описываем левую платформу
 const leftPaddle = {
   // Ставим её по центру
   x: grid * 2,
   y: canvas.height / 2 - paddleHeight / 2,
   // Ширина — одна клетка
   width: grid,
   // Удваиваем размер платформы
   height: paddleHeight * 2,
   // Платформа на старте никуда не движется
   dy: 0
};

Пока ничего никуда не движется, но сейчас мы это исправим.

Запускаем платформу

Для этого нам нужно добавить одну строчку сразу после предыдущего раздела. Она задаёт платформе начальную скорость, которая у нас прописана в константе paddleSpeed:

leftPaddle.dy = paddleSpeed;

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

Пинг-понг против компьютера на JavaScript

Делаем отскок от границ

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

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

 // Если левая платформа пытается вылезти за игровое поле вниз,
      if (leftPaddle.y < grid) {
          // то оставляем её на месте
          leftPaddle.y = grid;
  leftPaddle.dy = paddleSpeed;
      }
      // Проверяем то же самое сверху
      else if (leftPaddle.y > maxPaddleY) {
          leftPaddle.y = maxPaddleY;
  leftPaddle.dy = -paddleSpeed;
      }

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

Пинг-понг против компьютера на JavaScript

Исправляем баг с размером

Штука в том, что код не знает, что у левой платформы изменилась длина, несмотря на то, что мы это явно прописали в самом начале. Всё дело в том, что этот блок сравнения, который мы только что правили, оперирует другой константой: maxPaddleY. В исходной версии игры платформы были одинакового размера, поэтому всё было в порядке, но теперь у нас две разных длины.

Чтобы исправить этот баг с размером, нам нужно будет завести две отдельные константы — с длиной левой и правой платформ по отдельности. Немного модифицируем правую и добавим левую:

    const LeftmaxPaddleY = canvas.height - grid - paddleHeight * 2;
    const RightmaxPaddleY = canvas.height - grid - paddleHeight;

Теперь видно, что левая платформа в 2 раза больше правой, и координаты крайней точки тоже изменились. Теперь нам нужно просмотреть весь код и найти все места, где используется старая константа maxPaddleY и заменить её на новую, учитывая сторону платформы.

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

// Если левая платформа пытается вылезти за игровое поле вниз,
  if (leftPaddle.y < grid) {
        // то оставляем её на месте
    leftPaddle.y = grid;
    leftPaddle.dy = paddleSpeed;
  }
  // Проверяем то же самое сверху
  else if (leftPaddle.y > LeftmaxPaddleY) {
    leftPaddle.y = LeftmaxPaddleY;
    leftPaddle.dy = -paddleSpeed;
  }
  // Если правая платформа пытается вылезти за игровое поле вниз,
  if (rightPaddle.y < grid) {
 // то оставляем её на месте
    rightPaddle.y = grid;
  }
  // Проверяем то же самое сверху
  else if (rightPaddle.y > RightmaxPaddleY) {
    rightPaddle.y = RightmaxPaddleY;
  }

Теперь всё как нужно. Можно играть против роботизированной ракетки и, наверное, довольно часто выигрывать. 

Пинг-понг против компьютера на JavaScript
<!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>
    // Обращаемся к игровому полю из документа
    const canvas = document.getElementById('game');
    // Делаем поле двухмерным
    const context = canvas.getContext('2d');
    // Размер игровой клетки
    const grid = 15;
    // Высота платформы
    const paddleHeight = grid * 5; // 80
    // Задаём максимальное расстояние, на которое могут двигаться платформы
    const LeftmaxPaddleY = canvas.height - grid - paddleHeight * 2;
    const RightmaxPaddleY = canvas.height - grid - paddleHeight;
    // Скорость платформы
    var paddleSpeed = 6;
    // Скорость мяча
    var ballSpeed = 4;
    // Описываем левую платформу
    const leftPaddle = {
      // Ставим её по центру
      x: grid * 2,
      y: canvas.height / 2 - paddleHeight / 2,
      // Ширина — одна клетка
      width: grid,
      // Высоту берём из константы
      height: paddleHeight * 2,
      // Платформа на старте никуда не движется
      dy: 0
    };
    leftPaddle.dy = paddleSpeed;
    // Описываем правую платформу
    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
    };
    // Проверка на то, пересекаются два объекта с известными координатами или нет
    // Подробнее тут: 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;
        leftPaddle.dy = paddleSpeed;
      }
      // Проверяем то же самое сверху
      else if (leftPaddle.y > LeftmaxPaddleY) {
        leftPaddle.y = LeftmaxPaddleY;
        leftPaddle.dy = -paddleSpeed;
      }
      // Если правая платформа пытается вылезти за игровое поле вниз,
      if (rightPaddle.y < grid) {
        // то оставляем её на месте
        rightPaddle.y = grid;
      }
      // Проверяем то же самое сверху
      else if (rightPaddle.y > RightmaxPaddleY) {
        rightPaddle.y = RightmaxPaddleY;
      }


      // Рисуем платформы белым цветом
      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);
      }
      // Если мяч коснулся левой платформы,
      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('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);
  </script>
</body>
</html>

Что дальше

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

Но это потом. А сейчас — обыграйте глупую платформу.

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