phina.jsでモンストっぽいゲームを作る【４】ー敵の配置と当たり判定ー

# はじめに
モンストもどきを作るという試みの第４回となる今回は、敵の追加と当たり判定を作っていきます。

## アセット定義追加
まずは、敵のアセット情報を追加します。


```js
    'dragon': 'https://rawgit.com/alkn203/piko_strike/master/assets/enemy/pipo-enemy021.png',
```

## 敵を配置する
### Enemyクラス
敵を配置する前に、後に敵を使いまわし出来るように**Enemy**クラスを定義します。**Sprite**クラスを継承したクラスです。

```js
/*
 * 敵クラス
 */
phina.define("Enemy", {
  // 継承
  superClass: 'Sprite',
  // コンストラクタ
  init: function(name) {
    // 親クラス初期化
    this.superInit(name);
  },
});
```

### 敵を追加する
プレイヤーを追加する時と基本的には同じで、シーンに追加します。

```js
    // 敵キャラ
    var dragon = Enemy('dragon').addChildTo(this);
    dragon.setPosition(this.gridX.center(), this.gridY.center(-4));
```

## プレイヤーと敵の当たり判定
### Colliderアクセサリを使った当たり判定
当たり判定は、**phina.js**に元々用意されている**hitTestElement**で判定をしても良いのですが、今回は自作した**Collider**アクセサリを使ってみます。**Collider**アクセサリについては、[[phina.js]Colliderアクセサリを作ってみた話](https://qiita.com/alkn203/items/8c24d433e9eb8ea0f4bc)に書いていますので、参考にして下さい。

### プレイヤーのCollider
プレイヤーの**Collider**は単純な矩形にするので、以下のコードを**Player**クラスのコンストラクタに追加します。

```js
  // コライダー
  this.collider.show();
```

**show**メソッドを呼び出しておくと、以下のようにプレイヤーの周りに**Collider**が表示されます。

![monst-tut-05-1.gif](https://qiita-image-store.s3.amazonaws.com/0/67114/68dd729b-6df0-a8a4-a943-5c355d15fc9b.gif)


### 敵のCollider
プレイヤーと同じく矩形にしても良いのですが、上下左右の当たり判定を明確に区別するために、以下のように４方向の**Collider**をそれぞれ設定してみました。

![monst-tut-05-2.gif](https://qiita-image-store.s3.amazonaws.com/0/67114/c396f3db-b27b-4eb4-fd78-11fb388cd6ff.gif)

敵クラスのコンストラクタに以下のコードを追加します。

```js
var s1 = this.width * 0.8;
var s2 = this.width / 10;
var half = this.width / 2;
// コライダー
var top = Collider().attachTo(this).setSize(s1, s2).offset(0, -half).show();
top.id = 'top';
var bottom = Collider().attachTo(this).setSize(s1, s2).offset(0, half).show();
bottom.id = 'bottom';
var left = Collider().attachTo(this).setSize(s2, s1).offset(-half, 0).show();
left.id = 'left';
var right = Collider().attachTo(this).setSize(s2, s1).offset(half, 0).show();
right.id = 'right';
```

* **Collider**の大きさは、オブジェクトのサイズ変更に対応できるように計算で求めています。
* **Collider**の位置指定は相対座標で行います。
* 当たり判定でどの位置の**Collider**に当たったかを知るために、**id**というプロパティを作っています。

## プレイヤーと敵の当たり判定
プレイヤーと敵の当たり判定は、**reflectEnemy**という処理専用の関数を定義してその中で行います。
**reflectEnemy**は、**MainScene**の**update**関数内から呼び出します。

```js
// 敵との反射処理
reflectEnemy: function() {
  var velocity = this.player.physical.velocity;
  var player = this.player;
  var enemy = this.dragon;
  // 敵との当たり判定
  enemy.accessories.each(function(accessory) {
    if (accessory.hitTest(player.collider)) {
      if (velocity.x > 0 && accessory.id === 'left') velocity.x *= -1;
      if (velocity.x < 0 && accessory.id === 'right') velocity.x *= -1;
      if (velocity.y > 0 && accessory.id === 'top') velocity.y *= -1;
      if (velocity.y < 0 && accessory.id === 'bottom') velocity.y *= -1;
    }  
  });
},
```

* オブジェクトにアタッチしたアクセサリは、そのオブジェクトの**accessories**という配列に追加されていますので、その配列をループして判定を行っています。
* 同時に**velocity**の符号をチェックすることで、予期しない反射の動きを防ぐことができます。

## 動作確認
[[runstantプロジェクトへのリンク]](http://runstant.com/alkn203/projects/3d29dc7c)

##  全体コード
```js
// グローバルに展開
phina.globalize();
// アセット
var ASSETS = {
  // 画像
  image: {
    'bg': 'https://rawgit.com/alkn203/piko_strike/master/assets/bg.png',
    'tomapiko': 'https://rawgit.com/phinajs/phina.js/develop/assets/images/tomapiko.png',
    'arrow': 'https://rawgit.com/alkn203/piko_strike/master/assets/arrow.png',
    'dragon': 'https://rawgit.com/alkn203/piko_strike/master/assets/enemy/pipo-enemy021.png',
  },
};
// 定数
var SCREEN_WIDTH = 640;
var SCREEN_HEIGHT = 960;
var PLAYER_SPEED = 50;
var FRICTION = 0.98;
/*
 * メインシーン
 */
phina.define("MainScene", {
  // 継承
  superClass: 'DisplayScene',
  // コンストラクタ
  init: function() {
    // 親クラス初期化
    this.superInit();
    // 背景
    Sprite('bg').addChildTo(this)
                .setPosition(this.gridX.center(), this.gridY.center());
    // 矢印
    var arrow = Sprite('arrow').addChildTo(this).hide();
    arrow.alpha = 0.75;
    // プレイヤー
    this.player = Player().addChildTo(this);
    this.player.setPosition(this.gridX.center(), this.gridY.span(13));
    // 敵キャラ
    var dragon = Enemy('dragon').addChildTo(this);
    dragon.setPosition(this.gridX.center(), this.gridY.center(-4));
    // 矢印
    arrow.setPosition(this.player.x, this.player.y);
    // シーン全体から参照できるようにする
    this.dragon = dragon;
    this.arrow = arrow;
  },
  // 毎フレーム更新処理
  update: function() {
    var velocity = this.player.physical.velocity;
    // 画面端反射処理
    this.reflectScreen();
    // 敵との反射処理
    this.reflectEnemy();
    // 一定速度を下回ったら強引に停止させる
    if (velocity.length() > 0 && velocity.length() < 2) {
      velocity.set(0, 0);
      // 画面タッチを有効にする
      this.setInteractive(true);
    }    
  },
  // 画面端との反射処理
  reflectScreen: function() {
    var velocity = this.player.physical.velocity;
    var player = this.player;
    // 画面端反射処理
    if (velocity.x < 0 && player.left < 0) velocity.x *= -1;
    if (velocity.x > 0 && player.right > SCREEN_WIDTH) velocity.x *= -1;
    if (velocity.y < 0 && player.top < 0) velocity.y *= -1;
    if (velocity.y > 0 && player.bottom > SCREEN_HEIGHT) velocity.y *= -1;
  },
  // 敵との反射処理
  reflectEnemy: function() {
    var velocity = this.player.physical.velocity;
    var player = this.player;
    var enemy = this.dragon;
    // 敵との当たり判定
    enemy.accessories.each(function(accessory) {
      if (accessory.hitTest(player.collider)) {
        if (velocity.x > 0 && accessory.id === 'left') velocity.x *= -1;
        if (velocity.x < 0 && accessory.id === 'right') velocity.x *= -1;
        if (velocity.y > 0 && accessory.id === 'top') velocity.y *= -1;
        if (velocity.y < 0 && accessory.id === 'bottom') velocity.y *= -1;
      }  
    });
  },
  // タッチ開始時処理
  onpointstart: function(e) {
    // タッチ位置を記録
    this.startPos = Vector2(e.pointer.x, e.pointer.y);
    // 矢印表示
    this.arrow.setPosition(this.player.x, this.player.y).show();
    // 縦を0に縮小
    this.arrow.scaleY = 0;
  },
  // タッチ移動時処理
  onpointmove: function(e) {
    // 矢印の方向を求める
    var pos = Vector2(e.pointer.x, e.pointer.y);
    this.arrow.rotation = this.getDegree(this.startPos, pos) + 90;
    // 距離に応じて矢印を拡大縮小
    var distance2 = Vector2.distanceSquared(this.startPos, pos);
    this.arrow.scaleY = distance2 / 10000;
  },
  // タッチ終了時処理
  onpointend: function(e) {
    // 画面タッチを無効にする
    this.setInteractive(false);
    // 矢印非表示
    this.arrow.hide();
    // 矢印の角度から方向を決める
    var deg = this.arrow.rotation - 90;
    // 角度からベクトルを求めてプレイヤーに設定
    this.player.physical.velocity = Vector2().fromDegree(deg, PLAYER_SPEED);
    // 摩擦をかけて徐々に減速させる
    this.player.physical.friction = FRICTION;
  },
  // 2点間の角度を求める
  getDegree: function(from, to) {
    return Math.radToDeg(Math.atan2(from.y - to.y, from.x - to.x));
  },
});
/*
 * プレイヤークラス
 */
phina.define("Player", {
  // 継承
  superClass: 'Sprite',
  // コンストラクタ
  init: function() {
    // 親クラス初期化
    this.superInit('tomapiko');
    // コライダー
    this.collider.show();
  },
});
/*
 * 敵クラス
 */
phina.define("Enemy", {
  // 継承
  superClass: 'Sprite',
  // コンストラクタ
  init: function(name) {
    // 親クラス初期化
    this.superInit(name);

    var s1 = this.width * 0.8;
    var s2 = this.width / 10;
    var half = this.width / 2;
    // コライダー
    var top = Collider().attachTo(this).setSize(s1, s2).offset(0, -half).show();
    top.id = 'top';
    var bottom = Collider().attachTo(this).setSize(s1, s2).offset(0, half).show();
    bottom.id = 'bottom';
    var left = Collider().attachTo(this).setSize(s2, s1).offset(-half, 0).show();
    left.id = 'left';
    var right = Collider().attachTo(this).setSize(s2, s1).offset(half, 0).show();
    right.id = 'right';
  },
});
/*
 * メイン処理
 */
phina.main(function() {
  // アプリケーションを生成
  var app = GameApp({
    title: 'Piko Strike',
    // メインシーンから開始
    startLabel: 'main',
    // アセット読み込み
    assets: ASSETS,
  });
  // 実行
  app.run();
});
```

# 次回予定
次回は、敵のライフゲージを表示とダメージ処理を実装したいと思います。