2015年02月05日

プチコン3号 22日目 SmileBASICでOOP(5)クラスオブジェクトの使い方

pc22-1.jpg

前回のサンプルで、シューティングゲームにおける自機、および自機の弾の処理をOOP風に記述してみました。
今回は同じシューティングゲームでの敵機と敵の弾の処理をOOP風に記述してみます。

敵機の動作は、

・画面上端にランダムに出現
・真下へ向かって移動する
・Y座標がプレイヤー機以上になったら、プレイヤー機へ近づく方向への水平移動に切り替える

とします。
(この動作が初めてゲームに登場したのは1984年のテーカン「STAR FORCE」だったと思います。)

また、敵弾は敵機が下へ向かって移動している間だけ、プレイヤー機に向けてランダムに発射することにします。
pc22-2.jpg

以上を実現するために、「敵オブジェクト」「敵弾オブジェクト」の2つのクラスを新たに追加します。
メインプログラムのクラス生成を行う部分は以下のようになります。
赤が前回からの追加部分です。
VAR _MY,_SHOT,_ENEMY,_ESHOT

_MY =SP_NEWCLASS("MY", 1)
_SHOT =SP_NEWCLASS("SHOT", 5)
_ENEMY =SP_NEWCLASS("ENEMY", 20)
_ESHOT =SP_NEWCLASS("ESHOT", 20)


VAR I

MY_NEWSHIP
WHILE TRUE
FOR I=0 TO __MAXOBJ-1
IF SP_ISACTIVE(I) THEN SP_UPDATE I
NEXT
VSYNC 1
WEND

敵機と敵弾は、それぞれ最大20個にしました。

次に、敵機の生成です。

敵機の生成は、自機の生成と同様にメインループの中で行うこともできます。
しかし、できるだけ関連するコードを一箇所に集めたほうがプログラムが見やすいので、敵機の生成はクラスオブジェクトで行うことにします。

敵機のクラスを生成した直後は、敵機のインスタンスは未生成状態で、クラスオブジェクト(_ENEMY)のみが存在しています。
クラスオブジェクトはアクティブな状態なので、SP_UPDATEからクラスオブジェクトのメソッドUPDATE_ENEMY_Cがメインループの中で呼び出されます。
敵機の生成処理は、このUPDATE_ENEMY_Cの中で行います。

なお「UPDATE_クラス名_C」は、クラスオブジェクトのメソッドで、インスタンスオブジェクトのメソッド「UPDATE_クラス名」とは異なります。
(詳しくは19日目の記事を参照してください。)
1つのクラスには、クラスオブジェクトは1つしかありませんので、UPDATE_クラス名_Cはループの中で必ず1度だけ実行されます。

UPDATE_ENEMY_Cの実装は以下のようになります。
(以降、OOP関連の命令を赤で、それ以外のユーザ定義命令を青で示します。)
DEF UPDATE_ENEMY_C SELF
VAR SP
IF MAINCNT MOD 20 != 0 THEN RETURN
SP=SP_NEWOBJECT(SELF)
IF SP != _S_NULL THEN
SPCHR SP,3251
SPOFS SP,50+RND(_SCW-100),-8
SPANIM SP,"XY+",-180,0,256,1
SPANIM SP,"I+",5,0,5,1,5,2,0
ENDIF
END

MAINCNTは経過時間を1/60秒単位で示すシステム変数です。
UPDATE_ENEMY_Cは20/60秒すなわち1/3秒に1回、敵機オブジェクトを生成し、移動アニメーションとキャラクタアニメーションを設定します。
移動アニメーションは、180ticks(3秒)で256ピクセルだけ画面下方へ移動するようになっています。


さて、敵オブジェクトが生成されると、今度は敵オブジェクトのメソッドUPDATE_ENEMYがメインループからSP_UPDATE経由で毎回呼び出されます。

敵機の状態は、「下方向へ移動」と、「水平方向へ移動」の2つがあります。
これらを、オブジェクトの状態を表すために使っているスプライト変数1番の値の違いで区別することにします。

オブジェクト生成直後は変数値は_S_ACTIVEですので、これを下方向へ移動中の状態と考えます。
水平方向へ移動中は、「変数値が_S_ACTIVE+1」とします。
(もう少し状態が多い場合は、状態一つ一つにそれぞれ個別の変数名を定義したほうが良いでしょう。)

敵機のY座標がプレイヤー機のY座標以上になると、移動をプレイヤー機方向への水平移動に切り替えます。
また、下方向へ移動中の場合は、確率的に敵弾を発射します。

以上の処理を行うUPDATE_ENEMYの実装は以下のようになります。

GET_MY_POSは自機のXY座標を返す手続きです。
自機のオブジェクトはクラス_MYの最初のインスタンスである__I1ST[_MY]になります。
(本当は、このアクセサは_MYのクラスメソッドか、せめて_ENEMYのクラスメソッドとして定義すべきですね。)
自機オブジェクトがアクティブでないときは、(-100,-100)を返します。
DEF GET_MY_POS OUT X,Y
VAR SP
SP=__I1ST[_MY]
IF SP_ISACTIVE(SP) THEN
SPOFS SP OUT X,Y
ELSE
X=-100:Y=-100
ENDIF
END

DEF UPDATE_ENEMY SELF
VAR X,Y,MX,MY,A
SPOFS SELF OUT X,Y
IF Y > _SCH THEN SP_FREE SELF:RETURN
IF X < 0 || X > _SCW THEN SP_FREE SELF:RETURN
IF SP_GETSTATE(SELF) == _S_ACTIVE THEN
GET_MY_POS OUT MX,MY
IF Y >= MY THEN
SPANIM SELF,"XY+",-150,SGN(MX-X)*400,0,1
SP_SETSTATE SELF,_S_ACTIVE+1
ELSE
IF RND(100)==0 THEN
A=ATAN((MX-X)/(MY-Y))
NEW_ESHOT X,Y,SIN(A)*500,COS(A)*500,150
ENDIF
ENDIF
ENDIF
END

Y座標が画面下端に達したか、あるいはX座標が画面右端または左端に達したら、SP_FREEでオブジェクトを廃棄します。

Y座標がプレイヤー機のY座標より小さい場合は、1/100の確率で敵弾を生成します。
(小さい確率に思えるかもしれませんが、毎秒60回×敵機の数だけ実行されますので、平均して毎秒数発は生成される計算になります。)

敵弾の移動方向は、自身から見たプレイヤー機の方向です。
方向は座標を元に三角関数ATANで求め、さらにSINとCOSでその方向のX成分とY成分を求めています。
pc22-3.jpg

敵弾のインスタンス生成は関数NEW_ESHOTで行います。
この処理は前回の自機の弾発射と同じです。
敵弾のUPDATE処理も、画面外へ出たらオブジェクトを廃棄するだけです。
実装は以下の通りです。
DEF NEW_ESHOT X,Y,DESTX,DESTY,TICK
VAR SP
SP=SP_NEWOBJECT(_ESHOT)
IF SP != _S_NULL THEN
SPCHR SP,3372
SPOFS SP,X,Y
SPANIM SP,"XY+",-TICK,DESTX,DESTY,1
SPANIM SP,"I+",5,0,5,1,5,2,5,3,0
ENDIF
END

DEF UPDATE_ESHOT SELF
VAR X,Y
SPOFS SELF OUT X,Y
IF Y < 0 || X < 0 THEN SP_FREE SELF
IF Y > _SCH || X > _SCW THEN SP_FREE SELF
END


以上で、敵機と敵弾を作ることができました。

ただ、ゲームとして成立させるためには、まだ当たり判定がありません。
当たり判定は次回に解説します。
posted by boochow at 01:17| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年02月03日

プチコン3号 21日目 SmileBASICでOOP(4)サンプル

pc21-1.jpg

前回までで、オブジェクト指向(風)プログラミングに必要な関数は一通りそろいましたので、使い方をサンプルでお見せします。
例題は

・スライドパッドで自機移動
・Aボタンで弾発射

という、作りかけのシューティングゲームみたいなものです。

プチコン3号本体の命令としては、3日目で書いたボタン入力、15日目で書いたスプライトのアニメーション(移動)を使います。
今回は特に説明はしませんので、上記の記事を参照してください。

おさらいになりますが、オブジェクト指向用の関数・命令として以下のものがあります。
詳細は以前の記事を参照してください。

・クラス作成
SP_NEWCLASS("クラス名称",インスタンスの最大数)
返り値:クラスオブジェクト(作成失敗時は_S_NULL)

・オブジェクト生成
SP_NEWOBJECT(クラスオブジェクト)
返り値:インスタンスオブジェクト(作成失敗時は_S_NULL)

・オブジェクト廃棄
SP_FREE インスタンスオブジェクト

・オブジェクトの状態チェック
SP_ISACTIVE(オブジェクト)
返り値:アクティブ(表示等更新要)ならTRUE、非アクティブならFALSE

・オブジェクトの表示等更新処理
UPDATE_クラス名称 オブジェクト
(SP_UPDATEから呼び出される)

今回のサンプルでは、「自機」「弾」の2つのクラスを作成します。

自機のインスタンスは、プログラム開始時に1つ生成します。
自機はスライドパッドで移動し、Aボタンで弾を発射します。

弾のインスタンスは、自機が生成します。
弾はアニメーションで勝手に画面外へ向かって飛んでいきます。
弾オブジェクトは、毎回座標をチェックし、画面外へ出ていたら自らを廃棄します。
pc21-2.jpg

メインプログラムは以下のようになります。
VAR _MY,_SHOT

_MY =SP_NEWCLASS("MY", 1)
_SHOT =SP_NEWCLASS("SHOT", 5)

VAR I

MY_NEWSHIP
WHILE TRUE
FOR I=0 TO __MAXOBJ-1
IF SP_ISACTIVE(I) THEN SP_UPDATE I
NEXT
VSYNC 1
WEND

まず自機のクラス"MY"と弾のクラス"SHOT"を生成し、それぞれクラスオブジェクトを_MYと_CLASSに代入しています。
自機のインスタンスは最大1、弾のインスタンスは最大5つのリソースを確保しています。
(本来ここで_MYや_SHOTが_S_NULLになっていないかチェックが必要ですが、今回は省略します。)

MY_NEWSHIPは_MYのインスタンスを生成する命令ですが、後で説明します。

ループの中ではアクティブな全てのオブジェクトの更新を行っています。
この部分は作成した全てのクラスで有効です。
クラス固有の処理はUPDATE_クラス名のほうで記述します。

自機のクラス_MYに関連する処理は以下のようになります。
DEF MY_NEWSHIP
VAR SP
SP=SP_NEWOBJECT(_MY)
IF SP != _S_NULL THEN
SPCHR SP,3311
SPOFS SP,_SCW>>1,200
ENDIF
END

DEF UPDATE_MY SELF
VAR SX,SY,X,Y,B
STICK OUT SX,SY
SPOFS SELF OUT X,Y
X=MIN(_SCW-8,MAX(X+SX*8,8))
Y=MIN(_SCH-8,MAX(Y-SY*8,100))
SPOFS SELF,X,Y,1

B=BUTTON(1)
IF B AND 16 THEN
NEW_SHOT X,Y,60
ENDIF
END

MY_NEWSHIPで_MYの新しいインスタンスを生成しています。
オブジェクトは実際にはスプライト管理番号ですので、生成したオブジェクトをそのままスプライト管理番号として、キャラクタと座標を設定しています。
_SCWは掲載していない部分で定義しており、スクリーンの横方向ピクセル数-1(399)です。

メインプログラムのSP_UPDATEを介して、UPDATE_MYが呼び出されます。
UPDATE_MYは、クラス_MYのインスタンスメソッドの位置づけです。

インスタンスメソッドの引数SELFにはオブジェクト自身が入っています。
このオブジェクトがクラス_MYのインスタンスであることは保証されています。
オブジェクト指向言語では、SELFはパラメータではなく主体なので、SELF.SP_UPDATEというような書き方をしますが、BASICではパラメータにせざるを得ません。

UPDATE_MYの処理はスライドパッドによる自機の移動と、Aボタンを押したときの弾の発射です。
弾オブジェクトの生成はNEW_SHOTで行っています。
NEW_SHOTのパラメータは初期座標とY軸方向の移動速度の逆数(240ピクセル移動するのに要する時間を1/60秒単位で示した数。60なら1秒で240ピクセル移動)です。

弾のクラス_SHOTに関するコードを以下に示します。
DEF NEW_SHOT X,Y,SPEED
VAR SP
SP=SP_NEWOBJECT(_SHOT)
IF SP != _S_NULL THEN
SPCHR SP,3353
SPOFS SP,X,Y
SPANIM SP,"XY+",-SPEED,0,-240,1
ENDIF
END

DEF UPDATE_SHOT SELF
VAR X,Y
SPOFS SELF OUT X,Y
IF Y < 0 THEN SP_FREE SELF
END

NEW_SHOTで弾のクラス_SHOTの新しいインスタンスを生成しています。
そして、座標とアニメーションの設定を行っています。

メインプログラムのSP_UPDATEを介して、UPDATE_SHOTが呼び出されます。
UPDATE_MYはクラス_SHOTのインスタンスメソッドです。

弾オブジェクトはSPANIMで移動させていますので、移動処理は不要です。
現在の座標を取得して、Yが負(画面上端に到達)ならばオブジェクトを廃棄しています。

クラス_SHOTの生成時に、インスタンス数を最大5に指定していますので、弾は5発まで連射可能です。
5発とも画面内に存在している間は、NEW_SHOTからSP_NEWOBJECTを呼び出したときに_SP_NULLが返りますので、新しい弾オブジェクトを生成できません。

「OOP風」プログラムのサンプルは以上です。
「BASICで無理やりOOPをやろうとしている」という雰囲気は感じていただけたでしょうか。
posted by boochow at 00:13| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年02月01日

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

posted by boochow at 11:51| Comment(3) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年01月30日

プチコン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での記述に変換するようなプリプロセッサを作ることは可能かもしれません。
posted by boochow at 00:49| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年01月25日

プチコン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
posted by boochow at 20:28| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする
人気記事