Своя игра: создаём собственную «Змейку»
Большой разбор: как ИИ играет в змейку
Своя игра: создаём собственную «Змейку»
Собираем змейку на Arduino

Не так дав­но мы раз­би­ра­ли, как искус­ствен­ный интел­лект учит­ся играть в змей­ку. А теперь мы сами сде­ла­ем такую игру, что­бы ей мог­ли насла­дить­ся обыч­ные люди. Что нам пона­до­бит­ся:

  • HTML, что­бы мож­но было играть пря­мо в бра­у­зе­ре;
  • CSS для укра­ше­ний;
  • JavaScript для самой игры.

Логика игры

У клас­си­че­ской змей­ки пра­ви­ла про­стые:

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

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

После­до­ва­тель­ность наших дей­ствий будет такой:

  1. Дела­ем пустую HTML-страницу.
  2. Настра­и­ва­ем внеш­ний вид с помо­щью CSS.
  3. Рису­ем игро­вое поле.
  4. Пишем скрипт, кото­рый и будет отве­чать за всю игру.

Делаем HTML-страницу

С этим всё про­сто: берём стан­дарт­ный код и сохра­ня­ем его как файл snake.html.

    
language: HTML
<!DOCTYPE html>

<html>

<head>

  <title>Змейка</title>

 

  <style>

   </style>

 

</head>

<body>

  <!-- Содержимое страницы -->

</body>

</html>


Ско­пи­ро­вать код
Код ско­пи­ро­ван

Это даст нам пустую стра­ни­цу, кото­рую мы сей­час немно­го настро­им сти­ля­ми.

Настраиваем внешний вид

За внеш­ний вид на стра­ни­це у нас отве­ча­ет раз­дел <style>, поэто­му мы про­сто доба­вим в него CSS-код:

    
language: CSS
html, body {

  height: 100%;

  margin: 0;

}

 

/*Задаём глобальные параметры*/

body {

  background: black;

  display: flex;

  align-items: center;

  justify-content: center;

}

 

/*Делаем границу вокруг игрового поля*/

canvas {

  border: 1px solid white;

}


Ско­пи­ро­вать код
Код ско­пи­ро­ван

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

Рисуем игровое поле

Поле дела­ет­ся очень про­сто:

<canvas id="game" width="400" height="400"></canvas>

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

Пишем скрипт

1. Зада­дим все пере­мен­ные, кото­рые нам пона­до­бят­ся.

    
language: JavaScript
// Поле, на котором всё будет происходить, — тоже как бы переменная

var canvas = document.getElementById('game');

 

// Классическая змейка — двухмерная, сделаем такую же

var context = canvas.getContext('2d');

 

// Размер одной клеточки на поле — 16 пикселей

var grid = 16;

 

// Служебная переменная, которая отвечает за скорость змейки

var count = 0;

 

// А вот и сама змейка

var snake = {

 

  // Начальные координаты

  x: 160,

  y: 160,

  

  // Скорость змейки — в каждом новом кадре змейка смещается по оси Х или У. На старте будет двигаться горизонтально, поэтому скорость по игреку равна нулю.

  dx: grid,

  dy: 0,

  

  // Тащим за собой хвост, который пока пустой

  cells: [],

  

  // Стартовая длина змейки — 4 клеточки

  maxCells: 4

};

 

// А это — еда. Представим, что это красные яблоки.

var apple = {

 

  // Начальные координаты яблока

  x: 320,

  y: 320

};


Ско­пи­ро­вать код
Код ско­пи­ро­ван

2. Сде­ла­ем гене­ра­тор слу­чай­ных чисел. Он нам пона­до­бит­ся, что­бы раз­ме­щать еду на поле слу­чай­ным обра­зом.

// Делаем генератор случайных чисел в заданном диапазоне
function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}

3. Напи­шем основ­ной игро­вой цикл, кото­рый будет рабо­тать бес­ко­неч­но.

    
language: JavaScript
// Игровой цикл — основной процесс, внутри которого будет всё происходить

