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

в 11:49, , рубрики: Gamedev, html, javascript, причем тут linux?, Программирование, разработка игр, создание игр на javascript

Давно уже не писал для хабра, сегодня хотелось бы порассуждать на тему того, как организовать простейшую механику уровней для игры 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 минут

Физика игры

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

Автор: Skaner

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js