Search Unity

How do I edit prefabs from scripts?

Discussion in 'Prefabs' started by Baste, May 28, 2019.

  1. IARI

    IARI

    Joined:
    May 8, 2014
    Posts:
    70
    I'm not sure about a Prefab Type being that much of an improvement - but then again, so far I really don't understand the prefabUtility api at all and don't feel like i hav.

    Is there any reference to use cases that edit existing prefabs?

    For me It would help a lot for now, if there was just at least some clear documentation, and maybe a tutorial and some more example use-cases that do more than just the create-prefabs case (which only involves simple creation/writing)

    * Something that involves editing existing prefabs
    * editing existing prefabs with an unknown path, given a reference to a gameobject within the opened scene (like the Selection)
    * in the latter case a decision needs to be made, at which level editing occurs in case of nested prefabs or variants

    For example:
    Given a Gameobject reference (from selection, or target from within a custom inspector):
    How do I add/remove a component and store the changes made to the nearest prefab instance?

    Here is some example code that i have tried which does not work as I hoped:

    Code (CSharp):
    1.  
    2.     public static void EditPrefab(this GameObject go, Action<GameObject> action)
    3.     {
    4.         var prefabRoot = PrefabUtility.GetCorrespondingObjectFromSource(go);
    5.         var assetPath = AssetDatabase.GetAssetPath(prefabRoot);
    6.         Debug.Log($"assetRoot: {prefabRoot}, assetPath: {assetPath}");
    7.         var loadedPrefabRoot = PrefabUtility.LoadPrefabContents(assetPath);
    8.         action.Invoke(loadedPrefabRoot);
    9.         PrefabUtility.SaveAsPrefabAsset(loadedPrefabRoot, assetPath);
    10.         PrefabUtility.UnloadPrefabContents(loadedPrefabRoot);
    11.     }
    12.  
    I just hope that im missing something, and the whole thing is a lot easier
     
    Last edited: Oct 13, 2020
    MarekLg likes this.
  2. acmoles

    acmoles

    Joined:
    Jun 28, 2016
    Posts:
    4
    This worked perfectly - many thanks for sharing
     
  3. calpolican

    calpolican

    Joined:
    Feb 2, 2015
    Posts:
    425
    Can anyone explain how this syntax in Baste's code works? I've never encounter that use of the keyword "using" before, and I'm interesting in understanding it.

    Code (CSharp):
    1.     using (var editScope = new EditPrefabAssetScope(assetPath))
    2.     {
    3.         editScope.prefabRoot.AddComponent<BoxCollider>();
    4.     }
     
  4. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    Google.com: “c# using keyword” = 183,000 results.

    its a core part of C#. It creates a temp object and immediately autodestroys it at end of braces. But really ... for C# questions, google is your friend.

    (a common trick is to use the constructor and destructor from ‘using’ as a place to copy/paste tour setup/teardown code, knowing that the compiler will ensure they are called, in the right order, and no future person editing your code can accidentally delete one, or forget to inckude one when coy pasting this code for use elsewhere)
     
    Walter_Hulsebos likes this.
  5. calpolican

    calpolican

    Joined:
    Feb 2, 2015
    Posts:
    425
    Ok, thanks for the tip
     
  6. michealcaj

    michealcaj

    Joined:
    Aug 18, 2017
    Posts:
    191
    The prefab utility don't seem to be working in build mode
     
  7. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    By definition it cannot. There is no such thing as a "prefab" outside the editor. Whatever you're trying to do won't work :).
     
  8. paulatwarp

    paulatwarp

    Joined:
    May 17, 2018
    Posts:
    135
    I've read through this thread and I'm more confused than when I started. I need to edit nested prefabs in a batch process. Basically I need to copy text alignment settings from an earlier version that got corrupted due to a bug when changing assets from binary mode to text mode. There are thousands of them.

    I've tried using many of the methods listed above but none of them seem to be working for me.
     
  9. paulatwarp

    paulatwarp

    Joined:
    May 17, 2018
    Posts:
    135
    I finally got it working. There were two issues. The first was that the original prefabs must have been created before TextMeshPro made the change to not serialise its SubMeshUI objects. Deleting these and forcing it to re-create them before attempting to serialise fixed one issue.

    The second problem was with a prefab that was a RectTransform and had a Canvas object attached. Attempting to re-serialise this using LoadPrefabContents wiped the scale and anchor information from the object.

    Here is a 2019.4.26f1 project showing the second problem: https://github.com/paulatwarp/SerialiseRectTransform

    I have also included a variation which appears to work by creating a preview canvas using LoadPrefabContents and then manually instantiating the prefab object onto that.

    I'm not sure if this is a bug or undefined behaviour.
     
  10. jovg

    jovg

    Joined:
    Oct 14, 2016
    Posts:
    2
    This is not working me, I need to edit some values for a component that exists within the prefab root children. When saved the values aren't reflected on the prefab.
     
  11. SI-Shawn

    SI-Shawn

    Joined:
    Jun 30, 2021
    Posts:
    16
    Thanks for sharing this. This helped me understand the difference between working with prefab stage and asset database contexts.
     
  12. crybirb

    crybirb

    Joined:
    Jun 13, 2022
    Posts:
    5
    Can someone give a GIST of this? I read the entire post and none of the snippets seen to be working, I'm trying something as:

    Code (CSharp):
    1. var prefab = PrefabUtility.LoadPrefabContents(prefabPath);
    2. var childComponents = prefabLoaded.GetComponentsInChildren<ChildComponent>();
    3.  
    4. foreach (var comp in components)
    5. {
    6.     comp.ListInsideChildComponent.Clear();
    7. }
    8.  
    9. PrefabUtility.SavePrefabAsAsset(prefab, prefabPath);
    10. PrefabUtility.UnloadPrefabContents(prefab)
    After that, opening the Prefab, instantiating or whatever doesn't seem to have the changes applied. The vectors are still populated.
     
    Last edited: Jul 19, 2022
  13. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    Like ... that's the whole point of this thread. Use the code provided - that has "using" in the first line.

    If you don't: it will never work.

    If you do: it will always work.
     
    Walter_Hulsebos likes this.
  14. crybirb

    crybirb

    Joined:
    Jun 13, 2022
    Posts:
    5
    I used the code provided that has "using" in the first line. I tried also the Macro that was added into the PrefabUtility API. I tried everything in this thread, and the changes doesn't apply to the prefab.

    It didn't work. That's why I'm here.
     
    Last edited: Jul 19, 2022
  15. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    So the API https://docs.unity3d.com/2020.1/Doc...ce/PrefabUtility.EditPrefabContentsScope.html is broken for you?

    If so ... I think you should create a minimal test case and submit a bug report.

    FYI: I've been using this for a couple of years now and its worked in a wide variety of complex cases on multiple projects, on everything from 2018 (using Baste's workaround) to 2021 (using the official replacement). It's pretty good! But it could still have new bugs :(
     
  16. crybirb

    crybirb

    Joined:
    Jun 13, 2022
    Posts:
    5
    Yes it is not working for me, so the case I have that isn't working can be described as:

    I have a Prefab which contains other Prefabs inside, what I want to do is update a Custom Component inside the hierarchy, for that the code below is the attempt of achieving that

    Code (CSharp):
    1. using (var editingScope = new PrefabUtility.EditPrefabContentsScope(prefabPath))
    2.                 {
    3.                     var prefabLoadedContents = editingScope.prefabContentsRoot;
    4.                     var components = prefabLoadedContents.GetComponentsInChildren<CustomComponent>();
    5.  
    6.                     foreach (var comp in components)
    7.                     {
    8.                         Debug.Log($"{comp.name} {comp.ArrayInComponent.Count}");
    9.                         comp.ArrayInComponent.Clear();
    10.                         Debug.Log($"{comp.name} {comp.ArrayInComponent.Count}");
    11.                     }
    12.                 }
    The array Count is printed correctly, it shows going from X amount to 0. However when I open the prefab the change didn't apply to it. I want to know how can I consolidate said changes. I tried numerous different methods, my last resort being manually getting the serialized property in the field and changing it but that would require a very long and troublesome. I would love if someone can help me identify what I'm doing wrong or missing with this.

    Unity Ver: 2020.3.25f1
     
    Last edited: Jul 19, 2022
  17. crybirb

    crybirb

    Joined:
    Jun 13, 2022
    Posts:
    5
    It seems in this way I can do almost everything besides change the value of a Serialized Field. I can change the name of the object, create a new one, enable or disable it, remove a component, add a new one. Everything but changing the value. Tried also going through serialized objects and serialized properties to edit, no avail.

    EDIT: Discovered the problem, nothing to do with the Prefab API, it was a custom Inspector / Type override that was blocking the values from changing.
     
    Last edited: Jul 19, 2022
    a436t4ataf likes this.
  18. a_ermolinsky

    a_ermolinsky

    Joined:
    Sep 29, 2022
    Posts:
    1
    Thanks for a thread. I was trying to delete component from prefab and this thread clearify what functions needed to be called.
     
  19. denevraut

    denevraut

    Joined:
    Jan 23, 2017
    Posts:
    7
  20. baktubak

    baktubak

    Joined:
    Sep 17, 2018
    Posts:
    1
    Just wanted to share a success, here's a script that allows me to add a component to a particular child of a prefab and save it.

    The critical things seem to be as follows:
    • Create an instance of the prefab you want to change
    • Apply the change you want to make to the prefab instance
    • Save the change using a function from the PrefabUtility class, in my case, "ApplyAddedComponent" was most appropriate.
    My use case was that I needed to add a component to the children with a Renderer to about 65 prefabs, so this just lets me select the prefabs I want and click a button to do it.

    Code (CSharp):
    1.  private void AddStealthPropertiesToPrefabs()
    2.     {
    3.         renderers = new List<Renderer>();
    4.         GameObject[] selectedObjects = Selection.gameObjects;
    5.  
    6.         foreach (GameObject prefab in selectedObjects)
    7.         {
    8.             GameObject prefabInstance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
    9.             if (prefabInstance == null)
    10.             {
    11.                 Debug.LogError("Failed to instantiate prefab: " + prefab.name);
    12.                 continue;
    13.             }
    14.  
    15.             prefabInstance.GetComponentsInChildren(renderers);
    16.             foreach (Renderer renderer in renderers)
    17.             {
    18.                 if (renderer != null)
    19.                 {
    20.                     StealthMaterialProperties stealthProps = renderer.GetComponent<StealthMaterialProperties>();
    21.                     Debug.Log(stealthProps + "<-- stealthProps ; prefab " + prefab.name);
    22.                     if (stealthProps == null)
    23.                     {
    24.                         stealthProps = renderer.gameObject.AddComponent<StealthMaterialProperties>();
    25.                         Debug.Log("StealthMaterialProperties added to " + prefab.name);
    26.  
    27.                         // Apply the added component change to the prefab asset
    28.                         string prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(prefabInstance);
    29.                         PrefabUtility.ApplyAddedComponent(stealthProps, prefabPath, InteractionMode.UserAction);
    30.  
    31.                         // Destroy the instantiated prefab instance
    32.                         DestroyImmediate(prefabInstance);
    33.                     }
    34.                     else
    35.                     {
    36.                         Debug.Log("StealthMaterialProperties already exist on " + prefab.name);
    37.                         // Destroy the instantiated prefab instance
    38.                         DestroyImmediate(prefabInstance);
    39.                     }
    40.                 }
    41.             }
    42.         }
    43.     }
     

    Attached Files:

  21. VRARDAJ

    VRARDAJ

    Joined:
    Jul 25, 2017
    Posts:
    35
    I wanted to update the prefab file of a nested prefab. I saw PrefabUtility.EditPrefabContentsScope() was invented in this thread and it looked like exactly what I need. I used PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot() to access the path of a component whose field I needed to change via reflection and then used the EditPrefabContentsScope make the change. Seemed to work great . That component has the correct newly assigned field when I check the prefab in its stage, but when I step out to the top level scene stage, the new value of that field is still empty and out of sync. Never seen that happen before. Whatever the egnine is doing to keep stages in sync, I think I broke that. Not sure how to avoid that breakage. Anyone here know what I did wrong?

    Code (CSharp):
    1. var componentFields = myComponent.GetType().GetFields(_flags);
    2. var targetField = componentFields.FirstOrDefault(f => f.FieldType == classOfTheFieldINeedToChange.GetType());
    3. var path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(myComponent);
    4. using (var editingScope = new PrefabUtility.EditPrefabContentsScope(path))
    5. {
    6.     var myComponents = editingScope.prefabContentsRoot.GetComponentsInChildren(myComponent.GetType());
    7.     foreach (var component in myComponents)
    8.         targetField.SetValue(component, newValue);
    9. }
    10. AssetDatabase.SaveAssets();
    EDIT: Restarting Unity puts them back in sync, but I'd prefer them not to go out of sync in the first place.
     
    Last edited: Mar 17, 2024
  22. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,336
    Hmmm, maybe try importing the asset? So modify to:

    Code (csharp):
    1. AssetDatabase.SaveAssets();
    2. AssetDatabase.ImportAsset(path);
    The asset being wrong in the scene sounds like Unity hasn't realized it has to reimport the asset with the changes yet, or something. I assume it's a bug, but import asset should be able to work around it.
     
  23. VRARDAJ

    VRARDAJ

    Joined:
    Jul 25, 2017
    Posts:
    35
    I appreciate the help. I just tried this suggestion, but no luck yet.

    I need to run this operation on multiple fields at once, and oddly enough, when I run it on two fields in the same prefab, only the latter of the two goes out of sync until reboot.

    The fields in question are ScriptableObjects that I had just instanced in the block immediately prior to this, if that gives any clues.
     
  24. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,336
    Just noticed that you don't seem to use SetDirty on the prefab anywhere? That could cause it to not scan for changes when you save things.
     
  25. VRARDAJ

    VRARDAJ

    Joined:
    Jul 25, 2017
    Posts:
    35
    I do that for all my SOs, but I guess I assumed that part is built into the SaveAsPrefabAsset command, which is included in the EditingScope, yes? I'll try it anyway.

    Follow-up: I think I figured my issue. I had a couple bugs. One was the instances that were being assigned as newValue were scriptable objects generated in some cases from c# files I'd autogenerated immediately prior, which included multiple SO classes instanced out of the same c# file, which I've since realized is a no-no. Fixed that. I was still able to reproduce the same issue after that, but the 2nd bug was me losing the accurate ref to the newValue instance just before this block for reasons that have nothign to do with prefab edits. Fixed that too, and now all is well.

    So I guess the conclusion is that you can put prefab stages out of sync w/ reflection if you assign an SO instance that doesn't have the same name as the SO class's file name. I think that's what caused the initial problem.

    Side note: I tried SetDirty() to no avail, but after resolving my bugs, I removed all EditorUtility and AssetDatabase calls after the EditingScope block, including no more SetDirty(), SaveAssets(), ImportAsset(), nor ForceReserializeAssets(). Running just EditingScope by itself successfully saves and syncs the stages once my SO refs are clean.

    Thanks for your help, @Baste !