新たなPrefabワークフローでPrefabのRootCanvasのRectTransformが崩れる問題の対策

## 概要

https://blogs.unity3d.com/jp/2018/06/20/introducing-new-prefab-workflows
Unity 2018.3から新たなPrefabワークフローが入るようになりました。
Nested prefabやPrefabモードなど、様々な便利機能が入ることになりプレハブ作業が以前より楽になりました。

そんな中、2018.3になってからプレハブを触り始め、気づいた問題の話をやってみようと思います。

### きっかけ

[Unity UI の最適化に関するヒント](https://unity3d.com/jp/how-to/unity-ui-optimization-tips)

UIの最適化の話を見ると、「Canvasを分ける」という話があります。これは再構築のコストを下げるためであり、Canvasの1つ以上の要素に変更があるとCanvas全体を再構築する必要があるためです。(詳しくは上記リンクをご覧ください)
ということで、管理のしやすくするために親Canvasを持たせて、機能要素単位でプレハブを分割し、そのプレハブの頭にCanvasを持たせて動的に親Canvasの下に生成することをよく見かけます。

こういうワークフローとしてプレハブを使っていたところで、Unity 2018.3になってからプレハブにごく小さな変更を加えたところ、PlayModeでプレハブの内容が表示されなくなったことを気づきました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/165110/9597f432-b4c9-81d4-a4cb-1f62fbec78bd.png)

説明に使うプレハブです。親にCanvasを持たせていて子としてテキストを持つシンプルなプレハブです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/165110/15d7eaf5-1085-4c8e-29b1-0bc0c9e91556.png)

な..なにこれ?! RectTransformのプロパティが全てリセットされてしまっているようです。
コードの問題なのかなと思い、コードを変えてみました。

```csharp
//変更前
Instantiate(Hoge, HogeParent, false);

//変更後
var hoge = Instantiate(Hoge);
hoge.SetParent(HogeParent, false);
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/165110/7c104b8b-22c0-138f-740b-aef8357ab57c.png)

これだとサイズはなんとか取れたようですが、anchorが崩れたままで固定サイズになってしまうので、問題になりえる余地がまだ残っています。

余談で上記のコードの挙動の違いですが、
**一回でもプレイ中に該当プレハブをRootObjectとして生成すると、キャンバスのサイズが動的に計算され、SetParentとして移した際にその残った数値が入るようになる**ようです。

## 本題

さて、何故RectTransformプロパティがリセットされてしまうようになっているのか?
それは新たに入った、**Prefabモード**という機能が問題になりました。

### Prefabモードって何?

今までのPrefabの作業ワークフローとしては、まず空のシーン(もしくは適当なシーン)上で該当プレハブをシーン上にドラッグし、修正項目を適用した上でApplyボタンを押すことで解決してました。
毎回新たなシーンを作るのもだるいし、特に修正のために特定のセットアップが必要になる(例えばCanvasとカメラを持たせたり)際には手間がかかることが多かったです。
それを改善するために、現状のシーンと別途分離されている完全にプレハブだけを操作できるモードをユニティさんが用意してくれました。それがPrefabモードです。
作業シーンに既に乗っかっているプレハブからそれだけの操作できるシーンにすぐ飛びるのができるし、プレハブから直接移動することも可能です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/165110/e6f8dbbd-539f-35e4-6c2a-fff5a886fd6d.png)

このボタンを押すことで、プレハブのみを操作できる仮のシーンが開けます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/165110/7804e223-0a54-ec03-00a8-209341ce9483.png)

### 今までの通りシーン上での作業はできませんか?

以前サンプルの通りのプレハブで作業する際には、まず親キャンバスを持たせ、その下にプレハブをぶち込んだ状態で作業をし、そのままApplyを押すことで問題なくRectTransformの値が保存できてました。
それを思い出したところやってみようと思いましたが、なんと、Prefabモードで入らないままだと、Overridesという機能を使わない限りApplyはできません。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/165110/16ba38f0-3529-b9c6-91c7-44d18c8c474b.png)

しかも、Overridesに検知されないプロパティはApplyボタンが表示されないため、元のプレハブのApplyもできません。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/165110/fbd1956f-ccb6-87dd-3bc3-b40bb65ceb87.png)

じゃあ、Prefabモードを使わざるを得ない！

### しかし......

![image.png](https://qiita-image-store.s3.amazonaws.com/0/165110/ca7337a1-837a-dd93-da0a-c7b7a32689c0.png)

はい、CanvasがついているRootのRectTransformは変更不可であります。また、何故かこの状態でプレハブの保存を行うとRectTransformの値が初期化されてしまいます。(別件ですが、何故かScaleも0に戻ってしまいます。)

また、既にキャンバスがついているプレハブに関してはCanvasの中にも入れてくれないようです。↓
[Prefab処理のEditorUnityReferenceページ](https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/SceneManagement/StageManager/PrefabStage/PrefabStageUtility.cs#L333)

ここで見つけたのが Editing Environmentです。

### Prefab Editing Environment
Prefab Editing Environmentは、Prefabモードで開かれるシーンを特定できる機能です。
ProjectSettings -> Editor -> Prefab Editing Environmentsの参照にシーンを入れることで、プレハブを操作する際には該当シーンに飛ぶことができます。
CsReferenceのコード通りだと、カスタムシーンに色々セットアップを行ったCanvasを置くことで、ユーザーがカスタマイズしたCanvasの下に入れてくれるようです。**RootにCanvasがないプレハブに限って。**

なので、これだけでは不十分で、スクリプトを書く必要がありました。

### スクリプト

```csharp
using UnityEditor;
using UnityEditor.Experimental.SceneManagement;
using UnityEngine;

