Source: Automaton.js

Automaton.js

/* global phina:false */

/**
 * @typedef {Object} StateObject
 * @property {function} [enter] - 状態が変わった際に実行する処理
 * @property {function} [update] - 毎フレーム実行する処理
 */

/**
 * @experimental
 * FSMの概念を実装したクラス。
 * あらかじめセットしたstateオブジェクトに応じてtargetに特定の振る舞いをさせる
 * @see http://www.lancarse.co.jp/blog/?p=1039
 *
 * @class phina.accessory.Automaton
 * @memberOf phina.accessory
 * @extends phina.accessory.Accessory
 *
 * @example
 * import { Automaton } from '@pentamania/phina-extensions';
 * // or const Automaton = phina.accessory.Automaton;
 *
 * phina.define('Enemy', {
 *   superClass: phina.display.Sprite,
 *   init: function() {
 *     this.superInit("enemyImage");
 *     const automaton = Automaton().attachTo(this)
 *       .registerState('search', {
 *         enter: function() {
 *           // 状態を切り替えたときに見た目を変更する
 *           this.setFrameIndex(0);
 *         },
 *         update: function(app) {
 *           // うろつく処理…
 *           if (プレイヤーを見つけた) {
 *             // 状態を変更する際は状態文字列を返す
 *             return 'chase';
 *           }
 *         },
 *       })
 *       .registerState('chase', {
 *         enter: function() {
 *           this.setFrameIndex(1);
 *         },
 *         update: function(app) {
 *           // プレイヤーを追っかける処理
 *           if (プレイヤーを見失った) {
 *             return 'search';
 *           }
 *         },
 *       })
 *     ;
 *     // 初期状態
 *     automaton.setState('search');
 *   },
 * });
 */
export default phina.createClass({
  superClass: phina.accessory.Accessory,

  init: function() {
    this.superInit();
    this._stateList = {};
    this._currentStateLabel = null;
  },

  update: function(app) {
    const currentState = this._getState(this._currentStateLabel);
    if (!currentState) {
      // warnする?
      return;
    }

    // const nextState = currentState.update(this.target, app);
    const nextState = currentState.update.call(this.target, app);
    if (nextState && nextState !== this._currentStateLabel) {
      // this._getState(nextState).enter(this.target, app);
      this._getState(nextState).enter.call(this.target, app);
      this._currentStateLabel = nextState;
    }
  },

  _getState: function(stateLabel) {
    return this._stateList[stateLabel];
  },

  /**
   * @instance
   * @param {string|symbol} stateLabel - 状態を示すラベル
   * @return {this} - chainable
   */
  setState: function(stateLabel) {
    this._currentStateLabel = stateLabel;
    return this;
  },

  /**
   * @instance
   * @param {string|symbol} stateLabel - 状態を示すラベル
   * @param {StateObject} stateObject - 状態オブジェクト
   * @return {this} - chainable
   */
  registerState: function(stateLabel, stateObject) {
    /* stateObjectをそのまま渡す */
    this._stateList[stateLabel] = stateObject;

    // /* パターン2:actorStateをphina class(function)で定義する場合 */
    // if (typeof actorState !== 'function') {
    //   // 文字列で渡された
    //   actorState = phina.using(actorState);
    // }
    // if (!actorState) {
    //   console.error('[phina-extensions]: 存在しないステートです')
    // }
    // this._stateList[stateLabel] = new actorState();

    return this;
  },

});