LoginSignup
8
4

More than 5 years have passed since last update.

phina.jsでモンストっぽいゲームを作る【6】ー敵のターン攻撃ー

Last updated at Posted at 2018-05-05

はじめに

モンストもどきを作るという試みの第6回となる今回は、敵のターン攻撃を実装します。

monst-tut-06.gif

アセットの追加

// アセット
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',
    'crash': 'https://rawgit.com/phinajs/phina.js/develop/assets/images/shooting/crash.png',
  },
};

敵は火の玉で攻撃することにして、その画像をアセットに追加します。

火の玉クラスを作成する

Spriteクラスを継承した以下のようなクラスを用意します。

/*
 * 火の玉クラス
 */
phina.define("Fire", {
  // 継承
  superClass: 'Sprite',
  // コンストラクタ
  init: function() {
    // 親クラス初期化
    this.superInit('crash', 64, 64);
    // フレームインデックス指定
    this.setFrameIndex(15);
  },
});

今回は爆発の画像のフレームアニメーションから、setFrameIndexで特定のコマを指定しています。

ターン変更処理

プレイヤーの攻撃が終わったら、敵のターンに変更する必要があります。
今回はupdate関数でプレイヤーの動きをチェックして、止まった時にターンを変更するようにしています。

  // 毎フレーム更新処理
  update: function() {
    var velocity = this.player.physical.velocity;
    // 画面端反射処理
    this.reflectScreen();
    // 敵との反射処理
    this.reflectEnemy();
    // 一定速度を下回ったら強引に停止させる
    if (velocity.length() > 0 && velocity.length() < 2) {
      velocity.set(0, 0);
      // ターンチェンジ
      this.changeTurn();
    }
  },

changeTurn関数

実際にターンを変更する処理を行うchangeTurnという関数を用意しました。

// ターン変更処理
  changeTurn: function() {
    var massage = this.massage;
    // プレイヤー or 敵のターン
    massage.text = this.turn === 'player' ? 'ENEMY TURN' : 'PLAYER TURN';
    // ラベルのキャンバス幅
    var width = massage.calcCanvasWidth();
    massage.x = SCREEN_WIDTH + width / 2;
    massage.y = this.gridY.center();
    massage.show();

    var self = this;
    // 右から左に流れるアニメーション
    massage.tweener.clear()
                   .to({x: this.gridX.center()}, 500, "easeOutCubic")
                   .wait(200)
                   .to({x: -(width / 2)}, 500, "easeInCubic")
                   .call(function(){
                     // ターン変更
                     if (self.turn === 'player') {
                       self.setInteractive(false);
                       self.turn = 'enemy';
                       self.attackPlayer();
                     }
                     else {
                       self.setInteractive(true);
                       self.turn = 'player';
                     }
                   });
  },
  • ターン変更を知らせるラベルを作成して、tweenerで画面右から左に流す処理を行っています。
  • ラベルの位置決めをする必要があるのですが、ラベルはデフォルトでは文字数に関係なく64X64のサイズになっていますので、calcCanvasWidth()関数で実際の横幅を求めています。
  • ターンはturnというクラス変数でを管理します。敵のターンになるとattackPlayerという関数を呼び出した後に、プレイヤーを操作できないようにしています。

attackPlayer関数

敵の攻撃を開始する関数ですが、今回は敵にattackイベントを登録してそれを発火させる仕様にします。汎用的な仕様にすることで、複数の敵を作った時にも楽に対応できるようにしています。

// 敵の攻撃処理
  attackPlayer: function() {
    this.enemyGroup.children.each(function(enemy) {
      enemy.flare('attack');
    }); 
  },

敵の攻撃パターンを定義する

