Criação de jogos 3D baseados em navegador do zero em html, css e js puros. Parte 1/2

A tecnologia de computação moderna permite que você crie jogos de computador legais! E agora os jogos com gráficos 3D são bastante populares, já que jogá-los você mergulha em um mundo ficcional e perde toda a conexão com a realidade. O desenvolvimento das tecnologias da Internet e dos navegadores tornou possível rodar quebra-cabeças e jogos de tiro em seu Chrome favorito, Mozilla ou qualquer outra coisa lá (vamos ficar calados quanto ao Explorer) online, sem baixar. Então, aqui vou lhe dizer como criar um jogo de navegador tridimensional simples.



A escolha do gênero, enredo e estilo de jogo é uma tarefa bastante interessante, e o sucesso do jogo pode depender da solução dessas questões. Além disso, a escolha da tecnologia com base na qual o produto será criado também traz nuances próprias. Meu objetivo é mostrar o básico desse processo divertido, então farei um labirinto tridimensional com um design simples. Além disso, farei isso em código puro, sem usar bibliotecas e motores, como three.js (embora seja melhor fazer grandes projetos nele) para mostrar como você pode criar um motor para suas necessidades. Um jogo totalmente escrito pelo próprio pode ser original e, portanto, interessante. Em geral, ambas as abordagens têm seus prós e contras.



Suponho que se você está lendo este artigo, então você está interessado no tópico de criação de jogos para o Google Chrome, o que significa que você entende como o pacote html-css-javaScript funciona, então não vou me alongar no básico, mas vou começar a desenvolver imediatamente. Em html5 e css3, que são suportados por todos os navegadores modernos (o Explorer não conta), é possível organizar blocos no espaço tridimensional. Também existe um elemento no qual você pode desenhar linhas e primitivos gráficos. A maioria dos mecanismos de navegador usa <canvas> porque mais coisas podem ser feitas nele e o desempenho é melhor nele. Mas para coisas simples, é bem possível usar métodos transform-3d, que requerem menos código.



1. Ferramentas de desenvolvimento



Eu só uso 2 navegadores para verificar sites e jogos: Chrome e Mozilla. Todos os outros navegadores (exceto o próprio Explorer) são construídos no primeiro motor, então não vejo sentido em usá-los, porque os resultados são exatamente os mesmos que no Chrome. O Notepad ++ é suficiente para escrever código.



2. Como o espaço 3D é implementado em html?



Vejamos o sistema de coordenadas do bloco:







Por padrão, o bloco filho tem coordenadas (esquerda e superior) 0 pixels em xe 0 pixels em y. Offset (translação), também 0 pixels em todos os três eixos. Vamos mostrar isso com um exemplo, para o qual criaremos uma nova pasta. Nele, criaremos os arquivos index.html, style.css e script.js. Vamos abrir index.html e escrever o seguinte lá:



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


No arquivo style.css, vamos definir os estilos para os elementos “container” e “world”.



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


Vamos salvar.







Vamos abrir index.html com o Chrome, temos: Vamos tentar aplicar translate3d ao elemento “mundo”:



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






Como você entende, mudei para o modo de tela inteira. Agora vamos definir o deslocamento Z:

transform: translate3d (200px, 100px, -1000px);



Se você abrir o arquivo html no navegador novamente, não verá nenhuma alteração. Para ver as mudanças, você precisa definir a perspectiva do objeto "container":



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


Como resultado: o







Square se afastou de nós. Como a perspectiva funciona em html? Vamos dar uma olhada na figura:







d é a distância do usuário ao objeto, e z é sua coordenada. Z negativo (em html é translateZ) significa que afastamos o objeto e positivo - vice-versa. O valor da perspectiva determina o valor de d. Se a propriedade perspectiva não for configurada, então o valor de d é considerado infinito e, neste caso, o objeto não muda visualmente para o usuário com uma mudança em z. Em nosso caso, definimos d = 600px. Por padrão, o ponto de vista da perspectiva está no centro do elemento, no entanto, ele pode ser alterado definindo a propriedade perspectiva-origem :.



Agora vamos girar "mundo" em torno de algum eixo. Existem 2 formas de rotação que podem ser usadas no css. O primeiro é a rotação em torno dos eixos x, y e z. Para fazer isso, use as propriedades de transformação rotateX (), rotateY () e rotateZ (). A segunda é a rotação em torno de um determinado eixo usando a propriedade rotate3d (). Usaremos o primeiro método, pois é mais adequado para nossas tarefas. Observe que os eixos de rotação saem do centro do retângulo!







O ponto em que as transformações ocorrem pode ser alterado definindo a propriedade translate-origin:. Então, vamos definir a rotação de "mundo" ao longo do eixo x:



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










Obtemos : Deslocamento perceptível no sentido anti-horário. Se adicionarmos rotateY (), obteremos um deslocamento ao longo do eixo Y. É importante notar que quando o bloco é girado, os eixos de rotação também giram. Você também pode experimentar diferentes valores de rotação.

Agora, dentro do bloco "mundo", criaremos outro bloco, para isso adicionaremos uma tag ao arquivo html:



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


Adicione estilos a este bloco em style.css:



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


Obtemos:







Ou seja, os elementos dentro do bloco "mundo" serão transformados como parte desse bloco. Vamos tentar girar “square1” ao longo do eixo y adicionando um estilo de rotação a ele:

transform: rotateY (30deg);



No final:







"Onde está a rotação?" - você pergunta? Na verdade, é exatamente assim que se parece a projeção do bloco “square1” no plano formado pelo elemento “world”. Mas não precisamos de uma projeção, mas de uma rotação real. Para tornar todos os elementos dentro do "mundo" volumétricos, você precisa aplicar a propriedade transform-style: preserve-3d. Após substituir a propriedade dentro da lista de estilos "mundo", verifique as alterações:







Excelente! Metade do bloco “quadrado” está escondido atrás do bloco azul. Para mostrar por completo, vamos remover a cor do bloco "mundo", ou seja, remover a linha da cor de fundo: # C0FFFF; Se adicionarmos mais retângulos dentro do bloco "mundo", podemos criar um mundo 3D. Agora vamos remover o deslocamento "mundo" removendo a linha de propriedade de transformação nos estilos para este elemento.



3. Crie movimento em um mundo tridimensional



Para que o usuário possa se mover ao redor do mundo, você precisa definir manipuladores para pressionamentos de tecla e movimentos do mouse. Os controles serão padrão, o que está presente na maioria dos jogos de tiro 3D. Com as teclas W, S, A, D, vamos avançar, retroceder, esquerda, direita, com a barra de espaço saltaremos (ou seja, mover para cima), e com o mouse mudaremos a direção do nosso olhar. Para fazer isso, abra o arquivo script.js, que ainda está vazio. Primeiro, vamos adicionar as seguintes variáveis ​​lá:



//   ?

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


Nenhuma tecla foi pressionada inicialmente. Se pressionarmos uma tecla, o valor de uma determinada variável mudará para 1. Se o liberarmos, ele se tornará 0. Implementaremos isso adicionando manipuladores para pressionar e soltar as teclas:



//   

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


O número 32 é um código de espaço. Como você pode ver, existe uma variável onGround que indica se estamos no solo. Por enquanto, vamos permitir um movimento para cima adicionando a variável onGround após pressionar ... variáveis:



//    ?

var onGround = true;


Então, adicionamos um algoritmo push e pull. Agora precisamos adicionar o próprio movimento. O que, de fato, estamos movendo. Vamos imaginar que temos um objeto que estamos movendo. Vamos chamá-lo de “peão”. Como é habitual para desenvolvedores normais, criaremos uma classe “Player” separada para ele. As classes em javaScript são criadas, por incrível que pareça, usando funções:



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


Vamos colar esse código em script.js bem no início do arquivo. No final do arquivo, vamos criar um objeto deste tipo:



//   

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


Vamos escrever o que essas variáveis ​​significam. x, y, z são as coordenadas iniciais do jogador, rx, ry são os ângulos de sua rotação em relação aos eixos xey em graus. A última linha escrita significa que criamos um objeto "peão" do tipo "jogador" (estou escrevendo um tipo especificamente, não uma classe, uma vez que classes em javascript significam algumas outras coisas) com zero coordenadas iniciais. Quando movemos o objeto, a coordenada mundial não deve mudar, mas a coordenada "peão" deve mudar. Isso é em termos de variáveis. E do ponto de vista do usuário, o jogador está em um lugar, mas o mundo está se movendo. Assim, você precisa forçar o programa a mudar as coordenadas do jogador, lidar com essas mudanças e, no final, mover o mundo. Na verdade, isso é mais fácil do que parece.



