Играем в лабиринт
easy

Играем в лабиринт

Наконец-то

Мы с вами прошли по лабиринту уже большой путь, смотрите сами:

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

Что делаем

Сегодня у нас финал:

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

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

За основу возьмём код третьей версии проекта, где мы добавляли выход и оптимизацию.

Добавляем игрока

Скопируем код скрипта drawMaze.js в новый файл drawMaze_player.js и все изменения будем вносить уже туда. Также сразу заменим вызов скрипта в HTML-файле:

<script src="drawMaze_player.js"></script>

Чтобы управлять игроком, добавим новую глобальную переменную в скрипт:

// стартовые координаты игрока
var player = {};
player.X = 0;
player.Y = 0;

Мы указали координату (0,0) — она чётная, поэтому в лабиринте на этом месте точно не будет стены. Ещё это левый верхний угол карты — как раз в этом месте мы в прошлый раз сделали вход в лабиринт.

Для простоты нарисуем фигурку игрока в виде красного квадрата. Создадим для отрисовки новую функцию, которая просто рисует этот квадрат по текущим координатам игрока:

// рисуем фигурку игрока
function drawPlayer() {
	// берём красный цвет
	context.fillStyle = 'red';
	// начинаем рисовать новую фигуру
	context.beginPath();
	// рисуем белый прямоугольник над первой ячейкой лабиринта
	context.rect(padding + player.X * fieldSize, padding + player.Y * fieldSize, fieldSize, fieldSize);
	// закрашиваем его белым
	context.fill();
}

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

// рисуем игрока
drawPlayer();
Играем в лабиринт
У нас появился игрок, которым мы будем скоро управлять

Настраиваем управление

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

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

Запишем это в виде кода:

// Отслеживаем нажатия клавиш
document.addEventListener('keydown', function(e) {

	// стрелка вверх,
	if (e.which === 38) {
		// если мы не упираемся в стену или в границу лабиринта при этом ходе, 
		if ( ((player.Y - 1) >=0) && (getField(player.X, player.Y - 1) != '▉') ) {
			// то двигаем игрока вверх
			player.Y -= 1;
		}

	};

	// стрелка вниз
	if (e.which === 40) {
		// если мы не упираемся в стену или в границу лабиринта при этом ходе, 
	  if ( ((player.Y + 1) <= rowsSize - 1) && (getField(player.X, player.Y + 1) != '▉') ) {
			// то двигаем игрока вниз
			player.Y += 1;

		}
	};

	// стрелка влево
	if (e.which === 37) {
		// если мы не упираемся в стену или в границу лабиринта при этом ходе, 
	  if ( ((player.X - 1) >=0) && (getField(player.X - 1, player.Y) != '▉') ) {
			// то двигаем игрока влево
			player.X -= 1;
		}
	};
	// стрелка вправо
	if (e.which === 39) {
		// если мы не упираемся в стену или в границу лабиринта при этом ходе, 
	  if ( ((player.X + 1) <= columnsSize - 1) && (getField(player.X + 1, player.Y) != '▉') ) {
			// то двигаем игрока вправо
			player.X += 1;

		}
	};
});

Запускаем анимацию

Если мы сейчас нажмём на стрелки на клавиатуре, то игрок никуда не сдвинется — у нас ещё нет анимации движения. Код запуска анимации мы тоже подсмотрим в игре про змейку, а логика будет такая:

  1. Рисуем лабиринт.
  2. Рисуем вход и выход из лабиринта.
  3. Рисуем игрока по текущим координатам.
  4. Всё это прокручиваем в цикле до тех пор, пока не выйдем из лабиринта.
  5. Как только дошли до выхода — останавливаем анимацию.

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

Так как мы заранее не знаем, чётное число клеток будет в лабиринте или нечётное, то не сможем заранее узнать толщину правой и нижней стенки. Чтобы сделать универсальную проверку, сделаем так:

  1. Возьмём длину и ширину лабиринта.
  2. Вычтем оттуда единицу, так как толщина стены лабиринта как минимум одна клетка.
  3. Ещё вычтем оттуда остаток от деления (длины + 1) на 2. Если в лабиринте нечётное количество клеток, то у нас получится чётное число и остаток будет равен нулю. А если чётное, то будет единица — на это расстояние и будет смещён выход.

Код готовой анимации:

