tomo_manaのブログ

tomo_manaのブログ

Unityで一からゲームを作る方法を紹介しています。

Unity学習#26-5 ContentSizeFitter を使って LayoutGroup を Element の合計サイズにフィットさせる (Unity 2019.4.4f1)

#26 の実装にあたって、少しLayoutについて調べた内容を備忘のためにまとめます。#26-2 で、テキストやイメージに合わせて枠がスケールするようにしたい場合に、LayoutGroup を2重にすることで実現できることを書きました。ContentSizeFitter でも同様の挙動を実現できます(こちらの方が一般的?)。

<目次>


#26-2と合わせて読むと分かりやすいです。
tomo-mana.hatenablog.com

概要

ContentSizeFitterを使ってテキストやイメージに合わせて枠がスケールするようにするには、以下のようにします。(コーディング不要です)

f:id:tomo_mana:20210227224754p:plain
Hirarchy
f:id:tomo_mana:20210227225613p:plain
Inspector

LayoutGroupを2段構えにする方法に比べて、ContentSizeFitterでの設定方法はとてもシンプルです。以下に動作イメージを示します。

動作イメージ

ContentSizeFitter は、自分が属しているゲームオブジェクトが持っている ILayoutElement の情報で、自分の RectTransform を更新します。以下の図の場合、LayoutGroup が ILayoutElement を実装しています(ContentSizeFitter 自身は ILayoutElement 情報を持ちません)。

f:id:tomo_mana:20210227093623p:plain
ContentSizeFitter

#26-2 で示した LayoutGroup を2段構えにした時の図と比較すると、ContentSizeFitter は、下の図の親 (Parent) LayoutGroup の代わりをしていることが分かります。

f:id:tomo_mana:20210227093222p:plain
LayoutGroup 2段

RectTransformの更新方法の違い

では、ContentSizeFitter を使用するのと、LayoutGroup を2段構えにするのとでは、何が違うのでしょうか。

ContentSizeFitter: RectTransform の anchor, pivot 情報は変更せず、サイズ (Width, Height)だけを更新します。※FitMode.Unconstrainedの場合は何もしません。

LayoutGroup(2段): RectTransform の anchor, pivot 情報 と サイズ (Width, Height)の両方を更新します。
ただし親側のLayoutGroup は anchor, pivot 情報がそのまま使えるので、この点では ContentSizeFitter と違いはありません。・・・違いがあるとしたら、親側のLayoutGroup が持っている Width, Height が、最大値(クリッピング)の役目を果たすくらいかと思います。(今回は調べていませんが、これもMaskを使用することで解消するのではと思います)


繰り返しになりますが、#26-2 と併せて調べたことで、Layout の処理がよく理解できました。

参考

クラス図

f:id:tomo_mana:20210226080213p:plain
クラス図 - ContentSizeFitter

ContentSizeFitter は、Layout関連のインターフェースのうち、ILayoutSelfController のみを実装したコンポーネントです。そのため、ContentSizeFitter 自体は自身のRectTransform の Width や height を計算するための情報を持っていません。ILayoutElementを持ったゲームオブジェクトと一緒に使うことで効果を発揮します。

f:id:tomo_mana:20210228004122p:plain
UI.ContentSizeFitter - Unity スクリプトリファレンス


シーケンス図

