Как написать свою браузерную игру

Современная вычислительная техника позволяет создавать классные компьютерные игры! И сейчас, достаточно популярны игры с 3d-графикой, так как, играя в них, ты окунаешься в вымышленный мир и теряешь всякую связь с реальностью. Развитие интернета и браузерных технологий сделало возможным запускать головоломки и стрелялки в любимом Хроме, Мозилле или еще в чем-то там (про Эксплорер помолчим) в онлайн-режиме, без загрузки. Так вот, здесь я расскажу о том, как создать простую трехмерную браузерную игру.

Выбор жанра, сюжета и стилистики игры является достаточно интересной задачей, и от решения этих вопросов может зависеть успех игры. Кроме этого, свои нюансы вносит и выбор технологии, на основе которой будет создаваться продукт. Моя цель – показать элементарные основы этого увлекательного процесса, поэтому я буду делать 3-мерный лабиринт с незамысловатым оформлением. Более того, я это сделаю на чистом коде без использования библиотек и движков, типа three.js (хотя большие проекты лучше делать все-таки на нем), чтобы показать, как можно создать движок для своих нужд. Полностью самописная игра может быть оригинальной, а потому интересной. В общем, оба подхода имеют свои плюсы и минусы.

Я полагаю, если вы читаете эту статью, то вам интересна тема создания игр для гугл Хром, а, значит, понимаете, как работает связка html-css-javaScript, поэтому не буду останавливаться на основах, а сразу приступлю к разработке. В html5 и css3, которые поддерживают все современные браузеры (Эксплорер не в счет), есть возможность расположения блоков в 3-мерном пространстве. Также есть элемент , в котором можно рисовать линии и графические примитивы. Большинство браузерных движков используют <сanvas>, так как на нем можно сделать больше вещей, да и производительность на нем выше. Но для простых вещей вполне можно использовать методы transform-3d, которые будут занимать меньше кода.

1. Инструменты для разработки

Я использую для проверки сайтов и игр только 2 браузера: Chrome и Mozilla. Все остальные браузеры (кроме того самого Эксплорера) построены на движке первого, поэтому использовать их я не вижу смысла, ибо результаты точно такие же, как и в Chrome. Для написания кода достаточно Notepad++.

2. Как реализуется трехмерное пространство в html?

Посмотрим на систему координат блока:

По умолчанию, дочерний блок имеет координаты (left и top) 0 пикселей по x и 0 пикселей по y. Смещение (translate), также 0 пикселей по всем трем осям. Покажем это на примере, для чего создадим новую папку. В нем создадим файлы index.html, style.css и script.js. Откроем index.html и запишем туда следующее:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world">
        </div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

В файле style.css зададим стили для элементов “container” и “world”.

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
}
#world{
	width:300px;
	height:300px;
        background-color:#C0FFFF;
}

Сохраним. Откроем index.html c помощью Chrome, получим:

Попробуем применить translate3d к элементу “world”:

#world{
	width:300px;
	height:300px;
        background-color:#C0FFFF;
        transform:translate3d(200px,100px,0px);
}

Как вы поняли, я перешел в полноэкранный режим. Теперь зададим смещение по оси Z:
transform:translate3d(200px,100px,-1000px);

Если вы снова откроете html-файл в браузере, то никаких изменений вы не увидите. Чтобы увидеть изменения, нужно задать перспективу для объекта “container”:

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
	perspective:600px;
}

В результате:

Квадрат отдалился от нас. Как работает перспектива в html? Взглянем на картинку:

d – расстояние от пользователя до объекта, а z – его координата. Отрицательный z (в html это translateZ) означает, что мы отдалили объект, а положительный – наоборот. Значение perspective определяет величину d. Если же свойство perspective не задано, то значение d принимается за бесконечность, а в этом случае объект визуально не изменяется для пользователя с изменением z. В нашем случае мы задали d = 600px. По умолчанию, точка взгляда перспективы находится в центре элемента, однако ее можно изменить путем задания свойства perspective-origin: .

Теперь повернем “world” вокруг какой-нибудь оси. В сss можно использовать 2 способа вращения. Первый – вращение вокруг осей x,y и z. Для этого используются transform-свойства rotateX(), rotateY() и rotateZ(). Второй – вращение вокруг заданной оси с помощью свойства rotate3d(). Мы будем использовать первый способ, так как он больше подходит для наших задач. Обратите внимание, что оси вращения выходят из центра прямоугольника!

Точка, относительно которой происходят трансформации, может быть изменена путем задания свойства translate-origin: . Итак, зададим вращение “world” по оси x:

#world{
	width:300px;
	height:300px;
background-color:#C0FFFF;
transform:translate3d(200px,100px,0px) rotateX(45deg);
}

Получим:

Заметно смещение против часовой стрелки. Если же мы добавим rotateY(), то получим смещение уже по оси Y. Важно заметить, что при вращении блока оси вращения также поворачиваются. Вы также можете поэкспериментировать с различными значениями вращения.
Теперь внутри блока “world” создадим еще один блок, для этого добавим тег в html-файл:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world">
			<div id="square1"></div>
		</div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

В style.css добавим стили к этому блоку:

#square1{
	position:absolute;
	width:200px;
	height:200px;
	background-color:#FF0000;
}

Получим:

То есть, элементы внутри блока “world” будут трансформироваться в составе этого блока. Попробуем повернуть “square1” по оси y, добавив к нему стиль вращения:
transform: rotateY(30deg);

В итоге:

«Где вращение?» — спросите вы? На самом деле именно так выглядит проекция блока “square1” на плоскость, образуемую элементом “world”. Но нам нужна не проекция, а настоящее вращение. Чтобы все элементы внутри “world” стали объемными, необходимо применить к нему свойство transform-style:preserve-3d. После подстановки свойства внутрь списка стилей “world” проверим изменения:

Отлично! Половина блока “square” скрылась за голубым блоком. Чтобы его полностью показать, уберем цвет блока “world”, а именно, удалим строку background-color:#C0FFFF; Если мы добавим еще прямоугольников внутрь блока “world”, то мы можем создать трехмерный мир. Сейчас же уберем смещение мира “world”, удалив строку со свойством transform в стилях для этого элемента.

3. Создаем движение в трехмерном мире

Для того, чтобы пользователь мог по этому миру передвигаться, нужно задать обработчики нажатия клавиш и перемещения мыши. Управление будет стандартным, какое присутствует в большинстве 3д-шутеров. Клавишами W, S, A, D мы будем перемещаться вперед, назад, влево, вправо, пробелом мы будем прыгать (проще говоря – перемещаться вверх), а мышью мы будем менять направление взгляда. Для этого откроем пока еще пустой файл script.js. Сначала впишем туда такие переменные:

// Нажата ли клавиша?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;

Изначально клавиши не нажаты. Если мы нажмем клавишу, то значение определенной переменной изменится на 1. Если отпустим ее, то она снова станет 0. Реализуем это посредством добавления обработчиков нажатия и отжатия клавиш:

// Обработчик нажатия клавиш

document.addEventListener("keydown", (event) =>{
	if (event.key == "a"){
		PressLeft = 1;
	}
	if (event.key == "w"){
		PressForward = 1;
	}
	if (event.key == "d"){
		PressRight = 1;
	}
	if (event.key == "s"){
		PressBack = 1;
	}
	if (event.keyCode == 32 && onGround){
		PressUp = 1;
	}
});

// Обработчик отжатия клавиш

document.addEventListener("keyup", (event) =>{
	if (event.key == "a"){
		PressLeft = 0;
	}
	if (event.key == "w"){
		PressForward = 0;
	}
	if (event.key == "d"){
		PressRight = 0;
	}
	if (event.key == "s"){
		PressBack = 0;
	}
	if (event.keyCode == 32){
		PressUp = 0;
	}
});

Номер 32 – код пробела. Как видите, тут появилась переменная onGround, указывающая на то, находимся ли мы на земле. Пока разрешим движение вверх, добавив после переменных press… переменную onGround:

// На земле ли игрок?

var onGround = true;

Итак, мы добавили алгоритм нажатия и отжатия. Теперь необходимо добавить само передвижение. Что, собственно, мы передвигаем. Представим, что у нас есть объект, который мы двинаем. Назовем его “pawn”. Как и принято у нормальных разработчиков, для него мы создадим отдельный класс “Player”. Классы в javaScript создаются, как ни странно, с помощью функций:

function player(x,y,z,rx,ry) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.rx = rx;
	this.ry = ry;
}

Вставим этот код в script.js в самом начале файла. В конце же файла создадим объект данного типа:

// Создаем новый объект

var pawn = new player(0,0,0,0,0);

Распишем, что означают эти переменные. x, y, z – это начальные координаты игрока, rx, ry – углы его поворота относительно осей x и y в градусах. Последняя записанная строка означает, что мы создаем объект “pawn” типа “player” (специально пишу тип, а не класс, так как классы в javascript означают несколько другие вещи) с нулевыми начальными координатами. Когда мы двигаем объект, координата мира изменяться не должна, а должна изменяться координата «pawn». Это с точки зрения переменных. А с точки зрения пользователя, игрок находится на одном месте, а вот мир двигается. Таким образом, нужно заставить программу изменять координаты игрока, обрабатывать эти изменения и двигать, в конце концов, мир. На деле это проще, чем кажется.

Итак, после загрузки документа в браузер мы запустим функцию, которая перерисовывает мир. Напишем функцию перерисовки:

function update(){
	
	// Высчитываем смещения
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = PressUp;
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	
	// Изменяем координаты мира (для отображения)
	
	world.style.transform = 
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

В новых браузерах world будет соответствовать элементу с id=«world», однако надежнее ее присвоить перед функцией update() с помощью следующей конструкции:

var world = document.getElementById("world");

Мы будем изменять положение мира каждые 10 мс (100 обновлений в секунду), для чего запустим бесконечный цикл:

TimerGame = setInterval(update,10);

Запустим игру. Ура, теперь мы можем двигаться! Однако мир вылазит за пределы рамок элемента «container». Чтобы этого не происходило, зададим css-свойство для него в style.css. Добавим строку overflow:hidden; и посмотрим на изменения. Теперь мир остается в пределах контейнера.

Вполне возможно, что вы не всегда понимаете, куда нужно записывать те или иные строчки кода, поэтому сейчас я вам представлю файлы, которые, как я полагаю, у вас должны получиться:

index.html:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world">
			<div id="square1"></div>
		</div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

style.css:

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
	perspective:600px;
	overflow:hidden;
}
#world{
	position:absolute;
	width:300px;
	height:300px;
	transform-style:preserve-3d;
}
#square1{
	position:absolute;
	width:200px;
	height:200px;
	background-color:#FF0000;
	transform:rotateY(30deg);
}

script.js:

// Конструктор Pawn

function player(x,y,z,rx,ry) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.rx = rx;
	this.ry = ry;
}

// Нажата ли клавиша?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;

// На земле ли игрок?

var onGround = true;

// Обработчик нажатия клавиш

document.addEventListener("keydown", (event) =>{
	if (event.key == "a"){
		PressLeft = 1;
	}
	if (event.key == "w"){
		PressForward = 1;
	}
	if (event.key == "d"){
		PressRight = 1;
	}
	if (event.key == "s"){
		PressBack = 1;
	}
	if (event.keyCode == 32 && onGround){
		PressUp = 1;
	}
});

// Обработчик отжатия клавиш

document.addEventListener("keyup", (event) =>{
	if (event.key == "a"){
		PressLeft = 0;
	}
	if (event.key == "w"){
		PressForward = 0;
	}
	if (event.key == "d"){
		PressRight = 0;
	}
	if (event.key == "s"){
		PressBack = 0;
	}
	if (event.keyCode == 32){
		PressUp = 0;
	}
});

