弾を発射したい。
まずは弾の画像を用意しよう。これも何でもよいです。どうせ描画するとき画像サイズ調整するし
初めに画像の用意のところで使ったサイトなどから適当に弾丸用の画像を用意しよう。
僕はここのやつ
プレイヤーと敵のキャラクターを作った時のように、キャラクターの構造体を作って、弾一つずつに画像(textureAsset)を割り当てて描画する
自機と敵から出る弾に対する定数たちを設定していく(既出のものもあり)
//弾丸の表示サイズの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登録!
void Main() { 省略 \\弾丸画像の登録 TextureAsset::Register(U"BULLET", U"images\\shots\\1.png"); 省略
//プレイヤーの機銃構造体
struct PlayerGun
{
gameChar PlayerBullet[プレイヤーの弾丸の最大値の定数決めたよね?];
};
//敵の機銃構造体
struct EnemyGun
{
gameChar EnemyBullet[敵の弾丸の最大値の定数決めたよね?];
};
それで、こいつらをgameDataに追加しちゃうんです
ってことは、PlayerGunとEnemyGunはgameDataよりは前に宣言されてないとダメだよね。
struct gameData { GAME_STATE gState; //プレイヤーキャラ関係 gameChar Player; PlayerGun playerGUN; //敵キャラ関係 gameChar Enemy; EnemyGun enemyGUN; };
これで、弾のデータもいろんなUpdate、Draw関数に一緒に渡すことができる。
次はどうする?
そうね。今までもそうだったけど、データ作ったら初期化関数!
初期化関数をPlayerGun用とEnemyGun用に作るんだけど、
とりあえず、プレイヤーの弾だけ作ってみよう。
InitEnemyBulletの中身は空にしとけば問題ない。
void InitPlayerBullet(PlayerGun& _playerGun); void InitEnemyBullet(EnemyGun& _enemyGun);
実装は以下の様にする。まぁ、プレイヤーキャラと敵キャラの初期化と変わらん感じでやれる
これをPlayerGunが持っている弾丸数分おこなう。
総合すると
void InitPlayerBullet(PlayerGun& _playerGun) { for (弾数分くりかえし) { i番目の弾の.posの初期化 SetCharaRect(i番目の弾, SizeF{プレイヤーの弾のRectサイズの幅と高さ}); i番目の弾の.isAlive の初期化 i番目の弾の.speed の初期化 i番目の弾の.moveDir の初期化 i番目の弾の.texの初期化 } } //これはとりあえず空にしとく void InitEnemyBullet(EnemyGun& _enemhyGun) { }
作ったら忘れずに、InitGameData関数で呼び出してあげよう。
キャラクタ(ゲームオブジェクト)の準備ができたら弾を撃ってみる。
弾を撃つ手順は以下の通り
発射された弾は、isAliveがtrueになる(On,OFFで言ってるときのONね)。
つまり発射ボタン(後でキーボードの何かキーに割り当てるとして)が押されたら、全部の弾丸をサーチしてまだ撃てる弾があるか探す。
そんな関数を作る。やることは簡単で全部の弾丸を走査して、途中でisAliveがfalseのものがあればそのインデックスを返すだけである。
こん時に全部の弾丸を走査してもisAliveがfalseの弾が見つからなかったら、弾無し状態としてPLAYER_MAX_BULLET_NUMを返す。
プレイヤーの持ってる弾丸数の配列は0~PLAYER_MAX_BULLET_NUMー1までのインデックスを持つので、PLAYER_MAX_BULLET_NUMが返った時は空きがなかったということが分かる仕組み。
//引数は_datでもいいが、なるべく余計なものはいじらないようにした方がいいのでPlayerGun型のみにしておく //この関数はプレイヤーの位置とかいらなくて、弾の配列だけ取ってこれれば用が足りるからね。 int GetBlankBullet(PlayerGun& _playerGun) { for (すべての弾丸) { if (isAliveがfalseのものを見つけたら) return i; } return PLAYER_MAX_BULLET_NUM; }
空き弾を見つけたら撃つ!撃つべし
撃つのは簡単で、
void FirePlayerBullet関数を追加し、その中でisAliveフラグのONと、弾丸の発射初期値をプレイヤーの自機のposと同じに設定するだけである。
何回も言うけど、gameSequence.hにプロトタイプ宣言は書いておいてね。
void FirePlayerBullet(プレイヤーの弾とプレイヤーの自機の位置が必要だよ。何を渡す?一度に渡す?分ける?) //引数は適切なものを渡せば、一度にまとめて渡しても、2つに分かれててもよいです。 //面倒だから1つにしてもいいし、必要最低限だけ渡すために、2つに分けても良い、どちらもそんなに悪い思想ではない。 { int n = GetBlankBullet(playerGunからサーチ); if (空き球が見つからなかったら){ Print << U"Empty"; //デバッグ用、後で消す。 早期におかえり願う(関数から抜ける); } 空き弾の.isAliveをtrueに; 空き弾の.posを自機の位置に; }
発射されたら弾丸を描く関数を実装する
これも簡単で、弾丸を全部走査してisAliveがONになっているものだけの.texをposの場所にdrawAtしてやればよい。
void DrawPlayerBullet(PlayerGun& _playerGun) { for (全部の弾丸をサーチして) { if (.isAliveがONなら) { 弾丸の.texをPLAYER_BULLET_SIZEにリサイズしてdrawAtでposの位置に描画する笑 } } }
あとは適当に、updatePlayerの中で、スペースキーの押下を検出してFirePlayerBulletを呼び出してやると発射できるよね!
void UpdatePlayer(gameData& _dat) { if (KeySpace.down())//スペースキー押されたら { FirePlayerBullet(_dat); //撃つべし } 省略 }
聡明な諸君らの頭脳なら、結果は予想できたと思うが、自機の位置にスタンプのように弾丸のグラフィックが張り付けられていったと思う。
その数が、MAX_PLAYER_BULLET_NUMを超えると、弾切れになってグラフィックは出てこなくなる。
とりあえずはこれで発射されたと思ってOKである。
あとは、この弾が発射フラグがオンになると同時にUpdate関数内で位置が更新されてくれれば、moveDir方向にspeedの速さで飛んでいく予定なのである
じゃぁ、次は何するかわかるよね!
次は、UpdatePlayerBullet関数を作って、発射された弾の位置を自動で更新していくようにする。
今回の弾は基本的に初期化で決めたとおり、発射位置から画面情報に向かって一直線に飛んでいく(ものとする)。
ついでに、Player、Enemyの時と同様に、当たり判定に使えるように自分の位置からrectを計算して設定していく。
このままだと、撃った弾は画面外の無限遠点までも位置が更新されてしまう。なので、画面外に弾がでたら死に弾に変更する(isAliveをOFF)
処理は以下のように行う
void UpdatePlayerBullet(プレイヤーの銃だけわかれば全弾更新できるよね?) { for (すべての弾丸について) { if (isAliveがONなら) { posの更新 SetCharaRect(適切なサイズに設定して); if (画面外に出たら) { isAliveをOFF; //デバッグ用 //Print << U"FALSE"; } } } }
出来たら、PlayUpdate ⇐(の中で呼び出し)⇐ PlayerUpdate ⇐(の中で呼び出し)⇐ PlayerBulletUpdate で呼び出すようにする
void UpdatePlayer(gameData& _dat) { if (KeySpace.down())//スペースキー押されたら { FirePlayerBullet(_dat); //撃つべし } UpdatePlayerBullet(ごにょごにょ); 省略 //こっちでUpdatePlayerBullet呼ぶと、キー入力がないときは弾も動かなくなる。どうしてか考えてみよう }
ついでに、DrawPlayerBulletで、弾丸のrectをDrawFrameで描画するように変更しておこう
実行結果は以下のようになるよ。
Updateによって、画面外に出た弾丸がリセットされてもう一度使えるようになっていることが確認できる。
画面内には多くて同時にPLAYER_MAX_BULLET_NUM個の弾丸が描画されるということになる
ちなみに、ファミコン時代にはハードの都合でスプライトの同時表示数が4つまでという制限があり、弾幕シューティングのようなゲームは相当工夫しないと作ることができなかった。