function loop() {
	lp = requestAnimationFrame(loop);

	// рисуем рамку и готовимся к отрисовке лабиринта
	init();
	// рисуем лабиринт
	drawMap();
	// делаем вход и выход из лабиринта
	drawExit();
	// рисуем игрока
	drawPlayer();
	// проверяем, дошёл ли игрок до выхода
	if ((player.X == columnsSize - 1 - ((columnsSize + 1) % 2)) && (player.Y == rowsSize - 1 -((rowsSize + 1) % 2)) ) {
	        // останавливаем анимацию
	        cancelAnimationFrame(lp);
}

// запускаем игру
lp = requestAnimationFrame(loop);
Играем в лабиринт

Добавляем фейерверк

Сделаем красивую концовку — запустим фейерверк на странице, когда игрок дойдёт до выхода.

Мы не будем сами писать этот код, а возьмём готовое решение из интернета  — наберём в поиске «фейерверк на CSS» и выберем самое простое решение:

1. Добавляем в HTML-страницу такой код:

<div class="pyro">
	<div class="before"></div>
	<div class="after"></div>
</div>

2. Для запуска фейерверка нам остаётся только добавить CSS-стиль в раздел <head> на этой же странице.

Чтобы фейерверк не запускался раньше времени, мы вставим код добавления стиля в проверку выхода из лабиринта. Если игрок дошёл до выхода — вставляем код и смотрим на фейерверк. Для вставки используем любимый приём с инжектом кода:

// вставляем стили в HTML-файл для запуска фейерверка
var js1 = document.createElement('link'); 
js1.href = "https://mihailmaximov.ru/projects/maze/style.css"; 
js1.rel = "stylesheet";
document.head.appendChild(js1);
Играем в лабиринт

Пройти лабиринт на странице проекта.

// количество колонок в лабиринте
const columnsSize = 35;
// количество строк в лабиринте
const rowsSize = 35;
// размер клетки в лабиринте
const fieldSize = 7;
 // рамка (внешняя граница лабиринта)
const padding = 10;

// стартовые координаты игрока
var player = {};
player.X = 0;
player.Y = 0;


// находим холст на странице по имени элемента
const canvas = document.querySelector('canvas');
// создаём переменную, через которую будем работать с холстом
const context = canvas.getContext('2d');

// количество тракторов
const tractorsNumber = 50;
// создаём новую карту лабиринта, которую будем отрисовывать
const map = generateMaze(columnsSize, rowsSize, tractorsNumber);

// переменные сдвига
var shiftX = 0;
var shiftY = 0;

// рисуем рамку и готовимся к отрисовке лабиринта
function init () {
	// устанавливаем размеры холста
	canvas.width = padding * 2 + columnsSize * fieldSize;
	canvas.height = padding * 2 + rowsSize * fieldSize;

	// цвет заливки
	context.fillStyle = 'black';
	// рисуем прямоугольник на весь холст с координатами левого верхнего и правого нижнего углов
	context.rect(0, 0, canvas.width, canvas.height);
	// закрашиваем его выбранным цветом
	context.fill();

	// делаем белое поле внутри рамки, чтобы потом нарисовать на нём стены
	context.fillStyle = 'white';
	// сообщаем скрипту, что сейчас будем рисовать новую фигуру
	context.beginPath();
	// рисуем прямоугольник, отступив от границ холста на толщину рамки
	context.rect(padding, padding, canvas.width - padding * 2, canvas.height - padding * 2);
	// закрашиваем его белым 
	context.fill();
}


// получаем значение ячейки из лабиринта
function getField (x, y) {
	// если хотя бы одна из координат не находится в границах карты
	if (x < 0 || x >= columnsSize || y < 0 || y >= rowsSize) {
		// выходим из функции и говорим, что такой ячейки нет
		return null;
	}
	// если дошли до сюда, значит координата верная и мы возвращаем её значение из карты лабиринта
	return map[y][x];
}

// отрисовываем карту
function drawMap () {
	// обрабатываем по очереди все ячейки в каждом столбце и строке
	for (let x = 0; x < columnsSize; x++) {
		for (let y = 0; y < rowsSize; y++) {
			// если на карте лабиринта эта ячейка помечена как стена
			if (getField(x, y) === '▉') {
				// берём чёрный цвет
				context.fillStyle = 'black';
				// начинаем рисовать новую фигуру
				context.beginPath();
				// делаем прямоугольник на месте этой ячейки
				context.rect(padding + x * fieldSize, padding + y * fieldSize, fieldSize, fieldSize);
				// закрашиваем его чёрным
				context.fill();
			}
		}
	}
}

// рисуем вход и выход из лабиринта
function drawExit() {
	// берём белый цвет
	context.fillStyle = 'white';
	// начинаем рисовать новую фигуру
	context.beginPath();
	// рисуем белый прямоугольник над первой ячейкой лабиринта
	context.rect(padding, 0, fieldSize, padding);
	// закрашиваем его белым
	context.fill();

	// берём белый цвет
	context.fillStyle = 'white';
	// начинаем рисовать новую фигуру
	context.beginPath();

	// считаем размеры сдвига
	if (columnsSize % 2 == 0) {shiftX = fieldSize};
	if (rowsSize % 2 == 0) {shiftY = fieldSize};

	// рисуем белый прямоугольник под последней ячейкой лабиринта
	context.rect((columnsSize - 1) * fieldSize + padding - shiftX, rowsSize * fieldSize + padding - shiftY, fieldSize, padding + shiftY);
	// закрашиваем его белым
	context.fill();

}

// рисуем фигурку игрока
function drawPlayer() {
	// берём красный цвет
	context.fillStyle = 'red';
	// начинаем рисовать новую фигуру
	context.beginPath();
	// рисуем белый прямоугольник над первой ячейкой лабиринта
	context.rect(padding + player.X * fieldSize, padding + player.Y * fieldSize, fieldSize, fieldSize);
	// закрашиваем его белым
	context.fill();
}

// Отслеживаем нажатия клавиш
document.addEventListener('keydown', function(e) {

	// стрелка вверх,
	if (e.which === 38) {
		// если мы не упираемся в стену или в границу лабиринта при этом ходе, 
		if ( ((player.Y - 1) >=0) && (getField(player.X, player.Y - 1) != '▉') ) {
			// то двигаем игрока вверх
			player.Y -= 1;
		}

	};

	// стрелка вниз
	if (e.which === 40) {
		// если мы не упираемся в стену или в границу лабиринта при этом ходе, 
	  if ( ((player.Y + 1) <= rowsSize - 1) && (getField(player.X, player.Y + 1) != '▉') ) {
			// то двигаем игрока вниз
			player.Y += 1;

		}
	};

	// стрелка влево
	if (e.which === 37) {
		// если мы не упираемся в стену или в границу лабиринта при этом ходе, 
	  if ( ((player.X - 1) >=0) && (getField(player.X - 1, player.Y) != '▉') ) {
			// то двигаем игрока влево
			player.X -= 1;
		}
	};
	// стрелка вправо
	if (e.which === 39) {
		// если мы не упираемся в стену или в границу лабиринта при этом ходе, 
	  if ( ((player.X + 1) <= columnsSize - 1) && (getField(player.X + 1, player.Y) != '▉') ) {
			// то двигаем игрока вправо
			player.X += 1;

		}
	};
});

// анимация
function loop() {
	lp = requestAnimationFrame(loop);

	// рисуем рамку и готовимся к отрисовке лабиринта
	init();
	// рисуем лабиринт
	drawMap();
	// делаем вход и выход из лабиринта
	drawExit();
	// рисуем игрока
	drawPlayer();
	// проверяем, дошёл ли игрок до выхода
	if ((player.X == columnsSize - 1 - ((columnsSize + 1) % 2)) && (player.Y == rowsSize - 1 -((rowsSize + 1) % 2)) ) {
	        // останавливаем анимацию
	        cancelAnimationFrame(lp);

	        // вставляем стили в HTML-файл для запуска фейерверка
	    	var js1 = document.createElement('link'); 
	    	js1.href = "https://mihailmaximov.ru/projects/maze/style.css"; 
	    	js1.rel = "stylesheet";
	    	document.head.appendChild(js1);
		}
}

// запускаем игру
lp = requestAnimationFrame(loop);

<!DOCTYPE html>
<html lang="ru">
<head>
	<meta charset="UTF-8">
	<title>Лабиринт</title>
</head>
<body>
	<!-- подготавливаем пустой холст, чтобы работать с ним из скрипта -->
	<canvas></canvas>
	<!-- скрипт, который создаёт лабиринт -->
	<script src="generateMaze.js">	</script>
	<!-- этот скрипт отвечает за отрисовку лабиринта -->
	<script src="drawMaze_player.js"></script>

	<div class="pyro">
		<div class="before"></div>
		<div class="after"></div>
	</div>
</body>
</html>

Что дальше

Если будет настроение, отправимся в новое приключение — будем рисовать путь-подсказку, если лабиринт оказался слишком сложным. А пока поиграем так.

Текст:

Михаил Полянин

Редактор:

Михаил Полянин

Обложка:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Мария Дронова

Соцсети:

Юлия Зубарева

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