C#はゲーム開発においても威力を発揮するためのさまざまなツールと特徴を提供しています。 その中でも「List(リスト)」という便利な機能を使えば、動的なデータ管理がより簡単に、そして柔軟に行えるでしょう。
この記事では List について解説していきます。
List とは
Listは、C#における動的配列の一種です。 配列と同じように複数の要素を保持できますが、動的に(ゲーム中でも)サイズを変更できます。 これが配列と大きく異なる部分です。
最初にサイズを決める必要もなく、いつでも要素の追加や削除が容易です。 そのため、配列よりも柔軟に利用できる機能を持っています。
List の用途例
ゲーム開発において、Listはさまざまな用途で活用されます。
例えば、プレイヤーの所持アイテムリストを管理する際に、新しいアイテムを追加したり、不要なアイテムを削除したりする際に便利です。
敵キャラクターの出現管理にも使用できます。 動的な敵の出現や消滅に対応するため、Listを使って敵キャラクターのインスタンスを追加・削除することができます。
また、スコアの履歴やランキング情報を管理するためにもListを使用できます。 プレイヤーごとのスコアを追加し、順位付けや更新を行う場合に役立ちます。
さらに、イベント管理やクエスト進行のためにもListを使用し、プレイヤーの行動や進捗を管理することが可能です。
このように、Listはゲーム内の動的な要素を管理するための便利なツールです。 配列よりも柔軟性が高く、ゲームの進行やプレイヤーとのやり取りを効果的に制御するのに大変役立ちます。
List の定義
最初に C# の提供元である MicroSoft の List のマニュアルのリンクを提示します。
List は System.Collections.Generic ネームスペースに定義されています。 そのため、利用する際には宣言が必要になります。
using System.Collections.Generic;
Unity の場合、新しいスクリプトを作成すると、自動的にこの宣言は追加されています。 つまり、いつでも List が利用可能な状態になっています。
List の定義は次のようになっています。
List<T> クラス
List はクラスの一種です。
そして
List の宣言のルール
それでは実際に使う方法を紹介します。
List を利用する場合、他の型と同じように変数を宣言して使います。
アクセス修飾子についても、他の変数と同様です。 private、public、protected といった形で付与できます。
アクセス修飾子を記述しない場合、自動的に private 扱いになります。
例えば、int 型の List は次のように宣言できます。
List<int> numberList = new List<int>();
numberList は変数名ですので、任意の名称をつけることが出来ます。
自作したクラスも利用できます。 例えば Player クラスを定義している場合、Player 型の List は次のように宣言できます。
pulbic List<Player> playerList = new List<Player>();
List の宣言についてはいくつかルールがありますので、順番に説明します。
1.<> を使う
List を宣言するときには、List と書いたあとに、<> で型名を指定します。 これにより、その変数が List であることが示されます。そのあとに変数名を書きます。
List<int> numberList;
この場合、int 型の List ある numberList 変数を宣言しています。
2.命名規則
List の変数名は、他の変数と同じように識別子のルールに従って命名されます。 その際、配列や List といった複数の要素が存在することを示すため、 変数名に複数形を使用する(語尾に s をつける)、あるいは語尾に ~ List と付けるという慣習があります。
例えば先ほどの変数も numberList としていることで、List の変数であることが明確に分かるようになっています。 numbers のような複数系の命名も可能ですが、この場合は配列と混同される可能性があるため、 一目見て List とわかる、~List という命名の方が理想的です。
コーディングする際には、変数名だけを見てもその変数が List であることを理解しやすくしておくとよいでしょう。
3.初期化の必要性
配列と同じで、List を宣言する際には、初期化が必要です。 List の変数の宣言のタイミングで明示的に初期化する必要がありますが、配列とは異なり、要素数の指定は省略することができます。 これは List が動的にサイズを変更できるためです。いつでもサイズが変更可能なので、最初にサイズを指定しなくても問題ないという解釈です。
なお、初期化を行わない場合、コンパイルエラーが発生します。
これらを念頭に、実際に List の宣言に必要な用語と初期化を行う方法を学習しましょう。
List の用語
これは配列とほとんど同じです。
要素(Element)、要素番号/インデックス(Index) 、要素数などです。
こちらの配列の記事を参考にしてください。
List の初期化
それでは早速 List を作成して、使える状態にしましょう。
List を初期化する方法にはいくつかの方法があります。
以下に例を示します。
1.直接初期化
List を宣言と同時に初期化する方法です。 初期値を中括弧 {} で指定します。 この { } を利用する初期化の方法を初期化子といい、List の場合にはコレクション初期化子といいます。
この場合、要素数の指定は不要で、初期値を用意した数の分だけ要素が作成されます。
// Listの宣言と初期化 List<string> fruitsList = new List<string> { "Apple", "Banana", "Orange" }; Debug.Log(fruitsList[0]); // ログに Apple と表示される Debug.Log(fruitsList[2]); // ログに Orange と表示される
この初期化の方法は、各要素に自由な値を代入することが出来ますが、変数の宣言と同時にしか行えません。 ログを確認するとわかるように、{ } 内に記述した順番に List 内に要素が追加されます。
2.newキーワードを使用した初期化
new キーワードを使用して、List のインスタンスを明示的に作成し、その後に要素数を指定します。 この方法では、要素の初期値はデータ型によって初期値(デフォルト値)が割り当てられます。
また要素数を指定しない場合でも初期化できます。この場合、要素がない List が作成されます。
// Listのインスタンスを作成し、要素数を指定 List<int> numberList = new List<int>(3); // 要素数 3 の整数型のList。また、各要素には int 型の初期値である 0 がそれぞれ代入される numberList[2] = 100; // インデックスを指定し、100を代入する List<int> nameList = new List<string>(5); // 要素数 5 の文字列型のList。また、各要素には string 型の初期値である null がそれぞれ代入される public List<bool> flagList = new List<bool>(); // 要素数 0 の真偽値型の List
このケースの場合、初期化後にインデックスを指定して要素を代入します。
現在(2023.8月時点)の Unity 2022.3 LTS においては、C# 9.0 での記法が記述できます。 そのため、new キーワードによる初期化については、型を省略して初期化することが出来ます。
このとき、2つの方法があります。
1つは型推論型と呼ばれる機能で、左辺の型を省略し、var と記述します。 この場合、右辺の型によって、変数の型が自動的に確定するようになっています。
// 左辺の型宣言を省略してListのインスタンスを作成し、要素数を指定 var numberList = new List<int>(3); // 要素数を3として確保
もう1つの方法は Target-typed new 表現とよばれる機能で、右辺の型を省略できます。 この場合は先ほどの型推論型とは逆で、左辺の型によって、new 演算子のみでインスタンスの作成が行えるようになっています。 そのため、右辺での型指定が不要になります。
// Target-typed new 表現により、new 演算子のみで List 用のインスタンスを作成し、要素数を指定 List<Player> playerList = new(); // 要素数を指定せず、初期化のみしている
List の基本的な使い方
List の基本的な使い方です。
この記事では自作した enum とクラスを利用して説明を行います。
public enum JobType { 戦士, 魔術師, 僧侶, 盗賊 }
[System.Serializable] public class Player { public string name; public int id; public JobType job; // コンストラクタ public Player(string playerName, int playerId, JobType playerJob) { name = playerName; id = playerId; job = playerJob; } }
サンプルコードは実際に自分でも書いてみてください。大変学習になります。
1.要素の取得
リスト内の要素には、インデックスを指定してアクセスできます。 これにより、特定の位置にある要素を取得することができます。
以下はサンプルコードです。 この記事内での List の要素の初期化には Target-typed new 表現を利用した省略記法を用いています。
List<Player> playerList = new() { new ("アリス", 1, JobType.戦士), new ("ボガード", 2, JobType.魔術師), new ("チェイサー", 3, JobType.僧侶) }; Player secondPlayer = playerList[1]; // 2番目のプレイヤー(ボガード)を取得 Debug.Log("2番目のプレイヤー: " + secondPlayer.name + ", ID: " + secondPlayer.id);
2.要素の更新(代入)
リスト内の要素は、インデックスを指定してアクセスし、新しい値で上書き(代入)することができます。
List<Player> playerList = new() { new ("アリス", 1, JobType.戦士), new ("ボガード", 2, JobType.魔術師), new ("チェイサー", 3, JobType.僧侶) }; Player updatedPlayer = new ("ドーンズ", 4, JobType.盗賊); playerList[1] = updatedPlayer; // 2番目のプレイヤー(ボガード)を新しいプレイヤー(ドーンズ)で上書き Debug.Log("2番目のプレイヤーを更新: " + playerList[1].name + ", ID: " + playerList[1].id);
上記の例では、2番目のプレイヤー(ボガード)を新しいプレイヤー(ドーンズ)で更新しています。
どちらの方法もインデックスを指定して操作するので、配列と同じ形式ですね。
List の持つ機能①
それでは List の持つ機能について、順番に解説していきます。
1.Count プロパティ ーList の長さを取得するー
List には Count(カウント)プロパティがあり、このプロパティを使用することで、List の現在の要素数(長さ)を取得できます。 Count は読み取り専用です(代入処理ができない)。
List の長さは可変するため、Count プロパティを実行したときの現在の要素数(長さ)を取得します。
利用する場合には、Count プロパティを利用したい List 変数に続けて、.Count と書きます。
それではサンプルコードです。 動作を確認するためには作成したスクリプトを、Create Empty したゲームオブジェクトにアタッチして利用します。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PartyManager : MonoBehaviour { public List<Player> playerList = new (); private void Start() { playerList.Add(new ("アリス", 1, JobType.戦士)); Debug.Log("現在の要素数: " + playerList.Count); playerList.Add(new ("ボガード", 2, JobType.魔術師)); playerList.Add(new ("チェイサー", 3, JobType.僧侶)); playerList.Add(new ("ドーンズ", 4, JobType.盗賊)); Debug.Log("現在の要素数: " + playerList.Count); // List の中身を順番にログ出力する for (int i = 0; i < playerList.Count; i++) // Count の値は 4 { Debug.Log($"名前: {playerList[i].name}, ID: {playerList[i].id}, 職業: {playerList[i].job}"); } } }
for 文の条件式の部分に Count プロパティを利用し、List の要素の数だけループ処理を行っています。 実行結果としては、次のような内容がログに表示されます。
現在の要素数: 1 現在の要素数: 4 名前: アリス, ID: 1, 職業:戦士 名前: ボガード, ID: 2, 職業: 魔術師 名前: チェイサー, ID: 3, 職業: 僧侶 名前: ドーンズ, ID: 4, 職業: 盗賊
注意点としまして、要素のインデックスは 0 から連番になりますが、Count プロパティによる現在の要素数は、要素を 1 から数えた数になります。 この例であれば playerList のインデックスは 0 ~ 3 の範囲になりますが、その要素数は 4 になります。
これは配列の Length プロパティと同じ数え方ですので、Count プロパティによる現在の要素数の数え方は Length プロパティと一緒であると覚えておきましょう。 学習していく際の秘訣です。
List は public 修飾子で宣言するか、[SerializeField] 属性を付与することでシリアライズされます。 そのため、スクリプトをアタッチしているゲームオブジェクトを選択すると、インスペクターにて List の内容を確認することが出来ます。
自作したクラスを List で扱う場合、 [System.Serializable] 属性が付与しておくと、List に自作クラスの内容を表示出来るようになります。 今回の Player クラスには付与してありますので、スクリプトを見直してみてください。
ゲームを実行し、Console ビューだけではなく、インスペクターも確認してみましょう。 ゲーム実行前と実行後を見比べると、処理がどのように動いているのか可視化できます。
List 内にちゃんと要素が追加されているのがわかりますね。
なお、インスペクター内をドラッグアンドドロップすることで、要素の順番の入れ替えも出来ます。
2.List に要素を追加する ーAdd / AddRange メソッドー
リストに要素を追加するには、Addメソッドを使用します。 また、別のリストの要素をまとめて追加するにはAddRangeメソッドを使用します。 AddRange メソッドを利用した場合、追加先の List の末尾に追加されます。
利用する場合、Add メソッドや AddRange メソッドを利用したい List 変数に続けて、.Add()、あるいは .AddRange() と書きます。 Add メソッドの引数の指定には List と同じ型の値を1つ指定します。これが要素になります。 AddRange の引数の指定には、List と同じ型の List になっている値を指定します。これがまとめて要素になります。
以下は、要素を追加するサンプルコードです。
先ほどの PartyManager スクリプトの Start メソッドを修正してみましょう。 for 文よりも上の部分だけを上記のように書き換えると、処理のログが確認できます。
playerList = new() { new ("アリス", 1, JobType.戦士), new ("ボガード", 2, JobType.魔術師), }; playerList .Add(new ("チェイサー", 3, JobType.僧侶)); // playerList に新しいプレイヤーが追加される List<Player> additionalPlayerList = new() { new ("ドーンズ", 4, JobType.盗賊), new ("エヴァン", 5, JobType.戦士), }; playerList.AddRange(additionalPlayerList); // additionalPlayerList のプレイヤーが playerList に追加される
処理の結果です。
3.List の要素を削除する ーRemove / RemoveAll メソッドー
List 内から要素を削除するには、Removeメソッドを使用します。 特定の条件に合致する要素を全て削除するには、RemoveAllメソッドを使用します。
利用する場合には、Remove / RemoveAll メソッドを利用したい List 変数に続けて、.Remove()、あるいは .RemoveAll() と書きます。 Remove メソッドの引数の指定には List 内の要素を1つ指定します。その要素が削除されます。 RemoveAll の引数の指定には、ラムダ式を利用して List 内から削除したいタイプを指定します。 List 内から、その条件を満たす全ての要素を削除します。
以下は、要素を削除するサンプルコードです。 同じように PartyManager スクリプトの Start メソッドを修正してみましょう。
playerList = new() { new ("アリス", 1, JobType.戦士), new ("ボガード", 2, JobType.魔術師), new ("アリス", 3, JobType.盗賊), new ("ドーンズ", 4, JobType.盗賊) }; playerList.Remove(playerList[1]); // 2番目のプレイヤー(ボガード)が削除される playerList.RemoveAll(player => player.name == "アリス"); // 名前が"アリス"のプレイヤーが全て削除される
上記の処理により、List に残るのはドーンズだけです。
まとめ
ここまでプログラムが正常に動作したら、書いてきた処理を改良してみましょう。 new する Player を追加したり、Remove するインデックスの値を変えたり、 RemoveAll の名前指定や、JobType の指定を変えることで、削除する要素を変更できます。
インスペクターがあなたの強い味方になります。 是非処理を改良して動作を確認し、処理への理解を深めてください。
List の持つ機能②
それでは List の持つ機能について、順番に解説していきます。
この記事内での List の要素の初期化には Target-typed new 表現を利用した省略記法を用いています。
1.Clear メソッド ーList のすべての要素を削除するー
Clear メソッドは、リスト内のすべての要素を削除して空にするためのメソッドです。
利用する場合には、Clear メソッドを利用したい List 変数に続けて、.Clear() と書きます。 引数の指定はありません。
以下にサンプルコードを提示します。
using System.Collections.Generic; using UnityEngine; public class ListMethodsExample : MonoBehaviour { public List<Player> playerList = new (); void Start() { // プレイヤーをリストに追加 playerList.Add(new ("アリス", 1, JobType.戦士)); playerList.Add(new ("ボガード", 2, JobType.魔術師)); playerList.Add(new ("チェイサー", 3, JobType.僧侶)); playerList.Add(new ("ドーンズ", 4, JobType.盗賊)); // Clear メソッドの利用 playerList.Clear(); Debug.Log($"プレイヤーリストの要素数 : { playerList.Count }"); // 出力: プレイヤーリストの要素数: 0 } }
他にも、ゲーム内で一連のアクションや新しいセクションへの移行時に、リストを初期状態に戻すのに便利です。
2.Contains メソッド ー指定した要素が List 内に存在するか調べるー
Contains メソッドは、指定した要素がリスト内に存在するかどうかを確認するためのメソッドです。 このメソッドは bool 型の戻り値を持ちます。引数に指定した要素がリスト内に存在する場合は true を返し、存在しない場合は false を返す機能を持ちます。
その結果を利用して、if 文による条件分岐に使用できます。
利用する場合には、Contains メソッドを利用したい List 変数に続けて、.Contains() と書きます。 引数には、調べたい要素を指定します。
以下にサンプルコードを提示します。 前回と同様に、Start メソッド内に処理を追加して、動作の確認を行ってみましょう。
// プレイヤーを再度追加 playerList.Add(new ("エヴァン", 5, JobType.戦士)); playerList.Add(new ("フレイリア", 6, JobType.魔術師")); // チェックしたい要素を指定 Player targetPlayer = new ("エヴァン", 5, JobType.戦士); // Contains メソッドを利用して、引数で指定した要素が List 内に含まれているかチェック if (playerList.Contains(targetPlayer)) { Debug.Log($"プレイヤーリストに { targetPlayer.name } は含まれています。"); } else { Debug.Log($"プレイヤーリストに { targetPlayer.name } は含まれていません。"); }
上記の場合、要素内の情報がすべて合致しますので、if 文の条件式の結果は true となり、List に含まれていることを示すログが出力されます。
Contains メソッドは、要素が一致しているかどうかを判定します。 要素の各フィールドの値が全て一致する場合に、その要素がリスト内に含まれていると判定されます。 よって、要素の内容が同じであれば、別のインスタンスであってもリスト内に含まれていると判定され、Contains メソッドは true を返します。
そのため、要素の内容が同じであることを確認するために Contains メソッドを使用する場合、インスタンスそのものではなく、要素の内容に注目する必要があります。
では、次のようなケースはどうなるでしょうか。
playerList.Add(new ("アリス", 1, JobType.戦士)); playerList.Add(new ("ボガード", 2, JobType.魔術師)); // チェックしたい要素を指定 Player anotherTargetPlayer = new ("ボガード", 2, JobType.僧侶); if (playerList.Contains(anotherTargetPlayer)) { Debug.Log($"プレイヤーリストに { anotherTargetPlayer.name } は含まれています。"); } else { Debug.Log($"プレイヤーリストに { anotherTargetPlayer.name } は含まれていません。"); }
上記の場合、要素内のうち、JobType の情報が合致しません。 そのため、if 文の条件式の結果は false となり、List に含まれていないことを示すログが出力されます。
このように、調べた要素内に1つでも異なる値がある場合、Contains メソッドは「一致しない」と判断します。
3.Sort メソッド(引数指定なし) ーList の要素をデフォルトの昇順に並べ替えるー
Sort メソッドは、リスト内の要素をデフォルトの昇順に並べ替えるためのメソッドです。 プレイヤーのランキング表示や、所持しているアイテムの整列などに使えます。
利用する場合には、Sort メソッドを利用したい List 変数に続けて、.Sort() と書きます。 引数の指定には複数の種類があります。
C# の組込型(数字、文字列など)については、引数の指定なしで実行でき、並べ替えられます。 これは各組み込み型には既に比較演算子(<、>、==など)が定義されており、Sort()メソッドがこれらの演算子を使用して要素の比較を行うためです。 そのため、デフォルトの昇順に並び替えることが出来ます。
以下にサンプルコードを提示します。
using System.Collections.Generic; using UnityEngine; public class SortingExample : MonoBehaviour { void Start() { List<int> src = new(){ 3, 1, 5, 4, 2 }; List<int> list = new(){ 8, 0, 6, 9, 7 }; // listに要素を追加 list.AddRange(src); // この時点での list 内の要素の順番を表示 Debug.Log($"[{string.Join(", ", list)}]"); // ログには [8, 0, 6, 9, 7, 3, 1, 5, 4, 2] と表示される // listをソート list.Sort(); // ソート後の順番を表示 Debug.Log($"[{string.Join(", ", list)}]"); // ログには [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] と表示される } }
list 変数内の要素を昇順にソートしていますので、結果も昇順に並んだものとなります。
Player クラスのような自作クラスは、Sort() 【引数の指定なし】では並び替えが出来ません。
こちらについては、関連する機能を学習してから紹介したいと思いますので、割愛します。
4.IndexOf / LastIndexOf メソッド ー指定した要素のインデックスを取得するー
IndexOf メソッドは、指定した要素のリスト内でのインデックス(要素番号)を取得するためのメソッドです。 LastIndexOf メソッドは、最後に現れるインデックスを取得します。例えば、所持しているアイテムの位置特定や、検索結果の表示などに使えます。
利用する場合には、IndexOf / LastIndexOf メソッドを利用したい List 変数に続けて、.IndexOf() か、 .LastIndexOf() と書きます。 引数には Contains メソッドと同じで、検索して取得したい要素を指定します。
// リストをクリアしてから要素を追加 playerList.Clear(); playerList.Add(new ("アリス", 1, JobType.戦士)); playerList.Add(new ("ボガード", 2, JobType.魔術師)); playerList.Add(new ("チェイサー", 3, JobType.僧侶)); playerList.Add(new ("ドーンズ", 4, JobType.盗賊")); playerList.Add(new ("エヴァン", 5, JobType.戦士)); // IndexOf メソッドの利用 int indexOfEvan = playerList.IndexOf(new ("エヴァン", 5, JobType.戦士)); Debug.Log($"エヴァンのインデックス: { indexOfEvan }"); // 出力: エヴァンのインデックス: 4 // 他の要素のインデックスも検索 int indexOfBogard = playerList.IndexOf(new ("ボガード", 2, JobType.魔術師)); Debug.Log($"ボガードのインデックス: { indexOfBogard }"); // 出力: ボガードのインデックス: 1 // LastIndexOf メソッドの利用 int lastIndexOfFray = playerList.LastIndexOf(new ("フレイリア", 6, JobType.僧侶")); Debug.Log($"フレイリアのインデックス: {l astIndexOfFray }"); // 出力: フレイリアのインデックス: 5
IndexOf メソッドは、指定した要素と一致する要素が最初に現れるインデックスを検索するため、要素内のすべての内容が合致する必要があります。 LastIndexOf メソッドも同様に、要素内のすべての内容が合致する場合に、最後に現れるインデックスを検索します。
インデックスの値は 0 から始まることに注意しましょう。
どちらの処理も、インスタンス自体が同じでなくても、要素の内容が一致すれば検出されます。
5.ToArray メソッド ーList を配列に変換するー
List には、ToArray メソッドが用意されており、List の要素を同じ並び順の配列に変換できます。
利用する場合には、ToArray メソッドを利用したい List 変数に続けて、.ToArray() と書きます。 引数の指定はありません。
// playerList を配列に変換 Player[] playerArray = playerList.ToArray(); // 配列の内容をログに表示 for (int i = 0; i < playerArray.Length; i++) { Debug.Log($"名前: { playerArray[i].name }, ID: { playerArray[i].id }, 職業: { playerArray[i].job }"); }
上記の例では、List内の要素をPlayer型の配列に変換しています。 変換後、for ループを使用して配列の各要素を順番にアクセスし、ログに表示しています。
まとめ
これらのメソッドを利用することで、リスト内の要素を操作し、ゲーム内のさまざまなシナリオで活用できます。
List にはまだまだ多くの機能が用意されていますので、実際に利用していきながら学習していくようにするとよいでしょう。 「こんな機能はないのかな?」という探求の目線を持つようにすると、より深い知識を得ることが出来ます。
ぜひとも習得して活用していただきたい機能です。