【解決】二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

アバター
サブちゃんB
記事: 27
登録日時: 2018年3月06日(火) 01:09

【解決】二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

投稿記事by サブちゃんB » 2022年12月13日(火) 15:56

RPGツクールMZの質問というよりJavaScriptの質問になってしまいそうで恐縮ですが。

二次元配列で現在所持しているアイテムを管理しています。
複数の条件が重複する行のアイテム個数を1行の個数へひとまとめにして残りの行へ0を代入する処理を、
プラグインから実行する関数によって試みています。

まとめ処理の前段階として二次元配列を次のようにソートしています。

コード: 全て選択

      $gameVariables.value(DBvalNum).sort(function(a, b) {
        //1列目は昇順。
        if (Number(a[0]) > Number(b[0])) return 1;
        if (Number(a[0]) < Number(b[0])) return -1;
        //2列目も昇順。
        if (Number(a[1]) > Number(b[1])) return 1;
        if (Number(a[1]) < Number(b[1])) return -1;
        // 4列目も昇順。
        if (Number(a[3]) > Number(b[3])) return 1;
        if (Number(a[3]) < Number(b[3])) return -1;
        //3列目をいったん降順に。
        if (Number(a[2]) > Number(b[2])) return -1;
        if (Number(a[2]) < Number(b[2])) return 1;
        //3列目も昇順。
        if (Number(a[2]) > Number(b[2])) return 1;
        if (Number(a[2]) < Number(b[2])) return -1;
        return 0;
      });


現在確認できる限りこれでソートはうまくいっているようなので、次に重複行の統合の処理を試みます。

コード: 全て選択

      for (let i = 0; i < $gameVariables.value(DBvalNum).length -1; i++) {
        console.log("実行i" + i);
        console.log($gameVariables.value(DBvalNum)[i][2]);
        if($gameVariables.value(DBvalNum)[i][2] == 0){
          console.log("i側何もしない");
          console.log($gameVariables.value(DBvalNum)[i][2]);
          //先頭の個数が0なら何もしない
        }else{
          if($gameVariables.value(DBvalNum)[i][0] == $gameVariables.value(DBvalNum)[i+1][0] && $gameVariables.value(DBvalNum)[i][1] == $gameVariables.value(DBvalNum)[i+1][1] && $gameVariables.value(DBvalNum)[i][3] == $gameVariables.value(DBvalNum)[i+1][3] && $gameVariables.value(DBvalNum)[i][2] > 0){
            let tmpForAdd = []; //要素同士の代入や加算の際に一時的に配列をコピーするためのローカル変数。
            for(let j = i + 1; j  < $gameVariables.value(DBvalNum).length -1 +1; j++){
              if($gameVariables.value(DBvalNum)[i][0] == $gameVariables.value(DBvalNum)[j][0] && $gameVariables.value(DBvalNum)[i][1] == $gameVariables.value(DBvalNum)[j][1] && $gameVariables.value(DBvalNum)[i][3] == $gameVariables.value(DBvalNum)[j][3] && $gameVariables.value(DBvalNum)[j][2] > 0){
                if($gameVariables.value(DBvalNum)[i][2] == 0){
                  //先頭の個数が0なら何もしない
                  console.log("j側何もしない");
                  console.log($gameVariables.value(DBvalNum)[i][2]);
                }else{
                  console.log("j加算前v81[i][2] ="+$gameVariables.value(DBvalNum)[i][2] + ": i= " + i);
                  console.log("j加算前v81[j][2] ="+$gameVariables.value(DBvalNum)[j][2] + ": j= " + j);
                  tmpForAdd = Array.from($gameVariables.value(DBvalNum));
                  $gameVariables.value(DBvalNum)[i][2] += tmpForAdd[j][2];
                  console.log("j加算後v81[i][2] ="+$gameVariables.value(DBvalNum)[i][2] + ": i= " + i);
                  console.log("j加算後v81[j][2] ="+$gameVariables.value(DBvalNum)[j][2] + ": j= " + j);
                  $gameVariables.value(DBvalNum)[j][2] = 0;
                  console.log("jへ0代入後v81[i][2] ="+$gameVariables.value(DBvalNum)[i][2] + ": i= " + i);
                  console.log("jへ0代入後v81[j][2] ="+$gameVariables.value(DBvalNum)[j][2] + ": j= " + j);
                  console.log("実行j" + j);
                  console.table($gameVariables.value(DBvalNum));
                  //retry0229 = 1; //行削除が行われたので処理が終わったらループ冒頭へ戻る
                }
               }
            }
          }
        }
      }

