オレンジブログ

オレンジブログ

GameMakerのちょっとしたTipsとか

【GMS2】array_createで多次元配列、構造体を初期化する時の問題と解決方法


はじめに

GMLには配列を作成するための関数として array_create 関数があります。

manual.gamemaker.io

array_create(size, [value]);

引数に要素数と初期化値を指定して配列を作り出す関数です。
記述が簡単でよく利用するのですが、多次元配列構造体を用いて初期化すると
全ての要素で同じ配列や構造体を参照してしまいます。

今回はその現象の例と解決策についてまとめます。

参照について、まだよくわからないと言う方はこちらの記事を読んでからの方が
本記事を理解がしやすくなります。

orangelily.hatenablog.com


array_create の動作

まずは array_create の動作についてまとめます。


数値の配列を作成する場合

array_createの第二引数に数値を代入した場合の処理です。

// 要素数5を-1で初期化
var _array = array_create(5, -1);
// インデックス3を100に変更
_array[3] = 100;
// 出力
show_debug_message($"array = {_array}");

この場合は下記のようなログが表示されます。

array = [ -1,-1,-1,100,-1 ]

全ての要素を-1で初期化した後、インデックス3のみ100に変更したので、
当然と言えば当然ですね。


多次元配列を作成する場合

次は配列の中に配列を入れる多次元配列についての例です。

// 要素数5を配列[-1, -1]で初期化
var _array = array_create(5, array_create(2, -1));
// インデックス3の配列のインデックス0をを100に変更
_array[3][0] = 100;
// 出力
show_debug_message($"array = {_array}");

全ての要素を[-1, -1]の配列で初期化した後、
インデックス3の配列のインデックス0のみを100に変更しています。

この場合は下記のようなログが表示されます。

array = [ [ 100,-1 ],[ 100,-1 ],[ 100,-1 ],[ 100,-1 ],[ 100,-1 ] ]


全部の配列のインデックス0が100になっている...!?

そうなんです。
これ、先ほどの数値の時と同じようにインデックス3の配列のみ影響すると思いきや全ての配列に影響を及します

原因としてはarray_create関数の第二引数で生成したarray_create(-1, -1)の参照を全ての要素に代入してしまうためです。

そのため_array[3][0] = 100; を実行すると すべての要素が同じ値を持つ ようになってしまいます。

array_createの第二引数の初期化は1度しか行われず
配列は参照渡しのために起こった問題ですね。*1


array_createを構造体で使用する場合

次は構造体の例です。

/**
 * Vector2構造体
 * 2次元ベクトルを表す構造体。
 * @constructor
 * @param {real} _x - X座標の値
 * @param {real} _y - Y座標の値
 */
function Vector2(_x, _y) constructor {
    // X座標の初期化
    x = _x;
    // Y座標の初期化
    y = _y;
}
// 要素数5で{1,1}のVector2で初期化
var _array = array_create(5, new Vector2(1,1));
// インデックス3のxのみを100に変更
_array[3].x = 100;

// 出力
show_debug_message($"array = {_array}");

全ての要素をコンストラクタ構造体Vector2(1,1)newした後、
インデックス3のxのみを100に変更しています。

この場合は下記のようなログが表示されます。

array = [ { x : 100, y : 1 },{ x : 100, y : 1 },{ x : 100, y : 1 },{ x : 100, y : 1 },{ x : 100, y : 1 } ]

全部のVector2のxが100になっている!!
配列と同様です。
構造体も参照渡しのため、同じ構造体を参照してしまったわけですね。


解決策

多次元配列や構造体を用いる場合、array_createを使用せず別の方法で初期化を行う必要があります。


forループを使用した初期化

おそらくほとんどの方がこの方法で初期化をしているはずです。
最もシンプルでわかりやすい書き方ですね。

// 要素数5の配列を作成(初期値は指定しない)
var _array = array_create(5);
// for文でループ
for(var _i = 0; _i < array_length(_array); _i++) {
    // 全てのインデックスに新しい配列[-1, -1]を作成
    _array[_i] = array_create(2, -1);
}
// インデックス3の配列のインデックス[0]のみを100にする
_array[3][0] = 100;
// 出力
show_debug_message($"array = {_array}");

このように、for文で全てのインデックスにアクセスし、
都度array_createを呼び出します。

すると、下記のようなログが出力されます。

array = [ [ -1,-1 ],[ -1,-1 ],[ -1,-1 ],[ 100,-1 ],[ -1,-1 ] ]

今回はうまくいきました

これはそれぞれのインデックスごとに異なる配列の参照を渡しているため、
配列内の参照が独立したので問題なかったわけです。

基本はこの書き方で初期化するのが良いでしょう。


array_create_ext を使って初期化式を指定する

他にも、array_create_extを使った方法があります。


array_create_extとは

manual.gamemaker.io

array_createと同じく配列を作る関数ですが、
第二引数が値ではなくコールバック関数を登録するようになっています。

array_create_ext(size, function);
引数 説明
size 実数 作成する配列のサイズ
function 関数 各要素を初期化するためのコールバック関数

第二引数には引数にインデックスを受け取るコールバック関数を登録し、
第二引数に指定した関数の戻り値がそのインデックスに代入されます。

サンプル

// コールバック関数の宣言
// 引数にインデックスを受け取るようにし、それを+1して返す
var _f = function(_index)
{
    return _index + 1;
}
// 先ほど宣言した関数を引数に指定
array = array_create_ext(100, _f);
// 出力
show_debug_message(array);

上記のコードは_indexを引数として受け取って、その値に1を加えて返すローカル関数_fを作成しています。
その後、array_create_ext_f関数を第二引数に指定して呼び出します。

すると1から100までの整数が順に格納された配列を作成できます。
出力結果

[ 1,2,3,4,5, ... 100]


わかりにくいかもしれませんが、つまりさっきのサンプルはfor文と同じことをしています。

さっきのサンプルをfor文に書き換えたコード

array = array_create(100);
for(var _i = 0; _i < array_length(array); _i ++){
    array[_i] = _i + 1;
}
show_debug_message(array);

この関数を用いることでも配列内の参照を独立させることが可能です。
(for文と同じことをしているんだからそれはそう)


array_create_extを使ったサンプルコード

下記のようなコードを書きます。

// 要素数5の配列を「return new Vector2(1,1)」を返す関数を使って初期化
var _array = array_create_ext(5, function(_index){ return new Vector2(1,1) });
// インデックス3のxのみを100に変更
_array[3].x = 100;
// 出力
show_debug_message($"array = {_array}");

このようなコードを書くと、それぞれのインデックスにnew Vector2(1,1)が行われます。
ログの出力結果は下記になります。

array = [ { x : 1, y : 1 },{ x : 1, y : 1 },{ x : 1, y : 1 },{ x : 100, y : 1 },{ x : 1, y : 1 } ]

問題なく、第3インデックスのxのみが100に変更されていますね!


まとめ

今回はarray_create関数を用いた際に引っかかりやすい問題についてまとめました。
多次元配列やコンストラクタ構造体を用いる際は特に引っかかりやすいため、
for文やarray_create_extを用いて正しく初期化していきましょう。


*1:厳密には参照渡しではない