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

  • 0
    いいね
  • 0
    コメント

    今週もやってきました!週刊マイ◯ラWeb!本日の司会進行も、うーぴょん(さすらいうさぎ)が担当していきたいと思います。先週は所々の事情で休んでしまいましたが、今週から再び頑張っていこうと思います!
    今週は、エンティティーを作っていきたいと思います。エンティティーは重力の影響を受けるもの、また動くものととりあえず定義しておきます!

    アジェンダ的な

    1. Entityクラスの作成と最低限の実装
    2. EntityCreatureクラスの作成
    3. EntityPlayerクラスの作成

    Let's tryの時間

    Entityクラスの実装

    全てのエンティティーの親クラスとなるEntityクラスを作っていきます。持っておきたいプロパティは以下の通りです。

    プロパティ

    • world: エンティティが所属するワールド
    • x: エンティティの中心のx座標
    • y: エンティティの中心のy座標
    • z: エンティティの中心のz座標
    • nextX: エンティティの次に移動するx座標
    • nextY: エンティティの次に移動するy座標
    • nextZ: エンティティの次に移動するz座標
    • vx: エンティティのx軸方向の速度
    • vy: エンティティのy軸方向の速度
    • vz: エンティティのz軸方向の速度
    • width: エンティティの幅・奥行き
    • height: エンティティの高さ
    • gravity: 重力加速度(定数)
    • updateInterval: 更新を行う頻度
    • needToUpdate: 更新を行う必要があるか

    Entityのあたり判定は、底面が正方形の直方体で考えていこうと思います(本家もそうやってるぽいので)。

    js/entity/Entity.js
    class Entity {
      constructor(targetWorld, posX, posY, posZ) {
        this.gravity = 9800;
        this.updateInterval = 1;
        this.needToUpdate = true;
        this.world = targetWorld;
        this.x = posX;
        this.y = posY;
        this.z = posZ;
        this.nextX = 0;
        this.nextY = 0;
        this.nextZ = 0;
        this.vx = 0;
        this.vy = 0;
        this.vz = 0;
        this.width = 0;
        this.height = 0;
    
        this.update();
      }
    
      update() {
        if(this.needToUpdate) {
          this.nextX = this.x + this.vx * this.updateInterval / 1000;
          this.nextY = this.y + this.vy * this.updateInterval / 1000;
          this.nextZ = this.z + this.vz * this.updateInterval / 1000;
          this.vy -= this.gravity * this.updateInterval / 1000;
    
          if(this.canMove()) {
            this.x = this.nextX;
            this.y = this.nextY;
            this.z = this.nextZ;
          }
        }
        setTimeout(() => this.update(), this.updateInterval);
      }
    
    
      canMove() {
        return true;
      }
    
      isOnGround() {
        return true;
      }
    }
    
    index.html
    <script src="/js/blocks/Blocks.js"></script>
    <script src="/js/entity/Entity.js"></script><!-- このコードを追加 -->
    

    メソッドを説明します。コンストラクタは各種初期化を行っています。udpate()メソッドは、一定時間おきに処理したいものをまとめておきます。(重力の計算とか当たり判定とか)
    canMove()メソッドは移動が可能か返すメソッドです。
    canMove()メソッドの実装は後に回します。isOnGround()メソッドは、地面にエンティティが乗っかっているか返します。

    EntityCreatureクラスの作成

    Entityクラスを継承してEntityCreatureクラスを作成します。こちらは生き物を想定したメソッドになります。

    以下のプロパティをさらに追加します。

    • yaw: ヨー
    • pitch: ピッチ
    • sightX: 視線ベクトルのx成分
    • sightY: 視線ベクトルのy成分
    • sightZ: 視線ベクトルのz成分
    js/entity/EntityCreature.js
    class EntityCreature extends Entity {
      constructor(targetWorld, posX, posY, posZ) {
        super(targetWorld, posX, posY, posZ);
        this.pitch = 0;
        this.yaw = 0
      }
    
      walk(direction) {
        const radian = Math.PI / 180;
        let moveX = 50 * Math.cos(radian * this.yaw), moveZ = 50 * Math.sin(radian * this.yaw);
    
        if(direction == 0) {
          this.vx = moveX;
          this.vz = moveZ;
        } else if(direction == 1) {
          this.vx = -moveZ;
          this.vz = moveX;
        } else if(direction == 2) {
          this.vx = -moveX;
          this.vz = -moveZ;
        } else if(direction == 3) {
          this.vx = moveZ;
          this.vz = -moveX;
        }
      }
    
      stop() {
        this.vx = 0;
        this.vz = 0;
      }
    
      jump() {
        if(this.isOnGround()) {
          this.vy = 10;
        }
      }
    
      fly(direction) {
        if(direction == 0) {
          this.y += 0.3;
        } else if(direction == 1) {
          this.y -= 0.3;
        }
      }
    
      rotate(deltaYaw, deltaPitch) {
        const radian = Math.PI / 180;
        this.yaw += deltaYaw;
        this.pitch -= deltaPitch;
        if(this.pitch > 90) {
          this.pitch = 90;
        } else if(this.pitch < -90) {
          this.pitch = -90;
        }
        this.sightX = Math.cos(radian * this.pitch) * Math.cos(radian * this.yaw);
        this.sightY = Math.sin(radian * this.pitch);
        this.sightZ = Math.cos(radian * this.pitch) * Math.sin(radian * this.yaw);
      }
    
      isOnGround() {
        return true;
      }
    
      getEyeSight() {
        return [this.sightX, this.sightY, this.sightZ];
      }
    }
    
    index.html
    <script src="/js/blocks/Entity.js"></script>
    <script src="/js/entity/EntityCreature.js"></script><!-- このコードを追加 -->
    

    メソッドも幾つか追加していますが、だいたい名前の通りなので割愛します。

    EntityPlayerクラスの作成

    EntityCreatureクラスを継承します。新たに次のプロパティを追加します。

    • target: 攻撃・破壊の対象。

    STEP1 EntityPlayerクラスを作る

    js/entity/EntityPlayer.js
    class EntityPlayer extends EntityCreature {
      constructor(targetWorld, posX, posY, posZ) {
        super(targetWorld, posX, posY, posZ);
        this.width = 0.6;
        this.height = 1.7;
        this.target = null;
      }
    
      update() {
        let getTargetBlockPos = (targetX, targetY, targetZ) => {
          let getPenetratingPosOnBlock = (blockPosX, blockPosY, blockPosZ) => {
            let result = [];
            let getPositionWhen = (axis, v) => {
              let t = 0;
              switch(axis) {
                case 0:
                  t = (v - this.x) / this.sightX;
                  return [v, this.y + 0.75 + t * this.sightY, this.z + t * this.sightZ];
                case 1:
                  t = (v - this.y - 0.75) / this.sightY;
                  return [this.x + t * this.sightX, v, this.z + t * this.sightZ];
                case 2:
                  t = (v - this.z) / this.sightZ;
                  return [this.x + t * this.sightX, this.y + 0.75 + t * this.sightY, v];
                default:
                  return null;
              }
            };
            result.push(getPositionWhen(1, blockPosY + 0.5));
            result.push(getPositionWhen(0, blockPosX + 0.5));
            result.push(getPositionWhen(2, blockPosZ + 0.5));
            result.push(getPositionWhen(0, blockPosX - 0.5));
            result.push(getPositionWhen(2, blockPosZ - 0.5));
            result.push(getPositionWhen(1, blockPosY - 0.5));
    
            return result.map(pos => (pos[0] >= blockPosX - 0.5 && pos[0] <= blockPosX + 0.5 && pos[1] >= blockPosY - 0.5  && pos[1] <= blockPosY + 0.5 && pos[2] >= blockPosZ - 0.5 && pos[2] <= blockPosZ + 0.5) ? pos : null);
          };    
          let pos = getPenetratingPosOnBlock(targetX, targetY, targetZ);
          let getDistance = (a, b) => Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2));
    
          if(targetX < 0 || targetY < 0 || targetZ < 0) {
            return null;
          }
    
          if(getDistance([this.x, this.y + 0.75, this.z], [targetX, targetY, targetZ]) > 10) {
            return null;
          }
    
          if(this.world.getBlock(targetX, targetY, targetZ) != Blocks.air) {
            let side = null;
            if(pos[0] != null && this.sightY < 0) {
              side = 0;
            } else if(pos[1] != null && this.sightX < 0) {
              side = 1;
            } else if(pos[2] != null && this.sightZ < 0) {
              side = 2;
            } else if(pos[3] != null && this.sightX > 0) {
              side = 3;
            } else if(pos[4] != null && this.sightZ > 0) {
              side = 4;
            } else if(pos[5] != null && this.sightY > 0) {
              side = 5;
            }
            return [[targetX, targetY, targetZ], side];
          }
    
          if(pos[0] != null && this.sightY > 0) {
            return getTargetBlockPos(targetX, targetY + 1, targetZ);
          } else if(pos[1] != null && this.sightX > 0) {
            return getTargetBlockPos(targetX + 1, targetY, targetZ);
          } else if(pos[2] != null && this.sightZ > 0) {
            return getTargetBlockPos(targetX, targetY, targetZ + 1);
          } else if(pos[3] != null && this.sightX < 0) {
            return getTargetBlockPos(targetX - 1, targetY, targetZ);
          } else if(pos[4] != null && this.sightZ < 0) {
            return getTargetBlockPos(targetX, targetY, targetZ - 1);
          } else if(pos[5] != null && this.sightY < 0) {
            return getTargetBlockPos(targetX, targetY - 1, targetZ);
          }
    
          return null;
        }
    
        this.target = getTargetBlockPos(Math.floor(this.x), Math.floor(this.y + 0.75), Math.floor(this.z));
        super.update();
      } 
    
      getEyePos() {
        return [this.posX, this.posY + 0.75, this.posZ];
      }
    
    
      destroyBlock() {
        if(this.target == null) {
          return null;
        }
    
        let blockPos = this.target[0];
        this.world.setBlock(blockPos[0], blockPos[1], blockPos[2], Blocks.air);
        return blockPos;
      }
    
      putBlock() {
        if(this.target == null) {
          return null;
        }
        let pos = [this.target[0][0], this.target[0][1], this.target[0][2]];
        let side = this.target[1];
        switch(side) {
          case 0:
            pos[1] += 1;
            break;
          case 1:
            pos[0] += 1;
            break;
          case 2:
            pos[2] += 1;
            break;
          case 3:
            pos[0] -= 1;
            break;
          case 4:
            pos[2] -= 1;
            break;
          case 5:
            pos[1] -= 1;
            break;
        }
        this.world.setBlock(pos[0], pos[1], pos[2], Blocks.dirt);
        return pos;
      }
    }
    
    index.html
    <script src="/js/blocks/EntityCreature.js"></script>
    <script src="/js/entity/EntityPlayer.js"></script><!-- このコードを追加 -->
    

    STEP2 今までのコードの書き換え

    Entityクラス達ができて一部処理が移ったので、諸々書き換えます。

    js/Rendering.js
    class Rendering {
      constructor(canvasId, targetWorld, targetPlayer) {
        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 = [];
        this.world = targetWorld;
        this.player = targetPlayer;
        document.getElementById(canvasId).appendChild(this.renderer.domElement);
        this.init();
        this.update();
      } 
    
      init() {
        let light = new THREE.AmbientLight(0xffffff);
        this.cube = new THREE.BoxGeometry(1, 1, 1);
        this.scene.add(light);
        this.resetWorld();
      }
    
      resetWorld() {
        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, this.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 = () => {
          let [cameraX, cameraY, cameraZ] = this.player.getEyeSight();
          requestAnimationFrame(update);
          this.camera.position.set(this.player.x, this.player.y + 0.75, this.player.z);
          this.camera.lookAt(new THREE.Vector3(this.player.x + cameraX, this.player.y + cameraY + 0.75, this.player.z + cameraZ));
          if(this.player.target != null) {
            let info = this.player.target;
            let pos = (info != null) ? info[0] : null;
            let side = (info != null) ? info[1] : null;
            this.showSelectedBlockMarker(pos[0], pos[1], pos[2]);
          } else {
            this.removeSelectedBlockMarker();
          }
          this.renderer.render(this.scene, this.camera);
        };
        update();
      }
    
      updateMesh(x, y, z) {
        let block = new THREE.Mesh(this.cube, this.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;
      }
    
      showSelectedBlockMarker(x, y, z) {
        if(this.selectedBlockMarker != undefined) {
          this.scene.remove(this.selectedBlockMarker);
        }
    
        this.selectedBlockMarker = new THREE.EdgesHelper(this.meshes[x][z][y], 0x000000);
        this.scene.add(this.selectedBlockMarker);
      }
    
      removeSelectedBlockMarker() {
        if(this.selectedBlockMarker != undefined) {
          this.scene.remove(this.selectedBlockMarker);
        }
      }
    }
    
    js/application.js
    window.onload = () => {
      let world = new World();
      let player = new EntityPlayer(world, 0, 64.85, 0);
      //デバッグの時は入れておくと良い
      //player.gravity = 0;
      //player.vy = 0
      let rendering = new Rendering("canvas_wrapper", world, player);
      let mouseButtons = 0;
      let destroyOrPutBlock = mode => {
        if(mode == 1) {
          let blockPos = player.destroyBlock();
          if(blockPos != null) {
            rendering.updateMesh(blockPos[0], blockPos[1], blockPos[2]);
          } 
        } else if(mode == 2) {
          let blockPos = player.putBlock();
          if(blockPos != null) {
            rendering.updateMesh(blockPos[0], blockPos[1], blockPos[2]);
          } 
        }
      }
    
      document.onkeydown = (e) => {
        if(e.keyCode == 87) {
          player.walk(0);
        } else if(e.keyCode == 68) {
          player.walk(1);
        } else if(e.keyCode == 83) {
          player.walk(2);
        } else if(e.keyCode == 65) {
          player.walk(3);
        }
        if(e.keyCode == 32 && e.shiftKey) {
          player.fly(1);
        } else if(e.keyCode == 32) {
          player.fly(0);
        }
        destroyOrPutBlock(mouseButtons);
      }
    
      document.onkeyup = (e) => {
        if(e.keyCode == 87 || e.keyCode == 68 || e.keyCode == 83 || e.keyCode == 65) {
          player.stop();
        }
      }
    
      document.onmousemove = (e) => {
        let deltaX = e.momentX || e.webkitMovementX || e.mozMovementX || e.movementX;
        let deltaY = e.momentY || e.webkitMovementY || e.mozMovementY || e.movementY;
    
        player.rotate(deltaX / 0.8, deltaY / 0.8);
        destroyOrPutBlock(mouseButtons);
      }
    
      document.onmousedown = (e) => {
        mouseButtons = e.buttons;
        destroyOrPutBlock(mouseButtons);
      }
    
      document.oncontextmenu = () => false;
    
      document.onmouseup = () => {
        mouseButtons = 0;
      }
    }
    

    処理がまとまったおかげでかなりスッキリしましたね!特記事項はないので説明を割愛します!
    重力加速度が入っている影響でプレイヤーが下に落ちてしまうので、デバッグする時はプロパティを上書きします!

    あとがき

    かなり大規模に書き換えを行なったので、バグが非常に出てきて大変でした。しかし、重要なコードを使い回せるようにできたので、ここから一気に進められると思います!
    当たり判定まで頑張りたかったのですが、これだけでいっぱいだったので今週はここまで!

    第6週はこちらから

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

    第8回はこちらから

    WebでMinec◯aft的なものを作る! ~第8回目~