One of Unity's most powerful features is the Prefab system, which allows developers to build reusable game objects and component structures. As projects grow, batch operations like "adding a component to hundreds of Prefabs" or "updating a parameter across all UI panels" become essential. To automate this, developers often write custom C# Editor scripts.

However, you might encounter a frustrating bug: you fetch the Prefab asset via AssetDatabase.LoadAssetAtPath, modify the component's properties, call EditorUtility.SetDirty, and save using AssetDatabase.SaveAssets, yet: (1) the changes revert when you reopen the Prefab, (2) the asset updates but scene instances do not inherit the changes, or (3) nested prefabs and variants get corrupted.

This article explains the root cause of these issues in Unity's modern Prefab system (introduced in Unity 2018.3) and guides you through correct C# workflows using PrefabUtility to safely modify and save Prefabs.

1. The Root Cause: Modern Prefab System & Asset Virtualization

In Unity 2018.3, the Prefab system was rewritten to support Nested Prefabs and Prefab Variants. Under this architecture, a Prefab asset (.prefab file) is no longer a simple flat serialized object in memory; it behaves like a closed, virtual scene.

In legacy Unity versions (2018.2 and earlier), you could load a Prefab asset reference directly, modify its component properties, and mark it dirty. But in the current system, directly modifying the raw asset reference in memory bypassing its virtualization scene is either blocked or will corrupt the serialization link.

Conceptual diagram showing EditPrefabContentsScope loading a Prefab into a temporary scene compared to raw asset modification failures.

Figure: Raw asset modifications vs. EditPrefabContentsScope (correct propagation)

Direct modification fails for two main reasons:

  • Missing Virtual Scene: Prefab assets must be loaded into a temporary editing scene to generate valid serialized data. Direct editing causes Unity to treat changes as transient memory states, which are discarded upon entering Play Mode or triggering script compilation.
  • Desynced Instance Overrides: Scene-placed Prefab instances link to their source Prefab assets via unique FileIDs, tracking local changes as "Overrides." Directly modifying the raw asset breaks this tracking, preventing Unity from calculating and propagating updates to placed instances.

2. Solution 1: Safe Prefab Asset Modification with EditPrefabContentsScope (Recommended)

To safely modify the default values of a Prefab asset, use PrefabUtility.EditPrefabContentsScope inside a C# using block.

This API tells the Unity Editor to load the Prefab into a hidden temporary scene, exposing it as an editable GameObject tree. When the using block exits (triggers Dispose), Unity automatically serializes the changes back to the .prefab file and reconstructs relationships with placed instances.

Example: C# Editor Script to Batch Edit Prefab Properties

using UnityEditor;
using UnityEngine;

public class BatchPrefabModifier
{
    [MenuItem("Tools/Batch Modify Prefab Properties")]
    public static void ModifyPrefabs()
    {        string prefabPath = "Assets/Prefabs/MyEnemy.prefab";

        // 1. Load the Prefab asset into the temporary editing scene
        using (var editingScope = new PrefabUtility.EditPrefabContentsScope(prefabPath))
        {            // Access the root GameObject in the virtual scene
            GameObject rootPrefabObj = editingScope.prefabContentsRoot;

            // Find and modify target components
            EnemyController enemy = rootPrefabObj.GetComponent<EnemyController>();
            if (enemy != null)
            {                // Update properties
                enemy.maxHp = 150;
                enemy.attackPower = 25;

                // 2. Mark the component as dirty in the virtual scene
                EditorUtility.SetDirty(enemy);
                Debug.Log($"Updated EnemyController on Prefab: {prefabPath}");
            }
            else
            {                Debug.LogWarning($"EnemyController not found on Prefab: {prefabPath}");
            }
        } // 3. Disposing the scope auto-saves modifications and updates instances.

        // Force save assets database to sync changes
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
    }
}

This scope safely triggers scene dirty states, preserving Nested Prefab structures and variant hierarchies, while ensuring scene instances receive updates immediately.

3. Solution 2: Record and Apply Modifications on Scene Instances

If you want to modify a Prefab instance already placed in a scene and save those changes back to its source asset, follow this two-step process.

Step 1: Record Modifications on the Scene Instance

Modifying a scene instance directly via code doesn't register as a persistent serialized override. You must call PrefabUtility.RecordPrefabInstancePropertyModifications so Unity tracks the change.

// Find the instance in the active scene
GameObject instanceObj = GameObject.Find("MyEnemyInstance");
EnemyController enemy = instanceObj.GetComponent<EnemyController>();

if (enemy != null)
{
    enemy.maxHp = 200; // Modify value
    // Register the override in serialization
    PrefabUtility.RecordPrefabInstancePropertyModifications(enemy);
}