function loop() {

 

  // Дальше будет хитрая функция, которая замедляет скорость игры с 60 кадров в секунду до 15. Для этого она пропускает три кадра из четырёх, то есть срабатывает каждый четвёртый кадр игры. Было 60 кадров в секунду, станет 15.

  requestAnimationFrame(loop);

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

  if (++count < 4) {

    return;

  }

 

  // Обнуляем переменную скорости

  count = 0;

 

  // Очищаем игровое поле

  context.clearRect(0,0,canvas.width,canvas.height);

  // Двигаем змейку с нужной скоростью

  snake.x += snake.dx;

  snake.y += snake.dy;

 

  // Если змейка достигла края поля по горизонтали — продолжаем её движение с противоположной стороны

  if (snake.x < 0) {

    snake.x = canvas.width - grid;

  }

  else if (snake.x >= canvas.width) {

    snake.x = 0;

  }

  

  // Делаем то же самое для движения по вертикали

  if (snake.y < 0) {

    snake.y = canvas.height - grid;

  }

  else if (snake.y >= canvas.height) {

    snake.y = 0;

  }

 

  // Продолжаем двигаться в выбранном направлении. Голова всегда впереди, поэтому добавляем её координаты в начало массива, который отвечает за всю змейку.

  snake.cells.unshift({x: snake.x, y: snake.y});

 

  // Сразу после этого удаляем последний элемент из массива змейки, потому что она движется и постоянно особождает клетки после себя

  if (snake.cells.length > snake.maxCells) {

    snake.cells.pop();

  }

 

  // Рисуем еду — красное яблоко

  context.fillStyle = 'red';

  context.fillRect(apple.x, apple.y, grid-1, grid-1);

 

  // Одно движение змейки — один новый нарисованный квадратик 

  context.fillStyle = 'green';

 

  // Обрабатываем каждый элемент змейки

  snake.cells.forEach(function(cell, index) {

    // Чтобы создать эффект клеточек, делаем зелёные квадратики меньше на один пиксель, чтобы вокруг них образовалась чёрная граница

    context.fillRect(cell.x, cell.y, grid-1, grid-1);  

   

    // Если змейка добралась до яблока...

    if (cell.x === apple.x && cell.y === apple.y) {

 

      // увеличиваем длину змейки

      snake.maxCells++;

      // Рисуем новое яблочко

      // Помним, что размер холста у нас 400x400, при этом он разбит на ячейки — 25 в каждую сторону

      apple.x = getRandomInt(0, 25) * grid;

      apple.y = getRandomInt(0, 25) * grid;

    }

 

    // Проверяем, не столкнулась ли змея сама с собой

    // Для этого перебираем весь массив и смотрим, есть ли у нас в массиве змейки две клетки с одинаковыми координатами 

    for (var i = index + 1; i < snake.cells.length; i++) {

      

      // Если такие клетки есть — начинаем игру заново

      if (cell.x === snake.cells[i].x && cell.y === snake.cells[i].y) {

        // Задаём стартовые параметры основным переменным

        snake.x = 160;

        snake.y = 160;

        snake.cells = [];

        snake.maxCells = 4;

        snake.dx = grid;

        snake.dy = 0;

        // Ставим яблочко в случайное место

        apple.x = getRandomInt(0, 25) * grid;

        apple.y = getRandomInt(0, 25) * grid;

      }

    }

  });

}


Ско­пи­ро­вать код
Код ско­пи­ро­ван

4. Сде­ла­ем управ­ле­ние стре­лоч­ка­ми на кла­ви­а­ту­ре.

    
language: JavaScript
// Смотрим, какие нажимаются клавиши, и реагируем на них нужным образом

