Creación de juegos 3D basados ​​en navegador desde cero en html puro, css y js. Parte 2/2

En este artículo, continuaremos creando un juego de laberinto de navegador 3D en html, css y javascript puro. En la parte anterior, hicimos un mundo tridimensional simple, implementamos movimiento, control, colisiones del jugador con objetos estáticos. En esta parte agregaremos gravedad, luz solar estática (sin sombras), cargaremos sonidos y crearemos menús. Por desgracia, como en la primera parte, no habrá demos aquí.



Recordemos el código que hicimos en la parte anterior. Tenemos 3 archivos:



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. Aplicación de la física de la gravedad y el salto



Tenemos varias variables que se crean en diferentes partes del archivo javascript. Será mejor si los trasladamos a un solo lugar:



//  

var lock = false;
var onGround = true;
var container = document.getElementById("container");
var world = document.getElementById("world");


Agreguemos aceleración de caída libre:



var g = 0.1;




Agregue 3 variables al constructor del reproductor: vx, vy y 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;
}


Estas son velocidades variables. Al cambiarlos, podemos cambiar la velocidad de carrera y la velocidad de salto inicial del jugador. Por ahora, apliquemos las nuevas variables en 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;


El jugador ahora se mueve más rápido. Pero no cae ni salta. Es necesario permitir un salto cuando está sobre algo. Y se mantendrá en pie cuando choque con una superficie horizontal (o casi). ¿Cómo definir la horizontalidad? Necesitamos encontrar la normal del plano del rectángulo. Esto se hace de forma sencilla. La normal se dirige a lo largo del eje z en relación con las coordenadas del rectángulo. Entonces la normal ha transformado las coordenadas en coordenadas mundiales. Encuentre lo normal (agregue la variable local 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]);


Para que la superficie sea horizontal, el producto escalar de la normal al eje y en las coordenadas mundiales debe ser 1 o -1, y el plano casi horizontal debe estar cerca de 1 o -1. Establezcamos la condición para un plano casi horizontal:



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


No olvide que, en ausencia de colisiones, el jugador definitivamente no estará en el suelo, por lo que, de forma predeterminada, al comienzo de la función collision (), configure onGround = false:



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


Sin embargo, si el jugador choca con la superficie desde abajo, él también aparecerá en el suelo. Para evitar esto, verifiquemos que el jugador esté en la parte superior del avión (el punto 3 [1] debe ser menor que el punto 2 [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;



¿Que estamos haciendo? mire la imagen: la







cruz roja debe estar debajo de la naranja en el sistema de coordenadas mundial (o la coordenada y debe ser más grande). Esto es lo que comprobamos en el punto3 [1]> punto2 [1]. Y point3 son solo las coordenadas del punto rojo. Muevamos la inicialización del punto 2 dentro de la condición de colisión:



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


Pasemos a actualizar (). También haremos cambios aquí. Primero, agregue gravedad y elimine el desplazamiento y al presionar la barra espaciadora:



//    

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; 
 


En segundo lugar, si el jugador está en el suelo, prohibimos la gravedad, prohibimos el desplazamiento en y (de lo contrario, después de caminar sobre una superficie inclinada, el jugador despegará) y agregamos la capacidad de saltar (si la condición (en el suelo)):



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


Naturalmente, inmediatamente después del salto, prohibimos el salto repetido estableciendo el parámetro onGround en falso. En la condición de barra espaciadora, la validez de este parámetro ya no es necesaria:



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


Para probar los cambios, cambiemos el mundo:



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"]
];


Si iniciamos el juego, veremos que el jugador puede escalar una pared verde casi vertical. Deshabilite esto agregando 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;
			}


Por lo tanto, las colisiones con paredes muy verticales no cambian el desplazamiento y. Por lo tanto, el overclocking en tales paredes ahora está completamente excluido. Intentemos escalar la pared verde. No podremos hacer esto ahora. Entonces, descubrimos la gravedad y los saltos, y ahora podemos escalar de manera bastante realista en superficies ligeramente inclinadas. Revisemos el código:



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. Crea un menú



