プチコン3号 18日目 SmileBASICでOOP(1)スプライトをオブジェクトと見なす

今回からしばらく初心者向けに詳しく書くのはお休みして、通常運転で行きたいと思います。(内容が、あまり初心者向けではありませんので・・・)

これまで1ヶ月あまりSmileBASICを使ってきましたが、やはりBASICはプログラミング言語としては記述能力が限定されており、プログラミングするのはストレスがたまります。
本当はこう書きたいんだけれどBASICの文法ではこのようにしか表現できない! という状況がよく起こります。

SmileBASICはDEF文で手続きや関数が記述でき、OUT記法で複数の値を返すこともできます。
そのため、昔のBASICよりはプログラムをモジュール構造にしやすいのですが、データの表現として構造体を使うことができないため、データ間の関係をプログラムで明示的に扱うことが難しくなっています。

例えば14日目で、複数の円が部屋の中を跳ね回るプログラムを書きましたが、これなどは「中心座標」「半径」「色」「初速」「現在速度」等をまとめて構造体で表現し、複数の円はその構造体の配列として表現したいところです。

しかし実際には「Xの配列」「Yの配列」「半径の配列」というように個別の変数の配列とするか、必要なデータをすべて1つの配列に集めて「配列の先頭はX座標」「2番目はY座標」「3番目は半径」・・・というルールを決め、配列の配列=2次元配列というデータ構造にせざるを得ません。

いずれにしても、データ間の関係はプログラマの頭の中には存在していますが、BASICではそれを素直にプログラムとして表現するのは困難であり、コメントとして記述するか、あるいはこのブログのように別の文書で記述しなければならない、いわゆる「可読性が低い」プログラムになってしまいます。

また、データ構造に上記のような制約があるため、プログラムも

・配列を多用することになり、プログラムは配列の添え字だらけ
・似た処理を行う文(X座標の加算とY座標の加算とか)が並ぶ
・入れ子になったFOR~NEXTループ
・引数がやたらに多いユーザ定義関数

という、重複した表現の多い見た目になってしまいがちです。

以上のような問題点を、スプライト変数を使うことで多少なりとも軽減できないか・・・ということを最近試しています。
スプライト変数は、スプライトに8つの変数を保存できる仕組みで、

書き込み: SPVAR Sprite,Index, Value
読み出し: SPVAR(Sprite,Index)

という形式でアクセスできます。
(値は数値のみで、配列や文字列を代入することはできません。)

スプライト変数は、見方によっては、スプライトを「8つのメンバ変数を持つ構造体」として扱っていることになります。
変数名はインデックス番号になりますが、DEF文でラップしてやれば、インデックス番号の代わりに変数名でアクセスすることも可能です。

さらに、8つの変数のうちの1つを、「オブジェクトのクラス」を表すために使えば、擬似オブジェクト指向プログラミングができます。
例えば、ゲームのメインループの中で各スプライトの処理を、スプライト管理番号ではなくクラスに応じて行うことができます。
端的に言えば、あるスプライトが敵なのかプレイヤーなのか、あるいは敵の弾丸なのか、それはそのスプライト自身が知っていて、処理方法もスプライト自身から知ることができる、ということになります。

もちろんスプライト変数へのアクセスは、通常の変数へのアクセスに比べて若干オーバーヘッドがあります。
実際に計測してみたところ、スプライト変数は通常の変数よりも3倍弱の処理時間がかかりました。
とはいえプチコン3号の処理速度は十分に速いので、変数アクセスのオーバーヘッドは、処理全体の中では大きな問題にはならないように思います。

スプライトは512個しか存在しないので、スプライトを各クラスのオブジェクトに割り当てるリソース管理も重要です。
大きく分けて、

・動的管理: 必要なときに必要な量を割り当て、不要になったものは返却
・静的管理: あらかじめ決まった量を割り当て、その中でやりくり

という2つの方針があります。
動的管理のほうが汎用性があり、そのぶん複雑な処理になりますが、その代わりアプリケーション本体の処理は楽になります。
といっても、512個くらいなら静的管理で詳細はアプリケーション側でも管理しきれるような気がします。

というようなことを考えて、スプライトをクラスとして扱うための以下のような関数群を作ってみました。

VAR __MAXOBJ
DIM __I1ST[512],__ILAST[512],__CNAME$[512]

DEF SP_NEWCLASS(CNAME,N_INSTANCES)

DEF SP_CLASS(SELF)

