Search Unity

Excluding certain Monobehaviour classes from build?

Discussion in 'Scripting' started by FeastSC2, Oct 11, 2018.

  1. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    There are certain classes that I only use in the editor and I would like that when I build the game they don't get included in it.

    I thought defining preprocessor directives with #if UNITY_EDITOR around the whole class would be enough but I get messages in the build that the script is missing from the gameObjects it is on in editor mode.

    What can I do to not include certain scripts in the build despite the fact that they're on gameObjects?
     
  2. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    Well, no instructions you give to the C# compiler are going to alter what components are included in objects in your scene. Hypothetically, you'd need some sort of instructions to Unity's build process that tells it to remove those components from the scene when exporting a build. I'm not sure if any feature like that exists.

    You could use #if UNITY_EDITOR to remove the methods from the classes, so that they don't have any executable code (other than what they inherit from MonoBehaviour).

    Why exactly are you concerned about removing those classes from your build?
     
  3. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    Because they give me warnings that the component on the gameObject is missing and it's just not clean :). I could go for removing all the contents of the class instead of the whole class but it's something I'd rather avoid.

    I was wondering if there wasn't a tag to put on the class itself to not include it in the build?
     
  4. MSplitz-PsychoK

    MSplitz-PsychoK

    Joined:
    May 16, 2015
    Posts:
    1,278
    I do this sometimes:

    Code (CSharp):
    1. public class MyClass : Monobehaviour
    2. {
    3.  
    4. #if UNITY_EDITOR
    5.  
    6.     // Do your normal editor stuff
    7.  
    8. #else
    9.  
    10.     void OnEnable()
    11.     {
    12.         // This will remove this component and won't destroy the GameObject
    13.         Destory(this);
    14.     }
    15.  
    16. #endif
    17.  
    18. }
     
    FeastSC2 likes this.
  5. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
  6. IMHO it's very bad to mix editor and run-time functionality. Editor scripts (stuff what are running in the editor) should be in Editor folders.
    I don't understand what references will be removed? You asked the question, why you're getting errors that some scripts are missing. You're getting the error because you're keeping garbage references to editor scripts but you are not including those scripts in the run-time.

    You shouldn't have any references to editor scripts EVER in run-rime scripts and/or game objects.
     
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,528
    I do this as well... minus the destroy. I mean a dummy/empty component out there doesn't really cost at all that much. So I don't really waste my time destroying it anyhow.

    A place I use this is something like this:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4. using System.Linq;
    5.  
    6. using com.spacepuppy;
    7. using com.spacepuppy.Collections;
    8. using com.spacepuppy.Utils;
    9.  
    10. using com.mansion.Entities.Weapons;
    11. using com.mansion.Messages;
    12.  
    13. namespace com.mansion.DebugScripts
    14. {
    15.  
    16. #if (UNITY_EDITOR || DEVELOPMENT_BUILD)
    17.     public class DebugStartup : SPComponent, IGameStateChangedGlobalHandler
    18. #else
    19.     public class DebugStartup : SPComponent
    20. #endif
    21.     {
    22.  
    23. #if UNITY_EDITOR
    24.        
    25.         [SerializeField]
    26.         private EpisodeSettings _episodeSettings;
    27.  
    28.         [SerializeField]
    29.         [Tooltip("Set with scene name to load if death occurs before any checkpoint occurs. Otherwise no reload occurs.")]
    30.         private string _debugCheckpointReloadScene;
    31.  
    32.         protected override void Awake()
    33.         {
    34.             base.Awake();
    35.            
    36.             if (!Game.Initialized)
    37.             {
    38.                 Debug.Log("DEBUG INITIALIZING GAME");
    39.                 //Cursor.visible = false;
    40.                 Game.Init((g) =>
    41.                 {
    42.                     if (_episodeSettings != null) g.SetEpisodeSettings(_episodeSettings);
    43.                 });
    44.                 _dispatchStartMessage = true;
    45.  
    46.                 if (_episodeSettings != null && _episodeSettings.ScenarioType != null)
    47.                     Game.StartScenario(_episodeSettings.ScenarioType).StartGame(new SaveGameToken()
    48.                     {
    49.                         Data = null,
    50.                         Checkpoint = new CheckpointState(string.IsNullOrEmpty(_debugCheckpointReloadScene) ? Constants.DEBUG_STARTUP_CHECKPOINT_SCENE : _debugCheckpointReloadScene)
    51.                     });
    52.             }
    53.         }
    54.  
    55. #endif
    56.  
    57. #if (UNITY_EDITOR || DEVELOPMENT_BUILD)
    58.  
    59.         private bool _dispatchStartMessage;
    60.         private bool _showDebugMenu;
    61.         private Texture2D _background;
    62.  
    63.         protected override void Start()
    64.         {
    65.             base.Start();
    66.  
    67.             _background = new Texture2D(1, 1);
    68.             _background.SetPixels(new Color[] { Color.gray });
    69.  
    70.             Messaging.RegisterGlobal<IGameStateChangedGlobalHandler>(this);
    71.  
    72.             if(_dispatchStartMessage)
    73.             {
    74.                 Messaging.FindAndBroadcast<IDebugStartMessageHandler>((o) => o.OnDebugStart());
    75.             }
    76.         }
    77.  
    78.         protected override void OnDestroy()
    79.         {
    80.             base.OnDestroy();
    81.  
    82.             Messaging.UnregisterGlobal<IGameStateChangedGlobalHandler>(this);
    83.         }
    84.  
    85.         private void Update()
    86.         {
    87.             if(Input.GetKeyDown(KeyCode.BackQuote))
    88.             {
    89.                 const string TOKEN_DEEBUGPAUSE = "*DebugMenuPause*";
    90.  
    91.                 _showDebugMenu = !_showDebugMenu;
    92.                 if (_showDebugMenu)
    93.                     Game.Scenario.GameStateStack.Push(GameState.Paused, TOKEN_DEEBUGPAUSE);
    94.                 else
    95.                     Game.Scenario.GameStateStack.Pop(TOKEN_DEEBUGPAUSE);
    96.             }
    97.         }
    98.  
    99.         private void OnGUI()
    100.         {
    101.             if (!_showDebugMenu) return;
    102.  
    103.             var style = new GUIStyle(GUI.skin.box);
    104.             style.normal.background = _background;
    105.             GUI.Box(new Rect(10f, 10f, 500f, 300f), "DEBUG MENU", style);
    106.  
    107.             GUILayout.BeginArea(new Rect(15f, 60f, 490f, 245f));
    108.            
    109.             //validate player exists
    110.             var playerEntity = IEntity.Pool.Find<IEntity>((e) => e.Type == IEntity.EntityType.Player);
    111.             if (playerEntity == null)
    112.             {
    113.                 GUILayout.BeginHorizontal();
    114.                 GUILayout.FlexibleSpace();
    115.                 GUILayout.Label("No player was located.");
    116.                 GUILayout.FlexibleSpace();
    117.                 GUILayout.EndHorizontal();
    118.  
    119.                 goto Finish;
    120.             }
    121.  
    122.             //infinite ammo
    123.             var ammo = playerEntity.FindComponent<AmmoPouch>();
    124.             if(ammo != null)
    125.             {
    126.                 foreach(var e in System.Enum.GetValues(typeof(AmmoType)).Cast<AmmoType>())
    127.                 {
    128.                     GUILayout.BeginHorizontal();
    129.  
    130.                     var oldcnt = ammo.GetAmmoCount(e);
    131.                     var cnt = DiscreteFloatField(e.ToString() + " Ammo Count: ", oldcnt);
    132.                     if(oldcnt != cnt)
    133.                         ammo.SetAmmoCount(e, cnt);
    134.  
    135.                     bool oldInfAmmo = float.IsPositiveInfinity(cnt);
    136.                     bool infAmmo = GUILayout.Toggle(oldInfAmmo, " Infinite Ammo");
    137.                     if (oldInfAmmo != infAmmo)
    138.                     {
    139.                         ammo.SetAmmoCount(e, infAmmo ? float.PositiveInfinity : 10f);
    140.                     }
    141.  
    142.                     GUILayout.EndHorizontal();
    143.                 }
    144.             }
    145.  
    146.             //Health - God Mode
    147.             if(playerEntity.HealthMeter != null)
    148.             {
    149.                 playerEntity.HealthMeter.MaxHealth = FloatField("Max Health", playerEntity.HealthMeter.MaxHealth);
    150.                 playerEntity.HealthMeter.Health = FloatField("Health", playerEntity.HealthMeter.Health);
    151.  
    152.                 bool oldGodMode = float.IsPositiveInfinity(playerEntity.HealthMeter.Health);
    153.                 bool godMode = GUILayout.Toggle(oldGodMode, " God Mode");
    154.                 if(godMode != oldGodMode)
    155.                 {
    156.                     if(godMode)
    157.                     {
    158.                         playerEntity.HealthMeter.MaxHealth = float.PositiveInfinity;
    159.                         playerEntity.HealthMeter.Health = float.PositiveInfinity;
    160.                     }
    161.                     else
    162.                     {
    163.                         playerEntity.HealthMeter.MaxHealth = 100f;
    164.                         playerEntity.HealthMeter.Health = 100f;
    165.                     }
    166.                 }
    167.             }
    168.  
    169.             //1-btn grapple release
    170.             /*
    171.             var grapple = playerEntity.FindComponent<com.mansion.Entities.Actors.Player.PlayerGrappledState>();
    172.             if(grapple != null)
    173.             {
    174.                 GUILayout.BeginHorizontal();
    175.                 GUILayout.Label("Release Odds: " + grapple.GrappleReleaseOdds.ToString("P"));
    176.                 grapple.GrappleReleaseOdds = Mathf.Clamp01(GUILayout.HorizontalSlider(grapple.GrappleReleaseOdds, 0f, 1f));
    177.                 GUILayout.EndHorizontal();
    178.             }
    179.             */
    180.             var actionMotor = playerEntity.FindComponent<com.mansion.Entities.Actors.Player.PlayerActionMotor>();
    181.             if(actionMotor != null && actionMotor.DefaultMovementSettings != null)
    182.             {
    183.                 GUILayout.BeginHorizontal();
    184.                 GUILayout.Label("Release Odds: " + actionMotor.DefaultMovementSettings.GrappleReleaseOdds.ToString("P"));
    185.                 actionMotor.DefaultMovementSettings.GrappleReleaseOdds = Mathf.Clamp01(GUILayout.HorizontalSlider(actionMotor.DefaultMovementSettings.GrappleReleaseOdds, 0f, 1f));
    186.                 GUILayout.EndHorizontal();
    187.             }
    188.  
    189. Finish:
    190.             GUILayout.EndArea();
    191.  
    192.         }
    193.  
    194.  
    195.         private static float DiscreteFloatField(string label, float value)
    196.         {
    197.             GUILayout.BeginHorizontal();
    198.  
    199.             GUILayout.Label(label);
    200.  
    201.             string val = GUILayout.TextField(value.ToString());
    202.             //val = System.Text.RegularExpressions.Regex.Replace(val, @"^[0-9]", "");
    203.             if (!float.TryParse(val, out value))
    204.                 value = 0;
    205.  
    206.             GUILayout.EndHorizontal();
    207.  
    208.             return value;
    209.         }
    210.  
    211.         private static float FloatField(string label, float value)
    212.         {
    213.             GUILayout.BeginHorizontal();
    214.  
    215.             GUILayout.Label(label);
    216.  
    217.             string val = GUILayout.TextField(value.ToString());
    218.             //val = System.Text.RegularExpressions.Regex.Replace(val, @"^[0-9\.\+\-]", "");
    219.             if (!float.TryParse(val, out value))
    220.                 value = 0;
    221.  
    222.             GUILayout.EndHorizontal();
    223.  
    224.             return value;
    225.         }
    226.  
    227.  
    228.         void IGameStateChangedGlobalHandler.OnGameStateChanged(GameState last, GameState current)
    229.         {
    230.             if ((current & GameState.Paused) == 0)
    231.             {
    232.                 _showDebugMenu = false;
    233.             }
    234.         }
    235.  
    236. #endif
    237.  
    238.     }
    239.  
    240. }
    241.  
    Basically in our current project we want to be able to start our game from any scene in the editor.

    But if you do this... the game isn't necessarily initialized yet. Certain actions that usually occur at the start of the game in our boot scene haven't necessarily occurred yet if you just press play in the editor.

    I also use this as a place to have an optional debug HUD display in the game if I press the tilde key.

    But in game when I finally deploy... all that's left is an empty 'DebugStartup' script that does nothing.
     
    Fitbie and FeastSC2 like this.
  8. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,333
    You can use [PostProcessScene] to delete all components of the type on build:

    Here's the relevant parts from our codebase:

    Code (csharp):
    1. public static class DestroyUnwantedScripts {
    2.  
    3.     private static readonly Type[] typesToDeleteOnBuild = {
    4.         typeof(A),
    5.         typeof(B),
    6.         // etc
    7.     };
    8.  
    9.     [PostProcessScene]
    10.     public static void DeleteObjects() {
    11.         if (BuildPipeline.isBuildingPlayer) {
    12.             foreach (var type in typesToDeleteOnBuild) {
    13.                 Debug.Log($"Destroying all instances of {type.Name} on build!");
    14.                 foreach (var obj in ObjectFinder.FindAll(type, true)) {
    15.                     Object.DestroyImmediate(obj);
    16.                 }
    17.             }
    18.         }
    19.     }
    20. }
    There's more to it, but that's the important bits. ObjectFinder is a utility we have that also looks for the components on inactive objects, you can use FindObjectsOfType instead, but that will skip over inactive objects.

    We also have some types that we destroy on entering play mode or on builds (they're simply used to attach OnSceneGUI to some objects or things like that), but that code-path is horrible and slow.
     
  9. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    That's really interesting! Do you mind showing me the FindAll method of your ObjectFinder class? I tried it on my builds and it has destroyed my scripts even after the builld (it deleted my scripts in the project).
    But it might be because I'm not using your method.
     
  10. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,333
    I'm at home now until Monday, so that'll take a while.

    It essentially looks at the current scene, grabs the root objects, calls GetComponents on those, and does the same with all of the root objects' children.

    It shouldn't be removing the scripts outside of builds, if you include the BuildPipeline.isBuildingPlayer check. Unless you're using Resources.FindObjectsOfTypeAll, which you should not. I was referencing Object.FindObjectsOfType.
     
    FeastSC2 likes this.
  11. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    Here's the script I used:

    Code (CSharp):
    1. #if UNITY_EDITOR
    2. using System;
    3. using System.Collections.Generic;
    4. using UnityEditor;
    5. using UnityEditor.Callbacks;
    6. using UnityEngine;
    7. using UnityEngine.SceneManagement;
    8. using Object = UnityEngine.Object;
    9.  
    10. public static class DestroyEditorMonobehaviourScripts
    11. {
    12.     private static readonly Type[] typesToDeleteOnBuild = {
    13.         typeof(DT_Object),
    14.         typeof(DT_Environment),
    15.         typeof(DT_Sprite),
    16.         typeof(DT_Background),
    17.  
    18.         typeof(EasyAnimate),
    19.         typeof(TestAnimate),
    20.         typeof(AnimatorPreviewer),
    21.  
    22.         typeof(TerrainCreator),
    23.         typeof(ShapeMesh),
    24.     };
    25.  
    26.     [PostProcessScene]
    27.     public static void DeleteObjects()
    28.     {
    29.         if (BuildPipeline.isBuildingPlayer)
    30.         {
    31.             foreach (var type in typesToDeleteOnBuild)
    32.             {
    33.                 Debug.Log("Destroying all instances of " + type.Name + " on build!");
    34.                 foreach (var obj in FindObjectsOfTypeAll(type, true))
    35.                 {
    36.                     Object.DestroyImmediate(obj);
    37.                 }
    38.             }
    39.         }
    40.     }
    41.     /// Use this method to get all loaded objects of some type, including inactive objects.
    42.     /// This is an alternative to Resources.FindObjectsOfTypeAll (returns project assets, including prefabs), and GameObject.FindObjectsOfTypeAll (deprecated).
    43.     public static List<Component> FindObjectsOfTypeAll(Type _type, bool _findInactive = false)
    44.     {
    45.         var results = new List<Component>();
    46.         for (int i = 0; i < SceneManager.sceneCount; i++)
    47.         {
    48.             var s = SceneManager.GetSceneAt(i);
    49.             if (s.isLoaded)
    50.             {
    51.                 var allGameObjects = s.GetRootGameObjects();
    52.                 for (int j = 0; j < allGameObjects.Length; j++)
    53.                 {
    54.                     var go = allGameObjects[j];
    55.                     results.AddRange(go.GetComponentsInChildren(_type, _findInactive));
    56.                 }
    57.             }
    58.         }
    59.         return results;
    60.     }
    61. }
    62. #endif
    This works quite well but I realized it's still giving me some errors in the development build.
    Code (CSharp):
    1. A script behaviour (probably EasyAnimate?) has a different serialization layout when loading. (Read 32 bytes but expected 328 bytes)
    2. Did you #ifdef UNITY_EDITOR a section of your serialized properties in any of your scripts?
    Specifically one related to a script called EasyAnimate, a Monobehaviour script attached on the effects I instantiate @ runtime.
    EasyAnimate's entire class is surrounded by a preprocessor directive #ifdef UNITY_EDITOR but then again so do all the other classes, yet those don't give me any errors.

    It seems that we're only removing the scripts from all the scenes and not from potential prefabs that could also have those scripts on them? Am I understanding this correctly?
     
  12. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,333
    Yeah, that only removes those scripts from scenes.

    There's currently no way to post-process prefabs at builds, unless you do the hard work yourself to keep a backup of the non-processed prefab that you restore after the build. There was a thread on it just a week ago.


    Note that for the script as it is now, you don't have to iterate the scenes and remove objects. PostProcessScene is called once per scene, with that scene being the active scene when it's running.
     
    FeastSC2 likes this.
  13. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,609
  14. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    An asset is marked with HideFlags.DontSave but is included in the build:
    Asset: 'Assets/Art/_FX/_FX_Skills/Player/Air/JumpUp_Start/FX_Smear.prefab'
    Asset name: FX_Smear
    (You are probably referencing internal Unity data in your build.)
    UnityEditor.BuildPlayerWindow:BuildPlayerAndRun()

    1st option doesn't work
    and the 2nd is for the entire gameObject, which I want to keep.