// Создаем новый объект

var pawn = new player(0,0,0,0,0);

// Привяжем новую переменную к world

var world = document.getElementById("world");

function update(){
	
	// Задаем локальные переменные смещения
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = - PressUp;
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	
	// Изменяем координаты мира (для отображения)
	
	world.style.transform = 
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

TimerGame = setInterval(update,10);

Если у вас что-то по-другому, обязательно поправьте!

Мы научились двигать персонажа, однако мы еще не умеем поворачивать его! Поворот персонажа, конечно же, будет осуществляться с помощью мыши. Для мыши к переменным состояния клавиш press… мы добавим переменные состояния движения мыши:

// Нажата ли клавиша и двигается ли мышь?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;

А после обработчиков нажатия-отжатия вставим обработчик движения:

// Обработчик движения мыши

document.addEventListener("mousemove", (event)=>{
	MouseX = event.movementX;
	MouseY = event.movementY;
});

В функцию update добавим поворот:

	// Задаем локальные переменные смещения
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	pawn.rx = pawn.rx + drx;
	pawn.ry = pawn.ry + dry;

Обратите внимание на то, что движение мыши по оси y вращает pawn по оси x и наоборот. Если мы посмотрим на результат, то ужаснемся от увиденного. Дело в том, что если смещения нет, то MouseX и MouseY остаются прежними, а не приравниваются к нулю. Значит, после каждой итерации update смещения миши должно обнуляться:

// Задаем локальные переменные смещения
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;

// Обнулим смещения мыши:
	
	MouseX = MouseY = 0;

// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	pawn.rx = pawn.rx + drx;
	pawn.ry = pawn.ry + dry;

Уже лучше, мы избавились от инерции вращения, однако вращение происходит все равно странно! Чтобы понять, что все-таки происходит, добавим div-элемент «pawn» внутрь «container»:

	<div id="container">
		<div id="world">
			<div id="square1"></div>
		</div>
		<div id="pawn"></div>
	</div>

Зададим ему стили в style.css:

#pawn{
	position:absolute;
	width:100px;
	height:100px;
	top:400px;
	left:600px;
	transform:translate(-50%,-50%);
	background-color:#0000FF;
}

Проверим результат. Теперь все ровно! Единственное — синий квадрат остается впереди, но пока оставим это. Чтобы сделать игру от первого лица, а не от третьего, нужно приблизить мир к нам на значение perspective. Сделаем это в script.js в функции update():

world.style.transform = 
	"translateZ(600px)" +
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";

Теперь можно делать игру от первого лица. Скроем pawn добавив строку в style.css:

#pawn{
	display:none;
	position:absolute;
	top:400px;
	left:600px;
	width:100px;
	height:100px;
	transform:translate(-50%,-50%);
	background-color:#0000FF;
}

Отлично. Сразу скажу, что ориентироваться в мире с одним квадратом крайне тяжело, поэтому создадим площадку. Добавим в «world» блок «square2»:

	<div id="world">
			<div id="square1"></div>
			<div id="square2"></div>
		</div>

А в style.css добавим стили для него:

#square2{
	position:absolute;
	width:1000px;
	height:1000px;
	top:400px;
	left:600px;
	background-color:#00FF00;
	transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px);
}

Теперь все четко. Ну… не совсем. Когда мы нажимаем по клавишам, мы движемся строго по осям X и Z. А мы хотим сделать движение по направлению взгляда. Сделаем следующее: в самом начале файла script.js добавим 2 переменные:

// Мировые константы

var pi = 3.141592;
var deg = pi/180;

Градус — это pi/180 от радиана. Нам придется применить синусы и косинусы, которые считаются от радиан. Что нужно сделать? Взгляните на рисунок:

Когда наш взгляд направлен под углом и мы хотим пойти вперед, то изменятся обе координаты: X и Z. В случае перемещения в сторону тригонометрические функции просто поменяются местами, а перед образовавшимся синусом изменится знак. Изменим уравнения смещений в update():

// Задаем локальные переменные смещения
	
	let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
	let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);	
	let dy = -PressUp;
	let drx = MouseY;
	let dry = - MouseX;

Внимательно просмотрите все файлы полностью! Если у вас что-то оказалось не так, то потом обязательно буду ошибки, из-за которых вы сломаете голову!

index.html:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world">
			<div id="square1"></div>
			<div id="square2"></div>
		</div>
		<div id="pawn"></div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

style.css:

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
	perspective:600px;
	overflow:hidden;
}
#world{
	position:absolute;
	width:inherit;
	height:inherit;
	transform-style:preserve-3d;
}
#square1{
	position:absolute;
	width:200px;
	height:200px;
	top:400px;
	left:600px;
	background-color:#FF0000;
	transform:translate(-50%,-50%) rotateY(30deg);
}
#square2{
	position:absolute;
	width:1000px;
	height:1000px;
	top:400px;
	left:600px;
	background-color:#00FF00;
	transform:translate(-50%,-50%) rotateX(90deg) translateZ(-100px);
}
#pawn{
	display:none;
	position:absolute;
	top:400px;
	left:600px;
	transform:translate(-50%,-50%);
	width:100px;
	height:100px;
	background-color:#0000FF;
}

script.js:

// Мировые константы

var pi = 3.141592;
var deg = pi/180;

// Конструктор Pawn

function player(x,y,z,rx,ry) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.rx = rx;
	this.ry = ry;
}

// Нажата ли клавиша и двигается ли мышь?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;

// На земле ли игрок?

var onGround = true;

// Обработчик нажатия клавиш

document.addEventListener("keydown", (event) =>{
	if (event.key == "a"){
		PressLeft = 1;
	}
	if (event.key == "w"){
		PressForward = 1;
	}
	if (event.key == "d"){
		PressRight = 1;
	}
	if (event.key == "s"){
		PressBack = 1;
	}
	if (event.keyCode == 32 && onGround){
		PressUp = 1;
	}
});

// Обработчик отжатия клавиш

document.addEventListener("keyup", (event) =>{
	if (event.key == "a"){
		PressLeft = 0;
	}
	if (event.key == "w"){
		PressForward = 0;
	}
	if (event.key == "d"){
		PressRight = 0;
	}
	if (event.key == "s"){
		PressBack = 0;
	}
	if (event.keyCode == 32){
		PressUp = 0;
	}
});

// Обработчик движения мыши

document.addEventListener("mousemove", (event)=>{
	MouseX = event.movementX;
	MouseY = event.movementY;
});


// Создаем новый объект типа player

var pawn = new player(0,0,0,0,0);

// Привяжем новую переменную к world

var world = document.getElementById("world");

function update(){
	
	// Задаем локальные переменные смещения
	
	let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
	let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;
	
	// Обнулим смещения мыши:
	
	MouseX = MouseY = 0;
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	pawn.rx = pawn.rx + drx;
	pawn.ry = pawn.ry + dry;

	
	// Изменяем координаты мира (для отображения)
	
	world.style.transform = 
	"translateZ(600px)" +
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

TimerGame = setInterval(update,10);

С движением мы почти разобрались. Но осталось неудобство: курсор мыши может двигаться только в пределах экрана. В трехмерных шутерах можно вращать мышью сколь угодно долго и сколь угодно далеко. Сделаем также: при нажатии на экран игры (на “container”) курсор будет пропадать, и мы сможем вращать мышью без ограничений на размер экрана. Активируем захват мыши при нажатии на экран, для чего перед обработчиками нажатия клавиш поставим обработчик нажатия мыши на “container”:

// Привяжем новую переменную к container

var container = document.getElementById("container");

// Обработчик захвата курсора мыши

container.onclick = function(){
	container.requestPointerLock();
};

Теперь совсем другое дело. Однако лучше вообще сделать так, чтобы вращение производилось только тогда, когда курсор захвачен. Введем новую переменную после переменных нажатия клавиш press…

// Введен ли захват мыши?

var lock = false;

Добавим обработчик изменения состояния захвата курсора (захвачен или нет) перед обработчиком захвата курсора (извините за тавтологию):

// Обработчик изменения состояния захвата курсора

document.addEventListener("pointerlockchange", (event)=>{
	lock = !lock;
});

А в update() добавим условие вращения “pawn”:

// Если курсор захвачен, разрешаем вращение

	if (lock){
		pawn.rx = pawn.rx + drx;
		pawn.ry = pawn.ry + dry;
	};

А сам захват мыши при клике по контейнеру разрешим только тогда, когда курсор еще не захвачен:

// Обработчик захвата курсора мыши

container.onclick = function(){
	if (!lock) container.requestPointerLock();
};

С движением мы полностью разобрались. Перейдем к генерации мира

4. Загрузка карты

Мир в нашем случае удобнее всего представить в виде множества прямоугольников, имеющих разное местоположение, поворот, размеры и цвет. Вместо цвета также можно использовать текстуры. На самом деле, все современные трехмерные миры в играх – это набор треугольников и прямоугольников, которые называют полигонами. В крутых играх их количество может достигать десятков тысяч в одном только кадре. У нас же их будет около сотни, так как браузер сам по себе имеет невысокую графическую производительность. В предыдущих пунктах мы вставляли блоки “div” внутрь “world”. Но если таких блоков много (сотни), то вставлять каждый из них в контейнер очень утомительно. Да и уровней может быть много. Поэтому пусть эти прямоугольники вставляет javaScript, а не мы. Для него же мы будем создавать специальный массив.

Откроем index.html и удалим из блока “world” все внутренние блоки:

<BODY>
	<div id="container">
		<div id="world"></div>
		<div id="pawn"></div>
	</div>
</BODY>

Как видим, в “world” теперь ничего нет. В style.css удалим стили для #square1 и #square2 (вообще удалим #square1 и #square2 из этого файла), а вместо них создадим стили для класса .square, который будет общим для всех прямоугольников. Причем зададим для него только одно свойство:


.square{
	position:absolute;
}

Теперь создадим массив прямоугольников (запихнем его, примеру, между конструктором player и переменными press… в script.js):

// Массив прямоугольников

var map = [
		   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],
		   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],
		   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],
		   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],
		   [0,100,0,90,0,0,2000,2000,"#666666"]
]

Можно было это сделать в виде конструктора, но пока обойдемся чисто массивом, так как запуск цикла расстановки прямоугольников проще реализовать именно через массивы, а не через конструкторы. Я же поясню, что означают цифры в нем. Массив map содержит одномерные массивы из 9 переменных: [,,,,,,,,]. Я думаю, вы понимаете, что первые три числа – это координаты центра прямоугольника, вторые три числа – углы поворота в градусах (относительно того же центра), затем два числа – его размеры и последнее число – фон. Причем фон может быть сплошным цветом, градиентом или фотографией. Последнее очень удобно использовать в качестве текстур.

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

function CreateNewWorld(){
	for (let i = 0; i < map.length; i++){
		
		// Создание прямоугольника и придание ему стилей
		
		let newElement = document.createElement("div");
		newElement.className = "square";
		newElement.id = "square" + i;
		newElement.style.width = map[i][6] + "px";
		newElement.style.height = map[i][7] + "px";
		newElement.style.background = map[i][8];
		newElement.style.transform = "translate3d(" +
                (600 - map[i][6]/2 + map[i][0]) + "px," +
		(400 - map[i][7]/2 + map[i][1]) + "px," +
		(map[i][2]) + "px)" +
		"rotateX(" + map[i][3] + "deg)" +
		"rotateY(" + map[i][4] + "deg)" +
		"rotateZ(" + map[i][5] + "deg)";
		
		// Вставка прямоугольника в world
		
		world.append(newElement);
	}
}

