Search Unity

Beta 12: Can someone elaborate on added "Support for reparenting prefab root in Awake and OnEnable"?

Discussion in 'Prefabs' started by bac9-flcl, Nov 28, 2018.

  1. bac9-flcl

    bac9-flcl

    Joined:
    Dec 5, 2012
    Posts:
    829
    As of Beta 11 I directly patch UnityEditor assembly with Harmony patcher to support some prefabs requiring custom enveloping objects (thanks to Shaun-Peoples for showing me how to do that!). I'm specifically targeting MovePrefabRootToEnvironmentScene in PrefabStageUtility: that's where standard UI gets special treatment, so I append my code there to give special parenting treatment to other systems like NGUI.

    Am I understanding right that in Beta 12 its possible to accomplish the same thing using OnAwake/OnEnable? If so, can somebody post an example?

    For context, here is what I do to prefabs in the aforementioned Beta 11 patch:

    Code (csharp):
    1. [HarmonyPatch (typeof (PrefabStageUtility))]
    2. [HarmonyPatch ("MovePrefabRootToEnvironmentScene")]
    3. public class PatchSceneRoot
    4. {
    5.     static void Postfix (Scene __result, GameObject prefabInstanceRoot)
    6.     {
    7.         var instanceRoot = prefabInstanceRoot;
    8.         var scene = __result;
    9.  
    10.         var rootsInPrefab = instanceRoot.GetComponentsInChildren<UIRoot> (true);
    11.         var panelsInPrefab = instanceRoot.GetComponentsInChildren<UIPanel> (true);
    12.  
    13.         bool missingRoot = rootsInPrefab.Length == 0;
    14.         bool missingPanel = panelsInPrefab.Length == 0;
    15.  
    16.         if (!missingRoot && !missingPanel)
    17.             return;
    18.  
    19.         GameObject container = EditorUtility.CreateGameObjectWithHideFlags ("UIRoot (Environment)", HideFlags.DontSave);
    20.         container.layer = LayerMask.NameToLayer ("UI");
    21.  
    22.         if (missingRoot)
    23.             container.AddComponent<UIRoot> ();
    24.  
    25.         if (missingPanel)
    26.             container.AddComponent<UIPanel> ();
    27.  
    28.         SceneManager.MoveGameObjectToScene (container, scene);
    29.         instanceRoot.transform.SetParent (container.transform, false);
    30.     }
    31. }
    I'd certainly be happy if I could move that stuff into normal events instead of a brittle patch targeting constantly changing private parts of the assembly.
     
    Last edited: Nov 28, 2018
  2. Mads-Nyholm

    Mads-Nyholm

    Unity Technologies

    Joined:
    Aug 19, 2013
    Posts:
    217
    Hi,

    Yes the loading of Prefabs into a PrefabStage changed in beta12. Check the following script example of how to handle custom reparenting when opening a Prefab and how to add other custom environment objects in e.g: Awake(). Note that you cannot have other root environment GameObjects with this type of script behavior as you have currently no way to determine which is the prefab root.

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEditor.Experimental.SceneManagement;
    3. using UnityEngine;
    4. using UnityEngine.SceneManagement;
    5.  
    6. [ExecuteAlways]
    7. public class ScriptWithExecuteAlways_CreatesEnvironmentObjects : MonoBehaviour
    8. {
    9.     private void Awake()
    10.     {
    11. #if UNITY_EDITOR
    12.  
    13.         // Check if this script's GameObject is in a PrefabStage
    14.         var prefabStage = PrefabStageUtility.GetPrefabStage(gameObject);
    15.  
    16.         if (prefabStage != null)
    17.         {
    18.             // Note that while the Prefab Stage is loading we get this Awake() called. The PrefabStage is not
    19.             // fully initialized so you cannot call prefabStage.prefabContentsRoot yet so check if parent is null
    20.             // to ensure you are handling the root
    21.             if (gameObject.transform.parent != null)
    22.                 return;
    23.  
    24.             // Add our own environment root and ensure it is in the PrefabStage scene
    25.             var newParent = new GameObject("NewParent (Environment)");
    26.             SceneManager.MoveGameObjectToScene(newParent, gameObject.scene);
    27.             transform.parent = newParent.transform;
    28.  
    29.             // Test various dynamic GameObject creation for other environment objects
    30.             var capsule = GameObject.CreatePrimitive(PrimitiveType.Capsule);
    31.             capsule.transform.position += new Vector3(-2, 0, 0);
    32.             SceneManager.MoveGameObjectToScene(capsule, prefabStage.scene);
    33.  
    34.             // Also instantiating other Prefabs as environments objects are supported
    35.             var instance = PrefabUtility.InstantiatePrefab(AssetDatabase.LoadAssetAtPath<GameObject>("SomePrefab.prefab")) as GameObject;
    36.             instance.transform.position += new Vector3(2, 0, 0);
    37.             SceneManager.MoveGameObjectToScene(instance, prefabStage.scene);
    38.         }
    39. #endif
    40.     }
    41. }
    42.  
    Please let us know if this approach work for you.
     
    Last edited: Nov 29, 2018
  3. bretternst

    bretternst

    Joined:
    Apr 8, 2017
    Posts:
    6
    I’m curious, is there any way to reparent to an object that already exists in the environment scene in Awake? It seems the scene isn’t ready for a GetRootGameObjects() call yet, so I haven’t found a way to get a reference to an existing object. This would be pretty useful in many UI scenarios because you could have “slots” set up for different UI components.

    If not, I think the prefab instantiation might be a suitable alternative for what I’m trying to do, assuming you can set an object inside it as the parent). Basically I’d like Canvas -> Container -> Prefab Root.

    Any thoughts welcome.
     
  4. bretternst

    bretternst

    Joined:
    Apr 8, 2017
    Posts:
    6
    Following up on my last comment, for anybody who might find it useful - here's my current approach to cover the more general UI cases. This seems to me much more flexible than setting a single scene in the project settings, and I'm SUPER happy this change made it in.

    It would be cool, for future versions, to maybe have this functionality as an attribute. For example, [UseEnvironmentPrefabForEditing("path/to/prefab", container="MyContainer")]. Or maybe a prefab metadata setting so it's not tied to a single component.

    Code (CSharp):
    1. #if UNITY_EDITOR
    2. using UnityEditor;
    3. using UnityEditor.Experimental.SceneManagement;
    4. using UnityEngine;
    5. using UnityEngine.SceneManagement;
    6.  
    7. public static class PrefabEditorHelper {
    8.     public static void UsePrefabAsset(GameObject obj, string prefabPath, string parentName = "PrefabContainer") {
    9.         var prefabStage = PrefabStageUtility.GetPrefabStage(obj);
    10.         if (prefabStage != null && obj.transform.parent == null) {
    11.             var instance = PrefabUtility.InstantiatePrefab(
    12.                 AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath)) as GameObject;
    13.             if (instance) {
    14.                 SceneManager.MoveGameObjectToScene(instance, obj.scene);
    15.                 var container = FindObjectRecursive(instance, parentName);
    16.                 if (container) {
    17.                     obj.transform.SetParent(container.transform, false);
    18.                 }
    19.                 else {
    20.                     Debug.LogWarning("No object named " + parentName + " found in environment prefab " + prefabPath);
    21.                 }
    22.             }
    23.             else {
    24.                 Debug.LogWarning("Could not find environment prefab: " + prefabPath);
    25.             }
    26.         }
    27.     }
    28.    
    29.     static GameObject FindObjectRecursive(GameObject obj, string name) {
    30.         if (obj.name == name)
    31.             return obj;
    32.        
    33.         for (var i = 0; i < obj.transform.childCount; i++) {
    34.             var go = FindObjectRecursive(obj.transform.GetChild(i).gameObject, name);
    35.             if (go != null)
    36.                 return go;
    37.         }
    38.  
    39.         return null;
    40.     }
    41. }
    42. #endif
    Code (CSharp):
    1. #if UNITY_EDITOR
    2.     void Awake() {
    3.         PrefabEditorHelper.UsePrefabAsset(gameObject, "Assets/Prefabs/Editor/SlideEditor.prefab");
    4.     }
    5. #endif
     
    Mads-Nyholm likes this.
  5. Mads-Nyholm

    Mads-Nyholm

    Unity Technologies

    Joined:
    Aug 19, 2013
    Posts:
    217
    Good to hear

    We want to add a Editor-only tag you can use for tagging which GameObject should be the Prefab root parent when opening Prefab Mode (suggested by Shaun-Peoples here: https://forum.unity.com/threads/creating-prefab-editing-environments.574795/)
     
    Last edited: Dec 4, 2018