ContentSizeFitter は、VerticalLayoutGroup など、ILayoutElement を内包する ILayoutController と併用することで威力を発揮します。あらかじめILayoutElement の機能である CalculateLayoutInputHorizontal() などのインターフェースで子要素の合計サイズ(preferred)を計算し、その後でVerticalLayoutGroup などの ILayoutController よりも先に自分のサイズを更新します。(各インターフェースのコール順は #26-1 に詳しく書いてあります)

f:id:tomo_mana:20210228001833p:plain
シーケンス - ContentSizeFitter

(以上)

Unity学習#29 戦闘シーン、マネージャーシーンの追加 (Unity 2019.4.4f1)

今回は、戦闘の実装に先立って、これまで作ってきたフィールドとは別に、戦闘シーンを作成して、フィールドと戦闘を切り替える管理シーン(マネージャーシーン)を準備します。

方針

シーンの加算ロードを利用して、起動時にフィールドと戦闘の両方をロードします。可視性はカメラで切り替えます。

加算シーンに関わるインターフェースは以下にまとめました。
tomo-mana.hatenablog.com

シーンの構成

フィールドと戦闘でシーンを分け、両方を切り替えるマネージャーシーンを用意します。

f:id:tomo_mana:20210224223527p:plain
Manager から Field、Battle を加算ロード

シーン(.Unityファイル)の作成

マネージャーシーンの作成

Projectウィンドウから、空のシーンを作成します。
(1) Projectウィンドウ:シーンが置かれたフォルダ上で右クリック > create > scene
(2) 作成した Sceneファイル 上でクリック > rename
"ManagerScene" に変更

戦闘シーンの作成

Projectウィンドウから、空の戦闘シーンも作成しておきます。
(作業はマネージャーシーン作成時と同じ。"BattleScene"に変更)

フィールドシーンの名称変更

これまで作ってきたシーンも、"FieldScene"に名前を変えます。

f:id:tomo_mana:20210224223922p:plain
Projectウィンドウ - Scene

BuildSettingsに登録

作成したシーンを読み出せるようにするには、BuildSettingsに登録が必要です。
(1) Unity画面のメニュー: File > BuildSettings
(2) Build Settings ウィンドウ: Scenes In Build 欄に、ManagerScene、FieldScene、BattleScene の3つを追加(Projectウィンドウからドラッグ&ドロップ)

f:id:tomo_mana:20210223220134p:plain
BuildSettings

(3) 画面右上の×ボタンで画面を閉じます。(ドラッグ&ドロップした時点で登録されるようです)

各シーンの設計

戦闘シーン

戦闘シーンは、今の時点ではカメラだけOFFにしておきます。(フィールドのカメラを使用するため)

(1) Projectウィンドウ:BattleSceneを選択
(2) Inspectorウィンドウopen ボタンを押してBattleSceneの編集画面にします。

f:id:tomo_mana:20210224224333p:plain
Inspector - Scene選択時

(3) Hierarchyウィンドウ: Main Cameraを選択
(4) Inspectorウィンドウ: Main Camera の横にあるチェックを外し、Main Cameraゲームオブジェクト全体を非表示にします。

f:id:tomo_mana:20210224224544p:plain
Inspector - MainCameraゲームオブジェクト

マネージャーシーン

マネージャーシーンから起動する時に、フィールドと戦闘の両方をロードする処理を追加します。フィールドのメインカメラをONにする処理も追加します。
(1) Hierarchyウィンドウ: [▼+] > Create Empty
 初回の動作だけさせるつもりなので、名前を Initializer にしました。
(2) Projectウィンドウ上で、C#スクリプトを作成
 Start() で フィールドと戦闘シーンの両方を非同期に読み込む処理と、読み込みが完了したらフィールドカメラをONにする処理とを追加します。
(3) (1)で作成したゲームオブジェクト (Initializer) に追加します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class ManagerSceneManager : MonoBehaviour
{
    bool initialized = false;
    
    void Start()
    {
        // シーン起動
        SceneManager.LoadSceneAsync("FieldScene", LoadSceneMode.Additive);
        SceneManager.LoadSceneAsync("BattleScene", LoadSceneMode.Additive);
    }
    
    void Update()
    {
        if( !initialized ){
            if( SceneManager.GetSceneByName("FieldScene").isLoaded ){
                
                Scene scene = SceneManager.GetSceneByName("FieldScene");
                
                // フィールドシーンをアクティベート
                SceneManager.SetActiveScene(scene);
                
                // カメラON
                GameObject[] gameObjects = scene.GetRootGameObjects();
                foreach( GameObject g in gameObjects ){
                    if( g.GetComponent<Camera>() != null ){
                        g.SetActive(true);
                        break;
                    }
                }
            }
        }
    }
}

動作テスト

マネージャーシーンに切り替えて動作を確認します。
(1) Projectウィンドウ:ManagerSceneを選択
(2) Inspectorウィンドウ: open を押してManagerSceneの編集画面にする
(3) 再生する

ManagerSceneから2つのシーンがロードされて、FieldSceneがアクティブになり、これまで通りキャラクターも動かせることを確認できました。

f:id:tomo_mana:20210224225557p:plain
加算ロードのテスト

次回

戦闘画面の設計

Unity学習#29-1 シーンのロード/アンロード (Unity 2019.4.4f1)

戦闘シーンの追加にあたり、シーンの基本的な操作について調べました。


備忘のために、スクリプトからシーンを操作する時に必要になる関数などをまとめました。

シーン

シーンはプロジェクト設定を記述したテキストファイルで、それ自体が一つのクラスインスタンス化します。ただしクラスインスタンスとしてアクセスできるのはHierarchy ウィンドウ上でシーンの直下にぶら下がっているGameObject または Transform だけです。

ファイルとしてのシーン

ファイルとしてのシーンは、プロジェクトの設定ファイルです。通常はProject > Asset > Scene に作成されます。拡張子は .unity です。

f:id:tomo_mana:20210223001457p:plain
UnityEditor上でのScene
f:id:tomo_mana:20210223001659p:plain
FileExplorer上でのScene (.Unity)

ファイルの中身はテキストで、シーンに属するゲームオブジェクトなどの情報が記述されています。

クラス・インスタンスとしてのシーン

シーンは、後述するシーンマネージャーによって、インスタンスにできます。通常は現在実行中のシーンがインスタンスとして動いていて、開発者はシーンをあまり意識しなくてもスクリプトを実装できます。シーンにはスクリプトを付与できません。

シーンの識別子

後述するシーンマネージャーを使って、以下の識別子でシーンを探すことができます。

(シーンの識別子)

識別子 Scene変数 SceneManagerインターフェース
BuildSettingsのID Scene::buildIndex SceneManager.GetSceneByBuildIndex()
シーン名 Scene::name SceneManager.GetSceneByName()
ファイルパス Scene::path SceneManager.GetSceneByPath()

しかしながら、シーンは他のプレハブと同じように、同一のシーンを複数ロード(インスタンス化)できます。そのため、シーンを複製する場合は、以下のインターフェースに頼ることになります。

(シーンマネージャー上のリスト番号)

識別子 SceneManagerインターフェース
シーンマネージャー上のリスト番号 SceneManager.GetSceneAt()

シーンマネージャー

複数のシーンを並列的に動かす仕組みです。シーンをリストとして持ち、前述のリスト番号で各シーンにアクセスできます。シーンマネージャーを使用するには以下をインクルードします。

using UnityEngine.SceneManagement;

シーンマネージャーはstaticなインターフェースを提供します。

(シーンマネージャーのインターフェース)

機能 インターフェース
シーンのロード LoadSceneAsync("シーン名", LoadSceneMode.Additive)
現在選択されたシーン Scene = GetActiveScene()
選択されたシーンの変更 SetActiveScene(Scene)
シーンのアンロード UnloadSceneAsync()

(シーンのインターフェース)

状態 定義ラベル
シーンがロードされたか Scene::isLoaded


以下、各インターフェースについてまとめます。

シーンのロード

シーンのロードはシーンマネージャーから実行します。

SceneManager. LoadSceneAsync("シーン名", LoadSceneMode.Additive);

f:id:tomo_mana:20210223000852p:plain

シーンがロードされたか

シーンがロードされたかどうかは、シーンの状態で確認します。

SceneManager.GetSceneByName("シーン名").isLoaded

シーンをロードする条件

シーンをロードするために、あらかじめBuildSettingsにシーンを登録しておく必要があります。
(1) Unityエディタメニューから、File > BuildSettings
(2) 登録するシーンをBuildSettings画面にドラッグ&ドロップ
(3) BuildSettings画面を閉じます。(Buildボタンなどは押さなくてよい)

注意点1: Main Camera の多重化

LoadSceneMode.Additive でシーンをロードした時、Main Camera が Active だと、選択された画面に Additive されたカメラが適用されて、変な表示になることがあります。ロードするシーンは、MainCamera を 非Active にしておく方が良さそうです。

注意点2: AudioListenerの多重化

AudioListener は 2つ以上あると以下の警告が出ます。こちらも、Main Camera と同じく、ロードされた時に非Activeにして、同時に2つの AudioListenerを立ち上げないようにする必要があります。

現在選択されたシーン

LoadSceneMode.Additive で呼ばれたシーンはロードされただけでは選択状態になりません。選択状態にあるシーンだけ、GameObjectなどの作成が可能なようです。選択されているシーンは以下の関数で確認ができます。

Scene scene = SceneManager.GetActiveScene();

シーンの切り替え(シーンがロードされた状態で)

選択されたシーンの変更は以下の関数で行います。

Scene newScene = GetSceneAt( 1 );
SceneManager.SetActiveScene( newScene );

シーンのアンロード

シーンのアンロードは以下の関数で行います。

SceneManager.UnloadSceneAsync("シーン名");

f:id:tomo_mana:20210221230626p:plain

アンロードに関しても、注意点があります。

注意点1: Activeなシーンが削除された場合、ほかにActiveになれるシーンが選択される

しかしながら、通常はバグでもない限り、Activeなシーンを削除しないと思います。

注意点2: 最後の一つのシーンは削除できない

アンロードしようとするシーンが最後の1つだった場合、以下の警告が出て削除できません。こちらも通常は行わないオペレーションですね。
f:id:tomo_mana:20210221230708p:plain
Unloading the last loaded scene Assets/Scenes/FieldScene.unity(build index: 0), is not supported. Please use SceneManager.LoadScene()/EditorSceneManager.OpenScene() to switch to another scene.
UnityEngine.SceneManagement.SceneManager:UnloadSceneAsync(Scene)


(以上)

Unity学習#28 ランダムエンカウント (Unity 2019.4.4f1)

今回は、ランダムエンカウント(一定歩数歩いたら敵が出る)に挑戦しました。


UnityEngine.Random については、別の記事にまとめました。
tomo-mana.hatenablog.com

以下の2種類を試しました。
A) 1歩あるくごとに乱数を振り出す(1歩ごと乱数振出)
B) 目標歩数の補正値として乱数を振り出す(目標歩数+補正値)
⇒ AよりもBの方がゲームバランスが取りやすそうでした。