Creemos el menú en forma de paneles html y bloques html. El diseño de todo el menú será aproximadamente el mismo: el fondo y el estilo de los botones se pueden establecer en común para todos. Por lo tanto, establezcamos tres paneles de menú: el menú principal, las instrucciones y la salida de resultados al finalizar el juego. Las transiciones entre el menú, la transición al mundo y viceversa se realizarán mediante scripts javascript. Para no saturar el archivo script.js, cree un nuevo archivo menu.js para la navegación del menú e inclúyalo en index.html:



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


En el contenedor, crearemos 3 elementos que serán barras de menú:



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


Vamos a diseñarlos agregando propiedades para la clase "menú" a style.css:



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


Agregue botones con títulos apropiados al menú (en el archivo 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>


Para los botones, también estableceremos estilos en 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;
}


Pero no vemos el menú, ya que tienen la pantalla: none style. Cuando el juego comienza, uno de los elementos del menú debe estar visible, así que en el html para el primer menú, agregue la entrada style = "display: block;", y se verá así sería así:



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


El menú ahora se ve así:







Genial. Pero si hacemos clic en el botón, se capturará el cursor. Por lo tanto, debemos permitir la captura del mouse solo en el caso de un juego. Para hacer esto, ingrese la variable canlock en script.js y agréguela al elemento crear variables:



//  

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


Ahora podemos hacer clic en el menú. Configuremos las transiciones usando scripts en el archivo 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";
}


Ahora funcionan todos los botones del menú excepto "iniciar juego". Ahora configuremos el botón button1. Si recuerda, en el archivo script.js, las funciones CreateNewWorld () y setInterval () se activan cuando se carga la página web. Eliminémoslos de allí. Solo los llamaremos cuando se presione el botón 1. Vamos a hacerlo:



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


Hemos creado un menú. Sí, todavía es feo, pero mejora fácilmente.



3. Creemos objetos y transición de niveles.



Primero, definamos las reglas del juego. Tenemos tres tipos de artículos: monedas (cuadrados amarillos), llaves (cuadrados rojos) y acabado (cuadrado azul). Las monedas aportan puntos. El jugador necesita encontrar la llave y solo entonces llegar a la línea de meta. Si llega a la meta sin una llave, recibirá un mensaje sobre la necesidad de encontrar primero la llave. Los objetos se crearán de la misma forma que el mapa. Los escribiremos usando matrices. Pero no haremos una función separada para ellos. Simplemente escribiremos una nueva función que organice los elementos del mapa y del rectángulo y transfiera los comandos de CreateNewWorld (). Llamémoslo CreateSquares (). Así que agreguemos la siguiente entrada al final del archivo 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);
	}
}


Y cambiaremos el contenido de createNewWorld ():



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


La cadena es necesaria para establecer la identificación del nombre. El juego no ha cambiado nada todavía. Ahora agreguemos 3 matrices: monedas (cosas), llaves (llaves) y terminar (terminar). Insértelos justo después de la matriz de mapas:



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"]];


Y en menu.js, usemos la función CreateSquares () dentro del controlador para presionar el botón “button1”:



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


Ahora configuremos la desaparición de objetos. En menu.js, creemos una función para verificar las distancias del reproductor a los objetos:



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


Además, en el mismo archivo, cree la función repeatFunction () y agréguele los comandos:



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


Y ejecutaremos su llamada cíclica en setInterval dentro de button1:



TimerGame = setInterval(repeatFunction,10);


Ahora los objetos desaparecen cuando nos acercamos a ellos. Sin embargo, no hacen absolutamente nada. Y queremos que se nos sumen puntos cuando se tomen los cuadrados amarillos, y cuando se tomen los rojos, tenemos la oportunidad de tomar el azul y terminar el juego. Modifiquemos la función 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]++;
		};
	};
}


Cambiemos los parámetros de entrada para las llamadas a esta función:



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


