PixiJSでブラウザゲーム 06ドラクエ風流れるメッセージ

PixiJSでブラウザゲーム 05方向キーで操作では矢印方向キーでキャラを操作する方法を紹介しました。 今回は、ドラクエ風にメッセージが流れるパネルを実装してみたいと思います。 サンプルでは、JSONデータとして与えた文字列の配列を1要素づつ流して行きます。 流れている途中でメッセージパネルをクリックするとすぐに全文字列が表示され、もう一度クリックすると次の要素の文字列を流します。

目次:

前提

  • PixiJS v5.2.1

サンプルコード

06-flow-text.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>ドラクエ風流れるメッセージパネル</title>
</head>

<body>
  <div>
    <div>ドラクエ風にメッセージを流します。メッセージパネルをクリックすると次のメッセージが流れます。</div>
    <div>流すデータ:</div>
    <textarea id="ta" rows="8" style="width:100%">
    </textarea>
    <button onclick="callApi()">読み込み</button>
    <div id="game"></div>
  </div>
  <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.1.3/pixi.min.js"></script>
  <script>
  // Create a Pixi Application
  const app = new PIXI.Application({
    width: 512,
    height: 512,
    antialiasing: true,
    transparent: false,
    resolution: 1,
  });

  let gameScene;
  let background; // 背景
  let msgArea; // 文字表示領域

  // #game要素にpixiApp.view追加
  document.querySelector("#game").appendChild(app.view);

  const dummyjson = {
    "text": [
      "いろはにほへと ちりぬるを わかよたれそ つねならむ",
      "うゐのおくやま けふこえて",
      "あさきゆめみし ゑひもせすん",
    ],
  };
  $("#ta").val(JSON.stringify(dummyjson, null, 2));

  // fire this application!
  load(dummyjson);

  /**
   * ロード
   * @param  {Object} json - テキストエリアから入力されるJSONデータ
   */
  function load(json) {
    console.log("initializing by json:", json);
    //load an image and run the `setup` function when it's done
    PIXI.Loader.shared
      // リソースを登録
      // 第一引数は任意のキー、第二引数は実体パス
      .add("dungeon", "https://i.imgur.com/EzpxVBZ.png")
      .load(function(loader, res) {
        // ゲームシーンを作成
        console.log("creating gameScene...");
        gameScene = new PIXI.Container();
        app.stage.addChild(gameScene);
        //
        // 背景作成(ここで画面の大枠サイズを確定)
        //
        console.log("creating background...");
        background = new PIXI.Sprite(PIXI.utils.TextureCache["dungeon"]);
        gameScene.addChild(background);
        //
        // 文字表示領域を作成
        // 
        console.log("creating messaage area...");
        msgArea = new MessageArea({
          parent: gameScene,
          json,
        });
        console.log("initializing done!");
      });
  }


  /********************************************************************
   *
   * Custom component
   * 
   ********************************************************************/

  /**
   * 文字表示領域クラス
   */
  class MessageArea {
    /**
     * コンストラクタ
     * @param  {Container} parent - このクラスが配置される親クラス
     * @param  {Object} json - データ: { text: Array<String> }
     */
    constructor({
      parent,
      json, // from serverside
    }) {
      const root = new PIXI.Graphics();
      root.beginFill(0x000000);
      root.lineStyle(5, 0x00FF00);
      root.drawRect(0, 0, 440, 140);
      parent.addChild(root);
      const textObject = new PIXI.Text("", {
        fill: "#ccffcc",
        stroke: '#4a1850',
        strokeThickness: 6,
        breakWords: true,
        wordWrap: true,
        wordWrapWidth: root.width,
      });
      root.addChild(textObject);
      // 文字ポジ調整
      textObject.position.x = 20;
      textObject.position.y = 20;
      // 領域ポジ調整
      root.position.x = 30;
      root.position.y = 340;

      this.textObject = textObject;
      this.root = root;
      this.json = json;
      this.init();
    }

    /** 流れるメッセージを開始 */
    init(json) {
      if (json) this.json = json;
      json = this.json;
      if (this.status === 1) return; // 処理中はスルー
      this.texts = json.text; // メッセージの配列
      this.maxIndex = json.text.length - 1; // メッセージの最大配列index
      this.currentIndex = -1; // 現在表示しているメッセージ配列のindex番号
      this.status = 0; // 0:メッセージ未表示, 1:メッセージ表示中, 2:メッセージ表示完了
      this.immediate = false; // 即座表示フラグ
      this.clear();
      const txt = this.next();
      if (txt) {
        this.flowText(txt);
      }
    }

    /**
     * 文字列を流れるように表示
     * @param  {String} s - 文字列
     */
    flowText(s) {
      const self = this;
      self.status = 1; // 「文字表示中...」状態に
      self.immediate = false; // 即座表示クリア
      const txtObj = this.textObject;
      const arr = s.split("");
      const ite = function*() {
        addClickEvent(); // クリックイベントを設定
        for (let len = arr.length, i = 0; i < len; i++) {
          if (self.immediate) {
            txtObj.text = s; // 即座に表示
            self.immediate = false; // 即座表示フラグを戻す
            break;
          }
          const item = arr[i];
          yield setTimeout(() => {
            txtObj.text = txtObj.text + item;
            ite.next();
          }, 100);
        }
        // まだ次のメッセージラインが在る場合は▼を付与
        if (self.currentIndex < self.maxIndex) {
          txtObj.text = txtObj.text + "▼";
        }
        self.status = 2; // 全文字表示完了
      }();
      ite.next();

      /** クリックイベントを付加 */
      function addClickEvent() {
        if (self.root.eventNames().length >= 2) return; // 既にイベントが登録されている場合はスルー
        self.root.interactive = true;
        self.root
          .on("click", onClick)
          .on("touchstart", onClick);
      }

      /** クリックハンドラ */
      function onClick(e) {
        // 文字列表示中は重複処理を避けるために処理を抜ける
        // ただし今すぐ全文字列を表示させる
        if (self.status === 1) {
          self.immediate = true;
          return;
        }
        // 全文字列表示が完了し、次に表示すべきメッセージがある場合はそれを流す
        if (self.status === 2) {
          const txt = self.next();
          if (txt) {
            txtObj.text = null;
            self.status = 0; // 全文字表示完了フラグをリセット
            self.flowText(txt);
          }
        }
      }
    }

    /**
     * 次のメッセージラインを取得(無ければnullを返却)
     */
    next() {
      const self = this;
      if (self.currentIndex >= self.maxIndex) {
        return null;
      }
      self.currentIndex += 1;
      return self.texts[self.currentIndex];
    }

    /** 文字列をクリア */
    clear() {
      const self = this;
      self.textObject.text = null;
    }

    show() {
      const self = this;
      self.root.visible = true;
    }

    hide() {
      const self = this;
      self.root.visible = false;
    }
  }

  /********************************************************************
   *
   * Global functions
   * 
   *********************************************************************/

  /** IMPLEMENT: APIが呼ばれてJSONを取得した風の処理 */
  function callApi() {
    // dummy!
    onMessage(JSON.parse($("#ta").val()))
  }

  /** IMPLEMENT: データ受信ハンドラ */
  function onMessage(json) {
    console.log("onMessage:", json);
    msgArea.init(json);
  }

  /** IMPLEMENT: エラーハンドラ */
  function onError(json) {
    console.error("onError:", json);
  }
  </script>
</body>

</html>

メッセージパネルを MessageArea というコンポーネントクラスにしてちょっと汎用性を高めてみました。

実行

ひとりごと

PixiJSネタもそろそろネタ切れの予感。さて、次は何をやろうかな?

宣伝: PixiJSを使ったブラウザゲーム開発なら田中ソフトウェアラボにご相談ください!

オススメ:Zattoyomiで時事ネタチェックの時間節約!
芽萌丸プログラミング部 @programming
プログラミング関連アカウント。Web標準技術を中心に書いていきます。フロントエンドからサーバサイドまで JavaScript だけで済ませたい人たちの集いです。記事は主に @TanakaSoftwareLab が担当。