[ExecuteInEditMode]
public class NestPrefabRootCanvasInPrefabMode : MonoBehaviour
{
    [SerializeField]
    Transform RootCanvasTransformOnScene;

    private void Awake()
    {
        PrefabStage.prefabStageOpened += OnPrefabStageOpen;
    }

    private void OnDestroy()
    {
        PrefabStage.prefabStageOpened -= OnPrefabStageOpen;
    }

    private void OnPrefabStageOpen(PrefabStage stage)
    {
        var prefabContentsRoot = stage.prefabContentsRoot;
        var rootCanvasOnPrefab = prefabContentsRoot.GetComponent<Canvas>();
        if (rootCanvasOnPrefab != null)
        {
            var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(stage.prefabAssetPath);
            var prefabRectTransform = prefab.GetComponent<RectTransform>();
            var prefabContentsRootRectTransform = prefabContentsRoot.GetComponent<RectTransform>();

            //CanvasをNestする
            rootCanvasOnPrefab.transform.SetParent(RootCanvasTransformOnScene, false);

            //崩れたRectTransformプロパティを元に戻す
            prefabContentsRootRectTransform.anchorMin = prefabRectTransform.anchorMin;
            prefabContentsRootRectTransform.anchorMax = prefabRectTransform.anchorMax;
            prefabContentsRootRectTransform.anchoredPosition = prefabRectTransform.anchoredPosition;
            prefabContentsRootRectTransform.sizeDelta = prefabRectTransform.sizeDelta;
        }
    }
}
```

いつの間にかPrefabStageというクラスが追加され、Prefabモードに入る際の処理を担当しているようで、運が良くイベントが公開されていたのでこれを使うことにしました。親として使うCanvasをPrefab Editing Environmentシーンに生成し、スクリプトのSerializeFieldに渡せばOKです。
最初書いた通り、一回でもRootとして使われてしまったCanvasがついているGameObjectは、RectTransformのプロパティが崩れてしまったため、自動でユニティが置くプレハブのRectTransformプロパティを元のプレハブの状態に戻すコートも入れました。これで親にCanvasがついていても問題なく元の形を維持することができました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/165110/6a75a073-63bf-2999-caca-2d1b76612632.png)


## 終わりに
最近(特にUnity2017~2018辺り)色々ユニティの発展が加速し、嬉しいことばっかりで喜んでいましたが、その分勉強しないといけないものも同然増えるでしょうねーという感想ですね。
こういう問題に直面するとは全く思わなかったです。もっと勉強しないと。