Assim, após carregar o documento no navegador, executaremos uma função que redesenha o mundo. Vamos escrever uma função de redesenho:



function update(){
	
	//  
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = PressUp;
	
	//    
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	
	//    ( )
	
	world.style.transform = 
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};


Em novos navegadores, world irá combinar o elemento com id = "world", mas é mais seguro atribuí-lo antes da função update () usando a seguinte construção:



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


Mudaremos a posição do mundo a cada 10 ms (100 atualizações por segundo), para o qual iniciaremos um loop infinito:



TimerGame = setInterval(update,10);


Vamos começar o jogo. Viva, agora podemos nos mover! No entanto, o mundo rasteja para fora dos limites do elemento "contêiner". Para evitar que isso aconteça, vamos definir uma propriedade css para ele em style.css. Adicione o estouro de linha: oculto; e veja as mudanças. O mundo agora permanece dentro do contêiner.



É possível que você nem sempre entenda onde precisa escrever certas linhas de código, então agora apresentarei os arquivos que, acredito, você deve obter:



index.html:



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




style.css:

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


script.js:



//  Pawn

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

//   ?

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

//    ?

var onGround = true;

//   

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

//   

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

//   

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

//     world

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

function update(){
	
	//    
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = - PressUp;
	
	//    
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	
	//    ( )
	
	world.style.transform = 
	"rotateX(" + (-pawn.rx) + "deg)" +
	"rotateY(" + (-pawn.ry) + "deg)" +
	"translate3d(" + (-pawn.x) + "px," + (-pawn.y) + "px," + (-pawn.z) + "px)";
	
};

TimerGame = setInterval(update,10);


Se você tiver algo diferente, certifique-se de corrigi-lo!



Aprendemos como mover o personagem, mas ainda não sabemos como girá-lo! A rotação do personagem, é claro, será feita com o mouse. Para o mouse, adicionaremos as variáveis ​​de estado do movimento do mouse às variáveis ​​de estado das teclas ...



//       ?

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


E após os manipuladores push-release, insira o manipulador de movimento:



//   

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


Adicione uma rotação à função de atualização:



	//    
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;
	
	//    
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	pawn.rx = pawn.rx + drx;
	pawn.ry = pawn.ry + dry;


Observe que mover o mouse ao longo do eixo y gira o peão ao longo do eixo x e vice-versa. Se olharmos para o resultado, ficaremos horrorizados com o que vimos. A questão é que, se não houver deslocamento, MouseX e MouseY permanecerão iguais e não iguais a zero. Isso significa que após cada iteração de atualização, os deslocamentos de misha devem ser redefinidos para zero:



//    
	
	let dx = (PressRight - PressLeft);
	let dz = - (PressForward - PressBack);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;

//   :
	
	MouseX = MouseY = 0;

//    
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	pawn.rx = pawn.rx + drx;
	pawn.ry = pawn.ry + dry;


Melhor ainda, nos livramos da inércia rotacional, mas a rotação continua estranha! Para ter uma ideia do que está acontecendo, vamos adicionar a div "peão" dentro do "contêiner":



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


Vamos estilizar em style.css:



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


Vamos verificar o resultado. Agora está tudo tranquilo! A única coisa é que o quadrado azul fica na frente, mas por enquanto vamos deixar isso. Para fazer o jogo na primeira pessoa, e não na terceira, você precisa trazer o mundo para mais perto de nós por um valor de perspectiva. Vamos fazer isso em script.js na função update ():



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


Agora você pode fazer o jogo na primeira pessoa. Oculte o peão adicionando uma linha a style.css:



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


Excelente. Devo dizer desde já que é extremamente difícil navegar em um mundo com um quadrado, então vamos criar um site. Vamos adicionar o bloco "square2" ao "mundo":



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


E em style.css adicione estilos para ele:



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


