Search Unity

[Bug] Problem with indirectly adding components

Discussion in 'Prefabs' started by ChrisIceBox, Aug 7, 2018.

  1. ChrisIceBox

    ChrisIceBox

    Joined:
    Sep 16, 2013
    Posts:
    334
    Apologies if this has been brought up before - but I couldn't see mention of it elsewhere.

    I've found that the current prefab experimental build has an issue when adding a component to a prefab through script. To recreate:
    1. Add a new empty GameObject to the scene, rename it EmptyPrefab, make it a new prefab and remove the instance from the scene
    2. Add the code snippet below as a new script (ComponentTest.cs)
    3. Attach the new Component Test component to any GameObject in the scene, and assign EmptyPrefab in its Inspector's "Game Object To Affect" field
    4. Choose "Add Rigidbody To GameObject" via its context menu. This will attempt to add a Rigidbody component to the prefab, but instead the following error appears:
    Assertion failed on expression: '!go.TestHideFlag(Object::kNotEditable)'
    UnityEngine.GameObject:AddComponent()
    ComponentTest:AddComponent() (at Assets/Test/ComponentTest.cs:16)

    Opening up the EmptyPrefab in Prefab Mode will show no such Rigidbody. However, when an instance of EmptyPrefab is created into the scene, a Rigidbody will be there locally. This will not be the case after restarting Unity.

    ComponentTest.cs:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class ComponentTest : MonoBehaviour
    6. {
    7.  
    8.    public GameObject gameObjectToAffect;
    9.  
    10.  
    11.    [ContextMenu("Add Rigidbody to GameObject")]
    12.    private void AddComponent ()
    13.    {
    14.        if (gameObjectToAffect.GetComponent <BoxCollider>() == null)
    15.        {
    16.            gameObjectToAffect.AddComponent <BoxCollider>();
    17.            Debug.Log ("Added box collider to " + gameObjectToAffect, gameObjectToAffect);
    18.        }
    19.    }
    20.  
    21. }
    22.  
    Said steps produces no errors with Unity 2018.2 or earlier.
     
  2. runevision

    runevision

    Joined:
    Nov 28, 2007
    Posts:
    1,892
    Hi!

    We need to get this info into the Upgrade Guide (edit: done), but essentially you can't make arbitrary modifications to Prefab Assets anymore (just like you can't do it in the editor) because it's now an imported asset type.

    Instead you need to use PrefabUtility.LoadPrefabContents which loads a Prefab Asset at a given path into an isolated scene and returns the root GameObject of the Prefab. Then you can modify those contents. Once you have modified the Prefab you have to write it back using PrefabUtility.SaveAsPrefabAsset and then call PrefabUtility.UnloadPrefabContents to release the Prefab and isolated scene from memory.
     
    Last edited: Aug 7, 2018
    Maeslezo and erenaydin like this.
  3. ChrisIceBox

    ChrisIceBox

    Joined:
    Sep 16, 2013
    Posts:
    334
    Ouch. OK, well I appreciate the steps pointing me in the right direction. So this would be effectively the same as opening up Prefab Mode manually? Do you think the right code would allow for this to all occur instantly, i.e. seamlessly as it is at the moment?
     
  4. runevision

    runevision

    Joined:
    Nov 28, 2007
    Posts:
    1,892
    Yes, it's equivalent to making edits in Prefab Mode.

    It's not clear how this could work. Would every API that can be used to modify a Prefab (adding/removing/modifying components, adding/removing/reparenting/modifying GameObjects etc.) have code to do special handling if it's a Prefab Asset? Would adding 10 components load and unload the Prefab 10 times (and cause chain reimports 10 times of all other Prefabs that depend on it)? The fact that you make it clear at what point you're done making edits is quite important, because the importer will only kick in at that point.

    We understand that it's a bit more cumbersome API to use compared to before, and we did think a lot about if there couldn't be a simpler way, but it didn't seem so. In the end the robustness and clearer intention this brings is needed to be able to support the new features such as nesting.
     
    Deleted User likes this.
  5. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    As long as we don't notice that the editor went into and out of prefab mode, that API sounds fine.

    Of course, this requires that you output understandable, actionable error messages if we try to edit prefabs from script in the old way. Otherwise people will have a lot of insidious bugs.
     
  6. ChrisIceBox

    ChrisIceBox

    Joined:
    Sep 16, 2013
    Posts:
    334
    I've been tinkering with this, and yes - the switch to and from Prefab Mode is seamless.

    It is the case, however, that all modification of prefabs has to be done this way. If I want to run a method on a component after adding it, and such a method makes modifications to it, that also has to occur while the instance is loaded in before releasing it. The reasons for the change are understandable, but I agree that a dedicated error message shown if modifications are attempted the "wrong way" would be extremely welcome.
     
  7. RedHillbilly

    RedHillbilly

    Joined:
    Mar 24, 2014
    Posts:
    39
    I am running into a similar issue when adding a component to a non-instantiated GameObject at runtime (essentially a similar code than shown above, but in Awake).

    The proposed solution works for this use-case as it is intended to work in the editor, but won't work at runtime. Do you have an idea on how to achieve this with runtime code?

    For reference, I also posted my question here: https://answers.unity.com/questions/1571081/error-on-adding-a-component-to-a-non-instantiated.html but thought you guys might already know a solution.

    Thanks
     
  8. ChrisIceBox

    ChrisIceBox

    Joined:
    Sep 16, 2013
    Posts:
    334
    My own needs only required to only work in the Editor, I'm afraid.

    If your object isn't instantiated, do you mean you're trying to mofify the components of a prefab asset? I don't think that's allowed.
     
  9. RedHillbilly

    RedHillbilly

    Joined:
    Mar 24, 2014
    Posts:
    39
    thanks for your reply.

    Oh, ok thanks, didn't know that. Funny enough that it still works then, just throws this error which does not seem to stop the execution.
    The assets are all generated with the right components attached.

    I guess I will have to think of a workaround then. Thanks again.
     
  10. MatthieuPr

    MatthieuPr

    Joined:
    May 4, 2017
    Posts:
    56
    I will repeat what Unity folks have been posting all over the forum:

    Prefabs do not exist at runtime. so at runtime you can't change prefabs as they do not exist in that environment.

    Why are you trying to edit prefabs at runtime? Can you elaborate a bit more on what you are trying to achieve and maybe can be of more help on that part ;)

    PS: will add this to your question as well ;)
     
  11. idbrii

    idbrii

    Joined:
    Aug 18, 2014
    Posts:
    51
    To clarify how to apply this workflow (this thread has good PageRank), here's a snippet that worked for me on 2018.3.13f1:

    Code (CSharp):
    1. // This code would work in an EditorWindow.
    2. PrefabAsset = EditorGUILayout.ObjectField("Prefab", PrefabAsset, typeof(GameObject), allowSceneObjects: false);
    3. var asset_path = AssetDatabase.GetAssetPath(PrefabAsset);
    4. var editable_prefab = PrefabUtility.LoadPrefabContents(asset_path);
    5.  
    6. // Do your changes here.
    7. var dest = editable_prefab.GetComponent<MeshRenderer>();
    8. Undo.RecordObject(dest, "Disable MeshRenderer");
    9. dest.enabled = false;
    10.  
    11. // NO SAVE - ArgumentException: Can't save a Prefab instance.
    12. //~ PrefabUtility.SavePrefabAsset(editable_prefab);
    13. // NO SAVE - Prefab still has MeshRenderer enabled.
    14. //~ PrefabUtility.SavePrefabAsset(PrefabAsset);
    15. // This save method works.
    16. PrefabUtility.SaveAsPrefabAsset(editable_prefab, asset_path);
    17. PrefabUtility.UnloadPrefabContents(editable_prefab);
    18.  
    I don't understand why SavePrefabAsset doesn't work.

    Additionally, I think I also think this process seems clumsy and error prone, so I wrote a scope for it:

    Code (CSharp):
    1. public class PrefabEditScope : IDisposable {
    2.     public GameObject EditablePrefab { get; private set; }
    3.     string m_AssetPath;
    4.     GameObject m_Ref;
    5.  
    6.     public PrefabEditScope(GameObject prefab_reference) {
    7.         m_AssetPath = AssetDatabase.GetAssetPath(prefab_reference);
    8.         m_Ref = prefab_reference;
    9.         EditablePrefab = PrefabUtility.LoadPrefabContents(m_AssetPath);
    10.     }
    11.  
    12.     public void Dispose() {
    13.         // You'd think you could call SavePrefabAsset, but you can't in 2018.3.
    14.         //~ PrefabUtility.SavePrefabAsset(m_Ref);
    15.         PrefabUtility.SaveAsPrefabAsset(EditablePrefab, m_AssetPath);
    16.         PrefabUtility.UnloadPrefabContents(EditablePrefab);
    17.     }
    18. }
    19.  
    That allows the above example to be simplified to:

    Code (CSharp):
    1. PrefabAsset = EditorGUILayout.ObjectField("Prefab", PrefabAsset, typeof(GameObject), allowSceneObjects: false);
    2. using (var editor = new PrefabEditScope(PrefabAsset))
    3. {
    4.     // Do your changes here.
    5.     var dest = editor.EditablePrefab.GetComponent<MeshRenderer>();
    6.     Undo.RecordObject(dest, "Disable MeshRenderer");
    7.     dest.enabled = false;
    8. }
    9.  
     
    Baste likes this.