document.addEventListener('keydown', function(e) {

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

  

  // Стрелка влево

  // Если нажата стрелка влево, и при этом змейка никуда не движется по горизонтали…

  if (e.which === 37 && snake.dx === 0) {

    // то даём ей движение по горизонтали, влево, а вертикальное — останавливаем

    // Та же самая логика будет и в остальных кнопках

    snake.dx = -grid;

    snake.dy = 0;

  }

  // Стрелка вверх

  else if (e.which === 38 && snake.dy === 0) {

    snake.dy = -grid;

    snake.dx = 0;

  }

  // Стрелка вправо

  else if (e.which === 39 && snake.dx === 0) {

    snake.dx = grid;

    snake.dy = 0;

  }

  // Стрелка вниз

  else if (e.which === 40 && snake.dy === 0) {

    snake.dy = grid;

    snake.dx = 0;

  }

});


Ско­пи­ро­вать код
Код ско­пи­ро­ван

5. Запус­ка­ем игру. Для это­го доста­точ­но запу­стить преды­ду­щий бес­ко­неч­ный цикл, поэто­му пишем:

requestAnimationFrame(loop);

6. Насла­жда­ем­ся резуль­та­том:


Что­бы у вас тоже полу­чи­лось такое, про­сто ско­пи­руй­те гото­вый код, сохра­ни­те его как HTML-файл и открой­те в бра­у­зе­ре.

Готовый код

    
language: HTML
<!DOCTYPE html>

<html>

<head>

  <title>Змейка</title>

 

  <style>

 

    html, body {

      height: 100%;

      margin: 0;

    }

 

    /*Задаём глобальные параметры*/

    body {

      background: black;

      display: flex;

      align-items: center;

      justify-content: center;

    }

 

    /*Делаем границу вокруг игрового поля*/

    canvas {

      border: 1px solid white;

    }

  </style>

 

</head>

<body>

  <!-- Рисуем игровое поле -->

  <canvas width="400" height="400" id="game"></canvas>

 

  <!-- Сам скрипт с игрой -->

  <script>

 

    // Поле, на котором всё будет происходить, — тоже как бы переменная

    var canvas = document.getElementById('game');

 

    // Классическая змейка — двухмерная, сделаем такую же

    var context = canvas.getContext('2d');

 

    // Размер одной клеточки на поле — 16 пикселей

    var grid = 16;

 

    // Служебная переменная, которая отвечает за скорость змейки

    var count = 0;

    

    // А вот и сама змейка

    var snake = {

 

      // Начальные координаты

      x: 160,

      y: 160,

      

      // Скорость змейки — в каждом новом кадре змейка смещается по оси Х или У. На старте будет двигаться горизонтально, поэтому скорость по игреку равна нулю.

      dx: grid,

      dy: 0,

      

      // Тащим за собой хвост, который пока пустой

      cells: [],

      

      // Стартовая длина змейки — 4 клеточки

      maxCells: 4

    };

 

    // А это — еда. Представим, что это красные яблоки.

    var apple = {

 

      // Начальные координаты яблока

      x: 320,

      y: 320

    };

 

// Делаем генератор случайных чисел в заданном диапазоне