var flows = [];
    // 敵の攻撃パターン登録
    this.enemyGroup.children.each(function(enemy) {
      enemy.on('attack', function() {
        // 火の玉をランダムな角度に発射
        (8).times(function(i) {
          // flow作成
          var flow = Flow(function(resolve) {
            var fire = Fire().addChildTo(self.fireGroup).setPosition(enemy.x, enemy.y);
            // 到達地点を計算
            var deg = Random.randint(0, 360);
            var x = Math.cos(Math.degToRad(deg)) * SCREEN_WIDTH;
            var y = Math.sin(Math.degToRad(deg)) * SCREEN_HEIGHT;
            // tweenerで移動
            fire.tweener.by({x: x, y: y}, 2000)
                        .call(function() {
                          // 到達後処理
                          fire.remove();
                          resolve();
                        }).play();
          });
          // 配列に追加
          flows.push(flow);
        });
        // 全てのflow実行後
        Flow.all(flows).then(function(messages) {
          // ターンチェンジ
          self.changeTurn();
        });
      });
    });
  • 敵の攻撃パターンは、敵のattackイベントに登録します。
  • 別の箇所からflareで呼び出すことで、登録した内容が処理されます。
  • 敵の攻撃は、ランダムな方向に火の玉を発射するパターンにしています。
  • tweenerで動かし、Flowで非同期処理を行うことで、攻撃の終了を検知できるようにしています。
  • 攻撃が終わるとプレイヤーのターンに変更します。
  • Flowについては、Flowで非同期処理を行うを参考にして下さい。

動作確認

[runstantプロジェクトへのリンク]

次回予定

敵の攻撃は実装できましたが、プレイヤーとの当たり判定がないなどまだゲームとしては成立していません。
次回は、よりゲームらしくしていきたと思います。

全体コード

コード表示
// グローバルに展開
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',
    'crash': 'https://rawgit.com/phinajs/phina.js/develop/assets/images/shooting/crash.png',
  },
};
// 定数
var SCREEN_WIDTH = 640;
var SCREEN_HEIGHT = 960;
var PLAYER_SPEED = 50;
var FRICTION = 0.98;
var PLAYER_ATTACK_POWER = 25;
/*
 * メインシーン
 */
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));
    // グループ
    this.enemyGroup = DisplayElement().addChildTo(this);
    this.fireGroup = DisplayElement().addChildTo(this);
    // 敵キャラ
    var dragon = Enemy('dragon').addChildTo(this.enemyGroup);
    dragon.setPosition(this.gridX.center(), this.gridY.center(-4));

    var self = this;
    var flows = [];
    // 敵の攻撃パターン登録
    this.enemyGroup.children.each(function(enemy) {
      enemy.on('attack', function() {
        // 火の玉をランダムな角度に発射
        (8).times(function(i) {
          // flow作成
          var flow = Flow(function(resolve) {
            var fire = Fire().addChildTo(self.fireGroup).setPosition(enemy.x, enemy.y);
            // 到達地点を計算
            var deg = Random.randint(0, 360);
            var x = Math.cos(Math.degToRad(deg)) * SCREEN_WIDTH;
            var y = Math.sin(Math.degToRad(deg)) * SCREEN_HEIGHT;
            // tweenerで移動
            fire.tweener.by({x: x, y: y}, 2000)
                        .call(function() {
                          // 到達後処理
                          fire.remove();
                          resolve();
                        }).play();
          });
          // 配列に追加
          flows.push(flow);
        });
        // 全てのflow実行後
        Flow.all(flows).then(function(messages) {
          // ターンチェンジ
          self.changeTurn();
        });
      });
    });
    // 矢印
    arrow.setPosition(this.player.x, this.player.y);
    // ターン
    this.turn = 'player';
    // メッセージ
    this.massage = Label().addChildTo(this).hide();
    // シーン全体から参照できるようにする
    this.arrow = arrow;
  },
  // ターン変更処理
  changeTurn: function() {
    var massage = this.massage;
    // プレイヤー or 敵のターン
    massage.text = this.turn === 'player' ? 'ENEMY TURN' : 'PLAYER TURN';
    // ラベルのキャンバス幅
    var width = massage.calcCanvasWidth();
    massage.x = SCREEN_WIDTH + width / 2;
    massage.y = this.gridY.center();
    massage.show();

    var self = this;
    // 右から左に流れるアニメーション
    massage.tweener.clear()
                   .to({x: this.gridX.center()}, 500, "easeOutCubic")
                   .wait(200)
                   .to({x: -(width / 2)}, 500, "easeInCubic")
                   .call(function(){
                     // ターン変更
                     if (self.turn === 'player') {
                       self.setInteractive(false);
                       self.turn = 'enemy';
                       self.attackPlayer();
                     }
                     else {
                       self.setInteractive(true);
                       self.turn = 'player';
                     }
                   });
  },
  // 敵の攻撃処理
  attackPlayer: function() {
    this.enemyGroup.children.each(function(enemy) {
      enemy.flare('attack');
    }); 
  },
  // 毎フレーム更新処理
  update: function() {
    var velocity = this.player.physical.velocity;
    // 画面端反射処理
    this.reflectScreen();
    // 敵との反射処理
    this.reflectEnemy();
    // 一定速度を下回ったら強引に停止させる
    if (velocity.length() > 0 && velocity.length() < 2) {
      velocity.set(0, 0);
      // ターンチェンジ
      this.changeTurn();
    }
  },
  // 画面端との反射処理
  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;
    // 敵との当たり判定
    this.enemyGroup.children.each(function(enemy) {
      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;
          // 敵のライフを減らす
          enemy.damage(PLAYER_ATTACK_POWER);
        }  
      });
    });
  },
  // タッチ開始時処理
  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;
  },
});
/*
 * 敵クラス
 */
