• 検索結果がありません。

@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_B

E

GINもDELIMIT

E

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、定数にはしていませ ん)、実数(R

E

AL)に含まれる文字が渡さ れたとき、TR

UE

を返す関数です。たと えばIsR

E

ALsChar(“1”)はTR

UE

で すがIsR

E

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と 判定されてしまいます。これではR

E

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 R

E

ALで扱うことになるのですが、

全体としてみると変数名です。これでは 実数だと判定すべきなのか、変数名なの か、実数と変数の間にプログラマがなに か書き忘れたのか判別が出来ません。な

ドキュメント内 PDFファイル 20112 ゲーム制作雑誌 がまぐ! (ページ 120-200)

関連したドキュメント