Bomberman на JavaScript

Вре­мя новых игр!

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

👉 Игра доволь­но объ­ём­ная по испол­не­нию, да и в нашей вер­сии это не совсем игра, а ско­рее пер­вый набро­сок. Но, забе­гая немно­го впе­рёд, мож­но уже поиг­рать в первую вер­сию. А даст бог — сде­ла­ем более слож­ную вер­сию с вра­га­ми и секретами.

Что делаем

Дела­ем клас­си­че­скую игруш­ку из ста­рых при­ста­вок — «Бом­бар­ди­ра», он же Bomberman. Пра­ви­ла и логи­ка такие:

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

Управ­ле­ние стрел­ка­ми на кла­ви­а­ту­ре. Что­бы поста­вить бом­бу, нажи­ма­ем пробел. 

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

Шаблон страницы

<!DOCTYPE html>
<html>
<head>
  <title>Бомбардир на JavaScript</title>
  <meta charset="UTF-8">
  <style>
  
  </style>
</head>
	<body>

	</body>
</html>

Настраиваем стили

Нари­су­ем зелё­ное игро­вое поле на чёр­ном фоне и поме­стим его по цен­тру стра­ни­цы. Для это­го доба­вим код в стили:

html, body {
	/* общие настройки страницы */
  height: 100%;
  margin: 0;
}
/* настройки внешнего вида страницы и положения элементов */
body {
  background: black;
  display: flex;
  align-items: center;
  justify-content: center;
}

canvas {
	/* делаем зелёный фон у холста */
  background: forestgreen;
}

Сра­зу нари­су­ем холст в основ­ном раз­де­ле стра­ни­цы <body> и доба­вим раз­дел с пустым скриптом:

<!-- рисуем холст с нужными размерами -->

<canvas width="960" height="832" id="game"></canvas>

<!-- главный скрипт игры -->

<script>

<!-- содержимое скрипта -->

</script>

Как обыч­но, всё осталь­ное мы нари­су­ем с помо­щью JavaScript 

Подготовка и переменные

Игро­вое поле будет состо­ять из яче­ек раз­ме­ром 64 на 64 пик­се­ля. Все­го таких яче­ек будет 15 в шири­ну и 13 в высо­ту. Каж­дая ячей­ка может содержать:

  • раз­ру­ша­е­мую кир­пич­ную стену
  • нераз­ру­ша­е­мую камен­ную стену
  • бом­бу
  • игро­ка
  • вооб­ще ниче­го не содер­жать, тогда по этой ячей­ке мож­но двигаться.

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

Для камен­ной сте­ны доста­точ­но одно­го боль­шо­го пря­мо­уголь­ни­ка с кон­ту­ром. Един­ствен­ное, что мы в него доба­вим — белые поло­сы, что­бы ими­ти­ро­вать объём:

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

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

Дело в том, что кро­ме игро­ка у нас появ­ля­ют­ся бом­бы и взры­вы, кото­рые дей­ству­ют и сра­ба­ты­ва­ют отдель­но от все­го. Когда игрок ста­вит бом­бу, она начи­на­ет рабо­тать авто­ном­но, и наша зада­ча — обес­пе­чить эту авто­ном­ность. Для это­го мы поме­ща­ем такие объ­ек­ты в мас­сив и посто­ян­но его про­смат­ри­ва­ем. Если объ­ект созрел для дей­ствий — выпол­ня­ем их парал­лель­но с основ­ной программой.

👉 Мас­сив с сущ­но­стя­ми — важ­ное отли­чие этой игры от осталь­ных, кото­рые мы дела­ли. Без него не полу­чи­лось бы сде­лать так, что­бы все нуж­ные объ­ек­ты в игре рабо­та­ли самостоятельно.

Теперь запи­шем это в виде JavaScript-кода:

// переменная для работы с холстом
const canvas = document.getElementById('game');
// содержимое холста
const context = canvas.getContext('2d');
// размеры одной клетки, количество строк и столбцов
const grid = 64;
const numRows = 13;
const numCols = 15;

// создаём кирпичные стены, которые потом расставим по всему полю и будем взрывать. На них будет кирпичный рисунок. Наша задача — нарисовать на стене этот рисунок.
const softWallCanvas = document.createElement('canvas');
const softWallCtx = softWallCanvas.getContext('2d');
// размер квадратика стены равен размеру клетки игрового поля
softWallCanvas.width = softWallCanvas.height = grid;

