プチコン3号 20日目 SmileBASICでOOP(3)コンストラクタ・デストラクタ

18日目で関数SP_NEWCLASSを作成し、クラスを生成すると同時にインスタンスのリソースも確保しました。
今回は、インスタンスの管理(生成と廃棄)を扱います。

インスタンスの個数が状況に応じて増えたり減ったりする場合、確保したリソースを上限として、インスタンスを増やしたり減らしたりする処理が必要です。
たとえばシューティングゲームにおいて、自機は通常画面内に1機ですが、敵は新しく登場したりプレイヤーの攻撃によって消滅したりするため、数が変動します。
新しく敵を登場させるには、リソースに空きがあるかどうか確認が必要ですし、敵が消滅したらそのリソースは返却しなければなりません。

オブジェクト指向言語ではインスタンスを生成する処理をコンストラクタ、廃棄する処理をデストラクタと呼びます。
今回は、このコンストラクタとデストラクタを作ります。
といっても、実質的にはオブジェクト=リソース=スプライトですので、

・未使用のスプライトと使用中のリスプライトを、スプライト変数1番の値で区別する。
・新しくインスタンスを生成するときは、未使用のスプライトから割り当てる。
・インスタンスを廃棄する時は、割り当てていたスプライトを未使用状態に戻す。

という処理をするだけです。

スプライト変数0番はクラスに割り当てましたので、スプライト変数の利用状態は下図のようになります。

pc20-1.jpg

1ビットの情報を表現するのに、残り7つしかない変数を一つ使ってしまうのはちょっともったいない気もしますので、スプライト変数1番は、広く「オブジェクトの状態」を表す変数とします。
そして、「未使用」はその状態の1つで値は0とし、状態が0以外の値なら使用中と見なすことにします。

そのほか、スプライトは廃棄時にSPHIDEで不可視にし、生成時にSPSHOWで可視にします。
ただしクラスオブジェクトのスプライトはクラス生成時にSPHIDEします。

コンストラクタ・デストラクタの実装は以下のようになっています。

VAR _S_UNUSED:_S_UNUSED = 0
VAR _S_ACTIVE:_S_ACTIVE = 1

DEF SP_NEWOBJECT(CLASS)
VAR I
IF __I1ST[CLASS] != _S_NULL THEN
FOR I=__I1ST[CLASS] TO __ILAST[CLASS]
IF SP_GETSTATE(I)==_S_UNUSED THEN
SP_SETSTATE I, _S_ACTIVE
SPSHOW I
RETURN I
ENDIF
NEXT
ENDIF
RETURN _S_NULL
END

DEF SP_FREE SELF
SPHIDE SELF
SP_SETSTATE SELF,_S_UNUSED
END

_S_UNUSEDはリソースが未使用である状態を示す定数です。
_S_ACTIVEは、状態が_S_UNUSEDではないことを表すために便宜上定義した定数です。

SP_NEWOBJECT(CLASS)は、CLASSの新しいインスタンスを生成して返します。
リソース不足で生成できなかった場合には、_S_NULLを返します。
呼び出し側では、返り値が_S_NULLかどうかを必ずチェックしなければなりません。

SP_FREEは、オブジェクトを廃棄します。引数は廃棄するオブジェクト自身で、返り値はありません。

また、オブジェクトの状態に関して以下のようなユーティリティ関数を作成しました。

DEF SP_SETSTATE SELF,STATE
_SPVAR SELF,1,STATE
END

DEF SP_GETSTATE(SELF)
RETURN _SPVAR_(SELF,1)
END

DEF SP_ISACTIVE(SELF)
RETURN (SP_GETSTATE(SELF) != _S_UNUSED)
END

DEF SP_COUNT_INSTANCE(CLASS,STATE)
VAR I,C
C=0
IF __I1ST[CLASS] != _S_NULL THEN
FOR I=__I1ST[CLASS] TO __ILAST[CLASS]
IF SP_GETSTATE(I)==STATE THEN INC C
NEXT
ENDIF
RETURN C
END

SP_GETSTATEとSP_SETSTATEはオブジェクトの状態を取得・設定します。
SP_ISACTIVEはオブジェクトが未使用ならFALSE、使用中ならTRUEを返します。
SP_COUNT_INSTANCE(CLASS,STATE)は、CLASSのインスタンスで状態がSTATEであるものの個数を返します。
SP_COUNT_INSTANCE(CLASS,_S_UNUSED)で、あと何個インスタンスを作成可能かが分かります。

スプライト変数1番にアプリケーション本体からアクセスするのではなく、これらの関数を介してアクセスすることで、オブジェクトの実装を将来変更した場合の影響を小さくします。
例えば、現在クラスは512個しか作成できないのですから、スプライト変数0番の上位ビットに状態を表す数を設定することで、スプライト変数1番を使わないようにすることができます。
これらの関数を使っていれば、上のような変更をした場合でも、これらの関数の実装を変更するだけで対応できます。

このほか、関数SP_NEWCLASSにクラス作成時点でのオブジェクトの状態を設定するためのコードを追加しました。
赤が追加部分です。
クラスオブジェクトは使用中、インスタンスオブジェクトは全て未使用に設定します。

DEF SP_NEWCLASS(CNAME,N_INSTANCES)
VAR CLASS,OBJ
CLASS=__MAXOBJ
OBJ=SP_SETCLASS(CLASS,CLASS)
SP_SETSTATE CLASS, _S_ACTIVE
SPHIDE CLASS
INC __MAXOBJ
FOR I=1 TO N_INSTANCES
OBJ=SP_SETCLASS(__MAXOBJ,CLASS)
SP_SETSTATE OBJ, _S_UNUSED
SPHIDE OBJ
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

プチコン3号 19日目 SmileBASICでOOP(2)ポリモーフィズム

ゲームプログラムでは、例えばシューティングなら自機、敵機、自分の弾、敵の弾など、さまざまなオブジェクトが登場します。
各オブジェクトは挙動が異なりますが、一方で画面上での位置などの更新、ゲーム開始時の初期化、イベント発生時の処理(たとえば自機がやられた場合シーンの最初からリスタートするなど)など、共通性の高い面もあります。
そういった処理は、実際の処理内容が異なっても、同じ名称(「更新」「初期化」「リスタート」など)で呼ぶのが自然に思われます。

オブジェクト指向プログラミングでは、手続きや関数はオブジェクトの属性の一種なので、異なるオブジェクトが同一の名称の手続きや関数を持ち、それらの処理内容がオブジェクトによって異なるのは普通です。
よく言われる例えは

・オブジェクト「犬」が「鳴く」→ワン
・オブジェクト「猫」が「鳴く」→ニャア

というもので、異なるオブジェクト「犬」「猫」が、同じ「鳴く」という名称の処理をすることができ、その結果は犬では「ワン」猫では「ニャア」となります。

このような特性をポリモーフィズム(多相性とか多態性と訳されています)と呼びます。

SmileBASICはオブジェクト指向言語ではないので、ポリモーフィズムを備えていません。
そのため、一般的には以下の2つのどちらかの手段を使うことになります。

(1)処理「ワン」や処理「ニャア」にそれぞれ別の名称を与える

DEF 犬_鳴く
ワン
END

DEF 猫_鳴く
ニャア
END

(2)処理「鳴く」を定義し、IF文で分岐する

DEF 鳴く(動物)
IF 動物==犬 THEN ワン :RETURN
IF 動物==猫 THEN ニャア :RETURN
END

これらはどちらも利用できますが、

(1)の場合:処理の名称が多数になり、覚えにくい。呼び出し側でも使い分けが必要。
(2)の場合:動物の種類が増えるたびにIF文を追加しなければならない。

という問題があります。

実際には、(2)の方法だけでは処理本体が長くなってしまいますので、(1)と組み合わせて

DEF 鳴く(動物)
IF 動物==犬 THEN 犬_鳴く:RETURN
IF 動物==猫 THEN 猫_鳴く:RETURN
END

DEF 犬_鳴く
ワン
END

DEF 猫_鳴く
ニャア
END

のように書かざるを得ず、(1)(2)両方の問題を抱えることになります。

ポリモーフィズムはこのような問題を解決してくれます。
BASICではポリモーフィズム自体は使うことができませんが、前回から解説している「スプライトを使ったオブジェクト指向風プログラミング」では、(2)の問題についてSmileBASICの「CALL」命令を使って対策しています。

