Непобедимый пинг-понг на JavaScript
Пинг-понг на JavaScript Пинг-понг против компьютера на JavaScript
Непобедимый пинг-понг на JavaScript
Добавляем секретный уровень в пинг-понг на 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 — это фронтенд
Если вы поня­ли, как устро­е­на эта игра, при­хо­ди­те в Прак­ти­кум, полу­чай­те новую про­фес­сию и зара­ба­ты­вай­те ещё больше.