Search Unity

  1. Good news ✨ We have more Unite Now videos available for you to watch on-demand! Come check them out and ask our experts any questions!
    Dismiss Notice

Modifying prefabs during a build

Discussion in 'Editor & General Support' started by Sycobob, Jul 23, 2015.

  1. Sycobob

    Sycobob

    Joined:
    Feb 1, 2013
    Posts:
    77
    Is anyone aware of a way to modify prefabs while making a build? Specifically, I want to be able to remove debug and utility scripts for the build, but leave them intact in the project. I had hoped setting HideFlags.DontSaveInBuild on a MonoBehaviour would cause it to be stripped out during builds, but this is not the case. The documentation is unclear on exactly how this value is supposed to function, so I'm not sure if it's broken, or does something different altogether.

    Ultimately, I'm trying to implement a clean way to have MonoBehaviours that only exist in the Editor. On first glance one may assume using custom Editors or EditorWindows would be the way to go, but there are cases where these are not sufficient. A couple examples:
    • Type-safe, multi-tagging of GameObjects

      The current tagging system leaves much to be desired. It's based on strings, which are not safe against refactoring, fail at runtime instead of compile time, and can fail silently. Further, you can only tag an object with a single tag. We use MonoBehaviours to tag objects for debug purposes and to identify objects as targets for automated development processes, such as cutting meshes into multiple pieces at build time to allow them to be broken apart at runtime. There is no existing MonoBehaviour to target in this case, since these are just meshes and no additional options are needed.

    • Debug functionality

      A useful debugging tool is to toss an additional script on an object that adds debug functionality. That functionality may be additional logging, place to stick breakpoints/tracepoints, checking for bad state, forcing LODs, displaying additional debug GUI/objects, etc.
    I know there are workarounds for this, such as doing a bunch of #if UNITY_EDITOR or if ( Application.isEditor ), destroying the script in Awake, etc. But that just clutters code, is prone to mistakes, and leaves the script for the engine to load, deserialize, and activate.
     
  2. ObliviousHarmony

    ObliviousHarmony

    Joined:
    Jul 3, 2014
    Posts:
    79
    Hello Sycobob,

    To begin, I'd like to offer up a suggestion. What you might do is make a base class for these editor utilities that you want automatically removed, and then you can put the Awake() removal there, and never have to worry about forgetting to do it on the editor utilities. If you're dead set on removing the components however, I've got just the thing for you!

    In my game, I remove MeshRenderer and MeshFilter components at build-time to create a lightweight version of the game for dedicated server distributions. The code for this process is:

    Code (csharp):
    1. /// <summary>
    2. /// This method will duplicate the prefabs scene and create two versions of it, one for the client, and one for the server
    3. /// The client version will not contain any of the complex prefabs
    4. /// The server version will contain no art assets at all
    5. /// </summary>
    6. private static void PrepareScenes()
    7. {
    8.     // start by duplicating the Prefabs scene twice
    9.     FileUtil.ReplaceFile("Assets/Scenes/Prefabs.unity", "Assets/Scenes/Prefabs_Client.unity");
    10.     FileUtil.ReplaceFile("Assets/Scenes/Prefabs.unity", "Assets/Scenes/Prefabs_Server.unity");
    11.  
    12.     // store the current scene
    13.     string szCurrentScene = EditorApplication.currentScene;
    14.  
    15.     // prompt them to save the scene
    16.     EditorApplication.SaveCurrentSceneIfUserWantsTo();
    17.  
    18.     // open the client scene
    19.     EditorApplication.OpenScene("Assets/Scenes/Prefabs_Client.unity");
    20.  
    21.     // remove the complex prefabs
    22.     GameObject obj = GameObject.Find("ComplexPrefabs");
    23.     GameObject.DestroyImmediate(obj);
    24.  
    25.     // save
    26.     EditorApplication.SaveScene();
    27.  
    28.     // open the server scene
    29.     EditorApplication.OpenScene("Assets/Scenes/Prefabs_Server.unity");
    30.  
    31.     // get the prefabs
    32.     obj = GameObject.Find("Prefabs");
    33.  
    34.     // remove any components we don't want to include in the build
    35.     foreach (MeshRenderer mesh in obj.GetComponentsInChildren<MeshRenderer>())
    36.         GameObject.DestroyImmediate(mesh);
    37.     foreach (MeshFilter mesh in obj.GetComponentsInChildren<MeshFilter>())
    38.         GameObject.DestroyImmediate(mesh);
    39.  
    40.     // get the complex prefabs
    41.     obj = GameObject.Find("ComplexPrefabs");
    42.  
    43.     // remove the init script
    44.     ComplexPrefabInit initscript = obj.GetComponent<ComplexPrefabInit>();
    45.     GameObject.DestroyImmediate(initscript);
    46.  
    47.     // nuke the camera
    48.     obj = GameObject.FindGameObjectWithTag("MainCamera");
    49.     GameObject.DestroyImmediate(obj);
    50.  
    51.     // save
    52.     EditorApplication.SaveScene();
    53.  
    54.     // re-open the original scene
    55.     EditorApplication.OpenScene(szCurrentScene);
    56. }
    While it doesn't do EXACTLY what you want (I used scenes instead of prefabs), it does demonstrate the basic principle.

    1. Copy the original prefab to a temp location, where it will not be included in the build.
    2. Load the prefab in question
    3. Remove the components you don't want.
    4. Save the prefab
    5. Build your player
    6. Move the original back from the temp location
     
    mtaesiri and wobes like this.
  3. jpease

    jpease

    Joined:
    Apr 18, 2013
    Posts:
    11
    If you're OK with putting the components you want to strip out into their own leaf GameObjects, you can set the tag on those GameObjects to "EditorOnly" in the inspector, and Unity appears to honor that tag when making builds. This is the only really built-in solution I know of.

    Besides that, the workarounds you mention can be OK (I often use #if UNITY_EDITOR to at least disable the code). HideFlags.DontSaveInBuild sounds promising but indeed it doesn't appear to work, at least on individual components.

    You can also try putting something like this in an Editor folder in your project:
    Code (csharp):
    1. public class Whatever {
    2.   [UnityEditor.Callbacks.PostProcessScene]
    3.   public static void OnPostprocessScene() {
    4.     if(UnityEditor.BuildPipeline.isBuildingPlayer) { // alternatively, could check !UnityEngine.Application.isPlaying
    5.       foreach(UnityEngine.Object o in UnityEngine.Resources.FindObjectsOfTypeAll(typeof(ComponentYouWantToDelete))) {
    6.         if(o && !UnityEditor.AssetDatabase.Contains(o)) {
    7.           UnityEngine.Object.DestroyImmediate(o);
    8.         }
    9.       }
    10.     }  
    11.   }
    12. }
    This should handle many cases, but probably not the case of needing to remove components from inside a prefab that is dynamically instantiated. It might be possible, albeit tricky, to modify it to handle that case too.
     
    mtaesiri likes this.
  4. Sycobob

    Sycobob

    Joined:
    Feb 1, 2013
    Posts:
    77
    Thanks @ObliviousHarmony I appreciate the input. As I said, I consider using Awake to be a work around. That leaves the script reference serialized as part of the enclosing prefab/scene. It means unnecessary data in the file. The engine must read that data, create a new object, deserialize data into it, and then the object just immediately destroys itself. This just seems silly and wasteful to me.

    This post is about seeking a "clean" solution that doesn't do unnecessary work.

    Your idea of making backups, modifying the original, then restoring the back up is a solid idea and it would work for smaller projects fairly well. But that's not scalable for large, complex projects. It introduces a bunch of disk work. Unless you want to do that for every prefab/scene in the entire project that's not even going into the build (in our case, that's probably thousands) you're going to need to manually determine which prefabs are actually going into the build and which aren't. That means reconstructing Unity's rules for what gets included: Resources, StreamingAssets, asset bundles, etc.

    The amount of unnecessary work starts to get pretty out of hand for something as simple as "don't include this debug component in builds".
     
  5. Sycobob

    Sycobob

    Joined:
    Feb 1, 2013
    Posts:
    77
    This may just work. Moving the scripts to separate GameObjects is too open to mistakes, but it looks like Component has a tag field as well. I didn't even realize there was an EditorOnly tag. Admittedly, I've never used the tag system. As soon as I saw strings I just shuddered and ran. If the engine respects the Component tag, that might be problem solved.

    Yea, prefabs are the hard part. Finding stuff in the scene is super easy (a no-upkeep solution can be implemented in just a few lines with an attribute). If you can't modify prefabs on-the-fly as they are included in the build it gets obnoxious fast, as I mention in the previous post.
     
  6. jpease

    jpease

    Joined:
    Apr 18, 2013
    Posts:
    11
    Unfortunately Component doesn't really have a tag field. (Tag is stored in the GameObject, and Component.tag is a property that acts as an alias for the GameObject's tag.)
     
  7. ObliviousHarmony

    ObliviousHarmony

    Joined:
    Jul 3, 2014
    Posts:
    79
    As far as performance goes, I often forget what life was like before the invention of the SSD. While I'm not sure it would work, you could potentially try using AssetDatabase.FindAssets() and searching by type, then doing the copy/replace stuff before building. That way at least you could drop your result set down to just the assets that include your editor only components. I do agree maybe this should be a class attribute or something though, as that would make life a hell of a lot easier.
     
  8. Sycobob

    Sycobob

    Joined:
    Feb 1, 2013
    Posts:
    77
    Good call
     
  9. ObliviousHarmony

    ObliviousHarmony

    Joined:
    Jul 3, 2014
    Posts:
    79
    Glad I could help :D

    I LOVE the AssetDatabase utilities, they are fantastic, and incredibly useful.
     
  10. liortal

    liortal

    Joined:
    Oct 17, 2012
    Posts:
    3,455
    We have used a system that patches prefabs at build time in the past (mentioned here: http://www.slideshare.net/LiorTal1/lessons-learned-with-unity-and-webgl)

    The solution for us was to create a custom build system (sounds complex, but this can be a simple menu item with a "Build" option).

    Once you start a build, you can run your custom stuff, in your case you could look up the asset database (assets in your project) to find whatever prefab type you're looking for. You would need some kind of system for "tagging" these objects or components. The build system would "patch" the prefab (stores an Undo point after doing so) and create a build.

    Once the build is done, you can undo the changes and the prefab goes back to its original state.

    For us this system was used in conjunction with a [PostProcessScene] callback that also stripped out stuff from game objects in scenes.
     
    Last edited: Dec 20, 2015
    NuclearC00kie and Sycobob like this.
  11. FIFTYTWO

    FIFTYTWO

    Joined:
    Oct 21, 2014
    Posts:
    35
    I tried to implement this approach on the top of IProcessSceneWithReport, IPreprocessBuildWithReport, IPostprocessBuildWithReport interfaces. I prepare some stuff in IPreprocessBuildWithReport, later in IProcessSceneWithReport I get scene dependencies and make prefab changes with Undo calls. But in IPostprocessBuildWithReport Undo history is empty. If I simulate build process it works good but in real build Undo history does not exist unfortunately. Could you explain how did you preserve Undo stack in the end of the build process? Or maybe there are some tricks?
     
  12. francismoy

    francismoy

    Joined:
    Jul 5, 2017
    Posts:
    29
    What I did in my project (although this is for changing a scene at build time instead of prefabs), is to tag the game objects that I want removed from the release build. Then, for every scene in the project, I run the following code in the OnProcessBuild from IPreprocessBuildWithReport interface:

    Code (CSharp):
    1.  
    2.         GameObject[] objects = GameObject.FindGameObjectsWithTag("RemoveFromLiveBuild");
    3.         for (int i = 0; i < objects.Length; ++i)
    4.         {
    5.             Object.DestroyImmediate(objects[i]);
    6.         }
    7.  
    8.         Debug.Log($"[PreProcessBuildScript] Destroyed {objects.Length} game objects from game scene");
    9.         EditorSceneManager.SaveScene(gameScene);
    10.  
    11.         EditorSceneManager.CloseScene(gameScene, false);
     
unityunity