Unityでインディゲームを作る!

Unityでのゲーム制作を目指し、それに関わる話題についてのブログ

配列の浅いコピーと深いコピーについて | 配列の宣言、代入

配列とは、タイプが同一の変数の数列を扱うためのデータタイプです。

 

 配列(array)にはいくつかの制限があります。宣言時に指定した要素数で固定されますし、そのせいで自由にデータの挿入が出来なかったり。C#では、例えばListの方が使いやすいので、無理に配列を使わなくてもいいかもしれません。

*追記*Listでも本記事と同様な現象は起きます。


 しかし、今回はそんな配列について、思わぬ事態に遭遇したのでまとめてみたいと思います。配列に配列を代入したら、どうなるのか?

 

浅いコピー、深いコピー

 タイトルの浅いコピー深いコピーというのは、Shallow Copy, Deep Copyから来ています。まず変数は変数に代入することで、その内容をコピーすることが出来ます。

int a = 12, b = 8;
a = b;

// a >> 8, b >> 8
 これは単純なintタイプの変数ですが、この時はaの値は上書きされ、8となります。ただ、その後にbに2を代入すると、aは8、bは2という風に別々の値になります。別々の変数なので当然ですが。

b = 2;

// a >> 8, b >> 2

 しかし、配列も同様に、単純な代入でコピーをしようとすると、不思議なことが起こります。

配列のコピー

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

int[ ] array2 = array1;

 配列の場合、ある配列に対して別の配列を代入すると、たしかに内容、各要素の値は同じな配列となるので、コピーできたかのように見えます。しかし、その後が問題です。

array1[1] = 0;

// array1 >> 1,0,3,4,5

// array2 >> 1,0,3,4,5

 array1の値を変更すると、なぜかarray2の値も変更されます。これこそが、浅いコピーによる不可思議な現象です。


 変数をコピーする目的は、値が同じだが独立した別々の変数、配列を得る、というもの。しかし、Shallow Copyでは、メモリ上に存在する一つのデータ列を2つの配列が共有する、という現象が起きてしまいます。

array1 >> {1, 2, 3, 4, 5} << array2

 

配列とポインタ

 実は、配列は本質的にポインタと同じ構造となっています。ポインタとは、ある変数のメモリ上のアドレス値を格納するための変数です。(C#でポインタは基本的にはあまり触れられませんが、CやC++では初心者殺しで有名)

 

 配列変数は複数のデータを実際にまとめて格納しているわけではなく、実際には一番先頭の値(つまり、[0]の値)が保存されているメモリアドレス値を格納してるので、仕組みは実はポインタと同じになります。

 配列の各要素のデータは、メモリ上に連続的に保存されています。アドレス値を利用したインデクシング(indexing)によって、各要素の値を出し入れできるというのが、配列の仕組みです。

 

array << アドレス番号が記録されている

[1]     << そのタイプのバイト数×添え字の分だけアドレス番号をずらす

array[2] << arrayがintの配列なら、intは4バイトなので、4*2=8 arrayのアドレスがもし100なら、三番目の要素は100+8 = 108番地のメモリに記録されていることになり、そのアドレスのメモリに格納されているデータをコンピュータが読み込む、ということになります。


 こうした仕組みはC#であっても同じで、つまり配列に配列を直接代入しても、アドレス値がコピーされるだけで、同じ要素を持った別の配列は作られません。

 なので、浅いコピーに対する深いコピーとは、プログラマが明示的に同じ要素数を持った別の配列を宣言して、一つずつコピー先の要素に対応したコピー元の要素の値を代入していく、ということになります。

int[ ] array2 = new int[ array1.Length ]

for(int i= 0; i < array1.Length; i++){

    array2[i] = array1[i];

}
 こうすれば、同じ値を持った別個の配列が作られるので、片方の操作がもう片方に反映されることはありません。

 

きっかけ、倉庫番

 倉庫番というゲームをコンソールアプリで作っていて、この事態に遭遇しました。コンソールアプリはCUIで、文字しか表示できません。しかし、ローグという偉大なゲームもCUIから誕生していますし、単純な昔ながらのゲームを原始的に再現するのは練習になるだろうと思ったので、挑戦しました。ただし、ノーヒントではなく、Python版の解説には目を通してます。


 ルールとしては、プレイヤーは荷物や箱を押すことしか出来なく(引くことは出来ない)、指定の場所(床)に全ての箱を移動させたらクリアです。ゲームが積むこともあるので、レベルリセット機能も必要になります。

 

 プログラムの流れとしては、まずマップを読み込み、画面に表示する。そして、プレイヤーの入力を受け付けた後、プレイヤーや箱を移動させ、コンソールの表示をConsole.Clearで消して、入力後のマップを直ぐに描画する、というもので、これをゲームクリアするまでループします。

 

 また、この手のマス目の2Dゲームは、マップをテキストで表すことが出来ます。実際、その方式の練習も兼ねていました。応用すれば、3Dゲームでも同様にステージやダンジョンを単純なテキストデータで表現、保存することも可能なようです。


プログラム内部では、charの配列として扱います。

char[ ][ ] map = {

new char[ ] = {'#', '#', '#', '#', '#'},

new char[ ] = {'#', ' ', ' ', ' ', '#'},

new char[ ] = {'#', '#', '#', '#', '#'}

} //こんな感じです

 2Dマップなので、二次元配列でなければいけません。二次元配列とは、配列の配列です。しかし、ジャグ配列を採用しました。二次元配列とちがい、各行の要素数を自由に変更できるからです。

 また、各面を管理するのに、配列の配列(マップ)を配列(各面)として、まとめる必要があり、大きさの違うマップをより簡潔にまとめるのにも適していました。

 

二つのマップを利用する

 そんなわけで、ひとまず岩を押す機能は置いておいて、移動の実現を優先しました。移動キーを押したら、その先のマスをチェックして、壁でなかったらプレイヤーの文字をそのマスに表示させ、プレイヤーのいたマスには、マップの元の床を表示させる、という入れ替え方式です。


 これには、その面のマップ構造を表す配列と、現在のマップ状態を表す配列の2つの配列が必要でした。それで、最初はマップを読み込んだ時点で、今回扱った浅いコピーをやってしまったのでした。

 

 単純な移動だけだと、問題は顕在化しなかったのですが、岩を押す機能を加えたとたんに、奇妙な動作をするようになりました。プレイヤーが増えたり、岩が増えたりです。最初はとにかく原因不明で焦りました。


 それで、とりあえず2つの配列を同時に画面に映そう、とやってみたら、見事に操作にあわせて、2つのマップがシンクロして動きました。

 

 マップ構造の配列は、変わってしまっては駄目です。プレイヤーや岩が移動した後に、元々その床が何だったのかを参照するデータベースだからです。繰り返しますが、浅いコピーによって、独立した2つの配列に出来ていなかったのが原因。移動だけの時は上手くいっていた(ように見えていた)ので、原因究明が難しかったです。

 

 原因が分かったので、先程書いたようにきちんと深いコピーで配列を作り、無事問題は解決されました。

 

配列には気をつけよう!

 というわけで、配列の宣言、代入やあるいはポインタとしての性質について、書いてみました。C#使いであっても、ある程度ポインタの知識は必要ですね。

 ただ、難しく考えすぎず、変数は箱ではなくて、メモリ上の一区画、と捉えて、そのメモリの番地、住所がアドレス、だと考えればいいと思います。