0からスタート!ゲーム開発ブログ

ゲーム開発に関する様々な記事を更新します

【Unityで始めるC#】配列の上手な使い方

 プログラミングにおいて、データの管理は非常に重要です。 特に、複数のデータを一つのまとまりとして効率的に扱いたい場合には、配列が役立ちます。

 この記事では、2回に分けて、初心者向けに配列の基本的な機能や使い方について解説します。



配列とは


 配列とは、同じ型のデータを一連の連続したメモリに保存するデータ構造です。 具体的にいうと、1つの変数内に、複数の同じ型のデータを管理させるための機能です。

 配列の概念を理解するために、アパートや団地の部屋を想像してみましょう。 各部屋には異なる人が住んでおり、それぞれの部屋には部屋番号が割り当てられています。

 このアパートを配列にたとえることで、配列の機能と役割をより具体的に理解できます。 例えば、以下のようにアパートの部屋を配列に見立てた場合を考えてみましょう。


string[] residents = { "Alice", "Bob", "Charlie", "David" };


 この例では、residentsという配列が宣言されています。 各要素にはアパートの部屋に住む人の名前が格納されています。

 この場合、residents[0]には"Alice"、residents[1]には"Bob"、residents[2]には"Charlie"、residents[3]には"David"が格納されています。

 具体的な処理の読み解き方は後程説明しますが、まずは、このように、各部屋に別の人が住んでいるように考えることで 配列の要素やインデックスの概念がよりイメージしやすくなると思います。

 なお、配列の機能を利用せずに変数を用意する場合、次のように変数を用意することになります。


string residents_0 = "Alice";
string residents_1 = "Bob";
string residents_2 = "Charlie";
string residents_3 = "David";


 4つの情報を扱う必要があるため、同じ型の変数を複数個用意しています。 ですが、これを100名分作るとしたら? 変数が大量かつ、どの変数が何を扱っているのか、管理も煩雑になってしまいますね。

 そういったケースに対応するため、煩雑さをなくし、効率的なデータの管理が行えるように配列が用意されています。



配列の用途例


 配列は、同じ種類の複数のデータを管理するのに非常に役立ちます。 例えば、ゲームのスコアを管理する必要があるとき、各プレーヤーのスコアを個別に管理する代わりに、スコアを格納する配列を作成することができます。

 また、配列はデータの操作を効率化することも可能です。 例えば、すべてのデータに対して同じ操作を行いたい場合、配列とループを組み合わせて一度に行うことができます。



配列の宣言のルール


 配列の宣言にはいくつかのルールがあります。 以下にそのルールを説明します。



1.[ ] を使う


 配列を宣言するときには、型名の後ろに[]を付けます。 これにより、その変数が配列であることが示されます。 そのあとに変数名を書きます。


string[] residents;


 この場合、string 型の配列である residents 変数を宣言しています。



2.命名規則


 配列の変数名は、他の変数と同じように識別子のルールに従って命名されます。 その際、要素が複数存在することを示すために、変数名に複数形を使用するという慣習があります。 これは、配列が複数の要素を格納するためのデータ構造であることを明確に示すためです。

 例えば先ほどの変数も residents と複数形にしていることで、配列の変数を使う場面でも分かるようになっています。



3.初期化の必要性


 配列を宣言する際には、初期化が必要です。 配列の変数の宣言のタイミングで明示的に配列の数を指定する必要があります。 初期化を行わない場合、コンパイルエラーが発生します。


 これらを念頭に、実際に配列の宣言に必要な用語と初期化を行う方法を学習しましょう。




配列の用語


 配列には多くの用語があります。 配列を利用する際の知識として、これらをきちんと理解しておくことが大切です。



1.要素(Element)


 配列において、要素とはデータの値そのものを指します。 例えば、整数型の配列であれば、各要素は整数の値を保持します。

 要素は配列内で個別にアクセスできる単位です。 配列は複数の要素を持ち、それぞれの要素は独立してデータを保持します。


string[] residents = { "Alice", "Bob", "Charlie", "David" };


 この例の場合であれば、"Alice", "Bob", "Charlie", "David" が各要素に当たります。



2.要素番号/インデックス(Index)


 配列は1つの変数で複数の要素を管理しているため、「どの」要素かを指定する必要があります。 先ほどの例であれば、4つの要素のうち、"Alice", "Bob", "Charlie", "David" の特定が出来ないと、どの要素を使えばいいのか指示が出せません。

 それを決める機能が、要素番号/インデックスです。 これは配列内の要素を一意に識別するための番号です。最初のインデックスは 0 から始まることに注意してください。

 そのため、配列の最初の要素のインデックスは0、次の要素のインデックスは1、2、3...と続きます。 要素番号を使用することで、特定の要素にアクセスしたり、要素の値を変更したりすることができます




 配列では、[] の中にインデックスを用います。 そうすることで、そのインデックスの番号に対応する要素の情報にアクセスできます。


string[] residents = { "Alice", "Bob", "Charlie", "David" };
Debug.Log(residents[0]);   // ログに "Alice" と出力されます。

