game-engineer:classes:2023:something-else:summertime-special-cource:cosmichooligun-3

弾を発射したい。
まずは弾の画像を用意しよう。これも何でもよいです。どうせ描画するとき画像サイズ調整するし
初めに画像の用意のところで使ったサイトなどから適当に弾丸用の画像を用意しよう。
僕はここのやつ
プレイヤーと敵のキャラクターを作った時のように、キャラクターの構造体を作って、弾一つずつに画像(textureAsset)を割り当てて描画する

自機と敵から出る弾に対する定数たちを設定していく(既出のものもあり)

Listing. 1: 弾丸界隈の定数宣言
gameSequence.h
//弾丸の表示サイズの1辺(本当は幅高さにした方がいいけど、今回は正方形縛り)
const int ENEMY_BULLET_SIZE{ 15 };
//弾丸の移動スピード(今回は等速度直線運動しかしない)
const double ENEMY_BULLET_SPEED{ 250 };
 
//プレイヤー用の弾丸の画像サイズの1辺の長さ
const int PLAYER_BULLET_SIZE{ 15 };
//弾丸の移動スピード(今回は等速度直線運動しかしない)
const double PLAYER_BULLET_SPEED{ 250 };
 
//敵の弾の最大弾数
const int ENEMY_MAX_BULLET_NUM{ 10 };
//自機の弾の最大数
const int PLAYER_MAX_BULLET_NUM{ 5 };

上の4つは、前回までについでに宣言はしてあるはずなので、確認して既にあったら追加しなくともよい。

Asset追加

弾丸の画像決めたらAsset登録!

Listing. 2: Assetへの弾丸登録
Main.cpp
void Main()
{
省略
        \\弾丸画像の登録
	TextureAsset::Register(U"BULLET", U"images\\shots\\1.png");
省略
Listing. 3: PlayGunとEnemyGunの作成
//プレイヤーの機銃構造体
struct PlayerGun
{
	gameChar PlayerBullet[プレイヤーの弾丸の最大値の定数決めたよね?];
};
//敵の機銃構造体
struct EnemyGun
{
	gameChar EnemyBullet[敵の弾丸の最大値の定数決めたよね?];
};

それで、こいつらをgameDataに追加しちゃうんです
ってことは、PlayerGunとEnemyGunはgameDataよりは前に宣言されてないとダメだよね。

Listing. 4: gameData構造体へのメンバ追加
gameSequence.h
struct gameData
{
	GAME_STATE gState;
	//プレイヤーキャラ関係
	gameChar Player;
	PlayerGun playerGUN;
        //敵キャラ関係
        gameChar Enemy;
	EnemyGun enemyGUN;
};

これで、弾のデータもいろんなUpdate、Draw関数に一緒に渡すことができる。
次はどうする?
そうね。今までもそうだったけど、データ作ったら初期化関数!

弾丸初期化関数

初期化関数をPlayerGun用とEnemyGun用に作るんだけど、
とりあえず、プレイヤーの弾だけ作ってみよう。
InitEnemyBulletの中身は空にしとけば問題ない。

gameSequence.h
void InitPlayerBullet(PlayerGun& _playerGun);
void InitEnemyBullet(EnemyGun& _enemyGun);

実装は以下の様にする。まぁ、プレイヤーキャラと敵キャラの初期化と変わらん感じでやれる

  • pos
    • とりあえず(0,0)を指定、発射するときに自機の真ん中から発射するように設定する
  • speed
    • 定数で決めた弾丸のスピードを設定
  • tex
    • Mainで登録した弾丸用のTextureAssetを設定
  • rect
    • とりあえず{pos, Size{弾丸のRectサイズ定数、弾丸のRectサイズ定数}}で初期化
    • サイズは変更しないが、posが変わったら一緒にrectのposも変えてやる必要がある
  • moveDir
    • プレイヤーの弾は必ず、自機の位置(pos)から上に向かって飛ぶから方向ベクトルはどうなる?
  • isAlive
    • 発射したらtrue、発射してなかったらfalseにする

これをPlayerGunが持っている弾丸数分おこなう。
総合すると

Listing. 5: InitPlayerBulletの作成
gameSequence.cpp
void InitPlayerBullet(PlayerGun& _playerGun)
{
	for (弾数分くりかえし)
	{
		i番目の弾の.posの初期化
		SetCharaRect(i番目の弾, SizeF{プレイヤーの弾のRectサイズの幅と高さ});
		i番目の弾の.isAlive の初期化
		i番目の弾の.speed の初期化
		i番目の弾の.moveDir の初期化
		i番目の弾の.texの初期化
	}
}
 
//これはとりあえず空にしとく
void InitEnemyBullet(EnemyGun& _enemhyGun)
{
}

作ったら忘れずに、InitGameData関数で呼び出してあげよう。

キャラクタ(ゲームオブジェクト)の準備ができたら弾を撃ってみる。
弾を撃つ手順は以下の通り

  1. 発射フラグ(isAlive)がOFFの弾があるかサーチ(1個もなければ一時的に弾切れ)
  2. 見つけたら発射する弾丸の発射フラグをONにする
  3. 発射する弾丸のposを自機の位置に設定(自機のグラフィックの機銃とかから出したければ調整する)
  4. 弾丸のUpdate関数で弾丸とRectの位置を更新
  5. 更新した弾丸の位置で弾丸のDraw関数で描画
Fig. 1: 弾丸の管理と自機の位置(弾の発射フラグisActiveじゃなくisAliveだった)

空き弾を探す関数GetBlankBulletを探す

発射された弾は、isAliveがtrueになる(On,OFFで言ってるときのONね)。
つまり発射ボタン(後でキーボードの何かキーに割り当てるとして)が押されたら、全部の弾丸をサーチしてまだ撃てる弾があるか探す。
そんな関数を作る。やることは簡単で全部の弾丸を走査して、途中でisAliveがfalseのものがあればそのインデックスを返すだけである。
こん時に全部の弾丸を走査してもisAliveがfalseの弾が見つからなかったら、弾無し状態としてPLAYER_MAX_BULLET_NUMを返す。
プレイヤーの持ってる弾丸数の配列は0~PLAYER_MAX_BULLET_NUMー1までのインデックスを持つので、PLAYER_MAX_BULLET_NUMが返った時は空きがなかったということが分かる仕組み。

Listing. 6: int GetBlankBulletの作成
gameSequence.cpp
//引数は_datでもいいが、なるべく余計なものはいじらないようにした方がいいのでPlayerGun型のみにしておく
//この関数はプレイヤーの位置とかいらなくて、弾の配列だけ取ってこれれば用が足りるからね。
int GetBlankBullet(PlayerGun& _playerGun)
{
	for (すべての弾丸)
	{
		if (isAliveがfalseのものを見つけたら)
			return i;
	}
	return PLAYER_MAX_BULLET_NUM;
}

空き弾を撃つ関数FirePlayerBulletを追加

空き弾を見つけたら撃つ!撃つべし
撃つのは簡単で、

  1. 発射フラグ(isAlive)がOFFの弾があるかサーチ(1個もなければ一時的に弾切れ)
    • int GetBlankBullet
  2. 見つけたら発射する弾丸の発射フラグをONにする
    • FirePlayerBullet
  3. 発射する弾丸のposを自機の位置に設定(自機のグラフィックの機銃とかから出したければ調整する)
    • FirePlayerBullet
  4. 弾丸のUpdate関数で弾丸とRectの位置を更新
    • UpdatePlayerBullet
  5. 更新した弾丸の位置で弾丸のDraw関数で描画
    • DrawPlayerBullet

void FirePlayerBullet関数を追加し、その中でisAliveフラグのONと、弾丸の発射初期値をプレイヤーの自機のposと同じに設定するだけである。
何回も言うけど、gameSequence.hにプロトタイプ宣言は書いておいてね。

Listing. 7: void FirePlayerBullet関数の作成
gameSequence.cpp
void FirePlayerBullet(プレイヤーの弾とプレイヤーの自機の位置が必要だよ。何を渡す?一度に渡す?分ける?)
//引数は適切なものを渡せば、一度にまとめて渡しても、2つに分かれててもよいです。
//面倒だから1つにしてもいいし、必要最低限だけ渡すために、2つに分けても良い、どちらもそんなに悪い思想ではない。
{
	int n = GetBlankBullet(playerGunからサーチ);
	if (空き球が見つからなかったら){
                Print << U"Empty";  //デバッグ用、後で消す。
		早期におかえり願う(関数から抜ける);
        }
 
	空き弾の.isAlivetrue;
	空き弾の.posを自機の位置に;
}

発射されたら弾丸を描く関数を実装する
これも簡単で、弾丸を全部走査してisAliveがONになっているものだけの.texをposの場所にdrawAtしてやればよい。

Listing. 8: void DrawPlayerBullet関数の追加
gameSequence.cpp
void DrawPlayerBullet(PlayerGun& _playerGun)
{
	for (全部の弾丸をサーチして)
	{
		if (.isAliveがONなら) {
 
			弾丸の.texをPLAYER_BULLET_SIZEにリサイズしてdrawAtでposの位置に描画する笑
		}
	}
}

あとは適当に、updatePlayerの中で、スペースキーの押下を検出してFirePlayerBulletを呼び出してやると発射できるよね!

gameSequence.cpp
void UpdatePlayer(gameData& _dat)
{
	if (KeySpace.down())//スペースキー押されたら
	{
		FirePlayerBullet(_dat); //撃つべし
	}
省略
}

実行結果

Fig. 2: 弾丸の発射の実行結果

聡明な諸君らの頭脳なら、結果は予想できたと思うが、自機の位置にスタンプのように弾丸のグラフィックが張り付けられていったと思う。
その数が、MAX_PLAYER_BULLET_NUMを超えると、弾切れになってグラフィックは出てこなくなる。
とりあえずはこれで発射されたと思ってOKである。
あとは、この弾が発射フラグがオンになると同時にUpdate関数内で位置が更新されてくれれば、moveDir方向にspeedの速さで飛んでいく予定なのである
じゃぁ、次は何するかわかるよね!

次は、UpdatePlayerBullet関数を作って、発射された弾の位置を自動で更新していくようにする。
今回の弾は基本的に初期化で決めたとおり、発射位置から画面情報に向かって一直線に飛んでいく(ものとする)。
ついでに、Player、Enemyの時と同様に、当たり判定に使えるように自分の位置からrectを計算して設定していく。
このままだと、撃った弾は画面外の無限遠点までも位置が更新されてしまう。なので、画面外に弾がでたら死に弾に変更する(isAliveをOFF)

処理は以下のように行う

  • PlayerのすべてのisAliveがtrueの弾について
    1. 弾の次のフレームの位置を、現在のpos、moveDir、speed、Scene::DeltaTimeから計算する(これはも余裕だよね)
    2. posとPLAYER_BULLET_SIZEから、弾丸のrectを設定する
    3. 発射した弾が画面の外に出たら、isAliveをfalseにする

つくっていこう

Listing. 9: UpdatePlayerBullet関数の実装
gameSeauence.cpp
void UpdatePlayerBullet(プレイヤーの銃だけわかれば全弾更新できるよね?)
{
	for (すべての弾丸について)
	{
		if (isAliveがONなら) {
			posの更新
			SetCharaRect(適切なサイズに設定して);
			if (画面外に出たら) {
				isAliveをOFF;
				//デバッグ用
                                //Print << U"FALSE";
			}
		}
	}
}

出来たら、PlayUpdate ⇐(の中で呼び出し)⇐ PlayerUpdate ⇐(の中で呼び出し)⇐ PlayerBulletUpdate で呼び出すようにする

gameSequence.cpp
void UpdatePlayer(gameData& _dat)
{
	if (KeySpace.down())//スペースキー押されたら
	{
		FirePlayerBullet(_dat); //撃つべし
	}
        UpdatePlayerBullet(ごにょごにょ);
省略
       //こっちでUpdatePlayerBullet呼ぶと、キー入力がないときは弾も動かなくなる。どうしてか考えてみよう
}

ついでに、DrawPlayerBulletで、弾丸のrectをDrawFrameで描画するように変更しておこう

実行結果

実行結果は以下のようになるよ。
Updateによって、画面外に出た弾丸がリセットされてもう一度使えるようになっていることが確認できる。
画面内には多くて同時にPLAYER_MAX_BULLET_NUM個の弾丸が描画されるということになる
ちなみに、ファミコン時代にはハードの都合でスプライトの同時表示数が4つまでという制限があり、弾幕シューティングのようなゲームは相当工夫しないと作ることができなかった。

Fig. 3: 弾丸のtexとrectの描画

その4 ついに出た当たり判定 へ

  • game-engineer/classes/2023/something-else/summertime-special-cource/cosmichooligun-3.txt
  • 最終更新: 2年前
  • by root