// цвет швов между кирпичами
softWallCtx.fillStyle = 'black';
// закрашиваем ими всю клетку
softWallCtx.fillRect(0, 0, grid, grid);
// цвет кирпича
softWallCtx.fillStyle = '#a9a9a9';

// первый ряд кирпичей 
softWallCtx.fillRect(1, 1, grid - 2, 20);
// второй ряд кирпичей
softWallCtx.fillRect(0, 23, 20, 18);
softWallCtx.fillRect(22, 23, 42, 18);
// третий ряд кирпичей
softWallCtx.fillRect(0, 43, 42, 20);
softWallCtx.fillRect(44, 43, 20, 20);

// теперь создадим неразрушаемые блоки — их нельзя будет уничтожить
const wallCanvas = document.createElement('canvas');
const wallCtx = wallCanvas.getContext('2d');
// тоже размером с игровую клетку
wallCanvas.width = wallCanvas.height = grid;

// цвет тени
wallCtx.fillStyle = 'black';
wallCtx.fillRect(0, 0, grid, grid);
// цвет верхнего освещения — для объёма
wallCtx.fillStyle = 'white';
wallCtx.fillRect(0, 0, grid - 2, grid - 2);
// цвет стены
wallCtx.fillStyle = '#a9a9a9';
wallCtx.fillRect(2, 2, grid - 4, grid - 4);

// сопоставляем объекты со значениями на карте
const types = {
  wall: '▉',
  softWall: 1,
  bomb: 2
};

// создаём карту игрового поля
// ▉ означает, что здесь будет неразрушаемый блок
// x означает, что здесь не могут появиться кирпичные блоки. Эти места нам нужны, чтобы в них мог появиться сам игрок и проходить к углам карты. 
let cells = [];
const template = [
  ['▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉'],
  ['▉','x','x',   ,   ,   ,   ,   ,   ,   ,   ,   ,'x','x','▉'],
  ['▉','x','▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉','x','▉'],
  ['▉','x',   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,'x','▉'],
  ['▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉'],
  ['▉',   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,'▉'],
  ['▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉'],
  ['▉',   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,'▉'],
  ['▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉'],
  ['▉','x',   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,   ,'x','▉'],
  ['▉','x','▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉',   ,'▉','x','▉'],
  ['▉','x','x',   ,   ,   ,   ,   ,   ,   ,   ,   ,'x','x','▉'],
  ['▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉']
];

// здесь будем отслеживать все игровые сущности, которые нужно будет обработать
let entities = [];

Заполняем уровень

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

  • если в шаб­лоне сто­ит ▉, то в этой ячей­ке будет камен­ный блок;
  • если в шаб­лоне ниче­го не сто­ит, то с веро­ят­но­стью 90% там будет камен­ный блок (или ячей­ка оста­нет­ся пустой в остав­ши­е­ся 10%);
  • если в шаб­лоне сто­ит кре­стик, то на этом месте ниче­го не ставим.

// заполняем уровень каменными и кирпичными блоками
function generateLevel() {
	// на старте пока уровень пустой
  cells = [];

  // cначала считаем строки
  for (let row = 0; row < numRows; row++) {
    cells[row] = [];

    // потом столбцы
    for (let col = 0; col < numCols; col++) {

    	// с вероятностью 90% в этой ячейке будет кирпичная стена
      if (!template[row][col] && Math.random() < 0.90) {
        cells[row][col] = types.softWall;
      }
      else if (template[row][col] === types.wall) {
        cells[row][col] = types.wall;
      }
    }
  }
}

Игрок

Для про­сто­ты сде­ла­ем игро­ка в виде бело­го кру­га. В сле­ду­ю­щих вер­си­ях нари­су­ем что-то дру­гое, сей­час пока будет так.

У игро­ка есть такие характеристики:

  • раз­мер,
  • коли­че­ство бомб, кото­рые он может ставить,
  • рас­сто­я­ние, на кото­рое взры­ва­ет­ся бомба.

👉 Что­бы мож­но было отри­со­вы­вать игро­ка в любой момент, мы доба­вим к харак­те­ри­сти­кам функ­цию отри­сов­ки. Полу­ча­ет­ся, что одно из свойств игро­ка будет отве­чать за то, что­бы нари­со­вать его на экране, и не при­дёт­ся писать для это­го отдель­ную функ­цию. Такое мы тоже исполь­зу­ем пер­вый раз.

