Membuat minecraft Anda di JavaScript

Selamat datang di arsitektur proyek yang paling rumit. Ya, saya bisa menulis pengantar ...



gambar



Mari kita coba membuat demo minecraft kecil di browser. Pengetahuan tentang JS dan three.js akan berguna.



Sedikit konvensi. Saya tidak mengklaim sebagai aplikasi terbaik abad ini. Ini hanya implementasi saya untuk tugas ini. Ada juga versi video buat yang males baca (ada maknanya sama, tapi beda kata).



Ini adalah versi videonya




Ada semua tautan yang Anda butuhkan di akhir artikel. Saya akan mencoba air sesedikit mungkin dalam teks. Saya tidak akan menjelaskan cara kerja setiap baris. Sekarang kamu bisa mulai.



Untuk memulainya, untuk memahami apa hasilnya, berikut adalah demo gimnya .



Mari bagi artikel menjadi beberapa bagian:



  1. Struktur proyek
  2. Game loop
  3. Pengaturan permainan
  4. Pembuatan peta
  5. Kamera dan kontrol


Struktur proyek



Seperti inilah tampilan struktur proyek.



gambar



index.html - Lokasi kanvas, beberapa antarmuka dan hubungan gaya, skrip.

style.css - Gaya untuk tampilan saja. Yang terpenting adalah kursor khusus untuk game tersebut, yang terletak di tengah layar.



tekstur - Ini berisi tekstur untuk kursor dan blok tanah untuk game.

core.js - Skrip utama tempat proyek diinisialisasi.

perlin.js - Ini adalah perpustakaan untuk kebisingan Perlin.

PointerLockControls.js - Kamera dari three.js.

controls.js - Kontrol kamera dan pemutar.

generationMap.js - Generasi dunia.

three.module.js - Three.js sendiri sebagai modul.

settings.js - Pengaturan proyek.



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

      
      





Game loop



Di core.js, Anda perlu menginisialisasi three.js, mengkonfigurasinya dan menambahkan semua modul yang diperlukan dari game + event handler ... dan memulai game loop. Mengingat semua pengaturannya standar, tidak ada gunanya menjelaskannya. Anda dapat berbicara tentang peta (dibutuhkan adegan permainan untuk menambahkan blok) dan contorl. dibutuhkan beberapa parameter. Yang pertama adalah kamera dari three.js, pemandangan untuk menambahkan blok dan peta sehingga Anda dapat berinteraksi dengannya. update bertanggung jawab untuk memperbarui kamera, GameLoop adalah game loop, render adalah standar dari three.js untuk memperbarui frame, acara resize juga merupakan standar untuk bekerja dengan kanvas (ini adalah implementasi adaptif).



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

      
      





Pengaturan



Dimungkinkan untuk mengambil parameter lain ke dalam pengaturan, misalnya, pengaturan three.js, tetapi saya melakukannya tanpa mereka dan sekarang hanya ada beberapa parameter yang bertanggung jawab untuk ukuran blok.



settings.js



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

      
      





Pembuatan peta



Di kelas Map, kami memiliki beberapa properti yang bertanggung jawab atas cache material dan parameter kebisingan Perlin. Dalam metode pembangkitan, kami memuat tekstur, membuat geometri dan mesh. noise.seed bertanggung jawab atas grain awal untuk pembuatan peta. Anda dapat mengganti random dengan nilai statis sehingga kartunya selalu sama. Dalam satu lingkaran di sepanjang koordinat X dan Z, kita mulai menyusun kubus. Koordinat Y dihasilkan oleh pustaka pretlin.js. Akhirnya, kami menambahkan kubus dengan koordinat yang diinginkan ke tempat kejadian melalui 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 );
				
			}
		}
	}
}

      
      





Kamera dan kontrol



Saya sudah mengatakan bahwa kontrol mengambil parameter dalam bentuk kamera, pemandangan, dan peta. Juga di konstruktor, kami menambahkan array kunci untuk kunci dan movingSpeed โ€‹โ€‹untuk kecepatan. Untuk mouse, kami memiliki 3 metode. onClick menentukan tombol mana yang diklik, dan onRightClick serta onLeftClick sudah bertanggung jawab atas tindakannya. Klik kanan (penghapusan blok) terjadi melalui raycast dan mencari elemen yang berpotongan. Jika tidak ada, maka kita berhenti bekerja, jika ada, maka kita hapus elemen pertama. Klik kiri berfungsi pada sistem serupa. Pertama, mari buat satu blok. Kami memulai siaran sinar dan jika ada blok yang melintasi sinar tersebut, maka kami mendapatkan koordinat dari blok ini. Selanjutnya, kami menentukan dari sisi mana klik terjadi. Kami mengubah koordinat untuk kubus yang dibuat sesuai dengan sisi tempat kami menambahkan blok. gradasi dalam 5 unit karena ini adalah ukuran blok (ya, Anda dapat menggunakan properti dari pengaturan di sini).



Bagaimana cara kerja kontrol kamera ?! Kami memiliki tiga metode inputKeydown, inputKeyup dan update. Di inputKeydown, kami menambahkan tombol ke array kunci. inputKeyup bertanggung jawab untuk menghapus tombol dari array yang telah ditekan. Dalam pembaruan, tombol diperiksa dan moveForward dipanggil pada kamera, parameter yang diambil metode ini adalah kecepatan.



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

      
      





Tautan



Seperti yang sudah saya janjikan. Semua materi yang berguna.



Jika mau, Anda tidak dapat menambahkan fungsionalitas Anda ke proyek di github.



perlin.js

three.js

GitHub



All Articles