Make SceneAsset full time citizen asset in the Runtime

Hello,

I was trying for some time to create a scene-state management system, where I can map a GameState class to a specific SceneAsset. I created a ScriptableObject where I have this mapping data. Then I made a root scene where I have the StateManager and the reference to the mapping data. From this test I found that the Unity’s scene management is a bit inflexible:

  • SceneAsset is only available in the UnityEditor API but not in the UnityEngine, even thought it can be serialized as a UnityEngine.Object which is the base of all assets.
  • SceneManager.LoadScene[Async] has only 2 ways to identify a scene asset: by the index in the Build Settings scene list or by the scene name/path. No option to identify a scene by a unique identifier that is not dependent on the scene name, path in the project’s folder or any ordering.
  • Any SceneAsset that is not included in the BuildSettings scene list, even thought is it referenced in the main scene like any other asset, does not get included into the build.
    Because these limitations, I had to make some workarounds. Added a script that hooks to the build event from where I had manually injectall the referenced SceneAssets to the Build Settings scenes list and map GameStates to the corresponding scene name/path. Also, on any scene asset change of it’s name or path, I had to re-save the ScriptableObject data to map GameStates to the new scene asset’s path.

My suggestion is not to completely change the way current system works. Let the scene list from the build settings work as it is now, but also let SceneAsset be available in the runtime API and also contribute the the build process as all the other assets do. So, if there is a scene in the build settings that inside has a game object with a script that contains a reference to a SceneAsset, that SceneAsset gets added to the BuildSettings scene list at build time (transparent for the user), and can be used with SceneManager.LoadScene[Async] by it’s SceneAsset reference. As I know, each asset has an unique identifier at the runtime(after the build process) that is based on the guid (witch is the Editor global identifier of a file) and it’s local fileID (to identify multiple assets that can be contained in one file: fbx has skeleton, geometry, animations and materials).

Regards.

Hi,

yes, it’s really a pain and somewhat strange that sceneAsset is editor only. Unity should live by the “eat its own dog food” rule from time to time and adjust theses issues.

check this out, it provides a solution/hack: [Unity3D] A Reliable, user-friendly way to reference SceneAssets by script. · GitHub

Bye,

Jean

I actually agree w this. Unexpected since these types of posts are usually dumb.

Having references to scene assets in unityengine instead of editor would make a bunch of stuff easier.

Yeah, I also think it’s pretty weird that we’ve got a proper object to refer to scenes, but at runtime have to resort to strings or numbers.

This is the only thing I’m not sure about. I’m not saying I disagree, I’m really not sure either way.

On one hand, yeah, scenes don’t need to be treated differently to any other data. On the other hand, it’s quite handy to have one place where we can pick what’s included in or excluded from a build. Of course that itself depends on how we implement other things, and there are other solutions which could be more practical anyway…

I was able to make it work with this code. I only tested this in 2020.3.6f1, Linux x64, Mono.
This is the closest to what I’d expect Unity to do if they were to implement it.
I’m releasing it to public domain (no warranties), so feel free to use it in your projects without attribution.

https://bitbucket.org/alfish/workspace/snippets/yXr856/referencing-sceneasset-by-guid-at-runtime

It works by storing the GUID of the SceneAsset in a struct, so you can move, rename and change build indices freely. The property drawer displays it as a Scene object in the editor, so you can drag-drop, etc, as normal.
A build processor is used to include a pre-loaded asset with all GUID to build-index mappings in the build, so these GUIDs are accessible at runtime.

The code is in root namespace, but you can easily change it if you want. I suggest using a local or embedded package to include it in your projects.

Example usage in a MonoBehaviour/ScriptableObject field:

var loading = SceneManager.LoadSceneAsync(sceneToLoad.sceneIndex);```

Great solution alfish! It’s the best I could find googling this. Would it be alright if you could write the code in these forums in case the link dies in the future? I also suggest making a github repo, so us mere mortals can star or fork it to show appreciation. :wink:

Also, I added a constructor

public SceneReference(Guid sceneGuid)
{
    this.sceneGuid = sceneGuid;
}

to the SceneReference struct, as it was previously not possible to generate it through code. Might be worth considering adding.

EDIT: You might prefer the registry version instead of git url. See my comment below .

I’ve updated the snippet, and also made it into a Unity Package. The code for the pkg is in my git repo here.
You can add it in Unity Package Manager using this Git URL (should auto-update with the main branch):
https://bitbucket.org/alfish/com.unity_x.modules.sceneref.git
Or, you can also download v0.1.0 as a standalone tgz file and add it locally (no updates). I’m also adding a backup in this comment as attachment (.tgz isn’t supported in forums directly, so I wrapped it in a .zip).

Note that the snippet version is in root namespace, but the package version is in UnityEngine namespace and it goes into a separate UnityEngineX.SceneRef.dll. I opted for just leaving it at that namespace, so it feels as close as possible to a native implementation (and I don’t really want to use a namespace of my own for this). If Unity ever decides to to use the same name (SceneReference(s)), they’d probably be implementing this feature, so in that case you’d probably want to switch to that anyways. If you don’t like this, you can, of course, change it as you prefer, and/or clone or fork the repo, etc. Or just use the snippet version.

7243364–872402–com.unity_x.modules.sceneref-0.1.0.tgz.zip (4.04 KB)

You sir have transcended the realm of gods. Thank you for the convenient Git URL and package :stuck_out_tongue:

I’m curious, do you expect to make any changes to the functionality in the future?

As of now, no (I can’t think of any related functionality worth implementing anyways), so don’t expect updates, unless a Unity update breaks it or something. Major version is zero because I didn’t do any extensive testing.

However, there’s a minor issue that is happening in my machine (2020.3.6f1, Linux x64) where the array of preloaded assets sometimes is getting added with null entries on builds. I’m pretty sure it’s NOT caused by my code, but I can’t be 100% sure with the few tests I did. This looks like a Unity bug. If you guys are not having that happen, but it happens with my code, please let me know, because I can add code to remove all null entries on the post build hook (but I didn’t want to “fix” this issue if it has nothing to do with my code).

Also, note that I did no testing at all with “subscenes” (it seems this is a thing when you install some package related to ECS). I don’t even know how that works, so if you’re using that, you have to test it on your own to see if it works or not.

For unrelated reasons, I downgraded to 2019.4 to test something. I noticed that the code in SceneReference.cs and SceneReferenceBuildProcessor.cs use C# features which are unavailable in 2019. The offending pieces of C# features are the following two

public int sceneIndex => this.sceneGuid switch
        {
            var sceneGuid => Array.FindIndex(UnityEditor.EditorBuildSettings.scenes, s => s.guid.ToString() == sceneGuid)
        };
// Which I changed to
public int sceneIndex
        {
            get
            {
                var sceneGuid = this.sceneGuid;
                return Array.FindIndex(UnityEditor.EditorBuildSettings.scenes, s => s.guid.ToString() == sceneGuid);
            }
        }

And

if (PlayerSettings.GetPreloadedAssets() is {} preloaded) {
      int n = preloaded.Length - 1, i = n;
// Which I changed to
UnityEngine.Object[] preloaded = { };           
    if (PlayerSettings.GetPreloadedAssets() != null) {
      int n = preloaded.Length - 1, i = n;

I am not totally sure my changes are equivalent. However, unless the features cannot be replaced by older C# code, it may be worth considering making it compatible with 2019.

Yes it’s equivalent. I’ll incorporate those changes later and update.
EDIT: Oops, my bad. I guess I was was half-asleep when I read your post, @theforgot3n1 .
Actually, only first is equivalent. Second is a non-null check with var definition, so it means:

var preloaded = PlayerSettings.GetPreloadedAssets(); if (preloaded != null) {

I updated the package’s git repo. Changelog

Contrary to what I thought, it doesn’t seem like Unity notifies or check updates when using git URLs.
It seems you’d need to manually re-add the URL for it to update.

For this reason, I have published it on an open registry so that Unity can actually do updates.
Installation instructions are here. Once you add the registry, it will appear in the Package Manager window after a refresh.

7273855–878506–com.unity_x.modules.sceneref-0.1.1.tgz.zip (4.59 KB)
7273855–878509–com.unity_x.modules.sceneref-0.2.0.tgz.zip (5.29 KB)

You have to manually up the version number in the package JSON file before committing and it will allow you to update without doing that.

Edit: Read the package.JSON file, looks like you’re doing the right thing. Don’t believe that’s how it’s supposed to work.

Yeah, that makes the git URL method almost pointless. It’s merely a convenience.
See: https://docs.unity3d.com/2021.1/Documentation/Manual/upm-ui-update.html

It should at least notify updates when you click refresh, but it won’t even do that. You would have to watch the repo to know of updates, then do it manually.

So it’s better to just use a registry instead.

I have installed the package successfully and I have replaced SceneAsset with SceneReference in my scripts. My problem is that I had a Custom inspector with something like this ```
panelItem_SO.scene=(SceneAsset)EditorGUILayout.ObjectField(“Scene”, panelItem_SO.scene, typeof(SceneAsset), true);


This doesn't work for SceneReference though. I am new to inspector scripting, so do you have any idea how I could make it work ?

Firstly, which version are you using? You can check the SceneReferenceDrawer class at v0.1.1 to see how it’s handled. The code in v0.1.1 (linked snippet) is a lot simpler than v0.2.0 (latest package version), so you might prefer checking that version to understand editor scripting. The other, v0.2.0, is more complex because I do a trick to display “missing” references properly. In v0.1.x missing refs are displayed the same as “none”.

That said, bear in mind that SceneReference is not a UnityEngine.Object, it’s not even a reference type. It’s a struct, simply containing the GUID string that POINTS to the SceneAsset Object. So my guess is that you want to do something like:
EditorGUILayout.PropertyField("Scene", property, ...)
where property is a SerializedProperty on that scene field (a struct). That method will automatically call the drawer, which knows how to display the struct.
Otherwise (if you’re not using SerializedProperty but accessing the target object directly, which seems to be what you’re doing), you’d have to get scene guid field as a string and handle that yourself with AssetDatabase and stuff, like I’m doing in SceneReferenceDrawer (see linked snippet v0.1.1 code). Though the code is not too complex, I recommend the first method instead, if you can use it.

All of that is, of course, assuming your custom script needs to use SceneReference for runtime code in the first place. Depending on your use case, you could be fine with just using SceneAsset if it’s for editor-only code. But I assume that’s not your case, obviously.

I am using the latest version. I do not want to display the struct so the first option doesn’t work for me. To be more accurate, I want to call your CustomPropertyDrawer from mine. There must be a way to call it but I cant find it.

//Field on PanelItem_SO
...
[HideInInspector]
    public SceneReference scene;
...
//Code in PanelItem_SO_Editor

public override void OnInspectorGUI()
    {
        DrawDefaultInspector();

        panelItem_SO.itemType = (panelItemType)EditorGUILayout.EnumPopup("Item Type", panelItem_SO.itemType);
        switch (panelItem_SO.itemType)
        {
            case panelItemType.Scene:
                {
                   //Display here SceneReference -- Call Your  SceneReferenceDrawer for PanelItem_SO.scene
                    break;
                }
}
}

If I use

 case panelItemType.Scene:
                {
                    SerializedObject so = new SerializedObject(panelItem_SO);
                    SerializedProperty serializedProperty = so.FindProperty("scene");
                    EditorGUILayout.PropertyField(serializedProperty, true);
                    so.ApplyModifiedProperties();
                    break;
                }

Nothing is being draw

No, EditorGUILayout.PropertyField will call the custom property drawer, not the generic struct drawer.

I believe that’s because of HideInInspector. Don’t use that attribute if you’re custom-drawing the class. I believe that attribute will mark the property as “invisible”.

If I understood correctly what you’re trying to do, it seems you want to selectively show a field or not, based on another field’s value. So I think you want to do something like this:

using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(InspectedClass), true)]
public class InspectedClassEditor : UnityEditor.Editor
{
  public override void OnInspectorGUI() {
    // this var will be set when panelItem property is reached; this means that panelItem
    // field must be in InspectedClass BEFORE any other fields which depend on that value
    var itemType = default(PanelItemType);
 
    serializedObject.Update();
    var property = serializedObject.GetIterator();
    if (!property.NextVisible(true)) return;
    // root has children; now at first child property
    do {
      switch (property.propertyPath) {
        // property = child[i]
        case "m_Script": continue; // if you don't want to draw script field
        case nameof(InspectedClass.someField):
          // whenever someField is default drawer, use break (or no case for it)
          // else draw it here and use continue
          drawSomeField();
          continue;
        case nameof(InspectedClass.panelItem):
          // draw and set variable
          itemType = drawPanelItemAndGetValue(); // whatever is your implementation
          continue;
        case nameof(InspectedClass.scene):
          // if selected, use default drawer for SceneReference
          if (itemType == PanelItemType.Scene) break;
          else continue; // else, dont draw anything
        case nameof(InspectedClass.otherType):
          // if selected, use default drawer for that field
          if (itemType == PanelItemType.OtherType) break;
          else continue; // else, dont draw anything
        // case etc: etc
      }
      // use default drawer for any other field
      EditorGUILayout.PropertyField(property, property.isExpanded);
    } while (property.NextVisible(false));
    serializedObject.ApplyModifiedProperties();
  }
}

With some modification it worked perfectly. Thank you very much ! Your code was extremely useful!

Bro, my brain is numb. I have no idea how to use your package after downloading it? Is there a namespace or something. I’ve been trying to build an mmorpg and i have been running into issue after issue with mirror because it cant handle scene changes. so, I built masterscenemanager and loaded the sceneasset and using unity editor cant be used in the application so, i have no way of grabbing my scenesset build on runtime through standalone. I downloaded you package. whats the easiest way to use it? cause again, my brain is literally going numb from all this shit i’ve been dealing with, in the past week with issue after issue after issue. lol Make a youtube video. lol