Поясню, что происходит: мы создаем новую переменную, которая указывает на только что созданный элемент. Ему мы присваиваем id и css-класс (именно это и имеется ввиду под словом класс в языке javaScript), задаем ширину с высотой, фон и трансформацию. Примечательно, что в трансформации помимо координат центра прямоугольника мы указываем смещение на 600 и 400 и половины размеров для того, чтобы центр прямоугольника точно оказался в точке с нужными координатами. Запустим генератор мира перед таймером:

CreateNewWorld();
TimerGame = setInterval(update,10);

Теперь мы видим площадку с розовыми стенами и серым полом. Как видите, создание карты технически несложно реализовать. А в результате ваш код в трех файлах должен получиться примерно таким:

index.html:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world"></div>
		<div id="pawn"></div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

style.css

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
	perspective:600px;
	overflow:hidden;
}
#world{
	position:absolute;
	width:inherit;
	height:inherit;
	transform-style:preserve-3d;
}
.square{
	position:absolute;
}
#pawn{
	display:none;
	position:absolute;
	top:400px;
	left:600px;
	transform:translate(-50%,-50%);
	width:100px;
	height:100px;
}

script.js:

// Мировые константы

var pi = 3.141592;
var deg = pi/180;

// Конструктор player

function player(x,y,z,rx,ry) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.rx = rx;
	this.ry = ry;
}

// Массив прямоугольников

var map = [
		   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],
		   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],
		   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],
		   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],
		   [0,100,0,90,0,0,2000,2000,"#666666"]
]

// Нажата ли клавиша и двигается ли мышь?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;

// Введен ли захват мыши?

var lock = false;

// На земле ли игрок?

var onGround = true;

// Привяжем новую переменную к container

var container = document.getElementById("container");

// Обработчик изменения состояния захвата курсора

document.addEventListener("pointerlockchange", (event)=>{
	lock = !lock;
});

// Обработчик захвата курсора мыши

container.onclick = function(){
	if (!lock) container.requestPointerLock();
};

// Обработчик нажатия клавиш

document.addEventListener("keydown", (event) =>{
	if (event.key == "a"){
		PressLeft = 1;
	}
	if (event.key == "w"){
		PressForward = 1;
	}
	if (event.key == "d"){
		PressRight = 1;
	}
	if (event.key == "s"){
		PressBack = 1;
	}
	if (event.keyCode == 32 && onGround){
		PressUp = 1;
	}
});

// Обработчик отжатия клавиш

document.addEventListener("keyup", (event) =>{
	if (event.key == "a"){
		PressLeft = 0;
	}
	if (event.key == "w"){
		PressForward = 0;
	}
	if (event.key == "d"){
		PressRight = 0;
	}
	if (event.key == "s"){
		PressBack = 0;
	}
	if (event.keyCode == 32){
		PressUp = 0;
	}
});

// Обработчик движения мыши

document.addEventListener("mousemove", (event)=>{
	MouseX = event.movementX;
	MouseY = event.movementY;
});

// Создаем новый объект

var pawn = new player(0,0,0,0,0);

// Привяжем новую переменную к world

var world = document.getElementById("world");

function update(){
	
	// Задаем локальные переменные смещения
	
	let dx =   (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
	let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;
	
	// Обнулим смещения мыши:
	
	MouseX = MouseY = 0;
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	
	// Если курсор захвачен, разрешаем вращение
	
	if (lock){
		pawn.rx = pawn.rx + drx;
		pawn.ry = pawn.ry + dry;
	};

	// Изменяем координаты мира (для отображения)
	
	world.style.transform = 
	"translateZ(" + (600 - 0) + "px)" +
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

function CreateNewWorld(){
	for (let i = 0; i < map.length; i++){
		
		// Создание прямоугольника и придание ему стилей
		
		let newElement = document.createElement("div");
		newElement.className = "square";
		newElement.id = "square" + i;
		newElement.style.width = map[i][6] + "px";
		newElement.style.height = map[i][7] + "px";
		newElement.style.background = map[i][8];
		newElement.style.transform = "translate3d(" +
		(600 - map[i][6]/2 + map[i][0]) + "px," +
		(400 - map[i][7]/2 + map[i][1]) + "px," +
		                    (map[i][2]) + "px)" +
		"rotateX(" + map[i][3] + "deg)" +
		"rotateY(" + map[i][4] + "deg)" +
		"rotateZ(" + map[i][5] + "deg)";
		
		// Вставка прямоугольника в world
		
		world.append(newElement);
	}
}

CreateNewWorld();
TimerGame = setInterval(update,10);

Если все хорошо, переходим к следующему пункту.

5. Столкновения игрока с объектами мира

Мы создали технику движения, генератор мира из массива. Мы можем передвигаться по миру, который может быть красивым. Однако наш игрок еще никак не взаимодействует с ним. Чтобы это взаимодействие происходило, нам необходимо проверять, сталкивается ли игрок с каким-нибудь прямоугольником или нет? То есть, мы будем проверять наличие коллизий. Для начала вставим пустую функцию:

function collision(){
	
}

А вызывать ее будем в update():

// Обнулим смещения мыши:
	
	MouseX = MouseY = 0;
	
	// Проверяем коллизию с прямоугольниками
	
	collision();

Как это происходит? Представим себе, что игрок – это шар с радиусом r. И он движется в сторону прямоугольника:

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

function coorTransform(x0,y0,z0,rxc,ryc,rzc){
	let x1 =  x0;
	let y1 =  y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);
	let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);
	let x2 =  x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);
	let y2 =  y1;
	let z2 =  x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);
	let x3 =  x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg);
 	let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);
	let z3 =  z2;
	return [x3,y3,z3];
}

И обратную функцию:

function coorReTransform (x3,y3,z3,rxc,ryc,rzc){
	let x2 =  x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);
	let y2 =  x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);
	let z2 =  z3
	let x1 =  x2*Math.cos(ryc*deg) + z2*Math.sin(ryc*deg);
	let y1 =  y2;
	let z1 = -x2*Math.sin(ryc*deg) + z2*Math.cos(ryc*deg);
	let x0 =  x1;
	let y0 =  y1*Math.cos(rxc*deg) - z1*Math.sin(rxc*deg);
	let z0 =  y1*Math.sin(rxc*deg) + z1*Math.cos(rxc*deg);
	return [x0,y0,z0];
}

Вставим эти функции после функции update(). Я не буду объяснять, как это работает, потому что мне не хочется рассказывать курс аналитической геометрии. Скажу, что есть такие формулы перевода координат при вращении и мы просто ими воспользовались. С точки зрения прямоугольника наш игрок расположен вот так:

В этом случае условие коллизии становится таким: если после смещения шара на величину v (v – это вектор) координата z между –r и r, а координаты x и y лежат в пределах прямоугольника или отстоят от него на величину, не большую r, то объявляется коллизия. В этом случае координата игрока по z после смещения будет составлять r или – r (в зависимости от того, с какой стороны придет игрок). В соответствии с этим, смещение игрока изменяется. Мы специально вызываем коллизию перед тем, как в update() координаты игрока будут обновлены, чтобы вовремя изменить смещение. Таким образом, шар никогда не пересечется с прямоугольником, как бывает в других алгоритмах коллизии. Хотя физически игрок будет представлять собой, скорее, случае куб, мы не будем обращать на это внимание. Итак, реализуем это в javaScript:

function collision(){
	for(let i = 0; i < map.length; i++){
		
		// рассчитываем координаты игрока в системе координат прямоугольника
		
		let x0 = (pawn.x - map[i][0]);
		let y0 = (pawn.y - map[i][1]);
		let z0 = (pawn.z - map[i][2]);
		
		let x1 = x0 + dx;
		let y1 = y0 + dy;
		let z1 = z0 + dz;
		
		let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
		let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
		let point2 = new Array();
		
		// Условие коллизии и действия при нем
		
		if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
			point1[2] = Math.sign(point0[2])*50;
			point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
			dx = point2[0] - x0;
			dy = point2[1] - y0;
			dz = point2[2] - z0;
		}
	};
}

x0,y0 и z0 – начальные координаты игрока в системе координат прямоугольника (без поворотов. x1,y1 и z1 – координаты игрока после смещения без учета коллизии. point0, point0, point1 и point2 – начальный радиус-вектор, радиус-вектор после смещения без коллизии и радиус-вектор с коллизией соответственно. map[i][3] и другие, если вы помните, это углы поворота прямоугольника. Заметим, что в условии мы к размерам прямоугольника прибавляем не 100, а 98. Это костыль, зачем, подумайте сами. Запустите игру и вы увидите довольно качественные столкновения.

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


if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][1]**2 + map[i][2]**2)){
		
			let x1 = x0 + dx;
			let y1 = y0 + dy;
			let z1 = z0 + dz;
		
			let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
			let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
			let point2 = new Array();
		
			// Условие коллизии и действия при нем
		
			if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
				point1[2] = Math.sign(point0[2])*50;
				point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
				dx = point2[0] - x0;
				dy = point2[1] - y0;
				dz = point2[2] - z0;
			}
			
		} 

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

index.html:

<!DOCTYPE HTML>
<HTML>
<HEAD>
	<TITLE>Игра</TITLE>
	<LINK rel="stylesheet" href="style.css">
	<meta charset="utf-8">
</HEAD>
<BODY>
	<div id="container">
		<div id="world"></div>
		<div id="pawn"></div>
	</div>
</BODY>
</HTML>
<script src="script.js"></script>

style.css

#container{
	position:absolute;
	width:1200px;
	height:800px;
	border:2px solid #000000;
	perspective:600px;
	overflow:hidden;
}
#world{
	position:absolute;
	width:inherit;
	height:inherit;
	transform-style:preserve-3d;
}
.square{
	position:absolute;
}
#pawn{
	display:none;
	position:absolute;
	top:400px;
	left:600px;
	transform:translate(-50%,-50%);
	width:100px;
	height:100px;
}

script.js:

// Мировые константы

var pi = 3.141592;
var deg = pi/180;

// Конструктор player

function player(x,y,z,rx,ry) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.rx = rx;
	this.ry = ry;
}

// Массив прямоугольников

var map = [
		   [0,0,1000,0,180,0,2000,200,"#F0C0FF"],
		   [0,0,-1000,0,0,0,2000,200,"#F0C0FF"],
		   [1000,0,0,0,-90,0,2000,200,"#F0C0FF"],
		   [-1000,0,0,0,90,0,2000,200,"#F0C0FF"],
		   [0,100,0,90,0,0,2000,2000,"#666666"]
];

// Нажата ли клавиша и двигается ли мышь?

var PressBack = 0;
var PressForward = 0;
var PressLeft = 0;
var PressRight = 0;
var PressUp = 0;
var MouseX = 0;
var MouseY = 0;

// Введен ли захват мыши?

var lock = false;

// На земле ли игрок?

var onGround = true;

// Привяжем новую переменную к container

var container = document.getElementById("container");

// Обработчик изменения состояния захвата курсора

document.addEventListener("pointerlockchange", (event)=>{
	lock = !lock;
});

// Обработчик захвата курсора мыши

container.onclick = function(){
	if (!lock) container.requestPointerLock();
};

// Обработчик нажатия клавиш

document.addEventListener("keydown", (event) =>{
	if (event.key == "a"){
		PressLeft = 1;
	}
	if (event.key == "w"){
		PressForward = 1;
	}
	if (event.key == "d"){
		PressRight = 1;
	}
	if (event.key == "s"){
		PressBack = 1;
	}
	if (event.keyCode == 32 && onGround){
		PressUp = 1;
	}
});

// Обработчик отжатия клавиш

document.addEventListener("keyup", (event) =>{
	if (event.key == "a"){
		PressLeft = 0;
	}
	if (event.key == "w"){
		PressForward = 0;
	}
	if (event.key == "d"){
		PressRight = 0;
	}
	if (event.key == "s"){
		PressBack = 0;
	}
	if (event.keyCode == 32){
		PressUp = 0;
	}
});

