Bienvenido a la arquitectura de proyectos más confusa. Sí, puedo escribir una introducción ...
Intentemos hacer una pequeña demostración de Minecraft en el navegador. El conocimiento de JS y three.js será útil.
Un poco de convención. No pretendo ser la mejor aplicación del siglo. Esta es solo mi implementación para esta tarea. También hay una versión en video para aquellos que son demasiado perezosos para leer (hay el mismo significado, pero en diferentes palabras).
Aquí está la versión de video
Hay todos los enlaces que necesita al final del artículo. Probaré la menor cantidad de agua posible en el texto. No explicaré cómo funciona cada línea. Ahora puedes empezar.
Para empezar, para entender cuál será el resultado, aquí hay una demostración del juego .
Dividamos el artículo en varias partes:
- Estructura del proyecto
- Bucle de juego
- Configuraciones de juego
- Generación de mapas
- Cámara y controles
Estructura del proyecto
Así es como se ve la estructura del proyecto.
index.html: la ubicación del lienzo, alguna interfaz y la conexión de estilos, scripts.
style.css: estilos solo para apariencia. Lo más importante es el cursor personalizado para el juego, que se encuentra en el centro de la pantalla.
textura: contiene las texturas del cursor y el bloque de suelo del juego.
core.js: el script principal donde se inicializa el proyecto.
perlin.js: esta es una biblioteca para ruido Perlin.
PointerLockControls.js: cámara de three.js.
controls.js: controles de la cámara y el reproductor.
generationMap.js: generación del mundo.
three.module.js: Three.js en sí mismo como módulo.
settings.js: configuración del proyecto.
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);
}
Bucle de juego
En core.js, necesitas inicializar three.js, configurarlo y agregar todos los módulos necesarios del juego + controladores de eventos ... bueno, comienza el ciclo del juego. Teniendo en cuenta que todas las configuraciones son estándar, no tiene sentido explicarlas. Puedes hablar sobre mapa (se necesita la escena del juego para agregar bloques) y contorls. se necesitan varios parámetros. La primera es una cámara de three.js, una escena para agregar bloques y un mapa para que puedas interactuar con ella. update es responsable de actualizar la cámara, GameLoop es el bucle del juego, render es el estándar de three.js para actualizar el marco, el evento resize también es el estándar para trabajar con lienzo (esta es la implementación del 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);
});
Configuraciones
Era posible eliminar otros parámetros en la configuración, por ejemplo, la configuración de three.js, pero lo hice sin ellos y ahora solo hay un par de parámetros responsables del tamaño del bloque.
settings.js
export class Settings {
constructor() {
//
this.blockSquare = 5;
//
this.chunkSize = 16;
this.chunkSquare = this.chunkSize * this.chunkSize;
}
}
Generación de mapas
En la clase Map, tenemos varias propiedades que son responsables de la caché de material y los parámetros para el ruido Perlin. En el método de generación, cargamos texturas, creamos geometría y malla. noise.seed es responsable del grano inicial para la generación de mapas. Puede reemplazar aleatorio con un valor estático para que las cartas sean siempre las mismas. En un bucle a lo largo de las coordenadas X y Z, comenzamos a organizar los cubos. La coordenada Y es generada por la biblioteca pretlin.js. Finalmente, agregamos el cubo con las coordenadas deseadas a la escena a través de 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ámara y controles
Ya dije que los controles toman parámetros en forma de cámara, escena y mapa. También en el constructor agregamos una matriz de claves para las claves y un MovingSpeed para la velocidad. Para el mouse, tenemos 3 métodos. onClick determina en qué botón se hace clic, y onRightClick y onLeftClick ya son responsables de las acciones. El clic derecho (eliminación de bloque) pasa por raycast y busca elementos cruzados. Si no están, entonces dejamos de trabajar, si los hay, borramos el primer elemento. El clic izquierdo funciona en un sistema similar. Primero, creemos un bloque. Comenzamos raycast y si hay un bloque que cruzó el rayo, obtenemos las coordenadas de este bloque. A continuación, determinamos de qué lado se produjo el clic. Cambiamos las coordenadas del cubo creado de acuerdo con el lado al que agregamos el bloque. gradación en 5 unidades porque este es el tamaño del bloque (sí, puede usar una propiedad de la configuración aquí).
¿Cómo funciona el control de la cámara? Tenemos tres métodos inputKeydown, inputKeyup y update. En inputKeydown, agregamos el botón a la matriz de claves. inputKeyup es responsable de borrar los botones de la matriz que se han pulsado. En la actualización, se verifican las teclas y se llama a moveForward en la cámara, los parámetros que toma el método son la velocidad.
controles.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);
}
}
}
Enlaces
Como lo prometí. Todo el material que viene muy bien.
Si lo desea, puede agregar su funcionalidad al proyecto en github.
perlin.js
three.js
GitHub