Three.jsで簡単なゲームの原型を作った

# Three.jsとは
WebGLを簡単に扱うことのできるライブラリです。さらに噛み砕いて言えばブラウザで3DCGが簡単に扱える代物になっております。今回はこれのチュートリアル的なあれで簡単なパズルゲームを作ってみようというお話です。

ちなみに行き当たりばったりで作った上にリファクタもしてないのでかなりのクソコードです。許してください。

完成品がこちら
http://mosathree.mitsu-hide.com/
パズルゲームです。色のついた面がすべて表に出ればよいです。完成形がわからないって人はhint連打してくれれば完成形にたどり着きます。

# 中身
ウェブ系制作といったらこれ。まずは簡単にindex.htmlを作成します。


<details>
<summary>index.html</summary>
<div>

```.html
<!doctype html>
<html lang="ja">
    <head>
        <title>WebGL Demo</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <script src="liblaries/three.js"></script>
        <script src="liblaries/OrbitControls.js"></script>
        <script src="liblaries/dat.gui.js"></script>

        <script src="module.js"></script>
        <script src="initialize.js"></script>
        <script src="func.js"></script>
        <script src="main.js"></script>

        <style>
            .ac {  /* prevent dat-gui from being selected */
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
            }
            .no-pointer-events {
            pointer-events: none;
            }
            .control-disabled {
            color: #888;
            text-decoration: line-through;
            }
            body{
                margin: 0
            }
        </style>
    </head>
    <body>
        <canvas id="main_canvas" style="z-index: -10;" margin=0></canvas>
    </body>

</html>
```

</div>
</details>

必要なライブラリのインポート及びwebglを描画するためのcanvasを用意しています。styleはおまじないみたいなものです。.jsのファイルを無駄に分けてますが、正直こんなに分ける必要なかったと思います(行き当たりばったり開発)

次にmain.jsです。
<details>
<summary>main.js</summary>
<div>