// Обработчик движения мыши

document.addEventListener("mousemove", (event)=>{
	MouseX = event.movementX;
	MouseY = event.movementY;
});

// Создаем новый объект

var pawn = new player(-900,0,-900,0,0);

// Привяжем новую переменную к world

var world = document.getElementById("world");

function update(){
	
	// Задаем локальные переменные смещения
	
	dx =   (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
	dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
	dy = - PressUp;
	drx = MouseY;
	dry = - MouseX;
	
	// Обнулим смещения мыши:
	
	MouseX = MouseY = 0;
	
	// Проверяем коллизию с прямоугольниками
	
	collision();
	
	// Прибавляем смещения к координатам
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	console.log(pawn.x + ":" + pawn.y + ":" + pawn.z);
	
	// Если курсор захвачен, разрешаем вращение
	
	if (lock){
		pawn.rx = pawn.rx + drx;
		pawn.ry = pawn.ry + dry;
	};

	// Изменяем координаты мира (для отображения)
	
	world.style.transform = 
	"translateZ(" + (600 - 0) + "px)" +
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

function CreateNewWorld(){
	for (let i = 0; i < map.length; i++){
		
		// Создание прямоугольника и придание ему стилей
		
		let newElement = document.createElement("div");
		newElement.className = "square";
		newElement.id = "square" + i;
		newElement.style.width = map[i][6] + "px";
		newElement.style.height = map[i][7] + "px";
		newElement.style.background = map[i][8];
		newElement.style.transform = "translate3d(" +
		(600 - map[i][6]/2 + map[i][0]) + "px," +
		(400 - map[i][7]/2 + map[i][1]) + "px," +
		(map[i][2]) + "px)" +
		"rotateX(" + map[i][3] + "deg)" +
		"rotateY(" + map[i][4] + "deg)" +
		"rotateZ(" + map[i][5] + "deg)";
		
		// Вставка прямоугольника в world
		
		world.append(newElement);
	}
}

function collision(){
	for(let i = 0; i < map.length; i++){
		
		// рассчитываем координаты игрока в системе координат прямоугольника
		
		let x0 = (pawn.x - map[i][0]);
		let y0 = (pawn.y - map[i][1]);
		let z0 = (pawn.z - map[i][2]);
		
		if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][6]**2 + map[i][7]**2)){
		
			let x1 = x0 + dx;
			let y1 = y0 + dy;
			let z1 = z0 + dz;
		
			let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
			let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
			let point2 = new Array();
		
			// Условие коллизии и действия при нем
		
			if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
				point1[2] = Math.sign(point0[2])*50;
				point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
				dx = point2[0] - x0;
				dy = point2[1] - y0;
				dz = point2[2] - z0;
			}
			
		}
	};
}

function coorTransform(x0,y0,z0,rxc,ryc,rzc){
	let x1 =  x0;
	let y1 =  y0*Math.cos(rxc*deg) + z0*Math.sin(rxc*deg);
	let z1 = -y0*Math.sin(rxc*deg) + z0*Math.cos(rxc*deg);
	let x2 =  x1*Math.cos(ryc*deg) - z1*Math.sin(ryc*deg);
	let y2 =  y1;
	let z2 =  x1*Math.sin(ryc*deg) + z1*Math.cos(ryc*deg);
	let x3 =  x2*Math.cos(rzc*deg) + y2*Math.sin(rzc*deg);
 	let y3 = -x2*Math.sin(rzc*deg) + y2*Math.cos(rzc*deg);
	let z3 =  z2;
	return [x3,y3,z3];
}

function coorReTransform(x3,y3,z3,rxc,ryc,rzc){
	let x2 =  x3*Math.cos(rzc*deg) - y3*Math.sin(rzc*deg);
	let y2 =  x3*Math.sin(rzc*deg) + y3*Math.cos(rzc*deg);
	let z2 =  z3
	let x1 =  x2*Math.cos(ryc*deg) + z2*Math.sin(ryc*deg);
	let y1 =  y2;
	let z1 = -x2*Math.sin(ryc*deg) + z2*Math.cos(ryc*deg);
	let x0 =  x1;
	let y0 =  y1*Math.cos(rxc*deg) - z1*Math.sin(rxc*deg);
	let z0 =  y1*Math.sin(rxc*deg) + z1*Math.cos(rxc*deg);
	return [x0,y0,z0];
}

CreateNewWorld();
TimerGame = setInterval(update,10);

К браузерным играм не привыкли относиться всерьез, но на самом деле – это неплохой бизнес для небольшой команды друзей, у которых нет денег, но есть светлые мысли. А если еще и деньги есть, то тогда все значительно упрощается.

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

Как создать браузерную игру ничего не изучая?

Ответ – никак. Для начала нужно будет выучить HTML – язык разметки, который работает в связке с каскадной таблицей стилей – CSS. Эти языки располагают текст и изображения на странице, собирая картинку из множества фрагментов. Поскольку вы намерены сделать именно браузерную игру, то ей обязательно понадобится сайт, желательно красивый и функциональный. Поэтому не обойтись без JavaScript – самого простого способа визуализировать вашу игру на стороне геймера. Этот язык позволяет менять местами элементы страницы без ее перезагрузки. По сути, JavaScript управляет элементами CSS и HTML в реальном времени.

Для создания браузерной игры придется выучить HTML, CSS, PHP и JavaScript, так что бессонные ночи вам гарантированы

Язык программирования PHP нужен для создания движка игры. Он используется на сервере для обработки данных, поступающих от разных геймеров. Это – ядро, которое приводит в действие все внутренние механизмы.

Я не хочу ничего учить, но у меня есть деньги

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

  • Программист – 2 штуки
  • Дизайнер и художник – 2 штуки
  • Гейм-дизайнер – 1 штука
  • Комьюнити-менеджер – 1 штука
  • Гейм-мастер – 1 штука.

Если у вас нет личных знакомств, то придется обращаться к аутсорсингу. Обычно такие ребята трутся на биржах фрилансеров, самой популярной их которых является www.fl.ru/.

Всех необходимых работников можно найти на биржах фрилансеров

Правда, в этом случае нужно быть очень осторожным, поскольку фрилансеры привыкли регулярно срывать сроки. Как говорят опытные руководители, можно бесконечно смотреть на 3 вещи: как течет память, как горит дедлайн и как не работает фрилансер.

Во сколько обойдется разработка?

Во столько, сколько у вас есть денег. Вот здесь приведены примерные выкладки и структура расходов на создание среднестатистической браузерки. Итоговая сумма – 25 миллионов рублей. Однако не стоит пугаться этой цифры. В статье автор посчитал расходы на разработку по максимуму, так что при желании эту сумму модно уменьшить в разы и даже десятки раз. Или увеличить, если ваш папа – прокурор. Безусловно, такие проекты, как Drakensang Online или City of Steam требуют сумм как минимум с шестью нулями, но простую табличную браузерку можно сделать за пару десятков тысяч рублей.

В конце концов, вы всегда можете обратиться к краудфандингу. Пути Кикстартера неисповедимы, взлететь там может любой проект. Главное правильно подать блюдо.

Этапы разработки

Создание браузерной игры с нуля можно разделить на несколько этапов:

1. Поиск идеи

Поскольку у вас вряд ли есть 25 миллионов рублей и вы вряд ли являетесь гением в программировании и дизайне, единственное, чем вы можете зацепить пользователя, это необычный сеттинг. Даже пресловутая кампания из эльфов, орков, людей и гномов способна увлечь клиента, если погрузить их в необычную среду или заставить делать странные вещи. Например, игра Ботва Онлайн была написана «по приколу» четырьмя людьми в свободное от работы время, но благодаря своему необычному сеттингу быстро стала успешным бизнес проектом.

2. Разработка сюжета

Сюжет очень важен (если конечно, вы не делаете стратегию). Если у самого воображения не хватает, наймите кого-нибудь, у кого оно есть. В крайнем случае, обратитесь к аутсорсингу и найдите толкового копирайтера, у которого уже есть опыт создания квестов.

3. Создание игры

Даже если вы выучили вдоль и поперек JavaScript и CSS, вы все еще ничего не знаете о том, как создать браузерную игру. Это примерно то же самое, как если бы вы выучили теорию плавания, ни разу не побывав в бассейне. Лучший выход в этом случае – воспользоваться самоучителем. Вот здесь можно многое узнать о разработке игр, причем не только браузерных.

Скорее всего, ваша первая браузерка будет выглядеть так. Это – легендарный Бойцовский Клуб

4. Продвижение проекта

Последний этап, на котором дело в свои руки берут комьюнити-менеджер и специалист по маркетингу (если таковой имеется). В крупных компаниях на долю рекламы уходит примерно 60% бюджета, поэтому на эту статью денег лучше не жалеть. Если же ваши финансы поют романсы, то тогда не остается ничего другого, как самому перепахивать игровые форумы и завлекать геймеров всеми возможными способами.

Как создать браузерную игру с помощью конструктора

Оказывается, ушлые программисты уже давно создали кучу конструкторов MMO, которые могут помочь новичку создать браузерную онлайн игру бесплатно. Это не значит, что вы можете успокоиться и удалить все закладки по программированию, но с помощью конструктора можно здорово ускорить процесс.

Популярный игровой конструктор Construct 2

Большой популярностью пользуется MMO Constructor – отечественный продукт, в котором можно создать все элементы полноценной браузерной RPG. Взамен авторы требуют совсем ничего – 50% от прибыли проекта. Разобраться в конструкторе непросто, но добрые люди уже написали гайды. Также чтобы создать браузерную игру, можно воспользоваться такими программами, как Construct Classic, Eclipse, FPS Creator.

Больше информации о конструкторах можно узнать на форуме Gcup, где тусуются все: и продвинутые геймдевы, и зеленые новички.

Так что там насчет миллиона?

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

  • Не жадничайте! Более 80% игроков вообще не платят, либо платят копейки. Однако они создают массовку, на которую подтягивается крупная рыба. Сконцентрируйтесь на ней. Остальным дайте возможность резвиться бесплатно.
  • Постарайтесь сделать так, чтобы донаторы не сильно выделялись из общей массы, иначе масса уйдет, оставив вас без донаторов и без денег. В общем, бряцайте мускулами аккуратно.
  • Лучше всего тратят деньги те, кто не хочет тратить свое время, поэтому монетизируйте наиболее рутинные операции. Не разменивайтесь по мелочам.
  • Уроки истории создания игр показывают, то онлайн-проект должен быть бесконечным. Если у геймера появится чувство, что ему нечего делать — он уйдет, а туда, где нет онлайна, новый геймер не придет никогда.

Напоследок хочется сказать – не так страшен черт, как его малюют. Даже лучшие геймдевы когда-то начинали с нуля, и кто знает, может быть именно вы станете следующим Джоном Кармаком или Ричардом Гэрриотом?

Полезные ссылки:

  • Движки для создания браузерных игр
  • Раздел «Разработка игр» на бирже фриланса
  • Список движков на HTML 5
  • Игровой движок Unity

#Руководства

  • 11 окт 2019

  • 14

Современные браузеры позволяют создавать игры с полноценной графикой. Рассказываем, как написать простые гонки на JavaScript и HTML5.

 vlada_maestro / shutterstock

Евгений Кучерявый

Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.

Сейчас браузеры дают JavaScript-разработчикам огромное количество возможностей для создания интересных сайтов. Раньше для этого использовался Flash — он был популярен, и на нём было создано бессчётное количество игр, плееров, необычных интерфейсов и так далее. Однако они уже не запустятся ни в одном современном браузере.

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

Canvas — это холст, на котором можно рисовать с помощью JS-команд. Его можно использовать для создания анимированных фонов, различных конструкторов и, самое главное, игр.

