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) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年01月18日

プチコン3号 17日目 三角関数でスプライトを動かす&星拾いゲーム完成!

pc17-1.jpg

前回のプログラムに星を降らせる部分と星を拾う部分を追加して、ゲームは完成です。
星は、画面内に妖精を飛ばせて、その妖精から落とさせます。
プレイヤーが星を拾ったら、次の星を降らせるようにします。
弾む円は最初は数を少なくして、プレイヤーが星を取るたびに円を1つずつ増やしていきます。

また、ゲームの終了処理も少し追加します。
迷路探検ゲームのときはゴールすればそのまま終了にしていましたが、今回はプレイヤーがやられた後、円の弾む動きを続行し、画面外へすべての円が出て行くのを待ってゲーム終了にしてみました。

星の動きのアニメーションは、15日目で作成した「落ちた後2回弾んで止まる」という動きを使います。
また、星を落とす妖精はスプライトで表示します。

星の動きのアニメーションは、開始地点のY座標が50のときに画面下端まで落ちるように設計しています。
従って、妖精の動きは下図のように、画面上端から50ピクセルの位置で、左右に往復するようにします。
また、落とした星が画面外に出ないように、星のアニメーション開始のX座標は画面中央100ピクセルの範囲とします。

妖精の移動範囲はこれよりも広い幅300ピクセルの範囲としました。
これで、プレイヤーが星を取ってから、次の星が降ってくるまでのタイミングに変化が生じます。
pc17-2.jpg

妖精が左右に行ったり来たりする動きは、振り子の動きのように、端へ近づくほどスピードを落とすと雰囲気が出ます。
そのような動きをさせるために、今回は三角関数の一つであるサイン関数を使います。

サイン関数は角度をパラメータとする関数で、その値は下のグラフのように-1〜1の範囲を往復します。
これを妖精の動きのX座標に使えば、振り子が往復するような動きをさせることができます。
なお、横軸の単位は円周率πです。グラフの右端はθ=2ですが、これはθ=2πと読み替えてください。
θが0、π、2πのときsin(θ)=0、θがπ/2のときsin(θ)=1、θが3π/2のときsin(θ)=-1となります。
pc17-3.jpg


以上の処理を行うプログラムは以下のようになります。(今回はプログラム全体は大きいので、一部分だけ掲載します。)
赤字の箇所が妖精の処理、青字が星の処理です。
ACLS
_MAN=0:_FRY=1:_STAR=2
GPRIO 1
'PLAYER
MANX=200
SPSET _MAN,1128
SPCOL _MAN,12,16
SPANIM _MAN,"I",9,1128,9,1129,9,1130,9,1131,0
'FAIRY
FRYT=0
SPSET _FRY,900
SPHOME _FRY,8,8
SPSCALE _FRY,1.5,1.5
SPANIM _FRY,"I",15,900,15,901,0
'STAR
SPSET _STAR,226
SPCOL _STAR,16,16
SPANIM _STAR,"C",4,&HFFFFFFFF,4,&HFF808080,0


NOSTAR=TRUE
SCORE=0
BGMPLAY 39
N=1 'NUMBER OF BALLS-1
HITX=6:HITY=7 'PLAYER HIT TEST AREA
HITW=16-HITX*2:HITH=16-HITY
DIM PXL[HITW*HITH]
@LOOP
GSAVE MANX+HITX,224+HITY,HITW,HITH,PXL,1
HIT=0
FOR I=O TO HITW*HITH-1
HIT=HIT OR PXL[I]
NEXT
IF HIT!=0 THEN GOTO @EXIT

STICK OUT SX,SY
MANX=MANX+SX*4
MANX=MIN(384,MAX(0,MANX))
SPOFS _MAN,MANX,224,0

FRYT=FRYT+.02
FRYX=200+SIN(FRYT)*150
IF FRYT > 2*PI() THEN FRYT=0
SPOFS _FRY,FRYX,50,-1

IF NOSTAR && 150 < FRYX && FRYX < 250 THEN
NOSTAR=FALSE
SPOFS _STAR,FRYX,50,0
IF PI()/2 < FRYT && FRYT < 3*PI()/2 THEN
SPANIM _STAR,"XY+",LFALL,1
ELSE
SPANIM _STAR,"XY+",RFALL,1
ENDIF
ENDIF