Y al principio del archivo, agregue cuatro nuevas variables:



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


¿Por qué creamos matrices de un elemento, preguntas, y no solo variables? El punto es que queríamos pasar estas variables a interact () por referencia, no por valor. En javascript, las variables regulares se pasan solo por valor y las matrices se pasan por referencia. Si pasamos solo una variable a interact (), entonces num será una copia de la variable. Cambiar num no cambiará k o m. Y si pasamos una matriz, entonces num será una referencia a la matriz kom, y cuando cambiemos num [0], entonces k [0] y m [0] cambiarán. Era posible, por supuesto, crear 2 funciones casi idénticas, pero es mejor arreglárselas con una, un poco más universal.



Para el final, todavía tienes que crear una función separada:



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";
		};
	};
};


Y configure clearWorld () en script.js:



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


Como puede ver, limpiar el mundo es bastante simple. Agregue finishInteract () a repeatFunction ():



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


¿Qué está pasando en finishInteract ()? Si no hemos tomado la clave (k [0] == 0), todavía no pasa nada. Si lo hicieron, entonces el juego termina y sucede lo siguiente: el mundo se borra, la función repeatFunction () se detiene, el cursor deja de ser capturado, el contador de teclas se reinicia y vamos al menú principal. Comprobemos ejecutando el juego. Todo esta funcionando Sin embargo, después de hacer clic de nuevo en el juego, nos encontramos inmediatamente en la línea de meta y algunos elementos desaparecen. Esto se debe a que no ingresamos la ubicación del engendro inicial del jugador y los arreglos cambian durante el juego. Agreguemos un punto de generación para el jugador a button1, es decir, equiparemos sus coordenadas con los elementos de la matriz 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;
}


El jugador ahora aparece en el origen. Pero la pregunta es: ¿y si hay varios niveles en el juego? Agreguemos una variable de nivel a 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;


Rehagamos el mapa de variables, cosas, claves, inicio, finalización dentro de script.js en matrices, cambiando ligeramente su nombre:



// 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"]];


Agreguemos el segundo nivel:



// 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"]];


Y las matrices mismas se inicializan antes que los niveles:



//   

var mapArray = new Array();
var thingsArray = new Array();
var keysArray = new Array();
var startArray = new Array();
var finishArray = new Array();


la función CreateNewWorld () deberá cambiarse agregando un argumento allí:



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


Cambiemos la llamada a CreateNewWorld () en el archivo 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;
}


Ahora la consola dará un error al iniciar. Así es, debido a que cambiamos el nombre del mapa de variables, las cosas, las claves y el final, ahora JavaScript no puede averiguar cuáles son estas variables. Los inicializamos nuevamente en script.js:



//   

var map;
var things;
var keys;
var start;
var finish;


Y en button1 (en menu.js) asignamos copias de los elementos de las matrices mapArray, thingsArray, keysArray y finishArray a estas variables (para una mejor legibilidad, pon comentarios):



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


Donde userSlice () es la función que copia la matriz:



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


Si simplemente escribimos, por ejemplo, keys = keysArray [level], entonces no se transferirían copias de los arreglos a las variables, sino punteros a ellas, lo que significa que cambiarían durante el juego, lo cual es inaceptable, porque cuando reinicia la clave en el lugar original ya no existiría. Probablemente se esté preguntando por qué no solo usé keysArray [level] .slice (), sino que inventé mis propias funciones. Después de todo, slice () también copia matrices. Traté de hacer esto, pero estaba copiando la referencia a la matriz, no la matriz en sí, como resultado de lo cual cambiar las claves llevó a cambiar keysArray [nivel], lo que significaba que la clave desaparecía al reiniciar. El hecho es que la documentación dice que en algunos casos percibe arreglos como arreglos y los copia, en otros percibe arreglos como objetos y copia solo punteros a ellos. Cómo lo define es un misterio para mí,así que si alguien me puede decir por qué slice () no está funcionando como estaba planeado, le estaré muy agradecido.