Из этой статьи вы узнаете, как создать браузерную игру на JavaScript и HTML5. Но прежде рекомендуем ознакомиться с объектно-ориентированным программированием в JS (достаточно понимать, что такое класс, метод и объект). Оно лучше всего подходит для создания игр, потому что позволяет работать с сущностями, а не с абстрактными данными. Однако есть и недостаток: ООП не поддерживается ни в одной из версий Internet Explorer.

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

<!DOCTYPE html>
<html>
    <head>
        <title>JS Game</title>
        <link rel="stylesheet" href="style.css">
        <meta charset="utf-8">
    </head>
    <body>
        <div class="wrapper">
            <canvas width="0" height="0" class="canvas" id="canvas">Ваш браузер не поддерживает JavaScript и HTML5!</canvas>
        </div>
        <script src="game.js"></script>
    </body>
</html>

Теперь нужно добавить стили:

body, html
{
    width: 100%;
    height: 100%;
    padding: 0px;
    margin: 0px;
    overflow: hidden;
}
 
.wrapper
{
    width: 100%;
    height: 100%;
}
 
.canvas
{
    width: 100%;
    height: 100%;
    background: #000;
}

Обратите внимание, что в HTML элементу canvas были заданы нулевые ширина и высота, в то время как в CSS указано 100%. В этом плане холст ведёт себя как изображение. У него есть фактическое и видимое разрешение.

С помощью стилей меняется видимое разрешение. Однако при этом размеры картинки останутся прежними: она просто растянется или сожмётся. Поэтому фактические ширина и высота будут указаны позже — через скрипт.

Для начала добавим заготовку скрипта для игры:

var canvas = document.getElementById("canvas"); //Получение холста из DOM
var ctx = canvas.getContext("2d"); //Получение контекста — через него можно работать с холстом
 
var scale = 0.1; //Масштаб машин
 
Resize(); // При загрузке страницы задаётся размер холста
 
window.addEventListener("resize", Resize); //При изменении размеров окна будут меняться размеры холста
 
window.addEventListener("keydown", function (e) { KeyDown(e); }); //Получение нажатий с клавиатуры
 
var objects = []; //Массив игровых объектов
var roads = []; //Массив с фонами
 
var player = null; //Объект, которым управляет игрок, — тут будет указан номер объекта в массиве objects
 
function Start()
{
    timer = setInterval(Update, 1000 / 60); //Состояние игры будет обновляться 60 раз в секунду — при такой частоте обновление происходящего будет казаться очень плавным
}
 
function Stop()
{
    clearInterval(timer); //Остановка обновления
}
 
function Update() //Обновление игры
{
    Draw();
}
 
function Draw() //Работа с графикой
{
    ctx.clearRect(0, 0, canvas.width, canvas.height); //Очистка холста от предыдущего кадра
}
 
function KeyDown(e)
{
    switch(e.keyCode)
    {
        case 37: //Влево
            break;
 
        case 39: //Вправо
            break;
 
        case 38: //Вверх
            break;
 
        case 40: //Вниз
            break;
 
        case 27: //Esc
            break;
    }
}
 
function Resize()
{
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}

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

Во время вызова функции Update() будут меняться состояния игровых объектов. После этого они отрисовываются на canvas с помощью функции Draw(). То есть на самом деле мы не двигаем объекты на холсте — мы рисуем их один раз, потом меняем координаты, стираем старое изображение и выводим объекты с новыми координатами. Всё это происходит так быстро, что создаётся иллюзия движения.

Рассмотрим это на примере дороги.

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

Для этого создадим класс Road:

class Road
{
    constructor(image, y)
    {
        this.x = 0;
        this.y = y;
 
        this.image = new Image();
        
        this.image.src = image;
    }
 
    Update(road) 
    {
        this.y += speed; //При обновлении изображение смещается вниз
 
        if(this.y > window.innerHeight) //Если изображение ушло за край холста, то меняем положение
        {
            this.y = road.y - this.image.height + speed; //Новое положение указывается с учётом второго фона
        }
    }
}

В массив с фонами добавляются два объекта класса Road:

var roads = 
[
    new Road("images/road.jpg", 0),
    new Road("images/road.jpg", 626)
]; //Массив с фонами

Теперь можно изменить функцию Update(), чтобы положение изображений менялось с каждым кадром.

function Update() //Обновление игры
{
    roads[0].Update(roads[1]);
    roads[1].Update(roads[0]);
 
    Draw();
}

Остаётся только добавить вывод этих изображений:

function Draw() //Работа с графикой
{
    ctx.clearRect(0, 0, canvas.width, canvas.height); //Очистка холста от предыдущего кадра
 
    for(var i = 0; i < roads.length; i++)
    {
        ctx.drawImage
        (
            roads[i].image, //Изображение для отрисовки
            0, //Начальное положение по оси X на изображении
            0, //Начальное положение по оси Y на изображении
            roads[i].image.width, //Ширина изображения
            roads[i].image.height, //Высота изображения
            roads[i].x, //Положение по оси X на холсте
            roads[i].y, //Положение по оси Y на холсте
            canvas.width, //Ширина изображения на холсте
            canvas.width //Так как ширина и высота фона одинаковые, в качестве высоты указывается ширина
        );
    }
}

Теперь можно посмотреть, как это работает в игре:

Пора добавить игрока и NPC. Для этого нужно написать класс Car. В нём будет метод Move(), с помощью которого игрок управляет своим автомобилем. Движение NPC будет осуществляться с помощью Update(), в котором просто меняется координата Y.

class Car
{
    constructor(image, x, y)
    {
        this.x = x;
        this.y = y;
 
        this.image = new Image();
 
        this.image.src = image;
    }
 
    Update()
    {
        this.y += speed;
    }
 
    Move(v, d) 
    {
        if(v == "x") //Перемещение по оси X
        {
            this.x += d; //Смещение
 
            //Если при смещении объект выходит за края холста, то изменения откатываются
            if(this.x + this.image.width * scale > canvas.width)
            {
                this.x -= d; 
            }
    
            if(this.x < 0)
            {
                this.x = 0;
            }
        }
        else //Перемещение по оси Y
        {
            this.y += d;
 
            if(this.y + this.image.height * scale > canvas.height)
            {
                this.y -= d;
            }
 
            if(this.y < 0)
            {
                this.y = 0;
            }
        }
        
    }
}

Создадим первый объект, чтобы проверить.

var objects = 
[
    new Car("images/car.png", 15, 10)
]; //Массив игровых объектов
var player = 0; //номер объекта, которым управляет игрок

Теперь в функцию Draw() нужно добавить команду отрисовки автомобилей.

for(var i = 0; i < objects.length; i++)
{
    ctx.drawImage
    (
        objects[i].image, //Изображение для отрисовки
        0, //Начальное положение по оси X на изображении
        0, //Начальное положение по оси Y на изображении
        objects[i].image.width, //Ширина изображения
        objects[i].image.height, //Высота изображения
        objects[i].x, //Положение по оси X на холсте
        objects[i].y, //Положение по оси Y на холсте
        objects[i].image.width * scale, //Ширина изображения на холсте, умноженная на масштаб
        objects[i].image.height * scale //Высота изображения на холсте, умноженная на масштаб
    );
}

В функцию KeyDown(), которая вызывается при нажатии на клавиатуру, нужно добавить вызов метода Move().

function KeyDown(e)
{
    switch(e.keyCode)
    {
        case 37: //Влево
            objects[player].Move("x", -speed);
            break;
 
        case 39: //Вправо
            objects[player].Move("x", speed);
            break;
 
        case 38: //Вверх
            objects[player].Move("y", -speed);
            break;
 
        case 40: //Вниз
            objects[player].Move("y", speed);
            break;
 
        case 27: //Esc
            if(timer == null)
            {
                Start();
            }
            else
            {
                Stop();
            }
            break;
    }
}

Теперь можно проверить отрисовку и управление.

Следующий шаг — добавление машин. Они будут создаваться на ходу и удаляться, когда зайдут за край.

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

function RandomInteger(min, max) 
{
    let rand = min - 0.5 + Math.random() * (max - min + 1);
    return Math.round(rand);
}

С её помощью в функции Update() с определённой вероятностью будет создаваться объект и добавляться в массив objects:

if(RandomInteger(0, 10000) > 9700)
{
    objects.push(new Car("images/car_red.png", RandomInteger(30, canvas.width - 50), RandomInteger(250, 400) * -1));
}

Теперь можно увидеть, как новые машины появляются вверху экрана:

При столкновении пока ничего не происходит, но это будет исправлено позже. Сначала нужно убедиться, что объекты, которые пропали из виду, удаляются. Это нужно, чтобы не забивать оперативную память.

В класс Car добавляем поле dead со значением false, а потом меняем его в методе Update():

if(this.y > canvas.height + 50)
{
    this.dead = true;
}

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

var hasDead = false;
 
for(var i = 0; i < objects.length; i++)
{
    if(i != player)
    {
        objects[i].Update();
 
        if(objects[i].dead)
        {
            hasDead = true;
        }
    }
}
 
if(hasDead)
{
    objects.shift();
}

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

Теперь можно приступить к реализации коллизии (англ. collision — столкновение). Для это нужно написать для класса Car метод Collide(), в котором будут проверяться координаты машин:

Collide(car)
{
    var hit = false;
 
    if(this.y < car.y + car.image.height * scale && this.y + this.image.height * scale > car.y) //Если объекты находятся на одной линии по горизонтали
    {
        if(this.x + this.image.width * scale > car.x && this.x < car.x + car.image.width * scale) //Если объекты находятся на одной линии по вертикали
        {
            hit = true;
        }
    }
 
    return hit;
}

Теперь нужно в функцию Update() добавить проверку коллизии:

var hit = false;
 
for(var i = 0; i < objects.length; i++)
{
    if(i != player)
    {
        hit = objects[player].Collide(objects[i]);
 
        if(hit)
        {
            alert("Вы врезались!");
            Stop();
            break;
        }
    }
}

Вот что будет в игре:

В момент коллизии можно добавить любую логику:

  • включение анимации;
  • добавление эффекта;
  • удаление объекта;
  • изменение здоровья и так далее.

Всё это остаётся на усмотрение разработчика.

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

Использование canvas хорошо подходит для работы с графикой: он даёт большие возможности и не сильно загружает браузер. Также сейчас разработчикам доступна библиотека WebGL (примеры и использование), с помощью которой можно значительно повысить производительность и работать с 3D (canvas так не может).

Разобраться в WebGL может быть непросто — вероятно, вместо этого многим интереснее попробовать движок Unity, который умеет компилировать проекты для их запуска в браузере.

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

Участвовать

Рассказывает Алвин Лин, разработчик программного обеспечения из Нью-Йорка


В 2014 году я впервые побывал на CodeDay в Нью-Йорке. И хотя CodeDay не совсем хакатон, это было моё первое знакомство с подобными мероприятиями. Там мы с моим другом Кеннетом Ли написали многопользовательскую игру в танчики. Так как несколько моих друзей спрашивали меня о том, как я её написал, я решил описать процесс её создания.

В этом посте я вкратце опишу ход своих рассуждений и покажу, как воссоздать архитектуру, а также дам некоторые советы, если вы захотите сделать игру сами. Этот пост рассчитан на тех, кто владеет основами JavaScript и Node.js. Если вы с ними не знакомы, то есть много замечательных онлайн-ресурсов, где можно их изучить.

Прим. перев. На нашем сайте есть много познавательных материалов как по JavaScript, так и по Node.js — обязательно найдёте что-нибудь подходящее.

Бэкенд игры написан на Node.js с использованием веб-сокетов, которые позволяют серверу и клиенту общаться в режиме реального времени. Со стороны клиента игра отображается в HTML5-элементе Canvas. Для начала нам, конечно же, понадобится Node.js. В этой статье описана работа с версией 6.3.1, но вы можете использовать любую версию выше 0.12.