乱数データ

Random.InitState(255) を指定した後に振り出される300回分の乱数データを使いました。
f:id:tomo_mana:20210219232112p:plain

A:1歩ごと乱数振出

アルゴリズム

歩数の整数部が前回と異なる場合、エンカウント処理をします。エンカウントは20歩に1回(乱数の値が {0〜0.05} ならエンカウント)を目標にします。

(1)前回と今回の歩数が違うなら
(2)前回との今回の歩数の差だけ、乱数を振り出す
(3)乱数が1回でも閾値以下ならエンカウント回数+1

結果

乱数300回振り出す間に13回エンカウントし、平均エンカウント歩数は+3σ(99.7%)で 20.69±74.15歩、最小=2, 最大=78 でした。無限に繰り返せば平均値に収束すると思いますが、エンカウント歩数の最大値が予測できないのが弱点と思われます。

f:id:tomo_mana:20210218230613p:plain
ランダムエンカウントー1歩ごとに乱数振出

尚、処理速度の面で以下を懸念していましたが、大丈夫でした。
⚫︎1歩ごとに1回エンカウント処理は処理が間に合うか→ok
⚫︎ナナメ歩きすると2歩としてカウントアップされることが、処理が追従できるか→ok

B:目標歩数+補正値

アルゴリズム

A と同じく目標歩数は20歩とします。
a) 平均20±5歩以内に敵が出る確率は80%(10回中8回は15歩~25歩の間にエンカウント)
b) 45歩以内には100%敵が出現(10回中2回は25歩~45歩の間にエンカウント)

途中の式は省略しますが、それぞれ以下の式になります。
a) エンカウント歩数 = 12.5f x 乱数 + 12.5f
b) エンカウント歩数 = 100.0 x 乱数 + 25.0f

結果

A に合わせて13回分のエンカウント歩数を計算しました。平均エンカウント歩数は+3σ で 21.15±30.22歩、min=13, max=44でした。理論上は20歩と35歩を中心値としたふた山の平均に収束します。

f:id:tomo_mana:20210218230640p:plain
目標歩数+補正値

コード

第27回のコードを少し改造して、以下のようにしました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerControl2 : MonoBehaviour
{
    // キー入力
    private Vector2 input;
    
    // 移動
    private float speed;
    private Rigidbody2D rigidBody;
    
    // 歩数カウンタ
    [SerializeField]
    int stepsInt = 0;
    private float steps = 0.0f;
    
    // ランダムエンカウント
    [SerializeField]
    ulong encountered = 0;
    [SerializeField]
    ulong nextEncounterStep = 0;
    [SerializeField, Range(0f, 1f)]
    float encountRate = 0.05f;
    
    // Start is called before the first frame update
    void Start()
    {
        speed = 0.1f;
        rigidBody = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        steps = 0;
        
        UnityEngine.Random.InitState(255);
        NextEncount();
    }

    public void Move(Vector2 v)
    {
        input = v;
    }
    
    void FixedUpdate() {
        // 移動
        if (input == Vector2.zero){
            return;
        }
        rigidBody.position += input * speed;
        
        /* 中略 */
        
        // 歩数カウンタ
        float localSteps = steps;
        if( input.x != 0 ){
            localSteps += speed;
        }
        if( input.y != 0 ){
            localSteps += speed;
        }
        
        // ランダムエンカウント
        RandomEncount( localSteps );
    }
    
    void RandomEncount( float localSteps )
    {
        steps = localSteps;
        stepsInt = (int)steps;
        
        if ( steps > nextEncounterStep ){
            Encount();
            encountered++;
            
            // 次のエンカウント歩数を算出
            NextEncount();
        }
    }
    
    void NextEncount(){
        float r = Dice();
        float s;
        if( r > 0.2 ){
            s = 12.5f * (r - 0.2f) + 12.5f;
        } else {
            s = 100.0f * r + 25.0f;
        }
        nextEncounterStep += (ulong)s;
    }
    
    float Dice(){
        float r = UnityEngine.Random.value;
        return r;
    }
    
    void Encount(){
        // 今は何もしない
    }
}

画面表示

f:id:tomo_mana:20210219234207p:plain
Inspector - Player

※実際に動かしてみると、テスト前に取得していた乱数の値の2回目と3回目が(Start~最初のエンカウントの間の)どこかで使われてしまったのか、少しシミュレーションした値と算出結果が異なりました。

余談

昔、「魔界塔士SaGa2」というゲームでは、ロード直後の2~3回のエンカウントに再現性がありました。その2~3回のうちに能力値がアップするように行動すると、セーブ⇒電源再起動⇒エンカウントを繰り返すことで簡単に能力値を最大まで増やせるという裏技がありました。(ただし素早さを上げることで敵の攻撃パターンが変わってしまうと、乱数の振り出され方が変わるため能力値がアップしなくなる)

次回

戦闘シーンの作成に向けて、シーンロードに挑戦します。

Unity学習#28-1 乱数の生成 (Unity 2019.4.4f1)

ランダムエンカウントで使用する乱数について調べた内容をまとめます。


Unityで使用される乱数はXorShiftを使用している?XORとSHIFTだけで計算するため軽量、かつ一周するまでに2128-1のため周期性を予測されにくいといわれています。

(参考にしたサイト)
techblog.kayac.com

インターフェース

using UnityEngine;

機能 定義ラベル
初期化 void Random.InitState (int seed)
範囲指定出力 float Random.Range (float min, float max)
出力 float Random.value

static のため、マルチスレッドで使用禁止のようです。

※なお、Randomクラスは、System.Random と UnityEngine.Randomがあります。Systemをincludeする場合は、UnityEngine.Randamで呼びます。

乱数の値

(1) 再現性があるか→ある(Unity公式にも記載ありました)
(2) シード値から乱数傾向を容易に予測できないか→できない

再現性

いくつかのシード値で10回乱数を振り出した時の値を確認しました。

seed 1 2 3 4 5 6 7 8 9 10
255 0.43 0.99 0.81 0.02 0.22 0.15 0.35 0.19 0.34 0.33
254 0.97 0.86 0.82 0.36 0.44 0.32 0.55 0.01 0.81 0.66
1 1.00 0.77 0.68 0.46 0.59 0.78 0.91 0.14 0.26 0.56
0 0.58 0.58 0.67 0.77 0.31 0.65 0.65 0.50 0.28 0.13

(Random.value の値をログ出力したもの)

※シード値が同じであれば、毎回上記の順に出力されます(上記の表は小数点2桁で丸めてあります)

予測可能性

先ほどの乱数をシード値ごとにグラフ化しました。10回程度では何とも言えませんが、シード値で乱数の傾向を予想することは難しいのではないかと思います。

f:id:tomo_mana:20210217235142p:plain
seed値ごとの乱数

(以上)

Unity学習#27 歩数カウンタ (Unity 2019.4.4f1)

今回から、戦闘シーンの設計に入ります。今回は、歩いた時に敵に遭遇する(ランダムエンカウント)を実現するために、歩いたかどうかをチェックする機能(歩数カウンタ)を追加します。

仕様

機能そのものは非常にシンプルです。
プレイヤーの画面上の移動量をそのまま歩数カウンタに格納します。xyそれぞれの移動量をカウントアップします。
ランダムエンカウント処理は、歩数が整数値で1以上変更になったら行う想定です。

実装

ベースとなるコード

