Appearance
第05回 2Dシューティングゲーム入門 2 / 配列で弾を複数扱う
前回: 第04回 2Dシューティングゲーム入門 1 / 構造体と衝突判定の基本 / 次回: 第06回 2Dシューティングゲーム入門 3 / 状態管理とゲームオーバー
前回の振り返り
構造体を利用して、基本的な衝突判定を実装しました
- 構造体
structについて学びました GameObject構造体を定義し、ゲームキャラクターの位置と大きさの情報を まとめて管理できるようにしました- プレイヤーと敵を画像で表示し、衝突判定を行いました 判定結果は色や文字で画面に表示できるようになりました
- これまでの内容を踏まえて、 今回はシューティングゲームに必須な弾を複数扱う仕組みを学びます
今回の目的
配列で複数の弾を管理できるようにする
- 弾1発ぶんの情報を構造体として整理できる
- 配列を使って複数の弾をまとめて扱える
- 空き要素、発射間隔、再利用の考え方を説明できる
- ゲームループの中で複数の弾を更新できる
- 弾の更新と描画を関数に分けて、コードを読みやすくできる
今回の授業内容
2Dシューティングゲーム入門 2 / 配列で弾を複数扱う
- 弾を複数扱うための考え方
Bullet構造体と配列- 空いている要素を使った発射処理
- 発射間隔の管理
- 弾の更新、描画、画面外での再利用
- 弾処理を関数に分ける考え方
- 前回のゲームループへの組み込み
2Dシューティング ゲーム入門 2
複数の弾を発射する
たくさん表示すると たのしいからね
なぜ配列を使うのか
弾は1発だけでは足りない
- シューティングでは、前に出した弾が残っている間にも次の弾を出します
bulletXとbulletYを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型の変数を用意しますisActiveがtrueなら使用中、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 の中
}ifやforでは、{}の中に処理を書きます{}の中にある行だけが、 その条件や繰り返しの対象になります これをスコープと呼びます{}が重なって入れ子になっている場合は、 どの{}に入っているかを意識しましょう
空いている弾を探して発射する
配列を先頭から順番に調べて 使っていない場所を見つける
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で制限できます
発射された弾を更新する
使用中の弾だけを動かす
- 動かす必要があるのは使用中の弾だけです
- また、画面の外に出た弾は
isActiveをfalseに戻して 再利用できるようにします - これで配列の中の同じ弾を 何度も使いまわせるようになります
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は、発射後に何フレーム待つかを表しますshotTimerが0のときだけ、次の弾を撃てるようにします
ローカル変数とグローバル変数
「作った場所」で使える範囲が変わる
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キーが押され、 かつshotTimerが0のときだけ 空いている弾を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による状態管理も加えて、 ゲームオーバーやゲームクリアまで含めた流れを作ります
実習: 弾を複数飛ばす
配列を使った弾処理を完成させる
Bullet構造体を定義し、弾1発ぶんの情報をまとめるbulletMaxを決めて、弾の配列を用意するshotTimerを用意し、弾が出すぎないようにする- 複数の関数で使う弾の配列を
WinMainの外に置く Zキーで空いている場所に弾を1発登録するUpdateBullets()で、使用中の弾だけを上へ動かすDrawBullets()で、使用中の弾だけが表示されることを確認する
- 余裕があれば、弾の速度や最大数を調整してみましょう
よくあるつまずき
弾が出ないときの確認ポイント
Bullet bullets[bulletMax] = {};の初期化を忘れていないかbulletsをUpdateBullets()やDrawBullets()より前に定義しているか- 発射処理で
isActive = trueを入れ忘れていないか - 更新処理と描画処理の両方で
isActiveを見ているか break;の位置がずれて、同時に大量発射していないか- 弾の
y座標が更新されているか、 画面外に出たときにisActiveをfalseにしているか
今回のまとめ
今日のポイント
配列を使うと、同じ種類のデータを複数まとめて扱えます
配列と
for文を組み合わせると、複数のオブジェクトをまとめて扱えます変数を使ってタイマーを作れば、弾の発射間隔を調整するなどの制御が可能です
複数の関数で使う変数は、今回は
WinMainの外に置きました 後の回でGameクラスにまとめて整理します更新や描画を関数に分けると、ゲームループの流れが読みやすくなります
これらの考え方は、敵の制御やエフェクトのアニメーションなど 他の要素にもそのまま応用できます
次回は第04回の命中判定と今回の弾処理をつなぎ、 敵の移動や状態管理も加えてゲームを完成に近づけます