【ゲーム開発のためのC#入門講座・応用拡張編】メジャーなコレクションを使ってみよう【#7】

5.0_C#応用拡張編

コレクションって?

プログラミングにおいて、複数のデータやインスタンスをまとめて管理・操作する機能やクラスのことをコレクションといいます

これまで学んできたコレクションは配列だけでしたが、プログラミングにはもっとたくさんのコレクションが用意されています。その中でも代表的なものを学び、複数のデータやインスタンスをより簡単に操作できるようにしましょう。

というわけで、まずは最も使いやすいコレクション「リスト」です。

メジャーなコレクション①リスト

リストとは?

リストは複数のデータやインスタンスをまとめて管理・操作するクラスです。

それだけ聞くと配列と何も変わらないと思われるかもしれませんが、リストはデータ数の増減を行うことができるという特徴があります。

早速実例を見てみましょう。

List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
numbers.Add(4);
numbers.Add(5);
​
// ↓Countは、配列におけるLengthと同じで、データ数が格納されたプロパティだよ!
for (int i = 0; i < numbers.Count; i++)
{
    Console.WriteLine(numbers[i]);
}
コンソール画面

1
2
3
4
5

実行してみると、確かに「Add」というメソッドで追加したデータがすべて出力されていますね。アクセスも登録した順に0から割り振られるインデックスで行えるため、データ数を増減できるという点を除けばほとんど配列と同じ感覚で扱えそうです。

唯一の違いはクラスの型についている「<格納するデータ型名>」という部分でしょうか。これはジェネリックと呼ばれる機能によって実現しているのですが、説明すると非常に難しくなるし、ゲーム開発に必要な知識という程でもないので割愛します。

データ型を「<>」の部分に記述すれば、対象のデータ型をまとめて管理することができるリストを作れるんだな、という点だけ理解してもらえればOKです。

// ↓<>の中にリストで管理したいデータ型を記述すればOK!
List<int> numbers = new List<int>();

なお、先程データ数の増減とお伝えした通り、データ数を減らすことができます。

List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
numbers.Add(4);
numbers.Add(5);
​
// ↓index4のデータを削除
numbers.RemoveAt(4);
// ↓index0のデータを削除
numbers.RemoveAt(0);
​
for (int i = 0; i < numbers.Count; i++)
{
    Console.WriteLine(numbers[i]);
}
コンソール画面

2
3
4

きちんと削除できていますね!

このように、リストは配列と違ってプログラムの途中でデータ数の増減を行うことができます。また、複数データをまとめて操作するための色んなメソッドも実装されており、非常に扱いやすく便利です。

でも、プログラムの途中でデータ数を変えてしまうなんて、処理負荷大きそうですよね。メモリへの影響とか、大丈夫なんでしょうか?

リストのメモリはリレー方式

実はリストというのは、宣言した時点ではほとんどメモリ領域を確保していません。これからいくつデータが格納されるかはプログラム都合ですから、勝手にまとめて確保する訳にいかないですよね。

代わりに、追加予定のデータのアドレスを確保しておくための領域が確保されます。

データアドレス次indexのアドレス保存領域
ListアドレスAnull(未設定)

そして「Add」メソッドによってデータが追加されると、そのアドレスを事前に確保しておいた場所に保存します。

データアドレス次indexのアドレス保存領域
listアドレスAnull(未設定) <=アドレスBを保存
ひとつ目のデータアドレスBnull(未設定)

あとはこの繰り返しです。

データが追加される度に、直前のデータが次データのアドレスを保持します。

<データ追加1回目>

データアドレス次indexのアドレス保存領域
listアドレスAアドレスB
ひとつ目のデータアドレスBnull(未設定) <=アドレスCを保存
ふたつ目のデータアドレスCnull(未設定)

<データ追加2回目>

データアドレス次indexのアドレス保存領域
listアドレスAアドレスB
ひとつ目のデータアドレスBアドレスC
ふたつ目のデータアドレスCnull(未設定) <=アドレスDを保存
みっつ目のデータアドレスDnull(未設定)

そしてもしみっつ目のデータを参照する命令が実行されると、

// ↓みっつ目のデータ=[index2]のデータを参照したい!
Console.WriteLine(list[2]);
  1. リストが格納されたアドレスAを参照し、そこに保存されたアドレスBを見る(起点となるindex0へ移動)
  2. アドレスBに格納されたアドレスCを見る(アドレス移動回数1)
  3. アドレスCに格納されたアドレスDを見る(アドレス移動回数2)
  4. アドレスDに格納されたデータを参照する

このように、起点となるindex0のデータから指定されたindexの回数分、アドレスをリレー方式で追いかけて目的のデータを参照します。

また、データを削除しても、アドレスの繋ぎ部分を更新するだけで対応できます。

