「そうめん」を使ってサウンドノベルを作ってみる#1

ActionScript3 用の疑似マルチスレッドライブラリ「そうめん」を使って、ブラウザ上で遊べるサウンドノベルを作ってみよーというエントリです。

「そうめん」
擬似的にマルチスレッドが実現できるライブラリ。スレッドクラスに処理の最小単位としていくつかの関数を用意しておき、指定したタイミング毎に実行する。関数内で次のタイミングに実行する関数を指定することで、スレッドの処理を継続させる。スレッドを複数走らせられるほか、条件が満たされるまで関数の実行を休止したり、例外に対するハンドルを指定したり、別スレッドから割り込みをかけたりできる。考え方はゲームプログラミングで使われるタスクシステムに近い。
サウンドノベル
文章に絵と音で演出を施したもので、物語を楽しむことを目的としたゲーム。途中で選択肢が表示されて物語の進行を自分で選べるものが多い。ネット上でもフリーの作品が多数公開されている。http://freegame.on.arena.ne.jp/rank/game/hitokata.htmlとかhttp://freegame.on.arena.ne.jp/rank/game/moonlightblue.htmlとか、知る人ぞ知る、知らない人はとことん知らないゲーム。でも俺は大好きです。

このエントリはちょっと長くなりそうなので続き物です。今回はメインスレッドの作成とテキストの表示を行います。それじゃ、はじめましょう。

1、スレッドを作る

そうめんを利用するにはまずメインとなるスレッドを作る必要があります。スレッドはThreadクラスのサブクラスで、処理の最小単位である関数をいくつか持ちます。これらの関数はThread派生クラスのメソッドとして実装するのが一般的なんですが、今回は配列に複数の関数を格納しメンバ変数として持つようにしてみました。
実装は以下のとおり

package
{
  import flash.display.Sprite;
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.filters.GlowFilter;
  import flash.text.TextField;
  import flash.text.TextFormat;
  import org.libspark.thread.Thread;

  public class NovelThread extends Thread
  {
    private var root:Sprite;
    private var textField:TextField = new TextField();
    private var count:int=0;

    public function NovelThread(root:Sprite):void {
      this.root = root;
    }

    override protected function run():void {
      next(step); // 次はstepメソッドを呼び出す
    }
    
    private function step(...args:Array):void {
      // 次に呼び出すメソッドは自分自身
      next(step);
      // scenarioから関数をひとつ取り出して実行
      var f:Function = scenario[count++] as Function;
      if(f!=null) f.call(this);
      else next(null);
    }

    private var scenario:Array = [
      function ():void {
        // メッセージを表示するTextFieldの初期化
        var format:TextFormat = new TextFormat();
        format.size = 16;
        format.color = 0xffffff;
        format.letterSpacing = 5;
        format.leading = 10;
        format.bold = true;
        textField.defaultTextFormat = format;
        textField.setTextFormat(format);
        textField.x = 10;
        textField.y = 10;
        textField.width = 300;
        textField.height = 300;
        textField.multiline = true;
        textField.selectable = false;
        textField.wordWrap = true;
        textField.filters = [new GlowFilter(0x000000, 1.0, 4, 4, 20)];
        root.addChild(textField);        
      },
      function ():void {
        textField.text = "俺の名前は甘辛子俊介(あまがらししゅんすけ)";
        event(root, MouseEvent.MOUSE_DOWN, step);
        next(null);
      },
      function ():void {
        textField.text = "とある地方の駅前に事務所を構えている三流探偵だ。";
        event(root, MouseEvent.MOUSE_DOWN, step);
        next(null);
      },
      function ():void {
        textField.text = "残念ながらあまり儲かっているとは言えない。";
        event(root, MouseEvent.MOUSE_DOWN, step);
        next(null);
      },
      function ():void {
        textField.text = "[続く]";
        event(root, MouseEvent.MOUSE_DOWN, step);
        next(null);
      },
    ];
  }
}

こんな感じです。NovelThread はカウンターを持っていて、scenario 配列に格納された関数を順番に実行します。scenario[0]にはテキストフィールドを初期化する関数が、scenario[1]〜scenario[4]にはメッセージを表示する関数が入っています。メッセージ表示関数内では next(null) として次に実行する関数を無指定にしています。これによりスレッドは待機状態に入ります。待機状態から抜けるために、event メソッドで画面がクリックされたときに step メソッドが呼ばれるようにしています。

実際にサンプルをさわると、マウスがクリックされるたびに関数が順次実行されメッセージが切り替わっていくのがわかると思います。

2、ドキュメントクラスを作る

このNovelThreadを呼び出すドキュメントクラスは以下のような感じになります。

package {
  import flash.display.Sprite;
  import flash.events.MouseEvent;
  import flash.filters.GlowFilter;
  import flash.text.TextField;
  import flash.text.TextFormat;
  import org.libspark.thread.Thread;
  import org.libspark.thread.EnterFrameThreadExecutor;

  public class Novel extends Sprite {
    public function Novel () :void {
      // エラー表示用のテキストフィールド
      var tf:TextField = new TextField;
      tf.width = 320;
      tf.height = 240;
      tf.textColor = 0xffffff;
      addChild(tf);
      
      // 「そうめん」の初期化
      if (!Thread.isReady) {
        Thread.initialize(new EnterFrameThreadExecutor());
      }
      // スレッド内でエラーが起こったときに呼ばれる関数
      Thread.uncaughtErrorHandler = 
        function(error:Error, thread:Thread):void { tf.text = error.message; }

      // スレッド開始
      var thread:Thread = new NovelThread(this);
      thread.start();
    }
  }
}

ドキュメントクラスNovelのコンストラクタでは、スレッド実行の前に二つ準備をしています。

まず、そうめんライブラリを使用する下準備として Thread.initialize メソッドを呼び出しています。ここで引数に渡すExecutorによってスレッド処理の最小単位である関数がどのタイミングで実行されるかを決定します。

EnterFrameThreadExecutor
フレーム毎に実行
IntervalThreadExecutor
指定した時間毎に実行

IntervalThreadExecutorはフレーム毎よりも細かいタイミングでスレッドを実行できるのですが、このクラスの内部で使われている flash.utils.TimerクラスはFlashにかかる負荷によって実行間隔が変わります。*1フレームと同期していないという点も、のちのち問題になったりするので、特に理由がなければEnterFrameThreadExecutorを使うのがいいかと思います。

次にスレッド内で起きた例外を処理するエラーハンドラを設定しています。スレッド内で起きた例外は親スレッドに投げられるのですが、親スレッドがないスレッドが例外を投げた場合は、このハンドラで処理されます。うっかりこれを設定し忘れると、スレッド内で起きた例外は黙殺されてしまいます。エラーハンドラ内で throw してみても、やはりスレッドシステムの外側に例外は届かないようです。上の例ではテキストフィールドにメッセージを出力するようにしていますが、trace関数を使ってもOKです。例外が見えないとデバッグ時はいろいろ不便なので、忘れずに設定しておきましょう。

今回はここまで

現時点ではまだ画像の表示ができてません。これを行うためには、素材の読み込みを行う必要があるのですが、外部ファイルの読み込みは非同期に行われるのでコードが煩雑になりがちです。
次回は外部からの素材読み込みの処理を「そうめん」を使って書いてみることにします。

*1:ドキュメントドキュメントの例では80ms→100ms程度の変化があるそうですが、測ったところもっと大きく揺れることもあるようです。余談ですが俺はこのタイミングの変化を指標にしてFlashにかかっている負荷を図ったりしてました。