Willkommen in der kompliziertesten Projektarchitektur. Ja, ich kann eine Einführung schreiben ...
Versuchen wir, eine kleine Minecraft-Demo im Browser zu erstellen. Kenntnisse in JS und three.js sind hilfreich.
Ein bisschen Konvention. Ich behaupte nicht, die beste App des Jahrhunderts zu sein. Dies ist nur meine Implementierung für diese Aufgabe. Es gibt auch eine Videoversion für diejenigen, die zu faul zum Lesen sind (es gibt die gleiche Bedeutung, aber mit anderen Worten).
Hier ist die Videoversion
Am Ende des Artikels finden Sie alle Links, die Sie benötigen. Ich werde im Text so wenig Wasser wie möglich probieren. Ich werde nicht erklären, wie jede Zeile funktioniert. Jetzt können Sie beginnen.
Um zu verstehen, was das Ergebnis sein wird, folgt zunächst eine Demo des Spiels .
Teilen wir den Artikel in mehrere Teile:
- Projektstruktur
- Spielschleife
- Spieleinstellungen
- Kartengenerierung
- Kamera und Bedienelemente
Projektstruktur
So sieht die Projektstruktur aus.
index.html - Der Speicherort der Zeichenfläche, einige Schnittstellen und die Verbindung von Stilen und Skripten.
style.css - Stile nur für das Erscheinungsbild. Das Wichtigste ist der benutzerdefinierte Cursor für das Spiel, der sich in der Mitte des Bildschirms befindet.
Textur - Hier sind die Texturen für den Cursor und den Grundblock für das Spiel.
core.js - Das Hauptskript, in dem das Projekt initialisiert wird.
perlin.js - Dies ist eine Bibliothek für Perlin-Rauschen.
PointerLockControls.js - Kamera von three.js.
controls.js - Kamera- und Player-Steuerelemente.
generationMap.js - Weltgeneration.
three.module.js - Three.js selbst als Modul.
settings.js - Projekteinstellungen.
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);
}
Spielschleife
In core.js müssen Sie three.js initialisieren, konfigurieren und alle erforderlichen Module aus den Game + Event-Handlern hinzufügen ... und die Spielschleife starten. Da alle Einstellungen Standard sind, macht es keinen Sinn, sie zu erklären. Sie können über Karten (es braucht die Spielszene, um Blöcke hinzuzufügen) und Contorls sprechen. Es werden mehrere Parameter benötigt. Die erste ist eine Kamera von three.js, eine Szene zum Hinzufügen von Blöcken und eine Karte, damit Sie damit interagieren können. update ist für die Aktualisierung der Kamera verantwortlich, GameLoop ist die Spieleschleife, render ist der Standard von three.js für die Aktualisierung des Frames, das Größenänderungsereignis ist auch der Standard für die Arbeit mit der Zeichenfläche (dies ist die Implementierung des adaptiven).
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);
});
die Einstellungen
Es war möglich, andere Parameter in die Einstellungen aufzunehmen, zum Beispiel die Einstellungen von three.js, aber ich habe darauf verzichtet, und jetzt sind nur noch wenige Parameter für die Blockgröße verantwortlich.
settings.js
export class Settings {
constructor() {
//
this.blockSquare = 5;
//
this.chunkSize = 16;
this.chunkSquare = this.chunkSize * this.chunkSize;
}
}
Kartengenerierung
In der Map-Klasse haben wir mehrere Eigenschaften, die für den Materialcache und die Parameter für Perlin-Rauschen verantwortlich sind. Bei der Generierungsmethode laden wir Texturen, erstellen Geometrie und Netz. NOise.seed ist für das Startkorn der Kartengenerierung verantwortlich. Sie können zufällig durch einen statischen Wert ersetzen, damit die Karten immer gleich sind. In einer Schleife entlang der X- und Z-Koordinaten beginnen wir, die Würfel anzuordnen. Die Y-Koordinate wird von der Bibliothek pretlin.js generiert. Zum Schluss fügen wir den Würfel mit den gewünschten Koordinaten über scene.add (Würfel) zur Szene hinzu.
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 und Bedienelemente
Ich habe bereits gesagt, dass Steuerelemente Parameter in Form einer Kamera, einer Szene und einer Karte annehmen. Außerdem fügen wir im Konstruktor ein Array von Schlüsseln für die Schlüssel und eine MovingSpeed für die Geschwindigkeit hinzu. Für die Maus haben wir 3 Methoden. onClick bestimmt, auf welche Schaltfläche geklickt wird, und onRightClick und onLeftClick sind bereits für Aktionen verantwortlich. Ein Rechtsklick (Blocklöschung) durchläuft Raycast und sucht nach geschnittenen Elementen. Wenn sie nicht da sind, hören wir auf zu arbeiten. Wenn ja, löschen wir das erste Element. Linksklick funktioniert auf einem ähnlichen System. Zuerst erstellen wir einen Block. Wir starten den Raycast und wenn es einen Block gibt, der den Strahl gekreuzt hat, erhalten wir die Koordinaten dieses Blocks. Als nächstes bestimmen wir, von welcher Seite der Klick erfolgte. Wir ändern die Koordinaten für den erstellten Würfel entsprechend der Seite, zu der wir den Block hinzufügen. Abstufung in 5 Einheiten, weil Dies ist die Blockgröße (ja, Sie können hier eine Eigenschaft aus den Einstellungen verwenden).
Wie funktioniert die Kamerasteuerung ?! Wir haben drei Methoden inputKeydown, inputKeyup und update. In inputKeydown fügen wir die Schaltfläche dem Keys-Array hinzu. inputKeyup ist dafür verantwortlich, die Tasten aus dem Array zu löschen, die gedrückt wurden. Beim Update werden die Tasten überprüft und moveForward wird auf der Kamera aufgerufen. Die Parameter, die die Methode verwendet, sind die Geschwindigkeit.
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
Wie ich versprochen habe. Das ganze Material, das praktisch ist.
Wenn Sie möchten, können Sie dem Projekt auf dem Github Ihre eigene Funktionalität hinzufügen.
perlin.js
three.js
GitHub