// как выглядит и что умеет игрок (пока игрок — это простой белый круг)
const player = {
  row: 1,
  col: 1,
  // сколько может ставить бомб
  numBombs: 1,

  // длина взыва бомбы
  bombSize: 3,
 
  // размер игрока
  radius: grid * 0.35,
 
  // отрисовываем белый круг в нужной позиции
  render() {
    const x = (this.col + 0.5) * grid;
    const y = (this.row + 0.5) * grid;

    context.save();
    context.fillStyle = 'white';
    context.beginPath();
    context.arc(x, y, this.radius, 0, 2 * Math.PI);
    context.fill();
  }
}

Бомба

Бом­ба уже послож­нее игро­ка, и харак­те­ри­стик у неё больше:

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

Сде­ла­ем здесь то же самое, что и у игро­ка: созда­дим свойство-функцию, кото­рое отве­ча­ет за отри­сов­ку бом­бы. Что­бы сра­зу видеть, что бом­ба актив­на и ско­ро взо­рвёт­ся, сде­ла­ем её ани­ми­ро­ван­ной. Для это­го каж­дые пол­се­кун­ды с момен­та уста­нов­ки будем уве­ли­чи­вать и умень­шать её раз­ме­ры, что­бы она как буд­то пуль­си­ро­ва­ла в ячей­ке. Здесь нам пона­до­бит­ся тай­мер и отсле­жи­ва­ние вре­ме­ни, про кото­рые пого­во­рим позже.

// функция, которая отвечает за создание бомбы
function Bomb(row, col, size, owner) {
	// координаты
  this.row = row;
  this.col = col;
  // радиус бомбы
  this.radius = grid * 0.4;
  // длина взрыва
  this.size = size;
  // кто поставил бобму
  this.owner = owner; 
  // активируем её
  this.alive = true;
  // ставим признак, что в этой ячейке — бомба
  this.type = types.bomb;

  // взывается через 3 секунды
  this.timer = 3000;

  // обновляем таймер на каждом кадре анимации
  this.update = function(dt) {
    this.timer -= dt;

    // когда таймер закончится — взрываем бомбу
    if (this.timer <= 0) {
      return blowUpBomb(this);
    }

    // меняем размер бомбы каждые полсекунды
    // для этого мы каждые 500 миллисекунд увеличиваем и уменьшаем радиус
    // на нечётном шаге интервала времени мы увеличиваем бомбу, а на нечётном — уменьшаем
    const interval = Math.ceil(this.timer / 500);
    if (interval % 2 === 0) {
      this.radius = grid * 0.4;
    }
    else {
      this.radius = grid * 0.5;
    }
  };

  // отрисовка бомбы
  this.render = function() {
  	// координаты
    const x = (this.col + 0.5) * grid;
    const y = (this.row + 0.5) * grid;

    // рисуем бомбу
    context.fillStyle = 'black';
    context.beginPath();
    context.arc(x, y, this.radius, 0, 2 * Math.PI);
    context.fill();

    // рисуем фитиль, который зависит от размера бомбы
    const fuseY = (this.radius === grid * 0.5 ? grid * 0.15 : 0);
    context.strokeStyle = 'white';
    context.lineWidth = 5;
    context.beginPath();
    context.arc(
      (this.col + 0.75) * grid,
      (this.row + 0.25) * grid - fuseY,
      10, Math.PI, -Math.PI / 2
    );
    context.stroke();
  };
}

Взрыв

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

У взры­ва, как и у дру­гих эле­мен­тов в игре, есть свои характеристики:

  • дли­тель­ность,
  • коор­ди­на­ты цен­тра взрыва,
  • направ­ле­ния.

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