Прим. перев. Если вы не знакомы с веб-сокетами, рекомендуем прочитать наш вводный материал.

Создание проекта

Для начала установите зависимости. Создайте папку проекта, перейдите в неё и запустите следующий код:

npm init
npm install --save express socket.io

Для быстрой настройки сервера целесообразно использовать фреймворк Express, а для обработки веб-сокетов на сервере — пакет socket.io. В файл server.js поместите следующий код:

// Зависимости
var express = require('express');
var http = require('http');
var path = require('path');
var socketIO = require('socket.io');
var app = express();
var server = http.Server(app);
var io = socketIO(server);

app.set('port', 5000);
app.use('/static', express.static(__dirname + '/static'));

// Маршруты
app.get('/', function(request, response) {
    response.sendFile(path.join(__dirname, 'index.html'));
});

// Запуск сервера
server.listen(5000, function() {
    console.log('Запускаю сервер на порте 5000');
});

Это довольно типичный код для сервера на связке Node.js + Express. Он устанавливает зависимости и основные маршруты сервера. Для этого демонстрационного приложения используется только один файл index.html и папка static. Создайте их в корневой папке проекта. Файл index.html довольно простой:

<html>
  <head>
    <title>Наша многопользовательская игра</title>
    <style>
      canvas {
        width: 800px;
        height: 600px;
        border: 5px solid black;
      }
    </style>
    <script src="/socket.io/socket.io.x16217.js"></script>
  </head>
  <body>
    <canvas id="canvas"></canvas>
  </body>
  <script src="/static/game.x16217.js"></script>
</html>

Ваш пользовательский интерфейс может содержать куда больше элементов, поэтому для более крупных проектов CSS-стили лучше помещать в отдельный файл. Для простоты я оставлю CSS в коде HTML. Обратите внимание, что я включил в код скрипт socket.io.js. Он автоматически заработает в рамках пакета socket.io при запуске сервера.

Теперь нужно настроить веб-сокеты на сервере. В конец файла server.js добавьте:

// Обработчик веб-сокетов
io.on('connection', function(socket) {
});

Пока что в игре нет никаких функций, поэтому в обработчик веб-сокетов ничего добавлять не нужно. Для тестирования допишите следующие строки в конец файла server.js:

setInterval(function() {
    io.sockets.emit('message', 'hi!');
}, 1000);

Эта функция будет отправлять сообщение с именем message и содержимым hi всем подключенным веб-сокетам. Позже не забудьте удалить эту часть кода, так как она предназначена только для тестирования.

В папке static создайте файл с именем game.js. Вы можете написать короткую функцию для регистрации сообщений от сервера, чтобы убедиться в том, что вы их получаете. В файле static/game.js пропишите следующее:

var socket = io();
socket.on('message', function(data) {
    console.log(data);
});

Запустите сервер командой node server.js и в любом браузере перейдите по ссылке http://localhost:5000. Если вы откроете окно разработчика (нажать правую кнопку мыши → Проверить (Inspect)), то увидите, как каждую секунду приходит новое сообщение:

Как правило, socket.emit(name, data) отправляет сообщение с заданным именем и данными серверу, если запрос идет от клиента, и наоборот, если запрос идет от сервера. Для получения сообщений по конкретному имени используется следующая команда:

socket.on('name', function(data) {
    // аргумент data может содержать любые отправляемые данные
});

С помощью socket.emit() вы можете отправить любое сообщение. Можно также передавать объекты JSON, что для нас очень удобно. Это позволяет мгновенно передавать информацию в игре от сервера к клиенту и обратно, что является основой многопользовательской игры.

Теперь пусть клиент отправляет некоторые состояния клавиатуры. Поместите следующий код в конец файла static/game.js:

var movement = {
  up: false,
  down: false,
  left: false,
  right: false
}
document.addEventListener('keydown', function(event) {
  switch (event.keyCode) {
    case 65: // A
      movement.left = true;
      break;
    case 87: // W
      movement.up = true;
      break;
    case 68: // D
      movement.right = true;
      break;
    case 83: // S
      movement.down = true;
      break;
  }
});
document.addEventListener('keyup', function(event) {
  switch (event.keyCode) {
    case 65: // A
      movement.left = false;
      break;
    case 87: // W
      movement.up = false;
      break;
    case 68: // D
      movement.right = false;
      break;
    case 83: // S
      movement.down = false;
      break;
  }
});

Это стандартный код, который позволяет отслеживать нажатие клавиш W, A, S, D. После этого добавьте сообщение, которое оповестит сервер о том, что в игре появился новый участник, и создайте цикл, который будет сообщать серверу о нажатии клавиш.

socket.emit('new player');
setInterval(function() {
  socket.emit('movement', movement);
}, 1000 / 60);

Эта часть кода позволит отправлять на сервер информацию о состоянии клавиатуры клиента 60 раз в секунду. Теперь необходимо прописать эту ситуацию со стороны сервера. В конец файла server.js добавьте следующие строки:

var players = {};
io.on('connection', function(socket) {
  socket.on('new player', function() {
    players[socket.id] = {
      x: 300,
      y: 300
    };
  });
  socket.on('movement', function(data) {
    var player = players[socket.id] || {};
    if (data.left) {
      player.x -= 5;
    }
    if (data.up) {
      player.y -= 5;
    }
    if (data.right) {
      player.x += 5;
    }
    if (data.down) {
      player.y += 5;
    }
  });
});
setInterval(function() {
  io.sockets.emit('state', players);
}, 1000 / 60);

Давайте разберёмся с этим кодом. Вы будете хранить информацию о всех подключенных пользователях в виде объектов JSON. Так как у каждого подключённого к серверу сокета есть уникальный id, клавиша будет представлять собой id сокета подключённого игрока. Значение же будет другим объектом JSON, содержащим координаты x и y.

Когда сервер получит сообщение о том, что присоединился новый игрок, он добавит новый вход в объект игроков при помощи id сокета, который будет в этом сообщении. Когда сервер получит сообщение о движении, то обновит информацию об игроке, который связан с этим сокетом, если он существует.

io.sockets.emit() — это запрос, который будет отправлять сообщение и данные ВСЕМ подключённым сокетам. Сервер будет отправлять это состояние всем подключённым клиентам 60 раз в секунду.

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

var canvas = document.getElementById('canvas');
canvas.width = 800;
canvas.height = 600;
var context = canvas.getContext('2d');
socket.on('state', function(players) {
  context.clearRect(0, 0, 800, 600);
  context.fillStyle = 'green';
  for (var id in players) {
    var player = players[id];
    context.beginPath();
    context.arc(player.x, player.y, 10, 0, 2 * Math.PI);
    context.fill();
  }
});

Этот код обращается к id Canvas (#canvas) и рисует там. Каждый раз, когда от сервера будет поступать сообщение о состоянии, данные в Canvas будут обнуляться, и на нём в виде зеленых кружков будут заново отображаться все игроки.

Теперь каждый новый игрок сможет видеть состояние всех подключенных игроков на Canvas. Запустите сервер командой node server.js и откройте в браузере два окна. При переходе по ссылке http://localhost:5000 вы должны будете увидеть нечто похожее:

Вот и всё! Если у вас возникли проблемы, посмотрите архив с исходным кодом.

Некоторые тонкости

Когда будете разрабатывать более функциональную игру, целесообразно разделить код на несколько файлов.

Такие многопользовательские игры — отличный пример архитектуры MVC (модель-представление-контроллер). Вся логическая часть должна обрабатываться на сервере, а всё, что должен делать клиент — это отправлять входные пользовательские данные на сервер и отображать информацию, которую получает от сервера.

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

while (true) {
  socket.emit('movement', { left: true });
}

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

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

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

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

В идеале циклы обновлений как у клиента, так и на сервере не должны зависеть от сокетов. Попытайтесь сделать так, чтобы обновления игры находились за пределами блока socket.on(). В противном случае вы можете получить много странных нелогичных действий из-за того, что обновление игры будет связано с обновлением сокета.

Кроме того, старайтесь избегать такого кода:

setInterval(function() {
  // код ...
  player.x += 5;
  // код ...
}, 1000 / 60);

В этом отрезке кода обновление координаты х для игрока связано с частотой смены кадров в игре. SetInterval() не всегда гарантирует соблюдение интервала, вместо этого напишите нечто подобное:

var lastUpdateTime = (new Date()).getTime();
setInterval(function() {
  // код ...
  var currentTime = (new Date()).getTime();
  var timeDifference = currentTime - lastUpdateTime;
  player.x += 5 * timeDifference;
  lastUpdateTime = currentTime;
}, 1000 / 60);

Это не так изящно, зато обеспечит более плавную и последовательную работу. Усложните демо-проект и попробуйте сделать так, чтобы обновление осуществлялось согласно времени, а не частоте смены кадров. Если не захотите на этом останавливаться, попытайтесь создать на сервере физический движок, который будет управлять движениями игроков.

Также можно сделать так, чтобы из игры удалялись отключенные игроки. Когда сокет отключается, автоматически отправляется сообщение о разъединении. Это можно прописать так:

io.on('connection', function(socket) {
  // обработчик событий ...
  socket.on('disconnect', function() {
    // удаляем отключившегося игрока
  });
});

Также попытайтесь создать собственный физический движок. Это сложно, но весело. Если захотите попробовать, то рекомендую прочитать книгу «The Nature of Code», в которой есть много полезных идей.

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

Перевод статьи «How To Build A Multiplayer Browser Game (Part 1)»

JavaScript – очень гибкий язык, поскольку начиная с JavaScript ES5 каждый новый стандарт добавляет в язык все больше функциональности. За это приходится платить некоторыми старыми костылями, вокруг которых нужно «плясать», но преимущества все равно перевешивают – на чистом JavaScript можно написать практически что угодно, и гайд ниже вам это продемонстрирует, поскольку мы будем писать 2d игру «Змейку». Заметим, что выучить JavaScript с нуля на этом гайде не получится – все же нужны некоторые предварительные знания. Кроме того, предупредим, что нам понадобится чистый HTML (включая HTML5 canvas) и CSS, хотя 95% времени все же будет посвящено языку JavaScript.

Шаг предварительный: дизайн

Шаг 2: размещаем еду, перехватываем нажатия клавиш

Шаг 3: пишем основную функцию

Где взять гайды по разработке игр?

Подготовка: HTML и CSS

Перед тем, как размещать код, нам нужно создать стандартный HTML-файл index.html. Выглядеть он должен следующим образом:

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<meta name="viewport", content="width=device-width, initial-scale=1.0">

<link rel="stylesheet" href="snake.css">

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

</head>

<body>

<canvas id="board"></canvas>

</body>

</html>

Здесь нет никакой магии: в head мы указываем всю мета-информацию, выставляем правильную ширину и подключаем CSS и скрипт, в котором будет расположена игра. В теле создаем канву, которая и будет основой нашей игры.

CSS выглядит вот так:

body {

text-align: center;

}

Это позволяет разместить нашу канву по центру экрана.

Шаг предварительный: дизайн

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

У нас есть некоторое поле с клетками, представляющее собой двухмерный массив. Изначально в случайных ячейках появляется еда, игрок появляется в фиксированной ячейке. Для простоты примера не будем делать отдельную кнопку «Старт», игра начнется при первом нажатии на любую стрелочку. Змейка управляется через стрелочки, она двигается в направлении, соответствующем стрелочке, нажатой последней. Змейка двигается с определенной скоростью и не может остановиться. Если змейка сталкивается с едой – она «съедает» ее: еда удаляется с экрана, змейка «прирастает» на 1 сегмент, на экране в незанятой клетке появляется новая еда. Если голова змейки сталкивается с другим сегментом змейки, игра заканчивается. Если голова змейки сталкивается с краем экрана, игра заканчивается.

Окончание игры сопровождается надписью «Вы проиграли!».

Шаг 1: создаем глобальные переменные и функцию main

Учитывая, что у нас – небольшой проект, будет очень удобно создать все необходимые переменные в глобальной области видимости, после чего обращаться к ним напрямую (в больших проектах так делать не стоит, потому что может случиться так, что вы случайно перекроете область видимости). Что нам нужно? Нам нужны переменные для: размера блока в пикселях, количества рядов и строк, служебные переменные для доступа к канве и ее содержимому, размеры и скорость змейки, массив для частей змейки, координаты еды, флаг окончания игры.

var blockSize = 25;

var rows = 20;

var cols = 20;

var board;

var context; 

var snakeX = blockSize * 5;

var snakeY = blockSize * 5;

var velocityX = 0;

var velocityY = 0;

var snakeBody = [];

var foodX;

var foodY;

var gameOver = false;

Опять же, с учетом того, что наш проект – маленький, будет неплохой идеей начать разработку с самой главной функции, в ней написать всю последовательность действий, после чего уже реализовывать функции более низкого уровня (вызываемые этой главной функцией). Начинать функцию будем по триггеру window.onload, в самой функции будем: 1) захватывать канву; 2) задавать размеры игрового поля в пикселях; 3) размещать еду; 4) подключать listener для нажатия кнопки; 5) реализовывать игровую механику с обработкой по фреймам. Код:

