第12話 ゴール（最終回）

**算数は必要だよ。実生活でも...**

これで、太郎と次郎の冒険は一旦終わります。
***
先日、うちの小学生が算数のテストで目も当てられないような点数を持って帰ってきました。スマホでゲームしたり、Youtubeばかり見てたり、（スマホを与えるなってｗ）少しは勉学に励んでほしいものです。そこで、

俺「ほら、算数できれば、こんなゲームみたいなの作れるよ？少しは勉強しなさいよ」

小学生「え、なにこれ？わかった！すぐクリアするよ！」

いや、違...逆効果でした...orz
***
最後は、移動処理の実装です。基本的には入力デバイスからの情報で、進むのか、方向変えるのかを判断して、カメラを操作すればいいわけです。
とりあえず、前進を考えてみます。

```js:poge.js
var moveForward = function() {
  deltaX = moveTransition[Player.direction].x;
  deltaY = moveTransition[Player.direction].y;
  var map = getMap(Player.x + deltaX, Player.y + deltaY);
  // 扉ならもう一歩進む
  var times = (map == "□") ? 2 : 1;
  if (map == "■") {
    return;
  }
  // 位置情報の更新
  Player.x += deltaX * times;
  Player.y += deltaY * times;
  // カメラの位置更新
  camera.position.x += deltaX * times * blockSize;
  camera.position.z += deltaY * times * blockSize;
  // 光源の位置更新
  light.position.copy( camera.position );
  light.position.x += deltaX;
  light.position.z += deltaY;
};
```
これを、update関数でコールします。

```js:poge.js
function update() {
  if (upKey.isDown || game.touchControl.cursors.up) {
    moveForward();
    console.log('UP');
  }
  if (leftKey.isDown || game.touchControl.cursors.left) {
```
このままで良いといえばいいかもしれませんが、なんかこう、ググッと歩いてる雰囲気がほしいじゃないですか！そこで、Tweenという機能でアニメーションさせてみます。
Tweenでは、初期値と最終的な値と時間をセットすると、初期値から最終値までセットした時間をかけて変化させてくれます。どんな変化をするのかというと、関数を自作もできますがプリセットがあるので、それを使ってみます。
[ここ](http://phaser.io/docs/2.4.6/Phaser.Easing.html)にプリセットがあります。どんな動きなのかは、[Tween.jsのグラフ](http://sole.github.io/tween.js/examples/03_graphs.html)が参考になるでしょうか...

先ほどのmoveForward関数のカメラの位置更新と光源の位置更新の部分が以下のようになります。

```js:poge.js
  // カメラと光源の位置更新
  var tween = game.add.tween(camera.position);
  tween.to({
    x : camera.position.x + deltaX * times * blockSize,
    z : camera.position.z + deltaY * times * blockSize
  }, 1000, Phaser.Easing.Quartic.In);
  tween.onUpdateCallback(function(){
    light.position.copy( camera.position );
    light.position.x += deltaX;
    light.position.z += deltaY;
  }, this);
  tween.start();
```
1000msかけて座標が変化します。光源の位置はコールバック関数で変化させています。

同様に向きの変更は、y軸で回転させればいいわけです。
***
≪後記≫
まぁ、息子に算数を勉強してほしい一心で、javascriptもThreeとかPhaserも使い始めて1週間ほどなので、稚拙な感じでアレですが、最後にデモとソースコードを貼っておきます。
***
[デモ](http://tkymgr.o0o0.jp/demo/)
![final.png](https://qiita-image-store.s3.amazonaws.com/0/105644/f8614f1e-01f1-2a6b-ae60-23c7c737f37c.png)

```js:poge-final.js
//*--- Const
// 方角
var direction = {
  north : 0,
  west  : 1,
  south : 2,
  east  : 3
};
// 迷路表示サイズ
var mazeScreen = {
  width : 16 * 30,
  height : 9 * 30
};
// カメラ用パラメータ
var cameraParams = {
  fov : 95,
  aspect : 1.8,
  near : 1,
  far : 50
};
// 光源用パラメータ
var lightParams = {
  ambient : 0x404040, // 環境光
  point   : 0xffffff, // 点光源
};
// 移動変移係数
var moveTransition = [
  { x :  0, y : -1 },
  { x : -1, y :  0 },
  { x :  0, y :  1 },
  { x :  1, y :  0 }
];
// Cubeの一辺長
var blockSize = 10;
var textureFile = {
  wall    : "img/wall.png",
  door    : "img/door.png",
  ceiling : "img/ceiling.png",
  floor   : "img/floor.png",
};
var joypadImage = {
  compass  : { text : 'compass',       file : "img/compass_rose.png" },
  touchSeg : { text : 'touch_segment', file : "img/touch_segment.png" },
  touch    : { text : "touch",         file : "img/touch.png" },
}
// 方角に対する回転角度
var rotationAngle = [
  0,
  Math.PI/2,
  Math.PI,
  -Math.PI/2
];
// 地図
var mapData = {
  ceiling : [
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■■■■■■■■■■"
  ],
  wall : [
    "■■■■■■■■■■",
    "■　　　■　　　　■",
    "■　　　■□■■　■",
    "■■□■■　　■　■",
    "■■　■■　　■　■",
    "■■　■■■□■　■",
    "■　　　　　　□　■",
    "■■■■■■■■■■"
  ],
  floor : [
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■■■■■■■■■■",
    "■　■■■■■■■■",
    "■■■■■■■■■■"
  ]
};

// Variable
// プレイヤー情報
var Player = {
  x : 6,
  y : 1,
  direction : direction.west,
};

// Three.js
var camera;
var scene;
var renderer;
var mesh;
var light;
// Phaser.js
var baseTexture;
var texture;
var textureFlame;
var upKey;
var leftKey;
var rightKey;
// ワーク
var deltaX;
var deltaY;
var isTween;

var game = new Phaser.Game(
  mazeScreen.width, mazeScreen.height, Phaser.AUTO, 'game',
  { preload: preload, create: create, update: update, render: render }
);

var calcPos = function(x){
  return x * blockSize;
};

var getMap = function(x, y){
  return mapData.wall[y].charAt(x);
};

function preload() {
  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(
    cameraParams.fov, cameraParams.aspect, cameraParams.near, cameraParams.far
  );
  camera.position.set( calcPos(Player.x), 0, calcPos(Player.y) );
  camera.rotation.y = rotationAngle[Player.direction];
  light = new THREE.PointLight( lightParams.point, 5, 10, 1 );
  light.position.copy( camera.position );
  light.position.z -= 1;
  scene.add( light );
  var ambient = new THREE.AmbientLight(lightParams.ambient);
  scene.add(ambient);
  var loader = new THREE.TextureLoader();
  var texture = {
    door   :loader.load(textureFile.door),
    wall   :loader.load(textureFile.wall),
    ceiling:loader.load(textureFile.ceiling),
    floor  :loader.load(textureFile.floor)
  };
  var material = {
    door   : new THREE.MeshPhongMaterial({map:texture.door, bumpMap:texture.door, bumpScale: 0.08}),
    wall   : new THREE.MeshPhongMaterial({map:texture.wall, bumpMap:texture.wall, bumpScale: 0.05}),
    ceiling: new THREE.MeshPhongMaterial({map:texture.ceiling, bumpMap:texture.ceiling,bumpScale: 0.09}),
    floor  : new THREE.MeshPhongMaterial({map:texture.floor, bumpMap:texture.floor, bumpScale: 0.09})
  };
  var geometry = new THREE.CubeGeometry(blockSize, blockSize, blockSize);
  for (y = 0; y < mapData.wall.length; y++) {
    for (x = 0; x < mapData.wall[y].length; x++) {
      if (mapData.wall[y].charAt(x) == "■") {
        var cube = new THREE.Mesh(geometry, material.wall);
        cube.position.set(blockSize * x, 0, blockSize * y);
        scene.add(cube);
      }
      if (mapData.wall[y].charAt(x) == "□") {
        var cube = new THREE.Mesh(geometry, material.door);
        cube.position.set(blockSize * x, 0, blockSize * y);
        scene.add(cube);
      }
      if (mapData.ceiling[y].charAt(x) == "■") {
        var cube = new THREE.Mesh(geometry, material.ceiling);
        cube.position.set(blockSize * x, blockSize, blockSize * y);
        scene.add(cube);
      }
      if (mapData.floor[y].charAt(x) == "■") {
      	var cube = new THREE.Mesh(geometry, material.floor);
      	cube.position.set(blockSize * x, -blockSize, blockSize * y);
      	scene.add(cube);
      }
    }
  }
  renderer = new THREE.WebGLRenderer({antialias: true});
  renderer.setSize( mazeScreen.width, mazeScreen.height );
  var canvas = renderer.domElement;
  baseTexture = new PIXI.BaseTexture(canvas);
  texture = new PIXI.Texture(baseTexture);
  textureFrame = new Phaser.Frame(
    0, 0, 0, mazeScreen.width, mazeScreen.height,
  'debug', game.rnd.uuid());
  var sprite = game.add.sprite(0, 0, texture, textureFrame);
  game.load.image(joypadImage.compass.text, 'img/compass_rose.png');
  game.load.image('touch_segment', 'img/touch_segment.png');
  game.load.image('touch', 'img/touch.png');
}

function create() {
  game.touchControl = game.plugins.add(Phaser.Plugin.TouchControl);
  game.touchControl.inputEnable();
  upKey = game.input.keyboard.addKey(Phaser.Keyboard.UP);
  leftKey = game.input.keyboard.addKey(Phaser.Keyboard.LEFT);
  rightKey = game.input.keyboard.addKey(Phaser.Keyboard.RIGHT);
  game.input.keyboard.addKeyCapture([
    Phaser.Keyboard.UP, Phaser.Keyboard.LEFT, Phaser.Keyboard.RIGHT
  ]);
  isTween = false;
}

function update() {
  if (upKey.isDown || game.touchControl.cursors.up) {
    moveForward();
    //console.log('UP');
  }
  if (leftKey.isDown || game.touchControl.cursors.left) {
    _turn(Math.PI / 2, 1);
    //console.log('LEFT');
  }
  if (rightKey.isDown || game.touchControl.cursors.right) {
    _turn(-Math.PI / 2, -1);
    //console.log('RIGHT');
  }
}

function render() {
  renderer.render(scene, camera);
  game.renderer.updateTexture(baseTexture);
}

var moveForward = function() {
  if (isTween) {
    return;
  }
  isTween = true;
  deltaX = moveTransition[Player.direction].x;
  deltaY = moveTransition[Player.direction].y;
  var map = getMap(Player.x + deltaX, Player.y + deltaY);
  var times = (map == "□") ? 2 : 1;
  if (map == "■") {
    isTween = false;
    oops();
    return;
  }
  Player.x += deltaX * times;
  Player.y += deltaY * times;
  var tween = game.add.tween(camera.position);
  tween.to({
    x : camera.position.x + deltaX * times * blockSize,
    z : camera.position.z + deltaY * times * blockSize
  }, 1000, Phaser.Easing.Quartic.In);
  tween.onUpdateCallback(function(){
    light.position.copy( camera.position );
    light.position.x += deltaX;
    light.position.z += deltaY;
  }, this);
  tween.onComplete.add(function(){
    isTween = false;
  });
  tween.start();
};

var _turn = function(rotation, direction) {
  if (isTween) {
    return;
  }
  isTween = true;
  var tween = game.add.tween(camera.rotation);
  tween.to( { y: camera.rotation.y + rotation }, 1000, Phaser.Easing.Cubic.InOut );
  tween.onUpdateCallback(function(){
    light.rotation.y = camera.rotation.y;
  }, this);
  tween.onComplete.add(function() {
    Player.direction += direction;
    if (Player.direction > 3) Player.direction = 0;
    if (Player.direction < 0) Player.direction = 3;
    isTween = false;
  });
  tween.start();
};

var oops = function(){
  if (isTween) {
    return;
  }
  isTween = true;
  deltaX = moveTransition[Player.direction].x;
  deltaY = moveTransition[Player.direction].y;
  var tween = game.add.tween(camera.position);
  tween.to({
    x : camera.position.x + deltaX,
    z : camera.position.z + deltaY
  }, 1000, Phaser.Easing.Cubic.InOut, false, 0, 0, true);
  tween.onUpdateCallback(function(){
    light.position.copy( camera.position );
    light.position.x += deltaX;
    light.position.z += deltaY;
  }, this);
  tween.onComplete.add(function(){
    isTween = false;
  });
  tween.start();
};

var turnLeft = function(){
  _turn(Math.PI / 2, 1);
};

var turnRight = function(){
  _turn(-Math.PI / 2, -1);
};
```