Agora está tudo claro. Bem, não exatamente. Quando pressionamos as teclas, estamos nos movendo estritamente ao longo dos eixos X e Z. E queremos fazer o movimento na direção da vista. Vamos fazer o seguinte: logo no início do arquivo script.js, adicione 2 variáveis:



//  

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


Um grau é pi / 180 de um radiano. Teremos que aplicar senos e cossenos, que são calculados a partir de radianos. O que deveria ser feito? Dê uma olhada na imagem:







quando nosso olhar está direcionado em um ângulo e queremos avançar, as duas coordenadas mudarão: X e Z. No caso de mover para o lado, as funções trigonométricas simplesmente mudarão de lugar e o sinal na frente do seno resultante mudará. Vamos mudar as equações de deslocamento em update ():



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


Reveja todos os arquivos cuidadosamente! Se algo acabar dando errado para você, com certeza haverá erros que irão quebrar sua cabeça!



index.html:



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


style.css:



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


script.js:



//  

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

//  Pawn

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

//       ?

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

//    ?

var onGround = true;

//   

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

//   

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

//   

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


//     player

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

//     world

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

function update(){
	
	//    
	
	let dx = (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
	let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;
	
	//   :
	
	MouseX = MouseY = 0;
	
	//    
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	pawn.rx = pawn.rx + drx;
	pawn.ry = pawn.ry + dry;

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

TimerGame = setInterval(update,10);


Quase descobrimos o movimento. Mas havia um inconveniente: o cursor do mouse só pode se mover dentro da tela. Em jogos de tiro tridimensionais, você pode girar o mouse o quanto e quanto quiser. Vamos fazer também: ao clicarmos na tela do jogo (no "container"), o cursor desaparecerá, e poderemos girar o mouse sem restrições ao tamanho da tela. Ativamos a captura do mouse ao clicar na tela, para o qual colocamos um manipulador para clicar com o mouse em “container” na frente dos manipuladores de teclas:



//     container

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

//    

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


Agora é uma questão completamente diferente. No entanto, geralmente é melhor fazer a rotação ocorrer apenas quando o cursor for capturado. Vamos introduzir uma nova variável após a impressão ...



//    ?

var lock = false;


Vamos adicionar um manipulador para alterar o estado da captura do cursor (capturado ou não) antes do manipulador de captura do cursor (desculpe pela tautologia):



//     

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


E em update () adicione a condição de rotação “peão”:



//   ,  

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


E a captura do próprio mouse ao clicar no container só é permitida quando o cursor ainda não foi capturado:



//    

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


Nós lidamos completamente com o movimento. Vamos continuar gerando o mundo



4. Carregando o mapa



O mundo em nosso caso é mais convenientemente representado como um conjunto de retângulos com diferentes localizações, rotação, tamanhos e cores. Texturas também podem ser usadas em vez de cores. Na verdade, todos os mundos 3D modernos em jogos são uma coleção de triângulos e retângulos chamados polígonos. Em jogos divertidos, seu número pode chegar a dezenas de milhares em apenas um quadro. Teremos cerca de uma centena deles, já que o próprio navegador tem baixo desempenho gráfico. Nos parágrafos anteriores, inserimos blocos “div” dentro do “mundo”. Mas se houver muitos desses blocos (centenas), inserir cada um deles no contêiner é muito tedioso. E pode haver muitos níveis. Portanto, deixe o javaScript inserir esses retângulos, não nós. Criaremos um array especial para ele.



Vamos abrir index.html e remover todos os blocos internos do bloco "mundo":



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


Como você pode ver, não há nada no "mundo" agora. Em style.css, remova os estilos de # square1 e # square2 (remova # square1 e # square2 deste arquivo juntos) e, em vez disso, crie estilos para a classe .square, que será comum a todos os retângulos. E vamos definir apenas uma propriedade para ele:




.square{
	position:absolute;
}


Agora vamos criar um array de retângulos (por exemplo, vamos colocá-lo entre o construtor do player e as variáveis ​​de pressão ... em script.js):



//  

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


Era possível fazer isso na forma de um construtor, mas por enquanto faremos com um array puramente, já que é mais fácil iniciar o ciclo de arranjo de retângulos por meio de arrays, e não por meio de construtores. Vou explicar o que os números significam. O array do mapa contém arrays unidimensionais de 9 variáveis: [,,,,,,,,]. Eu acho que você entende que os primeiros três números são as coordenadas do centro do retângulo, os segundos três números são os ângulos de rotação em graus (relativos ao mesmo centro), então dois números são suas dimensões e o último número é o fundo. Além disso, o fundo pode ser uma cor sólida, um gradiente ou uma fotografia. Este último é muito conveniente para usar como texturas.



Escrevemos o array, agora vamos escrever uma função que transformará esse array em retângulos:



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


Deixe-me explicar o que está acontecendo: estamos criando uma nova variável que aponta para o elemento que acabamos de criar. Atribuímos a ele um id e uma classe css (isso é o que queremos dizer com a palavra classe em javaScript), definimos a largura com altura, plano de fundo e transformação. Vale ressaltar que na transformação, além das coordenadas do centro do retângulo, especificamos um deslocamento de 600 e 400 e metade das dimensões para que o centro do retângulo fique exatamente no ponto com as coordenadas desejadas. Vamos iniciar o gerador mundial na frente do cronômetro:



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


Agora vemos uma área com paredes rosa e piso cinza. Como você pode ver, criar um mapa não é tecnicamente difícil de implementar. Como resultado, seu código em três arquivos deve ser semelhante a este:



index.html:



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


style.css



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


script.js:



//  

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

//  player

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

//  

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

//       ?

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

//    ?

var lock = false;

//    ?

var onGround = true;

//     container

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

//     

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

//    

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

//   

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

//   

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

//   

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

//   

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

//     world

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

function update(){
	
	//    
	
	let dx =   (PressRight - PressLeft)*Math.cos(pawn.ry*deg) - (PressForward - PressBack)*Math.sin(pawn.ry*deg);
	let dz = - (PressForward - PressBack)*Math.cos(pawn.ry*deg) - (PressRight - PressLeft)*Math.sin(pawn.ry*deg);
	let dy = - PressUp;
	let drx = MouseY;
	let dry = - MouseX;
	
	//   :
	
	MouseX = MouseY = 0;
	
	//    
	
	pawn.x = pawn.x + dx;
	pawn.y = pawn.y + dy;
	pawn.z = pawn.z + dz;
	
	//   ,  
	
	if (lock){
		pawn.rx = pawn.rx + drx;
		pawn.ry = pawn.ry + dry;
	};

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

function CreateNewWorld(){
	for (let i = 0; i < map.length; i++){
		
		//      
		
		let newElement = document.createElement("div");
		newElement.className = "square";
		newElement.id = "square" + i;
		newElement.style.width = map[i][6] + "px";
		newElement.style.height = map[i][7] + "px";
		newElement.style.background = map[i][8];
		newElement.style.transform = "translate3d(" +
		(600 - map[i][6]/2 + map[i][0]) + "px," +
		(400 - map[i][7]/2 + map[i][1]) + "px," +
		                    (map[i][2]) + "px)" +
		"rotateX(" + map[i][3] + "deg)" +
		"rotateY(" + map[i][4] + "deg)" +
		"rotateZ(" + map[i][5] + "deg)";
		
		//    world
		
		world.append(newElement);
	}
}

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


Se tudo estiver bem, passe para o próximo item.



5. Colisões de jogadores com objetos do mundo



Criamos uma técnica de movimento, um gerador do mundo a partir de um array. Podemos nos mover em um mundo que pode ser lindo. Porém, nosso jogador ainda não interage com ele. Para que essa interação ocorra, precisamos verificar se o player colide com algum retângulo ou não? Ou seja, vamos verificar se há colisões. Primeiro, vamos inserir uma função vazia:



function collision(){
	
}


E vamos chamá-lo em update ():



//   :
	
	MouseX = MouseY = 0;
	
	//    
	
	collision();


Como isso acontece? Vamos imaginar que o jogador é uma bola de raio r. E se move em direção ao retângulo:







Obviamente, se a distância da bola ao plano do retângulo for maior que r, então a colisão definitivamente não ocorre. Para descobrir essa distância, você pode traduzir as coordenadas do jogador para o sistema de coordenadas do retângulo. Vamos escrever a função de transferência do sistema mundial para o sistema retângulo:



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


E a função inversa:



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


Vamos inserir essas funções após a função update (). Não vou explicar como funciona, porque não estou com vontade de dar um curso de geometria analítica. Direi que existem essas fórmulas para a tradução de coordenadas durante a rotação e nós apenas as usamos. Do ponto de vista do retângulo, nosso player está posicionado assim:







Neste caso, a condição de colisão torna-se a seguinte: se, após deslocar a bola pelo valor v (v é um vetor), a coordenada z está entre –r e r, e as coordenadas x e y estão dentro do retângulo ou estão separadas dele por um valor não maior que r, então uma colisão é declarada. Nesse caso, a coordenada z do jogador após a mudança será r ou - r (dependendo de qual lado o jogador vem). Consequentemente, o deslocamento do jogador é alterado. Chamamos especificamente a colisão antes de atualizar () as coordenadas do jogador para alterar o deslocamento no tempo. Assim, a bola nunca se cruzará com o retângulo, como acontece em outros algoritmos de colisão. Embora fisicamente o jogador seja mais provavelmente um cubo, não vamos prestar atenção a isso. Então, vamos implementar isso em javaScript:



function collision(){
	for(let i = 0; i < map.length; i++){
		
		//       
		
		let x0 = (pawn.x - map[i][0]);
		let y0 = (pawn.y - map[i][1]);
		let z0 = (pawn.z - map[i][2]);
		
		let x1 = x0 + dx;
		let y1 = y0 + dy;
		let z1 = z0 + dz;
		
		let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
		let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
		let point2 = new Array();
		
		//      
		
		if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
			point1[2] = Math.sign(point0[2])*50;
			point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
			dx = point2[0] - x0;
			dy = point2[1] - y0;
			dz = point2[2] - z0;
		}
	};
}


x0, y0 e z0 são as coordenadas iniciais do jogador no sistema de coordenadas do retângulo (sem rotações.x1, y1 e z1 são as coordenadas do jogador após o deslocamento sem colisão. ponto0, ponto0, ponto1 e ponto2 são o vetor de raio inicial, vetor de raio após deslocamento sem colisões e vetor de raio com colisões, respectivamente. map [i] [3] e outros, se você se lembrar, esses são os ângulos de rotação do retângulo. Observe que, na condição, adicionamos não 100 ao tamanho do retângulo, mas 98. Isso é uma muleta, ora, pense Inicie o jogo e você verá algumas colisões de alta qualidade.



Como você pode ver, todas essas ações ocorrem no loop for para todos os retângulos. Com um grande número delas, tal operação torna-se muito cara, pois já existem 3 chamadas para as funções de transformação de coordenadas, que também realizam muitas operações matemáticas. Obviamente, se os retângulos estiverem muito distantes do jogador, não faz sentido contar a colisão. Vamos adicionar esta condição:




if ((x0**2 + y0**2 + z0**2 + dx**2 + dy**2 + dz**2) < (map[i][1]**2 + map[i][2]**2)){
		
			let x1 = x0 + dx;
			let y1 = y0 + dy;
			let z1 = z0 + dz;
		
			let point0 = coorTransform(x0,y0,z0,map[i][3],map[i][4],map[i][5]);
			let point1 = coorTransform(x1,y1,z1,map[i][3],map[i][4],map[i][5]);
			let point2 = new Array();
		
			//      
		
			if (Math.abs(point1[0])<(map[i][6]+98)/2 && Math.abs(point1[1])<(map[i][7]+98)/2 && Math.abs(point1[2]) < 50){
				point1[2] = Math.sign(point0[2])*50;
				point2 = coorReTransform(point1[0],point1[1],point1[2],map[i][3],map[i][4],map[i][5]);
				dx = point2[0] - x0;
				dy = point2[1] - y0;
				dz = point2[2] - z0;
			}
			
		} 


Portanto, lidamos com colisões. Podemos escalar facilmente em superfícies inclinadas, e a ocorrência de bugs só é possível em sistemas lentos, se, é claro, possível. Na verdade, toda a parte técnica principal terminava aí. Só temos que adicionar coisas privadas, como gravidade, coisas, menus, sons, belos gráficos. Mas isso é fácil de fazer e não tem nada a ver com o motor que acabamos de fazer. Portanto, falarei sobre isso na próxima parte . Agora verifique o que você obteve com meu 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;
}

//  

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



All Articles