window.onload = function() {

board = document.getElementById("board");

board.height = rows * blockSize;

board.width = cols * blockSize;

context = board.getContext("2d"); 

placeFood();

document.addEventListener("keyup", changeDirection);

// update();

setInterval(update, 1000/10); //100 milliseconds

}

1000/10 означает, что игра будет обновляться 10 раз в секунду, если хотите уменьшить/увеличить скорость – поиграйте с этим параметром.

Шаг 2: размещаем еду, перехватываем нажатия клавиш

У нас есть 2 небольшие функции, которые можно реализовать прямо сейчас – размещение еды и нажатие клавиши. С размещением еды все очень просто:

function placeFood() {

//(0-1) * cols -> (0-19.9999) -> (0-19) * 25

foodX = Math.floor(Math.random() * cols) * blockSize;

foodY = Math.floor(Math.random() * rows) * blockSize;

}

Вызываем функцию генерации случайных чисел, через нее получаем номер блока, умножаем на ширину блока – получаем x и y новой еды.

С направлением все чуть сложнее:

function changeDirection(e) { if (e.code == "ArrowUp" && velocityY != 1) { velocityX = 0; velocityY = -1; } else if (e.code == "ArrowDown" && velocityY != -1) { velocityX = 0; velocityY = 1; } else if (e.code == "ArrowLeft" && velocityX != 1) { velocityX = -1; velocityY = 0; } else if (e.code == "ArrowRight" && velocityX != -1) { velocityX = 1; velocityY = 0; } }

Мы считаем ускорение как смещение относительно текущей точки на координатной оси на 1 или -1 (1 – это вправо или вниз, -1 – влево или вверх). Мы анализируем ввод и для каждой стрелочки меняем ускорение тем или иным образом. Проверка после && в условии нужна для того, чтобы исключить случаи, когда, например, змейка движется вправо, а мы нажали стрелочку влево – если позволить змейке поменять направление движения таким образом, то она тут же наткнется сама на себя.

Поэтому мы проверяем, что нажата стрелочка влево и при этом змейка не движется вправо.

Шаг 3: пишем основную функцию

В серьезных игровых движках, типа Unity и UE, функция update доступна для каждого элемента, и update исполняется каждый фрейм. Мы же хотим создать браузерную игру своими силами, поэтому используем

setInterval(update, 1000/10);

После чего пишем основной код в update(). Сначала отрисовываем игровое поле и блок еды:

context.fillStyle="black";

context.fillRect(0, 0, board.width, board.height);

context.fillStyle="red";

context.fillRect(foodX, foodY, blockSize, blockSize);

Теперь проверяем, находится ли голова змейки и еда на одной точке, если находится – еду нужно «съесть» и сгенерировать новую:

if (snakeX == foodX && snakeY == foodY) {

snakeBody.push([foodX, foodY]);

placeFood();

}

Теперь нам нужно решить проблему перемещения: каждый раз, когда змейка перемещается на блок куда-либо, ее тело должно перемещаться вместе с ней. Решается это просто – поскольку у нас все тело задано в массиве координатами каждого сегмента, нам нужно пройтись по всему массиву и сместить все элементы на 1, а нулевому элементу присвоить координаты головы в текущий момент:

for (let i = snakeBody.length-1; i > 0; i--) {

snakeBody[i] = snakeBody[i-1];

}

if (snakeBody.length) {

snakeBody[0] = [snakeX, snakeY];

}

Теперь реализовываем саму змейку:

context.fillStyle="lime";

snakeX += velocityX * blockSize;

snakeY += velocityY * blockSize;

context.fillRect(snakeX, snakeY, blockSize, blockSize);

for (let i = 0; i < snakeBody.length; i++) {

context.fillRect(snakeBody[i][0], snakeBody[i][1], blockSize, blockSize);

}

Наконец, нужно задать условия окончания игры – если змейка столкнулась с концом экрана или если она укусила себя. Первое проверяется через сравнение координат головы с краями канвы, второе проверяется через сравнение координат головы с координатами каждого сегмента тела:

if (snakeX < 0 || snakeX > cols*blockSize -1 || snakeY < 0 || snakeY > rows*blockSize - 1) {

gameOver = true;

alert("Game Over");

}

for (let i = 0; i < snakeBody.length; i++) {

if (snakeX == snakeBody[i][0] && snakeY == snakeBody[i][1]) {

gameOver = true;

alert("Game Over");

}

}

Наконец, в самое начало функции нужно добавить проверку геймовера – если значение равно true, то нужно закончить игру и ничего больше не делать.

if (gameOver) {

return;

}

Все, имеем полностью работоспособную игру.

Шаг 4: тестируем

Все файлы нужно собрать в одной папке, после чего – открыть html-файл. Нажмите любую стрелочку – и увидите следующее:

Где взять гайды по разработке игр?

Какого-то универсального гайда по разработке игр нет – это все же творческий процесс, поэтому для разработки серьезных игр вам нужны сильные знания JavaScript и фантазия. Проще искать гайды по созданию конкретных игр и брать из этих гайдов идеи, после чего «пилить» свою собственную игру на основе этих идей. Вот вам парочка видеогайдов:

  • Канал ChrisCourses – огромное количество длинных и детальных гайдов по разработке самых сложных игр (вплоть до RPG про Покемонов)
  • Мультиплеерная игра на JavaScript с дополнительной библиотекой
  • Простой гайд по созданию игры на русском языке
  • Аналог Flappy Bird на JS
  • Игра, код которой умещается в 100 символов

Вывод

  • JS хорошо подходит для создания простых браузерных игр.
  • Вы можете самостоятельно по гайдам создавать такие простые игры, как змейку, гонки, Aliens Invaders, PacMan и другие.
  • Более сложные игры могут потребовать от вас пользоваться сторонними библиотеками.
  • В любом случае вам нужно хорошо знать JS, без этих знаний вы не сможете разрабатывать игры самостоятельно.

Игровой контент – одно из самых распространенных направлений в программировании. Разработчики и программеры стараются создавать развлекательный софт не только клиентского типа, но и браузерного. Для реализации поставленной задачи человеку потребуются определенные навыки, умения и знания. Тогда даже начинающий сможет справиться с self made контентом.

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

Движок – определение

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

При помощи соответствующих компонентов удается получить:

  • 3D и 2D-графику;
  • скриптинг;
  • звук;
  • кат-сцены;
  • рендеринг;
  • сетевые коды;
  • реализацию физического движка;
  • внедрить искусственный интеллект.

Это – настоящий подарок для тех, кто планирует заниматься разработкой self made контента. Говоря простыми словами, рассматриваемый платформер – это база для игр.

О языках

Игровые объекты и другие составляющие как клиентской, так и браузерной self игрушки лучше внедряются через движки. Они бывают готовыми («чужими») и собственными. Первый вариант применяется в большинстве случаев. Лишь изредка крупные разработчики софта пишут для тех или иных проектов собственные движки (пример – REEngine от Capcom).

В основе платформеров лежит программирование на различных языках. Браузерные self games пишутся преимущественно на:

  • JavaScript;
  • PHP;
  • Python.

Это – основные языки разработки веб-контента. Отдельное все они схожи между собой, но имеют собственные нюансы и особенности. Можно обучиться как одной «лексике» для успешной разработки self контента, так и нескольким. Второй вариант больше подойдет тем, кто планирует активное программирование «с нуля».

Лучшие платформеры

Игрушки, сделанные при помощи готовых платформеров-движков – это практически совершенный контент. Но многое зависит от того, какую именно «базу» выберет программер.

Вот несколько самых популярных на сегодняшний день вариантов:

  • CryEngine;
  • Unity 3D;
  • Unreal Engine 4;
  • GameMaker Studio 2;
  • Cocos;
  • Corona (он же Solar2D);
  • Godot;
  • libGDX.

Лучший контент для создания игр выбрать трудно. Но на практике в ходу софт и приложения, сделанные при помощи Unity 3D и Unreal Engine.

Что лучше для браузера

Self Made Games браузерного типа – это преимущественно 2D-софт. Для его воспроизведения используем разнообразные проигрыватели (пример – Flash Player) и расширения.

Браузерные утилиты должны быть:

  • «легкими» по объему, иначе возможна долгая загрузка;
  • с тщательно проработанной графикой;
  • обладающими высокой частотой кадров;
  • удобными;
  • быстрыми.

Поэтому для соответствующего self контента нужно выбирать веб-языки. В идеале – Python. Это – отличный вариант как для новичков, так и для тех, кто долгое время занимается разработкой софта.

Python – определение

Приложения, написанные на Питоне – это быстрые и удобные, практически совершенные self утилиты. Браузерный софт основывается на скриптах. Python состоит из соответствующих «составляющих».

Это – стремительно развивающийся скриптовый язык. Применяется при решении разноплановых задач и достижения целей. На нем пишут self утилиты для:

  • мобильных платформ;
  • компьютеров;
  • приставок.

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

Софт, написанный на Питоне, считают кроссплатформенным. Его легко перенести из одной операционной системы в другую. Разрабы часто используют соответствующую «лексику» для создания браузерных игрушек.

PyGame – это

Тем, кто хочет заниматься созданием игр на Python, рекомендуется обратить внимание на такой объект, как PyGame. Без него self made контент будет трудно сделать.

PyGame – элемент, который пригодится уже тем, кто имеет общее представление о Питоне. Пользователь должен быть знаком с такими понятиями как:

  • классы;
  • методы;
  • функции;
  • циклы.

Pygame – это некая библиотека, используется при создании self «мейд» софта 2D-типа. Сборник необходимых для реализации поставленной задачи инструментов.

Pygame – своеобразная оболочка мультимедийной библиотеке SDL. Используется для обработки опросов событий, вставки изображений в окна, а также «прикручивания» звуков и других важных для игрового процесса составляющих.

Pygame впервые появился в 2000 году, в ноябре. Обладает отличным комьюнити, а также сопутствующей документацией и всевозможными справками. Некоторые программеры называют Pygame фреймворком. Это не совсем правильно, но иногда такое «приравнивание» уместно. А еще Pygame часто считают игровым движком. При классификации соответствующего объекта можно сделать вывод: для ПО это – API Python к API библиотеке SDL.

База для игр

