JavaScript
HTML5
ライブラリ
ゲーム制作
phina.js

はじめに

この記事は、phina.js Advent Calendar 2016 の12日目の記事です。

非同期処理

基本的にプログラムはソースに書かれた順序で実行されます。これは一般的に同期処理と呼んでいます。
一方で、処理の一部を特定の処理が終わるまで待ってから実行するなど、書かれた順序に依存しない処理を非同期処理と呼んでおり、時間のかかる処理をバックグランドで実行させておく場合になどによく使用されます。

Flowとは

phina.jsには、Flowという非同期処理を実装するためのクラスが用意されています。javascriptに詳しい人なら、EcmaScript6Promiseと言えばピンと来るかもしれません。(そう言う私もそれほど詳しくありませんが)
FlowはそのPromiseと基本的に同じ機能をphina.jsで実装したものです。tmlib.js時代もFlowというクラス自体はありましたが、中身は大幅に違っています。

Flowの使い方

では、早速使い方を見ていきましょう。
以下がサンプルコードです。

phina.globalize();
// メインシーン
phina.define('MainScene', {
  superClass: 'DisplayScene',
  // コンストラクタ
  init: function() {
    // 親クラス初期化
    this.superInit();
    // 背景色
    this.backgroundColor = 'black';

    var self = this;
    var duration = 2000;
    // flow設定
    var flow = Flow(function(resolve) {
      RectangleShape()
        .addChildTo(self)
        .setPosition(self.gridX.center(), 0)
        .tweener.to({y: self.gridY.center()}, duration)
        .call(function() {
          resolve();
        });
    });

    var rect = RectangleShape().addChildTo(this);
    rect.setPosition(self.gridX.center(1), self.gridY.center(1));
    // 非同期処理(resoveで呼ばれる)
    flow.then(function() {
      rect.tweener.to({y: self.gridY.width}, duration).play();
    });
    // 同期処理
    RectangleShape()
      .addChildTo(self)
      .setPosition(self.gridX.center(-1), self.gridY.center(1))
      .tweener.to({y: self.gridY.width}, duration);
  },
});
// メイン
phina.main(function() {
  var app = GameApp({
    startLabel: 'main',
  });
  app.run();
});

[runstantで確認]

実行すると、左下の矩形は実行と同時に下に移動しますが、右下の矩形は上の矩形が止まってから移動を開始します。

コード説明

Flowの設定

// flow設定
var flow = Flow(function(resolve) {
  RectangleShape()
    .addChildTo(self)
    .setPosition(self.gridX.center(), 0)
    .tweener.to({y: self.gridY.center()}, duration)
    .call(function() {
      resolve('flow done');
      console.log(message);
    }).play();
});
  • 非同期で実行したい処理をFlow内に記述します。
  • 処理が終わった後は、resolveを呼び出します。
  • 上の例では、tweenerを使って一定時間かけて画面中央まで移動した後に、resolveをコールするようにしています。

後処理の記述

// resoveで実行される
flow.then(function() {
  rect.tweener.to({y: self.gridY.width}, duration).play();
});
  • resolveが呼ばれると、Flowクラスのメソッドthenが実行されるため、この中に後から実行したい処理を記述します。

複数のFlowを取り扱う

これまでは1つのFlowオブジェクトを取り扱ってきました。
1つであればtweenerを使えば似たような処理が出来なくもないですが、さらにFlowでは複数の非同期処理をウォッチすることができます。
サンプルコードは以下のとおりです。

phina.globalize();
// メインシーン
phina.define('MainScene', {
  superClass: 'DisplayScene',
  // コンストラクタ
  init: function() {
    // 親クラス初期化
    this.superInit();
    // 背景色
    this.backgroundColor = 'black';

    var self = this;
    var flows = [];

    (10).times(function(i) {
      // flow作成
      var flow = Flow(function(resolve) {
        var x = Random.randint(64, self.gridX.width - 64);
        var y = Random.randint(64, self.gridY.width - 64);
        var duration = Random.randint(500, 5000); 
        // ランダムな位置に円を作成
        CircleShape()
          .addChildTo(self)
          .setPosition(x, y)
          .tweener.fadeOut(duration) // フェードアウト
          .call(function() {
            resolve(i + 'done');
          }).play();
        });
        // 配列に追加
        flows.push(flow);
    });
    // 全てのflow実行後
    Flow.all(flows).then(function(messages) {
      console.log('all done!', messages);
    });
  },
});
// メイン
phina.main(function() {
  var app = GameApp({
    startLabel: 'main',
  });
  app.run();
});

[runstantで確認]

  • 実行すると、ランダムな位置に円が表示されランダムな時間でフェードアウトします。
  • 最後の円がフェードアウトしてから、メッセージがコンソールに表示されます。

コード説明

Flowの配列を作って追加する

var flows = [];

(10).times(function(i) {
  // flow作成
  var flow = Flow(function(resolve) {
    var x = Random.randint(64, self.gridX.width - 64);
    var y = Random.randint(64, self.gridY.width - 64);
    var duration = Random.randint(500, 5000); 
    // ランダムな位置に円を作成
    CircleShape()
      .addChildTo(self)
      .setPosition(x, y)
      .tweener.fadeOut(duration) // フェードアウト
      .call(function() {
        resolve(i + 'done');
      }).play();
    });
    // 配列に追加
    flows.push(flow);
  • Flowを格納する配列を作ってpushしています。

Flow.allで全てのFlowをウォッチ

// 全てのflow実行後
Flow.all(flows).then(function(messages) {
  console.log('all done!', messages);
});
  • Flow.allの引数にウォッチしたいFlowの配列を代入すると、与えれられた全ての非同期処理の終了をウォッチすることができます。
  • 今回は配列でまとめて代入しましたが、個々にFlowを作成して代入することもできます。

おわりに

説明不足のところもあるかもしれませんが、今回紹介したFlowを使うことによって、phina.jsにおける非同期処理をよりスマートにコーディングすることができると思いますので、皆も色々と試してみて下さい。