これらの関数群は、スプライトをオブジェクトとして扱います。
スプライト管理番号は、オブジェクトを指すポインタとなります。

SP_NEWCLASSは新しいクラスを作成する関数です。
利用形態は

CLASS = SP_NEWCLASS(“CLASS_NAME”,K)

というようになります。
第一引数はクラス名、第二引数は確保するインスタンスの個数、返り値は生成したクラスです。

第二引数があることから分かるように、リソース管理は静的な割り当てで、クラスを生成する時点でK個分のインスタンスも生成していることになります。
ただしクラス設定以外の初期化はしていませんので、使用する前に設定は必要です。

クラスに属するオブジェクトには、クラスオブジェクトとインスタンスオブジェクトがあります。
インスタンスオブジェクトは、例えば1体の敵、1つの弾、など個別のモノを表します。
クラスオブジェクトは、そのクラス自体を表す抽象的なオブジェクトです。

クラスオブジェクトが、各クラスのインスタンスの管理を行うという想定をしています。
例えば敵の出現や弾の発射などの、インスタンスの生成処理を行います。

オブジェクトはすべて、所属クラスへのポインタを持ちます。
これは関数SP_CLASS()で取得できます。
実装上は、所属クラスはスプライト変数の0番へ格納されています。
クラスオブジェクトの所属クラスは自分自身になります。
つまりクラスオブジェクトは所属クラスが自分自身であるような特殊なオブジェクトです。

以上を図にすると、下図のようになります。

pc18-1.jpg

オブジェクトは実際にはスプライト管理番号ですので、Nは0~511の整数です。
この整数を、ここでは「オブジェクトへのポインタ」と見なしています。

クラスを生成すると、N番のスプライトがクラスオブジェクトになります。(NはSP_NEWCLASS関数が内部で決めます。)
同時に、そのクラスのK個のインスタンスオブジェクトが生成されます。
現在は、N+1~N+K番のスプライトがインスタンスになるような実装をしています。

これらのオブジェクトはスプライト変数0の値がNになっており、それによって所属クラスを識別できます。
残りのスプライト変数1~7は、クラス変数またはインスタンス変数として利用できます。

スプライトの管理はアプリケーション側で行いますが、効率的に管理するためには、クラスのインスタンスになっているスプライト管理番号を取得できることが必要です。
その情報は、配列__I1ST[512]と__ILAST[512]に格納されています。
クラスがNであれば、インスタンスの先頭は__I1ST[N]、末尾は__ILAST[N]となります。

また、オブジェクトとして使うスプライトは0番から順に確保しますが、確保された最後のオブジェクトのスプライト管理番号+1がグローバル変数__MAXOBJに格納されています。
これは、全てのオブジェクトを対象とした処理を行いたいときに使います。

参考までに、SP_NEWCLASSの実装を以下に掲載しておきます。
内部で呼び出しているSP_SETCLASS(S,C)は、SPVAR S,0,Cを実行し、Sを返す関数です。
また、SP_NEWCLASSを呼び出す前に

__INITIALIZE 511

でスプライトを初期化しなければなりません。(511はオブジェクト定義に使用する最大のスプライト管理番号。)
スプライト変数を設定するには、SPSETでスプライトを初期化する必要があるためです。
なお_S_NULLは定数で、値は何でも良いのですが現在の実装では負の大きな数にしています。

DEF SP_NEWCLASS(CNAME,N_INSTANCES)
VAR CLASS,OBJ
CLASS=__MAXOBJ
OBJ=SP_SETCLASS(CLASS,CLASS)
INC __MAXOBJ
FOR I=1 TO N_INSTANCES
OBJ=SP_SETCLASS(__MAXOBJ,CLASS)
INC __MAXOBJ
NEXT
__CNAME$[CLASS]=CNAME
IF N_INSTANCES > 0 THEN
__I1ST[CLASS]=CLASS+1
__ILAST[CLASS]=CLASS+N_INSTANCES
ELSE
__I1ST[CLASS]=_S_NULL
__ILAST[CLASS]=_S_NULL
ENDIF
RETURN CLASS
END

DEF __INITIALIZE MAXOBJ
VAR I,S
FOR I=0 TO MAXOBJ
SPSET I,32
S=SP_SETCLASS(I,_S_NULL)
NEXT
FOR I=0 TO 511
__I1ST[I]=_S_NULL
__ILAST[I]=_S_NULL
NEXT
END

コメント