Цветной арканоид на JavaScript

Арка­но­ид — клас­си­че­ская ком­пью­тер­ная игра. Сего­дня мы её вос­со­зда­дим.

Суть игры такая:

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

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

Логика игры

Если вы виде­ли наш про­ект с пинг-понгом, то мно­гое вам будет уже зна­ко­мо. Напри­мер, меха­ни­ка про­вер­ки на каса­ние объ­ек­тов, покад­ро­вая отри­сов­ка игры и отскок шари­ка со сме­ной направ­ле­ния — всё это мы уже дела­ли в той игре. 

Что­бы было понят­но, в какой после­до­ва­тель­но­сти мы пишем код, вот общая струк­ту­ра:

  1. Гото­вим стра­ни­цу и рису­ем на ней игро­вое поле. Всё осталь­ное будет про­ис­хо­дить в скрип­те.
  2. Заво­дим пере­мен­ные, кото­рые будут отве­чать за напол­не­ние уров­ня, цвет и раз­мер всех игро­вых эле­мен­тов.
  3. Сра­зу поме­ща­ем всё, что отно­сит­ся к кир­пи­чам, в одну боль­шую пере­мен­ную, с кото­рой будем рабо­тать всю игру.
  4. Добав­ля­ем функ­цию про­вер­ки на каса­ние объ­ек­тов — вся игра будет стро­ить­ся на ней.
  5. Дела­ем глав­ный цикл игры, в кото­ром по оче­ре­ди сдви­га­ем все объ­ек­ты, кото­рые дви­жут­ся, и уби­ра­ем те кир­пи­чи, кото­рые нуж­но убрать. Каж­дый кадр всё будет отри­со­вы­вать­ся зано­во, что­бы был эффект непре­рыв­но­го дви­же­ния.

Готовим страницу

Един­ствен­ное, что нам здесь пона­до­бит­ся, — сде­лать сти­ля­ми чёр­ный фон, что­бы цвет­ные кир­пи­чи смот­ре­лись на нём эффект­нее. Всё осталь­ное берём из шаб­ло­на пустой стра­ни­цы. Пока что на экране будет чер­но­та, рисо­вать будем даль­ше.

<!DOCTYPE html>
<html>

<head>
  <title>Арканоид</title>
  <style>
    /* стили для всей страницы */
    html, body {
      height: 100%;
      margin: 0;
    }
    /* отдельные параметры для фона */
    body {
      background: black;
      display: flex;
      align-items: center;
      justify-content: center;
    }
  </style>
</head>

<body>
  <!-- рисуем игровое поле -->
  <canvas width="400" height="500" id="game"></canvas>
  <!-- сам скрипт с игрой -->
  <script>
  </script>
</body>
</html>

Переменные

Для игры нам пона­до­бят­ся две основ­ные пере­мен­ные (поми­мо вся­ких вспо­мо­га­тель­ных).

Пер­вая пере­мен­ная отве­ча­ет за дизайн уров­ня — это дву­мер­ный мас­сив, где каж­дая стро­ка отве­ча­ет за свой ряд кир­пи­чей. Сами кир­пи­чи сра­зу обо­зна­ча­ют­ся бук­ва­ми, кото­рые отве­ча­ют за их цвет. Если кир­пи­чей нет, то ряд пустой и там ниче­го рисо­вать­ся не будет. Меня­ем эту пере­мен­ную — меня­ет­ся и внеш­ний вид уров­ня. В буду­щем этой пере­мен­ной может быть мно­го дизай­нов уров­ней.

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

// переменная для работы с холстом, на котором будет нарисована игра
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');

// каждый ряд состоит из 14 кирпичей. На уровне будут 6 пустых рядов, а затем 8 рядов с кирпичами
// цвета кирпичей: красный, оранжевый, зелёный и жёлтый
// буква в массиве означает цвет кирпича
const level1 = [
  [],
  [],
  [],
  [],
  [],
  [],
  ['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
  ['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
  ['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
  ['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
  ['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
  ['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
  ['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'],
  ['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y']
];

// сопоставляем буквы (R, O, G, Y) с цветами
const colorMap = {
  'R': 'red',
  'O': 'orange',
  'G': 'green',
  'Y': 'yellow'
};

// делаем зазор в 2 пикселя между кирпичами, чтобы отделить их друг от друга
const brickGap = 2;
// размеры каждого кирпича
const brickWidth = 25;
const brickHeight = 12;

// ширина стены должна занимать оставшееся место на холсте с каждой стороны
// у нас 14 кирпичей по 25 пикселей и 13 промежутков по 2 пикселя, а общая ширина холста — 400 пикселей
// получаем общую ширину стен: 400 − (14 × 25 + 2 × 13) = 24px. 
// разделим пополам, чтобы получить ширину каждой стены, и получим 12px
const wallSize = 12;

// платформа, которой управляет игрок
const paddle = {
  // ставим её внизу по центру поля
  x: canvas.width / 2 - brickWidth / 2,
  y: 440,
  // делаем её размером с кирпич
  width: brickWidth,
  height: brickHeight,

  // пока платформа никуда не движется, поэтому направление движения равно нулю
  dx: 0
};

// шарик, который отскакивает от платформы и уничтожает кирпичи
const ball = {
  // стартовые координаты
  x: 130,
  y: 260,
  // высота и ширина (для простоты это будет квадратный шарик)
  width: 5,
  height: 5,

  // скорость шарика по обеим координатам
  speed: 2,

  // на старте шарик пока никуда не смещается
  dx: 0,
  dy: 0
};

// основной массив для игры
const bricks = [];

// создадим уровень так: обработаем весь массив level1
// и те места, которые обозначены каким-либо цветом, поместим в игровой массив.
// там же будем хранить координаты начала каждого кирпича и его цвет

// пока у нас есть необработанные элементы в массиве с уровнем — обрабатываем их
for (let row = 0; row < level1.length; row++) {
  for (let col = 0; col < level1[row].length; col++) {

    // находим цвет кирпича
    const colorCode = level1[row][col];

    // создаём новый элемент игрового массива — с координатами кирпича, цветом, шириной и высотой кирпича
    bricks.push({
      x: wallSize + (brickWidth + brickGap) * col,
      y: wallSize + (brickHeight + brickGap) * row,
      color: colorMap[colorCode],
      width: brickWidth,
      height: brickHeight
    });
  }
}

Проверка на касание объектов

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

// проверка на пересечение объектов
// взяли отсюда: 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;
}

Отслеживаем нажатия на клавиши

Что­бы игрок мог управ­лять дви­же­ни­ем плат­фор­мы, доба­вим обра­бот­чик нажа­тий на кла­ви­ши. 

Стрел­ки вле­во и впра­во дви­га­ют плат­фор­му в сто­ро­ны, а про­бел запус­ка­ет шарик, если он не дви­жет­ся. Как толь­ко игрок отпус­ка­ет стрел­ки, плат­фор­ма пере­ста­ёт дви­гать­ся.

// отслеживаем нажатия игрока на клавиши
document.addEventListener('keydown', function(e) {
  // стрелка влево
  if (e.which === 37) {
    paddle.dx = -3;
  }
  // стрелка вправо
  else if (e.which === 39) {
    paddle.dx = 3;
  }

  // обрабатываем нажатие на пробел
  // если шарик не запущен — запускаем его из начальной точки, сверху вниз
  if (ball.dx === 0 && ball.dy === 0 && e.which === 32) {
    ball.dx = ball.speed;
    ball.dy = ball.speed;
  }
});

// как только игрок перестал нажимать клавиши со стрелками — останавливаем платформу
document.addEventListener('keyup', function(e) {
  if (e.which === 37 || e.which === 39) {
    paddle.dx = 0;
  }
});

Главный цикл игры

Вся игра — это один боль­шой цикл, где каж­дое новое поло­же­ние эле­мен­тов отри­со­вы­ва­ет­ся в новом кад­ре. После­до­ва­тель­ность дей­ствий будет такая:

  1. Очи­ща­ем игро­вое поле
  2. Сдви­га­ем плат­фор­му, если было нажа­тие на стрел­ки, и сле­дим, что­бы она не уле­те­ла за гра­ни­цы стен.
  3. Дви­га­ем шарик со сво­ей ско­ро­стью и направ­ле­ни­ем и смот­рим, что­бы он отска­ки­вал от стен и верх­ней гра­ни­цы.
  4. Если шарик упа­дёт вниз мимо платформы-ракетки — поме­ща­ем его на стар­то­вую пози­цию и оста­нав­ли­ва­ем, пока не игрок не нажмёт про­бел.
  5. Если шарик кос­нул­ся плат­фор­мы — дела­ем отскок в про­ти­во­по­лож­ную сто­ро­ну.
  6. Когда шарик кос­нёт­ся кир­пи­ча — тоже дела­ем отскок и сра­зу уби­ра­ем этот кир­пич.
  7. И толь­ко в самом кон­це мы отри­со­вы­ва­ем все игро­вые эле­мен­ты на сво­их новых пози­ци­ях.

// главный цикл игры
function loop() {
  // на каждом кадре — очищаем поле и рисуем всё заново
  requestAnimationFrame(loop);
  context.clearRect(0,0,canvas.width,canvas.height);

  // двигаем платформу с нужной скоростью 
  paddle.x += paddle.dx;

  // при этом смотрим, чтобы она не уехала за стены
  if (paddle.x < wallSize) {
    paddle.x = wallSize
  }
  else if (paddle.x + brickWidth > canvas.width - wallSize) {
    paddle.x = canvas.width - wallSize - brickWidth;
  }

  // шарик тоже двигается со своей скоростью
  ball.x += ball.dx;
  ball.y += ball.dy;

  // и его тоже нужно постоянно проверять, чтобы он не улетел за границы стен
  // смотрим левую и правую стенки
  if (ball.x < wallSize) {
    ball.x = wallSize;
    ball.dx *= -1;
  }
  else if (ball.x + ball.width > canvas.width - wallSize) {
    ball.x = canvas.width - wallSize - ball.width;
    ball.dx *= -1;
  }
  // проверяем верхнюю границу
  if (ball.y < wallSize) {
    ball.y = wallSize;
    ball.dy *= -1;
  }

  // перезагружаем шарик, если он улетел вниз, за край игрового поля
  if (ball.y > canvas.height) {
    ball.x = 130;
    ball.y = 260;
    ball.dx = 0;
    ball.dy = 0;
  }

  // проверяем, коснулся ли шарик платформы, которой управляет игрок. Если коснулся — меняем направление движения шарика по оси Y на противоположное
  if (collides(ball, paddle)) {
    ball.dy *= -1;

    // сдвигаем шарик выше платформы, чтобы на следующем кадре это снова не засчиталось за столкновение
    ball.y = paddle.y - ball.height;
  }

  // проверяем, коснулся ли шарик цветного кирпича 
  // если коснулся — меняем направление движения шарика в зависимости от стенки касания
  // для этого в цикле проверяем каждый кирпич на касание
  for (let i = 0; i < bricks.length; i++) {
    // берём очередной кирпич
    const brick = bricks[i];

    // если было касание
    if (collides(ball, brick)) {
      // убираем кирпич из массива
      bricks.splice(i, 1);

      // если шарик коснулся кирпича сверху или снизу — меняем направление движения шарика по оси Y
      if (ball.y + ball.height - ball.speed <= brick.y ||
          ball.y >= brick.y + brick.height - ball.speed) {
        ball.dy *= -1;
      }
      // в противном случае меняем направление движения шарика по оси X
      else {
        ball.dx *= -1;
      }
      // как нашли касание — сразу выходим из цикла проверки
      break;
    }
  }

  // рисуем стены
  context.fillStyle = 'lightgrey';
  context.fillRect(0, 0, canvas.width, wallSize);
  context.fillRect(0, 0, wallSize, canvas.height);
  context.fillRect(canvas.width - wallSize, 0, wallSize, canvas.height);

  // если шарик в движении — рисуем его
  if (ball.dx || ball.dy) {
    context.fillRect(ball.x, ball.y, ball.width, ball.height);
  }

  // рисуем кирпичи
  bricks.forEach(function(brick) {
    context.fillStyle = brick.color;
    context.fillRect(brick.x, brick.y, brick.width, brick.height);
  });

  // рисуем платформу
  context.fillStyle = 'cyan';
  context.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
}

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

Для запус­ка доста­точ­но одной коман­ды в самом кон­це скрип­та:

 requestAnimationFrame(loop);

Вот теперь мы видим всю кра­со­ту:

Мож­но посмот­реть, как это рабо­та­ет на стра­ни­це про­ек­та.

Готовый код игры

<!-- Лицензия CC0 1.0 Universal-->
<!-- оригинал — https://gist.github.com/straker/98a2aed6a7686d26c04810f08bfaf66b -->
<!DOCTYPE html>
<html>

<head>
  <title>Арканоид</title>
  <style>
    /* стили для всей страницы */
    html, body {
      height: 100%;
      margin: 0;
    }
    /* отдельные параметры для фона */
    body {
      background: black;
      display: flex;
      align-items: center;
      justify-content: center;
    }
  </style>
</head>

<body>
  <!-- рисуем игровое поле -->
  <canvas width="400" height="500" id="game"></canvas>
  <!-- сам скрипт с игрой -->
  <script>
  // переменная для работы с холстом, на котором будет нарисована игра
  const canvas = document.getElementById('game');
  const context = canvas.getContext('2d');

  // каждый ряд состоит из 14 кирпичей. На уровне будут 6 пустых рядов, а затем 8 рядов с кирпичами
  // цвета кирпичей: красный, оранжевый, зелёный и жёлтый
  // буква в массиве означает цвет кирпича
  const level1 = [
    [],
    [],
    [],
    [],
    [],
    [],
    ['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
    ['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
    ['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
    ['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
    ['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
    ['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
    ['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'],
    ['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y']
  ];

  // сопоставляем буквы (R, O, G, Y) с цветами
  const colorMap = {
    'R': 'red',
    'O': 'orange',
    'G': 'green',
    'Y': 'yellow'
  };

  // делаем зазор в 2 пикселя между кирпичами, чтобы отделить их друг от друга
  const brickGap = 2;
  // размеры каждого кирпича
  const brickWidth = 25;
  const brickHeight = 12;

  // ширина стены должна занимать оставшееся место на холсте с каждой стороны
  // у нас 14 кирпичей по 25 пикселей и 13 промежутков по 2 пикселя, а общая ширина холста — 400 пикселей
  // получаем общую ширину стен: 400 − (14 × 25 + 2 × 13) = 24px. 
  // разделим пополам, чтобы получить ширину каждой стены, и получим 12px
  const wallSize = 12;
  // основной массив для игры
  const bricks = [];

  // создадим уровень так: обработаем весь массив level1
  // и те места, которые обозначены каким-либо цветом, поместим в игровой массив.
  // там же будем хранить координаты начала каждого кирпича и его цвет

  // пока у нас есть необработанные элементы в массиве с уровнем — обрабатываем их
  for (let row = 0; row < level1.length; row++) {
    for (let col = 0; col < level1[row].length; col++) {

      // находим цвет кирпича
      const colorCode = level1[row][col];

      // создаём новый элемент игрового массива — с координатами кирпича, цветом, шириной и высотой кирпича
      bricks.push({
        x: wallSize + (brickWidth + brickGap) * col,
        y: wallSize + (brickHeight + brickGap) * row,
        color: colorMap[colorCode],
        width: brickWidth,
        height: brickHeight
      });
    }
  }

  // платформа, которой управляет игрок
  const paddle = {
    // ставим её внизу по центру поля
    x: canvas.width / 2 - brickWidth / 2,
    y: 440,
    // делаем её размером с кирпич
    width: brickWidth,
    height: brickHeight,

    // пока платформа никуда не движется, поэтому направление движения равно нулю
    dx: 0
  };

  // шарик, который отскакивает от платформы и уничтожает кирпичи
  const ball = {
    // стартовые координаты
    x: 130,
    y: 260,
    // высота и ширина (для простоты это будет квадратный шарик)
    width: 5,
    height: 5,

    // скорость шарика по обеим координатам
    speed: 2,

    // на старте шарик пока никуда не смещается
    dx: 0,
    dy: 0
  };

// проверка на пересечение объектов
// взяли отсюда: 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);

    // двигаем платформу с нужной скоростью 
    paddle.x += paddle.dx;

    // при этом смотрим, чтобы она не уехала за стены
    if (paddle.x < wallSize) {
      paddle.x = wallSize
    }
    else if (paddle.x + brickWidth > canvas.width - wallSize) {
      paddle.x = canvas.width - wallSize - brickWidth;
    }

    // шарик тоже двигается со своей скоростью
    ball.x += ball.dx;
    ball.y += ball.dy;

    // и его тоже нужно постоянно проверять, чтобы он не улетел за границы стен
    // смотрим левую и правую стенки
    if (ball.x < wallSize) {
      ball.x = wallSize;
      ball.dx *= -1;
    }
    else if (ball.x + ball.width > canvas.width - wallSize) {
      ball.x = canvas.width - wallSize - ball.width;
      ball.dx *= -1;
    }
    // проверяем верхнюю границу
    if (ball.y < wallSize) {
      ball.y = wallSize;
      ball.dy *= -1;
    }

    // перезагружаем шарик, если он улетел вниз, за край игрового поля
    if (ball.y > canvas.height) {
      ball.x = 130;
      ball.y = 260;
      ball.dx = 0;
      ball.dy = 0;
    }

    // проверяем, коснулся ли шарик платформы, которой управляет игрок. Если коснулся — меняем направление движения шарика по оси Y на противоположное
    if (collides(ball, paddle)) {
      ball.dy *= -1;

      // сдвигаем шарик выше платформы, чтобы на следующем кадре это снова не засчиталось за столкновение
      ball.y = paddle.y - ball.height;
    }

    // проверяем, коснулся ли шарик цветного кирпича 
    // если коснулся — меняем направление движения шарика в зависимости от стенки касания
    // для этого в цикле проверяем каждый кирпич на касание
    for (let i = 0; i < bricks.length; i++) {
      // берём очередной кирпич
      const brick = bricks[i];

      // если было касание
      if (collides(ball, brick)) {
        // убираем кирпич из массива
        bricks.splice(i, 1);

        // если шарик коснулся кирпича сверху или снизу — меняем направление движения шарика по оси Y
        if (ball.y + ball.height - ball.speed <= brick.y ||
            ball.y >= brick.y + brick.height - ball.speed) {
          ball.dy *= -1;
        }
        // в противном случае меняем направление движения шарика по оси X
        else {
          ball.dx *= -1;
        }
        // как нашли касание — сразу выходим из цикла проверки
        break;
      }
    }

    // рисуем стены
    context.fillStyle = 'lightgrey';
    context.fillRect(0, 0, canvas.width, wallSize);
    context.fillRect(0, 0, wallSize, canvas.height);
    context.fillRect(canvas.width - wallSize, 0, wallSize, canvas.height);

    // если шарик в движении — рисуем его
    if (ball.dx || ball.dy) {
      context.fillRect(ball.x, ball.y, ball.width, ball.height);
    }

    // рисуем кирпичи
    bricks.forEach(function(brick) {
      context.fillStyle = brick.color;
      context.fillRect(brick.x, brick.y, brick.width, brick.height);
    });

    // рисуем платформу
    context.fillStyle = 'cyan';
    context.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
  }

  // отслеживаем нажатия игрока на клавиши
  document.addEventListener('keydown', function(e) {
    // стрелка влево
    if (e.which === 37) {
      paddle.dx = -3;
    }
    // стрелка вправо
    else if (e.which === 39) {
      paddle.dx = 3;
    }

    // обрабатываем нажатие на пробел
    // если шарик не запущен — запускаем его из начальной точки, сверху вниз
    if (ball.dx === 0 && ball.dy === 0 && e.which === 32) {
      ball.dx = ball.speed;
      ball.dy = ball.speed;
    }
  });

  // как только игрок перестал нажимать клавиши со стрелками — останавливаем платформу
  document.addEventListener('keyup', function(e) {
    if (e.which === 37 || e.which === 39) {
      paddle.dx = 0;
    }
  });

  // запускаем игру
  requestAnimationFrame(loop);
  </script>
</body>
</html>

Что дальше

Сей­час у этой игры мно­го про­блем:

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

Что-то из это­го испра­вим в сле­ду­ю­щих вер­си­ях.

Текст и скрин­шо­ты:
Миша Поля­нин

Редак­тор:
Мак­сим Илья­хов

Кор­рек­тор:
Ира Михе­е­ва

Иллю­стра­тор:
Даня Бер­ков­ский

Вёрст­ка:
Маша Дро­но­ва

Достав­ка:
Олег Веш­кур­цев