CALL命令は文字列を命令として呼び出す命令で、

CALL STRING$,パラメータ

のように使います。
例えばSTRING$の中身が”PROC”であれば

PROC パラメータ

が実行されます。
この命令により、プログラム内で生成した文字列変数を命令として呼び出すことができます。

従って、クラス名(”犬”、”猫”)と処理名(”鳴く”)が文字列で与えられれば、これらからそれぞれの処理「”犬_鳴く”」「”猫_鳴く”」を生成して、それを命令として呼び出すことが可能です。

CALLを使うと、命令を直接呼び出す場合に比べてオーバーヘッドは生じますが、実測では直接呼び出す場合と比べて20%程度の処理時間増でしたので、それほど大きなオーバーヘッドではないと思います。

実際の使用例として、プチコン3号でリアルタイムゲームを作る場合を考えます。
ゲームのメインループを極端に単純化すると、以下のようになります。

@LOOP
FOR I=0 TO __MAXOBJ-1
SP_UPDATE I
NEXT
VSYNC 1
GOTO @LOOP

ここで、SP_UPDATEが、各オブジェクトの位置などを更新する処理です。

SP_UPDATEの処理を普通に書くと先ほどの(2)のように

DEF SP_UPDATE OBJECT
VAR CLASS
CLASS=SP_CLASS(OBJECT)
IF CLASS==CLASS1 THEN CLASS1_UPDATE OBJECT
IF CLASS==CLASS2 THEN CLASS2_UPDATE OBJECT
IF CLASS==CLASS3 THEN CLASS3_UPDATE OBJECT
       :
IF CLASS==CLASSN THEN CLASSN_UPDATE OBJECT
END

となってしまいますが、前回の記事にあるクラス名の配列__CNAME$とCALL命令を使うと、これを以下のように書くことができます。
__CNAME$[]は文字列の配列で、SP_NEWCLASSのパラメータで与えたクラス名が__CNAME$[CLASS]で参照できます。

DEF _SP_BIND_METHOD(SELF,METHOD$)
VAR CLASS,FM$
CLASS=SP_CLASS(SELF)
IF CLASS == _S_NULL THEN RETURN ""
IF CLASS != SELF THEN
FM$ = METHOD$+__CNAME$[CLASS]
ELSE
FM$ = METHOD$+__CNAME$[CLASS]+"_C"
ENDIF
RETURN FM$
END

DEF _SP_CALL_METHOD0 SELF,METHOD$
VAR FM$
FM$ = _SP_BIND_METHOD(SELF,METHOD$)
IF CHKCALL(FM$) THEN CALL FM$,SELF
END

DEF SP_UPDATE SELF
_SP_CALL_METHOD0 SELF,"UPDATE_"
END

_SP_BIND_METHODは、オブジェクトと呼び出したい処理名を受け取り、オブジェクトのクラス名を処理名の後に”_”で連結した文字列を返します。
オブジェクトがクラスオブジェクトだった場合には、さらに末尾に”_C”を付加します。

_SP_CALL_METHOD0は、_SP_BIND_METHODで生成した文字列にあたる命令が存在するかどうかチェックし、存在していればその命令をパラメータ無しで呼び出します。
(「_SP_CALL_METHOD0」の名称の最後の「0」は「パラメータ無し」を表すために付けました。)

SP_UPDATEは、与えられたオブジェクトについて、_SP_CALL_METHOD0で”UPDATE”処理を行います。

これをメインループの中から呼び出すと、全てのオブジェクトについて、そのオブジェクトの所属クラスに応じて”クラス名_UPDATE オブジェクト”という命令が実行されます。

この方法の良いところは、クラスを追加したときに、”クラス名_UPDATE””クラス名_UPDATE_C”という命令をDEFで定義しておけば、その命令はメインループから勝手に呼び出されるという点です。
メインループには何も書き加える必要がありません。

なお、本当は”クラス名_UPDATE”というDEFを書くよりも、例えばrubyのように

class 犬
def 鳴く
end
end

class 猫
def 鳴く
end
end

という記述をしたいところです。
ソースコード操作関連の命令を使えば、この記述をSmileBASICでの記述に変換するようなプリプロセッサを作ることは可能かもしれません。

プチコン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