Skip to content

第05回 2Dシューティングゲーム入門 2 / 配列で弾を複数扱う

前回: 第04回 2Dシューティングゲーム入門 1 / 構造体と衝突判定の基本 / 次回: 第06回 2Dシューティングゲーム入門 3 / 状態管理とゲームオーバー

前回の振り返り

構造体を利用して、基本的な衝突判定を実装しました

  • 構造体 struct について学びました
  • GameObject 構造体を定義し、ゲームキャラクターの位置と大きさの情報を まとめて管理できるようにしました
  • プレイヤーと敵を画像で表示し、衝突判定を行いました 判定結果は色や文字で画面に表示できるようになりました
  • これまでの内容を踏まえて、 今回はシューティングゲームに必須な弾を複数扱う仕組みを学びます

今回の目的

配列で複数の弾を管理できるようにする

  • 弾1発ぶんの情報を構造体として整理できる
  • 配列を使って複数の弾をまとめて扱える
  • 空き要素、発射間隔、再利用の考え方を説明できる
  • ゲームループの中で複数の弾を更新できる
  • 弾の更新と描画を関数に分けて、コードを読みやすくできる

今回の授業内容

2Dシューティングゲーム入門 2 / 配列で弾を複数扱う

  • 弾を複数扱うための考え方
  • Bullet 構造体と配列
  • 空いている要素を使った発射処理
  • 発射間隔の管理
  • 弾の更新、描画、画面外での再利用
  • 弾処理を関数に分ける考え方
  • 前回のゲームループへの組み込み

2Dシューティング ゲーム入門 2

複数の弾を発射する

たくさん表示すると たのしいからね

なぜ配列を使うのか

弾は1発だけでは足りない

  • シューティングでは、前に出した弾が残っている間にも次の弾を出します
  • bulletXbulletY を1組だけ持つ形では、弾は1発しか扱えません
  • 弾ごとに変数を増やすと、発射、移動、描画の処理も増えて管理が大変になります
  • 同じ種類のデータをまとめて扱うために 配列 を使います
  • 今回は Bullet 構造体を配列にして、 複数の弾を同じ処理で更新できるようにします

配列の番号は0から始まる

先頭から何個ずれた場所かを表す

  • 配列の番号は、「1番目、2番目」といった 普段の生活での数え方とは少し異なります
  • コンピューターでは、データの先頭から何個ずれた場所かで 計算した方が効率が良いため、配列の番号も0から始まります
  • 先頭の要素は、先頭から 0個 ずれた場所にあるため 配列の要素を指す最初の番号は 0 になります

配列とは

同じ型の値を順番に並べて持つ仕組み

cpp
int scores[5];
  • 配列は、同じ型のデータを順番に並べて持つための仕組みです 配列の中身は、たとえば scores[0], scores[1], scores[2] のように 先頭から何番目かの番号で取り出すことができます
  • int だけでなく、自分で定義した構造体の型も配列にできます 弾用の構造体 Bullet を作り、配列で複数管理すれば 複数の弾を扱えるようになります

弾専用の構造体を作る

Bullet 構造体を定義する

cpp
struct Bullet
{
    int x; // 弾の x 座標
    int y; // 弾の y 座標
    bool isActive; // 弾が使用中かどうかのフラグ
};
  • Bullet は、弾1発ぶんの情報をまとめるために定義する型です 今回は、弾の位置と使用中かどうかの情報をまとめます
  • 弾が使用中かどうかを表すために、isActive という bool 型の変数を用意します isActivetrue なら使用中、false なら使っていない状態です

※ 弾の命中判定を扱う 大きさの情報は 次回以降に追加します

説明量が増えすぎてしまうので

期待させて ごめんね

ここまでのまとめ

まだ弾1発ぶんの情報を定義しただけ

  • Bullet 構造体で、弾1発に必要な情報を決めました
  • ですが、複数の弾を保存する場所はまだ用意できていません 複数の弾を出すには、Bullet を複数置ける場所が必要です
  • 続いて、同時に発射できる弾の最大数を決めて Bullet の配列を用意します

弾の最大数を定数で決める

途中で変わらない値に名前を付ける