(キャラクターの移動は#2~#4のコードをベースにします)
tomo-mana.hatenablog.com

修正コード

修正部分のみピックアップします。(入力処理はInput Systemです。)

(コード)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerControl2 : MonoBehaviour
{
    // キー入力
    private Vector2 input;
    
    // 移動
    private float speed;
    private Rigidbody2D rigidBody;
    
    /* 中略 */
    
    // 歩数カウンタ
    [SerializeField]
    float steps;   // 追加

    void Start()
    {
        speed = 0.1f;
        rigidBody = GetComponent<Rigidbody2D>();
        steps = 0;   // 追加
    }

    public void Move(Vector2 v)
    {
        input = v;
    }
    
    void FixedUpdate() {
        // 移動
        if (input == Vector2.zero){
            return;
        } else {
            // 追加
            if( input.x != 0 ){
                steps += speed;
            }
            if( input.y != 0 ){
                steps += speed;
            }
        }
        rigidBody.position += input * speed;
        
        /* 以下略 */
    }
}

PlayerのInspector上で歩数が見られるようになりました

f:id:tomo_mana:20210217225537p:plain
歩数カウンタ

次回

次回はランダムエンカウントを実装します。

Unity学習#26-2 LayoutGroupを2重にすると LayoutGroup が Element の合計サイズにフィットする原理 (Unity 2019.4.4f1)

#26 の実装にあたって、少しLayoutについて調べた内容を備忘のためにまとめます。テキストやイメージに合わせて枠がスケールするようにしたい場合に、LayoutGroup を2重にします。どうしてLayoutGroupを2重にすることで枠をスケールできるようになるのかについて分かった範囲でまとめます。

<目次>


記事の大半は、前回#26-1の記事を参考にしています。
tomo-mana.hatenablog.com

概要

テキストやイメージに合わせて枠がスケールするようにするには、以下のようにします。(コーディング不要です)

(1) 枠用のゲームオブジェクトの親に、ダミーのLayoutGroupを配置します。(LayoutGroup2段構え)
(2) ダミーのLayoutGroup の Control Child Size = ON、枠用のLayoutGroup の Control Child Size = OFF にします。

f:id:tomo_mana:20210212231616p:plain
Hierarchy
f:id:tomo_mana:20210212231636p:plain
Inspector


以下、動作原理を簡単にまとめます。

LayoutGroup

1段(Group→Element)

LayoutGroup が LayoutElement を整列する手順は以下になります。

f:id:tomo_mana:20210210231956p:plain
LayoutGroup → LayoutElement

LayoutGroup が整列を行う時、LayoutRebuilder が全体に指示を出します。

(1) LayoutGroup の全ての LayoutElement に対して、CalculateLayoutInputHorizontal/Vertical() をコールします。LayoutElement は min, preferred, flexible width/height を計算します。このコールは子、親の順に呼ばれます。

(2) LayoutGroup も LayoutElement である必要があり、LayoutGroup がこの関数をコールされたときは、自分の子の min, preferred, flexible width/height を取得します。

(3) 次に、(LayoutGroup に属する全ての)LayoutGroup に対して SetLayoutHorizontal/Vertical() をコールして、先ほど取得したmin, preferred, flexible width/height を使って、子の位置(Anchor、Pos)を決めます(整列)。

この処理のポイントは、大きさを変更するためには RectTransform を更新する必要があり、RectTransform が更新されるのは LayoutGroup に属する LayoutElement だけ ということです。(LayoutElement は、大きさの情報を保持しているだけ)

2段(Group→Group→Element)

LayoutGroup を2重にしたときの処理は以下のようになります。

f:id:tomo_mana:20210211000251g:plain
LayoutGroup → LayoutGroup → LayoutElement

処理の順番は先ほどと同じです。違いは、

(4) 子供のLayoutGroupは、親のLayoutGroupから見ると、LayoutElementです。そのため、親のLayoutGroupは、子供のLayoutGroupのサイズ情報を使って、子供のRectTransformを更新します。

(5) 孫階層については1段の時と同じです。

そのため、親を子のサイズに合わせたい場合は、親の上にもう一段ダミーの親を挟めばよいことになります。

ただし、もう一つ条件があります。Control Child Size の指定です。

Control Child Size

LayoutGroup の Preferred Width/Height の計算

ControlChildSize LayoutGroupのPreferredSize
OFF 子LayoutElementのRectTransformの合計
ON 子LayoutElementのPreferredWidth/Heightの合計

※上記に、自身の padding(四隅に入れるスペース)+ spacing(LayoutElement間に入れるスペース)を足したもの。

表示サイズ(RectTransform)の計算

ControlChildSize LayoutGroupの表示サイズ LayoutElementの表示サイズ
OFF LayoutGroupのRectTransform LayoutElementのRectTransform
ON(親 > 子) LayoutGroupのRectTransform LayoutGroupに従って圧縮
ON(親 < 子) LayoutGroupのRectTransform LayoutElementのPreferredWidth/Height

※Control Child Size を有効にしたとき、子のRectTransformのwidth/heightは親によって決められます。

子の大きさで親をスケールさせる条件

そのため、子の大きさで親をスケールさせるには、以下のようにします。
孫は自分で大きさを決めたい → 子のControl Child Size は OFF
子は孫のサイズで決めたい → 親のControl Child Size は ON

f:id:tomo_mana:20210213222233p:plain
各階層のRectTransformの大きさの決定

※LayoutElementのpreferredの計算は別の記事にまとめます。

参考(シーケンス図)

1段

f:id:tomo_mana:20210208013014p:plain
f:id:tomo_mana:20210208013026p:plain

2段

f:id:tomo_mana:20210208235036p:plain
f:id:tomo_mana:20210208235050p:plain
f:id:tomo_mana:20210208235100p:plain