オレンジブログ

オレンジブログ

GameMakerのちょっとしたTipsとか

【GMS2】FlexPanelを複製する時に値の変更が反映されない問題の原因と解決策


はじめに

GameMakerのUIレイヤーを使ってFlexPanelをコピーした時、
何故か正しく値の変更が行われないことがあって困っていたことがあります。

本当は数字を1,2,3...としたかった

こうなってしまった原因として、

複製したFlexPanelのさらに子のFlexPanelを別々で編集した場合
複製先には反映されない

ことがわかりました。

今回はその現象について、解決法も含めて解説します。
同じ現象で悩んでいる方がいたら是非最後まで読んでみてください!


原因

原因ですが、下記の画像のように、
複製したいFlexPanelの子にさらにFlexPanelがあり、
その子の方のTextを変更したい場合に問題が生じます。

こんな感じの構成の場合

上記スクショと同じ構成の場合、スクリプト

  • 複製するFlexPanelを取得
  • さらに子のFlexPanelを取得
  • そのFlexPanelのTextを変更
  • 複製する

といった手順になると思います。

// UiTemplateレイヤーを取得
template_layer = layer_get_flexpanel_node("UITemplate");
// "ItemTemplate"ノードを取得(アイテム表示に使うテンプレート)
item_template = flexpanel_node_get_child(template_layer, "NumberTemplate");
// テンプレートの構造体を取得して保存しておく
item_template_struct = flexpanel_node_get_struct(item_template);
// item_templateの子のTextPanelを取得
text_template = flexpanel_node_get_child(item_template, "TextPanel");
// TextPanelの構造体を取得
text_template_struct = flexpanel_node_get_struct(text_template);
// テキストをundefinedで初期化
textElement = undefined;
// TextPanelの中に含まれるUI要素(layerElements)を1つずつチェック
for (var i = 0; i < array_length(text_template_struct.layerElements); i++) {
    // 現在の要素
    var _currentElement = text_template_struct.layerElements[i];
    // 要素のタイプが "Text" のとき
    if(_currentElement.type == "Text") {
        // Elementを取得
        textElement = _currentElement;
    }
}

for (var i = 0; i < 15; i++) {
    // テキストをindexにする
    textElement.textText = string(_index);
    // テンプレートから新しいノードを作成(クローンを作る)
    var _newNode = flexpanel_create_node(item_template_struct);
    // 配置インデックス
    var _number = flexpanel_node_get_num_children(item_grid);
    // item_gridに追加
    flexpanel_node_insert_child(item_grid, _newNode, _number);
}

しかし、この方法では正しく複製が行えず
テキストは一切変更されません

そんなバカな!

これマジで理不尽だな〜と思ってました。


原因の推測

自分は開発者ではないので、細かい原因までは掴めていないのですが、
おそらく、原因は下記のコードの部分です。

// "ItemTemplate"ノードを取得(アイテム表示に使うテンプレート)
item_template = flexpanel_node_get_child(template_layer, "NumberTemplate");
// テンプレートの構造体を取得して保存しておく
item_template_struct = flexpanel_node_get_struct(item_template);
// item_templateの子のTextPanelを取得
text_template = flexpanel_node_get_child(item_template, "TextPanel");
// TextPanelの構造体を取得
text_template_struct = flexpanel_node_get_struct(text_template);

ここの、それぞれのFlexPanelを取得している部分です。
コードだけ読むと、親のFlexPanelを取得して、さらにその子のFlexPanelを取得しているだけに見えますが、
これ実は親子関係が繋がっていません


1つずつ解説します。

// "ItemTemplate"ノードを取得(アイテム表示に使うテンプレート)
item_template = flexpanel_node_get_child(template_layer, "NumberTemplate");

まずはここでNumberTemplateという名前のFlexPanelを取得しますね。
ここまではOKです。

// テンプレートの構造体を取得して保存しておく
item_template_struct = flexpanel_node_get_struct(item_template);

ここでitem_templateの構造体を取得しています。


flexpanel_node_get_struct関数でitem_templateの構造体を受け取っていますが、
おそらくこの関数で受け取った構造体は値のコピーです。
item_template_structは構造体ですけど、item_templateとは参照が切れています


つまり、item_template_structをいくら編集しようが、
item_templateというFlexPanelには何も影響しません


ここが1つ目のポイントです。


次のコードではこのように書いてありますね。

// item_templateの子のTextPanelを取得
text_template = flexpanel_node_get_child(item_template, "TextPanel");
// TextPanelの構造体を取得
text_template_struct = flexpanel_node_get_struct(text_template);

同様にTextPanelを取得して、そのTextPanelの構造体を取得している部分です。
そして、この処理も先ほどと同じく、構造体とFlexPanelの参照は切れています


なので、いくらtext_template_structを変更しても、 text_templateというFlexPanelには影響しません


このように、flexpanel_node_get_structで取得した構造体は値のコピーとなっています
それが原因となり、それぞれの構造体の親子関係は切れてしまっています

複雑な相関図

なので、いくらtext_template_structに変更を加えても、
親のitem_template_structに影響を及ぼしません

最終的には親の構造体であるitem_template_structを元に複製しているので、
複製先に変更が反映されなかったわけですね。


解決法

大まかに分けて2つあります。

  • コピーは1回だけに済ませ、親の構造体から子の値を変更する
  • 一旦複製してしまい、直接値の変更を行う

この2つです。それぞれ説明します。


解決法1: コピーは1回だけに済ませ、親の構造体から子の値を変更する

最もシンプルな解決方法です。
結局複数回値のコピーをして参照切りをしているのが問題なので、
一番最初にコピーをした構造体から値の変更を直接行うのが簡単な解決方法です。


実装の手順としては

  • 親FlexPanelの構造体を取得する(コピー)
  • その構造体から直接TextElementを取得する
  • そのTextElementのtextTextを変更する

となります。


先ほどのコードを書き直したのが下記です。

/// @desc 指定ノード名から最初の Text 要素を返す
/// @param _struct FlexPanel構造体
/// @param _targetName ノード名
/// @return Text要素の構造体(見つからなければundefined)
function GetFirstTextElementByNodeName(_struct, _targetName) {
    for (var i = 0; i < array_length(_struct.nodes); i++) {
        var _child = _struct.nodes[i];

        if (_child.name == _targetName) {
            for (var j = 0; j < array_length(_child.layerElements); j++) {
                var _element = _child.layerElements[j];
                if (_element.type == "Text") {
                    return _element;
                }
            }

            return undefined; // ノードは見つかったがTextが無かった
        }
    }

    return undefined; // ノードが見つからなかった
}

// UiTemplateレイヤーを取得
template_layer = layer_get_flexpanel_node("UITemplate");
// "ItemTemplate"ノードを取得(アイテム表示に使うテンプレート)
item_template = flexpanel_node_get_child(template_layer, "NumberTemplate");
// テンプレートの中身を取得して保存しておく
item_template_struct = flexpanel_node_get_struct(item_template);
// textElementをitem_template_structの子のTextPanelから探して取得する
textElement = GetFirstTextElementByNodeName(item_template_struct, "TextPanel");

// 15個配置
for (var i = 0; i < 15; i++) {
    // テンプレートから新しいノードを作成(クローンを作る)
    var _newNode = flexpanel_create_node(item_template_struct);
    // 配置インデックス
    var _number = flexpanel_node_get_num_children(item_grid);
    // item_gridに追加
    flexpanel_node_insert_child(item_grid, _newNode, _number);
}

このようになります。

GetFirstTextElementByNodeName関数内で、子のFlexPanelを名前一致で検索をして
一致したFlexPanelからlayerElementsの中身を検索してtextElementを取得しています。


このような処理で行えば、親の構造体のtextElementのtextTextを変更しているため、
複製時にも変更が反映されます。

この通り


解決法2: 一旦複製してしまい、直接値の変更を行う

最初に言っておきますが、この方法はあまりオススメしません
しかし、コード自体はシンプルなので慣れないうちは使っても問題ないと思います

題名の通り、まずは一旦複製をして、
そのあとは直接値の変更を行う関数を使って値を変更します。

コードはこのように書きます。

// UiTemplateレイヤーを取得
template_layer = layer_get_flexpanel_node("UITemplate");
// "ItemTemplate"ノードを取得(アイテム表示に使うテンプレート)
item_template = flexpanel_node_get_child(template_layer, "NumberTemplate");
// テンプレートの中身(見た目や構造)を取得して保存しておく
item_template_struct = flexpanel_node_get_struct(item_template);

/// @function                IncrementItemGrid(_index)
/// @description             テンプレートUIを複製し、アイテムグリッドに追加する。
/// @param {Real}            _index     表示用のインデックス番号(テキストとして使用)
function IncrementItemGrid(_index) {
    // テンプレートから新しいノードを作成(クローンを作る)
    var _newNode = flexpanel_create_node(item_template_struct);
    // 配置インデックス
    var _number = flexpanel_node_get_num_children(item_grid);
    // item_gridに追加
    flexpanel_node_insert_child(item_grid, _newNode, _number);
    
    // 新しいノード内の "TextPanel" を取得
    var _text = flexpanel_node_get_child(_newNode, "TextPanel");

    // テキストパネルの構造体を取得
    var _textStruct = flexpanel_node_get_struct(_text);

    // テキストパネルの中に含まれるUI要素(layerElements)を1つずつチェック
    for (var j = 0; j < array_length(_textStruct.layerElements); j++) {
        var _currentElement = _textStruct.layerElements[j];

        // 要素のタイプが "Text" のときだけ中身の文字を書き換える
        if(_currentElement.type == "Text") {
            // 引数で受け取った数字を使って直接テキストの文字を書き変える
            layer_text_text(_currentElement.elementId, string(_index));
        }
    }
}

// デバッグ用にアイテムを15個配置
for (var i = 0; i < 15; i++) {
    IncrementItemGrid(i);
}

このようなコードになります。
大きな変更点としては、IncrementItemGrid関数の

// 新しいノード内の "TextPanel" を取得
var _text = flexpanel_node_get_child(_newNode, "TextPanel");

// テキストパネルの構造体を取得
var _textStruct = flexpanel_node_get_struct(_text);

// テキストパネルの中に含まれるUI要素(layerElements)を1つずつチェック
for (var j = 0; j < array_length(_textStruct.layerElements); j++) {
    var _currentElement = _textStruct.layerElements[j];

    // 要素のタイプが "Text" のときだけ中身の文字を書き換える
    if(_currentElement.type == "Text") {
        // 引数で受け取った数字を使って直接テキストの文字を書き変える
        layer_text_text(_currentElement.elementId, string(_index));
    }
}

この部分ですね。


ここの処理では、複製して配置した後のFlexPanelを取得し、
TextPanelを構造体として取得しています。


その構造体からlayerElementsでTextElementを探して、
見つかったTextElementのelementIdを取得し、
それを使ってlayer_text_text関数でテキストの中身を書き換えています。


layer_text_textは、直接レイヤー上に存在するTextElementのテキストを変更する関数です。

引数としてelementIdが必要になるので、それを使ってテキストを変更しています。
これを使えば、複製後のテキストの変更も簡単に行えますが、
汎用性はあまりないのでオススメはしません。


まとめ

今回はFlexPanelを複製するときに生じやすい問題について、原因と解決策についてまとめました。
自分もUIレイヤーに慣れていない頃引っかかってしまいまして...原因がわかるまで2週間くらい戦った記憶があります。
(普通親子関係くらいは引き継いでいると思うでしょ...)


UIレイヤーは扱いこなすのが難しく、色々な問題にぶつかりますが、
便利な機能だとは思うので頑張って使いこなしましょう!