cpp
const int bulletMax = 10;
  • const は、constant 、定数の意味で 後から変更しない値を表すためのキーワードです
  • 数値を 10 と直接書くより bulletMax と名前を付けることで 具体的な意味を読み取りやすくなります
  • また const の値を変更しようとするとコンパイルエラーになるため 誤って値を変えてしまうことを防げます
  • 今回は、同時に扱う弾の最大数として bulletMax を使います

配列で弾を用意する

最大何発までの弾を扱うか決める

cpp
const int bulletMax = 10;
Bullet bullets[bulletMax] = {};
  • ここでは最大 10 発まで同時に扱えるようにしています bullets[0] から bullets[9] までに弾の情報を入れられるようになります
  • = {} と書くと、構造体の中はすべて 0 や false で初期化されます 最初に全部を空き状態にしておくことで、弾を新しく発射する際の下準備が 済んだことになります

配列を使うときの考え方

同じ処理を何度も書かずに済ませたい

  • 弾が10発あると、移動や描画の処理も10発ぶん必要になります 100発なら100発ぶん必要になってしまい、現実的ではありません
  • ですが、弾ごとに処理の内容を変える必要はありません 「いま扱っている弾」を変えながら、同じ処理を順番に実行できれば十分です
  • 同じ形の処理をまとめて繰り返すために、C++ の for 文を使います

for 文とは

同じ処理を順番に繰り返すための書き方

cpp
for (int i = 0; i < bulletMax; i++)
{
    // 繰り返したい処理
}
  • i は何番目を見ているかを表すカウンターの変数です
  • i = 0 から始めて、1回ごとに i++ で 1 ずつ増やします i < bulletMax の間だけ繰り返すので、配列を先頭から順番に調べられます
  • bullets[i] と書くことで、配列の中の i 番目の弾を 1発ずつ処理することができます

中かっこと処理のまとまり

{} の中だけが、その処理の範囲

cpp
for (int i = 0; i < bulletMax; i++)
{
    if (bullets[i].isActive == false)
    {
        // ここは if の中
    }

    // ここは for の中
}
  • iffor では、{} の中に処理を書きます
  • {} の中にある行だけが、 その条件や繰り返しの対象になります これをスコープと呼びます
  • {} が重なって入れ子になっている場合は、 どの {} に入っているかを意識しましょう

空いている弾を探して発射する

配列を先頭から順番に調べて 使っていない場所を見つける

cpp
// 空いている場所を探して弾を1発だけ設定する
for (int i = 0; i < bulletMax; i++)
{
    // 未使用の弾を見つける
    if (bullets[i].isActive == false)
    {
        // 弾の情報を設定して使用中にする
        bullets[i].x = player.x;
        bullets[i].y = player.y;
        bullets[i].isActive = true;
        break;
    }
}
  • 配列内に使っていない場所が見つかったら、 そこに新しい弾の情報を入れます
  • 1発ぶんの情報を設定したら break; して ループを中断しています
  • 弾が見つからなかった場合は、 何もせずにループが終わります この方法で、同時に出せる弾の数を bulletMax で制限できます

発射された弾を更新する

使用中の弾だけを動かす

  • 動かす必要があるのは使用中の弾だけです
  • また、画面の外に出た弾は isActivefalse に戻して 再利用できるようにします
  • これで配列の中の同じ弾を 何度も使いまわせるようになります
cpp
for (int i = 0; i < bulletMax; i++)
{
    // 使用中の弾だけを動かす
    if (bullets[i].isActive)
    {
        // 弾を上に動かす
        bullets[i].y -= 8;

        // 弾が画面外に出たら未使用に戻す
        if (bullets[i].y < -16)
            bullets[i].isActive = false;
    }
}

描画も配列で繰り返す

使っている弾だけ表示する

  • 更新と同じように、 描画の処理も配列全体に対して繰り返します
  • 「発射」「更新」「描画」の処理を すべて配列で扱っている点に注目しましょう
cpp
for (int i = 0; i < bulletMax; i++)
{
    if (bullets[i].isActive)
    {
        DrawCircle(
            bullets[i].x, bullets[i].y, 6,
            GetColor(255, 255, 0), TRUE);
    }
}

弾が出すぎるのを抑える

発射間隔を実装する

cpp
int shotTimer = 0;
const int shotInterval = 10;
  • CheckHitKey(KEY_INPUT_Z) は、キーが押されている間ずっと true になります そのままだと毎フレーム弾が発射されてしまうため、 発射間隔を管理するための変数を用意します
  • shotTimer は、次の弾を撃てるまでの待ち時間です shotInterval は、発射後に何フレーム待つかを表します shotTimer0 のときだけ、次の弾を撃てるようにします

ローカル変数とグローバル変数

「作った場所」で使える範囲が変わる

cpp
 // score はグローバル変数
int score = 0;

int WINAPI WinMain(...)
{
    // playerX はローカル変数
    int playerX = 320;

    for (int i = 0; i < bulletMax; i++)
    {
        // i もローカル変数
    }
}
  • 関数や { } の中で作った変数は、 その範囲の中だけで使える変数になります これを ローカル変数 と呼びます
  • 関数の外で作った変数は どこからでも使える変数になります こちらは グローバル変数 と呼びます
  • 全部の変数をグローバル変数にすると 名前の衝突などが起きやすくなります なるべくローカル変数を使いましょう

今回の変数の置き場所

複数の関数で使うものは外に置く

cpp
const int bulletMax = 10;
const int shotInterval = 10;
const int moveSpeed = 4;

Player player = { 320, 400 };
Bullet bullets[bulletMax] = {};
int shotTimer = 0;
  • 1つの処理だけで使う変数は、 その処理の近くにローカル変数として置きます
  • プレイヤーや弾の状態、発射間隔は、初期化、更新、 描画など複数の処理から使うため、 ローカル変数にはできません
  • 今回は関数からアクセスしやすいように、 これらの変数を WinMain の外に置きます

サンプルコード(1/5)

構造体と共有する変数

  • まずはプレイヤーや弾を扱うための構造体を 宣言します
  • Player 構造体はプレイヤーの位置情報 Bullet 構造体は弾の位置と状態の情報です
  • プレイヤーや弾に関する変数は、複数の処理で使うため WinMain の外に置きます
cpp
#include "DxLib.h"

// プレイヤーの位置を管理する構造体
struct Player
{
    // プレイヤー画像の左上座標
    int x;
    int y;
};

// 発射された弾の状態を管理する構造体
struct Bullet
{
    // 弾の中心座標
    int x;
    int y;
    // 画面上で有効な弾かどうか
    bool isActive;
};

// 同時に存在できる弾数と連射間隔
const int bulletMax = 10;
const int shotInterval = 10;

// プレイヤーの移動速度
const int moveSpeed = 4;

// プレイヤーの初期位置
Player player = { 320, 400 };

// 弾の管理配列
Bullet bullets[bulletMax] = {};

// 連射制御用タイマー
int shotTimer = 0;

サンプルコード(2/5)

弾の更新と描画を関数に分ける

  • UpdateBullets() は、使用中の弾だけを 上方向に動かします 画面外に出た弾は未使用に戻します
  • DrawBullets() は、使用中の弾だけを 表示します
  • 同じ bullets 配列を扱いますが、 更新と描画は別々の処理ですので 関数も分けて書いています
cpp
// 使用中の弾だけを上に動かす
void UpdateBullets()
{
    for (int i = 0; i < bulletMax; i++)
    {
        if (bullets[i].isActive)
        {
            bullets[i].y -= 8;

            if (bullets[i].y < -16)
            {
                bullets[i].isActive = false;
            }
        }
    }
}

// 使用中の弾だけを描画する
void DrawBullets()
{
    for (int i = 0; i < bulletMax; i++)
    {
        if (bullets[i].isActive)
        {
            DrawCircle(
                bullets[i].x, bullets[i].y, 6,
                GetColor(255, 255, 0), TRUE);
        }
    }
}

サンプルコード(3/5)

初期化処理

  • DX ライブラリの初期化に成功したら 続いて画像を読み込みます
  • プレイヤーの位置や移動速度は、 今回は WinMain の外に置いています
  • SetDrawScreen(DX_SCREEN_BACK); によって 裏画面に描画する準備をしています
cpp
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    // ウィンドウモードで起動する
    ChangeWindowMode(TRUE);

    if (DxLib_Init() == -1)
        return -1;

    SetDrawScreen(DX_SCREEN_BACK);

    // プレイヤー画像を読み込む
    int playerHandle = LoadGraph(L"Images/Player.png");

サンプルコード(4/5)

入力と発射処理

  • while の中が毎フレーム実行される ゲームループです
  • 十字キーでプレイヤーを動かします
  • Z キーが押され、 かつ shotTimer0 のときだけ 空いている弾を1発使用中にします
cpp
    while (ProcessMessage() == 0 && CheckHitKey(KEY_INPUT_ESCAPE) == 0)
    {
        // 十字キーでプレイヤーを移動する
        if (CheckHitKey(KEY_INPUT_LEFT)) player.x -= moveSpeed;
        if (CheckHitKey(KEY_INPUT_RIGHT)) player.x += moveSpeed;
        if (CheckHitKey(KEY_INPUT_UP))   player.y -= moveSpeed;
        if (CheckHitKey(KEY_INPUT_DOWN)) player.y += moveSpeed;

        // 連射制御用タイマーを減らす
        if (shotTimer > 0)
            shotTimer--;

        // 空いている場所を探して弾を1発だけ出す
        if (CheckHitKey(KEY_INPUT_Z) && shotTimer == 0)
        {
            for (int i = 0; i < bulletMax; i++)
            {
                if (bullets[i].isActive == false)
                {
                    bullets[i].x = player.x + 12;
                    bullets[i].y = player.y;
                    bullets[i].isActive = true;
                    shotTimer = shotInterval;
                    break;
                }
            }
        }

サンプルコード(5/5)

更新と描画を呼び出す

  • 弾の移動は UpdateBullets() に任せます
  • 描画では、画面を消してからプレイヤーと 弾を表示します
  • ScreenFlip() で、描画した内容を 画面に反映します
cpp
        UpdateBullets();

        // 1フレーム分の描画を行う
        ClearDrawScreen();

        DrawGraph(player.x, player.y, playerHandle, TRUE);
        DrawBullets();

        ScreenFlip();
    }

    DxLib_End();
    return 0;
}

弾を複数扱う仕組みが完成したので

次は命中判定と状態管理を組み込みます

  • 第04回では、プレイヤーと敵が重なったかを調べる命中判定を作りました
  • 今回は弾を複数出していますが、まだ弾と敵の判定は入っていません
  • 第06回では、この命中判定を弾と敵の関係にも広げて、ゲームのルールに組み込みます
  • さらに敵の移動、再出現、enum による状態管理も加えて、 ゲームオーバーやゲームクリアまで含めた流れを作ります

実習: 弾を複数飛ばす

配列を使った弾処理を完成させる

  1. Bullet 構造体を定義し、弾1発ぶんの情報をまとめる
  2. bulletMax を決めて、弾の配列を用意する
  3. shotTimer を用意し、弾が出すぎないようにする
  4. 複数の関数で使う弾の配列を WinMain の外に置く
  5. Z キーで空いている場所に弾を1発登録する
  6. UpdateBullets() で、使用中の弾だけを上へ動かす
  7. DrawBullets() で、使用中の弾だけが表示されることを確認する
  • 余裕があれば、弾の速度や最大数を調整してみましょう

よくあるつまずき

弾が出ないときの確認ポイント

  • Bullet bullets[bulletMax] = {}; の初期化を忘れていないか
  • bulletsUpdateBullets()DrawBullets() より前に定義しているか
  • 発射処理で isActive = true を入れ忘れていないか
  • 更新処理と描画処理の両方で isActive を見ているか
  • break; の位置がずれて、同時に大量発射していないか
  • 弾の y 座標が更新されているか、 画面外に出たときに isActivefalse にしているか

今回のまとめ

今日のポイント

  • 配列を使うと、同じ種類のデータを複数まとめて扱えます

  • 配列と for 文を組み合わせると、複数のオブジェクトをまとめて扱えます

  • 変数を使ってタイマーを作れば、弾の発射間隔を調整するなどの制御が可能です

  • 複数の関数で使う変数は、今回は WinMain の外に置きました 後の回で Game クラスにまとめて整理します

  • 更新や描画を関数に分けると、ゲームループの流れが読みやすくなります

  • これらの考え方は、敵の制御やエフェクトのアニメーションなど 他の要素にもそのまま応用できます

  • 次回は第04回の命中判定と今回の弾処理をつなぎ、 敵の移動や状態管理も加えてゲームを完成に近づけます

おつかれさまでした!

次回予告 第06回 2Dシューティングゲーム入門 3

次はいよいよ ゲームになるよ