ゲーム作成に役立つオブジェクト指向5
今回はInterpreterパターンについてです。
Interpreterというのは、
例えばテキストファイルに書かれた命令を、
実行時に一行ずつ解読しながらその内容を実行する、
というものです。
例えば、インタプリタは、
- アドベンチャーゲームのフローを制御するスクリプト
- シューティングゲームの敵キャラの行動パターンのスクリプト
といった使い方があります。
具体的には、
スクリプトの書かれたテキストファイルを外部に用意しておき、実行時にそれを読ませる、
という仕組みです。
これにより、
などといった、「開発の負担」を減らす効果があります。
それをオブジェクト指向でどう実装するか、
というのがInterpreterパターンです。
インタプリタの命令のパターン
Interpreterの命令には、ある一定のパターンがあります。
それは、一行の命令は、
- 構文の制御
- 命令の実行
のどちらかである、ということです。
「構文の制御」というのは、C言語で言えば、「while/if」などの命令です。
これらの命令は何かを実行するというよりも、構文そのものをどう扱うかを定義するものです。
「命令の実行」とは、実際に「3歩動け!」とか「弾を撃て!」とかの命令のことです。
それで、クラス図は?
ということで、クラス図なのですが、
どちらも同じ命令ということで、Node抽象クラスを基底に持ち、
「構文の制御」がNonterminalNodeクラス、
「命令の実行」がTerminalNodeクラスとなります。
Terminalとは終着駅のことで、「命令の実行」は構文の解析を一時停止するため、
このような名称となっています。
「じゃあ、その命令はどこからもらうの?」
ということなのですが、それがparseメソッドです。
parseメソッドはContextクラスを受け取ります。
このContextクラスが、命令が書かれたテキストファイルの読み込んだクラスです。
Nodeクラスの派生クラスは、Contextを解析して、命令を実行したりします。
ちなみに、
なぜ「Node」という名称がついているか、
ということなのですが、
例えば、「構文の制御」が「構文の制御」を呼んだ場合、
どんどん処理がネストしていくことになります。
こうやってできた「節点」をNodeと呼ぶからです。
処理の流れは?
では、どのように処理を実行していくのかを説明します。
- Contextにスクリプトを読み込ませる
- NonTerminalNodeにContextを渡す
- NonTerminalNodeの中で、現在行のスクリプトを判定する
- 「命令の実行」であれば、TerminalNodeにContextを渡し、命令を実行。
- 「命令の制御」であれば、NonterminalNodeにContextを渡す->3に戻る。
2についてなのですが、スクリプトの仕様として、
実行のエントリポイントとして「トップノード」を用意しておきます。
こうすることで、Nodeクラスの呼び出し側としては、
NonterminalNodeのparseメソッドを呼ぶだけ、
という簡潔な処理になります。
問題点
といった感じがInterpreterパターンなのですが、
実は問題点が2つあります。
1つは、
このクラス設計ですと、
- Node派生クラスに「命令実行」の処理を書かなければならない
ということです。
Node派生クラスの役割は、「構文の解析」でありますから、
これでは、クラスが肥大化してしまいますよね。
そこで、
これを解決するためにはVisitorパターンを使います。
(それについては次回で…)
もう1つの問題点は、
- 制御の中断
です。
この方法は、構文の解析をどんどんネストしています。
こうすると、構文の解析が終了するまで、ゲームループに制御が戻ってきませんよね。
それは困ったものです。
で、解決策としては、
- マイクロスレッド(ファイバ)
- 解析履歴を保持しておき、解析を中断する
という方法があります。
ゲームループに戻すためには、ネストを抜ける必要がありますので、
スタックを無理やり抜ける「マイクロスレッド」か、
解析履歴を持たせておけば、次回の呼び出し時に解析を再開できます。
といった感じですね…。
次回はVisitorパターンについてです。