ゲーム制作講座1−9

9.敵の弾が自機に向かっていくようにしよう


 前回は敵も弾を撃つようにしました。ですがまだ一直線に弾を撃つだけで
ボールくんに当たるかどうかなどは全く意識していません。
 というわけで今回は四角君の弾がボールくんに向かっていくようにしましょう。

 まず理屈です。四角君の位置からボール君の位置まで弾を飛ばすには、まず
四角君の位置からボール君の位置までの距離を把握する必要があります。
なぜならその距離を0にした時、目標であるボール君の座標に重なるからです。
 下の図を見てみてください。


 (sx,sy) と書かれている緑色の点が四角君、つまり敵のいる座標だと思って
ください。そして (bx,by) はボール君のいる座標です。
 さてこれから四角君からボール君に向かって弾を発射するわけですが、その
距離は sb、X座標系だけでみると bx-sx、Y座標系だけでみると by-sy
距離があります。四角君のいる座標 (sx,sy) から y 軸沿いに by-sy
x 軸沿いに bx-sx 移動した位置がボール君のいる座標となるわけです。

 四角君の位置からボール君の位置にたどり着くまでの距離はわかりました。
 この距離をそのまま弾の速度( bx-sx,by-sy )にしてしまえばめでたく撃ち出された
弾はボール君に命中するわけですが、( bx-sx, by-sy ) はあくまで距離なので、
弾の座標にこの値をフレームで加算してしまったら一瞬でボール君の位置に
たどり着いてしまい、ボール君はかわす事も目で確認することも出来ないうちに
命中してしまいます。
 つまり ( bx-sx , by-sy ) の距離はある程度の時間を掛けて縮めなくてはならないのです。

 なのでとりあえず発射から命中まで10フレーム掛かるようにしてみましょう。
これは簡単です。( bx-sx , by-sy )フレームで弾の座標に足しこむから
速すぎてしまうわけなので、これを10回に分けてしまおうと言うわけです。
 早速やってみましょう、全距離が ( bx-sx, by-sy ) なのでこれを10分割した
( (bx-sx)/10 , (by-sy)/10 ) を速度とし、弾の座標に毎フレーム足しこんで
やれば発射されてから10フレーム掛かってボール君に命中することになります。

 ですがこれでは問題があります。なぜなら四角君とボール君の位置がどんなに
遠くてもどんなに近くてもかならず10フレームたつまでボール君の位置まで
たどりつかない
からです。
 これは発射される弾は遠ければ遠いほど速く動き、近ければ近いほど遅く動く
ことを意味します。これでは実用性がありません。

 今のところ計算で割り出される値には飛んでいく方向と、速度が含まれています。
元々四角君とボール君の距離から導き出しているので距離によって速度が変化して
しまうのは当然の事です。つまり悪いのはボール君と四角君の距離 sb が変動して
しまうことです。 sb がどんな値でも方向は bx-sx と by-sy の値の比によって
決定される
ので、あとは sb が一定の値に保っていてくれれば良いのです。
そうですね、さしあたって常に1であって欲しいものです。

 そんな方法があるのかですが、あるんです。
 まず bx-sx と by-sy がある比率を保つ時の値大きさは sb の値の大きさと
1対1の関係にあります
。つまり bx-sxby-sy の値が例えば 83 だった場合
sb の値は 8.544003... 以外ありません。bx-sxby-sy比率が保たれていれば
その逆もまた然りです。
 つまり sb の値を10で割った場合。割られた後の sb に対応する bx-sx, by-sy
割られる前の sb に対応する bx-sxby-sy10で割った値になるというわけです。

 つまり sb の値を割った結果1にする値がわかれば sb が1の時の bx-sx と by-sy も
知る事が出来ます
sb の値が、というのは四角君からボール君に向けて伸びる直線の
長さ sbドットという事になり、当初の sb を一定の値にするという目的を達成する事
が出来ます。
 その値とは実は sb 自身です。sbsb の値で割れば結果は、つまり bx-sx と
by-sy
sb で割ってやる事で sbの時の bx-sx,by-sy を得られるわけです。

つまり

( bx-sx ) / sb = ( sb が1の時の bx-sx )
( by-sy ) / sb = ( sb が1の時の by-sy )

 さてこれで距離がどんなものであっても sb を一定の値、にするという目的を達成する
ことが出来ました。これで弾の速度を自由に決める事が出来ます。
 導き出された sb が1の時の bx-sx, by-sy はすなわち弾の速度が1フレーム当たり1ドット
の時のXY各軸の速度値
となっています。つまりこの bx-sx,by-sy それぞれに好きな値、例えば
80を掛けてやれば1フレーム当たり80ドット分進む速度値となります。

 長かったですが、これで理屈は終りです。
(ちなみに sb を求める時に使われている sb^2 = sbx^2 + sby^2 という式は、三平方の定理という公式
です。なぜこの式が成り立つのか、までは説明しませんので(というか私も忘れてしまったので(死))
興味のある方は調べてみてください。(汗))

 ではこの理屈を組みこんだ四角君がボール君に向かって弾を撃つプログラムに変更して
みたいと思います。

 で、こうなりました。

#include "DxLib.h"
#include <math.h>

#define SHOT 20

// WinMain関数
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
					 LPSTR lpCmdLine, int nCmdShow )
{
	int BallX , BallY , BallGraph ;
	int Bw, Bh, Sw, Sh ;
	int SikakuX , SikakuY , SikakuMuki , SikakuGraph ;
	int SikakuDamageFlag , SikakuDamageCounter , SikakuDamageGraph ;
	int ShotX[SHOT] , ShotY[SHOT] , ShotFlag[SHOT] , ShotGraph ;
	int SikakuW , SikakuH , ShotW , ShotH ;
	int ShotBFlag ;
	int i ;
	double ETamaX , ETamaY ;
	int ETamaFlag ;
	double ETamaSx, ETamaSy ;
	int ETamaW , ETamaH , ETamaGraph ;
	int ETamaCounter ;


	// 画面モードの設定
	SetGraphMode( 640 , 480 , 16 ) ;

	// DXライブラリ初期化処理
	if( DxLib_Init() == -1 ) return -1;

	// グラフィックの描画先を裏画面にセット
	SetDrawScreen( DX_SCREEN_BACK ) ;

	// ボール君のグラフィックをメモリにロード&表示座標をセット
	BallGraph = LoadGraph( "Ball.png" ) ;
	BallX = 288 ; BallY = 400 ;

	// 四角君のグラフィックをメモリにロード&表示座標をセット
	SikakuGraph = LoadGraph( "Sikaku.png" ) ;
	SikakuX = 0 ; SikakuY = 50 ;

	// 四角君のダメージ時のグラフィックをメモリにロード
	SikakuDamageGraph = LoadGraph( "SikakuDam.png" ) ;

	// 四角君が顔を歪めているかどうかの変数に『歪めていない』を表す0を代入
	SikakuDamageFlag = 0 ;

	// 敵の弾のグラフィックをロード
	ETamaGraph = LoadGraph( "EShot.png" ) ;

	// 敵の弾のグラフィックのサイズを得る
	GetGraphSize( ETamaGraph , &ETamaW , &ETamaH ) ;

	// 敵の弾が飛んでいるかどうかを保持する変数に『飛んでいない』を表す0を代入
	ETamaFlag = 0 ;

	// 敵が弾を撃つタイミングを取るための計測用変数に0を代入
	ETamaCounter = 0 ;

	// 弾のグラフィックをメモリにロード
	ShotGraph = LoadGraph( "Shot.png" ) ;

	// 弾が画面上に存在しているか保持する変数に『存在していない』を意味する0を代入しておく
	for( i = 0 ; i < SHOT ; i ++ )
	{
		ShotFlag[i] = 0 ;
	}

	// ショットボタンが前のフレームで押されたかどうかを保存する変数に0(押されいない)を代入
	ShotBFlag = 0 ;

	// 四角君の移動方向をセット
	SikakuMuki = 1 ;

	// 弾のグラフィックのサイズをえる
	GetGraphSize( ShotGraph , &ShotW , &ShotH ) ;

	// 四角君のグラフィックのサイズを得る
	GetGraphSize( SikakuGraph , &SikakuW , &SikakuH ) ;

	// ボール君と弾の画像のサイズを得る
	GetGraphSize( BallGraph , &Bw , &Bh ) ;
	GetGraphSize( ShotGraph , &Sw , &Sh ) ;


	// 移動ルーチン
	while( 1 )
	{
		// 画面を初期化(真っ黒にする)
		ClearDrawScreen() ;

		// ボール君の操作ルーチン
		{
			// ↑キーを押していたらボール君を上に移動させる
			if( CheckHitKey( KEY_INPUT_UP ) == 1 ) BallY -= 3 ;

			// ↓キーを押していたらボール君を下に移動させる
			if( CheckHitKey( KEY_INPUT_DOWN ) == 1 ) BallY += 3 ;

			// ←キーを押していたらボール君を左に移動させる
			if( CheckHitKey( KEY_INPUT_LEFT ) == 1 ) BallX -= 3 ;

			// →キーを押していたらボール君を右に移動させる
			if( CheckHitKey( KEY_INPUT_RIGHT ) == 1 ) BallX += 3 ;

			// スペースキーを押した場合は処理を分岐
			if( CheckHitKey( KEY_INPUT_SPACE ) )
			{
				// 前フレームでショットボタンを押したかが保存されている変数が0だったら弾を発射
				if( ShotBFlag == 0 )
				{
					// 画面上にでていない弾があるか、弾の数だけ繰り返して調べる
					for( i = 0 ; i < SHOT ; i ++ )
					{
						// 弾iが画面上にでていない場合はその弾を画面に出す
						if( ShotFlag[i] == 0 )
						{
							// 弾iの位置をセット、位置はボール君の中心にする
							ShotX[i] = ( Bw - Sw ) / 2 + BallX ;
							ShotY[i] = ( Bh - Sh ) / 2 + BallY ;

							// 弾iは現時点を持って存在するので、存在状態を保持する変数に1を代入する
							ShotFlag[i] = 1 ;

							// 一つ弾を出したので弾を出すループから抜けます
							break ;
						}
					}
				}

				// 前フレームでショットボタンを押されていたかを保存する変数に1(おされていた)を代入
				ShotBFlag = 1 ;
			}
			else
			{
				// ショットボタンが押されていなかった場合は
				// 前フレームでショットボタンが押されていたかを保存する変数に0(おされていない)を代入
				ShotBFlag = 0 ;
			}

			// ボール君が画面左端からはみ出そうになっていたら画面内の座標に戻してあげる
			if( BallX < 0 ) BallX = 0 ;

			// ボール君が画面右端からはみ出そうになっていたら画面内の座標に戻してあげる
			if( BallX > 640 - 64 ) BallX = 640 - 64  ;

			// ボール君が画面上端からはみ出そうになっていたら画面内の座標に戻してあげる
			if( BallY < 0 ) BallY = 0 ;

			// ボール君が画面下端からはみ出そうになっていたら画面内の座標に戻してあげる
			if( BallY > 480 - 64 ) BallY = 480 - 64 ;

			// ボール君を描画
			DrawGraph( BallX , BallY , BallGraph , FALSE ) ;
		}

		// 弾の数だけ弾を動かす処理を繰り返す
		for( i = 0 ; i < SHOT ; i ++ )
		{
			// 自機の弾iの移動ルーチン( 存在状態を保持している変数の内容が1(存在する)の場合のみ行う )
			if( ShotFlag[ i ] == 1 )
			{
				// 弾iを16ドット上に移動させる
				ShotY[ i ] -= 16 ;

				// 画面外に出てしまった場合は存在状態を保持している変数に0(存在しない)を代入する
				if( ShotY[ i ] < -80 )
				{
					ShotFlag[ i ] = 0 ;
				}

				// 画面に弾iを描画する
				DrawGraph( ShotX[ i ] , ShotY[ i ] , ShotGraph , FALSE ) ;
			}
		}

		// 四角君の移動ルーチン
		{
			// 顔を歪めているかどうかで処理を分岐
			if( SikakuDamageFlag == 1 )
			{
				// 顔を歪めている場合はダメージ時のグラフィックを描画する
				DrawGraph( SikakuX , SikakuY , SikakuDamageGraph , FALSE ) ;

				// 顔を歪めている時間を測るカウンターに1を加算する
				SikakuDamageCounter ++ ;

				// もし顔を歪め初めて 30 フレーム経過していたら顔の歪んだ状態から
				// 元に戻してあげる
				if( SikakuDamageCounter == 30 )
				{
					// 『歪んでいない』を表す0を代入
					SikakuDamageFlag = 0 ;
				}
			}
			else
			{
				// 歪んでいない場合は今まで通りの処理

				// 四角君の座標を移動している方向に移動する
				if( SikakuMuki == 1 ) SikakuX += 3 ;
				if( SikakuMuki == 0 ) SikakuX -= 3 ;

				// 四角君が画面右端からでそうになっていたら画面内の座標に戻してあげ、移動する方向も反転する
				if( SikakuX > 576 )
				{
					SikakuX = 576 ;
					SikakuMuki = 0 ;
				}

				// 四角君が画面左端からでそうになっていたら画面内の座標に戻してあげ、移動する方向も反転する
				if( SikakuX < 0 )
				{
					SikakuX = 0 ;
					SikakuMuki = 1 ;
				}

				// 四角君を描画
				DrawGraph( SikakuX , SikakuY , SikakuGraph , FALSE ) ;

				// 弾を撃つタイミングを計測するためのカウンターに1を足す
				ETamaCounter ++ ;

				// もしカウンター変数が60だった場合は弾を撃つ処理を行う
				if( ETamaCounter == 60 )
				{
					// もし既に弾が『飛んでいない』状態だった場合のみ発射処理を行う
					if( ETamaFlag == 0 )
					{
						// 弾の発射位置を設定する
						ETamaX = SikakuX + SikakuW / 2 - ETamaW / 2 ;
						ETamaY = SikakuY + SikakuH / 2 - ETamaH / 2 ;

						// 弾の移動速度を設定する
						{
							double sb, sbx, sby, bx, by, sx, sy ;

							sx = ETamaX + ETamaW / 2 ;
							sy = ETamaY + ETamaH / 2 ;

							bx = BallX + Bw / 2 ;
							by = BallY + Bh / 2 ;

							sbx = bx - sx ;
							sby = by - sy ;

							// 平方根を求めるのに標準関数の sqrt を使う、
							// これを使うには math.h をインクルードする必要がある
							sb = sqrt( sbx * sbx + sby * sby ) ;

							// 1フレーム当たり8ドット移動するようにする
							ETamaSx = sbx / sb * 8 ;
							ETamaSy = sby / sb * 8 ;
						}

						// 弾の状態を保持する変数に『飛んでいる』を示す1を代入する
						ETamaFlag = 1 ;
					}

					// 弾を打つタイミングを計測するための変数に0を代入
					ETamaCounter = 0 ;
				}
			}
		}

		// 敵の弾の状態が『飛んでいる』場合のみ弾の移動処理を行う
		if( ETamaFlag == 1 )
		{
			// 弾を移動させる
			ETamaX += ETamaSx ;
			ETamaY += ETamaSy ;

			// もし弾が画面からはみ出てしまった場合は弾の状態を『飛んでいない』
			// を表す0にする
			if( ETamaY > 480 || ETamaY < 0 ||
				ETamaX > 640 || ETamaX < 0 ) ETamaFlag = 0 ;

			// 画面に描画する( ETamaGraph : 敵の弾のグラフィックのハンドル )
			DrawGraph( ( int )ETamaX , ( int )ETamaY , ETamaGraph , FALSE ) ;
		}

		// 弾と敵の当たり判定、弾の数だけ繰り返す
		for( i = 0 ; i < SHOT ; i ++ )
		{
			// 弾iが存在している場合のみ次の処理に映る
			if( ShotFlag[ i ] == 1 )
			{
				// 四角君との当たり判定
				if( ( ( ShotX[i] > SikakuX && ShotX[i] < SikakuX + SikakuW ) ||
					( SikakuX > ShotX[i] && SikakuX < ShotX[i] + ShotW ) ) &&
					( ( ShotY[i] > SikakuY && ShotY[i] < SikakuY + SikakuH ) ||
					( SikakuY > ShotY[i] && SikakuY < ShotY[i] + ShotH ) ) )
				{
					// 接触している場合は当たった弾の存在を消す
					ShotFlag[ i ] = 0 ;

					// 四角君の顔を歪めているかどうかを保持する変数に『歪めている』を表す1を代入
					SikakuDamageFlag = 1 ;

					// 四角君の顔を歪めている時間を測るカウンタ変数に0を代入
					SikakuDamageCounter = 0 ;
				}
			}
		}

		// 裏画面の内容を表画面にコピーする
		ScreenFlip() ;

		// Windows 特有の面倒な処理をDXライブラリにやらせる
		// -1 が返ってきたらループを抜ける
		if( ProcessMessage() < 0 ) break ;

		// もしESCキーが押されていたらループから抜ける
		if( CheckHitKey( KEY_INPUT_ESCAPE ) ) break ;
	}

	// DXライブラリ使用の終了処理
	DxLib_End() ;

	// ソフトの終了
	return 0 ;
}
<実行図>

 実行してみると四角君はボール君に向かって弾を発射するようになったと思います。

 プログラムには発射するところ以外にマイナーな変更が幾つか加えられています。まず
ETamaX, ETamaY は小数点以下の値も扱えるようにするために double 型変数として宣言
するように変更しました。
 そしてボール君のグラフィックの幅、高さを四角君が弾を発射する時の計算に使うため、
宣言の位置を変更しています。
 それ以外には四角君の弾の速度を表す ETamaSx, ETamaSy の追加と、弾の描画時の
座標指定に double 型変数値を int 型の値に変換するための ( int ) というキャストも加え
ました。そして四角君の弾が画面外に飛んでいったかどうかの判定を、今まで画面下
端しか見ていなかったので、すべての画面端を見るように変更しました。

 さて、これで四角君はボール君に向かって弾を撃つようになりました。

戻る