phina.define("Enemy", {
  // 継承
  superClass: 'Sprite',
  // コンストラクタ
  init: function(name) {
    // 親クラス初期化
    this.superInit(name);
    // ライフゲージ
    this.life = LifeGauge({
      width: this.width * 0.8,
      height: this.height / 10,
      life: 100,
    }).addChildTo(this);
    // ライフゲージの位置
    this.life.y = this.height / 2 - this.life.height * 2;
    var self = this;
    // ライフが無くなった時の処理
    this.life.on('empty', function() {
      self.remove();  
    });

    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);
    top.id = 'top';
    var bottom = Collider().attachTo(this).setSize(s1, s2).offset(0, half);
    bottom.id = 'bottom';
    var left = Collider().attachTo(this).setSize(s2, s1).offset(-half, 0);
    left.id = 'left';
    var right = Collider().attachTo(this).setSize(s2, s1).offset(half, 0);
    right.id = 'right';
  },
  // ダメージ処理
  damage: function(value) {
    this.life.value -= value;
  },
});
/*
 * 火の玉クラス
 */
phina.define("Fire", {
  // 継承
  superClass: 'Sprite',
  // コンストラクタ
  init: function() {
    // 親クラス初期化
    this.superInit('crash', 64, 64);
    // フレームインデックス指定
    this.setFrameIndex(15);
  },
});
/*
 * ライフゲージクラス
 */
phina.define("LifeGauge", {
  // 継承
  superClass: 'Gauge',
  // コンストラクタ
  init: function(param) {
    // 親クラス初期化
    this.superInit({
      width: param.width,
      height: param.height,
      fill: 'red',
      stroke: 'silver',
      gaugeColor: 'limegreen',
      maxValue: param.life,
      value: param.life,
    });
    // 値変化アニメーションの時間
    this.animationTime = 500;
  },
});
/*
 * メイン処理
 */
phina.main(function() {
  // アプリケーションを生成
  var app = GameApp({
    title: 'Piko Strike',
    // メインシーンから開始
    startLabel: 'main',
    // アセット読み込み
    assets: ASSETS,
  });
  // 実行
  app.run();
});

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4