```.js
window.addEventListener('DOMContentLoaded', init);

function init(){
    init_main_scene(pzm.par,pzm.con)
    load_ans();
    pzm.con.gui = new dat.GUI();
    pzm.con.gui.add(pzm.con.gui_func,"hint");



  window.addEventListener('resize', onResize);
  onResize();

  function onResize() {
    const width1 = window.innerWidth;
    const height1 = window.innerHeight;
    pzm.par.renderer.setPixelRatio(window.devicePixelRatio);
    pzm.par.renderer.setSize(width1, height1);
    pzm.par.camera.aspect = width1 / height1;
    pzm.par.camera.updateProjectionMatrix();
  }

const canvas = document.querySelector('#main_canvas');
const mouse = new THREE.Vector2();
canvas.addEventListener('mousemove', handleMouseMove);
function handleMouseMove(event) {
    const element = event.currentTarget;
    const x = event.clientX - element.offsetLeft;
    const y = event.clientY - element.offsetTop;
    const w = element.offsetWidth;
    const h = element.offsetHeight;
    mouse.x = ( x / w ) * 2 - 1;
    mouse.y = -( y / h ) * 2 + 1;
  }
  const raycaster = new THREE.Raycaster();
  canvas.addEventListener("click",function(){
    raycaster.setFromCamera(mouse, pzm.par.camera);
    const intersects = raycaster.intersectObjects(pzm.par.scene.children);
    if(intersects.length > 0){
        let cube_num = Number(intersects[0].object.name)
        let adr = pzm.par.cube_address[cube_num]
        if (pzm.par.occupied[adr]){
            let space = to_where(adr);
            if (space == -1){
                return
            }
            space = Number(space)
            pzm.par.cube_address[cube_num] = space
            pzm.par.init_state[space] = cube_num
            pzm.par.init_state[adr] = 7
            intersects[0].object.position.set(pzm.con.cube_pos[space][0],pzm.con.cube_pos[space][1],pzm.con.cube_pos[space][2]);
        }
      }
      if(is_finished()){
        pzm.par.not_clear = false
        pzm.par.renderer.render(pzm.par.scene, pzm.par.camera);
        pzm.par.control_cam.update();
        pzm.par.control_light.update();
        (function(){
            alert("clear")
        })();

        }
  })

  canvas.addEventListener("touchstart",function(event){
    pzm.par.tc_start = new Date().getTime();
  })


  canvas.addEventListener("touchend",function(event){
    pzm.par.tc_end = new Date().getTime();
    if (pzm.par.tc_end-pzm.par.tc_start <= 300){
    var to = event.changedTouches[0];
    let ele = document.getElementById("main_canvas")
    let touch = new THREE.Vector2();
    const x = to.clientX - ele.offsetLeft;
    const y = to.clientY - ele.offsetTop;
    const w = ele.offsetWidth;
    const h = ele.offsetHeight;
    touch.x = ( x / w ) * 2 - 1;
    touch.y = -( y / h ) * 2 + 1;
    raycaster.setFromCamera(touch, pzm.par.camera);
    const intersects = raycaster.intersectObjects(pzm.par.scene.children);
    if(intersects.length > 0){
      let cube_num = Number(intersects[0].object.name)
      let adr = pzm.par.cube_address[cube_num]
      if (pzm.par.occupied[adr]){
          let space = to_where(adr);
          if (space == -1){
              return
          }
          space = Number(space)
          pzm.par.cube_address[cube_num] = space
          pzm.par.init_state[space] = cube_num
          pzm.par.init_state[adr] = 7
          intersects[0].object.position.set(pzm.con.cube_pos[space][0],pzm.con.cube_pos[space][1],pzm.con.cube_pos[space][2]);
      }
    }
    if(is_finished()){
      pzm.par.not_clear = false
      pzm.par.renderer.render(pzm.par.scene, pzm.par.camera);
      pzm.par.control_cam.update();
      pzm.par.control_light.update();
      (function(){
          alert("clear")
      })();

        }
      }
  })

  tick();

  function tick() {
    pzm.par.renderer.render(pzm.par.scene, pzm.par.camera);
    pzm.par.control_cam.update();
    pzm.par.control_light.update();
    requestAnimationFrame(tick);
  }
}
```
</div>
<details>

関数にまとめろよ…って感じのところが多いですが、勘弁してください。そのうちきれいにします(気が向いたら)
ざっくり分けて、初期化、クリック(タッチ)の処理、WebGlの描画ループといったふうになっております。
初期化は後述します。クリックの処理に関しては、予め立方体に名前を与えておき、Raycastでクリックされた対象を取得します。その後その対象の位置、スペースの位置から移動方向を見つけ、移動及び状態の更新を行っています。

その他のファイルはこんなかんじ

<details>
<summary>func.js</summary>
<div>

```.js
function to_where(add){
    let path = pzm.con.cube_path[add]
    for (let i = 0; i < 3; ++i){
        if (!pzm.par.occupied[path[i]]){
            pzm.par.occupied[path[i]] = true
            pzm.par.occupied[add] = false
            return path[i];
        }
    }
    return -1;
}

function load_ans(){
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState == 4) {
        if (xmlhttp.status == 200) { 
        } else {
          alert("status = " + xmlhttp.status);
        } 
      }
    }
    xmlhttp.open("GET", "./all.txt");
    xmlhttp.send();
    xmlhttp.onload=(function() {
      data = xmlhttp.responseText.split('\n');
      for (let i = 0; i < data.length; ++ i){
          line = data[i].split(' ');
          pzm.con.ans[String(line[0])] = line[1];
      }
      init_cube(pzm.par,pzm.con)
    }
    )

}

function is_finished(){
    for (let i =0; i < 7; ++i){
        if (pzm.par.cube_address[i] !== i){
            return false
        }
    }
    return true
}

function parity(arr){
    let p = 0;
    let p2 = 0;
    for(let i =0; i < 8; ++i){
        for(let j =0; j <i; ++j){
            if (arr[i] < arr[j]){
                p+=1;
            }
        }
    }

    for (let i = 0; i< 8; ++i){
        if (arr[i] == 7){
            p2 = i
        }
    }
    if (p2==7){
        pp = 0
    }else if(p2 == 0){
        pp = 1
    }else if(p2 == 4 || p2 == 5 || p2 == 6){
        pp = 1
    }else{
        pp = 0
    }
    if (p%2 == pp){
        return true
    }else{
        return false
    }
}

function can_be_solved(state){
    s = ""
    for (let i = 0; i <8; ++i){
        s += String(state[i])
    }
    if (s in pzm.con.ans){
        return true
    }else{
        return false
    }
}
```
</div>
</details>

