Непобедимый пинг-понг на JavaScript
easy

Непобедимый пинг-понг на JavaScript

Попробуйте продержаться как можно дольше.

Завершаем трилогию про пинг-понг на 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>

Что будем делать сегодня:

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

Настраиваем левую платформу Сначала нам нужно отключить автостарт платформы при запуске игры. Находим строчку 53 и удаляем такое:

leftPaddle.dy = paddleSpeed;

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

// Если левая платформа пытается вылезти за игровое поле вниз,
if (leftPaddle.y < grid) {
  // то оставляем её на месте
  leftPaddle.y = grid;
  //  ВОТ ЭТО НАМ УЖЕ НЕ НАДО: leftPaddle.dy = paddleSpeed;
}
// Проверяем то же самое сверху
else if (leftPaddle.y > LeftmaxPaddleY) {
  leftPaddle.y = LeftmaxPaddleY;
  // И ВОТ ЭТО ТОЖЕ НЕ НАДО:  leftPaddle.dy = -paddleSpeed;
}

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

// Если мяч на предыдущем шаге куда-то двигался — пусть продолжает двигаться
   ball.x += ball.dx;
   ball.y += ball.dy;
// пусть платформа движется так же, как и мяч
   leftPaddle.dy = ball.dy;

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

Всё, победить уже невозможно.

Считаем очки

Принцип будет такой:

  1. Сначала у игрока 0 очков. Рекорд — тоже 0.
  2. За каждое отбивание мяча игрок получает одно очко.
  3. Если игрок побил свой старый рекорд — он тут же обновляется на текущее значение очков.
  4. Когда мяч улетел за платформу, очки обнуляются, а рекорд остаётся.
  5. Постоянно выводим на экран оба этих показателя: рекорд и количество набранных очков.

Делаем нужные переменные. Находим в самом начале раздел с переменными и добавляем нужный код после скорости мяча:

// Рекорд
var record = 0;
// Набранные очки
var count = 0;

Добавляем очки за отбивания игроком. Теперь нам нужно найти код, который проверяет касание правой платформы, и если было касание — увеличить количество набранных очков на единицу (count += 1;):

// Проверяем и делаем то же самое для правой платформы
   else if (collides(ball, rightPaddle)) {
       ball.dx *= -1;
       ball.x = rightPaddle.x - ball.width;
      count +=1;
   }

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

Всё это мы сделаем в коде, который обрабатывает вылет мяча за край платформы:

// Если мяч улетел за игровое поле влево или вправо — перезапускаем его
   if ( (ball.x < 0 || ball.x > canvas.width) && !ball.resetting) {
       // Помечаем, что мяч перезапущен, чтобы не зациклиться
       ball.resetting = true;
  // Если игрок набрал больше рекорда — записываем это как новый рекорд
            if (count > record) { record = count };
  // Обнуляем количество очков у игрока
       count = 0;
     // ДАЛЬШЕ ИДЁТ ОСТАЛЬНОЙ КОД ОБРАБОТКИ ВЫЛЕТА, ЕГО НЕ ТРОГАЕМ

Сохраняем рекорд в памяти компьютера. Когда мы делали свой планировщик задач на JavaScript, мы использовали localStorage для хранения данных в памяти браузера. Используем этот же подход, чтобы таким же образом сохранить текущее значение рекорда игрока.

Смысл в том, что мы точно так же, как и в планировщике, проверяем сначала размер хранилища. Если оно не пустое и в нём что-то есть — достаём значение рекорда оттуда. Если пустое — заводим запись и кладём туда ноль (значит, что рекорд пока никто не поставил). Добавим этот код после блока с переменными:

// Узнаём размер хранилища
var Storage_size = localStorage.length;
// Если в хранилище что-то есть…
if (Storage_size > 0){
    // Достаём оттуда текущее значение рекорда
     record = localStorage.getItem('record');
  // Если там ничего нет —
}  else
     {
         // Делаем новую запись и кладём туда ноль — рекорда пока нет
         localStorage.setItem('record',0);
     }

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

// Кладём значение рекорда в хранилище браузера
     localStorage.setItem('record',record);

Добавляем вывод значений на экран. Будем использовать стандартные свойства объекта context:

  • fillStyle — отвечает за цвет надписей (и за цвет в принципе),
  • font — каким шрифтом будем выводить надписи,
  • fillText — выводит заданный текст в определённых координатах.

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

// Цвет текста
   context.fillStyle = "#ff0000";
   // Задаём размер и шрифт
   context.font = "20pt Courier";
   // Сначала выводим рекорд
   context.fillText('Рекорд: ' + record, 150, 550);
   // Затем — набранные очки
   context.fillText(count, 450, 550);
Информативно и наглядно.

Убираем обработку нажатий

Единственное, что мы ещё не сделали — не убрали реакцию на клавиши управления левой платформой. Исправим это — найдём и удалим вот этот код:

// Если нажата клавиша W,
         if (e.which === 87) {
             // то двигаем левую платформу вверх
             leftPaddle.dy = -paddleSpeed;
         }
         // Если нажата клавиша S,
         else if (e.which === 83) {
             // то двигаем левую платформу вниз
             leftPaddle.dy = paddleSpeed;
         }

И этот:

// А если это W или S,
     if (e.which === 83 || e.which === 87) {
         // останавливаем левую платформу
         leftPaddle.dy = 0;
     }

<!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;
    // Рекорд
    var record = 0;
    // Набранные очки
    var count = 0;
    // Узнаём размер хранилища
    var Storage_size = localStorage.length;
    // Если в хранилище что-то есть…
    if (Storage_size > 0) {
      // Достаём оттуда текущее значение рекорда
      record = localStorage.getItem('record');
      // Если там ничего нет —
    } else {
      // Делаем новую запись и кладём туда ноль — рекорда пока нет
      localStorage.setItem('record', 0);
    }
    // Описываем левую платформу
    const leftPaddle = {
      // Ставим её по центру
      x: grid * 2,
      y: canvas.height / 2 - paddleHeight / 2,
      // Ширина — одна клетка
      width: grid,
      // Высоту берём из константы
      height: paddleHeight * 2,
      // Платформа на старте никуда не движется
      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 > LeftmaxPaddleY) {
        leftPaddle.y = LeftmaxPaddleY;
      }

      // Если правая платформа пытается вылезти за игровое поле вниз,
      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;
      // пусть платформа движется точно также, как и мяч
      leftPaddle.dy = 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;
        // Если игрок набрал больше рекорда — записываем это как новый рекорд
        if (count > record) { record = count };
        // Обнуляем количество очков у игрока
        count = 0;
        // Кладём значение рекорда в хранилище браузера
        localStorage.setItem('record', record);
        // Даём секунду на подготовку игрокам
        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;
        count += 1;

      }
      // Рисуем мяч
      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;
        }
      });
      // А теперь следим за тем, когда кто-то отпустит клавишу, чтобы остановить движение платформы
      document.addEventListener('keyup', function (e) {
        // Если это стрелка вверх или вниз,
        if (e.which === 38 || e.which === 40) {
          // останавливаем правую платформу
          rightPaddle.dy = 0;
        }
      });
      // Цвет текста
      context.fillStyle = "#ff0000";
      // Задаём размер и шрифт
      context.font = "20pt Courier";
      // Сначала выводим рекорд
      context.fillText('Рекорд: ' + record, 150, 550);
      // Затем — набранные очки
      context.fillText(count, 450, 550);
    }
    // Запускаем игру
    requestAnimationFrame(loop);
  </script>
</body>
</html>

Го играть

Вот итоговая страничка: заходите и играйте.

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