Créer votre minecraft en JavaScript

Bienvenue dans l'architecture de projet la plus complexe. Oui, je peux écrire une introduction ...



image



Essayons de faire une petite démo de minecraft dans le navigateur. La connaissance de JS et de three.js sera utile.



Un peu de convention. Je ne prétends pas être la meilleure application du siècle. Ceci est juste ma mise en œuvre pour cette tâche. Il existe également une version vidéo pour ceux qui sont trop paresseux pour lire (il y a le même sens, mais dans des mots différents).



Voici la version vidéo




Il y a tous les liens dont vous avez besoin à la fin de l'article. J'essaierai le moins d'eau possible dans le texte. Je n'expliquerai pas comment chaque ligne fonctionne. Maintenant vous pouvez commencer.



Pour commencer, pour comprendre quel sera le résultat, voici une démo du jeu .



Divisons l'article en plusieurs parties:



  1. Structure du projet
  2. Boucle de jeu
  3. Paramètres de jeu
  4. Génération de carte
  5. Caméra et commandes


Structure du projet



Voici à quoi ressemble la structure du projet.



image



index.html - L'emplacement du canevas, certaines interfaces et la connexion des styles, des scripts.

style.css - Styles pour l'apparence uniquement. La chose la plus importante est le curseur personnalisé pour le jeu, qui se trouve au centre de l'écran.



texture - Voici les textures pour le curseur et le bloc de sol pour le jeu.

core.js - Le script principal où le projet est initialisé.

perlin.js - Ceci est une bibliothèque pour le bruit Perlin.

PointerLockControls.js - Caméra de three.js.

controls.js - Commandes de l'appareil photo et du lecteur.

generationMap.js - Génération mondiale.

three.module.js - Three.js lui-même en tant que module.

settings.js - Paramètres du projet.



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

      
      





Boucle de jeu



Dans core.js, vous devez initialiser three.js, le configurer et ajouter tous les modules nécessaires du jeu + les gestionnaires d'événements ... eh bien, lancez la boucle de jeu. Étant donné que tous les paramètres sont standard, il est inutile de les expliquer. Vous pouvez parler de carte (il faut la scène du jeu pour ajouter des blocs) et de contorls. il faut plusieurs paramètres. Le premier est une caméra de three.js, une scène pour ajouter des blocs et une carte pour que vous puissiez interagir avec elle. update est responsable de la mise à jour de la caméra, GameLoop est la boucle de jeu, le rendu est le standard de three.js pour la mise à jour du cadre, l'événement de redimensionnement est également le standard pour travailler avec le canevas (c'est l'implémentation de l'adaptatif).



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

      
      





Réglages



Il était possible de supprimer d'autres paramètres dans les paramètres, par exemple les paramètres three.js, mais je l'ai fait sans eux et maintenant il n'y a que quelques paramètres responsables de la taille du bloc.



settings.js



export class Settings {
	constructor() {
		//  
		this.blockSquare 		= 5;
		//    
		this.chunkSize 			= 16;
		this.chunkSquare 		= this.chunkSize * this.chunkSize;
	}
}

      
      





Génération de carte



Dans la classe Map, nous avons plusieurs propriétés qui sont responsables du cache de matériaux et des paramètres du bruit Perlin. Dans la méthode de génération, nous chargeons des textures, créons une géométrie et un maillage. noise.seed est responsable du grain de départ pour la génération de la carte. Vous pouvez remplacer aléatoire par une valeur statique afin que les cartes soient toujours les mêmes. Dans une boucle le long des coordonnées X et Z, nous commençons à organiser les cubes. La coordonnée Y est générée par la bibliothèque pretlin.js. Finalement, nous ajoutons le cube avec les coordonnées souhaitées à la scène via scene.add (cube);



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

      
      





Caméra et commandes



J'ai déjà dit que les contrôles prennent des paramètres sous la forme d'une caméra, d'une scène et d'une carte. Également dans le constructeur, nous ajoutons un tableau de clés pour les clés et une vitesse de déplacement pour la vitesse. Pour la souris, nous avons 3 méthodes. onClick détermine le bouton sur lequel l'utilisateur clique, et onRightClick et onLeftClick sont déjà responsables des actions. Le clic droit (suppression de bloc) se produit via raycast et recherche les éléments intersectés. S'ils ne sont pas là, alors nous arrêtons de travailler, s'il y en a, nous supprimons le premier élément. Le clic gauche fonctionne sur un système similaire. Commençons par créer un bloc. Nous commençons raycast et s'il y a un bloc qui a traversé le rayon, alors nous obtenons les coordonnées de ce bloc. Ensuite, nous déterminons de quel côté le clic s'est produit. Nous modifions les coordonnées du cube créé en fonction du côté auquel nous ajoutons le bloc. gradation en 5 unités car c'est la taille du bloc (oui, vous pouvez utiliser une propriété des paramètres ici).



Comment fonctionne le contrôle de la caméra?! Nous avons trois méthodes inputKeydown, inputKeyup et update. Dans inputKeydown, nous ajoutons le bouton au tableau de clés. inputKeyup est responsable de l'effacement des boutons du tableau qui ont été enfoncés. Dans la mise à jour, les touches sont vérifiées et moveForward est appelé sur la caméra, les paramètres pris par la méthode sont la vitesse.



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

      
      





Liens



Comme je l'ai promis. Tout le matériel utile.



Si vous le souhaitez, vous pouvez ajouter vos propres fonctionnalités au projet sur le github.



perlin.js

trois.js

GitHub



All Articles