residents[1] = "Elric";  // residents[1] の要素("Bob")に "Elric" に代入して上書きする
Debug.Log(residents[1]);   // ログに "Elric" と出力されます。


 最初の内は何を書いてあるのか、とても読みにくい部分です。 また、インデックスで指定しているのは要素であるため、配列の何番目の要素、という風に読み変えて読んでいく必要があります。



3.要素数


 配列の長さ、とも呼ばれます。 配列を初期化する際に指定する要素の値であり、別の見方をするならば、 1つの配列に何個までの情報を入れることができるのか、その個数を指定する値です。


string[] residents = new string[5];


 この場合、string 型の要素が5つまで代入できる配列を作成しています。



配列の初期化


 それでは配列を作成して、使える状態にしましょう。

 配列を初期化する方法にはいくつかの方法があります。

 以下に例を示します。



1.直接初期化


 配列を宣言と同時に初期化する方法です。 初期値を中括弧 {} で指定します。(この { } を利用する初期化の方法を初期化子といいます)

 この場合、要素数の指定は不要で、初期値を用意した数の分だけ要素が作成されます。


int[] numbers = { 1, 2, 3, 4, 5 };


 この初期化の方法は、各要素に自由な値を代入することが出来ますが、変数の宣言と同時にしか行えません



2.newキーワードを使用した初期化


 new キーワードを使用して、配列のインスタンスを明示的に作成し、その後に要素数を指定します。 この方法では、要素の初期値はデータ型によってデフォルト値が割り当てられます


int[] numbers = new int[5]; // 要素数5の整数型の配列。また、各要素には int 型の初期値である 0 がそれぞれ代入される
numbers[2] = 100;   // インデックスを指定し、100を代入する

string[] names = new string[3]; // 要素数3の文字列型の配列。また、各要素には string 型の初期値である null がそれぞれ代入される


 このケースの場合、初期化後にインデックスを指定して要素を代入します。



 この他にも、要素数が不明な場合の初期化の方法などがあります。

 これらのルールに従って配列を宣言し、適切な初期化を行うことで、配列を正しく使用することができます。




配列の型


 配列は、同じデータ型の要素を1つの変数に格納するためのデータ構造です。 その型は値型(int、float、bool、構造体など)や参照型(string、クラスなど)であるかに関係なく、任意の型で宣言できます。

 以下に、異なる型の配列を宣言する例を示します。


// 値型
int[] numbers = { 1, 2, 3, 4, 5 }; // 整数型の配列
float[] grades = { 75.5f, 80.0f, 90.5f }; // 浮動小数点型の配列
bool[] flags = { true, false, true }; // 真偽値型の配列
Vector3[] positions =   // Vector3 構造体の配列
{
    new Vector3(1.0f, 2.0f, 3.0f),
    new Vector3(4.0f, 5.0f, 6.0f),
    new Vector3(7.0f, 8.0f, 9.0f)
};

// 参照型
string[] names = { "Alice", "Bob", "Charlie" }; // 文字列型の配列
GameObject[] objects = { obj1, obj2, obj3 }; // 参照型(GameObject)の配列


 上記の例では、順番に値型と参照型を要素とする配列が宣言されています。 自作したクラスを型とした配列の宣言も出来ます。

 以上のことから、自分の制作しているゲームの内容に応じて、適宜な型を選択して配列を作成し、利用する形式になります。



Lengthプロパティ


 配列には Length(レングス)プロパティがあり、このプロパティを使うことで、配列の現在の要素数を取得できます。 Lengthは読み取り専用(代入処理ができない)で、配列の宣言時に指定した要素数が返されます


int[] numbers = { 1, 2, 3, 4, 5 };

Debug.Log("配列の要素数: " +numbers.Length); //Lengthプロパティを利用して、デバッグログで要素数を表示


 実行すると「配列の要素数: 5」というログが表示されます。

 注意点としまして、要素のインデックスは 0 から連番になりますが、Lengthプロパティによる現在の要素数は、要素を 1 から数えた数になります。 この例であれば numbers のインデックスは 0 ~ 4 の範囲になりますが、その要素数は 5 になります。

 それでは実際に Length プロパティの利用例を見てみましょう。



Lengthプロパティの活用事例


 配列の Length プロパティを利用することで配列の要素数を簡単に取得できますので、 for文と組み合わせることで、配列の要素に順番にアクセスすることができます。

 例えば、以下のようなコードで配列内の各要素にアクセスできます。


int[] numbers = { 1, 2, 3, 4, 5 };
for (int i = 0; i < numbers.Length; i++)
{
    Debug.Log(numbers[i]);  // ログに 1,2,3,4,5 と表示される
}




 応用方法として、for 文内には if 文などの制御文を設けることも出来ますので、 配列内にある特定の条件に合致する要素だけを抽出することも出来ます。


int[] numbers = { 1, 2, 3, 4, 5 };
int targetValue = 3; // 合致条件となる値