// задаём характеристики взрыва
function Explosion(row, col, dir, center) {
	// координаты и направление
  this.row = row;
  this.col = col;
  this.dir = dir;
  this.alive = true;

  // взыв длится 300 миллисекунд
  this.timer = 300;

  // обновляем таймер длительности взрыва на каждом шаге анимации
  this.update = function(dt) {
    this.timer -= dt;

    if (this.timer <=0) {
      this.alive = false;
    }
  };

  // отрисовка взрыва
  this.render = function() {
    const x = this.col * grid;
    const y = this.row * grid;
    const horizontal = this.dir.col;
    const vertical = this.dir.row;

    // создаём эффект огня из красных, оранжевых и жёлтых полос
    // у каждого цвета — свой размер такой полосы

    // красная
    context.fillStyle = '#D72B16'; 
    context.fillRect(x, y, grid, grid);

    // оранжевая
    context.fillStyle = '#F39642';
    // определяем, как нам рисовать линии — по горизонтали или по вертикали
    // на центре отрисовываем в обоих направлениях
    if (center || horizontal) {
      context.fillRect(x, y + 6, grid, grid - 12);
    }
    if (center || vertical) {
      context.fillRect(x + 6, y, grid - 12, grid);
    }

    // жёлтая
    context.fillStyle = '#FFE5A8';
    // точно так же выбираем направления
    if (center || horizontal) {
      context.fillRect(x, y + 12, grid, grid - 24);
    }
    if (center || vertical) {
      context.fillRect(x + 12, y, grid - 24, grid);
    }
  };
}

Сработала бомба

Мы сде­ла­ли отдель­но бом­бу и отдель­но взрыв, но ещё не про­ду­ма­ли меха­ни­ку пове­де­ния объ­ек­тов при взры­ве. Испра­вим это.

Логи­ка будет такая:

  1. Поме­ча­ем бом­бу неак­тив­ной, что­бы не взо­рвать её ещё раз на сле­ду­ю­щем кад­ре общей анимации.
  2. Уста­нав­ли­ва­ем направ­ле­ния взры­ва. Что­бы доба­вить игре непред­ска­зу­е­мо­сти, мож­но выби­рать направ­ле­ния слу­чай­ным обра­зом, но мы пока это­го делать не будем. Про­сто знай­те, что есть такая возможность.
  3. После это­го обра­ба­ты­ва­ем каж­дое направ­ле­ние по очереди.
  4. Если взрыв заде­ва­ет дру­гую бом­бу — взры­ва­ем и её.
  5. Уби­ра­ем зна­чок бом­бы с игро­во­го поля.

👉 Хит­рость в том, что в момент взры­ва мы не можем поста­вить всю игру на пау­зу и рисо­вать толь­ко пла­мя. Нам нуж­но как-то ска­зать игре, что на сле­ду­ю­щем кад­ре мож­но начи­нать раз­во­дить огонь в опре­де­лён­ных ячей­ках. Для это­го мы исполь­зу­ем отдель­ный мас­сив entities — в нём хра­нит­ся всё, что нам нуж­но обра­бо­тать даль­ше, но не пря­мо сей­час. Туда мы поме­ща­ем бом­бы и пла­мя с их ани­ма­ци­ей. Глав­ное — не забыть обра­бо­тать этот мас­сив в глав­ном цик­ле игры.

// взываем бомбу и разрушаем соседние ячейки
function blowUpBomb(bomb) {

  // если бомба уже взорвалась — выходим, чтобы не взывать её снова :)
  if (!bomb.alive) return;

  // ставим флаг, что бомба взорвалась
  bomb.alive = false;

  // убираем бомбу с ячейки
  cells[bomb.row][bomb.col] = null;

  // устанавливаем направления взыва
  const dirs = [{
    // верх
    row: -1,
    col: 0
  }, {
    // низ
    row: 1,
    col: 0
  }, {
    // слева
    row: 0,
    col: -1
  }, {
    // справа
    row: 0,
    col: 1
  }];

  // обрабатываем каждое направление
  dirs.forEach((dir) => {
    for (let i = 0; i < bomb.size; i++) {
    	// помечаем каждую такую ячейку своими цифрами
      const row = bomb.row + dir.row * i;
      const col = bomb.col + dir.col * i;
      const cell = cells[row][col];

      // останавливаем взрыв, если он достиг неразрушаемой стены
      if (cell === types.wall) {
        return;
      }

      // начало анимации взыва всегда в месте установки бомбы
      // отправляем то, что нужно взорвать, в массив с сущностями
      entities.push(new Explosion(row, col, dir, i === 0 ? true : false));
      //  очищаем взорванную ячейку
      cells[row][col] = null;

      // если бомба при взрыве задевает другую бобму — взрываем и её
      if (cell === types.bomb) {

        // отправляем следующую бомбу в массив с остальными объектами на обработку
        const nextBomb = entities.find((entity) => {
          return (
            entity.type === types.bomb &&
            entity.row === row && entity.col === col
          );
        });
        // и взываем её
        blowUpBomb(nextBomb);
      }

      // если взорвали всё доступное — останавливаемся
      if (cell) {
        return;
      }
    }
  });
}

Управление

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

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

// обрабатываем нажатия на клавиши для управления игрой
document.addEventListener('keydown', function(e) {
	//  по умолчанию клавиши управляют движением игрока по строкам и столбцам игрового поля
  let row = player.row;
  let col = player.col;

  // влево
  if (e.which === 37) {
    col--;
  }
  // вверх
  else if (e.which === 38) {
    row--;
  }
  // вправо
  else if (e.which === 39) {
    col++;
  }
  // вниз
  else if (e.which === 40) {
    row++;
  }
  // если пробел — ставим бомбу
  else if (
    e.which === 32 && !cells[row][col] &&
    // считаем количество бомб, которые ставит игрок. Если бомб хватает — ставим.
    entities.filter((entity) => {
      return entity.type === types.bomb && entity.owner === player
    }).length < player.numBombs
  ) {
    // ставим бомбу в текущую позицию
    const bomb = new Bomb(row, col, player.bombSize, player);
// отправляем бомбу в массив, чтобы игра её нарисовала на следующем кадре
    entities.push(bomb);
    cells[row][col] = types.bomb;
  }

  // двигаем игрока только в пустые ячейки 
  if (!cells[row][col]) {
    player.row = row;
    player.col = col;
  }
});

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

Зада­ча глав­но­го цик­ла — по оче­ре­ди обра­ба­ты­вать все собы­тия, кото­рые про­ис­хо­дят в игре и обнов­лять про­ис­хо­дя­щее на экране. Для это­го на каж­дом шаге мы сти­ра­ем всё, что было на поле и рису­ем зано­во, при этом не забы­вая обра­ба­ты­вать мас­сив entities — в нём хра­нит­ся всё, что нуж­но нари­со­вать и сде­лать на этом шаге.

👉 Послед­нее нов­ше­ство в игре — тай­мер и отсле­жи­ва­ние вре­ме­ни. Так как у нас ани­ма­ция зави­сит от вре­ме­ни уста­нов­ки и сра­ба­ты­ва­ния бом­бы, то нам нуж­но запо­ми­нать вре­мя, про­шед­шее с момен­та послед­ней отри­сов­ки, и смот­реть, сколь­ко мил­ли­се­кунд про­шло. Это нам помо­жет точ­но управ­лять вре­ме­нем ани­ма­ции бом­бы и взрыва.

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

// время последней отрисовки кадра
let last;
// сколько прошло времени с момента последней отрисовки
let dt;
function loop(timestamp) {
  requestAnimationFrame(loop);
  // очищаем холст
  context.clearRect(0,0,canvas.width,canvas.height);

  // считаем разницу во времени с момента последней отрисовки.
  // эти параметры нам нужны для анимации пульсации бомбы и длительности взрыва
  if (!last) {
    last = timestamp;
  }
  dt = timestamp - last;
  last = timestamp;

  // заново рисуем всё на игровом поле
  for (let row = 0; row < numRows; row++) {
    for (let col = 0; col < numCols; col++) {
      switch(cells[row][col]) {
        case types.wall:
          context.drawImage(wallCanvas, col * grid, row * grid);
          break;
        case types.softWall:
          context.drawImage(softWallCanvas, col * grid, row * grid);
          break;
      }
    }
  }

  // обновляем и отрисовываем все игровые сущности
  entities.forEach((entity) => {
    entity.update(dt);
    entity.render();
  });

  // удаляем отработанные сущности, например, взорванные бомбы 
  entities = entities.filter((entity) => entity.alive);
  // рисуем игрока
  player.render();
}

Запуск

Для запус­ка сге­не­ри­ру­ем новый уро­вень и запу­стим общую анимацию:

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

generateLevel();

requestAnimationFrame(loop);

Если вы пока не пони­ма­е­те, что вы толь­ко что про­чи­та­ли, но хочет­ся поиг­рать — поиг­рай­те.

Что дальше

Доба­вим вто­ро­го игро­ка и посмот­рим, кто побе­дит. Под­пи­ши­тесь, что­бы не пропустить 🙂

Код:
Сти­вен Ламбер

Текст:
Миха­ил Полянин

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

Худож­ник:
Даня Бер­ков­ский

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

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

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