Bem-vindo à arquitetura de projeto mais confusa. Sim, posso escrever uma introdução ...
Vamos tentar fazer uma pequena demonstração do minecraft no navegador. Conhecimento de JS e three.js será útil.
Um pouco de convenção. Não estou afirmando ser o melhor aplicativo do século. Esta é apenas minha implementação para esta tarefa. Também existe uma versão em vídeo para quem tem preguiça de ler (tem o mesmo significado, mas com palavras diferentes).
Aqui está a versão em vídeo
Existem todos os links de que você precisa no final do artigo. Vou tentar o mínimo de água possível no texto. Não vou explicar como cada linha funciona. Agora você pode começar.
Para começar, para entender qual será o resultado, aqui está uma demonstração do jogo .
Vamos dividir o artigo em várias partes:
- Estrutura do projeto
- Loop de jogo
- Configurações do jogo
- Geração de mapas
- Câmera e controles
Estrutura do projeto
É assim que a estrutura do projeto se parece.
index.html - A localização da tela, alguma interface e a conexão de estilos, scripts.
style.css - Estilos apenas para aparência. O mais importante é o cursor personalizado para o jogo, que está localizado no centro da tela.
textura - contém as texturas para o cursor e o bloco de solo para o jogo.
core.js - O script principal onde o projeto é inicializado.
perlin.js - Esta é uma biblioteca para ruído Perlin.
PointerLockControls.js - Câmera de three.js.
controls.js - Controles da câmera e do player.
generationMap.js - Geração do mundo.
three.module.js - o próprio Three.js como um módulo.
settings.js - Configurações do projeto.
index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style/style.css">
<title>Minecraft clone</title>
</head>
<body>
<canvas id="game" tabindex="1"></canvas>
<div class="game-info">
<div>
<span><b>WASD: </b></span>
<span><b>: </b> </span>
<span><b>: </b> </span>
</div>
<hr>
<div id="debug">
<span><b></b></span>
</div>
</div>
<div id="cursor"></div>
<script src="scripts/perlin.js"></script>
<script src="scripts/core.js" type="module"></script>
</body>
</html>
style.css
body {
margin: 0px;
width: 100vw;
height: 100vh;
}
#game {
width: 100%;
height: 100%;
display: block;
}
#game:focus {
outline: none;
}
.game-info {
position: absolute;
left: 1em;
top: 1em;
padding: 1em;
background: rgba(0, 0, 0, 0.9);
color: white;
font-family: monospace;
pointer-events: none;
}
.game-info span {
display: block;
}
.game-info span b {
font-size: 18px;
}
#cursor {
width: 16px;
height: 16px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-image: url("../texture/cursor.png");
background-repeat: no-repeat;
background-size: 100%;
filter: brightness(100);
}
Loop de jogo
Em core.js, você precisa inicializar three.js, configurá-lo e adicionar todos os módulos necessários dos manipuladores de jogo + evento ... bem, inicie o loop de jogo. Considerando que todas as configurações são padrão, não vale a pena explicá-las. Você pode falar sobre o mapa (leva a cena do jogo para adicionar blocos) e contorls. leva vários parâmetros. O primeiro é uma câmera do three.js, uma cena para adicionar blocos e um mapa para que você possa interagir com ele. update é responsável por atualizar a câmera, GameLoop é o loop do jogo, render é o padrão do three.js para atualizar o quadro, o evento resize também é o padrão para trabalhar com o canvas (esta é a implementação do adaptativo).
core.js
import * as THREE from './components/three.module.js';
import { PointerLockControls } from './components/PointerLockControls.js';
import { Map } from "./components/generationMap.js";
import { Controls } from "./components/controls.js";
// three.js
const canvas = document.querySelector("#game");
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x00ffff);
scene.fog = new THREE.Fog(0x00ffff, 10, 650);
const renderer = new THREE.WebGLRenderer({canvas});
renderer.setSize(window.innerWidth, window.innerHeight);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(50, 40, 50);
//
let mapWorld = new Map();
mapWorld.generation(scene);
let controls = new Controls( new PointerLockControls(camera, document.body), scene, mapWorld );
renderer.domElement.addEventListener( "keydown", (e)=>{ controls.inputKeydown(e); } );
renderer.domElement.addEventListener( "keyup", (e)=>{ controls.inputKeyup(e); } );
document.body.addEventListener( "click", (e) => { controls.onClick(e); }, false );
function update(){
// /
controls.update();
};
GameLoop();
//
function GameLoop() {
update();
render();
requestAnimationFrame(GameLoop);
}
// (1 )
function render(){
renderer.render(scene, camera);
}
//
window.addEventListener("resize", function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
Configurações
Foi possível incluir outros parâmetros nas configurações, por exemplo, as configurações do three.js, mas eu fiz sem eles e agora existem apenas alguns parâmetros responsáveis pelo tamanho do bloco.
settings.js
export class Settings {
constructor() {
//
this.blockSquare = 5;
//
this.chunkSize = 16;
this.chunkSquare = this.chunkSize * this.chunkSize;
}
}
Geração de mapas
Na classe Map, temos várias propriedades que são responsáveis pelo cache de material e parâmetros para o ruído Perlin. No método de geração, carregamos texturas, criamos geometria e malha. noise.seed é responsável pelo grão inicial para a geração do mapa. Você pode substituir aleatório por um valor estático para que os cartões sejam sempre os mesmos. Em um loop ao longo das coordenadas X e Z, começamos a organizar os cubos. A coordenada Y é gerada pela biblioteca pretlin.js. Finalmente, adicionamos o cubo com as coordenadas desejadas à cena via scene.add (cubo);
generationMap.js
import * as THREE from './three.module.js';
import { Settings } from "./settings.js";
export class Map {
constructor(){
this.materialArray;
this.xoff = 0;
this.zoff = 0;
this.inc = 0.05;
this.amplitude = 30 + (Math.random() * 70);
}
generation(scene) {
const settings = new Settings();
const loader = new THREE.TextureLoader();
const materialArray = [
new THREE.MeshBasicMaterial( { map: loader.load("../texture/dirt-side.jpg") } ),
new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-side.jpg') } ),
new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-top.jpg') } ),
new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-bottom.jpg') } ),
new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-side.jpg') } ),
new THREE.MeshBasicMaterial( { map: loader.load('../texture/dirt-side.jpg') } )
];
this.materialArray = materialArray;
const geometry = new THREE.BoxGeometry( settings.blockSquare, settings.blockSquare, settings.blockSquare);
noise.seed(Math.random());
for(let x = 0; x < settings.chunkSize; x++) {
for(let z = 0; z < settings.chunkSize; z++) {
let cube = new THREE.Mesh(geometry, materialArray);
this.xoff = this.inc * x;
this.zoff = this.inc * z;
let y = Math.round(noise.perlin2(this.xoff, this.zoff) * this.amplitude / 5) * 5;
cube.position.set(x * settings.blockSquare, y, z * settings.blockSquare);
scene.add( cube );
}
}
}
}
Câmera e controles
Já disse que os controles tomam parâmetros na forma de câmera, cena e mapa. Também no construtor, adicionamos um array de chaves para as chaves e um movingSpeed para velocidade. Para o mouse, temos 3 métodos. onClick determina qual botão é clicado, e onRightClick e onLeftClick já são responsáveis pelas ações. Clique com o botão direito (exclusão de bloco) para acessar o raycast e pesquisar os elementos que se cruzam. Se eles não estiverem lá, paramos de trabalhar; se houver, excluímos o primeiro elemento. O clique esquerdo funciona em um sistema semelhante. Primeiro, vamos criar um bloco. Começamos o raycast e se houver um bloco que cruzou o raio, então obtemos as coordenadas deste bloco. Em seguida, determinamos de que lado o clique ocorreu. Mudamos as coordenadas do cubo criado de acordo com o lado ao qual adicionamos o bloco. gradação em 5 unidades porque este é o tamanho do bloco (sim, você pode usar uma propriedade das configurações aqui).
Como funciona o controle da câmera ?! Temos três métodos inputKeydown, inputKeyup e update. Em inputKeydown, adicionamos o botão ao array de chaves. inputKeyup é responsável por limpar os botões da matriz que foram pressionados. Na atualização, as teclas são verificadas e moveForward é chamado na câmera, os parâmetros que o método assume são a velocidade.
controls.js
import * as THREE from "./three.module.js";
import { Settings } from "./settings.js";
export class Controls {
constructor(controls, scene, mapWorld){
this.controls = controls;
this.keys = [];
this.movingSpeed = 1.5;
this.scene = scene;
this.mapWorld = mapWorld;
}
//
onClick(e) {
e.stopPropagation();
e.preventDefault();
this.controls.lock();
if (e.button == 0) {
this.onLeftClick(e);
} else if (e.button == 2) {
this.onRightClick(e);
}
}
onRightClick(e){
//
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera( new THREE.Vector2(), this.controls.getObject() );
let intersects = raycaster.intersectObjects( this.scene.children );
if (intersects.length < 1)
return;
this.scene.remove( intersects[0].object );
}
onLeftClick(e) {
const raycaster = new THREE.Raycaster();
const settings = new Settings();
//
const geometry = new THREE.BoxGeometry(settings.blockSquare, settings.blockSquare, settings.blockSquare);
const cube = new THREE.Mesh(geometry, this.mapWorld.materialArray);
raycaster.setFromCamera( new THREE.Vector2(), this.controls.getObject() );
const intersects = raycaster.intersectObjects( this.scene.children );
if (intersects.length < 1)
return;
const psn = intersects[0].object.position;
switch(intersects[0].face.materialIndex) {
case 0:
cube.position.set(psn.x + 5, psn.y, psn.z);
break;
case 1:
cube.position.set(psn.x - 5, psn.y, psn.z);
break;
case 2:
cube.position.set(psn.x, psn.y + 5, psn.z);
break;
case 3:
cube.position.set(psn.x, psn.y - 5, psn.z);
break;
case 4:
cube.position.set(psn.x, psn.y, psn.z + 5);
break;
case 5:
cube.position.set(psn.x, psn.y, psn.z - 5);
break;
}
this.scene.add(cube);
}
//
inputKeydown(e) {
this.keys.push(e.key);
}
//
inputKeyup(e) {
let newArr = [];
for(let i = 0; i < this.keys.length; i++){
if(this.keys[i] != e.key){
newArr.push(this.keys[i]);
}
}
this.keys = newArr;
}
update() {
//
if ( this.keys.includes("w") || this.keys.includes("") ) {
this.controls.moveForward(this.movingSpeed);
}
if ( this.keys.includes("a") || this.keys.includes("") ) {
this.controls.moveRight(-1 * this.movingSpeed);
}
if ( this.keys.includes("s") || this.keys.includes("") ) {
this.controls.moveForward(-1 * this.movingSpeed);
}
if ( this.keys.includes("d") || this.keys.includes("") ) {
this.controls.moveRight(this.movingSpeed);
}
}
}
Links
Como eu prometi. Todo o material que vem a calhar.
Se desejar, você pode adicionar sua funcionalidade ao projeto no github.
perlin.js
three.js
GitHub