function getRandomInt(min, max) {

  return Math.floor(Math.random() * (max - min)) + min;

}

 

    // Игровой цикл — основной процесс, внутри которого будет всё происходить

    function loop() {

 

      // Хитрая функция, которая замедляет скорость игры с 60 кадров в секунду до 15 (60/15 = 4)

      requestAnimationFrame(loop);

      // Игровой код выполнится только один раз из четырёх, в этом и суть замедления кадров, а пока переменная count меньше четырёх, код выполняться не будет

      if (++count < 4) {

        return;

      }

 

      // Обнуляем переменную скорости

      count = 0;

 

      // Очищаем игровое поле

      context.clearRect(0,0,canvas.width,canvas.height);

      // Двигаем змейку с нужной скоростью

      snake.x += snake.dx;

      snake.y += snake.dy;

 

      // Если змейка достигла края поля по горизонтали — продолжаем её движение с противоположной строны

      if (snake.x < 0) {

        snake.x = canvas.width - grid;

      }

      else if (snake.x >= canvas.width) {

        snake.x = 0;

      }

      

      // Делаем то же самое для движения по вертикали

      if (snake.y < 0) {

        snake.y = canvas.height - grid;

      }

      else if (snake.y >= canvas.height) {

        snake.y = 0;

      }

 

      // Продолжаем двигаться в выбранном направлении. Голова всегда впереди, поэтому добавляем её координаты в начало массива, который отвечает за всю змейку

      snake.cells.unshift({x: snake.x, y: snake.y});

 

      // Сразу после этого удаляем последний элемент из массива змейки, потому что она движется и постоянно освобождает клетки после себя

      if (snake.cells.length > snake.maxCells) {

        snake.cells.pop();

      }

 

      // Рисуем еду — красное яблоко

      context.fillStyle = 'red';

      context.fillRect(apple.x, apple.y, grid-1, grid-1);

 

      // Одно движение змейки — один новый нарисованный квадратик 

      context.fillStyle = 'green';

 

      // Обрабатываем каждый элемент змейки

      snake.cells.forEach(function(cell, index) {

        // Чтобы создать эффект клеточек, делаем зелёные квадратики меньше на один пиксель, чтобы вокруг них образовалась чёрная граница

        context.fillRect(cell.x, cell.y, grid-1, grid-1);  

       

        // Если змейка добралась до яблока...

        if (cell.x === apple.x && cell.y === apple.y) {

 

          // увеличиваем длину змейки

          snake.maxCells++;

          // Рисуем новое яблочко

          // Помним, что размер холста у нас 400x400, при этом он разбит на ячейки — 25 в каждую сторону

          apple.x = getRandomInt(0, 25) * grid;

          apple.y = getRandomInt(0, 25) * grid;

        }

 

        // Проверяем, не столкнулась ли змея сама с собой

        // Для этого перебираем весь массив и смотрим, есть ли у нас в массиве змейки две клетки с одинаковыми координатами 

        for (var i = index + 1; i < snake.cells.length; i++) {

          

          // Если такие клетки есть — начинаем игру заново

          if (cell.x === snake.cells[i].x && cell.y === snake.cells[i].y) {

            // Задаём стартовые параметры основным переменным

            snake.x = 160;

            snake.y = 160;

            snake.cells = [];

            snake.maxCells = 4;

            snake.dx = grid;

            snake.dy = 0;

            // Ставим яблочко в случайное место

            apple.x = getRandomInt(0, 25) * grid;

            apple.y = getRandomInt(0, 25) * grid;

          }

        }

      });

    }

 

    // Смотрим, какие нажимаются клавиши, и реагируем на них нужным образом

    document.addEventListener('keydown', function(e) {

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

      

      // Стрелка влево

      // Если нажата стрелка влево, и при этом змейка никуда не движется по горизонтали…

      if (e.which === 37 && snake.dx === 0) {

        // то даём ей движение по горизонтали, влево, а вертикальное — останавливаем

        // Та же самая логика будет и в остальных кнопках

        snake.dx = -grid;

        snake.dy = 0;

      }

      // Стрелка вверх

      else if (e.which === 38 && snake.dy === 0) {

        snake.dy = -grid;

        snake.dx = 0;

      }

      // Стрелка вправо

      else if (e.which === 39 && snake.dx === 0) {

        snake.dx = grid;

        snake.dy = 0;

      }

      // Стрелка вниз

      else if (e.which === 40 && snake.dy === 0) {

        snake.dy = grid;

        snake.dx = 0;

      }

    });

 

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

    requestAnimationFrame(loop);

  </script>

 

</body>

</html>


Ско­пи­ро­вать код
Код ско­пи­ро­ван

Как улучшить

Этот код — самая про­стая реа­ли­за­ция змей­ки, и игру мож­но сде­лать ещё луч­ше:

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

Про­го­ло­суй­те за тот вари­ант, кото­рый вам боль­ше все­го нра­вит­ся, в ком­мен­та­ри­ях, или сде­лай­те свою змей­ку, где всё это будет одно­вре­мен­но.

Как стать веб-разработчиком
Если вам инте­рес­но писать код и сра­зу видеть резуль­тат сво­ей рабо­ты — загля­ни­те в «Прак­ти­кум». Там есть класс­ные тре­на­жё­ры, инте­рес­ные про­ек­ты и мно­го хоро­ше­го кода!