Appearance
第05回 弾を撃つ
前回: 第04回 構造体と矩形 / 次回: 第06回 敵と衝突判定
今回の授業内容
- 弾を表す構造体を追加する
- 固定配列と有効フラグで複数の弾を管理する仕組み(オブジェクトプール)を理解する
- ショットキーで弾を発射し、連射間隔を制御する
- 画面外へ出た弾を無効化して枠を再利用する
- 上限数の考え方を、発射間隔と弾の寿命から見積もる
今回の開始状態
第04回終了時点では、プレイヤーが移動し、当たり判定の矩形を確認できます
今回の終了時点では、ZまたはSpaceで弾を撃てるようにします
まだBulletクラスは作りません
第05回では、Game内に弾用の構造体と配列を置いて、まず固定配列管理の流れを理解します
変更するファイル
| ファイル | 変更内容 |
|---|---|
Game.h | Bullet構造体、弾配列、連射間隔を追加する |
Game.cpp | 発射、更新、描画、無効化を追加する |
なぜ固定配列で管理するのか
弾のように何度も出現するオブジェクトを扱う方法として、発射のたびにnewで作り、消えたらdeleteする方法も考えられます
しかしこの方法には問題があります
毎フレーム何度もnewとdeleteを行うと、その都度メモリ確保の処理が発生し、負荷や確保先の断片化につながります
また、deleteし忘れたポインタが残るとメモリリークになり、逆に使用中のオブジェクトを誤ってdeleteすると不正アクセスになります
そこで、この授業では最初から決まった数の弾を配列として確保しておき、使う・使わないをboolの値(有効フラグ)で切り替える方法を使います
これをオブジェクトプールと呼びます
生成と破棄をメモリ確保ではなく「フラグの切り替え」だけで行うため、負荷が小さく、動きも予測しやすくなります
どの要素が今使われているかをデバッグ表示で確認しやすいことも利点です
弾のデータを用意する
Game.hに、弾を表す構造体を追加します
cpp
struct Bullet
{
Vec2 position;
float speed;
bool active;
};続けて、Gameクラスに弾の配列を追加します
cpp
static constexpr int maxBullets = 48;
Bullet bullets[maxBullets] = {};
int shotCooldown = 0;配列の要素は最初から48個あります
画面上で使っている弾だけをactive == trueとして扱い、使っていない要素はactive == falseのまま置いておきます
上限数48の根拠
上限数は当てずっぽうで決めるのではなく、発射間隔と弾が画面に残る時間から見積もります
この授業では連射間隔を8フレームにするため、1秒間(60フレーム)に撃てる弾は最大で60 / 8 = 7.5発です
弾の速度は8ピクセル/フレームなので、画面上端(y座標0付近)からプレイヤー位置まで約480ピクセルを飛ぶとすると、1発が画面に残る時間はおよそ480 / 8 = 60フレーム、つまり約1秒です
発射間隔と滞空時間がどちらも約1秒であれば、同時に画面上に存在する弾はおよそ7〜8発程度になりますmaxBullets = 48は、この見積もりに対して十分な余裕を持たせた値です
上限に余裕を持たせておくと、プレイヤーが連射のタイミングをずらしたり、後の回で連射速度を上げたりしても、弾が上限で消えてしまう心配が少なくなります
弾を発射する
Update()の中で、ショットキーが押されていて、連射待ちが終わっているかを確認します
cpp
if ((input.IsDown(KEY_INPUT_Z) || input.IsDown(KEY_INPUT_SPACE)) && shotCooldown <= 0)
{
SpawnBullet();
shotCooldown = 8;
}
if (shotCooldown > 0)
--shotCooldown;shotCooldownは「次の発射まで残り何フレームか」を表すカウンターです
発射した直後に8を代入し、毎フレーム1ずつ減らしますshotCooldownが0以下のときだけ発射できるようにすることで、キーを押しっぱなしにしても8フレームに1回しか弾が出ないようになります
SpawnBullet()では、配列を先頭から調べ、最初に見つかった空いている要素(active == false)へ初期値を入れます
cpp
void Game::SpawnBullet()
{
for (Bullet& bullet : bullets)
{
if (!bullet.active)
{
bullet.position = { playerPosition.x, playerPosition.y - 22.0f };
bullet.speed = 8.0f;
bullet.active = true;
return;
}
}
}空いている要素を見つけたらreturnで抜けるのは、1回の発射で1発だけ生成するためですreturnを忘れると、ループが最後まで回り、空いているすべての要素に弾が生成されてしまいます
配列を最後まで調べても空きが見つからない場合、ループはそのまま終了し、何も発射されません
これは不具合ではなく、同時に出せる弾の上限に達したという仕様です
上限に達したときにエラーを出したり強制的に古い弾を消したりする必要はありません
弾を更新して描画する
更新では、使っていない弾を飛ばし、使っている弾だけ上へ動かします
cpp
for (Bullet& bullet : bullets)
{
if (!bullet.active)
continue;
bullet.position.y -= bullet.speed;
if (bullet.position.y < -16.0f)
bullet.active = false;
}activeな弾だけを動かし、画面上端より上に出たらactive = falseで無効化します
無効化した弾のpositionは書き換えませんが、次にSpawnBullet()で再利用されたときに新しい値で上書きされるため、古い値が残っていても問題はありません
描画も同じように、activeな弾だけを描きます
cpp
for (const Bullet& bullet : bullets)
{
if (!bullet.active)
continue;
DrawBox(
static_cast<int>(bullet.position.x - 3),
static_cast<int>(bullet.position.y - 10),
static_cast<int>(bullet.position.x + 3),
static_cast<int>(bullet.position.y + 10),
yellow,
TRUE
);
}更新と描画の両方で「有効なものだけ処理する」という同じ条件を使っています
この条件がずれると、無効なはずの弾が描画されたり、逆に有効な弾が更新されなかったりする不具合につながります
動作確認
- ZまたはSpaceで弾が出ることを確認する
- 弾が上方向へ移動することを確認する
- 画面外へ出た弾が消えることを確認する
maxBulletsを3へ変更し、画面内に3発残っている間は新しい弾が出ないことを確認する- 確認後、
maxBulletsを48へ戻す
よくある問題
- 弾が1発しか出ない場合は、空き弾を探すループと
activeの切り替えを確認する。特にSpawnBullet()内のreturnが抜けていないか確認する - 弾が消えない場合は、画面外で
active = falseに戻しているか確認する - 連射が速すぎる場合は、
shotCooldownの値を確認する - 弾の初期位置がプレイヤーからずれる場合は、
playerPositionを基準にしているか確認する
今回の完成条件
- 固定配列で複数の弾を管理できる
- 有効フラグの役割と、オブジェクトプールを使う理由を説明できる
- ショットキーで弾を発射できる
- 画面外へ出た弾を無効化し、枠を再利用できる
- 上限数を発射間隔と滞空時間から見積もる考え方を説明できる
まとめ
今回は、固定配列と有効フラグで複数の弾を扱うオブジェクトプールを実装しました
次回は敵を追加し、弾との衝突でスコアを増やします