Step 2: Apply Instance Overrides to the Source Prefab Asset

To propagate the overrides back to the source Prefab file, use PrefabUtility.ApplyPrefabInstance. This simulates clicking "Overrides -> Apply All" in the Inspector.

GameObject instanceObj = GameObject.Find("MyEnemyInstance");

if (PrefabUtility.IsPartOfPrefabInstance(instanceObj))
{
    // Apply modifications to the source asset
    PrefabUtility.ApplyPrefabInstance(instanceObj, InteractionMode.AutomatedAction);
    Debug.Log("Applied scene instance overrides to source Prefab asset.");
}

4. Solution 3: Creating or Overwriting Prefabs from GameObjects

If you have built a new GameObject in the scene and want to save it as a new Prefab asset, or overwrite an existing Prefab completely, use PrefabUtility.SaveAsPrefabAsset.

GameObject tempObj = new GameObject("NewCoolItem");
tempObj.AddComponent<BoxCollider>();

string savePath = "Assets/Prefabs/NewCoolItem.prefab";

// Save as a brand new Prefab asset
GameObject prefabAsset = PrefabUtility.SaveAsPrefabAsset(tempObj, savePath);

// Or save and connect the scene object as an instance:
// GameObject connectedAsset = PrefabUtility.SaveAsPrefabAssetAndConnect(tempObj, savePath, InteractionMode.AutomatedAction);

// Destroy the temporary scene object
GameObject.DestroyImmediate(tempObj);

※ Overwriting an existing Prefab path via SaveAsPrefabAsset retains the asset's GUID. Other scenes referencing this Prefab won't break, though the object tree structure will be replaced entirely.

5. Prefab API Cheat Sheet

Use Case Recommended API / Method Notes
Batch edit Prefab assets directly using (var scope = new PrefabUtility.EditPrefabContentsScope(path)) Loads a virtual scene behind the scenes. Can be slow if run in a large loop.
Edit values on a scene instance Update properties, then call PrefabUtility.RecordPrefabInstancePropertyModifications(target) Mandatory to call, otherwise values revert during compile or scene load.
Sync scene instance changes to the source asset PrefabUtility.ApplyPrefabInstance(instance, InteractionMode.AutomatedAction) Be mindful of nested prefab hierarchies to avoid unwanted parent modifications.
Save a new scene object as a Prefab asset PrefabUtility.SaveAsPrefabAsset(gameObject, path) Maintains GUID when overwriting, but replaces the internal structure completely.

6. Performance Optimization for Large-Scale Batch Editing

If you need to batch-process hundreds or thousands of Prefab assets, calling EditPrefabContentsScope in a simple loop causes Unity to trigger asset re-imports and serialization updates for each Prefab. This results in extreme lag, editor freezes, or Out Of Memory crashes.

To avoid this, wrap your loops in AssetDatabase.StartAssetEditing() and AssetDatabase.StopAssetEditing(). This pauses automatic imports until all operations complete.

using UnityEditor;
using UnityEngine;
using System.IO;

public class BulkPrefabModifier
{
    [MenuItem("Tools/Bulk Modify Enemy Prefabs")]
    public static void ModifyAllEnemies()
    {        // Fetch all Prefab GUIDs in target folder
        string[] allPrefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Prefabs/Enemies" });
        
        try
        {            // Tip 1: Suspend asset imports temporarily
            AssetDatabase.StartAssetEditing();

            int total = allPrefabGuids.Length;
            for (int i = 0; i < total; i++)
            {                string path = AssetDatabase.GUIDToAssetPath(allPrefabGuids[i]);
                
                // Tip 2: Show progress bar to provide UX feedback
                EditorUtility.DisplayProgressBar("Batch Editing Prefabs", $"Processing {Path.GetFileName(path)} ({i + 1}/{total})", (float)i / total);

                using (var scope = new PrefabUtility.EditPrefabContentsScope(path))
                {                    GameObject root = scope.prefabContentsRoot;
                    EnemyController enemy = root.GetComponent<EnemyController>();
                    if (enemy != null)
                    {                        enemy.isBoss = false;
                        EditorUtility.SetDirty(enemy);
                    }                }
            }
        }
        finally
        {            // Mandatory: Resume imports, triggers a single batch import (extremely fast)
            AssetDatabase.StopAssetEditing();
            
            // Clear progress indicator
            EditorUtility.ClearProgressBar();
        }

        // Save database
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        
        Debug.Log("Bulk Prefab editing completed successfully.");
    }
}

By pausing the auto-import mechanism, you group all re-imports into a single batch operation. This can reduce processing time from tens of minutes to a few seconds. Always use this wrapping structure when building production-grade Editor extensions.