Создание браузерных 3d-игр с нуля на чистом html, css и js. Часть 2/2

от автора

В данной статье мы продолжим создавать трехмерную браузерную игру лабиринт на чистом html, css и javascript. В предыдущей части мы сделали простой 3-мерный мир, реализовали движение, управление, столкновения игрока со статическими объектами. В этой части мы будем добавлять гравитацию, статическое солнечное освещение (без теней), загружать звуки и делать меню. Увы, как и в первой части, демок здесь не будет.

Вспомним код, который мы сделали в предыдущей части. У нас имеются 3 файла:

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); 

1. Реализация гравитации и физики прыжка

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

// Создадим переменные  var lock = false; var onGround = true; var container = document.getElementById("container"); var world = document.getElementById("world"); 

Добавим ускорение свободного падения к ним:

var g = 0.1;

В конструктор player добавим 3 переменные — vx, vy и vz:

function player(x,y,z,rx,ry) { 	this.x = x; 	this.y = y; 	this.z = z; 	this.rx = rx; 	this.ry = ry; 	this.vx = 3; 	this.vy = 5; 	this.vz = 3; } 

Это переменные скорости движения. Меняя их, мы можем изменять скорость бега и начальную скорость прыжка игрока. Пока применим новые переменные в update():

         dx =   ((PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg))*pawn.vx; 	dz = ( -(PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg))*pawn.vz; 	dy = PressUp*pawn.vy; 	drx = MouseY; 	dry = - MouseX; 

Теперь игрок движется быстрее. Но он не падает и не прыгает. Нужно разрешить прыжок тогда, когда он на чем-то стоит. А стоять он будет тогда, когда столкнется с горизонтальной (или почти) поверхностью. Как определить горизонтальность? Нужно найти нормаль плоскости прямоугольника. Делается это просто. Относительно координат прямоугольника нормаль направлена вдоль оси z. Тогда в мировых координатах нормаль имеет преобразованные координаты. Найдем нормаль (добавим локальную переменную normal):

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(); let normal = coorReTransform(0,0,1,map[i][3],map[i][4],map[i][5]); 

Чтобы поверхность была горизонтальной, скалярное произведение нормали на ось y в мировых координатах должно равняться 1 или -1, а почти горизонтальная плоскость – близко к 1 или -1. Зададим условие почти горизонтальной плоскости:

if (Math.abs(normal[1]) > 0.8){ 	onGround = true; } 

Не забудем, что при отсутствии столкновений игрок точно не будет на земле, поэтому по умолчанию в начале функции collision() зададим onGround = false:

function collision(){ 	 	onGround = false; 	 	for(let i = 0; i < map.length; i++){ 

Однако, если игрок столкнется с поверхностью снизу, то он тоже окажется как бы на земле. Чтобы предотвратить это, проверим игрока на нахождение сверху плоскости (point3[1] должна быть меньше point2[1]):

let point3 = coorReTransform(point1[0],point1[1],0,map[i][3],map[i][4],map[i][5]); 				dx = point2[0] - x0; 				dy = point2[1] - y0; 				dz = point2[2] - z0; 				if (Math.abs(normal[1]) > 0.8){ 					if (point3[1] > point2[1]) onGround = true; 				} 				else dy = y1 - y0;  

Что мы делаем? взгляните на картинку:

Красный крест должен находиться ниже оранжевого в мировой системе координат (или y-координата должна быть больше). Это мы и проверяем в point3[1] > point2[1]. А point3 – есть как раз координаты красной точки. Перенесем инициализацию point2 внутрь условии коллизии:

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 normal = coorReTransform(0,0,1,map[i][3],map[i][4],map[i][5]); 		 			// Условие коллизии и действия при нем 		 			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; 				let point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]); 				let point3 = coorReTransform(point1[0],point1[1],0,map[i][3],map[i][4],map[i][5]); 				dx = point2[0] - x0; 				dy = point2[1] - y0; 				dz = point2[2] - z0; 				if (Math.abs(normal[1]) > 0.8){ 					if (point3[1] > point2[1]) onGround = true; 				} 			} 

Перенесемся в update(). Здесь мы тоже сделаем изменения. Во первых, добавим гравитацию и уберем смещение по y при нажатии на пробел:

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

Во вторых, если игрок находится на земле, запрещаем гравитацию, запрещаем смещения по y (иначе после хождения по наклонной поверхности игрок будет взлетать) и добавляем возможность прыжка (условие if (onGround)):

// Задаем локальные переменные смещения 	 	dx =   ((PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg))*pawn.vx; 	dz = ( -(PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg))*pawn.vz; 	dy = dy + g; 	if (onGround){ 		dy = 0; 		if (PressUp){ 			dy = - PressUp*pawn.vy; 			onGround = false; 		} 	}; 	drx = MouseY; 	dry = - MouseX; 

Естественно, сразу после произведения прыжка запрещаем повторный прыжок, переведя параметр onGround в false. В условии нажатия пробела правдивость этого параметра больше не нужна:

if (event.keyCode == 32){ 		PressUp = 1; 	} 

Для проверки изменений изменим мир:

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,0,-300,70,0,0,200,500,"#F000FF"], 		   [0,-86,-786,90,0,0,200,500,"#F000FF"], 		   [-500,0,-300,20,0,0,200,500,"#00FF00"], 		   [0,100,0,90,0,0,2000,2000,"#666666"] ]; 

Если мы запустим игру, то увидим, что игрок может взбираться по почти вертикальной зеленой стене. Запретим это, добавив else dy = y1 — y0:

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; 				let point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]); 				let point3 = coorReTransform(point1[0],point1[1],0,map[i][3],map[i][4],map[i][5]); 				dx = point2[0] - x0; 				dy = point2[1] - y0; 				dz = point2[2] - z0; 				if (Math.abs(normal[1]) > 0.8){ 					if (point3[1] > point2[1]) onGround = true; 				} 				else dy = y1 - y0; 			} 

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

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; 	this.vx = 3; 	this.vy = 5; 	this.vz = 3; }  // Массив прямоугольников  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,0,-300,70,0,0,200,500,"#F000FF"], 		   [0,-86,-786,90,0,0,200,500,"#F000FF"], 		   [-500,0,-300,20,0,0,200,500,"#00FF00"], 		   [0,-800,0,90,0,0,500,500,"#00FF00"], 		   [0,-400,700,60,0,0,500,900,"#FFFF00"], 		   [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 = false; var container = document.getElementById("container"); var world = document.getElementById("world"); var g = 0.1; var dx = dy = dz = 0;   // Обработчик изменения состояния захвата курсора  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){ 		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,-900,0,0,0);  function update(){ 	 	// Задаем локальные переменные смещения 	 	dx =   ((PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg))*pawn.vx; 	dz = ( -(PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg))*pawn.vz; 	dy = dy + g; 	if (onGround){ 		dy = 0; 		if (PressUp){ 			dy = - PressUp*pawn.vy; 			onGround = false; 		} 	}; 	drx = MouseY; 	dry = - MouseX; 	 	// Обнулим смещения мыши: 	 	MouseX = MouseY = 0; 	 	// Проверяем коллизию с прямоугольниками 	 	collision(); 	 	// Прибавляем смещения к координатам 	 	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); 	} }  function collision(){ 	 	onGround = false; 	 	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 normal = coorReTransform(0,0,1,map[i][3],map[i][4],map[i][5]); 		 			// Условие коллизии и действия при нем 		 			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; 				let point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]); 				let point3 = coorReTransform(point1[0],point1[1],0,map[i][3],map[i][4],map[i][5]); 				dx = point2[0] - x0; 				dy = point2[1] - y0; 				dz = point2[2] - z0; 				if (Math.abs(normal[1]) > 0.8){ 					if (point3[1] > point2[1]) onGround = true; 				} 				else dy = y1 - y0; 			} 			 		} 	}; }  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); 

