週刊 WebでMinec◯aft的なものを作る! ~第4週目~

  • 7
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

今週もやってきました!週刊Minec◯aft Webの時間です。今回も大学生プログラマ うーぴょん(さすらいうさぎ)がお送りしたいと思います。

前回、ワールドを動けるようになったので、そろそろブロックを置いたり壊したりしていきたいねってことで、今回はその準備を進めていきたいと思います!
具体的には、ブロックがどこに置かれているかを管理するシステムを作って行こうと思います!

アジェンダ的な

  1. Worldクラスを作ろう!
  2. Renderingクラスを作ろう!

Let's try!の時間

Worldクラスを作ろう!

STEP1 ファイルを作る

それといった解説がないので、さらっと流します。

ターミナル
$ touch js/world.js
js/World.js
class World {
  constructor() {
  }
}
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Minec◯aft Web Edition</title>
  <script src="/js/three.min.js"></script>
  <script src="/js/blocks/Block.js"></script>
  <script src="/js/blocks/BlockDirt.js"></script>
  <script src="/js/blocks/BlockRock.js"></script>
  <script src="/js/blocks/BlockBedRock.js"></script>
  <script src="/js/blocks/Blocks.js"></script>
  <script src="/js/World.js"></script>
  <script src="/js/application.js"></script>
</head>
<body>
  <div id="canvas_wrapper"></div>
</body>
</html>

STEP2 ブロックを管理する配列を準備する。

一旦、幅16ブロック × 奥行き16ブロック × 高さ256ブロックのワールドを作って行こうと思います。
1次元目が幅・2次元目が奥行き・3次元目が高さの添字である配列を作って座標とブロックの対応表を作ります

js/World.js
class World {
  constructor() {
    this.blocksAroundPlayer = [];
    for(let x = 0; x < 16; ++x) {
      this.blocksAroundPlayer[x] = [];
      for(let z = 0; z < 16; ++z) {
        this.blocksAroundPlayer[x][z] = [];
        for(let y = 0; y < 256; ++y) {
          this.blocksAroundPlayer[x][z][y] = [];
          if(y <= 1) { 
            this.blocksAroundPlayer[x][z][y] = Blocks.bedrock;
          } else if(y > 1 && y < 64) {
            this.blocksAroundPlayer[x][z][y] = Blocks.rock;
          } else if(y == 64) {
            this.blocksAroundPlayer[x][z][y] = Blocks.dirt;
          } else {
            this.blocksAroundPlayer[x][z][y] = Blocks.air;
          }
        }
      }
    }
  }

  getBlock(x, y, z) {
    return this.blocksAroundPlayer[x][z][y];
  }
}

合わせて、ブロックを取得するメソッドを作っています。
よくよく考えたら、空気ブロック必要だねってことで追加しました。

js/blocks/BlockAir.js
class BlockAir extends Block {
  constructor() {
    super("air");
  }

  setTextures() {
    this.material = new THREE.MeshBasicMaterial({color: 0xffffff, opacity: 0.003, transparent: true});
  }
}

opacitytransparentを使って透明なブロックを作っています。(あえて、完全に透明にしないことで霞を表現しています)

js/blocks/Blocks.js
const Blocks = {
  air: new BlockAir(),
  dirt: new BlockDirt(),
  rock: new BlockRock(),
  bedrock: new BlockBedrock()
}

for(let blockName in Blocks) {
  Blocks[blockName].registerTextures();
  Blocks[blockName].setTextures();
}
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Minec◯aft Web Edition</title>
  <script src="/js/three.min.js"></script>
  <script src="/js/blocks/Block.js"></script>
  <script src="/js/blocks/BlockAir.js"></script>
  <script src="/js/blocks/BlockDirt.js"></script>
  <script src="/js/blocks/BlockRock.js"></script>
  <script src="/js/blocks/BlockBedRock.js"></script>
  <script src="/js/blocks/Blocks.js"></script>
  <script src="/js/World.js"></script>
  <script src="/js/application.js"></script>
</head>
<body>
  <div id="canvas_wrapper"></div>
</body>
</html>

STEP3 Worldクラスを読み込もう!

続いてWorldクラスを読み込みます!

js/application.js(抜粋)
window.onload = () => {
  let [posX, posY, posZ] = [0, 64, 0];
  let [yaw, pitch] = [0, 0];
  let [sightX, sightY, sightZ] = [1, 0, 0];
  let [moveX, moveZ] = [1, 0];
  let renderer = new THREE.WebGLRenderer({antialias: true});
  let scene = new THREE.Scene();
  let camera = new THREE.PerspectiveCamera(40, 800 / 480);
  let cube = new THREE.BoxGeometry(1, 1, 1);
  let light = new THREE.AmbientLight(0xffffff);
  let world = new World();
  let updateCanvas = () => {
    requestAnimationFrame(updateCanvas);

    camera.position.set(posX, posY + 1.6, posZ);
    camera.lookAt(new THREE.Vector3(posX + sightX, posY + 1.6 + sightY, posZ + sightZ));
    renderer.render(scene, camera);
  }

  renderer.setSize(800, 480);
  renderer.setClearColor(0xffffff);
  document.getElementById("canvas_wrapper").appendChild(renderer.domElement);


  for(let x = 0; x < 16; ++x) {
    for(let z = 0; z < 16; ++z) {
      for(let y = 0; y < 256; ++y) {
        let block = new THREE.Mesh(cube, world.getBlock(x, y, z).getMaterial());

        block.position.set(x, y, z);
        scene.add(block);
      }
    }
  }

  scene.add(light);

地味にいろんなところを変えているので注意してください!
これを動かすとこんな感じになります!
スクリーンショット 2016-10-01 10.17.14.png
なんかそれっぽい。

Renderingクラスを作ろう!

レンダリングも抽象化してしまいます。

STEP1 ファイルの作成

ターミナル
$ touch js/Rendering.js
js/Rendering.js
class Rendering {
  constructor(canvasId, world) {
    this.renderer = new THREE.WebGLRenderer({antialias: true});
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(40, 800 / 480);
    this.renderer.setSize(800, 480);
    this.renderer.setClearColor(0xffffff);
    this.meshes = [];
    document.getElementById(canvasId).appendChild(this.renderer.domElement);
    this.init(world);
    this.update();
  } 

  init(world) {
    let light = new THREE.AmbientLight(0xffffff);
    this.cube = new THREE.BoxGeometry(1, 1, 1);
    [this.posX, this.posY, this.posZ] = [0, 64, 0];
    [this.sightX, this.sightY, this.sightZ] = [1, 0, 0];
    this.scene.add(light);
    this.resetWorld(world);
  }

  resetWorld(world) {
    for(let x = 0; x < 16; ++x) {
      this.meshes[x] = [];
      for(let z = 0; z < 16; ++z) {
        this.meshes[x][z] = [];
        for(let y = 0; y < 256; ++y) {
          let block = new THREE.Mesh(this.cube, world.getBlock(x, y, z).getMaterial());

          block.position.set(x, y, z);
          this.meshes[x][z][y] = block;
          this.scene.add(block);
        }
      }
    }
  }

  update() {
    let update = () => {
      requestAnimationFrame(update);
      this.camera.position.set(this.posX, this.posY + 1.6, this.posZ);
      this.camera.lookAt(new THREE.Vector3(this.posX + this.sightX, this.posY + 1.6 + this.sightY, this.posZ + this.sightZ));
      this.renderer.render(this.scene, this.camera);
    };
    update();
  }

  move(deltaX, deltaY, deltaZ) {
    this.posX += deltaX;
    this.posY += deltaY;
    this.posZ += deltaZ;
  }

  moveTo(newX, newY, newZ) {
    [this.posX, this.posY, this.posZ] = [newX, newY, newZ];
  }

  rotate(x, y, z) {
    [this.sightX, this.sightY, this.sightZ] = [x, y, z];
  }
}
js/application.js
window.onload = () => {
  let world = new World();
  let rendering = new Rendering("canvas_wrapper", world);
  let [posX, posY, posZ] = [0, 64, 0];
  let [yaw, pitch] = [0, 0];
  let [sightX, sightY, sightZ] = [1, 0, 0];
  let [moveX, moveZ] = [1, 0];

  document.onkeydown = (e) => {
    if(e.keyCode == 87) {
      rendering.move(0.3 * moveX, 0, 0.3 * moveZ);
    } else if(e.keyCode == 65) {
      rendering.move(0.3 * moveZ, 0, -0.3 * moveX);
    } else if(e.keyCode == 83) {
      rendering.move(-0.3 * moveX, 0, -0.3 * moveZ);
    } else if(e.keyCode == 68) {
      rendering.move(-0.3 * moveZ, 0, 0.3 * moveX);
    } else if(e.keyCode == 32 && e.shiftKey) {
      rendering.move(0, -0.3, 0);
    } else if(e.keyCode == 32) {
      rendering.move(0, 0.3, 0);
    }
  }

  document.onmousemove = (e) => {
    let deltaX = e.momentX || e.webkitMovementX || e.mozMovementX || e.movementX;
    let deltaY = e.momentY || e.webkitMovementY || e.mozMovementY || e.movementY;
    const radian = Math.PI / 180;

    yaw += deltaX / 0.8;
    pitch -= deltaY / 0.8;

    if(pitch > 90) {
      pitch = 90;
    } else if(pitch < -90) {
      pitch = -90;
    }

    rendering.rotate(Math.cos(radian * pitch) * Math.cos(radian * yaw), Math.sin(radian* pitch), Math.cos(radian * pitch) * Math.sin(radian * yaw));
    [moveX, moveZ] = [Math.cos(radian * yaw), Math.sin(radian * yaw)];
  }

}
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Minec◯aft Web Edition</title>
  <script src="/js/three.min.js"></script>
  <script src="/js/blocks/Block.js"></script>
  <script src="/js/blocks/BlockAir.js"></script>
  <script src="/js/blocks/BlockDirt.js"></script>
  <script src="/js/blocks/BlockRock.js"></script>
  <script src="/js/blocks/BlockBedRock.js"></script>
  <script src="/js/blocks/Blocks.js"></script>
  <script src="/js/World.js"></script>
  <script src="/js/Rendering.js"></script>
  <script src="/js/application.js"></script>
</head>
<body>
  <div id="canvas_wrapper"></div>
</body>
</html>

基本的にはコードの移し替えをしただけなので特に説明事項はありません。ただ、Renderingクラスにはmeshesというインスタンス変数を用意しています。これは、Meshオブジェクトを保存するのに使っています。これを用意することで、ワールドのある位置にあるブロックを消すことができます。(座標の情報を持っているのはMeshなので)

STEP2 ブロックを置き換えるメソッドを準備する。

WorldクラスにもRenderingクラスにも、情報を追加する必要があります。

js/World.js
class World {
  constructor() {
    this.blocksAroundPlayer = [];
    for(let x = 0; x < 16; ++x) {
      this.blocksAroundPlayer[x] = [];
      for(let z = 0; z < 16; ++z) {
        this.blocksAroundPlayer[x][z] = [];
        for(let y = 0; y < 256; ++y) {
          this.blocksAroundPlayer[x][z][y] = [];
          if(y <= 1) { 
            this.blocksAroundPlayer[x][z][y] = Blocks.bedrock;
          } else if(y > 1 && y < 64) {
            this.blocksAroundPlayer[x][z][y] = Blocks.rock;
          } else if(y == 64) {
            this.blocksAroundPlayer[x][z][y] = Blocks.dirt;
          } else {
            this.blocksAroundPlayer[x][z][y] = Blocks.air;
          }
        }
      }
    }
  }

  getBlock(x, y, z) {
    return this.blocksAroundPlayer[x][z][y];
  }

  setBlock(x, y, z, block) {
    this.blocksAroundPlayer[x][z][y] = block;
  }
}
js/Rendering.js
class Rendering {
  constructor(canvasId, world) {
    this.renderer = new THREE.WebGLRenderer({antialias: true});
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(40, 800 / 480);
    this.renderer.setSize(800, 480);
    this.renderer.setClearColor(0xffffff);
    this.meshes = [];
    document.getElementById(canvasId).appendChild(this.renderer.domElement);
    this.init(world);
    this.update();
  } 

  init(world) {
    let light = new THREE.AmbientLight(0xffffff);
    this.cube = new THREE.BoxGeometry(1, 1, 1);
    [this.posX, this.posY, this.posZ] = [0, 64, 0];
    [this.sightX, this.sightY, this.sightZ] = [1, 0, 0];
    this.scene.add(light);
    this.resetWorld(world);
  }

  resetWorld(world) {
    let cube = new THREE.BoxGeometry(1, 1, 1);
    for(let x = 0; x < 16; ++x) {
      this.meshes[x] = [];
      for(let z = 0; z < 16; ++z) {
        this.meshes[x][z] = [];
        for(let y = 0; y < 256; ++y) {
          let block = new THREE.Mesh(this.cube, world.getBlock(x, y, z).getMaterial());

          block.position.set(x, y, z);
          this.meshes[x][z][y] = block;
          this.scene.add(block);
        }
      }
    }
  }

  update() {
    let update = () => {
      requestAnimationFrame(update);
      this.camera.position.set(this.posX, this.posY + 1.6, this.posZ);
      this.camera.lookAt(new THREE.Vector3(this.posX + this.sightX, this.posY + 1.6 + this.sightY, this.posZ + this.sightZ));
      this.renderer.render(this.scene, this.camera);
    };
    update();
  }

  move(deltaX, deltaY, deltaZ) {
    this.posX += deltaX;
    this.posY += deltaY;
    this.posZ += deltaZ;
  }

  moveTo(newX, newY, newZ) {
    [this.posX, this.posY, this.posZ] = [newX, newY, newZ];
  }

  rotate(x, y, z) {
    [this.sightX, this.sightY, this.sightZ] = [x, y, z];
  }

  updateMesh(x, y, z, world) {
    let block = new THREE.Mesh(this.cube, world.getBlock(x, y, z).getMaterial());

    block.position.set(x, y, z);
    this.scene.remove(this.meshes[x][z][y]);
    this.scene.add(block);
    this.meshes[x][z][y] = block;
  }
}

ここで重要な点は、RenderingクラスのupdateMeshメソッドです。このメソッドでは、新たにMeshオブジェクトを作成して、古いMeshオブジェクトをSceneオブジェクトから取り除き(removeメソッド)、新しいMeshオブジェクトを追加しています。
こうしないと古いMeshが溜まっていくのでメモリを食いつぶしてしまうほか、オブジェクトが重なりあってしまいます。(本当は、Geometoryオブジェクト・Materialオブジェクト・Textureオブジェクト等も削除する必要があるのですが、今回は使いまわしているのでこの作業は不要です)

このメソッドが完成することで、ブロックの追加と削除が可能になりました!
(ブロックの削除は、BlockAirオブジェクトとの置換で実現します)

まとめ

SceneオブジェクトからMeshオブジェクトを取り除く時にはremoveメソッドを活用しましょう。この時に必要に応じて他のオブジェクト(GeometoryとかMaterialとか)を削除しないとメモリを食いつぶすので注意!

これでブロックを置いたり壊すための準備が整いました!次の回でユーザの操作でブロックの破壊や追加ができるようにしていきましょう!では、またっ!

号外1はこちら

週刊 WebでMinec◯aft的なものを作る! ~号外1~