<details>
<summary>initialize.js</summary>
<div>

```.js
function init_main_scene(main,con){
    main.renderer = new THREE.WebGLRenderer({
        canvas: document.querySelector('#main_canvas'),
        preserveDrawingBuffer: true
      });
      main.renderer.setPixelRatio(window.devicePixelRatio);
      main.renderer.setSize(con.init_width, con.init_height);
      main.scene = new THREE.Scene();
      main.camera = new THREE.PerspectiveCamera(45, con.init_width / con.init_height, 1, 50000);
      main.light = lighting(main.scene);
      main.control_cam = new THREE.OrbitControls(main.camera, main.renderer.domElement);
      main.control_light = new THREE.OrbitControls(main.light, main.renderer.domElement);
      main.control_light.enablePan = false;
      main.control_light.enableZoom = false;
      main.control_cam.enableKeys = false;
      main.control_cam.enablePan = false;
      main.control_light.enableKeys = false;
      main.camera.position.set(0, 0, 15);
      main.renderer.setClearAlpha(0);
}

function lighting(scene) {
    var light = new THREE.DirectionalLight(0xFFFFFF);
    light.intensity = 2;
    light.position.set(0, 0, 100);
    scene.add(light);
    return light;
  }

function init_cube(par,con){
    let break_loop_too_much = 0;
    while(true){
        console.log(break_loop_too_much)
    randomize(par.init_state)
    // if(parity(par.init_state)){
    //     break
    // }
    if (can_be_solved(par.init_state)){
        break;
    }
    break_loop_too_much+=1;
    if(break_loop_too_much > 20){
        break
    }
    }
    for (let i = 0; i < 7; ++i){
        let geo = new THREE.BoxGeometry(2,2,2);
        let cube = new THREE.Mesh(geo,con.cube_mat);
        pzm.con.cubes.push(cube);
    }
    for (let i = 0; i < 7; ++i){
        for (let j = 0; j < 8; ++j){
            if(pzm.par.init_state[j] == i){
                temp = j
            }
        }
        pzm.con.cubes[i].position.set(con.cube_pos[temp][0],con.cube_pos[temp][1],con.cube_pos[temp][2])
        pzm.con.cubes[i].name=i.toString()
        // for (let j = 0; j < 8; ++ j){
        //     if(par.init_state[j] == i){
        //         temp = j
        //     }
        // }
        par.cube_address[i] = temp
        par.occupied[temp] = true
        pzm.con.cubes[i].rotation.set(con.cube_rot[i][0],con.cube_rot[i][1],con.cube_rot[i][2])
        par.scene.add(pzm.con.cubes[i]);

    }

}

function randomize(array){
    for(var i = array.length - 1; i > 0; i--){
        var r = Math.floor(Math.random() * (i + 1));
        var tmp = array[i];
        array[i] = array[r];
        array[r] = tmp;
    }
}
```
</div>
</details>


initialize.jsでは主にシーンの設定(シーン、ライト、カメラの設定)及び、立方体の初期配置を決定しています。
初期配置は解けないものになると困るので、予め全手探索したデータを作っておきその中からランダムに選んでいます。全手探索の方法は初期状態から次状態を生成し、未探索状態が生成されなくなるまで繰り返すことによるものです。
実際のところは(証明はしていませんが)配置のパリティによって解けるか解けないか判定ができそうなのですが、ヒントを実装するにあたって毎回クライアント側に解の探索をさせるのは良くないと考えたので、解をすべて作るのついでに全手探索を行っています。