2. Создадим меню

Меню создадим в виде html-панелей и html-блоков. Оформление у всего меню будет примерно одинаковым: фон и стиль кнопок можно задать общими для всех. Итак, зададим три панели меню: главное меню, инструкция и вывод результатов по завершению игры. Переходы между меню, переход в мир и обратно будет выполняться скриптами javascript. Чтобы не нагромождать файл script.js, для переходов меню создадим новый файл menu.js, а в index.html подключим его:

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

В контейнере создадим 3 элемента, которые будут панелями меню:

<div id="container">     <div id = "world"></div>     <div id = "pawn"></div>     <div id = "menu1"></div>     <div id = "menu2"></div>     <div id = "menu3"></div> </div> 

Оформим их, добавив в style.css свойства для класса “menu”:

.menu{ 	display:none; 	position:absolute; 	width:inherit; 	height:inherit; 	background-color:#C0FFFF; } 

В меню (в файле index.html) добавим кнопки с соответствующими надписями:

                 <div class = "menu" id = "menu1"> 			<div id="button1" class="button"> 				<p>Начать игру</p> 			</div> 			<div id="button2" class="button"> 				<p>Инструкция</p> 			</div> 		</div> 		<div class = "menu" id = "menu2"> 			<p style="font-size:30px; top:200px"> 				<strong>Управление:</strong> <br> 				w - вперед <br> 				s - назад <br> 				d - вправо <br> 				a - влево <br> 				пробел - прыжок <br> 				!!! Включите английскую раскладку !!!<br> 				<strong>Задача:</strong> <br> 				Взять красный квадрат и найти голубой квадрат 			</p> 			<div id="button3" class="button"> 				<p>Назад</p> 			</div> 		</div> 		<div class = "menu" id = "menu3"> 			<p id = "result" style="top:100px"></p> 			<div id="button4" class="button"> 				<p>Вернуться назад</p> 			</div> 		</div> 

Для кнопок тоже зададим стили в style.css:

.button{ 	margin:0px; 	position:absolute; 	width:900px; 	height:250px; 	background-color:#FFF; 	cursor:pointer; } .button:hover{ 	background-color:#DDD; }  #button1{ 	top:100px; 	left:150px; } #button2{ 	top:450px; 	left:150px; } #button3{ 	top:450px; 	left:150px; } #button4{ 	top:450px; 	left:150px; } 

Но мы не видим меню, так как у них задан стиль display:none, При запуске же игры один из пунктов меню должен быть виден, поэтому в html для 1-го меню добавим запись style = “display:block;”, а выглядеть это будет следующим образом:

<div class = "menu" id = "menu1" style = "display:block;"> 

Меню стало выглядеть вот так:

Отлично. Но если мы нажмем на кнопку, то курсор у нас захватится. Значит нам нужно разрешить захват мыши только в случае игры. Для этого введем в script.js переменную canlock и добавим ее в пункт создадим переменные:

// Создадим переменные  var lock = false; var onGround = false; var container = document.getElementById("container"); var world = document.getElementById("world"); var g = 0.1; var dx = dy = dz = 0; var canlock = false;  А в обработчик захвата мыши изменим условие:  // Обработчик захвата курсора мыши  container.onclick = function(){ 	if (!lock && canlock) container.requestPointerLock(); }; 

Теперь мы можем щелкать меню. Настроим переходы с помощью скриптов в файле menu.js:

// Создаем переменные  var menu1 = document.getElementById("menu1"); var menu2 = document.getElementById("menu2"); var menu3 = document.getElementById("menu3"); var button1 = document.getElementById("button1"); var button2 = document.getElementById("button2"); var button3 = document.getElementById("button3"); var button4 = document.getElementById("button4");  // Настроим переходы  button2.onclick = function(){ 	menu1.style.display = "none"; 	menu2.style.display = "block"; }  button3.onclick = function(){ 	menu1.style.display = "block"; 	menu2.style.display = "none"; }  button4.onclick = function(){ 	menu1.style.display = "block"; 	menu3.style.display = "none"; } 

Теперь все кнопки меню, за исключением “начать игру”, работают. Настроим теперь кнопку button1. Если вы помните, в файле script.js функции CreateNewWorld() и setInterval() запускаются при загрузке веб-страницы. Удалим их оттуда. Вызывать их будем только при нажатии кнопки button1. Сделаем это:

button1.onclick = function(){ 	menu1.style.display = "none"; 	CreateNewWorld(); 	TimerGame = setInterval(update,10); } 

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

3. Создадим предметы и переход уровней.

Для начала определимся с правилами игры. У нас есть три типа предметов: монеты (желтые квадраты), ключи (красные квадраты) и финиш (голубой квадрат). Монеты приносят очки. Игроку необходимо найти ключ, и только потом прийти к финишу. Если он придет к финишу без ключа, то получит сообщение о необходимости сначала найти ключ. Предметы у нас будут создаваться также, как и карта. Записывать их мы будем с помощью массивов. Но делать для них отдельную функцию мы не будем. Мы просто напишем новую функцию, которая расставляет и элементы карты, и прямоугольника и перенесем команды из CreateNewWorld(). Назовем ее CreateSquares(). Итак, добавим в конец файла script.js следующую запись:

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

А содержимое createNewWorld() изменим:

function CreateNewWorld(){ 	CreateSquares(map,”map”); } 

Строка нужна для того, чтобы задавать имя id. Игра пока ничуть не изменилась. Теперь добавим 3 массива: монеты (things), ключи (keys) и финиш (finish). Вставим их сразу после массива карты:

var things = [[900,50,-900,0,0,0,50,50,"#FFFF00"], 		    [-400,50,900,0,0,0,50,50,"#FFFF00"], 		    [-400,50,-300,0,0,0,50,50,"#FFFF00"]]; 			   var keys = [[-100,50,600,0,0,0,50,50,"#FF0000"]];	  var start = [[-900,0,-900,0,0]];  var finish = [[-900,50,900,0,0,0,50,50,"#00FFFF"]]; 

А в menu.js применим функцию CreateSquares() внутри обработчика нажатия кнопки “button1”:

button1.onclick = function(){ 	menu1.style.display = "none"; 	CreateNewWorld(); 	CreateSquares(things,”thing”); 	CreateSquares(keys,”key”); 	CreateSquares(finish,”finish”); 	TimerGame = setInterval(update,10); 	canlock = true; } 

Теперь настроим исчезновение предметов. В menu.js создадим функцию проверки расстояний от игрока до предметов:

function interact(objects,string){ 	for (i = 0; i < objects.length; i++){ 		let r = (objects[i][0] - pawn.x)**2 + (objects[i][1] - pawn.y)**2 + (objects[i][2] - pawn.z)**2; 		if(r < (objects[i][7]**2)/4){ 			document.getElementById(string + i).style.display = "none";                         document.getElementById(string + i).style.transform =  			"translate3d(1000000px,1000000px,1000000px)"; 		}; 	}; } 

Также в этом же файле создадим функцию repeatFunction() и добавим в нее команды:

function repeatFunction(){ 	update(); 	interact(things,"thing"); 	interact(keys,"key"); } 

А ее циклический вызов запустим в setInterval внутри button1:

TimerGame = setInterval(repeatFunction,10); 

Теперь предметы исчезают, когда мы к ним подходим. Однако они ровно ничего не делают. А мы хотим, чтобы при взятии желтых квадратов нам добавлялись очки, при взятии красных – появлялась возможность взять синий и закончить игру. Модифицируем функцию interact():

function interact(objects,string,num){ 	for (i = 0; i < objects.length; i++){ 		let r = (objects[i][0] - pawn.x)**2 + (objects[i][1] - pawn.y)**2 + (objects[i][2] - pawn.z)**2; 		if(r < (objects[i][7]**2)){ 			document.getElementById(string + i).style.display = "none"; 			objects[i][0] = 1000000; 			objects[i][1] = 1000000; 			objects[i][2] = 1000000; 			document.getElementById(string + i).style.transform =  			"translate3d(1000000px,1000000px,1000000px)"; 			num[0]++; 		}; 	}; } 

Изменим входные параметры для вызовов этой функции:

function repeatFunction(){ 	update(); 	interact(things,"thing",m); 	interact(keys,"key",k); }

А в начале файла добавим четыре новые переменные:

var m = [0]; var k = [0]; var f = [0]; var score = 0; 

Вы спросите, почему мы создали массивы из одного элемента а не просто переменные? Дело в том, что мы хотели передать эти переменные в interact() по ссылке, а не по значению. В javascript обычные переменные передаются только по значению, а массивы по ссылке. Если мы передадим в interact() просто переменную, то num будет копией переменной. Изменение num не приведет к изменению k или m. А если мы передаем массив, то num будет ссылкой на массив k или m, и когда мы будем менять num[0], то будет меняться k[0] и m[0]. Можно было, конечно, создать 2 почти одинаковые функции, но лучше обойтись одной, чуть более универсальной.

Для финиша все-таки придется создать отдельную функцию:

function finishInteract(){ 	let r = (finish[0][0] - pawn.x)**2 + (finish[0][1] - pawn.y)**2 + (finish[0][2] - pawn.z)**2; 	if(r < (finish[0][7]**2)){ 		if (k[0] == 0){ 			console.log("найдите ключ"); 		} 		else{ 			clearWorld();                         clearInterval(TimerGame); 			document.exitPointerLock();                         score = score + m[0]; 			k[0] = 0;                         m[0] = 0; 			menu1.style.display = "block"; 		}; 	}; }; 

А clearWorld() настроим в script.js:

function clearWorld(){ 	world.innerHTML = ""; } 

Как видите, очистка мира проводится довольно просто. В repeatFunction() добавим finishInteract():

function repeatFunction(){ 	update(); 	interact(things,"thing",m); 	interact(keys,"key",k); 	finishInteract(); } 

Что происходит в finishInteract()? Если мы не взяли ключ (k[0] == 0), то пока ничего не происходит. Если взяли, то игра заканчивается, а происходит следующее: очищается мир, останавливается функция repeatFunction(), курсор перестает быть захваченным, счетчик ключей обнуляется, а мы переходим в главное меню. Проверим, запустив игру. Все работает. Однако после нажатия снова на игру, мы оказываемся сразу на финише, а некоторые предметы исчезают. Все потому что мы не ввели место первоначального спауна игрока, а массивы изменяются в течение игры. Давайте добавим в button1 точку спауна для игрока, а именно, приравняем его координаты к элементам массива start[0]:

button1.onclick = function(){ 	menu1.style.display = "none"; 	CreateNewWorld(); 	pawn.x = start[0][0]; 	pawn.y = start[0][1]; 	pawn.z = start[0][2]; 	pawn.rx = start[0][3]; 	pawn.rx = start[0][4]; 	CreateSquares(things,"thing"); 	CreateSquares(keys,"key"); 	CreateSquares(finish,"finish"); 	TimerGame = setInterval(repeatFunction,10); 	canlock = true; } 

Теперь игрок появляется в начале координат. Но вот вопрос: а если уровней в игре будет несколько? Добавим переменную уровней в menu.js:

// Создаем переменные  var menu1 = document.getElementById("menu1"); var menu2 = document.getElementById("menu2"); var menu3 = document.getElementById("menu3"); var button1 = document.getElementById("button1"); var button2 = document.getElementById("button2"); var button3 = document.getElementById("button3"); var button4 = document.getElementById("button4"); var m = [0]; var k = [0]; var f = [0]; var score = 0; var level = 0; 

Переделаем переменные map, things, keys, start, finish внутри script.js в массивы, слегка изменив их название:

// 1 уровень  mapArray[0] = [ 		   [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,0,-300,70,0,0,200,500,"#F000FF"], 		   [0,-86,-786,90,0,0,200,500,"#F000FF"], 		   [-500,0,-300,20,0,0,200,500,"#00FF00"], 		   [0,100,0,90,0,0,2000,2000,"#666666"] ];  thingsArray [0] = [[900,50,-900,0,0,0,50,50,"#FFFF00"], 			  [-400,50,900,0,0,0,50,50,"#FFFF00"], 			  [-400,50,-300,0,0,0,50,50,"#FFFF00"]]; 			   keysArray [0] = [[-100,50,600,0,0,0,50,50,"#FF0000"]];	  startArray[0] = [[-900,0,-900,0,0]];  finishArray [0] = [[-900,50,900,0,0,0,50,50,"#00FFFF"]]; 

Добавим 2-й уровень:

// 2 уровень  mapArray [1] = [ 		   [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,0,-300,70,0,0,200,500,"#F000FF"], 		   [0,-86,-786,90,0,0,200,500,"#F000FF"], 		   [-500,0,-300,20,0,0,200,500,"#00FF00"], 		   [0,100,0,90,0,0,2000,2000,"#666666"] ];  thingsArray [1] = [[900,50,-900,0,0,0,50,50,"#FFFF00"], 			  [-400,50,900,0,0,0,50,50,"#FFFF00"], 			  [-400,50,-300,0,0,0,50,50,"#FFFF00"]]; 			   keysArray [1] = [[-100,50,600,0,0,0,50,50,"#FF0000"]];  startArray[1] = [[0,0,0,0,0]];	  finishArray [1] = [[-900,50,900,0,0,0,50,50,"#00FFFF"]]; 

А сами массивы инициализируем перед уровнями:

// Инициализация массива уровней  var mapArray = new Array(); var thingsArray = new Array(); var keysArray = new Array(); var startArray = new Array(); var finishArray = new Array(); 

функцию CreateNewWorld() придется изменить, добавив туда аргумент:

function CreateNewWorld(map){ 	CreateSquares(map,"map"); } 

Изменим вызов CreateNewWorld() в файле menu.js:

button1.onclick = function(){ 	menu1.style.display = "none"; 	CreateNewWorld(map); 	pawn.x = start[0][0]; 	pawn.y = start[0][1]; 	pawn.z = start[0][2]; 	pawn.rx = start[0][3]; 	pawn.rx = start[0][4]; 	CreateSquares(things,"thing"); 	CreateSquares(keys,"key"); 	CreateSquares(finish,"finish"); 	TimerGame = setInterval(repeatFunction,10); 	canlock = true; } 

Теперь при запуске консоль выдаст ошибку. Верно, ведь мы переименовали переменные map, things, keys и finish, теперь javascript не может понять, что это за переменные. Заново их инициализируем в script.js:

// Инициализация переменных уровней  var map; var things; var keys; var start; var finish; 

А в button1 (в menu.js) этим переменным присвоим копии элементов массивов mapArray, thingsArray, keysArray и finishArray (для лучшей читабельности поставим комментарии):

button1.onclick = function(){ 	 	// Присвоение копий массивов 	 	map = userSlice(mapArray[level]); 	things = userSlice(thingsArray[level]); 	keys = userSlice(keysArray[level]);         start = userSlice(startArray[level]); 	finish = userSlice(finishArray[level]);	  	// Создание мира и расстановка предметов 	 	menu1.style.display = "none"; 	CreateNewWorld(map); 	pawn.x = start[0][0]; 	pawn.y = start[0][1]; 	pawn.z = start[0][2]; 	pawn.rx = start[0][3]; 	pawn.rx = start[0][4]; 	CreateSquares(things,"thing"); 	CreateSquares(keys,"key"); 	CreateSquares(finish,"finish"); 	 	// Запуск игры 	 	TimerGame = setInterval(repeatFunction,10); 	canlock = true; } 

Где userSlice() – функция, которая копирует массив:

function userSlice(array){ 	let NewArray = new Array(); 	for (let i = 0; i < array.length; i++){ 		NewArray[i] = new Array(); 		for (let j = 0; j < array[i].length; j++){ 			NewArray[i][j] = array[i][j]; 		} 	} 	return NewArray; } 

Если бы мы просто написали, к примеру, keys = keysArray[level], то в переменные были бы переданы не копии массивов, а указатели на них, а значит, они изменялись бы в процессе игры, что недопустимо, ибо при повторном запуске ключа на исходном месте уже не было бы. Вероятно, вы спросите, почему я не применил просто keysArray[level].slice(), а изобрел свои функции? Ведь slice() тоже копирует массивы. Я пробовал так сделать, однако он копировал именно ссылку на массив, а не сам массив, в результате чего изменение keys приводило к изменению keysArray[level], что означало пропадание ключа при повторном запуске. Дело в том, что в документации написано, что в одних случаях он воспринимает массивы как массивы и копирует их, в других же он воспринимает массивы как объекты и копирует лишь указатели на них. Как он это определяет, для меня загадка, поэтому если мне кто-нибудь подскажет, почему slice() не работает как планировалось, то я буду ему сильно благодарен.

Сделаем переход уровней. Это довольно просто. Изменим finishInteract(), добавив внутрь else следующие строки:

level++; if(level >= 2){ 	level = 0; 	score = 0; }; 

То есть, значение уровня прибавляется на 1, а если все уровни пройдены (у нас их 2), то уровни сбрасываются и очки score сбрасываются. Проверить это трудно, так как наши уровни сейчас ничем не отличаются. Изменим тогда mapArray[1]:

mapArray[1] = [ 		   [0,0,1000,0,180,0,2000,200,"#00FF00"], 		   [0,0,-1000,0,0,0,2000,200,"#00FF00"], 		   [1000,0,0,0,-90,0,2000,200,"#00FF00"], 		   [-1000,0,0,0,90,0,2000,200,"#00FF00"], 		   [0,100,0,90,0,0,2000,2000,"#666666"] ]; 

Мы поменяли цвет стен. Поиграем в игру. Видим, что после прохождения первого уровня (с фиолетовыми стенками и несколькими прямоугольниками) мы переходим ко второму (с зелеными стенками), а когда проходим второй, то возвращаемся обратно к первому. Итак, переход уровней мы закончили. Осталось только оформить игру, изменив шрифты, подкрасив мир, да и уровни сделать просто чуть посложнее. файлы index.html и style.css мы не изменяли, поэтому проверьте скрипты:

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; 	this.vx = 3; 	this.vy = 5; 	this.vz = 3; }  // Инициализация массива уровней  var mapArray = new Array(); var thingsArray = new Array(); var keysArray = new Array(); var startArray = new Array(); var finishArray = new Array();  // 1 уровень  mapArray[0] = [ 		   [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,0,-300,70,0,0,200,500,"#F000FF"], 		   [0,-86,-786,90,0,0,200,500,"#F000FF"], 		   [-500,0,-300,20,0,0,200,500,"#00FF00"], 		   [0,100,0,90,0,0,2000,2000,"#666666"] ];  thingsArray[0] = [[900,50,-900,0,0,0,50,50,"#FFFF00"], 			  [-400,50,900,0,0,0,50,50,"#FFFF00"], 			  [-400,50,-300,0,0,0,50,50,"#FFFF00"]]; 			   keysArray[0] = [[-100,50,600,0,0,0,50,50,"#FF0000"]];	  startArray[0] = [[-900,0,-900,0,0]];  finishArray[0] = [[-900,50,900,0,0,0,50,50,"#00FFFF"]];   // 2 уровень  mapArray[1] = [ 		   [0,0,1000,0,180,0,2000,200,"#00FF00"], 		   [0,0,-1000,0,0,0,2000,200,"#00FF00"], 		   [1000,0,0,0,-90,0,2000,200,"#00FF00"], 		   [-1000,0,0,0,90,0,2000,200,"#00FF00"], 		   [0,100,0,90,0,0,2000,2000,"#666666"] ];  thingsArray[1] = [[900,50,-900,0,0,0,50,50,"#FFFF00"], 			  [-400,50,900,0,0,0,50,50,"#FFFF00"], 			  [-400,50,-300,0,0,0,50,50,"#FFFF00"]]; 			   keysArray[1] = [[-100,50,600,0,0,0,50,50,"#FF0000"]];	  startArray[1] = [[0,0,0,0,0]];  finishArray[1] = [[-900,50,900,0,0,0,50,50,"#00FFFF"]];  // Инициализация переменных уровней  var map = new Array(); var things = new Array(); var keys = new Array(); var start = new Array(); var finish = new Array();  // Нажата ли клавиша и двигается ли мышь?  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 = false; var container = document.getElementById("container"); var world = document.getElementById("world"); var g = 0.1; var dx = dy = dz = 0; var canlock = false;   // Обработчик проверки изменения состояния захвата курсора  document.addEventListener("pointerlockchange", (event)=>{ 	lock = !lock; });  // Обработчик захвата курсора мыши  container.onclick = function(){ 	if (!lock && canlock) 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){ 		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);  function update(){ 	 	// Задаем локальные переменные смещения 	 	dx =   ((PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg))*pawn.vx; 	dz = ( -(PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg))*pawn.vz; 	dy = dy + g; 	if (onGround){ 		dy = 0; 		if (PressUp){ 			dy = - PressUp*pawn.vy; 			onGround = false; 		} 	}; 	drx = MouseY; 	dry = - MouseX; 	 	// Обнулим смещения мыши: 	 	MouseX = MouseY = 0; 	 	// Проверяем коллизию с прямоугольниками 	 	collision(); 	 	// Прибавляем смещения к координатам 	 	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(map){ 	CreateSquares(map,"map"); }  function clearWorld(){ 	world.innerHTML = ""; }  function collision(){ 	 	onGround = false; 	 	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 normal = coorReTransform(0,0,1,map[i][3],map[i][4],map[i][5]); 		 			// Условие коллизии и действия при нем 		 			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; 				let point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]); 				let point3 = coorReTransform(point1[0],point1[1],0,map[i][3],map[i][4],map[i][5]); 				dx = point2[0] - x0; 				dy = point2[1] - y0; 				dz = point2[2] - z0; 				if (Math.abs(normal[1]) > 0.8){ 					if (point3[1] > point2[1]) onGround = true; 				} 				else dy = y1 - y0; 			} 			 		} 	}; }  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]; };  function CreateSquares(squares,string){ 	for (let i = 0; i < squares.length; i++){ 		 		// Создание прямоугольника и придание ему стилей 		 		let newElement = document.createElement("div"); 		newElement.className = string + " square"; 		newElement.id = string + i; 		newElement.style.width = squares[i][6] + "px"; 		newElement.style.height = squares[i][7] + "px"; 		newElement.style.background = squares[i][8]; 		newElement.style.transform = "translate3d(" + 		(600 - squares[i][6]/2 + squares[i][0]) + "px," + 		(400 - squares[i][7]/2 + squares[i][1]) + "px," + 		(squares[i][2]) + "px)" + 		"rotateX(" + squares[i][3] + "deg)" + 		"rotateY(" + squares[i][4] + "deg)" + 		"rotateZ(" + squares[i][5] + "deg)"; 		 		// Вставка прямоугольника в world 		 		world.append(newElement); 	} } 

menu.js

// Создаем переменные  var menu1 = document.getElementById("menu1"); var menu2 = document.getElementById("menu2"); var menu3 = document.getElementById("menu3"); var button1 = document.getElementById("button1"); var button2 = document.getElementById("button2"); var button3 = document.getElementById("button3"); var button4 = document.getElementById("button4"); var m = [0]; var k = [0]; var f = [0]; var level = 0;  // Настроим переходы  button1.onclick = function(){ 	 	// Присвоение копий массивов 	 	map = userSlice(mapArray[level]); 	things = userSlice(thingsArray[level]); 	keys = userSlice(keysArray[level]); 	start = userSlice(startArray[level]); 	finish = userSlice(finishArray[level]); 	 	// Создание мира и расстановка предметов 	 	menu1.style.display = "none"; 	CreateNewWorld(map); 	pawn.x = start[0][0]; 	pawn.y = start[0][1]; 	pawn.z = start[0][2]; 	pawn.rx = start[0][3]; 	pawn.rx = start[0][4]; 	CreateSquares(things,"thing"); 	CreateSquares(keys,"key"); 	CreateSquares(finish,"finish"); 	 	// Запуск игры 	 	TimerGame = setInterval(repeatFunction,10); 	canlock = true; }  button2.onclick = function(){ 	menu1.style.display = "none"; 	menu2.style.display = "block"; }  button3.onclick = function(){ 	menu1.style.display = "block"; 	menu2.style.display = "none"; }  button4.onclick = function(){ 	menu1.style.display = "block"; 	menu3.style.display = "none"; }  // Функция проверки взаимодействия  function interact(objects,string,num){ 	for (i = 0; i < objects.length; i++){ 		let r = (objects[i][0] - pawn.x)**2 + (objects[i][1] - pawn.y)**2 + (objects[i][2] - pawn.z)**2; 		if(r < (objects[i][7]**2)){ 			document.getElementById(string + i).style.display = "none"; 			objects[i][0] = 1000000; 			objects[i][1] = 1000000; 			objects[i][2] = 1000000; 			document.getElementById(string + i).style.transform =  			"translate3d(1000000px,1000000px,1000000px)"; 			num[0]++; 		}; 	}; }  // Функция проверки взаимодействия с финишом  function finishInteract(){ 	let r = (finish[0][0] - pawn.x)**2 + (finish[0][1] - pawn.y)**2 + (finish[0][2] - pawn.z)**2; 	if(r < (finish[0][7]**2)){ 		if (k[0] == 0){ 			console.log("найдите ключ"); 		} 		else{ 			clearWorld(); 			clearInterval(TimerGame); 			document.exitPointerLock(); 			score = score + m[0]; 			k[0] = 0; 			m[0] = 0; 			menu1.style.display = "block"; 			level++; 			if(level >= 2){ 				level = 0; 				score = 0; 			}; 		}; 	}; };  // Функция, повторяющаяся в игре  function repeatFunction(){ 	update(); 	interact(things,"thing",m); 	interact(keys,"key",k); 	finishInteract(); }   // Пользовательский slice  function userSlice(array){ 	let NewArray = new Array(); 	for (let i = 0; i < array.length; i++){ 		NewArray[i] = new Array(); 		for (let j = 0; j < array[i].length; j++){ 			NewArray[i][j] = array[i][j]; 		} 	} 	return NewArray; } 

4. Оформим игру.

4.1 Изменим уровни

Создание уровней – очень интересное занятие. Как правило, этим занимаются отдельные люди, которых называют дизайнерами уровней. У нас уровень представляет из себя массивы чисел, которые скриптами из script.js преобразуются в трехмерный мир. Можно написать отдельную программу, упрощающую создание миров, но сейчас мы это делать не будем. Откроем файл script.js и загрузим туда массивы готовых лабиринтов:

Массивы уровней

// 1 уровень  mapArray[0] = [ 		   //основание 		   [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,"#EEEEEE"], 		    		   //1 		   [-700,0,-800,0,180,0,600,200,"#F0C0FF"], 		   [-700,0,-700,0,0,0,600,200,"#F0C0FF"], 		   [-400,0,-750,0,90,0,100,200,"#F0C0FF"], 		    		   //2 		   [100,0,-800,0,180,0,600,200,"#F0C0FF"], 		   [50,0,-700,0,0,0,500,200,"#F0C0FF"], 		   [400,0,-550,0,90,0,500,200,"#F0C0FF"], 		   [-200,0,-750,0,-90,0,100,200,"#F0C0FF"], 		   [300,0,-500,0,-90,0,400,200,"#F0C0FF"], 		   [350,0,-300,0,0,0,100,200,"#F0C0FF"], 		    		   //3 		   [700,0,-800,0,180,0,200,200,"#F0C0FF"], 		   [700,0,500,0,0,0,200,200,"#F0C0FF"], 		   [700,0,-150,0,90,0,1100,200,"#F0C0FF"], 		   [600,0,-150,0,-90,0,1300,200,"#F0C0FF"], 		   [800,0,-750,0,90,0,100,200,"#F0C0FF"], 		   [800,0,450,0,90,0,100,200,"#F0C0FF"], 		   [750,0,400,0,180,0,100,200,"#F0C0FF"], 		   [750,0,-700,0,0,0,100,200,"#F0C0FF"], 		    		   //4 		   [850,0,-100,0,180,0,300,200,"#F0C0FF"], 		   [850,0,0,0,0,0,300,200,"#F0C0FF"], 		    		   //5 		   [400,0,300,0,90,0,800,200,"#F0C0FF"], 		   [300,0,300,0,-90,0,800,200,"#F0C0FF"], 		   [350,0,-100,0,180,0,100,200,"#F0C0FF"], 		    		   //6 		   [400,0,800,0,0,0,800,200,"#F0C0FF"], 		   [450,0,700,0,180,0,700,200,"#F0C0FF"], 		   [800,0,750,0,90,0,100,200,"#F0C0FF"], 		   [100,0,550,0,90,0,300,200,"#F0C0FF"], 		   [0,0,650,0,-90,0,300,200,"#F0C0FF"], 		   [-100,0,500,0,0,0,200,200,"#F0C0FF"], 		   [-100,0,400,0,180,0,400,200,"#F0C0FF"], 		   [-200,0,750,0,90,0,500,200,"#F0C0FF"], 		   [-300,0,700,0,-90,0,600,200,"#F0C0FF"], 		    		   //7 		   [100,0,-250,0,90,0,900,200,"#F0C0FF"], 		   [0,0,-300,0,-90,0,800,200,"#F0C0FF"], 		   [-350,0,200,0,0,0,900,200,"#F0C0FF"], 		   [-350,0,100,0,180,0,700,200,"#F0C0FF"], 		   [-700,0,-50,0,90,0,300,200,"#F0C0FF"], 		   [-800,0,0,0,-90,0,400,200,"#F0C0FF"], 		   [-750,0,-200,0,180,0,100,200,"#F0C0FF"], 		    		   //8 		   [-500,0,600,0,90,0,800,200,"#F0C0FF"], 		   [-600,0,600,0,-90,0,800,200,"#F0C0FF"], 		    		   //9 		   [-600,0,-500,0,180,0,800,200,"#F0C0FF"], 		   [-650,0,-400,0,0,0,700,200,"#F0C0FF"], 		   [-200,0,-300,0,90,0,400,200,"#F0C0FF"], 		   [-300,0,-300,0,-90,0,200,200,"#F0C0FF"], 		   [-350,0,-100,0,0,0,300,200,"#F0C0FF"], 		   [-400,0,-200,0,180,0,200,200,"#F0C0FF"], 		   [-500,0,-150,0,-90,0,100,200,"#F0C0FF"], 		    		   //10 		   [-900,0,500,0,0,0,200,200,"#F0C0FF"], 		   [-900,0,400,0,180,0,200,200,"#F0C0FF"], 		   [-800,0,450,0,90,0,100,200,"#F0C0FF"] 		   ];  thingsArray[0] = [[900,50,-900,0,0,0,50,50,"#FFFF00"], 			  [-400,50,900,0,0,0,50,50,"#FFFF00"], 			  [-400,50,-300,0,0,0,50,50,"#FFFF00"]]; 			   keysArray[0] = [[-100,50,600,0,0,0,50,50,"#FF0000"]];	  startArray[0] = [[-900,0,-900,0,0]];  finishArray[0] = [[-900,50,900,0,0,0,50,50,"#00FFFF"]];  // 2 уровень  mapArray[1] = [ 		   //основание 		   [0,0,1200,0,180,0,2400,200,"#C0FFE0"], 		   [0,0,-1200,0,0,0,2400,200,"#C0FFE0"], 		   [1200,0,0,0,-90,0,2400,200,"#C0FFE0"], 		   [-1200,0,0,0,90,0,2400,200,"#C0FFE0"], 		   [0,100,0,90,0,0,2400,2400,"#EEEEEE"], 		    		   //1 		   [1100,0,-800,0,180,0,200,200,"#C0FFE0"], 		   [1000,0,-900,0,90,0,200,200,"#C0FFE0"], 		   [850,0,-1000,0,180,0,300,200,"#C0FFE0"], 		   [700,0,-950,0,-90,0,100,200,"#C0FFE0"], 		   [800,0,-900,0,0,0,200,200,"#C0FFE0"], 		   [900,0,-700,0,-90,0,400,200,"#C0FFE0"], 		   [750,0,-500,0,180,0,300,200,"#C0FFE0"], 		   [600,0,-450,0,-90,0,100,200,"#C0FFE0"], 		   [800,0,-400,0,0,0,400,200,"#C0FFE0"], 		   [1000,0,-550,0,90,0,300,200,"#C0FFE0"], 		   [1100,0,-700,0,0,0,200,200,"#C0FFE0"], 		    		   //2 		   [800,0,-200,0,180,0,800,200,"#C0FFE0"], 		   [400,0,-300,0,90,0,200,200,"#C0FFE0"], 		   [300,0,-400,0,180,0,200,200,"#C0FFE0"], 		   [200,0,-700,0,90,0,600,200,"#C0FFE0"], 		   [50,0,-1000,0,180,0,300,200,"#C0FFE0"], 		   [-100,0,-950,0,-90,0,100,200,"#C0FFE0"], 		   [0,0,-900,0,0,0,200,200,"#C0FFE0"], 		   [100,0,-600,0,-90,0,600,200,"#C0FFE0"], 		   [200,0,-300,0,0,0,200,200,"#C0FFE0"], 		   [300,0,-200,0,-90,0,200,200,"#C0FFE0"], 		   [750,0,-100,0,0,0,900,200,"#C0FFE0"], 		    		   //3 		   [500,0,-950,0,90,0,500,200,"#C0FFE0"], 		   [450,0,-700,0,0,0,100,200,"#C0FFE0"], 		   [400,0,-950,0,-90,0,500,200,"#C0FFE0"], 		    		   //4 		   [-700,0,-600,0,0,0,1000,200,"#C0FFE0"], 		   [-200,0,-500,0,-90,0,200,200,"#C0FFE0"], 		   [-300,0,-400,0,180,0,200,200,"#C0FFE0"], 		   [-400,0,-250,0,-90,0,300,200,"#C0FFE0"], 		   [-350,0,-100,0,0,0,100,200,"#C0FFE0"], 		   [-300,0,-200,0,90,0,200,200,"#C0FFE0"], 		   [-200,0,-300,0,0,0,200,200,"#C0FFE0"], 		   [-100,0,-500,0,90,0,400,200,"#C0FFE0"], 		   [-650,0,-700,0,180,0,1100,200,"#C0FFE0"], 		    		   //5 		   [-300,0,-850,0,90,0,300,200,"#C0FFE0"], 		   [-350,0,-1000,0,180,0,100,200,"#C0FFE0"], 		   [-400,0,-850,0,-90,0,300,200,"#C0FFE0"], 		    		   //6 		   [-600,0,-1050,0,90,0,300,200,"#C0FFE0"], 		   [-650,0,-900,0,0,0,100,200,"#C0FFE0"], 		   [-700,0,-1050,0,-90,0,300,200,"#C0FFE0"], 		    		   //7 		   [-900,0,-850,0,90,0,300,200,"#C0FFE0"], 		   [-950,0,-1000,0,180,0,100,200,"#C0FFE0"], 		   [-1000,0,-850,0,-90,0,300,200,"#C0FFE0"], 		    		   //8 		   [-600,0,-250,0,90,0,700,200,"#C0FFE0"], 		   [-650,0,100,0,0,0,100,200,"#C0FFE0"], 		   [-700,0,-250,0,-90,0,700,200,"#C0FFE0"], 		    		   //9 		   [-900,0,-150,0,90,0,900,200,"#C0FFE0"], 		   [-500,0,300,0,180,0,800,200,"#C0FFE0"], 		   [-100,0,650,0,90,0,700,200,"#C0FFE0"], 		   [-300,0,1000,0,0,0,400,200,"#C0FFE0"], 		   [-500,0,950,0,-90,0,100,200,"#C0FFE0"], 		   [-350,0,900,0,180,0,300,200,"#C0FFE0"], 		   [-200,0,650,0,-90,0,500,200,"#C0FFE0"], 		   [-600,0,400,0,0,0,800,200,"#C0FFE0"], 		   [-1000,0,-100,0,-90,0,1000,200,"#C0FFE0"], 		    		   //10 		   [-300,0,200,0,90,0,200,200,"#C0FFE0"], 		   [-350,0,100,0,180,0,100,200,"#C0FFE0"], 		   [-400,0,200,0,-90,0,200,200,"#C0FFE0"], 		    		   //11 		   [-800,0,600,0,180,0,800,200,"#C0FFE0"], 		   [-400,0,650,0,90,0,100,200,"#C0FFE0"], 		   [-800,0,700,0,0,0,800,200,"#C0FFE0"], 		    		   //12 		   [-700,0,1050,0,90,0,300,200,"#C0FFE0"], 		   [-850,0,900,0,180,0,300,200,"#C0FFE0"], 		   [-1000,0,950,0,-90,0,100,200,"#C0FFE0"], 		   [-900,0,1000,0,0,0,200,200,"#C0FFE0"], 		   [-800,0,1100,0,-90,0,200,200,"#C0FFE0"], 		    		   //13 		   [1050,0,700,0,180,0,300,200,"#C0FFE0"], 		   [900,0,800,0,-90,0,200,200,"#C0FFE0"], 		   [550,0,900,0,180,0,700,200,"#C0FFE0"], 		   [200,0,650,0,90,0,500,200,"#C0FFE0"], 		   [300,0,400,0,0,0,200,200,"#C0FFE0"], 		   [400,0,300,0,90,0,200,200,"#C0FFE0"], 		   [550,0,200,0,0,0,300,200,"#C0FFE0"], 		   [700,0,150,0,90,0,100,200,"#C0FFE0"], 		   [500,0,100,0,180,0,400,200,"#C0FFE0"], 		   [300,0,200,0,-90,0,200,200,"#C0FFE0"], 		   [200,0,300,0,180,0,200,200,"#C0FFE0"], 		   [100,0,650,0,-90,0,700,200,"#C0FFE0"], 		   [550,0,1000,0,0,0,900,200,"#C0FFE0"], 		   [1000,0,900,0,90,0,200,200,"#C0FFE0"], 		   [1100,0,800,0,0,0,200,200,"#C0FFE0"], 		    		   //14 		   [700,0,700,0,90,0,400,200,"#C0FFE0"], 		   [850,0,500,0,0,0,300,200,"#C0FFE0"], 		   [1000,0,300,0,90,0,400,200,"#C0FFE0"], 		   [950,0,100,0,180,0,100,200,"#C0FFE0"], 		   [900,0,250,0,-90,0,300,200,"#C0FFE0"], 		   [750,0,400,0,180,0,300,200,"#C0FFE0"], 		   [600,0,650,0,-90,0,500,200,"#C0FFE0"], 		    		   //15 		   [500,0,600,0,180,0,200,200,"#C0FFE0"], 		   [400,0,650,0,-90,0,100,200,"#C0FFE0"], 		   [500,0,700,0,0,0,200,200,"#C0FFE0"] 		   ];  thingsArray[1] = [[1100,50,900,0,0,0,50,50,"#FFFF00"], 			  [500,50,800,0,0,0,50,50,"#FFFF00"], 			  [-800,50,-500,0,0,0,50,50,"#FFFF00"], 			  [-900,50,1100,0,0,0,50,50,"#FFFF00"], 			  [-1100,50,-800,0,0,0,50,50,"#FFFF00"] 			  ]; 			   keysArray[1] = [[1100,50,-900,0,0,0,50,50,"#FF0000"]];	  startArray[1] = [[0,0,0,0,0]];  finishArray[1] = [[-1100,50,-500,0,0,0,50,50,"#00FFFF"]]; 

Теперь мы можем поиграть в игру. В результате уровни выглядят вот так:

Ориентироваться в таком мире крайне сложно. Плюс передвижение вдоль стенок содержит баги, так как на углах стенок игрок может застрять. Исправим это в collision(), заменив числа 98 на 90:

// Условие коллизии и действия при нем 		 			if (Math.abs(point1[0])<(map[i][6]+90)/2 && Math.abs(point1[1])<(map[i][7]+90)/2 && Math.abs(point1[2]) < 50){ 

4.2 Добавим статическое освещение

Чтобы ориентироваться стало проще, реализуем статическое солнечное освещение (без теней). Добавим вектор солнечного света:

var sun = [0.48,0.8,0.36]; 

Как создать освещенность? Посмотрите на рисунок:

Если вектор sun точно противонаправлен вектору n, то освещение максимально. Интенсивность освещенности зависит от угла падения света на поверхность. Если же луч света падает параллельно плоскости или падает с противоположной его стороны, то плоскость не освещается. Посчитать угол падения можно с помощью скалярного произведения n*sun: если оно отрицательно, то освещенность зависит от модуля скалярного произведения, а если положительно, то освещенность отсутствует. Освещенность поверхностей создадим при генерации мира, то есть, в CreateNewWorld(). А так как там есть только функция CreateSquare(), то и освещенность будем применять там. Но овещенность мы применим, пожалуй, только к миру, но не к вещам, так что добавим туда аргумент освещенности, да и сам CreateSquare() изменим:

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

Включим освещенность при генерации мира в CreateNewWorld():

function CreateNewWorld(map){ 	CreateSquares(map,"map",true); } 

И добавим отключение освещенности для предметов в button1.onclick (в CreateSquares последний параметр для них — false):

// Создание мира и расстановка предметов 	 	menu1.style.display = "none"; 	CreateNewWorld(map); 	pawn.x = start[0][0]; 	pawn.y = start[0][1]; 	pawn.z = start[0][2]; 	pawn.rx = start[0][3]; 	pawn.rx = start[0][4]; 	CreateSquares(things,"thing",false); 	CreateSquares(keys,"key",false); 	CreateSquares(finish,"finish",false); 

Запустим игру и заметим, что освещение стало более реалистичным, а ориентироваться в пространстве намного проще:

Добавим голубое небо. Зададим фон для #container в style.css:

background-color:#C0FFFF; 

Небо стало голубым:

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

4.3 Добавим вращение и свет предметам

В menu.js создадим отельную функцию вращения:

function rotate(objects,string,wy){ 	for (i = 0; i < objects.length; i++){ 		objects[i][4] = objects[i][4] + wy; 		document.getElementById(string + i).style.transform = "translate3d(" + 		(600 - objects[i][6]/2 + objects[i][0]) + "px," + 		(400 - objects[i][7]/2 + objects[i][1]) + "px," + 		(objects[i][2]) + "px)" + 		"rotateX(" + objects[i][3] + "deg)" + 		"rotateY(" + objects[i][4] + "deg)" + 		"rotateZ(" + objects[i][5] + "deg)"; 	}; } 

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

function repeatFunction(){ 	update(); 	interact(things,"thing",m); 	interact(keys,"key",k); 	rotate(things,"thing",0.5); 	rotate(keys,"key",0.5); 	rotate(finish,"finish",0.5);         finishInteract(); } 

Правда функцию rotate можно использовать не только для вращения предметов, но и их передвижения. Итак, предметы вращаются. Но если мы сделаем эти предметы светящимися, то будет вообще супер. Зададим для них цветные тени в style.css:

.thing{ 	box-shadow: 0 0 10px #FFFF00; } .key{ 	box-shadow: 0 0 10px #FF0000; } .finish{ 	box-shadow: 0 0 10px #00FFFF; } 

Теперь игрок точно понимает, что с этими предметами можно взаимодействовать.

4.4 Добавим виджеты

Обычно виджеты показывают количество очков, здоровье и другие необходимые числовые данные. У нас они будут показывать количество собранных монет (желтых квадратов) и ключей (красных квадратов), а изменять их можно из javascript. Сначала добавим в html новые элементы:

<div id="container"> 		<div id="world"></div> 		<div id="pawn"></div> 		<div class = "widget" id = "widget1"></div> 		<div class = "widget" id = "widget2"></div>                 <div class = "widget" id = "widget3"></div> 		… 

В menu.js привяжем к ним переменные:

var widget1 = document.getElementById("widget1"); var widget2 = document.getElementById("widget2"); var widget3 = document.getElementById("widget3"); 

А внутри button1.onclick() к ним добавим текст:

widget1.innerHTML = "<p style='font-size:30px'>Монеты: 0 из 0" </p>"; widget2.innerHTML = "<p style='font-size:30px'>Ключи:0</p>"; widget3.innerHTML = "<p style='font-size:40px'>Найдите красный квадрат!</p>"; 

Зададим стили для них в style.css():

/* Оформление виджетов */  .widget{ 	display:none; 	position:absolute; 	background-color:#FFF; 	opacity:0.8; 	z-index:300; } #widget1{ 	top:0px; 	left:0px; 	width:300px; 	height:100px; } #widget2{ 	top:0px; 	right:0px; 	width:300px; 	height:100px; } #widget3{ 	bottom:0px; 	left:0px; 	width:500px; 	height:200px; } 

Изначально они невидимы. Сделаем видимыми первые 2 виджета при запуске уровня внутри button1.onclick:

       // Вывод виджетов на экран и их настройка 	 	widget1.style.display = "block"; 	widget2.style.display = "block"; 	widget1.innerHTML = "<p style='font-size:30px'>Монеты: 0 из " + things.length + " </p>"; 	widget2.innerHTML = "<p style='font-size:30px'>Ключи:0</p>"; 	widget3.innerHTML = "<p style='font-size:40px'>Найдите красный квадрат!</p>"; 

Виджеты есть, но при взаимодействии с предметами еще ничего не происходит. Будем менять надписи виджетов при взаимодействии из функций interact (внутри if(r < (objects[i][7]**2)){…}):

			widget1.innerHTML = "<p style='font-size:30px'>Монеты: " + m[0] + " из " + things.length + " </p>"; 			widget2.innerHTML = "<p style='font-size:30px'>Ключи: " + k[0] + "</p>"; 

Теперь при взятии монет и ключа информация в виджетах меняется. Но при завершении игры виджеты не скрываются. Скроим их по окончании игры, добавив в finishInteract() внутрь else следующие строки:

widget1.style.display = «none»;
widget2.style.display = «none»;
widget3.style.display = «none»;

Виджеты скрыты. Осталось настроить виджет, который просит взять ключ в случае прихода к финишу без него. В finishInteract() вместо console.log(«найдите ключ») вставим следующие строки:

widget3.style.display = "block"; setTimeout(() => widget3.style.display = "none",5000); 

При неудачной попытки окончания игры мы, получаем сообщение, которое скрывается через 5 секунд. Наша игра сейчас выглядит вот так:

4.5 Оформим текст.

Создадим в папке с файлами папку Fonts. Скачаем отсюда файл font1.woff и вставим его в Fonts. В style.css добавим стили текста:

/* Оформление текста */  p{ 	margin:0px; 	font-size:60px; 	position:absolute; 	display:block; 	top:50%; 	left:50%; 	transform:translate(-50%,-50%); 	user-select:none; 	font-family:fontlab; }  @font-face{ 	font-family:fontlab; 	src:url("Fonts/font1.woff"); } 

Меню и игра преобразились:

4.6 Добавим звуки.

Скачаем отсюда архив со звуками Sounds.zip. Создадим в папке с проектом папку Sounds и вставьте туда звуки (они находятся в формате mp3). Сделаем переменные-ссылки на эти звуки:

// Загрузка звуков  var clickSound = new Audio; clickSound.src = "Sounds/click.mp3";  var keySound = new Audio; keySound.src = "Sounds/key.mp3";  var mistakeSound = new Audio; mistakeSound.src = "Sounds/mistake.mp3";  var thingSound = new Audio; thingSound.src = "Sounds/thing.mp3";  var winSound = new Audio; winSound.src = "Sounds/win.mp3"; 

В функции interact добавим аргумент звукового файла и проигрывание звука (soundObject.play()):

function interact(objects,string,num,soundObject){ 	for (i = 0; i < objects.length; i++){ 		let r = (objects[i][0] - pawn.x)**2 + (objects[i][1] - pawn.y)**2 + (objects[i][2] - pawn.z)**2; 		if(r < (objects[i][7]**2)){ 			soundObject.play(); 			document.getElementById(string + i).style.display = "none"; 			objects[i][0] = 1000000; 			objects[i][1] = 1000000; 			objects[i][2] = 1000000; 			document.getElementById(string + i).style.transform =  			"translate3d(1000000px,1000000px,1000000px)"; 			num[0]++; 			widget1.innerHTML = "<p style='font-size:30px'>Монеты: " + m[0] + " из " + things.length + " </p>"; 			widget2.innerHTML = "<p style='font-size:30px'>Ключи: " + k[0] + "</p>"; 		}; 	}; } 

В repeatFunction() изменим, соответственно, вызовы этой функции:

interact(things,"thing",m,thingSound); interact(keys,"key",k,keySound); 

А в finishInteract() добавим звуки mistakeSound и winSound:

function finishInteract(){ 	let r = (finish[0][0] - pawn.x)**2 + (finish[0][1] - pawn.y)**2 + (finish[0][2] - pawn.z)**2; 	if(r < (finish[0][7]**2)){ 		if (k[0] == 0){ 			widget3.style.display = "block"; 			setTimeout(() => widget3.style.display = "none",5000); 			mistakeSound.play(); 		} 		else{ 			clearWorld(); 			clearInterval(TimerGame); 			document.exitPointerLock(); 			score = score + m[0]; 			k[0] = 0; 			m[0] = 0; 			level++; 			menu1.style.display = "block"; 			widget1.style.display = "none"; 			widget2.style.display = "none"; 			widget3.style.display = "none"; 			winSound.play(); 			if(level >= 2){ 				level = 0; 				score = 0; 			}; 		}; 	}; }; 

При клике любой кнопки меню проиграем звук clickSound:

button1.onclick = function(){ 	 	clickSound.play(); 	 	...  }  button2.onclick = function(){ 	 	clickSound.play(); 	 	menu1.style.display = "none"; 	menu2.style.display = "block"; }  button3.onclick = function(){ 	 	clickSound.play(); 	 	menu1.style.display = "block"; 	menu2.style.display = "none"; }  button4.onclick = function(){ 	 	clickSound.play(); 	 	menu1.style.display = "block"; 	menu3.style.display = "none"; } 

Игра заиграла ярче. Осталось настроить вывод результатов после прохождения всех уровней:

4.7 Вывод результатов.

В menu.js в finishInteract() внутрь if(level >= 2){…} добавим строки:

if(level >= 2){ menu1.style.display = "none"; 	menu3.style.display = "block"; 	document.getElementById("result").innerHTML = "Вы набрали " + score + " очков"; 	level = 0; 	score = 0; }; 

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

canlock = false; 

А также:

button1.innerHTML = "<p>Продолжить</p>"; 

и

button1.innerHTML = "<p>Начать игру</p>"; 

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

function finishInteract(){ 	let r = (finish[0][0] - pawn.x)**2 + (finish[0][1] - pawn.y)**2 + (finish[0][2] - pawn.z)**2; 	if(r < (finish[0][7]**2)){ 		if (k[0] == 0){ 			… 		} 		else{ 			… 			canlock = false; 			button1.innerHTML = "<p>Продолжить</p>"; 			if(level >= 2){ 				menu1.style.display = "none"; 				menu3.style.display = "block"; 				document.getElementById("result").innerHTML = "Вы набрали " + score + " очков"; 				level = 0; 				score = 0; 				button1.innerHTML = "<p>Начать игру</p>"; 			}; 		}; 	}; }; 

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

top:50%; left:50%; transform: translate(-50%,-50%); 

А в body уберем отступы:

body{ 	margin:0px; } 

Итак, мы полностью написали браузерную трехмерную игру лабиринт. Благодаря ей мы обратили внимание на некоторые аспекты языка javascript, узнали о функциях, о которых вы раньше может быть и не слышали. А главное, мы показали, что делать простые игрушки для браузера даже на чистом коде не так уж и сложно. Полный исходный код вы можете скачать отсюда (исходники.zip). Сами скрипты можно существенно улучшить, добавив туда разные библиотеки, написать новые конструкторы или сделать что-нибудь еще.

Спасибо за внимание!

ссылка на оригинал статьи https://habr.com/ru/post/511418/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *