衝突判定、これを付けることで初めてプログラムにゲームとしての特色を植え込むことになります。何らかの判定無くしてはゲームは成り立たないためです。
主な判定種別には、「自機と敵」、「自機弾と敵」などがありますが、このうち「自機と敵」の判定は、対象が1対多数ということで単純なので、こちらから攻めてゆくことにします。(現在は登場する自機は1機のみということにします)
判定種別には他にも「自機と敵弾」などがありますが、判定方法は「自機と敵」と全く同じなので、ひとまとめにします。以後の説明では、衝突対象を「敵」で固定化していますが、これは敵弾でもアイテムでも可です。
サンプル:
まずはサンプルです。 collide1.zip
「擬似マルチタスクによるゲームプログラミング」のソースから引き継いでいるので、ファイルが多くて多少戸惑うかもしれません。 しかし、今回の重要な部分は、判定チェック関数"judgehit.c(judgehit.h)"と、自機・敵・敵弾タスク内の衝突判定用のデータ登録/関数コール部分です。
また、メインタスク内にはGAMEOVERのフェーズを追加しています。 GAMEOVERの処理自体は本題とは関係ないのですが、これがあると衝突したことが目で見て分かるので追加しました。GAMEOVER→タイトルの流れを作るために、ヘッダファイル"title.h"も新たに追加しているので注意してください。
ソース構造:
wwmodule.c WonderWitch用モジュール群(現在はキー入力と画面クリアのみ) gamemath.c ゲーム用数学関連関数群 task.c タスク管理モジュール sprite.c WonderWitch用スプライト管理モジュール judgehit.c 衝突判定モジュール
main.c 実行/メインループ部 game.c ゲームで使用するソースファイルのインクルード群 title.c タイトル処理タスク battle.c メインゲーム処理タスク myship.c 自機タスク/自機弾タスク enemy.c 3種類の敵のタスク bullet.c 敵弾関連
詳細 ──────
データ構造の定義:
衝突範囲のデータも構造体で用意するのが得策だと思います。自機にも敵にも同じ構造体を使用します。 範囲の形状は長方形になります。単純な計算で処理できるからです。
typedef struct {
short x1,x2; /* x1=判定判定左端、x2=判定範囲右端 */ short y1,y2; /* y1=判定判定上端、y2=判定範囲下端 */
}JUDGE;
設定する値はピクセル単位になります。 衝突判定範囲は、キャラクタ座標(XH,YH)からの相対座標で指定します。x1,y1が判定の左上地点で、x2,y2が右下になります。
charでなくshortを使用しているのは、後になって符号付きの計算を厄介にさせないためです。 例えば、判定の一部が画面外にはみだした場合を考慮する必要があります。また、登場キャラの大きさによっては、例えばボス系などでは128ピクセル以上の判定範囲を持つこともあり得るので、char型では不足となります。
この構造体を用いて、各オブジェクトごとに衝突範囲テーブルを定義してゆきます。 例)
/* 自機の衝突判定範囲 */ JUDGE jdg_myship = {
3-1, 4+1, /* x1,x2 */ 3-1, 4+2, /* y1,y2 */
};
/* 敵の衝突判定範囲 */ JUDGE jdg_enemy1 = {
3-4, 4+4, /* x1,x2 */ 3-4, 4+4, /* y1,y2 */
};
/* 敵弾の衝突判定範囲 */ JUDGE jdg_bullet = {
3-0, 4+0, /* x1,x2 */ 3-0, 4+0, /* y1,y2 */
};
スプライト表示と衝突判定の中心点について:
上記のデータ構造の定義で、[3-n,4-n]といった計算をしていますが、これはスプライト表示の段階で「8x8ピクセルのスプライトの左上を中心」という事にしているため、衝突判定を求める際の中心点は3または4という形になっています。
参考:自機のスプライト登録部分
/* スプライト登録 */ SetSpr( w->XH-4, w->YH-4, SPR_ATTR(SHIP_FONTENT+0, 12, 1)); SetSpr( w->XH+4, w->YH-4, SPR_ATTR(SHIP_FONTENT+1, 12, 1)); SetSpr( w->XH-4, w->YH+4, SPR_ATTR(SHIP_FONTENT+2, 12, 1)); SetSpr( w->XH+4, w->YH+4, SPR_ATTR(SHIP_FONTENT+3, 12, 1));
このあたりは好みの問題になると思いますが、変更する場合は、それに伴ってスプライト表示座標のオフセットも変更する必要があります。 しかし現在の表示座標指定は、Witchで複数のスプライトを並べる際に配置指定が楽なので、当方はこれで進めることにします。
データ構造の実体の持たせ方:
自機や敵の構造体に 衝突範囲構造体を含める際、「エネミーデータ内に、衝突範囲データを直接格納する」べきか、それとも「エネミーデータ内に、衝突範囲データへのポインタを格納する」べきか、といった選択で悩むところだと思います。
前者 { JUDGE jp; }
後者 { JUDGE *jp; }
ここではメモリ効率を優先して、後者のポインタ方式を使うことにしています。(ただしポインタ方式の場合、あとで衝突判定を伸び縮みさせたいといった場合に、別のタスクオブジェクトも同じデータを参照している可能性があることに注意してください)
set_enemyなどのオブジェクト発射関数内で、衝突判定テーブルのポインタを渡します。 例)
/*-----------------------------------------------------------------------------
void set_enemy1(void) {
TSKWRK *tp; ENEMY *w; tp = SetTsk(tsk_enemy1); /* タスク確保 */ w = (ENEMY*)&tp->wrk[0]; w->XH = (GetRand(0) % (SCR_XH-16))+8; w->YH = 0; w->vx = 0; w->vy = 256; w->cnt[0] = 10; w->jp = &jdg_enemy1; /* New! */
}
判定チェック関数:
自機と敵が衝突したか否かを判定する関数です。 関数は以下のようになります。
/*============================================================================= / 自機VS敵 衝突判定 /----------------------------------------------------------------------------- / 敵ルーチンからコールする / 分かりやすくするためにここでは敵と言っているが、敵弾やアイテムでもOK /============================================================================*/ int JudgeShip(short tx, short ty, JUDGE far *tj) {
short x, y; JUDGE *pj = pMyship->jp; x = pMyship->XH; y = pMyship->YH;
if(( y + pj->y1 )<=( ty + tj->y2 ) && ( ty + tj->y1 )<=( y + pj->y2 )){ if(( x + pj->x1 )<=( tx + tj->x2 ) && ( tx + tj->x1 )<=( x + pj->x2 )){ return 1; } } return 0;
}
pMyshipは、自機タスクのワークへのポインタです。myship.c内で宣言されています。
互いが衝突したか否かを見るためには、自機と敵の双方の座標を比較することが必要になります。 横範囲比較と縦範囲比較に分けて考えたほうが理解しやすいと思います。簡単な実践の為に、x方向だけ見てみることにします。 また、ポインタ参照が式を見づらくしているので、簡単な変数に直してみます。
( x + x1 )<=( tx + x2 ) かつ ( tx + tx1 )<=( x + tx2 )
x = 自機座標 tx = 敵座標 x1, x2 = 自機判定範囲(左端,右端) tx1,tx2 = 自機判定範囲(左端,右端)
上記の条件を満たせば、x座標は衝突しています。 これと同じ事をy方向に対しても行えば、矩形の判定になります。
自機の判定:
自機が多数の敵を一つ一つ見てゆくよりも、個々の敵から自機を見るようにします。敵は擬似マルチタスクにより既に個々が独立して動作しているので、敵から1つの自機を見たほうが明らかに効率が良くなります。これを、もし自機から多数の敵を見るようにしてしまうと、多数の敵を1つ1つサーチしてゆくためのループが必要になってしまうため、効率が悪くなります。
敵タスク関数の中で、自機の衝突判定処理を呼びます。
/*-----------------------------------------------------------------------------
void tsk_enemy1(TSKWRK *tp) {
ENEMY *w = (ENEMY*)&tp->wrk[0];
switch(tp->phase){ /*---------------------------------------------------*/ case P_INIT: tp->phase = P_MAIN; break;
/*---------------------------------------------------*/ case P_MAIN: /* 移動 */ w->X += w->vx; w->Y += w->vy;
/* 一定間隔で弾発射 */ if(--w->cnt[0] < 0){ /* X Y SPD DIR WAY VWAY */ Fire(w->XH, w->YH, 384, 0, 1, 0); w->cnt[0] = 32000; }
/* 移動範囲抑制 */ if(w->XH < 0 || w->XH > SCR_XH || w->YH < 0 || w->YH > SCR_YH){ tp->phase = P_END; }
/* スプライト登録 */ SetSpr( w->XH-4, w->YH-4, SPR_ATTR(ENEMY_FONTENT+0, 12, 1)); SetSpr( w->XH+4, w->YH-4, SPR_ATTR(ENEMY_FONTENT+1, 12, 1)); SetSpr( w->XH-4, w->YH+4, SPR_ATTR(ENEMY_FONTENT+2, 12, 1)); SetSpr( w->XH+4, w->YH+4, SPR_ATTR(ENEMY_FONTENT+3, 12, 1));
/* 衝突判定 New! */ if(JudgeShip(w->XH, w->YH, w->jp)!=0){ pMyship->hp = 0; tp->phase = P_END; }
break;
/*---------------------------------------------------*/ case P_END: DelTsk(tp); break; }
}
判定後の処理:
自機の構造体メンバに耐久力(hp)を設けておき、もし敵処理内で自機との衝突があった場合には、hpを0にします。 自機処理内では、このhpを常時監視するようにします。 こうすることにより、爆発処理や無敵処理などを、自機処理関数内のフェーズに含めることが出来ます。 自機の登場時にhpの初期設定を忘れないようにして下さい。
敵側)
/* 衝突判定 */ if(JudgeShip(w->XH, w->YH, w->jp)!=0){ pMyship->hp = 0; /* 自機HP = 0 */ tp->phase = P_END; }
自機側)
/* 耐久力チェック */ if(w->hp <= 0){ /* 1ミス時の処理 */ }
避けゲーの完成?:
自機の衝突判定とGAMEOVERを作ったので、あとはスコアを追加して敵にバリエーションを付ければ、とりあえず「避けゲー」としては成り立つことになります。
しかし、自機弾が撃てるのに相手に当たらないままでは、あまりに理不尽かもしれません。 次は自機の弾にも判定を付けたいと思っています。