【C#】【Unity】リストのループ処理をしながら要素を削除する

スポンサーリンク

UnityでC#を使ってゲーム開発をしていると、リスト管理しているGameObjectから一定の条件に当てはまるものを削除したい場面がありますよね。
これを実装するのは難しくないのですが……実はちゃんと考えないと思わぬ不具合が起こります。
ですので、今回は、そういった場面での削除する方法について少し考えてみようと思います。

スポンサーリンク

今回想定するケース

私は今、落ちゲーを作っているので下記ケースを想定します。

  • フィールドに積まれているブロック(GameObject)をリストで管理
  • ブロックが消される条件に当てはまったブロックは、消去アニメーション等を行い、それが終わると非表示にする(activeをFalse)
    • 実際にはactiveでは無く、独自プロパティを作ったりしていますが、それは蛇足になるのでここではactive制御とします
  • 非表示になったものを削除(Destroy)してリストから除外する

1. 真っ先に考えるパターン(NGパターン)

真っ先に思いつくのは、次のコードではないでしょうか?

// blocksがブロックを管理しているリストです List<GameObject> blocks;
foreach(GameObject block in blocks) {
  if (!block.activeSelf) {
    Destroy(block);
    blocks.Remove(block); // ★ エラーが発生する可能性があります
  }
}

この実装は、一見正しく見えるかもしれません。
ですが、リストをforeachループで回しながら要素を削除してしまうと、途中で要素の数・順番が変わってしまう為、思わぬエラーが発生してしまう可能性があります。
(恐らく、上記コードを実行すると警告が発生するのではないでしょうか?)
基本的に、リストの中身をループで処理している時には要素の追加や削除、置き換えや順番の変更など行わない方が安全です。

2. 安全パターン

ではどうすれば安全なのか?
リストをループで回している間に削除するのが危険なのであれば、一時的な削除対象リストを作成して、最後に一気に削除すれば良いのです。

List<GameObject> deleteObjects = new List<GameObject>();
foreach(GameObject block in blocks) {
  if (!block.activeSelf) {
    deleteObjects.Add(block);
  }
}
foreach (GameObject block in deleteObjects) {
  blocks.Remove(block);
  Destroy(block);
}

この方法では、1つ目のループで削除対象を集めて、2つ目のループで削除しています。
2つ目のループは一時リストdeleteObjectsでのforeachループになっていますので、この中でblocksの要素を削除しても安全です。
Destroyも2つ目のループで行っているのは、削除されているはずのObjectを操作しようとするのもエラーとなる可能性があるからです。

3. 効率化パターン

2つ目のパターンでも問題ないのですが、ループを2回まわしたり、Remove(block)で要素を削除するするのは少し効率が悪いです。
ですので、最後に、少し上級者向けですが、効率的な方法を考えてみます。

for (int i = blocks.Count - 1; i >= 0; i--) {
  GameObject block = blocks[i];
  if (!block.activeSelf) {
    blocks.RemoveAt(i);
    Destroy(block);
  }
}

この方法では、foreachでのループではなく、forループにしてリストの末尾から先頭に向かってインデックスを使って処理しています。
こうすると、要素を削除してもそれより前のインデックスがずれる事が無いので、エラーを回避できます。
また、RemoveAtでインデックスを指定して削除しているので、削除パフォーマンスの向上も期待できます。
Removeでの削除では、その要素がリストのどこに存在するのか不明なので、最悪リストの全検索を行う必要が出てくる為、要素の場所を指定するRemoveAtを使う方が効率が良くなると思われます)

まとめ

リストの要素を削除する際には、foreachループ内で直接削除しないように気をつけましょう。
初心者の方には、一時的なリストを使う安全パターンをお勧めします。慣れてきたら、効率化パターンを試してみてください。
今回はC#での実装で考えましたが、基本的には他の言語でも通用する考え方と思いますので、参考にしてみて下さいね。

スポンサーリンク