for (int i = 0; i < numbers.Length; i++)
{
    if (numbers[i] == targetValue)
    {
        Debug.Log(numbers[i]); // 合致条件に一致する場合にログを表示
    }
}


 上記のコードでは、numbersという整数型の配列を宣言し、初期化しています。 また、targetValueという変数を新たに追加し、合致条件となる値を設定しています(ここでは3としています)。

 for文を使用して、配列の要素を順番にアクセスし、if文で現在の要素とtargetValueを比較しています。 比較結果が一致する場合には、該当する要素の値をログで表示しています。

 上記のコードを実行すると、numbers配列の中から値が3に合致する要素がある場合、その要素の値がログに表示されます。 合致する要素がない場合は、ログには何も表示されません。

 このように、新しい変数との合致条件に基づいて制御文を追加することで、特定の条件に合致する配列の要素のみをログに表示することができます。

 実際にプログラムを書いて targetValue の値を変更し、ログの出力結果の変化を確認してみましょう。 処理を加工することで、プログラムの動きが見えてきますので、配列への理解を深めることにつながります。




配列のソートと入れ替え


 配列には、int 型や float 型の要素をソートする Array.Sort メソッドが用意されています。

 以下は、要素を昇順にソートするサンプルコードです。


int[] numbers = { 5, 2, 3, 1, 4 };
Array.Sort(numbers);  // numbersは{ 1, 2, 3, 4, 5 }となる




 降順で配列の要素をソートするには、Array.Sortメソッドの代わりに Array.Reverse メソッドを使用します。

 以下は、要素を降順にソートするサンプルコードです。


int[] numbers = { 5, 2, 3, 1, 4 };
Array.Sort(numbers);        // numbersは{ 1, 2, 3, 4, 5 }となる
Array.Reverse(numbers);  // numbersは{ 5, 4, 3, 2, 1 }となる


 このように降順で配列の要素をソートする場合は、ソートの前に配列を Array.Sortメソッドで昇順にソートし、それから逆順にすることで実現できます。




 なお、専用のメソッドはありませんが、配列内の要素を入れ替えることも出来ます。


int[] numbers = { 1, 2, 3 };

int temp = numbers[0];
numbers[0] = numbers[2];
numbers[2] = temp;


 ここでは、一時的に numbers[0] の要素を保持しておくための temp 変数を用意し、 それから numbers[0] に numbers[2] を代入して入れ替えます。 numbers[0] の要素を保持しておいた temp 変数を利用して、numbers[2] に numbers[0] の値を代入します。

 上記の要素の入れ替え処理の結果、numbersは{ 3, 2, 1 }となります。

 なおこの処理は、バリュータプルの機能を利用することで、もっと簡潔に記述する(temp 変数が不要になる)ことが出来ます。 是非調べてみてください。



ゲームでの実装事例


 この例では、シーンに存在するすべて敵のゲームオブジェクト(Enemy スクリプトをアタッチ済)から、特定の敵を探す処理です。


// シーンにある全てのゲームオブジェクトを検索
// そのうち Enemy スクリプトがアタッチされているゲームオブジェクトだけを配列に代入
Enemy[] enemies = FindObjectsByType<Enemy>();

// 検索する敵の名前
string searchName = "EnemyA";

// 検索して該当の敵が見つかったときに、入れておくための変数を用意
Enemy foundEnemy = null;

// 検索処理
for (int i = 0; i < enemies.Length; i++)
{
    if (enemies[i].name == searchName)
    {
        foundEnemy = enemies[i];
        break; // 目的の敵を見つけたのでループを終了
    }
}

// 検索結果の表示
if (foundEnemy != null)
{
    Debug.Log("目的の敵を見つけました: " + foundEnemy.name);
}
else
{
    Debug.Log("目的の敵が見つかりませんでした");
}


 処理の補足です。

 for文を使用して、敵の配列を順番に検索しています。 このとき、配列内のすべての敵を確認したいので、Length プロパティを利用し、配列の要素数分だけループ処理を行っています。 各ループ処理の中で if 文を利用し、その回の要素の敵の名前と目的の敵の名前を比較し、一致する場合はfoundEnemy変数にその敵を格納します。

 目的の敵が見つかった場合と見つからなかった場合に応じて、ここでも if 文を利用することで、異なるログを表示します。




 この他にも、すべてのボタン(あるいはゲームオブジェクト)を配列で管理して、 まとめてオンオフ切り替えを行ったり、if 文を使うことで特定のボタン(ゲームオブジェクト)だけをオフにしたり、といった制御を行うことも出来ます。



まとめ


 配列は複数のデータを一つのまとまりとして扱い、Lengthプロパティで要素数を取得できます。 特に for文と組み合わせて各要素にアクセスすることができますので、配列内の要素すべてに同じ処理を施すことも出来ますし、 特定の要素だけを絞り込んで命令を実行することも出来ます。

 配列を使いこなすことで、データの効率的な管理や操作が可能になり、プログラミングの力をより大きく発揮できるようになります。 特に配列の操作がわかりにくいと思いますし、なんとなく敬遠しがちですが、たくさん読み書きを繰り返すことで苦手意識を克服し、理解を深めていってください