10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

QualiArtsAdvent Calendar 2021

Day 14

[Unity]Prefabの同値での上書きを一括リバートするツール作った話

Last updated at Posted at 2021-12-14

#前置き
みなさんPrefab VariantsとNested Prefab使いこなしているでしょうか?
使いこなせばこなすほど、プロジェクトの物量が多くなればなるほど作業効率が上がります。共通する部品やそれから少し差分があるだけのPrefabをコピーせずに既に作ったPrefabを置くだけで良いし、後から共通部分の仕様変更があった時にNestedした親Prefab、継承元のPrefabを編集するだけで全てに反映される。自分も時間短縮をとても実感しております。

ただ、不安点もあります。継承、Nested配置されたPrefabを編集しているときに、Componentの値をInspector上で変えたりすると思うんですがその時親Prefabから上書されたという情報が編集中のPrefabに追加され青いラベルが該当箇所に付きます。この情報は元の値に戻したところで上書きされたという情報は消えることはなく、青いラベルの位置で右クリックしてリバートもしくはPrefabのRootのOverridesの一覧からリバートをしなければ消えない。
スクリーンショット 2021-12-16 18.28.06.png
スクリーンショット 2021-12-16 18.29.45.png

これに気づかず各所で無駄に同じ値で上書き情報を作っていると、親Prefabを変更した際にその上書き情報が入った部分には意図せず反映されないことになる。

見た目の確認や検証などのために一時的に変更したり(特にGameObjectのactiveのon/off)
一度変更したが結局元の状態でいくことになったり
その際に毎回プロジェクトに関わる人間が全員気をつけてリバートできていればいいが
確認忘れていたり、そのあたりの仕様を知らない人もいるので完全になくなることはない。
そんなリバートを少し楽にしたいと思ってUnity拡張ツールを作りました。

#ツール仕様

今回Unity拡張としてScene上のGameObjectに対しての右クリックにコマンドを追加し、選択オブジェクトが親Predfabだった時にその上書き情報を精査し親Prefabと同じ値で上書きしている情報を検知してそれらを一括でリバートするというものを作りました。
違う値で上書きされているものには触りません。
tool.png

#実装
##拡張ツール雛形
Scene上のGameObjectに対しての右クリックにコマンドを追加するのでまずテンプレ

    public static class RevertSameValueOverrideProperty
    {
        [MenuItem("GameObject/RevertSameValueOverrideProperty", true, 0)]
        public static bool Validate(MenuCommand command)
        {
            var selectObjects = Selection.gameObjects;
            return true;
        }
    
        [MenuItem("GameObject/RevertSameValueOverrideProperty", false, 0)]
        public static void Open(MenuCommand command)
        {
            var selectObjects = Selection.gameObjects;
        }
     }

上がメニューとして有効な時だけtrueを返すバリデート用関数
下がメニューを選んだ時に実行する内容の関数
MenuItemのAttributeの第2引数でtrueでバリデート用、falseで内容関数になります。

##バリデート関数

まず有効な状況の確認ですがPrefabUtilityがとても便利です。今回何度も使います。
上書き値が保存される対象は配置されたPrefabのRootのGameObjectです。ただしNestedされていて紐付いて配置されるPrefabに対する上書き値は全部、その紐付いて配置されたPrefabでなくそのPrefabで配置したPrefabに保持されている。
その判定はPrefabUtility.IsOutermostPrefabInstanceRootでできるので選択Objectにそれがあるかを判定します。

[MenuItem("GameObject/RevertSameValueOverrideProperty", true, 0)]
public static bool Validate(MenuCommand command)
{
    var selectObjects = Selection.gameObjects;
    foreach (var obj in selectObjects)
    {
        if (PrefabUtility.IsOutermostPrefabInstanceRoot(obj) == false)
        {
            return false;
        }
    }
    
    return true;
}

##内容関数

###上書き値情報の取得とバリデート

[MenuItem("GameObject/RevertSameValueOverrideProperty", false, 0)]
public static void Open(MenuCommand command)
{
  var selectObjects = Selection.gameObjects;
  foreach (var obj in selectObjects)
  {
     if(PrefabUtility.IsOutermostPrefabInstanceRoot(obj) == false){
         continue;
     }
     
     var mods = PrefabUtility.GetPropertyModifications(obj);
     foreach (var mod in mods)
     {
         if (mod.target == null) continue;
         if (PrefabUtility.IsDefaultOverride(mod)) continue;
         //同値上書き情報ならリバート(*後述)
     }
   }
}

まだ途中までのコードですがPrefabの上書き値を取得して必要のないものをバリデートしています。
PrefabのGameObjectに対してPrefabUtility.GetPropertyModificationsを使うと上書き値(PropertyModification)をリストとして取得できます。
PropertyModificationに対するPrefabUtility.IsDefaultOverrideはTransformにおけるlocalPositionなどの親の値を反映しない類のものかどうかを返してくれます。その類のものは関係がないので除外します。

Instance内対象のComponentの取得

[MenuItem("GameObject/RevertSameValueOverrideProperty", false, 0)]
public static void Open(MenuCommand command)
{
    var selectObjects = Selection.gameObjects;
    foreach (var obj in selectObjects)
    {
        if(PrefabUtility.IsOutermostPrefabInstanceRoot(obj) == false){
            continue;
        }
     
        var mods = PrefabUtility.GetPropertyModifications(obj);
        var components = new List<Component>();
        components.AddRange(obj.gameObject.GetComponentsInChildren<Component>(true));

        if (components.Count == 0)
        {
            continue;
        }

        foreach (var mod in mods)
        {
            if (mod.target == null) continue;
            if (PrefabUtility.IsDefaultOverride(mod)) continue;

            Object targetObject = null;
            if (mod.target is GameObject)
            {
              targetObject = components.Find(c => PrefabUtility.GetCorrespondingObjectFromSource(c.gameObject)?.GetInstanceID() == mod.target.GetInstanceID())?.gameObject;
            }
            else
            {
                var candidateComponents = components.FindAll(c => c.GetType() == mod.target.GetType());
                foreach (var candidateComponent in candidateComponents)
                {
                    var sourceObject = PrefabUtility.GetCorrespondingObjectFromSource(candidateComponent);
                    if (sourceObject == null) continue;
                        if (sourceObject.GetInstanceID() == mod.target.GetInstanceID())
                        {
                            targetObject = candidateComponent;
                            break;
                        }
                    }
                }
                //同値上書き情報ならリバート(*後述)
            }
        }
    }
}

リバート対象のComponentを確定するためにPropertyModificationに対応するInstance内のComponentを取得しないといけません。
componentsの変数にInstance側のPrefabの中のComponentを取得して入れて、それらに対し
PrefabUtility.GetCorrespondingObjectFromSourceを使用すると親Prefabの対象のObjectが取得できるのでPropertyModificationのtargetは親側ののObjectを指すのでそれと一致しているかを確認してすることでPropertyModificationに対応するInstance側のComponentを確定できます。

###同値か確認してリバート
**同値上書き情報ならリバート(*後述)**の部分に入るコードです

if (targetObject != null)
{
    var serializedOriginObj = new SerializedObject(mod.target);
    var originProperty = serializedOriginObj.FindProperty(mod.propertyPath);
    //nullの場合はArrayで参照元にそのIndexが存在しない時
    if (originProperty == null) continue;

    if (originProperty.propertyType == SerializedPropertyType.ExposedReference
        || originProperty.propertyType == SerializedPropertyType.ObjectReference)
        {
        var originValue = GetValue(originProperty);
        if (mod.objectReference == (Object) originValue)
        {
            RevertProperty(targetObject, mod);
        }
    }
    else
    {
        var originValue = GetValue(originProperty)?.ToString();
        if (mod.value == originValue)
        {
            RevertProperty(targetObject, mod);
        }
    }
}