これがうまくいけばあとは3列目が0の行をfilterなどで消せるかと思うのですが、どうもうまくいきません。

たとえば
スクリーンショット 2022-12-13 15.10.28.png

↑こちらの二次元配列を処理した場合、最終結果は
スクリーンショット 2022-12-13 15.14.58.png

↑このように悲惨です。
スクリーンショット 2022-12-13 15.18.07.png

↑最初のソートはうまくいっているようなのですが、
スクリーンショット 2022-12-13 15.15.43.png

1行目の3列目へ2行目の3列目の値を加算しようとする時点で連動(?)しており、
2行目の3列目へ0を代入すると1行目の3列目まで0になってしまって、
以降の処理がおかしくなるようです。

コード: 全て選択

                  console.log("j加算前v81[i][2] ="+$gameVariables.value(DBvalNum)[i][2] + ": i= " + i);
                  console.log("j加算前v81[j][2] ="+$gameVariables.value(DBvalNum)[j][2] + ": j= " + j);
                  tmpForAdd = Array.from($gameVariables.value(DBvalNum));
                  $gameVariables.value(DBvalNum)[i][2] += tmpForAdd[j][2];
                  console.log("j加算後v81[i][2] ="+$gameVariables.value(DBvalNum)[i][2] + ": i= " + i);
                  console.log("j加算後v81[j][2] ="+$gameVariables.value(DBvalNum)[j][2] + ": j= " + j);
                  $gameVariables.value(DBvalNum)[j][2] = 0;
                  console.log("jへ0代入後v81[i][2] ="+$gameVariables.value(DBvalNum)[i][2] + ": i= " + i);
                  console.log("jへ0代入後v81[j][2] ="+$gameVariables.value(DBvalNum)[j][2] + ": j= " + j);


上記のコードでは加算する値の入る配列をいったんtmpForAddへコピーして別の配列として扱うことを試みていますが、当初の直接加算するやり方、concatや[...]を使う方法でも結果は同じでした。
配列そのものは変数のように代入しても参照元は同じなのでconcatなどでコピーしないと分けられないということまでは分かったのですが、要素同士の連動というのは、どうしても調べきれませんでした。
配列の要素同士を足し算するのがダメなのかと思って、ローカル変数を宣言してそちらへ格納してから加算しましたがやはり連動しました。
整数を直接足しても連動しました。

これらの要素を連動させなくするか、あるいは複数行を1まとめにする別の良い方法がございましたら、ご教示頂けますと幸いです。
最後に編集したユーザー サブちゃんB on 2022年12月13日(火) 21:39 [ 編集 1 回目 ]

名無し蛙
記事: 308
登録日時: 2015年11月23日(月) 02:46

Re: 二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

投稿記事by 名無し蛙 » 2022年12月13日(火) 19:25

あんまり真面目に読み解いてないので適当な事を言ったらすみません。

liuhong さんが書きました:あるいは複数行を1まとめにする別の良い方法がございましたら、ご教示頂けますと幸いです。
要は重複行を削除したいんですか?
パッと思い浮かぶものだとtoString()をキーにハッシュに詰め込んでObject.valuesで取り出すとか。

コード: 全て選択

const tempHash = {};
$gameVariables.value(DBvalNum)
    .forEach(row => tempHash[row.toString()] = row);
const newArray = Object.values(tempHash);
これに加えてアイテム個数?を加算したいという事ですか?(どの列が個数なのかよく分からないけど)