<ふたつ目のデータ削除>

データアドレス次indexのアドレス保存領域
listアドレスAアドレスB
ひとつ目のデータアドレスBアドレスC <=アドレスDに更新!
ふたつ目のデータアドレスCアドレスDを保存
みっつ目のデータアドレスDnull(未設定)

これならプログラムの途中でいくつデータを追加しようが減らそうが、問題なく複数データを管理することができますね。これを考えた人、頭いい!

メジャーなコレクション②配列

先程のリストの説明を見た時、皆さん、どう思いましたか?

「プログラムの途中でデータ数増減できるなら完全に配列に上位互換じゃん。ぶっちゃけ配列いらなくね?」と思いませんでしたか?

正直使う分にはリストがめちゃくちゃ手軽で便利なので、積極的にリストを使うのは大賛成です。ただ、リストがあれば配列はいらないかと言われると、そんなこともないのです。

リストのメモリはリレー方式で管理されています。そしてデータを操作する時は、インデックス0からひたすらメモリの場所を渡り歩いていきます。

ということは、千件データがあったら、一万件データがあったら、その回数分渡り歩かないとデータに辿り着けないですよね。また、データと次のデータのアドレスがセットで保存されるため、ただデータを格納するだけよりもメモリ消費量も大きいです。

一方で、配列は生成時に指定された数のメモリをまとめて確保します。純粋にデータだけを格納すればよいのでメモリの消費量は小さく済みます。また、連続してメモリを確保しているため、起点となるindex0から指定されたindex番目のアドレスを参照するだけでデータにアクセスできます。

// ↓もし1000個のデータを管理する配列で、
int[] numbers = new int[1000];
​
// ↓index998のデータを参照したとしても、
Console.WriteLine(numbers[998]);
​
/*
    index0から、データサイズ * index数離れた場所の
    メモリを参照するだけで、データにアクセスできる。
    ↓こんな風に連続してメモリを確保してるからできることだね!
    ■■■■■■■....■■■■■■■■■■■■■■
*/

つまりリストよりも配列の方が、処理速度やメモリ消費量の点で優れているんですね。

そのため、少しでもベストを尽くしたいということであれば、データ数の増減を行いたいかどうかで使い分けるとよいでしょう。

メジャーなコレクション③Dictionary

最後にご紹介するのは、組み合わせで複数データを管理するコレクション「Dictionary」です。

例えば、リンゴは50円、オレンジは100円、バナナは150円だったとします。

もし配列やリストでこれを管理しようとすると複数のコレクションを用意しなければなりません。

// 対象名管理用と、お値段管理用のふたつのコレクションが必要
string[] fruitsName = {
    "リンゴ",
    "オレンジ",
    "バナナ"
};
​
int[] fruitsAmount = {
    50,
    100,
    150
};

ふたつのコレクションをインデックスで管理するのは面倒ですよね。

かといってこのためだけにクラスを作るというのも面倒です。

そこで出番となるのがこのDictionaryです。

Dictionaryは「キー」と「値」というふたつのデータをまとめて格納します。

// リストと同じように、<>の中にキーのデータ型と値のデータ型を指定するよ!
Dictionary<string, int> fruits = new Dictionary<string, int>();
​
// ↓キーと値をセットで追加するよ!
fruits.Add("リンゴ", 50);
fruits.Add("オレンジ", 100);
fruits.Add("バナナ", 150);

そしてデータ(値)にアクセスする時は、インデックスではなくキーを使ってアクセスします。

// インデックスではなく、[]の中にキーを記述する!
fruits["リンゴ"] += 10;
Console.WriteLine(fruits["リンゴ"]);
Console.WriteLine(fruits["バナナ"]);
コンソール画面

60
150

特定のキーに紐づく値をペアで管理できるコレクションなんですね。

ループ処理を使えば、キーと値のどちらも参照することができます。

Dictionary<string, int> fruits = new Dictionary<string, int>();
​
fruits.Add("リンゴ", 50);
fruits.Add("オレンジ", 100);
fruits.Add("バナナ", 150);
​
for (int i = 0; i < fruits.Keys.Count; i++)
{
    Console.WriteLine(fruits.Keys.ElementAt(i));
    Console.WriteLine(fruits[fruits.Keys.ElementAt(i)]);
}
コンソール画面

リンゴ
50
オレンジ
100
バナナ
150

先程の例のように、インデックス以外の方法(名前とか)で複数データを管理したい時には便利なコレクションです。

ただし、下記の特徴(落とし穴ともいう)があるので、その点ご注意下さい。

  • Dictionaryは追加されたデータの順番を保持しない(ループ処理した時に登録順の通りとは限らない)
  • キーはユニークでなければならないため、同じキーを追加しようとすると異常終了してしまう