Hagamos la transición de niveles. Es bastante simple. Modifiquemos finishInteract () agregando las siguientes líneas dentro del else:



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


Es decir, el valor del nivel se suma en 1, y si se pasan todos los niveles (tenemos 2 de ellos), entonces los niveles se reinician y los puntos de puntuación se reinician. Esto es difícil de verificar, ya que nuestros niveles ahora no son diferentes. Cambiemos entonces 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"]
];


Cambiamos el color de las paredes. Vamos a jugar un juego. Vemos que después de pasar el primer nivel (con paredes moradas y varios rectángulos), pasamos al segundo (con paredes verdes), y cuando pasamos el segundo volvemos al primero. Entonces, hemos terminado la transición de niveles. Solo queda diseñar el juego, cambiar las fuentes, colorear el mundo y hacer que los niveles sean un poco más difíciles. No cambiamos los archivos index.html y style.css, así que verifique los scripts:



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. Diseñemos el juego.



4.1 Cambiar los niveles



La construcción de niveles es una actividad muy divertida. Normalmente, esto lo realizan personas a las que se denomina diseñadores de niveles. Nuestro nivel está representado por matrices de números que se transforman mediante scripts de script.js en un mundo tridimensional. Es posible escribir un programa separado para simplificar la creación de mundos, pero ahora no lo haremos. Abramos el archivo script.js y carguemos allí las matrices de laberintos prefabricados:



Matrices de nivel
// 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"]];




Ahora podemos jugar el juego. Como resultado, los niveles se ven así:







es extremadamente difícil navegar en un mundo así. Además, el movimiento a lo largo de las paredes contiene errores, ya que el jugador puede quedarse atascado en las esquinas de las paredes. Arreglemos eso en collision (), reemplazando 98 con 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 Agregar iluminación estática



Para facilitar la navegación, implementamos iluminación solar estática (sin sombras). Agreguemos un vector de luz solar:



var sun = [0.48,0.8,0.36];


¿Cómo crear iluminación? Mire la imagen:







si el vector sol es exactamente opuesto al vector n, entonces la iluminación se maximiza. La intensidad de la luz depende del ángulo de incidencia de la luz en la superficie. Si el rayo de luz cae paralelo al plano o cae desde el lado opuesto, entonces el avión no está iluminado. El ángulo de incidencia se puede calcular usando el producto escalar n * sol: si es negativo, entonces la iluminación depende del módulo del producto escalar, y si es positivo, entonces no hay iluminación. Crearemos la iluminación de las superficies al generar el mundo, es decir, en CreateNewWorld (). Y como solo existe la función CreateSquare (), aplicaremos la iluminación allí. Pero probablemente solo aplicaremos la anunciación al mundo, pero no a las cosas, así que agregaremos el argumento de iluminación allí, y cambiaremos CreateSquare () en sí:



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


Encendamos la iluminación al generar el mundo en CreateNewWorld ():



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


Y agregue apagar la iluminación para los elementos en button1.onclick (en CreateSquares, el último parámetro para ellos es falso):



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


Comencemos el juego y observemos que la iluminación se ha vuelto más realista y es mucho más fácil navegar en el espacio:







agregue un cielo azul. Establezcamos el fondo para #container en style.css:



background-color:#C0FFFF;


El cielo se volvió azul:







hemos diseñado los niveles. Pero todavía es difícil encontrar elementos, ya que son estáticos y el jugador es intuitivamente difícil de entender que se pueden recolectar.



4.3 Agregar rotación y luz a los objetos



En menu.js, creemos una función de rotación separada:



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


Y lo llamaremos desde 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();
}


Es cierto que la función de rotación se puede utilizar no solo para rotar objetos, sino también para moverlos. Entonces los objetos están girando. Pero si hacemos que estos objetos sean luminosos, generalmente será genial. Establezcamos sombras de colores para ellos en style.css:



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


Ahora el jugador entiende con seguridad que se puede interactuar con estos elementos.



4.4 Agregar widgets



Normalmente, los widgets muestran la puntuación, el estado y otros datos numéricos necesarios. Aquí te mostrarán la cantidad de monedas recolectadas (cuadrados amarillos) y claves (cuadrados rojos), y puedes cambiarlas desde javascript. Primero, agreguemos nuevos elementos al 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>


En menu.js, les vincularemos variables:



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


Y dentro de button1.onclick () agregue texto a ellos:



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


Vamos a diseñarlos en 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;
}


Inicialmente son invisibles. Hagamos visibles los 2 primeros widgets al iniciar el nivel dentro de 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>";


Hay widgets, pero no sucede nada al interactuar con objetos. Cambiaremos las etiquetas de los widgets al interactuar desde funciones interactivas (dentro de if (r <(objetos [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>";


Ahora, al tomar monedas y una llave, la información en los widgets cambia. Pero cuando termina el juego, los widgets no están ocultos. Escóndelos al final del juego agregando las siguientes líneas para finishInteract () dentro de else:



widget1.style.display = "none";

widget2.style.display = "ninguno";

widget3.style.display = "ninguno";



Los widgets están ocultos. Queda por configurar un widget que te pide que cojas una llave si llegas a la meta sin ella. En finishInteract (), en lugar de console.log ("encuentra la clave"), inserta las siguientes líneas:



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


Si el intento de finalizar el juego no tiene éxito, recibimos un mensaje que desaparece después de 5 segundos. Nuestro juego ahora se ve así:











4.5 Formateemos el texto.



Creemos una carpeta de fuentes en la carpeta de archivos. Descargue el archivo font1.woff desde aquí y péguelo en Fonts. Agregue estilos de texto a 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");
}


El menú y el juego han cambiado:











4.6 Agregar sonidos.



Descargue el archivo Sounds.zip desde aquí . Cree una carpeta de Sonidos en la carpeta del proyecto e inserte los sonidos allí (están en formato mp3). Hagamos referencias variables a estos sonidos:



//  

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


En la función interactuar, agregue un argumento al archivo de sonido y reproduzca el sonido (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>";
		};
	};
}


En repeatFunction (), cambie las llamadas a esta función en consecuencia:



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


Y en finishInteract (), agregue los sonidos errorSound y 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;
			};
		};
	};
};


Cuando haga clic en cualquier botón del menú, reproduciremos el sonido 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";
}


El juego fue más brillante. Queda por personalizar la salida de los resultados después de pasar todos los niveles:



4.7 Salida de resultados.



En menu.js en finishInteract () dentro de if (level> = 2) {…} agregue las siguientes líneas:



if(level >= 2){
menu1.style.display = "none";
	menu3.style.display = "block";
	document.getElementById("result").innerHTML = "  " + score + " ";
	level = 0;
	score = 0;
};


Vemos la cantidad de puntos obtenidos después de completar todos los niveles.

Por cierto, no olvidemos agregar la línea a la misma función:



canlock = false;


Y:



button1.innerHTML = "<p></p>";


y



button1.innerHTML = "<p> </p>";


Como resultado:



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>";
			};
		};
	};
};


Ahora el botón de inicio del juego cambia según el paso de los niveles. Además, mueva "contenedor" al centro de la ventana, agregando las siguientes líneas a sus estilos:



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


Y quita las sangrías del cuerpo:



body{
	margin:0px;
}


Entonces, hemos escrito completamente un juego de laberintos 3D basado en navegador. Gracias a ella, llamamos la atención sobre algunos aspectos del lenguaje javascript, aprendimos sobre funciones de las que quizás no hayas oído hablar antes. Y lo más importante, hemos demostrado que no es tan difícil hacer juguetes sencillos para el navegador, incluso en código puro. Puede descargar el código fuente completo desde aquí (sources.zip) . Los scripts en sí pueden mejorarse significativamente agregando diferentes bibliotecas allí, escribiendo nuevos constructores o haciendo otra cosa.



¡Gracias por su atención!



All Articles