@HSP3
@shtijyou
あいさつと前書き
はじめまして、七条 彰紀です。これは 本名じゃありません。高校一年生です。
C
++でゲームつくってます。
タイトルのとおり、HSP3 で超(比較的) 単純なスクリプトエンジンを作ります。難 しいと思っても、これで単純な方です。
この記事の内容、実は
HSP には
向い ていません。もし書けるのでしたら、クラ スや独自のデータ構造を使えるJava や C++などで
書くことをおすすめします。そ の方がよりスマートに書けますし、仕様の拡張も
楽です。また、これは初心者向けの内容ではあ りません。中・上級者が対象となっていま す。
以上を踏まえてお読みください。
仕様を決める
プログラムを作るのだから、仕様を決 めなくてはなりません。
このスクリプトエンジンで実行させる スクリプト言語の形を決めます。今回は 使用目的をゲームキャラクタ 1 つだけの パラメータ、つまり移動方向や移動速度 の設定に絞り、次のような仕様にしました。
最初に、簡単には変更できない仕様を 提示します。この記事を参考にするなら、
この仕様を拡張したり消すことは難しい と考えてください。
•
形式は[パタメータ名]=[正の実数]
•
パタメータをスクリプト側から追 加することは出来ない•
パラメータ名に空白は使えない•
パラメータ名の先頭に数字は使え ない……身構えていたかもしれませんが、
実はこれだけです。
以下の仕様は一部を書き換えるだけ で変更可能です。特にパラメータ名の変 更、パラメータの追加は簡単です。
•
スクリプトファイル名 は”script.txt”•
”//”(スラッシュ 2 つ)から行末ま でコメント•
“,”(カンマ)で区切ると 1行に複 数個書ける•
1 ステップにつき 1行実行される•
ステップ実行の間隔のフレーム数 は、パラメータ : StepInterval で設 定可能•
パタメータの値は変更されない限 りそのまま•
パタメータは StepInterval の他に 以下のみ•
Direction : キャラクタの移動方 向(単位は度)•
Speed : キャラクタの移動速 度(単位はピクセル)以下の仕様の変更は、記事の最後に 書く「仕様
拡張の
ヒント」に詳しく書きま す。•
制御構文は無し仕様を粉々にする
何いってんだと思うかもしれませんが、
以上の仕様から細かい実装のレベルに 立ち入ろう、という話です。トップダウン 設計、といったりします。
全体の処理の流れは 3 つの段階に分 けられます。一つ目はスクリプトファイル 読み込み、次に読み込んだ文字列をバラ バラに分解する処理、最後にその実行で す。
最初に行うファイル読み込み部分は
HSP の標準
命令だけで
簡単にできます。最後のスクリプト実行部分も難しいこと は有りません。
一番プログラマの作業量が多いのが、
読み込んだ文字列をバラバラにする部 分です。字
句
解析処理といいます。これ は一文字一文字の種類、前後の文字か ら、その文字列を意味ごとに分割してい く処理です。字句
解析処理は意味のな い文字列を意味のある記号の連なりに 変換する作業とも言えます。字
句
解析には大まかに二つの実装方 式があるのですが、今回は字句
解析を かなり原始的なやり方で実装します。それでは実装です。
字句解析処理の下準備
今回は一番の山場から手をつけてい きましょう。字
句
解析処理です。今回は処理をまとめる方法として、専 用モジュールを定義してその中で字
句
解析を行う命令を作ります。モジュール
に関してはここでは説明しないので、分 からないという人はHSP に
付属する「プ ログラミング・マニュアル」の 4.1項、もし くは「HSP3 モジュール機能ガイド」を読 んでください。字
句
解析処理は量が多いので、Lexer.hsp にスクリプトを書いていきます。
スクリプトをファイルごとに分けることで、
うまく書けば「スクリプト実行部分の別
バージョンを作りたい」という欲求にすぐ 対応できます。
Lexer は字
句
解析をするもの(字句
解 析器と呼ばれます)を示す用語です。定数の定義
Lexer.hsp
#module LexersUtil ;モジュール宣言 ;文の要素の種類を表す定数
#const IDENT 1 ;変数など #const REAL 2 ;実数 #const DELIM 3 ;区切り文字 #const OPE 4 ;演算子 #const EOS 255 ;End Of Script #define COMMENT_BEGIN "//" ;コメントの初め #define SENTINEL "$" ;番兵
#define DELIMITER "," ;区切り文字
;true/falseがHSPには無いので定義 #const TRUE 1
#const FALSE 0
まずは処理の為の準備です。
最初に処理をスムーズに書くために定 数と特殊な意味を持つ文字を定義しま す。 これらは LexersUtil というモジュー ルに含まれています。”Lexer's
Utillity
”、「字句
解析器の小道具」とい う意味で名前をつけました。SENTIN
EL
は「番兵」と
呼ばれるも ので、今回はスクリプト一行の終わりに 付けて、処理中にスクリプト一行の終わ りが分かりやすいようにします。「番兵」
はよく使われるテクニックで、C言語や C
++で文
字列の最後に自動的に追加さ れる'\0'も似たような役目を持ちます。DELIMIT
E
R(デリミタ)は一行に複数 個の文を書くための区切り文字です。ソースコードにある
C
OMME
NT_BE
GINもDELIMITE
R も仕様に含まれているものです。逆に、この部分の仕様を変更したい場合はこの 定義を変えるだけで出来ます。
文字種類の定義
#defcfunc IsINTsChar var c ord=peek(c,0)
return (ord>='0')&&(ord<='9') #defcfunc IsREALsChar var c
return IsINTsChar(c) || (c==".") || (c=="-") #defcfunc IsAlphabetic var c
ord=peek(c,0)
return ((ord>='A')&&(ord<='Z')) ||
((ord>='a')&&(ord<='z')) #defcfunc IsIDENTsChar var c
return IsAlphabetic(c) || IsINTsChar(c) ||
(c=="_")
#defcfunc IsOperator var c return (c=="=") #defcfunc IsSymbol var c
return (c==DELIMITER) || (c==SENTINEL) ||
IsOperator(c)
次に、変数はどの種類の文字で出来 ているのか、実数はどの種類の文字で 出来ているのか、を定義します。
IsINTsChar や IsR
E
ALsChar はそ れぞれ整数(INT、定数にはしていませ ん)、実数(RE
AL)に含まれる文字が渡さ れたとき、TRUE
を返す関数です。たと えばIsRE
ALsChar(“1”)はTRUE
で すがIsRE
ALsChar(“A”)やIsR
E
ALsChar(“*
”)は FALSEです。Alphabetic は 英字、Operator は 演算子、 Symbol は演算子やその他
諸
々の記号を表す英単語です。IsAlphabetic などで
ord=peek(c,0)
という一文があります。peekは変数から 1 バイトだけとりだし整数として返す関数 で、文字列に使えばそれに含まれる任意 の文字の文字コードを取得することが出 来ます。文字コードは整数なので、演算 子が使えます。そこで
(ord>='A')&&(ord<='Z')
のようにすることで、その文字が'A
'か
ら'Z'まで(“A”に対応する文字コードか ら”Z
”に対応する文字コードまで)の間に ある文字か判定することができます。こ れは、c==“A” || c==”B” || c==”C” || c==”D”;...以下略
のようにするよりも遙かにスマートです。
あとはそれぞれの関数を演算子でつ ないだりして、シンプルに実装しています。
これらのまとめとして、ある文字を渡す とその文字がどの文の要素に含まれる ものなのかを返す関数を実装します。
;文字のタイプを返す関数 #defcfunc GetCharType str c switch(c)
case DELIMITER return DELIM
case SENTINEL return EOS swend
char=c ;GetCharType()内部の変数にする if(IsREALsChar(char)): return REAL if(IsOperator(char)): return OPE if(IsIDENTsChar(char)): return IDENT return FALSE
最初のswitch はいいとして、最後の 連続 if文は順番に気を付けないといけ ません。たとえば IsIDENTsChar と IsR
E
ALsChar の順番を逆にすると、IsIDENTsChar の判定条件には「その 文字がIsINTsChar でTR
UE」も含ま
れているので、整数の”1”もIDENTと 判定されてしまいます。これではRE
AL に判定されるのは”.”(小数点)だけです。少し実装が違うと困るものがあるので それもソースコードを掲載しますす。
;1文字だけ取りだす関数
#defcfunc GetNextChar var one_line,var index next_char=strmid(one_line,index,1) index+=1
return next_char
文字列のone_line から index目の
文字を取り出して返し、indexを一つ進 めるだけです。
他にコメント文を消す命
令、
空白(ス ペースとタブ)を消す命令、
両方消した後「番
兵」を
追加する関数も必要ですが、ど のように実装しても構わないので、ソース コードは省きます。準備の終に、
#global
と書いてLexersUtil モジュールの終で す。
これで準備は完
了。
それでは字
句
解析処理の本番です!字句解析処理
処理は次のような流れで行います。
•
一文字取り出して、その文字の種類で switch•
case 内でまた一文字取り出す•
また同じ種類の文字だったら出力結果 に追加して繰り返し、違ったら case を 出る•
最初に戻るここでよく理解できなかったとしても心 配することはありません。処理方法を「日 本語で言わないでプログラミング言語で 書いてくれ」と言う人はザラに居ます。私 もそういうものは全部C
++で
書いて欲し いです。では今度は詳細を省いたソースコード を見てみましょう。
字句解析処理(大雑把 ver)
#module Lexer ;モジュール宣言
ErrorMessage="" ;エラーが起きた時のエラー文 ;oneline_raw : DELIMITER区切りのスクリプト一行 ;var_and_vol : N*3の配列。一文の
; 左辺、演算子、右辺がバラバラに入れられる
#deffunc LexerOneLine var oneline_raw,array var_and_vol //モジュール内の変数にする
oneline_raw_=oneline_raw
VV_Index1=0 ;var_and_volの一次インデックス VV_Index2=0 ;var_and_volの二次インデックス Index=0 ;OneLine中の文字を示すインデックス ;コメント・空白の削除、「番兵」の追加
OneLine=DeleteUnnecessary(oneline_raw_) repeat ;break判定はswitch内で行う ;文字の種類で分岐
switch(GetCharType(GetNextChar(OneLine,Index))) ;変数など
case IDENT@LexersUtil ;変数の取り出し swbreak
;演算子
case OPE@LexersUtil ;演算子の取り出し swbreak
;実数
case REAL@LexersUtil ;実数の取り出し swbreak
;一文の終わり case DELIM@LexersUtil ;VV_index1を一つ進める swbreak
;スクリプト一行の終わり case EOS@LexersUtil default
break swbreak swend
VV_Index2+=1 ;VV_Index2を一つ進める loop
return
;エラー発生時の処理をする部分
*error
mes ErrorMessage stop
#global ;Lexerモジュールの終わり
長めですが、流れはさほど複雑では 有りません。
この部分はLexerUtil モジュールと 違い字
句
解析の本体なのでLexer モ ジュールに全体を収めました。case の部分で”XXX
@LexersUtil
” の様に書かれているものがあります。こ れはHSP では違うモジュール内で宣言
された定数・変数はこのようにしないと、未定義のものとされてしまうからです。な お、モジュール内で宣言した関数・命
令
は”@LexersUtil
”と付けなくても正しく 実行されます。流れを理解したところで、省略した部 分も実装します。
実は大雑把ver は case の内部しか 省略していません。なので以下では case の内部だけソースコードを示します。
case DELIMとcase EOS以外の case 内部は基本的にどれも同じ実装で す。特にcase IDENTと case OPEは 一行しか変わらないので、先に実装を見 せます。
//変数など
case IDENT@LexersUtil Index-=1
repeat
char=GetNextChar(OneLine,Index) if(IsIDENTsChar(char)==FALSE@Lexe rsUtil):break
var_and_vol(VV_Index1,VV_Index2) +=char
loop Index-=1 swbreak //演算子
case OPE@LexersUtil Index-=1 repeat
char=GetNextChar(OneLine,Index) if(IsOperator(char)==FALSE@Lexers Util):break
var_and_vol(VV_Index1,VV_Index2) +=char
loop Index-=1 swbreak
どうでしょうか。この二つは
IsIDENTsChar と IsOperator しか変 わりません。
それぞれの case 内部にある二つの
Index-=1
の意味がいまいち理解しにくいですが、
case の最初のものは
switch(GetCharType(GetNextChar(OneLine,Index)))
に、case の最後のものは
char=GetNextChar(OneLine,Index)
に対応しています。
case の最後にある
Index-=1
はbreakの後に実行されます。break の条件は日本語にすると「次の文字が 今扱っている種類の文字ではない」とい うようなものです。この条件判定にはもち ろん「次の文字」が必要です。それを取 得するため該当する if文の前に GetNextChar があるのですが、
GetNextChar では
Index+=1
が実行されています。このまま
switch(GetCharType(GetNextChar(OneLine,Index)))
を実行すると、この switch は 最後に var_and_vol に追加された文字の次の 次の文字、つまり二つ後の文字の種類を 判定することになってしまいます。それを 避けるため、GetNextChar で進められ た分、Indexを一つ戻しているのです。
case R
E
ALも大体処理は同じです。ですが”1ABC”や”56FG
H
”の様に、変数名(パラメータ名)の最初に数字が 付いているものはエラーにする処理が必 要です。
このようなものは switch の分岐判定 が字
句の
最初の文字によるものなので case RE
ALで扱うことになるのですが、全体としてみると変数名です。これでは 実数だと判定すべきなのか、変数名なの か、実数と変数の間にプログラマがなに か書き忘れたのか判別が出来ません。な