ところで、forループで確かに処理できましたが、何だか[]の中にさらに[]や()がついてて、非常に見辛かったですよね。

実はもっと手軽に書けるループ処理があります。それは次回学習するとしましょう。

Unityでの活躍ポイント

今回はUnityというより、C#におけるプログラミングそのものでの活躍が期待される機能の紹介であるため、こちらは割愛させていただきます。

キャラクターデータやゲームオブジェクトなど、ゲームは特にたくさんのデータをまとめて管理したいことが多いです。適切なコレクションを活用して、思い通りの実装を実現しましょう!

実践演習

それでは、今回新しく覚えたListとDictionaryを使ってみましょう。

演習①

仕様

下記の配列をやめ、代わりにリストを使うようにしてください。
その上で、
①新しいデータ「10」を追加
②index0のデータを削除
を行ってください。

テンプレート
// ↓配列ではなくリストに変更しよう!
int[] numbers = {
    5,
    6,
    7,
    8,
    9
};
​
// ↓ここでデータ追加と削除の処理を行おう!
​
// ここまで
​
// ↓リストの場合はLengthじゃなくてCountなので、忘れずに直しておこう!
for (int i = 0; i < numbers.Length; i++)
{
    Console.WriteLine(numbers[i]);
}

演習②

仕様

大乱闘した結果、各キャラクター毎に下記のスコアを稼ぎました。
スコアはキャラクター毎に配列に格納されています。
キャラクター名をキーとし、スコアの合計を値とするDictionary「totalScores」を作成してください。
リンク:撃墜王1500, 落下王-500, コンボが多い2500
ゼルダ:飛び道具のみ2000, まっさきにヒット500, 真ん中キープ2000

テンプレート
int[] linkScores = {
    1500,
    -500,
    2500
};
​
int[] zeldaScores = {
    2000,
    500,
    2000
};
​
// ↓新しいDictionary「totalScores」を作ろう!
​
// ここまで
​
// ↓Dictionary「totalScores」にスコアの合計を設定しよう!
​
// ここまで
​
for (int i = 0; i < totalScores.Keys.Count; i++)
{
    Console.WriteLine(totalScores.Keys.ElementAt(i));
    Console.WriteLine(totalScores[totalScores.Keys.ElementAt(i)]);
}

答え合わせ

演習①の答え

// ↓配列ではなくリストに変更しよう!
List<int> numbers = new List<int>();
numbers.Add(5);
numbers.Add(6);
numbers.Add(7);
numbers.Add(8);
numbers.Add(9);
​
// ↓ここでデータ追加と削除の処理を行おう!
numbers.Add(10);
numbers.RemoveAt(0);
// ここまで
​
// ↓リストの場合はLengthじゃなくてCountなので、忘れずに直しておこう!
for (int i = 0; i < numbers.Count; i++)
{
    Console.WriteLine(numbers[i]);
}
コンソール画面

6
7
8
9
10

ちなみに、リストも実は配列のように「{}」を使って初期値をまとめて定義できます。

List<int> numbers = new List<int>() {
    5,
    6,
    7,
    8,
    9
};

この書き方は便利なので、ぜひ覚えておきましょう。

演習②の答え

int[] linkScores = {
    1500,
    -500,
    2500
};
​
int[] zeldaScores = {
    2000,
    500,
    2000
};
​
// ↓新しいDictionary「totalScores」を作ろう!
Dictionary<string, int> totalScores = new Dictionary<string, int>();
// ここまで
​
// ↓Dictionary「totalScores」にスコアの合計を設定しよう!
totalScores.Add("リンク", 0);
totalScores.Add("ゼルダ", 0);
​
for (int i = 0; i < linkScores.Length; i++)
{
    totalScores["リンク"] += linkScores[i];
}
​
for (int i = 0; i < zeldaScores.Length; i++)
{
    totalScores["ゼルダ"] += zeldaScores[i];
}
// ここまで
​
for (int i = 0; i < totalScores.Keys.Count; i++)
{
    Console.WriteLine(totalScores.Keys.ElementAt(i));
    Console.WriteLine(totalScores[totalScores.Keys.ElementAt(i)]);
}
コンソール画面

リンク
3500
ゼルダ
4500

まとめ

  • Listは複数のデータやインスタンスをまとめて管理・操作するクラス
  • 配列と違ってデータ数の増減ができる。一方で、配列よりもちょっとだけ処理速度やメモリ消費量に難あり
  • Dictionaryはキーと値をペアでまとめて管理・操作するクラス
  • インデックス以外の方法(名前とか)で複数データを管理したい時には便利だが、順番保証はないこと、キー重複すると異常終了してしまう点に注意

次回は応用拡張編の最終回です。

それでは、今回もお疲れ様でした!

また次の記事でお会いしましょう!

Posted by 夕目紅