RPGゲームを作ろうとすると、描画処理等の下回りを作成した後、スクリプト 言語を実装する必要が出てくると思います。ゲームのイベント処理等はイベン トエディタ等でスクリプトを記述した方が効率が良いからです。 今回はシェルスクリプトっぽいインタプリタをJavaCCを用いて実装します。
1. やりたいこと
インタプリタでやりたいことを以下にまとめます。
* 変数を用いたい * 整数と文字列を用いたい * 簡単な算術演算を用いたい * スクリプトファイルで定義した関数を用いたい * ビルトインコマンドを呼び出したい * if文を用いたい * while文を用いたい
特にif文について解説します。 算術演算についてはCodeZineの記事を参考にしました。
1. TOKEN
TOKENを以下のように定義します。
TOKEN: { < LBLOCK: "{" > | < RBLOCK: "}" > | < LPAREN: "(" > | < RPAREN: ")" > | < LBRACKET: "[" > | < RBRACKET: "]" > | < IF: "if" > | < THEN: "then" > | < ELIF: "elif" > | < ELSE: "else" > | < FI: "fi" > | < FOR: "for" > | < WHILE: "while" > | < DO: "do" > | < DONE: "done" > | < BREAK: "break" > | < RETURN: "return" > | < ID: (["a"-"z"] | ["A"-"Z"]) (["a"-"z"] | ["A"-"Z"] | ["0"-"9"])* > | < REF: "$" > | < MUL: "*" > | < DIV: "/" > | < ADD: "+" > | < SUB: "-" > | < ASSIGN: "=" > | < GE: ">=" > | < GT: ">" > | < LE: "<=" > | < LT: "<" > | < EQ: "==" > | < NE: "!=" > | < QUESTION: "?" > | < NUM: (["0"-"9"])+ > }
2. if文
シェルスクリプトのif文は以下のような構造になります。
if [ $a < $b ] then return $a elif [ $a == $b ] then return $a else return $b fi
3. if文のjjtファイル
以下のIfStmt()という構文木のノードを定義します。javaファイルでは ASTIfStmtというクラスで表現されます。
void IfStmt(): {} { <IF> <LBRACKET> Cond() <RBRACKET> <THEN> Block() [ ( <ELIF> <LBRACKET> Cond() <RBRACKET> <THEN> Block() )+ ] [ <ELSE> Block() ] <FI> }
[<statement>]は省略可能なパターンを表します。また、(<statement>)+は1回 以上の繰り返しを表します。つまり、ifは必ず必要ですが、elifは複数個あっ てもなくてもよくて、elseはひとつあるかないかとなります。
JavaCCはIfStmtのパターンを見つけてくれると、<IF>や<LBRACKET>等のTOKEN は切り取ってくれて(というよりパターンマッチングにしか使わない)、 Cond()とBlock()のみをASTIfStmtでアクセスできるようになります。
Cond()とBlock()はそれぞれASTCondとASTBlockというクラスで表現されます。
4. IfStmtのVisitor
ASTXXXクラスはSimpleNodeクラスを継承しており、ASTIfStmtのjjtGetChild() でASTCondやASTBlockにアクセスできます。jjtGetNumChildren()で jjtGetChild()で取得可能なノードの数が分かります。それぞれのノードの jjtAccept()を実行すると、それぞれのノードのvisit()が呼ばれます。
@Override public Object visit(ASTIfStmt node, Object data) { /** * First child points if condition, second points if body, third points elif * condition, fourth points elif body, and the last child points else body. */ int i, size = node.jjtGetNumChildren(); for (i = 0; i < size - 1; i += 2) { ShellValue value = (ShellValue) node.jjtGetChild(i).jjtAccept(this, data); String getValue = value.getValue(); /** Check condition is 1 */ if (getValue.equals("1")) { node.jjtGetChild(i + 1).jjtAccept(this, null); return null; } } /** Check which else condition is exists. */ if (i >= size - 1 && size % 2 != 0) node.jjtGetChild(size - 1).jjtAccept(this, null); return null; }
jjtGetChild(0)はif文の判定文、jjtGetChild(1)はif文が真の場合に実行され るブロック、jjtGetChild(2)はelif文の判定文、jjtGetChild(3)はelif文が真 の場合に実行されるブロック、・・・、sizeが奇数の場合にjjtGetChild(size - 1)はelse文の実行されるブロックとなります。
それぞれ判定文を取得し、得られた値が真(ここでは文字列が"1")の場合に 続くブロックのvisitを実行するようにします。
5. コード
GitHubで公開してます。