IF SPHITSP(_MAN,_STAR) THEN
N=MIN(NMAX-1,N+1) 'INCREASE BALLS
SPOFS _STAR,-100,-100 'HIDE THE STAR
NOSTAR=TRUE
BEEP 12
SCORE=SCORE+1
LOCATE 0,0
COLOR 7
PRINT "★"*SCORE
COLOR 15
ENDIF


最初に、プレイヤー・妖精・星のスプライト番号を変数に代入しています。
これはプログラムを読みやすくするためで、BASICには名前つき定数が無いので変数を使っています。
_MAN=0:_FRY=1:_STAR=2

妖精の位置を表す変数はFRYTです。
これは座標ではなく、サイン関数に渡すための角度です。
SPHOMEでスプライトの原点をスプライトの中心とし(通常は原点は左上)、スプライトのサイズを1.5倍にしています。

ループの中で変数FRYTを0.02ずつ大きくし、2πを超えたら0にリセットしています。
FRYT=FRYT+.02
FRYX=200+SIN(FRYT)*150
IF FRYT > 2*PI() THEN FRYT=0
X座標FRYXは、三角関数でX=200からプラスマイナス150ピクセルの範囲で変化させています。

星のスプライトは、SPCOLで当たり判定を有効にしています。プレイヤーのスプライトもSPCOLを追加しています。
そして、星が画面内に表示されている状態かどうかを表す変数NOSTARを用意します。
星の出現処理は、NOSTARがTRUEのときのみ行います。
星を出現させたらNOSTARをFALSE、プレイヤーが星を取ったらNOSTARをTRUEにします。

星の出現処理は、ループの中の
IF NOSTAR && 150 < FRYX && FRYX < 250 THEN
という条件が満たされたとき実行します。
出現地点は妖精の現在位置です。
また、アニメーションは「右に落ちる動作(RFALL)」「左に落ちる動作(LFALL)」の2種類を用意し、妖精の移動方向に合わせて選択しています。

プレイヤーが星を取ったかどうかは
SPHITSP(_MAN,_STAR)
で判定します。
衝突していれば円を増やし、次の星を出現可能にし、スコア表示を更新します。


円とプレイヤーが衝突するとラベル@EXITへジャンプします。
それ以降がゲーム終了処理です。
円が左右ではね返る処理を無くし、すべての円が画面から出てくるまで待ってからプログラムを終了します。
プログラムはこちらです。
@EXIT
SPCHR _MAN,1141
BEEP 13
REPEAT
DONE=TRUE

GCLS
FOR I=0 TO N
GCIRCLE X[I],Y[I],R[I],C[I]
GPAINT PAINTX(X[I],R[I]),Y[I],C[I],C[I]
V[I]=V[I]+G
IF Y[I]+V[I] > SCH-R[I] THEN
V[I]=V0[I]
Y[I]=SCH-R[I]
ENDIF
INC Y[I],V[I]
INC X[I],DX[I]
IF -R[I] < X[I] && X[I] < SCW+R[I] THEN
DONE=FALSE
ENDIF

NEXT
VSYNC 1
UNTIL DONE

DEF PAINTX(X,R)
IF X < 0 && (X+R) > 0 THEN RETURN 0
IF X > SCW && (X-R) < SCW THEN RETURN SCW
RETURN X
END


BGMSTOP
ACLS

REPEAT文の内側の処理は、メインのループの中での処理とほとんど同じですが、一部だけ違っています。

まず、円が画面から半分以上出た時のPAINTの処理です。
メインループでは、PAINTの開始点を円の中心にしています。
下図左のような状態のときはメインループと同様の処理で問題ありませんが、下図右のようになった場合、円の中心が画面の外にあるため、PAINTが実行されません。
pc17-4.jpg

このような場合のPAINT開始点を求める関数PAINTXを定義して使っています。
Y座標は円の中心のY座標ですので、必要なのはX座標だけです。
また、X座標は円が画面左から画面外へ出て行く場合はスクリーン左端(0)、画面右から画面外へ出て行く場合はスクリーン右端(399)となります。

次に、全ての円が画面外へ出たかどうかの判定です。
これはFOR〜NEXTループ内で
IF -R[I] < X[I] && X[I] < SCW+R[I] THEN
のところで判断しています。
この条件を満たす円は、スクリーンに残っていると判断されます。
FOR〜NEXTループに入る前に変数DONEにTRUEを代入し、上記の条件を満たす円があったときはDONEにFALSEを代入します。
すべての円の処理が終わったとき、DONEがTRUEであれば、全ての円が画面外に出たことになります。

今回のプログラムはMiiverseで公開しています。
公開キーは【KKQEA1】です。
単純なゲームですが、BGMや効果音もつけて、それなりに遊べるゲームになったと思います。
posted by boochow at 23:07| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

プチコン3号 16日目 グラフィックスの当たり判定

今回と次回で、14日目の「グラフィックスで描いた弾む円」と15日目の「弾むスプライト」を組み合わせたゲームを作ってみます。
プレイヤーが円を避けながら左右に走って、降ってくる星を集めるゲームです。
円にぶつかったらゲームオーバーです。
pc16-1.jpg

これには、グラフィックスで描いている円と、スプライトで動かしているプレーヤーキャラクタとの衝突判定が必要になりますので、今回はその部分を作りました。

スプライト同士の衝突はSPHITSP命令がありますが、グラフィックスでの衝突判定は、以下の2つの方法が考えられます。

(1)幾何学的判定
円同士の衝突の場合は、
2つの円の中心間の距離 < 2つの円の半径の和
となっていれば2つの円が重なっていることが言えますので、これで判定できます。
この場合は、プレイヤーキャラクタの衝突判定領域を円で近似する必要があります。
円と四角形の衝突も、計算が面倒そうですが、幾何学的な判定は可能です。

(2)画像による判定
これは実際に描かれたグラフィックスから、衝突判定したい領域の画像を切り出し、その画像に対象の画像が含まれているかどうかで判定する方法です。
直感的には判りやすいですが、実際には対象の画像かどうかは「色」で判断するしかありません。従ってグラフィックスに複雑な画像が描かれている場合は、誤判定が起こりやすくなります。
pc16-2.jpg

一般的には、画像による判定は、画面が複雑になればなるほど困難になりますし、ハードウェアに依存する度合いも高くなるので、可能なら幾何学的に判定するほうが良いでしょう。

しかし今回は、画像による判定を行いました。
グラフィックスには円の画像しか描かれていませんので、単純に領域内のピクセルが「黒か黒でないか」だけで衝突の有無を判定できるからです。

SmileBASICには、このような目的にGSPOIT命令やGSAVE命令が使えます。
GSPOIT命令は、指定した座標のピクセルの色を読み取ります。
GSAVE命令は、指定した矩形の領域の画像を配列にコピーします。
GSAVE命令では1ピクセルが配列の1要素に入りますので、たとえば16×16ピクセルの領域の画像は、サイズが256の配列にコピーされます。
画像を配列に変換することで、より複雑な処理をすることができます。

今回は、ある領域を指定して衝突判定をしますので、GSAVE命令を使います。
調べたい領域のピクセルを配列に変換し、次に配列の要素の中に0(黒のピクセル)でないものがあるかどうかを調べます。

それではプログラムです。
14日目のプログラムとかなり重複がありますので、追加・修正部分に全て色をつけました。
NMAX=9:G=0.1:SCW=400:SCH=240
DIM X[NMAX+1],Y[NMAX+1],R[NMAX+1]
DIM V[NMAX+1],V0[NMAX+1],DX[NMAX+1],C[NMAX+1]
FOR I=0 TO NMAX
R[I]=10+RND(40)
X[I]=R[I]+RND(SCW-2*R[I]-20)+10
V0[I]=-(4+RND(20)/10)
Y[I]=SCH-R[I]-16
V[I]=V0[I]
DX[I]=(RND(2)*2-1)*(RND(3)+1)
C[I]=RND(&HFFFFFF) OR &HFF1F1F1F
NEXT

ACLS
GPRIO 1

MANX=200
SPSET 0,1128
SPCOL 0,12,16
SPANIM 0,"I",9,1128,9,1129,9,1130,9,1131,0

N=5
HITX=6:HITY=7
HITW=16-HITX*2:HITH=16-HITY
DIM PXL[HITW*HITH]
@LOOP
GSAVE MANX+HITX,224+HITY,HITW,HITH,PXL,1
HIT=0
FOR I=O TO HITW*HITH-1
HIT=HIT OR PXL[I]
NEXT
IF HIT!=0 THEN GOTO @EXIT

STICK OUT SX,SY
MANX=MANX+SX*4
MANX=MIN(384,MAX(0,MANX))
SPOFS 0,MANX,224,0

GCLS
FOR I=0 TO N
GCIRCLE X[I],Y[I],R[I],C[I]
GPAINT X[I],Y[I],C[I],C[I]
V[I]=V[I]+G
IF Y[I]+V[I] > SCH-R[I] THEN
V[I]=V0[I]
Y[I]=SCH-R[I]
ENDIF
INC Y[I],V[I]
INC X[I],DX[I]
IF X[I] > SCW-R[I] || X[I] < 1+R[I] THEN
DX[I]=-DX[I]
ENDIF
NEXT
VSYNC 1
GOTO @LOOP
@EXIT

ちょっと長いですが、今回のメインは青い部分です。

赤字の部分は変数名の変更や細かい修正です。
たとえば、円の中心座標は、スタート時にいきなりスプライトと重なってしまわないように、16ピクセル上に動かしています。
「GPRIO 1」はグラフィックス画面のZ軸の指定です。スクリーン(Z=0)のすぐ後にしています。
また、円が弾む際の画面下端との接触処理もより厳密にしています。

紫の部分は、プレイヤーのキャラクタを表示するスプライトの初期設定と、スティックでの操作です。
ここは3日目に説明しましたので省略します。
スプライトがグラフィックス画面のすぐ手前に表示されるよう、SPOFSでZ値を0に指定しています。

グラフィックスの当たり判定関連の部分は、青字にしています。
ループの外側は初期設定です。
HITX=6:HITY=7
HITW=16-HITX*2:HITH=16-HITY
それぞれの変数の意味は下図の通りです。
スプライトの16×16ピクセルの領域のうち、青枠で囲った部分だけを当たり判定の対象にします。
pc16-4.jpg

青枠の部分が狭いほど、当たり判定が甘く(当りにくく)なります。
画面下端付近では円の落下速度が速いため、やや甘い当たり判定にしてあります。

その次のDIM文で宣言している配列PXLを、画像を読み込むために使います。
実際の読み込みはループの開始直後、
GSAVE MANX+HITX,224+HITY,HITW,HITH,PXL,1
の部分です。
パラメータは、矩形領域の左上のXY座標、矩形領域の幅、高さ、読込先の配列、色変換指定です。
色変換指定は今回のプログラムでは、1(変換あり)でも0(変換なし)でも動作には違いはありません。

配列に読み込んだデータはFOR〜NEXTループで全部(といっても4×9=36個ですが)0かどうかチェックします。
今回は、ループでデータ全てのORを求め、その結果が0かどうかをループの後で判定しています。
(OR演算はビット演算で、少なくとも一方が1なら結果は1になります。0 OR 0=0、1 OR 0 = 0 OR 1 = 1 OR 1 = 1です。)

円と衝突したと判定した場合はプログラム末尾へジャンプしていますので、こうなります。
pc16-5.jpg


これで当たり判定ができたので、次回は星を降らせる処理や効果音などを追加してゲームを完成させたいと思います。
posted by boochow at 00:14| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年01月17日

プチコン3号 15日目 スプライトを決まった経路で移動させる

SmileBASICでは、ゲーム作成に便利なスプライト用命令をいろいろ持っていますが、その中でも特徴的なのが9日目の記事でも使ったSPANIM命令です。
SPANIM命令では「座標変更」を指定することもでき、これを使うと、あらかじめ決めたコースの上をスプライトを移動させていくことができます。

今回は前回の「落下する動き」「弾む動き」をスプライトにさせるプログラムを作ってみました。
前回は円が画面の中で弾む動きをさせるためにY座標をプログラムで計算させていましたが、今回は
(1)落下の座標計算をより簡略化し、
(2)計算結果を配列に記録し、SPANIM命令で再生する
ようにしてみます。

落下する動きの計算を簡単にすると、以下のようなダイレクトモードでも動かせる程度の短いプログラムになります。
これを入力してみると、スプライトが画面上端から画面下端まで落ちる動きをします。
pc15-1.jpg

SPSET 0,226
Y=0:FOR I=1 TO 9:FOR J=1 TO 5:Y=Y+I:SPOFS 0,200,Y:VSYNC 1:NEXT:NEXT
ここで行っている処理は、スプライトを下方向へ
「最初は1ピクセルずつ、5回動かす」「次は2ピクセルずつ、5回動かす」…「9ピクセルずつ、5回動かす」
というものです。これだけでも落下するような表現になります。

前回、落下する動作とは
「移動速度が時間経過に従って直線的に増えていく」
ような動作であると説明しました。
そして、ループの中で
V[I]=V[I]+G
INC Y[I],V[I]
という計算により、VをGずつ増やし、そのVをYに加算することで落下動作を計算していました。
pc14-5.jpg

先ほど示したプログラムは、「5回ごと」に「移動幅が1ピクセル」増えますので、「G=1/5ピクセル」としたのと同じことになります。

この「移動幅」と「回数」を変化させれば、「落下速度・落下する距離」が異なるいろいろな落下を表現できます。

上で示したプログラムでは、移動幅1ピクセル〜9ピクセル、同じピクセル数での移動回数5回です。
従ってトータルでの移動距離は
(1+2+3+4+5+6+7+8+9)×5 = 225

となります。
これは実は「画面の上から下まで落ちる動き」をさせる場合の移動距離
画面縦240ピクセル−スプライト縦16ピクセル=224ピクセル

にほぼ等しくなるようしたのです。

移動回数を5回ではなく4回にすると、より早く落下する動作になります。
この場合は「G=1/4ピクセル」としたことになります。
4回ずつの移動で、例えば移動幅を1ピクセル〜10ピクセルに変化させた場合、移動距離は
(1+2+3+4+5+6+7+8+9+10)×4 = 220

となります。

下図に、移動幅と移動回数から移動距離を求める早見図を載せておきます。
pc15-2.jpg


移動幅をマイナスから始めると、スプライトはいったん投げ上げられてから戻ってくる、すなわちジャンプするような動きになります。
例えば下のプログラムのように、-9ピクセル〜9ピクセルとすると、画面いっぱいの大ジャンプになります。
移動幅の初期値と終了値をもっと小さくして、例えば-5ピクセル〜5ピクセルとすると、より小さなジャンプになります。
SPSET 0,226
Y=224:FOR I=-9 TO 9:FOR J=1 TO 5:Y=Y+I:SPOFS 0,200,Y:VSYNC 1:NEXT:NEXT


それでは、ループの中でSPOFS命令で毎回座標を指定するのではなく、先に一連の座標を配列に記録してからSPANIM命令で再生してスプライトをアニメーションさせてみます。
この方法のメリットは、座標をあらかじめ計算済みなので、動かすたびに計算し直す必要がないことです。

SPANIMの座標変更機能は、
(1)座標の列をパラメータで直接指定する
(2)座標の列を配列で渡す
(3)座標の列を格納したDATA命令の直前のラベルを指定する
という3つの方法がありますが、今回は(2)を使います。

使い方は
SPANIM 管理番号,"XY",配列,ループ
です。
または
SPANIM 管理番号,"XY+",配列,ループ
です。
"XY"で指定した場合は、スプライトは指定された座標へ移動しますが、"XY+"で指定した場合は、スプライトは「SPANIM命令が実行された時点の座標+指定された座標」へ移動します。
つまり、SPOFSで移動の出発点を指定すると、そこから出発してスプライトを動かすことができます。
配列は1次元配列で、
フレーム数,X座標,Y座標
の3つを連続して格納します。
なお現在のプチコン3号では、配列の中にこの3つを33組以上格納するとエラーになるようです。

さらに、SPANIMには「補間機能」があり、これを使うと少ないパラメータで長時間の動きを表現できます。

補間機能は、1フレームごとに「移動距離/フレーム数」だけ移動させることができる機能です。
たとえば、今回の落下の動きは「最初は1ピクセルずつ、5回動かす」で始まりますが、これは
「5ピクセルを5フレームで動かす」という指定の仕方ができます。

下図を見てください。左側が1フレームごとの動作を指定した場合、右側が補間機能を指定した場合です。
補間機能を使う場合は、フレーム数を負の値で指定します。
左側は時間軸に沿って、1フレームごとにY座標を1ずつ5回増やしています。
右側では、グレーになっている部分の指定は不要で、フレーム数が「-5」Y座標が「5」という指定をしています。
これで左側と同じ指定をしたことになります。
このあとの5フレームは2ピクセルずつ動いて、Y座標が15まで増えますので、この次の指定はフレーム数が「-5」Y座標が「15」という指定をすればよいのです。
pc15-3.jpg


今回は「落下運動の座標を配列に入れて、SPANIMに渡し、スプライトに落下運動をさせる」プログラムを作りました。
リストはこちらです。
pc15-4.jpg
DIM A[0]
SP_FALL 0,0,1,-4,9,-5,A OUT X,Y
SP_FALL X,Y,1,-4,4,-5,A OUT X,Y
SP_FALL X,Y,1,-2,2,-5,A OUT X,Y
PUSH A,-5:PUSH A,X:PUSH A,Y
SPSET 0,226

@LOOP
SPOFS 0,160,50
SPANIM 0,"XY+",A,1
SPANIM 0,"C",4,&HFFFFFFFF,4,&HFF808080,0
WHILE SPCHK(0) AND 1 != 0:WAIT 1:WEND
GOTO @LOOP

DEF SP_FALL X,Y,DX,VMIN,VMAX,VTIME,ANIM OUT NEWX,NEWY
K=ABS(VTIME)
FOR I=VMIN TO VMAX
PUSH ANIM,VTIME
PUSH ANIM,X
PUSH ANIM,Y
X=X+DX*K
Y=Y+I*K
NEXT
NEWX=X
NEWY=Y
END

このプログラムを動かすと、下図のように星のスプライトが上から落ちて2回弾んで止まるという動作を繰り返します。
pc15-5.jpg

プログラムの中にループがありますが、このループはアニメーションを繰り返し再生させているだけです。
落下の動きそのものは
SPOFS 0,160,50
SPANIM 0,"XY+",A,1
だけで実現しています。

まずSPOFSで移動の開始ポイントを指定しています。
次にSPANIMでスプライトの移動開始です。配列Aに座標のデータが入っています。
ループ指定は1ですので、1回移動したら終了です。

その次の
SPANIM 0,"C",4,&HFFFFFFFF,4,&HFF808080,0
は、スプライトの色を4フレームごとに明るくしたり暗くしたりして、チカチカ瞬くように見せています。

その次の
WHILE SPCHK(0) AND 1 != 0:WAIT 1:WEND
は、スプライトの移動が終わるまでループして待つ処理です。
今回は詳しく説明しませんが、SPCHKはSPANIMで指定した動作の状態を調べる命令です。
パラメータはスプライトの管理番号で、結果はSPANIMで指定できる様々な動作について「1(実行中)」「0(完了)」がビットパターンで得られます。
「AND 1」で座標変更動作の状態を調べています。

SPANIMに渡す座標の計算は、15行目からの
DEF SP_FALL X,Y,DX,VMIN,VMAX,VTIME,ANIM OUT NEWX,NEWY
の中で行っています。
各パラメータの意味は、

・X,Y:座標の初期値
・DX:X軸方向の移動量
・VMIN、VMAX:Y軸方向の移動量の最小値と最大値
・VTIME:同じ移動量で移動するフレーム数
・ANIM:座標を格納する配列
・NEWX、NEWY:最後の座標(ANIMの中には格納しない)

となっています。
一番最初に挙げた例「最初は1ピクセルずつ、5回動かす」「次は2ピクセルずつ、5回動かす」…「9ピクセルずつ、5回動かす」の場合は、
・VMIN=1
・VMAX=9
・VTIME=5
に相当します。

計算結果はPUSHで配列の末尾に追加しています。
PUSH命令は12日目にスタックの説明のところで解説しましたが、今回は単に配列の最後にデータを追加するために使っているだけです。

最後の座標をANIMに入れずにNEWX、NEWYで返しているのは、SP_FALLを繰り返し呼んだ場合に、動きをスムーズにつなげるためです。
SP_FALLを呼び出している2行目から4行目までは、
SP_FALL 0,0,1,-4,9,-5,A OUT X,Y
SP_FALL X,Y,1,-4,4,-5,A OUT X,Y
SP_FALL X,Y,1,-2,2,-5,A OUT X,Y
となっていますが、1つ目の呼び出しが最初の落下動作、2つ目と3つ目の呼び出しが弾む動作です。
直前の呼び出しの最後の座標X,Yを、直後の呼び出しの際に座標の初期値として渡しています。
そして、最後に
PUSH A,-5:PUSH A,X:PUSH A,Y
で座標を配列の末尾に追加しています。
posted by boochow at 13:28| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする
人気記事