javascript

Разработка механики игры Bounce от Nokia на JavaScript

  • суббота, 11 марта 2017 г. в 03:14:24
https://habrahabr.ru/post/323622/
  • Разработка игр
  • Программирование
  • JavaScript
  • HTML


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

image

Начнем по порядку. Я не буду расписывать все ненужное, а просто пробегусь по коду комментируя те или иные моменты.

Первое, это index.html — основной запускаемый файл:

<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
	<meta name="viewport" content="width=device-width,user-scalable=no"/>
	<title>Bounce</title>
</head>
<body>
<script type="text/javascript" src="point.js"></script>
<script type="text/javascript" src="init.js"></script>
<script type="text/javascript" src="menu.js"></script>
<script type="text/javascript" src="game.js"></script>
<script type="text/javascript">
  game.startLoop('menu');
</script>
</body>
</html>

Смотреть на гитхабе

Тут, думаю, все ясно: мы создали каркас страницы и подключили несколько JavaScript файлов.

  • point.js — движок PointJS
  • init.js — инициализация всего и вся + глобальные переменные
  • menu.js — плагин к PointJS — Menu
  • game.js — файл, описывающий механику игру

Рассмотрим файл init.js:

// Тут подключение движка, далее объект pjs будет глобальным
var pjs = new PointJS('2D', 400, 400, {
	backgroundColor : '#C9D6FF'
});

// Включаем полностраничный режим
pjs.system.initFullPage();

// Объявляем ссылки на быстрый доступ к внутренностям движка
var log = pjs.system.log; // логирование событий
var game = pjs.game; // объект управления игровыми состояниями и объектами
var point = pjs.vector.point; // конструктор точек
var camera = pjs.camera; / доступ к камере
var brush = pjs.brush; / доступ к методам простого рисования
var OOP = pjs.OOP; / доступ к дополнительным обработчиком объектов
var math = pjs.math; / модуль игровой математики

// инициализируем мышь и клавиатуру
var key = pjs.keyControl.initKeyControl();
var mouse = pjs.mouseControl.initMouseControl();

// тут объявим глобальные переменные счета и рекорда
var score = 0;
var record = 0;

Смотреть на гитхабе

Такой вот получился код, пока этого достаточно.

Теперь нам надо создать меню для игры, так как у нас стартует игра с цикла «menu», все необходимое для этого уже имеется в подключенном нами файле menu.js, надо лишь инициировать соответствующий игровой цикл, который будет являться нашим меню, делается это в том же файле menu.js, просто допишем там следующее:

game.newLoopFromClassObject('menu', new Menu(pjs, {
	name  : 'Bounce', // Наименование игры (выводится вверху)
	author : 'SkanerSoft', // автор игры
	radius : 15, // радиус скругления пунктов меню
	items : { // сами пункты, формат: loopName : Видимая надпись
		game  : 'В игру', // перейдет в игровой цикл game
		about : 'Об игре', // перейдет в игровой цикл about
	}
}));

Смотреть весь файл menu.js на гитхабе

Если запустим, то получим запущенное меню:

image

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

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

game.newLoopFromConstructor('game', function () {
  /*your code*/
});

Тут мы можем задать игровому циклу наименование и конструктор.

Теперь переместимся внутрь конструктора, и напишем логику (механику) уровня внутри него. Нам на текущий момент понадобится только одно событие цикла: update. С ним работать и будем:

game.newLoopFromConstructor('game', function () {
	// объявим объект с данными карты
	var map = {
		width : 50, // ширина тайла
		height : 50, // высота тайла
		source : [ // исходные данные карты (массив строк)
			'',
			'',
			'               0-',
			'    |     P  0000', // P - позиция игрока
			'  00000 000 00000',
			'      0 0|     |0        |',
			'0000000 000000W00 000000000000',
			'      000    0W00 0 ',
			'             0W0  0 ',
			'             0W | 0 ',
			'             000000 ',
		]
	};

	// стартовая позиция (переменная)
	var plStartPosition = false;

	var walls = []; // массив стен (блоки, по которым возможно передвигаться)
	var cells = []; // цели (колечки, которые можно собирать)
	var waters = []; // тут будет вода (блоки полупрозрачных объектов синего цвета), меняющая поведение объекта

	// OOP.forArr - проходит быстро по массиву
	OOP.forArr(map.source, function (string, Y) { // идем по массиву строк (Y - порядковый номер строки сверху вниз)
		OOP.forArr(string, function (symbol, X) { // идем уже по самой строке (X - порядковый номер символа в строке)
			if (!symbol || symbol == ' ') return; // если пробел или ошибка считывания - выходим из итерации

			// теперь проверяем символы
			if (symbol == 'P') { // позиционируем персонажа
				// Займемся игроком
				plStartPosition = point(map.width*X, map.height*Y); // если формула не ясна, напишите в комменты
			} else if (symbol == 'W') { // вода
				waters.push(game.newRectObject({ // создаем объект
					w : map.width, h : map.height, // ширина высота
					x : map.width*X, y : map.height*Y, // позиция
					fillColor : '#084379', // цвет
					alpha : 0.5 // прозрачность
				}));
			} else if (symbol == '|') { // цель (колечко)
				cells.push(game.newRectObject({ 
					w : map.width/2, h : map.height,
					x : map.width*X, y : map.height*Y,
					fillColor : '#FFF953',
					userData : {
						active : true // флаг активности, пока не коснулся игрок - оно активно
					}
				}));
			} else if (symbol == '-') { // горизонтальное колечко
				cells.push(game.newRectObject({
					w : map.width, h : map.height/2,
					x : map.width*X, y : map.height*Y,
					fillColor : '#FFF953',
					userData : {
						active : true
					}
				}));
			} else if (symbol == '0') { // блок стены
				walls.push(game.newRectObject({
					w : map.width, h : map.height,
					x : map.width*X, y : map.height*Y,
					fillColor : '#B64141'
				}));
			}

		});
	});

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

	var player = game.newCircleObject({
		radius : 20,
		fillColor : '#FF9191',
		position : plStartPosition ? plStartPosition : point(0, 0)
	});
	player.gr = 0.5; // скорость падения
	player.speed = point(0, 0); // скорости по осям


	// а вот и тот самый обработчик на событие обновления
	this.update = function () {
		game.clear(); // очищаем прошлый кадр
		player.draw(); // отрисовываем игрока

		player.speed.y += player.gr; // используем гравитацию

		// управление с клавиатуры, думаю, ничего сложного
		if (key.isDown('RIGHT'))
			player.speed.x = 2; 
		else if (key.isDown('LEFT'))
			player.speed.x = -2;
		else
			player.speed.x = 0;

		// теперь вызываем функцию отрисовки массива стен
		OOP.drawArr(walls, function (wall) {
			if (wall.isInCameraStatic()) { // если объект в пределах камеры (его видно)
				// wall.drawStaticBox(); 
				if (wall.isStaticIntersect(player)) { // если объект столкнулся с игроком

					// теперь нам надо определить условия столкновения (подробное объяснение в видео ниже)

					// проверяем ось Y

					if (player.x+player.w > wall.x+wall.w/4 && player.x < wall.x+wall.w-wall.w/4) { 
						if (player.speed.y > 0 && player.y+player.h < wall.y+wall.h/2) { // если объект НАД стеной
							if (key.isDown('UP')) // если при соприкосновении с полом нажать кнопку "вверх"
								player.speed.y = -10; // установим скорость движения вверх
							else { // иначе просто "гасим" скорость падения прыжками
								player.y = wall.y - player.h;
								player.speed.y *= -0.3;
								if (player.speed.y > -0.3)
									player.speed.y = 0; // и в итоге просто обнуляем
							}
						} else if (player.speed.y < 0 && player.y > wall.y+wall.h/2) { // если пбъект ПОД стеной
							player.y = wall.y+wall.h; // позиционируем (избегаем проваливания)
							player.speed.y *= -0.1; // начинаем падать
						}
					}

					// и тут то же самое, только уже для оси X

					if (player.y+player.h > wall.y+wall.h/4 && player.y < wall.y+wall.h-wall.h/4) {

						if (player.speed.x > 0 && player.x+player.w < wall.x+wall.w/2) { // если стена справа
							player.x = wall.x-player.w; // избегаем проваливания
							player.speed.x = 0; // убираем скорость движения
						}

						if (player.speed.x < 0 && player.x > wall.x+wall.w/2) { // если стена слева
							player.x = wall.w+wall.x; // избегаем проваливания
							player.speed.x = 0; // убираем скорость движения
						}
					}


				}
			}
		});

		// теперь рисуем и орабатываем цели (колечки)
		OOP.drawArr(cells, function (cell) {
			if (cell.active) { // если колечко активно
				if (cell.isStaticIntersect(player)) { // проверяем столкновение
					cell.active = false; // снимаем активность
					cell.fillColor = '#9A9A9A'; // закрашиваем в другой цвет
					score++; // увеличиваем счет
				}
			}
		});

		// зададим еще переменную флаг, определяющую находится ли
		// объект в воде

		var onWater = false;

		// Рисуем и обрабатываем воду
		OOP.drawArr(waters, function (water) {
			// Если наш игрок уже находится в воде, ничего не делаем
			if (onWater) return;
			// Тут нам надо определить стролкновение
			// и направить скорость вверх (выталкивание)
			// Надо хорошенько все продумать

			// Нам требуется учесть, что выталкивающая сила начинает
			// работать только тогда, когда шар опустится в воду
			// примерно на половину от его высоты
			if (water.isStaticIntersect(player) && player.y+player.h/2 > water.y) {
				player.speed.y -= 0.9; // определим оптимальную скорость
				onWater = true;
			}
		});

		// тут само движение объектов

		if (player.speed.y) {
			player.y += player.speed.y;
		}

		if (player.speed.x) {
			player.x += player.speed.x;
		}

		// рисуем счет

		brush.drawTextS({ // команда рисования
			text : 'Score: '+score, // выводим саму надпись
			size : 30, // размер шрифта
			color : '#FFFFFF', // цвет текста
			strokeColor : '#002C5D', // цвет обводки текста
			strokeWidth : 1, // ширина обводки
			x : 10, y : 10, // позиция
			style : 'bold' // жирный шрифт
		});

		camera.follow(player, 50); // следим камерой за объектом игрока

	};
});

Смотреть файл на гитхабе

Теперь, если мы запустим и перейдем в игру, то увидим следующее:

image

Таким образом мы построили несложную механику игры, дополнить её новыми элементами не так сложно, вписать в обработку — аналогично.

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

Посмотреть исходник проекта Bounce на гитхаб
Запустить пример вижвую

Видео, в котором я разрабатываю алгоритм физики для игры 20 минут
Физика игры

Дополнение игры новыми объектами: