スポンサーリンク
Unity C#・スクリプト実装

UnityのListにオブジェクトを追加・削除するには?クリック・接触・切り替えまで解説

Unity C#・スクリプト実装

Unityでゲームを作っていると、「クリックしたオブジェクトを覚えておきたい」「触れたアイテムを記録したい」「武器を切り替えたい」といった場面によく出会いますよね。

そんなとき頼りになるのが配列やListですが、いざ実装しようとすると「どっちを使えばいいの?」「追加や削除はどう書くの?」と手が止まってしまう方も多いはず。

この記事では、配列とListの使い分けから、クリック・接触・切り替えといった具体的な実装方法、そして初心者がつまずきやすいポイントまで、順番に見ていきます。


配列とListどちらを使うべきか

結論から言うと、選ぶ基準はオブジェクトの数がゲーム中に増減するかどうかの一点だけです。増減するならList、最初から最後まで数が変わらないなら配列を選べば、実装で迷うことはほとんどありません。

「なんとなくListの方が便利そうだから」で全部Listにしてしまう方もいますが、用途に合わない選び方をすると、後からコードが読みにくくなったり、不要な処理が増えたりすることがあります。まずは自分が管理したいオブジェクトが「増えたり減ったりするもの」なのかを考えてみましょう。

数の増減で判断する基準

迷ったときは、以下のチェックリストで自分の状況と照らし合わせてみてください。

✅ ゲームの進行に応じてオブジェクトが増えたり減ったりする
✅ 最初にいくつ用意すればいいか、作る前の時点でわからない
✅ プレイヤーの行動によって管理する数が変わる

これらに当てはまるならListが向いています。逆に、武器の種類やキャラクターの候補など、ゲームを作る段階で数が決まっているものは配列で管理する方がシンプルです。

この基準さえ押さえておけば、あとで「これは配列で作るべきだった」と書き直す手間も減らせますよ。配列・List・Dictionaryの違いをもう少し詳しく知りたい方は、こちらの記事もあわせて参考にしてみてください。

配列でまとめる場合の例

候補があらかじめ決まっているケースでは、配列を使ってオブジェクトをまとめておくとスッキリ管理できます。たとえば、複数の武器モデルやキャラクターのスキンなどが代表的な例です。

このように「候補は決まっているけれど、まとめて扱いたい」というシーンでは、タグなどを使って対象のオブジェクトを配列に集める方法が便利です。具体的なやり方は、以下の記事で詳しく解説しています。

ここから先は、実際に手を動かしながら「Listへの追加・削除」を実装していきましょう。まずはクリックしたオブジェクトをListに記録する方法から見ていきます。




クリックしたオブジェクトをListに追加・削除する方法

クリックしたオブジェクトをListで管理したい場合は、Raycastでクリック位置を検知し、そのオブジェクトをListに追加・削除する方法が基本になります。ここでは実際の手順とコードを見ながら進めていきましょう。

まずは、クリック判定を行いたいオブジェクトの準備からです。

  1. Unityのエディター上で、クリック判定をしたいオブジェクト(CubeやSphereなど)をシーンに配置する
  2. そのオブジェクトのInspectorを開き、Box ColliderSphere ColliderなどのColliderコンポーネントがアタッチされているか確認する

Colliderが付いていないと、マウスから飛ばしたRay(光線)がオブジェクトにぶつかったことを検知できません。クリックしても反応しない場合は、まずここを疑ってみてください。

次に、Listで管理するためのスクリプトを作成します。

  1. Projectビューで右クリックし、CreateC# Scriptを選択する
  2. スクリプト名をClickObjectListManagerに変更する
  3. スクリプトを開き、以下のコードを記述する
using System.Collections.Generic;
using UnityEngine;

public class ClickObjectListManager : MonoBehaviour
{
    // 選択されたオブジェクトを格納するList
    private List<GameObject> selectedObjects = new List<GameObject>();

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;

            if (Physics.Raycast(ray, out hit))
            {
                GameObject clickedObject = hit.collider.gameObject;

                if (!selectedObjects.Contains(clickedObject))
                {
                    selectedObjects.Add(clickedObject);
                    Debug.Log("Listに追加されました: " + clickedObject.name);
                }
            }
        }

        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (selectedObjects.Count > 0)
            {
                GameObject removedObject = selectedObjects[0];
                selectedObjects.RemoveAt(0);
                Debug.Log("Listから削除されました: " + removedObject.name);
            }
        }
    }
}

コードが書けたら、あとは動かして確認するだけです。

  1. Hierarchyビューで右クリックし、Create Emptyで空のオブジェクトを作成する(名前はListManagerなど)
  2. 作成したClickObjectListManagerスクリプトを、そのオブジェクトにドラッグ&ドロップでアタッチする
  3. ゲームを再生し、Collider付きのオブジェクトをクリックしてConsoleに追加ログが出るか確認する
  4. スペースキーを押して、削除ログが出るか確認する

もしif文やfor文の書き方に不安がある場合は、先にC#の基本を押さえておくとこのあとの内容もスムーズに理解できます。

追加時に重複登録を防ぐ処理

同じオブジェクトを何度もクリックすると、そのたびにListへ追加されてしまいそうに感じるかもしれません。ですが、上のコードではContains()を使って「すでにListに入っているかどうか」を先にチェックしています。

そのため、一度追加したオブジェクトを再度クリックしても、Listの中身が重複することはありません。この一手間があるおかげで、あとから「同じアイテムが何個も並んでいる」といった不具合を防げます。

削除時に安全に処理する方法

削除の処理でポイントになるのは、selectedObjects.Count > 0という条件を先に確認している部分です。Listが空の状態で削除しようとすると、実行時エラーが発生してゲームが止まってしまいます。

今回のコードでは、Listの先頭(インデックス0番目)をRemoveAt(0)で取り除いていますが、これは「一番古く登録されたものから順番に削除する」という考え方です。

用途によっては、最後に追加したものから削除したい場合もあると思うので、その際はインデックスの指定をselectedObjects.Count - 1に変えれば対応できます。




接触したオブジェクトをListに自動で集める方法

「プレイヤーがアイテムに触れただけで、自動的にListへ記録したい」という場合は、Trigger(接触判定)を使って、触れた瞬間にListへ追加する方法が使えます。クリックのように操作する必要がないぶん、収集系のシステムと相性がいい実装です。

まずは、アイテム側の当たり判定を設定していきましょう。

  1. アイテムとして扱うオブジェクトをシーンに配置する
  2. InspectorのColliderコンポーネントにあるIs Triggerにチェックを入れる
  3. アイテムのTagを新規作成し、Itemという名前でアタッチする

Tagを付けておくと、プレイヤーが床や壁に触れたときに誤ってListへ追加されてしまうミスを防げます。「触れたものすべてを記録する」のではなく、「Itemタグが付いたものだけを記録する」という条件を後で使うためのひと手間です。

次に、プレイヤー側にも設定が必要です。

  1. プレイヤー(または接触を検知させたいオブジェクト)を選択する
  2. InspectorのAdd ComponentからRigidbodyを追加する

ここまで準備できたら、収集用のスクリプトを作成します。

  1. 新しいC#スクリプトを作成し、名前をItemCollectorにする
  2. 以下のコードを記述する
using System.Collections.Generic;
using UnityEngine;

public class ItemCollector : MonoBehaviour
{
    // 回収したアイテムを記録するList
    private List<GameObject> collectedItems = new List<GameObject>();

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Item"))
        {
            GameObject itemObject = other.gameObject;

            if (!collectedItems.Contains(itemObject))
            {
                collectedItems.Add(itemObject);
                Debug.Log("アイテムを取得して記録しました: " + itemObject.name);

                itemObject.SetActive(false);
            }
        }
    }
}

最後に、動作を確認しましょう。

  1. 作成したItemCollectorスクリプトを、Rigidbodyが付いているプレイヤーにドラッグ&ドロップでアタッチする
  2. ゲームを再生し、プレイヤーをItemタグの付いたオブジェクトに接触させる
  3. アイテムが非表示になり、Consoleに取得ログが表示されるか確認する

触れたオブジェクトの名前を画面に表示したい場合は、こちらの記事も参考になります。

Is TriggerとRigidbodyが両方必要な理由

「Is Triggerにチェックを入れたのに反応しない」という相談は意外と多いのですが、原因のほとんどはRigidbodyの付け忘れです。Unityの仕様上、接触を検知するにはColliderが両方に付いていること、そしてどちらか一方にRigidbodyが付いていることの両方が必要になります。

ColliderだけでRigidbodyが付いていない場合、Is Triggerを有効にしていてもOnTriggerEnterは呼び出されません。反応しないときは、プレイヤー側かアイテム側のどちらかにRigidbodyが付いているか、まず確認してみてください。

今回はプレイヤー側にRigidbodyを付けましたが、アイテム側に付けても同じように検知できます。どちらに付けるかは、動かしたいオブジェクトの都合に合わせて選んで問題ありません。




Listから削除してもオブジェクトが消えない理由

myList.Remove(target)を実行したのに、シーン上のオブジェクトがそのまま残っていて驚いたことはありませんか。実はこれ、バグではなくListの仕組み上そうなるのが正しい動作です。

C#のList<GameObject>は、オブジェクトそのものを箱の中に入れているわけではありません。実際に保持しているのは「そのオブジェクトがメモリ上のどこにあるか」を示す参照(アドレスのようなもの)だけです。

つまり、Listから削除する操作は「メモ帳に書いた名前を消しゴムで消す」ようなイメージに近く、名前を消したからといって、その人自身がいなくなるわけではないのと同じです。画面からオブジェクトを完全になくしたい場合は、削除の処理とは別にDestroy(target)を呼び出す必要があります。

myList.Remove(target)だけではオブジェクトは消えません。「Listからの削除」と「シーンからの削除」は別の処理だと覚えておくと、収集システムなどを作るときの見落としを防げます。

たとえば、アイテムを拾ったときに「Listに追加しつつ画面から消す」という処理を作る場合は、Listへの追加とは別にSetActive(false)Destroy()を組み合わせる必要があります。「Listを操作すれば画面の見た目も勝手に変わる」と思い込まないようにしておくと、実装時の混乱を減らせますよ。




foreach中に削除するとエラーになる原因と対処法

Listの中身をひとつずつ確認しながら削除しようとして、foreachループの途中でいきなりエラーが出た経験はありませんか。これは、foreachでループしている最中に、そのListの中身を変更してはいけないというC#の仕様が原因です。

foreachは「今、何番目の要素を見ているか」という情報を内部で管理しながら順番に処理を進めています。ところがループの途中でRemove()などを使ってListの中身を変えてしまうと、この管理情報とズレが生じてしまい、エラーとして検知される仕組みになっています。

この問題を避けるには、次のどちらかの方法を使います。

✅ 最後尾から逆順にforループを回して削除する
✅ 元のListを複製したリストを作り、複製の方をループしながら元のListを操作する

実務でよく使われるのは、シンプルに書ける逆順forループの方法です。実際のコードを見てみましょう。

// 安全な削除:逆順forループを使用する例
for (int i = myList.Count - 1; i >= 0; i--)
{
    if (条件)
    {
        myList.RemoveAt(i);
    }
}

後ろから順番に確認していくのがポイントです。先頭から削除していくと、削除するたびに残りの要素の番号がひとつずつズレてしまい、うっかり確認し忘れる要素が出てきてしまいます。後ろから処理すれば、そのズレの影響を受けずに済みます。

foreachの途中でList.Add()やRemove()を書きたくなったら要注意です。追加の場合も同じエラーが起きるので、要素を変更する処理は必ずforループか複製リストに切り替えましょう。

「なぜかエラーが出る」という状態に遭遇したときは、まずforeachの中でListを直接いじっていないかを確認してみてください。それだけで解決することがほとんどです。




よくある誤解・注意点

ここまでの実装とは別に、Listを扱ううえで知っておくと役立つポイントをまとめておきます。どれも「知らずに使うとハマりやすい」内容なので、一度目を通しておくと安心です。

まず気をつけたいのが、Listを=で別の変数に代入したときの挙動です。

List<int> listA = new List<int>(){ 1, 2, 3 };
List<int> listB = listA; // これを行うと、同じリストの実体を参照する
listB.Add(4);            // listAにも「4」が追加されてしまう

Listは参照型なので、=で代入すると「同じ実体」を指すコピーが作られるだけで、中身が独立した別物になるわけではありません。listBに何か追加すると、listAの中身も一緒に変わってしまいます。もし独立したコピーが欲しい場合は、次のようにnewを使って新しいListとして作り直す必要があります。

List<int> listB = new List<int>(listA); // 独立した新しいListとして複製される

次に気になるのが、Contains()Find()を使うときの負荷です。これらのメソッドは、Listの先頭から順番に1件ずつ確認していく仕組みで動いています。要素数が少ないうちは気にする必要はありませんが、扱うオブジェクトの数が多く、しかもUpdate()のように毎フレーム呼び出される処理の中でこれらを多用すると、環境によっては動作が重く感じられることがあります。

大量のオブジェクトを高速に検索・管理したい場合は、DictionaryHashSetといった別のデータ構造を検討する余地があります。ただしこのあたりは奥が深いテーマなので、本記事では概念の紹介にとどめておきます。

最後に、ListのCountCapacityの違いについても触れておきます。

ワンポイント

Countは「実際に入っている要素の数」、Capacityは「内部で確保している領域の大きさ」を指します。要素の増減のたびに領域を再確保すると負荷がかかるため、CapacityはCountより大きい状態になっていることがあります。メモリ使用量を厳密に抑えたい場合は、TrimExcess()を実行すると余分な領域を解放できます。

普段の実装でここまで意識する場面は多くありませんが、大量のオブジェクトを扱うプロジェクトでは知っておくと判断材料になります。




よくある質問

Q
Listと配列を1つのスクリプト内で混在させても問題ありませんか?
A
問題ありません。1つのスクリプトの中でも、数が変わらないデータは配列、増減するデータはListというように、用途ごとに使い分けて構いません。無理にどちらか一方に統一する必要はなく、それぞれの管理対象に合った方を選ぶのが基本の考え方です。
Q
List<GameObject>の中身をInspector上で確認する方法はありますか?
A
privateなListはInspector上に表示されませんが、変数の頭に[SerializeField]を付けると表示できるようになります。動作確認のために一時的に付けておき、デバッグが終わったら外すという使い方をすると、外部から書き換えられてしまうリスクを避けながら中身を確認できます。
Q
Listに入れられる要素数に上限はありますか?
A
明確な固定の上限はなく、実行環境で使えるメモリの範囲内であれば要素を追加し続けられます。ただし、要素数が非常に多くなるとメモリ消費や検索処理の負荷が増えていくため、実際にどこまで扱えるかは端末の性能や他の処理量によって変わってくると考えておくとよいでしょう。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク