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で公開してます。