liuhong さんが書きました:これらの要素を連動させなくするか、
パッと見る限り典型的なシャローコピーとディープコピーの混同例って感じじゃないですか。
Array.fromはシャロー(浅い)コピーなのでJsonEx.makeDeepCopyを使用してください。
JsonEx.makeDeepCopyはrmmz_core.jsで定義されている関数です。
ディープコピーとシャローコピーの違いについてはググればいくらでも出てくると思います。
アバター
サブちゃんB
記事: 27
登録日時: 2018年3月06日(火) 01:09

Re: 二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

投稿記事by サブちゃんB » 2022年12月13日(火) 19:56

名無し蛙様、いつもご指導頂きありがとうございます。
先日も大変貴重な知見をご提供頂いて、本当に助かりました。

シャローコピーとディープコピーはいくつかの記事で目にしましたが、個数列である3列(インデックス2)へ加算する値を整数にしても連動するため、これは関係ないかなと優先度を下げておりました。
改めて短時間ながら調べてみた上で考えると、仰るとおり確かに関係がありそうです。
このコードの加算部分のArray.fromをJsonEx.makeDeepCopyへ変えても結果は変わらなかったので、その前の処理のどこかでシャローコピーが行われている可能性を疑っています。
同じ配列の異なる要素同士の比較と四則演算のみに意識が行き、コピーについて意識しておりませんでした。
ご教示頂いたことをよく調べてからコードを見直したいと思います。

こちらが暗礁に乗り上げたら、教えて頂いたtoString()での個数まとめについても調べて検討したいと思います。
今回も大変ありがとうございます。
アバター
サブちゃんB
記事: 27
登録日時: 2018年3月06日(火) 01:09

Re: 二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

投稿記事by サブちゃんB » 2022年12月13日(火) 21:10

上記の説明では何を目指しているかが分かりづらいと思いましたので、画像を用意しました。
スクリーンショット 2022-12-13 20.15.16_added.jpg
処理がうまくいく元の配列

↑これを
スクリーンショット 2022-12-13 20.15.26_added.jpg
理想的な結果

↑こうしたいです。

行が少なかったりするとうまくいくこともあるのですが、冒頭の例ではうまくいきません。
アバター
サブちゃんB
記事: 27
登録日時: 2018年3月06日(火) 01:09

Re: 二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

投稿記事by サブちゃんB » 2022年12月13日(火) 21:17

速報ですが、成功パターンを見つけました!
どうやら処理を実行する前にいったんセーブし、ゲームを再起動してロードしてから処理を実行すると、うまくいきます。
もしかしたらその辺に答えがあるかもしれません。
解決した場合は改めて結果を報告致します。
宜しくお願い致します。
アバター
サブちゃんB
記事: 27
登録日時: 2018年3月06日(火) 01:09

Re: 二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

投稿記事by サブちゃんB » 2022年12月13日(火) 21:39

お陰様で無事に解決致しました!
名無し蛙様に教えて頂いたJsonEx.makeDeepCopyを使って、
関数冒頭でローカル変数に配列を渡して、
加算するときもローカル変数から配列をディープコピーしてから要素を加算し、
関数最後でローカル変数をディープコピーで元の変数へ戻すことで希望通りの挙動が実現出来ました!

当初自力で調べて出した結論ではディープコピー関連の可能性が薄いと判断していましたが、
結果としては名無し蛙様の仰るとおりディープコピーにより解決致しました。

この度もお助け頂きまして、本当にありがとうございました。
chro
記事: 86
登録日時: 2021年2月14日(日) 11:26

Re: 二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

投稿記事by chro » 2022年12月13日(火) 23:13

すっきり書くとこんな感じでしょうか。

コード: 全て選択

//パターン1
const sumMakeIndexArr = function(arr, keyIndex, sumIndex) {
    const parents = [];//親となる、最初に出現したIDがある配列のindex
    arr.forEach((a, i) => {
        const id = a[keyIndex];
        let parentIndex = parents[id];
        if (parentIndex === undefined)  {
            //初めて出現するIDの場合、親として扱い、数値はそのまま
            parents[id] = i;
        } else {
            //既にIDが存在した場合は、そのindexへ加算
            arr[parentIndex][sumIndex] += a[sumIndex];
            //親へ合算したため、0を代入
            a[sumIndex] = 0;
        }
    });
};

//パターン2
const sumMakeSumArr = function(arr, keyIndex, sumIndex) {
    const sumArr = []; //合算した数値を一時保存する配列
    //一時保存する配列に対して、合算する処理
    arr.forEach((a, i) => {
        const id = a[keyIndex];
        //一時保存する配列に、同じIDの値を合算する
        sumArr[id] = (sumArr[id] || 0) + a[sumIndex];
    });
    //一時保存した合計値を、それぞれの配列に合計か0を反映
    arr.forEach((a, i) => {
        const id = a[keyIndex];
        a[sumIndex] = sumArr[id];//一時保存(0もしくは合算)を、現在の合計に上書き
        if (sumArr[id]) sumArr[id] = 0; //一時保存した合計が0以外の場合に、0で上書きする
    });
};

//サンプル
var arr1= [[0,1,100,98,0,0,0],[0,2,100,98,0,0,0],[0,4,100,0,0,0,0],[0,2,100,98,0,0,0],[0,3,100,0,0,0,0],[0,2,100,98,0,0,0],[0,3,100,0,0,0,0]];
var arr2= [[0,1,100,98,0,0,0],[0,2,100,98,0,0,0],[0,4,100,0,0,0,0],[0,2,100,98,0,0,0],[0,3,100,0,0,0,0],[0,2,100,98,0,0,0],[0,3,100,0,0,0,0]];

sumMakeIndexArr(arr1, 1, 2); // (配列, アイテムIDのindex, 合計するindex)
sumMakeSumArr(arr2, 1, 2); // (配列, アイテムIDのindex, 合計するindex)

if (arr1.toString() === arr2.toString()) console.log('同一です');

arr1.sort((a,b)=> a[1] - b[1]);//IDの昇順に並び替えたい場合


パターン1と2は、アプローチが多少違うだけで結果は同じです。

コード: 全て選択

var arr = new Array(new Array(0, 1, 2), new Array({a:1,b:2}, function(){}));

二次元配列と言っても、単に配列の中に新しい配列で、擬似的に多次元配列として扱っています。
配列の中に、配列以外のオブジェクトや関数、値なども代入できるので、全てディープコピーされる事はありません。
アバター
サブちゃんB
記事: 27
登録日時: 2018年3月06日(火) 01:09

Re: 【解決】二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

投稿記事by サブちゃんB » 2022年12月14日(水) 18:32

chro様、大変素晴らしいコードのご提供ありがとうございます!
私が何時間もかけて四苦八苦しながら組んだ長文コードがこんなにスッキリ書けてしまうなんて、まるで魔法みたいです。
頂いたコードを次のような形で実装したところ、正常に動作しました。

パターン1

コード: 全て選択

  Game_Temp.prototype.sumMakeIndexArr_Sort = function(arrNum, keyIndex, sumIndex){
    let arr = JsonEx.makeDeepCopy($gameVariables.value(arrNum));
    const parents = [];//親となる、最初に出現したIDがある配列のindex
    arr.forEach((a, i) => {
        const id = a[keyIndex];
        let parentIndex = parents[id];
        if (parentIndex === undefined)  {
            //初めて出現するIDの場合、親として扱い、数値はそのまま
            parents[id] = i;
        } else {
            //既にIDが存在した場合は、そのindexへ加算
            arr[parentIndex][sumIndex] += a[sumIndex];
            //親へ合算したため、0を代入
            a[sumIndex] = 0;
        }
    });
    $gameVariables.setValue(arrNum, JsonEx.makeDeepCopy(arr));//ディープコピーして処理したソート結果を元の変数へディープコピーで返す。
  }


パターン2