親オブジェクトをSerializedObjectにして対象のPropertyをPropertyModification.propertyPathを元にFindPropertyで持ってきてそのシリアライズ値をPropertyModification.targetと比較しています。同じならリバートするといった作業

###その他関数
上記同値か確認してリバートの項目のRevertPropertyとGetValueは独自関数なので貼っておきます

####RevertProperty関数
ただリバートしてDirtyにしてるだけ

private static void RevertProperty(Object targetComponent, PropertyModification mod)
{
    var sObj = new SerializedObject(targetComponent);
    var targetProperty = sObj.FindProperty(mod.propertyPath);
    PrefabUtility.RevertPropertyOverride(targetProperty, InteractionMode.AutomatedAction);
    EditorUtility.SetDirty(targetComponent);
}

####GetValue関数
SerializedPropertyの値がタイプごとにToStringなどで対応できないので地道に対応したPropertyModificationと比較できるobjectを返しています。ここはもっといい方法があれば知りたいぐらい。

ちなみにポジション、スケールなどのVector3などはPropertyModificationとしてはxyz個別にfloatとして取得でき、serializedOriginObj.FindProperty(XXX.y)などで正しくSerializedPropertyも取得できるためこの関数で使用されるタイプはFloatである。

private static object GetValue(SerializedProperty p)
{
    switch (p.propertyType)
    {
        case SerializedPropertyType.Generic:
            Debug.LogWarning((object) "Get/Set of Generic SerializedProperty not supported");
            return null;
        case SerializedPropertyType.Integer:
            return p.intValue;
        case SerializedPropertyType.Boolean:
            return p.boolValue ? "1" : "0";
        case SerializedPropertyType.Float:
            return p.floatValue;
        case SerializedPropertyType.String:
            return p.stringValue;
        case SerializedPropertyType.Color:
            return (object) p.colorValue;
        case SerializedPropertyType.ObjectReference:
            return (object) p.objectReferenceValue;
        case SerializedPropertyType.ExposedReference:
            return (object) p.exposedReferenceValue;
        case SerializedPropertyType.LayerMask:
            return (object) p.intValue;
        case SerializedPropertyType.Enum:
            return (object) p.enumValueIndex;
        case SerializedPropertyType.Vector2:
            return (object) p.vector2Value;
        case SerializedPropertyType.Vector3:
            return (object) p.vector3Value;
        case SerializedPropertyType.Vector4:
            return (object) p.vector4Value;
        case SerializedPropertyType.Rect:
            return (object) p.rectValue;
        case SerializedPropertyType.ArraySize:
            return (object) p.intValue;
        case SerializedPropertyType.Character:
            return (object) p.stringValue;
        case SerializedPropertyType.AnimationCurve:
            return (object) p.animationCurveValue;
        case SerializedPropertyType.Bounds:
            return (object) p.boundsValue;
        case SerializedPropertyType.Gradient:
            Debug.LogWarning((object) "Get/Set of Gradient SerializedProperty not supported");
            return (object) 0;
        case SerializedPropertyType.Quaternion:
            return (object) p.quaternionValue;
        default:
            return (object) 0;
    }
}

#最後に
Scene全体のPrefabに適用するようにしたり、Prefab保存する時に自動でチェックをかけれるようする(InitializeOnLoadのAttributeでPrefabStage.prefabSavingで起動するとかでprefab内全体に適用)などすればこれを手動で使用せずとも絶対起こり得ない状態にできたりもする。意図的に同じ値で上書きするパターンがなければの話だが

新しい機能などを使ったわけでもなく地道なものですが、PrefabUtilityやSerializedObject、Reflection、UnityEditor.SceneManagement、UnityEngine.SceneManagementあたりのクラスを知っていればアイデア次第で他にもPrefab周りやComponent周りの便利ツールは作れるのでぜひこれ以外にも便利なツールが溢れてくれると嬉しい。

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?