Search Unity

ScriptableObject - life-time, object-identity, and loading

Discussion in 'Scripting' started by lordofduct, Dec 15, 2017.

  1. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    So since ScriptableObject has come out, I've used them more and more for various different things.

    But one thing that has always sat just out of my grips is exactly what the life-time, object-identity and loading of any given instance of a ScriptableObject is. The documentation is sparse in covering this (if at all), and googling about it is often for not since the results usually tend towards the more mundane and simple sides of ScriptableObject and not this in particular.

    life-time

    Lets say I create a custom ScriptableObject type, lets call it: InventoryItem:
    Code (csharp):
    1.  
    2. public class InventoryItem : ScriptableObject
    3. {
    4.     public Sprite Icon;
    5.     public string Title;
    6.     public string Description;
    7. }
    8.  
    Note Icon would be used in some inventory display (pause menu) as the icon that would show to the player so they know they have said item.

    Now, lets say in my assets I create a few different assets of this type, for various inventory items.

    BobbyPin
    ScrewDriver
    Pen

    And I assign corresponding icons and descriptions and what not.

    Now, in a scene if I have some script, lets call it 'AddItemToInventory':
    Code (csharp):
    1.  
    2. public class AddItemToInventory : MonoBehaviour
    3. {
    4.  
    5.     public InventoryItem Item;
    6.  
    7.     public void DoAddItem()
    8.     {
    9.         var inventory = Services.Get<InventoryManager>();
    10.         inventory.Add(Item);
    11.     }
    12.  
    13. }
    14.  
    And say some trigger box somewhere in the scene uses a UnityEvent to call 'DoAddItem' on this script. This adds the InventoryItem ScriptableObject to the InventoryManager.

    This manager persists between scenes.

    This means that when I load my next scene, I expect the InventoryItem to still remain in my inventory.

    And testing says that it does!

    But that leaves me scratching my head about ScriptableObjects. If they're not unloaded from memory between scenes, even though the engine really only knew of a single reference to it, and that reference being an in scene GameObject pointing at it... when would this ScriptableObject ever be unloaded?

    Wouldn't this impact our concerns about memory usage? What about that Sprite attached to it?

    ...

    Object-Identity

    Then... there is the matter of object-identity.

    So I can be clear, when I say object-identity I am referring to the uniqueness of a given instance of a class.

    Code (csharp):
    1.  
    2. var a = new Foo();
    3. var b = a;
    4. a == b
    5.  
    In this, a and b are the same object. There is only a single object, just 2 variables pointing at it.

    So, what does this mean in regards to ScriptableObject?

    Lets return to that InventoryItem and AddItemToInventory example. Lets say I had 2 distinct triggers each with their own distinct AddItemToInventory scripts attached. They each reference the same asset from the respective asset folder it's stored in.

    Are 2 distinct ScriptableObject's loaded? Or is it the same one on both?

    Well... my testing suggests they're both the same object. Which is what I would hope for!

    BUT...

    What if I have 2 distinct scenes, each with triggers, each with AddItemToInventory, and each referencing the same ScriptableObject.

    I play one scene, I pick up said item and it's added to my inventory.

    I then load the next scene, and I again activate its trigger adding the item to my inventory.

    But... it's already been added in the previous scene!

    Are these 2 ScriptableObjects distinct from one another? Or are they the same Object? Can I loop over the list of InventoryItem's I already have and test equality and get a 'true' back for the entry that already matches?

    Again... tests suggest they're the same object... but can I be certain of this since it's not documented anywhere that I know of?!

    Loading

    Also, just like how the life-time suggests that the object is loaded in one scene, but will persist to the next, and I have no idea under what circumstances it would ever unload (aside from the obvious closing of the application).

    What about how/when it's loaded.

    This independence from the scene in its persistence, leaves me wanting for a way to manually load it myself (without having to use the Resources folder... and lets not get into AssetBundles as that creates a whole new level of complexity about it being in both the AssetBundle and in a scene as where and that impact on object-identity!)

    Lets say in scene 1 the player picks up an item, they move to scene 2 and then save the game through my own serialization system that creates a save file on the user's disk somewhere.

    Later they return to the game and I start up from scene 2... but the ScriptableObject does not exist in scene 2! I need to make sure to load the ScriptableObject into memory so that it can be re-added to the inventory.

    Now of course, my save file doesn't directly save said object. Instead I'd save some guid or something for the ScriptableObject, and when I come back I look it up by that guid.

    Thing is... from where do I find that?

    Well... I'd have to create my own inventory asset manager of some sort. Basically a look up table that connects the guid to the actual ScriptableObject.

    BUT... what are the memory implications of this thing?

    It means every inventory item possibility must exist in this look up table. When are those loaded? What of the Sprite on it? Am I taking a memory hit for every single Sprite on every potential InventoryItem in said look-up table? And lets say said look-up table was a ScriptableObject itself, what impact does that have on the object-identity relative to the scene scripts that reference individual items?

    I could get around this with the Resources folder of course. Then I would be able to load the item when I needed to... and then my save file would just save the resource path (or a guid, and I have a look-up table to match it to the resource path).

    But there's MANY reasons I avoid using the Resources folder... including that even Unity themselves say "Don't use it!":
    https://unity3d.com/learn/tutorials/temas/best-practices/resources-folder
    And of course AssetBundles come with a whole new level of questions in regards to object-identity.

    ...

    Conclusion

    So yeah... there's a lot of questions in there. Some of which are rhetorical.

    Really what I'm here about is... does anyone know anything more about ScriptableObjects that I have failed to find that may relate to the topic on hand?

    Is there documentation I may have overlooked?

    Has anyone here thought about these issues like I have?

    Anyone else want to bitch and whine like me?
     
    Last edited: Dec 15, 2017
    littledwarf likes this.
  2. Ecocide

    Ecocide

    Joined:
    Aug 4, 2011
    Posts:
    293
    I think if your first script about the lifetime question points to the ScriptableObject asset in your hierarchy itself. Therefore, whenever you call DoAddItem() it points to the exact same object. However, as I understand, ScriptableObjects are more like templates. So in this case you want to change your script to something like this:

    Code (csharp):
    1.  
    2. public class AddItemToInventory : MonoBehaviour
    3. {
    4.  
    5.     public InventoryItem Item;
    6.  
    7.     public void DoAddItem()
    8.     {
    9.         var inventory = Services.Get<InventoryManager>();
    10.         inventory.Add(ScriptableObject.CreateInstance<InventoryItem>(Item));
    11.     }
    12. }
    13.  
    Btw, check this link: it explains an inventory example using SOs https://unity3d.com/de/learn/tutorials/modules/beginner/live-training-archive/scriptable-objects
     
  3. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    Reasoning about SOs becomes a lot easier when you just think of them as assets. Because that's what they are.

    What's the lifetime of an SO? It's the same as the lifetime of a mesh.
    Are two SOs the same object instance? It's the same as if two materials are the same object instance.
    How do you load SOs? The same way that you load textures!

    Also note that SOs have the same drawback as other assets - changes to them in at runtime (in the editor) persist after play mode is exited, if you're referencing the asset directly. So if you want a mutable SO, you have to clone it, either by Instantiating it, or by getting it from an AssetBundle.
     
    Ryiah and lordofduct like this.
  4. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    That method doesn't exit!
    To create a copy of an SO, you use Instantiate:

    Code (csharp):
    1. inventory.Add(Instantiate(item));
     
  5. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,537
    Most of your confusion seems to be driven by forgetting that List.Add(myScriptableObject) is going to add a reference to a prefab and not an instance of that SO. You'll notice if you modify that item in any way then the changes are pushed back to the prefab, even maintained post play mode.

    Of course that means that if the list persists between scenes then there is nothing to unload and the SO reference is maintained. If you didn't want this, simply instance the SO instead of directly referencing it.

    Code (csharp):
    1.     void Awake()
    2.     {
    3.             // StatsPreset is a pointer to the ScriptableObject prefab
    4.             Stats = Instantiate(StatPreset).Stats;
    5.  
    6.             // Don't want to accidentally corrupt the original at runtime.
    7.             StatPreset = null;
    8.  
    9.             // Looping over the instanced stats.
    10.             foreach (Stat s in Stats) s.Initialize(this);
    11.  
    12.             // GC will cleanup this properly when necessary.
    13.     }
     
    Last edited: Dec 15, 2017
  6. Ecocide

    Ecocide

    Joined:
    Aug 4, 2011
    Posts:
    293
    Well, it DOES exist, but it is probably not what I thought it was.
     
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    No, that's not where most of my confusion is coming from. Since 1) a ScriptableObject isn't a prefab, and 2) I know that if you modify it the asset changes.

    That's what I want to happen. I want there to be a reference, and not a new copy/instance.

    My confusion is that Unity has not covered anything about life-time or object-identity in the documentation. So I don't know if that reference in my List, will be referencing an object with the same identity as the SO pointed at in the next scene on some script.

    My testing shows that it is the same reference... but testing/assuming only gets me so far w/ Unity.

    So I have no faith that it will act the way I presume at runtime, because Unity has a history of being inconsistent and undocumented.
     
    Last edited: Dec 15, 2017
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    Yeah, that drawback though is what leads me to my assumptions, and I don't consider it a drawback.

    But my concerns are more about what happens at runtime after I've built the game.

    I 'believe' it to act like I described. I just have no faith in Unity to be consistent since there is no documentation in regards to it.

    I actually have similar questions about mesh's and materials. The difference being, the object-identity of which has never actually impacted me. If I get a copy of a mesh, I'm fine with that. If I get a copy of a SO, that could have some major implications about my design.
     
  9. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,537
    I habitually call stuff in the Project folder Prefabs - you're right SO's are Assets and not prefabs. Just like Textures, Models, Sounds, etc.

    Even so, they behave as expected. Despite there being nil documentation on the subject I have found that this behavior is totally reliable and consistent. Just because nobody wrote down how it works doesn't mean the way it works is somehow different between observations.

    If two separate things point at the same Asset and you compare those things then they will evaluate as equal because its pointing to the same asset. However, comparing instances of assets to actual assets is not so simple since Unity's equality checks are weird (read: marginally sane). I think it will still say that the instances are equal to the original asset. There are threads on this very thing somewhere around here with good tests in them.

    IMO This is a pretty niche situation, though. It's not clear - I agree, but when would this actually be a concern and more importantly is there a simple workaround? Typically I separate my references to assets and instances as different variables for clarity and use them as needed, knowing the difference (I use SO's practically everywhere I can). I think this is a fair way to exploit them. Thoughts?
     
  10. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    See this is what I'm here about.

    To get other people's experiences. If the behaviour has been consistent for them. To give me more faith in my assumptions.

    I say this... because OTHER parts of Unity's behaviour has NOT been consistent. I have experience with that stuff, and know how to anticipate it.

    I don't with this... so I don't like to just assume. I'd rather someone come along and say "yeah lordofduct, they behave the way you suggest. I've used them heavily in the manner you describe and they've acted consistently even at runtime in a build."

    I think is not reassuring. I would love to see such threads you're speaking about. This loosey goosey nature of it is where my concerns derive from.

    I personally don't think it's that niche. But I guess it could be, either way it's what I would like to do with them.

    I could think of much more complex work arounds. But I'd like to be able to say to my designer/artist: "Whatever item you want that trigger to add to inventory, just drag the related asset into that field in the inspector. I'll take care of the rest behind code. If you want to create a new inventory item, just right click, create InventoryItem, and drag that one on instead."

    Workarounds to ensure object-identity from scene to scene would require multiple more steps for my designer/artist. And I'd like to reduce that as much as possible.

    If I can't get assurance on my assumptions about object-identity in regards to SO's... well then, I'll have to do that.

    I can do that, I don't want to do that, but will if I must.
     
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    Note with further testing, I noticed that if I unload SO's in one scene, and load them up in the next. I can't tell if they're the same object (since I previously unloaded it), BUT it has the same instance ID (when calling Object.GetInstanceID).

    That's nice, and could be used as a way for me to confirm from scene to scene the identity of an object. But alas, with a lack of confirmation or documentation... I can assume all I want, but it's still me making an ass out of myself.
     
  12. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    It's like I want to add something helpful here, but not sure how that will turn out lol

    (trying to recall your questions that weren't completely answered by yourself.. :))
    I have the same observations, mostly; SOs reference 1 instance, basically.
    As for Sprites referenced in 'em, I'd say those aren't fully loaded into memory, unless used.

    Unloading would be, destroying? Or no longer referenced anywhere..

    For sure, to the best of my experience and knowledge (something i count on) is that two scripts adding the same object, as in your example, are definitely "talking about the same reference".

    Possibly useless post, but nevertheless, I hope you find some closure/trust on the issues you seek.

    I never use the Resources folder, myself. I do love Scriptable Objects. :)
     
    lordofduct likes this.
  13. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    Getting other people's experience is what I'm here about. That or someone knowing of some piece of documentation I don't. So great post!

    Unloading would be calling 'Resources.UnloadAsset'. Even if your asset isn't in the Resources folder, this method still unloads it (note, assets can't be destroyed, unity throws an exception):

    Code (csharp):
    1.  
    2.     public class zTest01 : MonoBehaviour
    3.     {
    4.  
    5.         public InventoryEntry Entry;
    6.  
    7.         private IEnumerator Start()
    8.         {
    9.             yield return new WaitForSeconds(2f);
    10.  
    11.             if (Entry != null)
    12.             {
    13.                 Debug.Log(Entry.name + " - " + Entry.GetInstanceID());
    14.                 Resources.UnloadAsset(Entry);
    15.                 Debug.Log(Entry == null); //this will be null since I unloaded it the line before
    16.             }
    17.  
    18.             yield return new WaitForSeconds(2f);
    19.  
    20.             UnityEngine.SceneManagement.SceneManager.LoadScene("WR_LoadTest");
    21.  
    22.         }
    23.  
    24.     }
    25.  
    Yeah, that's what I'm leaning towards.

    What about from scene to scene?

    Thanks again.
     
  14. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    For sure, the "destroy" part came to mind, as I was thinking about creating an new instance (copy) of a SO during runtime, I think. Dunno if that same can't be destroyed rule applies there, but that was my intention.
    Unloading, though, was the gist of it.

    Yes, for sure between scenes, I notice the same thing. Still reliable.

    My scope of programming knowledge isn't vast, but I imagine SOs like an instance of a class that is somehow a file & asset. lol Something I probably never would have invented on my own. So, much like if you held a reference to that instance from 1 scene to another, and expect it to match, I feel the same holds true for 'em.
     
  15. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    Edit to my own post. After some more research, I may have been off with the "not loaded until used". It looks more likely that this is false. However, true in the sense of not loaded in video ram (of course). I read a number of posts, and they all seemed to say that references to prefabs and other assets were loaded into memory.
    :)
     
  16. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    OK, so I just ran this weird test:

    In one scene I have this:
    Code (csharp):
    1.  
    2.     public class zTest01 : MonoBehaviour
    3.     {
    4.  
    5.         public InventoryEntry Entry;
    6.  
    7.         private IEnumerator Start()
    8.         {
    9.             yield return new WaitForSeconds(2f);
    10.  
    11.             if (Entry != null)
    12.             {
    13.                 Debug.Log(Entry.name + " - " + Entry.GetInstanceID()); //BobbyPin - -13292
    14.                 Resources.UnloadAsset(Entry);
    15.                 Debug.Log(Entry == null); //true
    16.                 Debug.Log(object.ReferenceEquals(Entry, null)); //false
    17.                 zTest02.StaticEntry = Entry;
    18.             }
    19.  
    20.             yield return new WaitForSeconds(2f);
    21.  
    22.             UnityEngine.SceneManagement.SceneManager.LoadScene("WR_LoadTest");
    23.  
    24.         }
    25.  
    26.     }
    27.  
    And then in WR_LoadTest, I have this:

    Code (csharp):
    1.  
    2.     public class zTest02 : MonoBehaviour
    3.     {
    4.  
    5.         public static InventoryEntry StaticEntry;
    6.  
    7.         public InventoryEntry Entry;
    8.      
    9.         IEnumerator Start()
    10.         {
    11.             yield return new WaitForSeconds(1f);
    12.  
    13.             if(Entry != null)
    14.             {
    15.                 Debug.Log(Entry.name + " - " + Entry.GetInstanceID()); //BobbyPin - -13292
    16.                 Debug.Log(Entry == StaticEntry); //true
    17.                 Debug.Log(object.ReferenceEquals(Entry, StaticEntry)); //false
    18.             }
    19.             else
    20.             {
    21.                 Debug.Log("BLARGH");
    22.             }
    23.          
    24.         }
    25.      
    26.     }
    27.  
    So basically in scene 1, I reference my asset 'BobbyPin' (asset/prefab whatevs).

    I unload it, basically like destroy. The C# object still technically exists (object.ReferenceEquals returns false), but the Unity object has been destroyed (Entry == null is true).

    Then load the next scene. That scene is also referencing 'BobbyPin'. It has the same name, and the same instance id.

    When I test if Entry == StaticEntry, it says 'true'.

    BUT

    When I say object.ReferenceEquals(Entry, StaticEntry)... I get false.

    This means that unity considers them the same object, even though they are not actually the same object.

    This is 'weird' to me... BUT, this information is super duper helpful.

    == doesn't just match if the unity object is destroyed, it also treats completely distinct objects as the same even though they technically are completely different objects.

    Which is the exact behaviour I was going to have to implement if Unity didn't do this.

    So it does what I expect.

    Sweet diggity dag!
     
  17. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,537
    lordofduct likes this.
  18. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    So now... all I have left is to come up with a way to allow me to dynamically load SO's at startup, even though the scene they're in has yet been loaded. Without using Resources folder....

    weeeeeeee
     
  19. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    I know it's not what you want to hear, but the answer is

    ...

    ...

    AssetBundles!
     
  20. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    lol

    yeah...

    It might end up being that. In which case I'm going to have to run tests on the object-identity in regards to objects coming out of AssetBundles vs referenced in scene.

    ...

    There's one other idea I have. And that is a 'loading scene'. In which I have an empty scene that just references all the possible inventory. I then pick out what inventory needs to be included for the load of the game, and then unload all the others with Resources.UnloadAsset. And then load the actual game scene.
     
  21. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    On an aside...

    Does anyone have experience with using the BuildPipeline to build their game?

    I created a ScriptableObject that allows me to define various build options for a game, and then click a Build button that builds the game in that manner. See:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using UnityEditor;
    4. using System.Collections.Generic;
    5.  
    6. using com.spacepuppy;
    7. using com.spacepuppy.Collections;
    8. using com.spacepuppy.Utils;
    9.  
    10. namespace com.spacepuppyeditor
    11. {
    12.  
    13.     [CreateAssetMenu(fileName = "BuildSettings", menuName = "Spacepuppy/Build Settings", order = int.MaxValue)]
    14.     public class BuildSettings : ScriptableObject
    15.     {
    16.  
    17.         #region Fields
    18.  
    19.         [SerializeField]
    20.         private SceneAsset _bootScene;
    21.  
    22.         [SerializeField]
    23.         [ReorderableArray]
    24.         private List<SceneAsset> _scenes;
    25.  
    26.         [SerializeField]
    27.         private BuildTarget _buildTarget = BuildTarget.StandaloneWindows;
    28.  
    29.         [SerializeField]
    30.         [EnumFlags]
    31.         private BuildOptions _buildOptions;
    32.  
    33.         [SerializeField]
    34.         [Tooltip("Leave blank if you want to use default settings found in the Input Settings screen.")]
    35.         private InputSettings _inputSettings;
    36.    
    37.         #endregion
    38.  
    39.         #region Properties
    40.  
    41.         public SceneAsset BootScene
    42.         {
    43.             get { return _bootScene; }
    44.             set { _bootScene = value; }
    45.         }
    46.  
    47.         public IList<SceneAsset> Scenes
    48.         {
    49.             get { return _scenes; }
    50.         }
    51.  
    52.         public BuildTarget BuildTarget
    53.         {
    54.             get { return _buildTarget; }
    55.             set { _buildTarget = value; }
    56.         }
    57.  
    58.         public BuildOptions BuildOptions
    59.         {
    60.             get { return _buildOptions; }
    61.             set { _buildOptions = value; }
    62.         }
    63.    
    64.         public InputSettings InputSettings
    65.         {
    66.             get { return _inputSettings; }
    67.             set { _inputSettings = value; }
    68.         }
    69.  
    70.         #endregion
    71.  
    72.     }
    73.  
    74.     [CustomEditor(typeof(BuildSettings), true)]
    75.     public class BuildSettingsEditor : SPEditor
    76.     {
    77.  
    78.         public const string PROP_BOOTSCENE = "_bootScene";
    79.         public const string PROP_SCENES = "_scenes";
    80.         public const string PROP_BUILDTARGET = "_buildTarget";
    81.         public const string PROP_BUILDOPTIONS = "_buildOptions";
    82.         public const string PROP_INPUTSETTINGS = "_inputSettings";
    83.  
    84.  
    85.         protected override void OnSPInspectorGUI()
    86.         {
    87.             this.serializedObject.Update();
    88.  
    89.             this.DrawScenes();
    90.  
    91.             this.DrawBuildOptions();
    92.  
    93.             this.DrawInputSettings();
    94.  
    95.             this.serializedObject.ApplyModifiedProperties();
    96.        
    97.             //build button
    98.             if (this.serializedObject.isEditingMultipleObjects) return;
    99.        
    100.             EditorGUILayout.Space();
    101.  
    102.             this.DrawBuildButtons();
    103.         }
    104.  
    105.         public virtual void DrawScenes()
    106.         {
    107.             this.DrawPropertyField(PROP_BOOTSCENE);
    108.             this.DrawPropertyField(PROP_SCENES);
    109.         }
    110.  
    111.         public virtual void DrawBuildOptions()
    112.         {
    113.             //TODO - upgrade this to more specialized build options gui
    114.             this.DrawPropertyField(PROP_BUILDTARGET);
    115.             this.DrawPropertyField(PROP_BUILDOPTIONS);
    116.         }
    117.  
    118.         public virtual void DrawInputSettings()
    119.         {
    120.             this.DrawPropertyField(PROP_INPUTSETTINGS);
    121.         }
    122.  
    123.         public virtual void DrawBuildButtons()
    124.         {
    125.             if (GUILayout.Button("Build"))
    126.             {
    127.                 var path = this.Build();
    128.                 EditorUtility.RevealInFinder(path);
    129.             }
    130.             if (GUILayout.Button("Build & Run"))
    131.             {
    132.                 var path = this.Build();
    133.                 if (!string.IsNullOrEmpty(path))
    134.                 {
    135.                     var proc = new System.Diagnostics.Process();
    136.                     proc.StartInfo.FileName = path;
    137.                     proc.Start();
    138.                 }
    139.             }
    140.             if (GUILayout.Button("Sync To Global Build"))
    141.             {
    142.                 this.SyncToGlobalBuild();
    143.             }
    144.         }
    145.  
    146.         public virtual string[] GetScenePaths()
    147.         {
    148.             using (var lst = TempCollection.GetList<string>())
    149.             {
    150.                 var settings = this.target as BuildSettings;
    151.                 if (settings.BootScene != null) lst.Add(AssetDatabase.GetAssetPath(settings.BootScene));
    152.  
    153.                 foreach (var scene in settings.Scenes)
    154.                 {
    155.                     lst.Add(AssetDatabase.GetAssetPath(scene));
    156.                 }
    157.  
    158.                 return lst.ToArray();
    159.             }
    160.         }
    161.  
    162.         public virtual void SyncToGlobalBuild()
    163.         {
    164.             var lst = new List<EditorBuildSettingsScene>();
    165.             var settings = this.target as BuildSettings;
    166.             foreach (var sc in this.GetScenePaths())
    167.             {
    168.                 lst.Add(new EditorBuildSettingsScene(sc, true));
    169.             }
    170.             EditorBuildSettings.scenes = lst.ToArray();
    171.         }
    172.  
    173.         public virtual string Build()
    174.         {
    175.             var settings = this.target as BuildSettings;
    176.             var scenes = this.GetScenePaths();
    177.  
    178.             var dir = EditorProjectPrefs.Local.GetString("LastBuildDirectory", string.Empty);
    179.             string path;
    180.             switch(settings.BuildTarget)
    181.             {
    182.                 case BuildTarget.StandaloneWindows:
    183.                 case BuildTarget.StandaloneWindows64:
    184.                     path = EditorUtility.SaveFilePanel("Build", dir, Application.productName + ".exe", "exe");
    185.                     break;
    186.                 case BuildTarget.StandaloneLinux:
    187.                 case BuildTarget.StandaloneLinuxUniversal:
    188.                     path = EditorUtility.SaveFilePanel("Build", dir, Application.productName + ".x86", "x86");
    189.                     break;
    190.                 case BuildTarget.StandaloneLinux64:
    191.                     path = EditorUtility.SaveFilePanel("Build", dir, Application.productName + ".x86_64", "x86_64");
    192.                     break;
    193.                 case BuildTarget.StandaloneOSXIntel:
    194.                 case BuildTarget.StandaloneOSXIntel64:
    195.                 case BuildTarget.StandaloneOSXUniversal:
    196.                     path = EditorUtility.SaveFilePanel("Build", dir, Application.productName + ".app", "app");
    197.                     break;
    198.                 default:
    199.                     path = EditorUtility.SaveFilePanel("Build", dir, Application.productName, "");
    200.                     break;
    201.             }
    202.  
    203.             if(!string.IsNullOrEmpty(path))
    204.             {
    205.                 EditorProjectPrefs.Local.SetString("LastBuildDirectory", System.IO.Path.GetDirectoryName(path));
    206.            
    207.                 if(settings.InputSettings != null)
    208.                 {
    209.                     var copy = InputSettings.LoadGlobalInputSettings(false);
    210.                     settings.InputSettings.ApplyToGlobal();
    211.  
    212.                     BuildPipeline.BuildPlayer(scenes, path, settings.BuildTarget, settings.BuildOptions);
    213.  
    214.                     copy.ApplyToGlobal();
    215.                 }
    216.                 else
    217.                 {
    218.                     BuildPipeline.BuildPlayer(scenes, path, settings.BuildTarget, settings.BuildOptions);
    219.                 }
    220.            
    221.                 return path;
    222.             }
    223.             else
    224.             {
    225.                 return string.Empty;
    226.             }
    227.         }
    228.  
    229.     }
    230.  
    231. }
    232.  
    And I end up with:
    BuildSettings.png
    (note - technically there are more options added, because I extended it for this project. This addded the TitleScreen and EpisodeName to the inspector)
    Code (csharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEditor;
    6.  
    7. using com.spacepuppy;
    8. using com.spacepuppy.Collections;
    9. using com.spacepuppyeditor;
    10.  
    11. using com.mansion;
    12.  
    13. namespace com.mansioneditor
    14. {
    15.  
    16.     [CreateAssetMenu(fileName = "EpisodeBuildSettings", menuName = "Spacepuppy/Episode Build Settings", order = int.MaxValue)]
    17.     public class EpisodeBuildSettings : BuildSettings
    18.     {
    19.         #region Fields
    20.  
    21.         [SerializeField]
    22.         private string _episodeName;
    23.         [SerializeField]
    24.         private SceneAsset _titleScreen;
    25.  
    26.         #endregion
    27.  
    28.         #region Properties
    29.  
    30.         public string EpisodeName
    31.         {
    32.             get { return _episodeName; }
    33.             set { _episodeName = value; }
    34.         }
    35.  
    36.         public SceneAsset TitleScreen
    37.         {
    38.             get { return _titleScreen; }
    39.             set { _titleScreen = value; }
    40.         }
    41.  
    42.         #endregion
    43.     }
    44.  
    45.     [CustomEditor(typeof(EpisodeBuildSettings))]
    46.     public class EpisodeBuildSettingsEditor : BuildSettingsEditor
    47.     {
    48.  
    49.         public const string PROP_EPISODENAME = "_episodeName";
    50.         public const string PROP_TITLESCREEN = "_titleScreen";
    51.  
    52.         public override void DrawScenes()
    53.         {
    54.             this.DrawPropertyField(PROP_EPISODENAME);
    55.             this.DrawPropertyField(PROP_BOOTSCENE);
    56.             this.DrawPropertyField(PROP_TITLESCREEN);
    57.             this.DrawPropertyField(PROP_SCENES);
    58.         }
    59.  
    60.         public override string[] GetScenePaths()
    61.         {
    62.             using (var lst = TempCollection.GetList<string>())
    63.             {
    64.                 var settings = this.target as EpisodeBuildSettings;
    65.                 if (settings.BootScene != null) lst.Add(AssetDatabase.GetAssetPath(settings.BootScene));
    66.  
    67.                 foreach (var scene in settings.Scenes)
    68.                 {
    69.                     lst.Add(AssetDatabase.GetAssetPath(scene));
    70.                 }
    71.  
    72.                 if (settings.TitleScreen != null) lst.Add(AssetDatabase.GetAssetPath(settings.TitleScreen));
    73.  
    74.                 return lst.ToArray();
    75.             }
    76.         }
    77.  
    78.         public override void SyncToGlobalBuild()
    79.         {
    80.             var settings = this.target as EpisodeBuildSettings;
    81.             var gameSettings = AssetDatabase.LoadAssetAtPath<Game>("Assets/Resources/GameSettings.asset");
    82.             if (gameSettings != null)
    83.             {
    84.                 Undo.RecordObject(gameSettings, "Sync Build Settings to GameSettings");
    85.                 gameSettings.EpisodeName = settings.EpisodeName;
    86.                 gameSettings.TitleScreenName = (settings.TitleScreen != null) ? settings.TitleScreen.name : string.Empty;
    87.             }
    88.  
    89.             base.SyncToGlobalBuild();
    90.         }
    91.  
    92.         public override string Build()
    93.         {
    94.             var settings = this.target as EpisodeBuildSettings;
    95.             var gameSettings = AssetDatabase.LoadAssetAtPath<Game>("Assets/Resources/GameSettings.asset");
    96.             string backupEpName = null, backupTitleScreen = null;
    97.             if(gameSettings != null)
    98.             {
    99.                 backupEpName = gameSettings.EpisodeName;
    100.                 backupTitleScreen = gameSettings.TitleScreenName;
    101.                 gameSettings.EpisodeName = settings.EpisodeName;
    102.                 gameSettings.TitleScreenName = (settings.TitleScreen != null) ? settings.TitleScreen.name : string.Empty;
    103.             }
    104.  
    105.             var result = base.Build();
    106.  
    107.             if (gameSettings != null)
    108.             {
    109.                 gameSettings.EpisodeName = backupEpName;
    110.                 gameSettings.TitleScreenName = backupTitleScreen;
    111.             }
    112.  
    113.             return result;
    114.         }
    115.  
    116.     }
    117.  
    118. }
    119.  
    120.  

    The functionality of this is that I can define configurations on a per platform level.

    And even have 'episodes', in which the same project actually has build options for different collections of scenes depending what episode they play. There are 3 episodes planned for this specific project (here's episode 1 for anyone interested).

    ...

    So despite what I said about Resources, I actually have a small handful of objects in my Resources folder. It's not some hard and fast rule of mine... I just try to avoid it when I don't need it.

    And one of my issues is that stuff in the 'Resources' folder will ALWAYS be included. I would love to come up with a way to add an option to these BuildSettings so that you can reference a list/foldername/or whatevers, to tell the compiler any auxiliary resources I want added. This way I could have resources that are Episode1 related, and others that are Episode2 related (or platform related). And not need to include said resources in both builds!

    BuildPipeline does not seem to have a feature like this:
    https://docs.unity3d.com/ScriptReference/BuildPipeline.html

    One of my ideas is to have a 'Resources Scene'.

    Basically it's just a scene that I fill with objects that I consider resources I want to include in my project. And I include said scene as a scene in my BuildSettings. No scene ever actually traverses there, it's just there to let the compiler know "we want these assets included!".

    Downside is that it's a weird user interface to have this scene that we just fill with objects we're concerned with. And also, I have no handle/id/path to use to dynamically load objects via, like how Resources have a path string.

    I don't know...

    Anyone have suggestions on another way to deal with it?
     
    Last edited: Dec 16, 2017
  22. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    That one-off scene doesn't sound so bad. I wish I could chime in with something about asset bundles, unfortunately I have zero knowledge of those lol.
    I don't see why you couldn't have the script copy the files you need to resources, if you really wanted it to be automated.
    However, a very "analogue" (haha) solution could be that you create a folder "Res for Ep1." and you know, drop it in Resources yourself, before you build? Maybe that's too simple, I'm not sure. :)
     
    lordofduct likes this.
  23. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    HOLY CRAP

    That's it!

    Dude, thanks so much!!!!
     
  24. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    No prob, man.. Glad I could help :)
     
    Last edited: Dec 16, 2017
  25. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    Ugh... of course, Unity doesn't include the files when you rename the file. Because you first have to wait for Unity to reprocess all the files before it builds.

    Need an editor time coroutine, or async (still on an older version of unity myself).

    Unity, always standing in my way of using Unity.
     
  26. olgimpy

    olgimpy

    Joined:
    Jun 4, 2013
    Posts:
    3
    Artist here! I dunno what any of that means.
    But it's all my fault :D
     
    lordofduct likes this.
  27. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,539
    Done... added an editor coroutine. Change folder name, waited a second, then ran the build, then changed the folder name back.

    All works fine!

    This is hack as all hell... but hey, it works.

    Hey Unity devs... maybe do something about this!!??
     
  28. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    Glad it's working for ya =)
     
  29. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    If you want to do it synchronously, you should be able to just call AssetDatabase.ImportAssets on the new folder. Or AssetDatabase.Refresh.
     
    lordofduct likes this.