コード: 全て選択

  Game_Temp.prototype.sumMakeSumArr = function(arrNum, keyIndex, sumIndex){
    let arr = JsonEx.makeDeepCopy($gameVariables.value(arrNum));
    const sumArr = []; //合算した数値を一時保存する配列
    //一時保存する配列に対して、合算する処理
    arr.forEach((a, i) => {
        const id = a[keyIndex];
        //一時保存する配列に、同じIDの値を合算する
        sumArr[id] = (sumArr[id] || 0) + a[sumIndex];
    });
    //一時保存した合計値を、それぞれの配列に合計か0を反映
    arr.forEach((a, i) => {
        const id = a[keyIndex];
        a[sumIndex] = sumArr[id];//一時保存(0もしくは合算)を、現在の合計に上書き
        if (sumArr[id]) sumArr[id] = 0; //一時保存した合計が0以外の場合に、0で上書きする
    });
    $gameVariables.setValue(arrNum, JsonEx.makeDeepCopy(arr));//ディープコピーして処理したソート結果を元の変数へディープコピーで返す。
  }


引数にそのまま配列を書く形ですと最後に$gameVariables.setValue()で戻せないので、ツクール側の変数番号を入力する形へ変更してあります。
加算する値までわざわざディープコピーする必要がないので、行が増えても軽快な動作も期待できます。
具体例を2つもお示し頂いたことで、他の組み方のパターンを考える上で大変参考になります。

undefinedを使った条件式は、splice()で指定の行を削除するforループ中に配列の長さが変わって処理漏れが出たり存在しない行を読もうとしてエラーが出たりするのを防ぐための終端検出に使えないかと横着したことがあります。
そんな虫の良い使い方が出来るなら、みんなやってますよね。

sort()については桁数が多くなると変な順番にソートされて混乱したことがあって、調べてみると文字型として辞書順にソートされるので値の大きさを比較するには工夫が必要ということが分かりました。
複数条件かつ数値順のソートのやり方を調べても分からなかったのですが、試しに比較する各値をNumber()で無理やり数値型にしたら(少なくともこれまで試した配列では)うまく動いたのでこのやり方でやっています。
もっとスマートなやり方がありそうな気がするのですが、とりあえず動けば正義ということでこうしています。

この度も大変貴重な知見をご教示頂き、感謝申し上げます。
chro
記事: 86
登録日時: 2021年2月14日(日) 11:26

Re: 【解決】二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

投稿記事by chro » 2022年12月14日(水) 19:13

参照渡しですので、戻したりする必要はないです。
たぶん値型と同じ考えでやっていて、どこかでミスしていたと思います。
その為、ディープコピーで解決されたので、今回はディープコピーが不要です。
ディープコピーでも、新しい配列を作成する場合でも、メモリ消費してしまいますので。
ディープコピーせず、そのまま変数の配列を渡しても、正常に動作するはずです。

JsonEx.makeDeepCopyはツクールが特殊なだけです。

コード: 全て選択

var arr1 = [[], []];
$gameVariables.setValue(11, arr1);
var arr2 = $gameVariables.value(11);
arr1[0][0] = 1;
console.log($gameVariables.value(11)[0][0] + arr2[0][0]);//2
if (arr1 === $gameVariables.value(11)) console.log('参照渡しです!');
if (arr2[0] === $gameVariables.value(11)[0]) console.log('参照渡しです!');
if ([1] !== [1]) console.log('参照が違います!');
アバター
サブちゃんB
記事: 27
登録日時: 2018年3月06日(火) 01:09

Re: 【解決】二次元配列の要素同士が連動(?)する条件と回避方法が分かりません。

投稿記事by サブちゃんB » 2022年12月14日(水) 20:12

スクリーンショット 2022-12-14 20.02.59_trim.jpg

パターン1の結果ですが、実は配列をそのまま渡して処理をすると、当初相談したときと同じ症状が出てしまいました。
処理前にセーブをしてゲームを再起動してからロードして処理をするとうまくいく、という点も同じです。
もしかしたら$gameVariablesでは通常の変数と事情が異なるのかも知れません。

“MZ:質問” へ戻る