<details>
<summary>module.js</summary>
<div>

```.js
var pzm={
    con:{
        init_width : 100, //old name width
        init_height : 100,// old name height
        cube_mat : [
            new THREE.MeshStandardMaterial({color:0x00FF00}),
            new THREE.MeshStandardMaterial({color:0xFFFFFF}),
            new THREE.MeshStandardMaterial({color:0x00FF00}),
            new THREE.MeshStandardMaterial({color:0xFFFFFF}),
            new THREE.MeshStandardMaterial({color:0x00FF00}),
            new THREE.MeshStandardMaterial({color:0xFFFFFF})
        ],
        cube_pos : [
            [1,1,1], // 0　手前左上
            [-1,1,1], //1　手前右上
            [1,-1,1], //2　手前左下
            [1,1,-1], //3　奥左上
            [-1,-1,1], //4　手前右下
            [1,-1,-1], //5　奥左下
            [-1,1,-1], //6　奥右上
            [-1,-1,-1] //7　奥右下
        ],
        cube_path:[
            [1,2,3], //0
            [0,4,6], //1
            [0,4,5], //2
            [0,5,6],    //3
            [1,2,7],      //4
            [2,3,7],      //5
            [1,3,7],      //6
            [4,5,6]       //7
        ],
        cube_rot:[
            [0,0,0], //0
            [0,0,Math.PI/2], //1
            [Math.PI/2,0,0], //2
            [Math.PI/2,Math.PI/2,-Math.PI/2],    //3
            [0,0,Math.PI],      //4
            [0,Math.PI,Math.PI],      //5
            [0,-Math.PI/2,Math.PI/2],      //6
        ],
        gui :0,
        gui_func:{
            hint:function(){
                let now = "";
                for (let i = 0; i < 8; ++i){
                    if(pzm.par.init_state[i] == 7){
                        now_space = i;
                    }
                }

                for (let i = 0; i < 8; ++i){
                    now += String(pzm.par.init_state[i])
                }

                let next = pzm.con.ans[now];
                if (next == ""){
                    console.log("end")
                    return
                }
                console.log(pzm.par.init_state)
                console.log(now)
                console.log(next)
                let next_space = 0;
                next = next.split("")
                for (let i = 0; i < 8; ++i){
                    if (next[i] == 7){
                        temp = i
                    }
                }
                next_space = temp
                console.log(next_space)
            

                let c_n = pzm.par.init_state[next_space]

            
                pzm.par.occupied[now_space] = true
                pzm.par.occupied[next_space] = false
                pzm.par.cube_address[c_n] = now_space
                pzm.par.init_state[now_space] = c_n
                pzm.par.init_state[next_space] = 7
                pzm.con.cubes[c_n].position.set(pzm.con.cube_pos[now_space][0],pzm.con.cube_pos[now_space][1],pzm.con.cube_pos[now_space][2]);
        }
        },
        ans :{},
        cubes : []
    },
    par:{
        not_clear:true,
        renderer:0,
        scene:0,
        camera:0,
        light:0,
        control_cam:0,
        control_light:0,
        cube_address:[-1,-1,-1,-1,-1,-1,-1],
        occupied:[false,false,false,false,false,false,false,false],
        init_state:[0,1,2,3,4,5,6,7],
        tc_start : 0,
        tc_end : 0

    }
}
```
</div>
</details>

module.jsは変数置き場です。企業開発経験とかがないのでここらへんどうしたらいいかわからなくて、とりあえず名前ついてればいいか理論でやってます。


# 反省とか展望とか
まずは関数をまとめて読みやすくすること及び不要なコードの消去とかを行っていきます。
後はタイムがないと競技性がないのでタイムの実装などいいかなと思っています。

また、3*3*3のスケールでの実装も考えていますが、こちらは到底全手探索が不可能なオーダーになってきますので、そこら編をどう実装するか考えて行きたいところです。