弾幕、それは漢の浪漫。この実装を追い求める事がプログラミング技術の向上であると言っても過言で…かもしれません。さて、C# でリストを管理する場合、システムには便利なその名もズバリの List という命令があります。座標を Vector2 で管理するとして、List をとります。使えば…
では、最初にかなり大きい数で、弾の管理を行う配列を作ってしまえば、少なくとも確保と破棄というガーベージコレクションが起きる要素はなくなります。とりあえずままよと 3000個ぐらい作ってしまいましょうか。こういう時は、後から簡単にサイズ変更出来るように、要素数も別途定数定義しておくと良いです。

片方向リストは「次の位置」だけを保持しています。現在の処理が終わったら、次の位置に移動していきます。次の位置がデータが無い値になれば終了となります。

双方向リストは「次の位置」「以前の位置」を保持しています。何かあれば一つ前に戻って処理をする事が出来ます。ただ、片方向と比べて若干ですがデータサイズが大きくなります。
弾幕はかなりの数を処理しますので、小さなサイズアップも大きな差になる事があります。そのため、ここでは片方向リストを採用する事にします。配列の位置はインデックスで管理していますので、先に提示した弾構造体に次の位置を追加しましょう。

たったこれだけです。弾を撃ったら
List<Vector2> bullets = new List<Vector2>();
等とすれば良いですし、弾が当たったり画面外に消えたら
bullets.Add(new Vector2(pos));
とするだけです。めちゃくちゃ簡単ですが、実はコレ、ゲーム制作には大変問題があります。かなりの高頻度で追加と削除を List に対して繰り返しますので、ガーベージコレクションは起きまくると思われますし、なにより List の Remove は悲しいほどに遅いのです。あまりに遅いので以前こんな記事を書いたぐらいです。総じて、遅くてゲームに使うのは厳しいと言わざるを得ません。
bullets.RemoveAt(idxRemove);
- 配列を事前に確保する
おそらく使用しないであろう new Vector2(int.MinValue, int.MinValue) なら使っていないとして、使ったときはここに Vector2 オブジェクトを入れれば良いですよね。…あれ?どうやって動かすんだっけ?こちらで解説したとおり、弾には最低限でも「現在位置」と「移動ベクトル」が必要です。そのため、これを要素とした Bullet 構造体を定義して配列化してしまいます。
const int maxBullet = 3000;
Vector2[] bullets = new Vector2[maxBullet];
※実際には、リストで使用中か空きかを判別していますので、配列の確保だけで問題ありません。
struct Bullet
{
public Vector2 vecPos; // 現在の位置
public Vector2 vecAdd; // 移動ベクトル
}
Bullet[] bullets = new Bullet[maxBullet];
readonly Vector2 vecEmpty = new Vector2(int.MinValue, int.MinValue);
for (int i = 0; i < bullets.Length; ++i)
{
bullets[i] = vecEmpty;
}
- 弾を発射する
さて、この出来た弾のオブジェクトを、空いている弾配列に入れるとしましょう。配列を検索して vecEmpty の場所が空いているので、そこに格納します。
const double DegToRad = 2 * Math.PI / 360;
Bullet blt = new Bullet()
{
vecPos = pos,
vecAdd = new Vector2(
(float)(Math.Sin(angle * DegToRad) * speed),
(float)(Math.Cos(angle * DegToRad) * speed))
}
…ちょっと待ってください。弾を一発撃つ毎に、配列の使用状況を検索する?最初の100や200ならまだしも、数千とか毎回検索するんです?これまた重そうですよね…。
for (int i = 0; i < bullets.Length; ++i)
{
if (bullets[i] != vecEmpty) continue;
bullets[i] = blt;
break;
}
- リスト構造で検索不要に

片方向リストは「次の位置」だけを保持しています。現在の処理が終わったら、次の位置に移動していきます。次の位置がデータが無い値になれば終了となります。

双方向リストは「次の位置」「以前の位置」を保持しています。何かあれば一つ前に戻って処理をする事が出来ます。ただ、片方向と比べて若干ですがデータサイズが大きくなります。
弾幕はかなりの数を処理しますので、小さなサイズアップも大きな差になる事があります。そのため、ここでは片方向リストを採用する事にします。配列の位置はインデックスで管理していますので、先に提示した弾構造体に次の位置を追加しましょう。
さて、最初は全て使っていないので、全ての配列を使っていないリストに繋げてしまいます。使用中リストは最初は何もないので、データなしとして初期化します。
struct Bullet
{
public int idxNext; // 次の弾インデックス
public Vector2 vecPos; // 現在の位置
public Vector2 vecAdd; // 移動ベクトル
}
int idxEmpty = 0; // 未使用リスト
int idxInvalid = -1; // 使用中リスト
for (int i = 0; i < maxBullet - 1; i++)
{
bullets[i].idxNext = i + 1;
}
bullets[maxBullet - 1].idxNext = -1;
初期化で、一番最後にはデータが無い事を示す -1 を入れるのがキモです。このような終端を示す数値を入れる事を「番兵法」と呼びます。これで未使用配列インデックスは idxEmpty に書いてありますし、使用中配列のインデックスは idxInvalid を見れば分かるようになりました。
- 改めて弾を発射する
そして、この弾は使用中リストに接続するため、次のリストに現在使用中のインデックスを書き込んで、自分自身をリストの先頭に入れてしまいます。
if (idxEmpty < 0) return; // 空きがない
int idx = idxEmpty; // 発射するIDX
idxEmpty = bullets[idx].idxNext; // 次の空ワークを最初のIDXの変更
あとは発射位置と移動ベクトルを設定すれば、発射準備完了となります。
bullets[idx].idxNext = idxInvalid; // 現在の使用中IDXを次に入れる
idxInvalid = idx; // 新しい発射IDXを使用中の先頭に
- 弾を削除する
int idxPrev = -1; // 一つ前のIDX
int idxNow = idxInvalid; // IDXを存在開始位置に
while (idxNow >= 0)
{
// 移動する
bullets[idxNow].vecPos += bullets[idxNow].vecAdd;
// 画面内チェック
if (rcCanvas.Contains(new Point(
(int)(bullets[idxNow].vecPos.X + 0.5f),
(int)(bullets[idxNow].vecPos.Y + 0.5f)))
){
idxPrev = idxNow; // 一つ前は現在のIDX
idxNow = bullets[idxNow].idxNext; // 次のIDXに遷移
continue;
}
// 画面範囲外なので消去する
int idxNext = bullets[idxNow].idxNext; // 次のIDXを保存
if (idxPrev < 0) // 以前かない?
{
// 先頭IDXを書き換える
idxInvalid = bullets[idxNow].idxNext;
}
else
{
// 一つ前の次のIDXを現在の次のIDXに書き換える
bullets[idxPrev].idxNext = bullets[idxNow].idxNext;
}
bullets[idxNow].idxNext = idxEmpty; // 現在のIDXの次を空のIDXにして
idxEmpty = idxNow; // 空になったIDXを先頭とする
idxNow = idxNext; // 次のIDXに遷移
}
削除の際、以前のインデックスが -1 なら先頭のデータなので、idxInvalid を次のインデックスに更新します。-1 以外であれば、以前のデータの次のインデックスに、現在のデータに保存されている次のインデックスを設定します。これで、削除しようとしているデータは飛ばされるようになりますので、現在のデータは空になりました。空のデータですので、idxEmpty に接続するため、次のインデックスに現在の idxEmpty の内容を入れて、自分自身は空の先頭位置にするため idxEmpty に格納します。
- 確実に相手を狙う
以前に指摘してますが、Atan2 の引数が場合により逆転している事があります。各自の開発環境上で Atan2 の仕様を確認するようにしてください。
var angle = Math.Atan2(vecTo.X - vecFrom.X, vecTo.Y - vecFrom.Y);




コメント