Создание браузерных 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). Сами скрипты можно существенно улучшить, добавив туда разные библиотеки, написать новые конструкторы или сделать что-нибудь еще.

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

    Похожие публикации

    Средняя зарплата в IT

    113 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 5 444 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 4

      +4
      Интересно, каков был ход рассуждений, чтобы выложить исходный код на github в zip архиве?
        0
        Мне показалось, что так удобнее)
          0
          На github в любом случае можно в zip-архиве скачать репозиторий, так что нет нужды
        0
        Спасибо большое! Супер!

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

        Самое читаемое