Важная часть 2D-игры – это простой скелет. Основная масса браузерного софта представлена в виде основного цикла. Кодификация будет выполняться множество раз в процессе реализации контента.

При создании self made games на Pygame важно уметь «прикреплять» различные объекты, при помощи которых человек сможет играть в браузере и наслаждаться процессом. Это не так трудно даже начинающим программерам.

Основы PyGame

Для того, чтобы написать собственную утилиту на рассматриваемом «движке», важно понимать, с чем предстоит работать. Для начала рекомендуется изучить следующий элементарный пример утилиты, написанной в Python. С ней будет осуществляться дальнейшая работа.

Теперь важно подключить библиотеку. Проводится соответствующее действие командой import pygame. Далее предстоит выполнить следующие манипуляции:

  1. Вызвать функцию init() для подготовки модулей «движка» к функционированию – pygame.init().
  2. Создать графическое окно – screen = pygame.display.set_mode ((1200, 800)).
  3. Запустить основной цикл утилиты. Здесь будет перехватываться закрытие основное графокна юзером.

Как только желаемое событие наступает, предстоит завершить работу с библиотекой (def update pygame) посредством pygame.quit(). Далее требуется вызвать exit() из модуля sys.

Создание геометрии

В утилите можно размещать разнообразные фигуры. Пример – прямоугольник. В Питоне и Pygame при создании Self Game используется тип Rect.

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

В библиотеке функции отображения фигур геометрического типа расположены в модуле draw. Рисуется рассматриваемый объект через rect().

Требуется передать в функции в виде аргументов поверхность, на которой размещается прямоугольник. Дополнительно прописываются:

  • цвет;
  • сама фигура;
  • толщина линий.

Вот пример приложения:

Стоит обратить внимание на последнюю строчку game. Если требуется разместить графические составляющие на главном экране, сначала они переходят в спецбуфер. Оттуда отображение корректировок вызываются посредством flip().

Прочие операции для рисования фигур

При создании Self игрушек в Pygame могут использоваться различные операции и функции. Их основа – геометрические фигуры:

  1. Pygame.draw.line(surface, color, start_pos, end_pos, width=1). Происходит рисование линии на поверхности заданного цвета. Начало – в точке «старт», конец – в «энд». Толщина линии – width.
  2. Pygame.draw.lines(Surfacecolorclosedpointlistwidth=1). Рисуется линия, которая соединяет последовательности «поинлист» на заданной поверхности установленным цветом. Каждая точка – это пара координат. Здесь xlosed равен True, а конец соединяется с начальной.
  3. Pygame.draw.circle(Surfacecolorposradiuswidth=0). Рисовка окружности с центром в точке «пос» и заданным радиусом. Если width = 0, происходит закрашивание круга.
  4. Pygame.draw.ellipse(SurfacecolorRectwidth=0). Создание в Pygame эллипс, ограниченный прямоугольником Rect. При нулевом параметре width происходит закрашивание фигуры.
  5. Pygame.draw.polygon(Surfacecolorpointlistwidth=0). Изображение многоугольника из последовательностей «поинтлист». Каждая точка – это пара координат. Если последний элемент = 0, фигура закрашивается полностью.

Пока этого будет достаточно для практики. Цветовые гаммы представляются моделью RGB. Цвет задается тройкой чисел от 0 до 255. Чем меньше значение числа, тем темнее получится в итоге оттенок.

Также есть модуль color, который содержит словарь thecolors. Там ключи – это цветовые гаммы. Подключение производится командой from pygame.color import thecolors.

Основной экран можно закрасить через метод fill().

Шрифт и текст

Если пользователь делает игру или иной контент, ему не обойтись без текста и шрифтов. Последние представлены Font. Для создания соответствующего типа используется функция SysFont (имя, размер, bold=False, italic=False).

Здесь:

  • задается имя шрифта – первый элемент после скобок;
  • размер в ПТ – «размер»;
  • bold и italic – на начертание.

Чтобы посмотреть все шрифты, имеющиеся в базе, стоит воспользоваться операцией get_fonts():

Теперь через метод render() можно вывести картинку с текстом, которая передается методу vlit() для отображения на основном экране:

В предложенном примере текст будет размещаться на главном дисплее по координатам (50, 50).

Как стать гейм-разработчиком

Для того, чтобы стать разработчиком или программером, который создает self made games для браузеров на Python или PHP, предстоит выбрать тот или иной путь развития. После того, как юзер определился, на каком языке работать, ему необходимо получить определенные знания.

Существуют следующие варианты развития событий:

  1. Поступление в ВУЗ для получения высшего образования. Сегодня университеты набирают как гейм-создателей, так и обычных программистов. Срок обучения в среднем составляет 5 лет. В процессе человека научат основам программирования на различных языках. По выпуску выдается диплом государственного образца.
  2. Самообразование. Наиболее долгое решение, но для многих оно является верным. Юзеру предстоит самостоятельно изучать основы программирования на выбранном языке. В интернете полно соответствующей литературы. Никаких сертификатов человек не получит, зато сможет сконцентрироваться только на важных для себя сферах.
  3. Обучение на курсах. Относительно новое решение для тех, кто не готов поступать в ВУЗ. В Москве и других регионах образовательные центры предлагают курсы как по программированию на языках, так и по созданию игр. В конце выдается специальный сертификат.

Последний вариант встречается на практике чаще остальных. Он часто сочетается с самообразованием. Данный вариант помогает довольно быстро охватить программирование на Питоне и создание собственных игрушек.

На самом деле разработка браузерных игр через Python и PHP – это не так трудно. С элементарными задачами сможет справиться даже новичок. А по ссылке можно отыскать полезные уроки по созданию собственной игрушки на Питоне. Также вам может быть интересен профессиональный курс Otus по Python-разработке:

Разбираемся вместе с техлидом BeaversBrothers

Для образовательного проекта Банка России мы сделали яркую веб-игру «Тайна потерянной копилки». Она привлекает внимание школьников к теме финансовой грамотности, знакомит с терминами, учит разумно распоряжаться деньгами. Игра понравилась не только детям, но и взрослым из разных городов России — в неё сыграли более 30 000 человек.

Мы долго просили нашего техлида рассказать о том, как происходила разработка веб-игры. Он не только рассказал, но и написал огненную статью о нашем опыте в этом проекте, о возникших сложностях, а также поделился лайфхаками в разработке браузерных игр. Получился насыщенный полезностью материал. Читайте.

Особенности разработки игры для браузера

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

Предположения об особенностях браузерной игры у нас были — значительные ограничения на доступный и используемый размер оперативной памяти (в ТЗ, например, зафиксировано, что для мобильных устройств должно хватать 1 Гб оперативной памяти), баланс между качеством игровых ресурсов (изображения, текстуры, спрайты, звуки, видео) и скоростью их скачивания.

К этому добавились требования от клиента — игра должна запускаться и работать во всех заявленных мобильных и десктопных браузерах (включая IE 11), на минимально возможных аппаратных характеристиках. Требования эти исходили из того, что игру предполагалось показывать при любой удобной возможности, на любом попавшемся под руку устройстве — например, в школе.

Как выбирали движок

У нас уже был опыт игровой разработки, поэтому направления выбора движка обозначили сразу:

  • Игровой движок Phaser — мы уже реализовывали на нём игру для Эльдорадо/Майкрософт.
  • Unity с возможностью экспорта игры в Web — мы делали игровые проекты на этом движке.
  • Также рассматривали малоизвестные нам движки LibGDX, Godot, PlayCanvas.

Неизвестные варианты отвалились по вполне очевидной причине — их надо было осваивать и изучать, что, некоторым образом, пугало, хоть и не казалось невозможным. Вариант с Unity отвалился потому, что ограничения движка и экспорта не позволяли, например, использовать аудио в IE 11. А ещё потому, что экспортируемый из Unity код Javascript получался очень большой, а IE 11 значительно более медленный в парсинге и исполнении кода, чем современные браузеры.

Таким образом, было решено взять свежую версию движка Phaser (на момент начала разработки это была версия Phaser 3.11). Выбрали этот движок ещё и потому, что вся логика и отрисовка — программные, а значит, мы могли контролировать в коде абсолютно любой аспект будущей игры.

Как писали

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

Взяли из готовых в документации примеров «перемещение персонажа» и «тайловая карта», собрали, запустили — работает: персонаж ходит, прыгает, перемещается по платформам. Запустили во всех доступных устройствах — всё ещё работает. Добавили виртуальные кнопки управления из примера «виртуальные кнопки» и запустили на мобильных — всё ещё работает. Добавили немного механик — удар, враг, нанесение и получение урона, сбор монет — для прототипа было достаточно.

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

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

Стек разработки был взят довольно типичный для подобного проекта — webpack, webpack-dev-server, babel, babel/preset-typescript.

Какие трудности были

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

Например, в ТЗ было предусмотрено, что игра при запуске пытается определить производительность устройства, и в случае низкой производительности отображается соответствующее уведомление. На данном этапе мы столкнулись с двумя проблемами. Во-первых, браузер не даёт практически никаких сведений на этот счёт. Во-вторых, производительность конкретной вкладки браузера в конкретный момент времени очень сильно зависит от внешних факторов — сколько вкладок открыто, нет ли тяжёлых сайтов в других вкладках, не свёрнут ли браузер.

Было опробовано несколько теоретических и практических гипотез, из которых родилось финальное решение. Решение состоит в следующем:

  • На экране загрузки игры, где идёт прогресс от 0 до 100 %, фактическая загрузка ресурсов заканчивается на 92 %.
  • После этого за пределами экрана создаётся сцена, на которую помещаются тяжёлые анимации и немного физики.
  • Далее в течение 5 секунд измеряется среднее время отрисовки одного кадра.
  • После этого принимается решение о производительности устройства и прогресс доходит до 100 %.

Очень важным было требование полной работоспособности игры в IE 11, что тоже доставляло неудобства. Оказалось, что при запущенных инструментах разработчика, и без того небыстрое выполнение Javascript в этом браузере ещё замедлялось.

То есть ты сталкиваешься с тормозами в игре, открываешь инструменты разработчика, чтобы найти причину, а игра тормозит ещё больше.

Игровые механики

Игровые механики сами по себе несложные, во многом вдохновлённые существующими играми.

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

Обратной стороной такого решения в анимации главного персонажа стало то, что для каждого пола на каждом из уровней нужен свой отдельный набор подготовленных анимаций с оружием. А на втором уровне потребовался ещё один дополнительный набор — в кредитных кроссовках. Итого получилось по четыре полноценных набора анимаций для каждого персонажа.

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

Интересной была механика с ловлей ключей от сундука. Для ключа, который нужно было поймать, область срабатывания сделана меньше, чем визуальный спрайт ключа, а также незначительно смещена в сторону случайным образом. Это привело к желаемому эффекту «у меня ключ с первого раза не собирается» — иногда нужно несколько раз попробовать собрать ключ, чтобы попасть на область его срабатывания. Иначе было слишком просто, хотя в финальном релизе область срабатывания всё-таки была чуть увеличена, чтобы уменьшить процент неудачных попыток.

Все остальные механики собственно тем же и являются — срабатывание приближения и пересечения персонажа и игровых объектов в определенные моменты времени и анимации.

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

Выводы и советы

Определитесь с жанром на самом раннем этапе.

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

Совет — относитесь к разработке веб-игры как к реальной игре, а не как к очередному «скрипту на странице». Тестируйте, профилируйте, не допускайте утечек памяти и повышенной нагрузки на процессор. Игроки и батареи их устройств будут довольны.

Понравилась статья? Поделить с друзьями:
  • Как написать свою биографию образец для школьника
  • Как написать свою биографию образец для студента
  • Как написать свою биографию образец для опеки
  • Как написать свою биографию на английском языке
  • Как написать свою библиотеку на питоне