JavaScriptであなたのminecraftを作る

最も複雑なプロジェクトアーキテクチャへようこそ。はい、紹介を書くことができます...



画像



ブラウザで小さなminecraftのデモを作ってみましょう。JSとthree.jsの知識が役に立ちます。



ちょっとした慣習。私は今世紀最高のアプリであるとは主張していません。これは、このタスクの私の実装にすぎません。怠惰すぎて読めない人のためのビデオ版もあります(同じ意味ですが、言葉が異なります)。



これがビデオバージョンです




記事の最後に必要なすべてのリンクがあります。本文中の水はできるだけ少なくしてみます。各行がどのように機能するかについては説明しません。これで開始できます。



まず、結果がどうなるかを理解するために、ここ にゲームのデモがあります。



記事をいくつかの部分に分けてみましょう。



  1. プロジェクト構造
  2. ゲームループ
  3. ゲームの設定
  4. マップの生成
  5. カメラとコントロール


プロジェクト構造



プロジェクトの構造は次のようになります。



画像



index.html-キャンバスの場所、いくつかのインターフェイス、スタイル、スクリプトの接続。

style.css-外観のみのスタイル。最も重要なのは、画面の中央にあるゲームのカスタムカーソルです。



テクスチャ-これは、ゲームのカーソルとグラウンドブロックのテクスチャです。

core.js-プロジェクトが初期化されるメインスクリプト。

perlin.js-これはPerlinノイズ用のライブラリです。

PointerLockControls.js-3.jsのカメラ。

Controls.js-カメラとプレーヤーのコントロール。

GenerationMap.js-世界の世代。

three.module.js-モジュールとしてのThree.js自体。

settings.js-プロジェクト設定。



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

      
      





ゲームループ



core.jsでは、three.jsを初期化し、構成し、ゲーム+イベントハンドラーから必要なすべてのモジュールを追加する必要があります...そしてゲームループを開始します。すべての設定が標準であることを考えると、それらを説明する意味はありません。マップ(ブロックを追加するにはゲームシーンが必要です)とコントロールについて話すことができます。いくつかのパラメータを取ります。1つ目は、three.jsのカメラで、ブロックとマップを追加して操作できるようにするためのシーンです。updateはカメラの更新を担当し、GameLoopはゲームループであり、renderはフレームを更新するためのthree.jsの標準であり、resizeイベントはキャンバスを操作するための標準でもあります(これはアダプティブの実装です)。



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

      
      





設定



他のパラメータ、たとえばthree.js設定を設定に取り込むことは可能でしたが、それらを使用せずに行ったため、ブロックサイズの原因となるパラメータは2つしかありません。



settings.js



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

      
      





マップの生成



Mapクラスには、Perlinノイズのマテリアルキャッシュとパラメータを担当するいくつかのプロパティがあります。生成方法では、テクスチャをロードし、ジオメトリとメッシュを作成します。noise.seedは、マップ生成の開始グレインを担当します。カードが常に同じになるように、ランダムを静的な値に置き換えることができます。X座標とZ座標に沿ったループで、キューブの配置を開始します。Y座標は、pretlin.jsライブラリによって生成されます。最終的に、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 );
				
			}
		}
	}
}

      
      





カメラとコントロール



コントロールはカメラ、シーン、マップの形でパラメータを取得することはすでに述べました。また、コンストラクターでは、キーのキーの配列と速度のmovingSpeedを追加します。マウスの場合、3つの方法があります。 onClickはクリックされるボタンを決定し、onRightClickとonLeftClickはすでにアクションを担当しています。右クリック(ブロックの削除)はレイキャストを通過し、交差する要素を検索します。それらが存在しない場合は動作を停止し、存在する場合は最初の要素を削除します。左クリックは同様のシステムで機能します。まず、ブロックを作成しましょう。レイキャストを開始し、レイを横切ったブロックがある場合は、このブロックの座標を取得します。次に、どちら側からクリックが発生したかを判別します。ブロックを追加する側に応じて、作成したキューブの座標を変更します。 5単位のグラデーションこれはブロックサイズです(はい、ここの設定からプロパティを使用できます)。



カメラ制御はどのように機能しますか?!inputKeydown、inputKeyup、updateの3つのメソッドがあります。inputKeydownで、ボタンをキー配列に追加します。inputKeyupは、押されたアレイからボタンをクリアする役割を果たします。更新では、キーがチェックされ、moveForwardがカメラで呼び出されます。メソッドが取るパラメーターは、速度です。



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

      
      





リンク



私が約束したように。重宝するすべての資料。



必要に応じて、githubのプロジェクトに独自の機能を追加できます。



perlin.js

three.js

GitHub



All Articles