- Home /
Inspector Field for Scene Asset
Pretty straightforward question: is there a way to create an inspector field for either a Scene asset or an array of Scene assets?
I'm trying to create a custom editor window that allows our designers to easily modify our level packs and the levels in them. It would be ideal if, rather than having to add scenes manually to the Build Settings list, I could provide an inspector field to slot in a Scene (.unity3d) asset. This way, I could throw warnings when a level isn't assigned a scene file, and ensure that all the scenes needed for the game are included when we do a build.
So far, this seems much harder than necessary because I can't find an appropriate asset type that corresponds to the scene asset type.
Thanks!
Answer by kromenak · Apr 24, 2012 at 05:18 PM
Found a workable solution through experimentation, thought I would share:
There is no class exposed in Unity's API to create an inspector reference to a Scene, but it turns out that a Scene does inherit from Object, so it is possible to have an Object reference and then slot in a Scene asset:
public Object[] mySceneAssets;
The only problem with this solution is that you can't really enforce that the list contain only Scene objects at compile time; there is nothing stopping someone on the team from inserting a reference to some other asset entirely. Still, it is better than nothing, and you can do some runtime checks to make sure it works.
With the scene asset reference, you can use the AssetDatabase object to get path information, and manually add the scene data to the build settings list of scenes.
List<EditorBuilderSettingsScene> scenes = new List<EditorBuilderSettingsScene>();
//have some sort of for loop to cycle through scenes...
string pathToScene = AssetDatabase.GetAssetPath(mySceneAssets[i]);
EditorBuildSettingsScene scene = new EditorBuildSettingsScene(pathToScene, true);
scenes.Add(scene);
//later on...
EditorBuildSettings.scenes = scenes.ToArray();
The scene object references become null for me in builds.
@JGriffith, yeah, for whatever reason, Scenes are editor-only assets, so they disappear when you actually create a build.
However, that doesn't have to be a problem, necessarily. During your build process, you can still access the scene objects and do some processing on them. When building your final build, you can add scenes to to the BuildSettings scene list (like above) or save the name of the scene to a string array (which will be available at runtime).
But as you say, when you try to run a final build file, these scene references won't be available. You should probably just consider them to be "editor only".
Answer by glitchers · Jun 17, 2016 at 10:10 AM
I have come across this issue a few times and I don't want to make a custom inspector for every class I need a scene reference I need so I have made this script.
You can just use it like public SceneField myScene
and drag a Scene to the Inspector and use Application.loadLevel( myScene )
or SceneManager.LoadScene( myScene )
and it will work. It will be serialized and work in builds. Include this script in your project and never worry about it again.
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
[System.Serializable]
public class SceneField
{
[SerializeField]
private Object m_SceneAsset;
[SerializeField]
private string m_SceneName = "";
public string SceneName
{
get { return m_SceneName; }
}
// makes it work with the existing Unity methods (LoadLevel/LoadScene)
public static implicit operator string( SceneField sceneField )
{
return sceneField.SceneName;
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(SceneField))]
public class SceneFieldPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect _position, SerializedProperty _property, GUIContent _label)
{
EditorGUI.BeginProperty(_position, GUIContent.none, _property);
SerializedProperty sceneAsset = _property.FindPropertyRelative("m_SceneAsset");
SerializedProperty sceneName = _property.FindPropertyRelative("m_SceneName");
_position = EditorGUI.PrefixLabel(_position, GUIUtility.GetControlID(FocusType.Passive), _label);
if (sceneAsset != null)
{
sceneAsset.objectReferenceValue = EditorGUI.ObjectField(_position, sceneAsset.objectReferenceValue, typeof(SceneAsset), false);
if( sceneAsset.objectReferenceValue != null )
{
sceneName.stringValue = (sceneAsset.objectReferenceValue as SceneAsset).name;
}
}
EditorGUI.EndProperty( );
}
}
#endif
Thanks for this it really helped us.
Just a slight addition:
if (sceneAsset != null)
{
EditorGUI.BeginChangeCheck();
Object value = EditorGUI.ObjectField(_position, sceneAsset.objectReferenceValue, typeof(SceneAsset), false);
if (EditorGUI.EndChangeCheck() )
{
sceneAsset.objectReferenceValue = value;
if (sceneAsset.objectReferenceValue != null)
{
sceneName.stringValue = (sceneAsset.objectReferenceValue as SceneAsset).name;
}
}
}
This allows for multi-editing (and stops multi-editing from causing issues).
How would you go about using your SceneField in a custom editor window ? I tried using ObjectField but I can't get it to work :(
Here is a slightly modified version, including @Halfbiscuit addition, but using scene paths ins$$anonymous$$d of names (since there are few scenes with same name are possible in the project):
using System;
using UnityEngine;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Utilities
{
[System.Serializable]
public class SceneField
{
[SerializeField] private Object sceneAsset;
[SerializeField] private string sceneName = "";
public string SceneName
{
get { return sceneName; }
}
// makes it work with the existing Unity methods (LoadLevel/LoadScene)
public static implicit operator string(SceneField sceneField)
{
return sceneField.SceneName;
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(SceneField))]
public class SceneFieldPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, GUIContent.none, property);
var sceneAsset = property.FindPropertyRelative("sceneAsset");
var sceneName = property.FindPropertyRelative("sceneName");
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
if (sceneAsset != null)
{
EditorGUI.BeginChangeCheck();
var value = EditorGUI.ObjectField(position, sceneAsset.objectReferenceValue, typeof(SceneAsset), false);
if (EditorGUI.EndChangeCheck())
{
sceneAsset.objectReferenceValue = value;
if (sceneAsset.objectReferenceValue != null)
{
var scenePath = AssetDatabase.GetAssetPath(sceneAsset.objectReferenceValue);
var assetsIndex = scenePath.IndexOf("Assets", StringComparison.Ordinal) + 7;
var extensionIndex = scenePath.LastIndexOf(".unity", StringComparison.Ordinal);
scenePath = scenePath.Substring(assetsIndex, extensionIndex - assetsIndex);
sceneName.stringValue = scenePath;
}
}
}
EditorGUI.EndProperty();
}
}
#endif
}
Thanks mate! Since you packed up everything that's good on this page, your code is the most functional :D
FYI this basically uses a backing field and serializes/stores the string name for later use so if you change the name of the SceneAsset file then it will not update the SceneField name string, thus breaking it.
So if you use this make sure you either don't rename your scenes after hooking them into SceneFields, or prepare to update the fields by drag-n-dropping them again to refresh the serialized backer string.
I used a similar approach, but with ISerializationCallbackReceiver to guarantee that the string path is valid: https://gist.github.com/Johannes$$anonymous$$P/ec7d3f0bcf167dab3d0d3bb480e0e07b
I Including a more designer-friendly PropertyDrawer
that provides at-a-glance build status and some convenient actions (with destructive action confirmation dialogues), and made sure that it respects version control checkout status of EditorBuildSettings.Asset
, such as when using Perforce:
When in Editor, only the Object reference is ever used, and only during serialization is the asset path string set, guaranteeing that they will never be out of sync. This means this approach fully supports moving/rena$$anonymous$$g scene assets.
The only time the path is ever used in-editor is if the object reference is invalid during serialization (such as after a failed scene/prefab merge), in which case the Object reference is recovered from the path if it actually references a SceneAsset. For builds only the path is stored, and is guaranteed to have been serialized from the Object reference.
Thanks, @Johannes$$anonymous$$P, like your solution a lot!
Answer by JohannesMP · Aug 13, 2018 at 10:36 PM
Just storing a path string (as the SceneAsset documentation suggests) is inadequate for production, because if the scene file is renamed or moved you've lost your reference. We can use an Object reference to the SceneAsset but we no longer have access to AssetDatabase to look up the path when not in editor.
To reliably handle both file renames and runtime paths you need two serialized pieces of data (Object reference in editor, string path at runtime). You can create a SceneReference object that uses ISerializationCallbackReceiver to ensure that the stored path is always valid based on the specified SceneAsset Object.
As an example, here is my approach to this problem: https://gist.github.com/JohannesMP/ec7d3f0bcf167dab3d0d3bb480e0e07b
Features
Custom PropertyDrawer that displays the current Build Settings status, including BuildIndex and convenient buttons for managing it with destructive action confirmation dialogues.
If (and only if) the serialized Object reference is invalid but path is still valid (for example if someone merged incorrectly) will recover object using path.
Buttons collapse to smaller text if full text cannot be displayed.
Includes detailed tooltips and respects Version Control if build settings is not checked out (tested with Perforce)
Answer by FriesB_DAEStudios · Jun 25, 2014 at 12:42 PM
UnityEngine.SceneAsset is not recognized by the compiler, but you can check if the Object.ToString() ends with "(UnityEngine.SceneAsset)". I used a workaround to create a level manager which creates the order in which levels will be played.
This is how I managed it:
public string LevelName;
private Object _levelScene;
public Object LevelScene
{
get {return _levelScene;}
set
{
//Only set when the value is changed
if(_levelScene != value && value != null)
{
string name = value.ToString();
if(name.Contains(" (UnityEngine.SceneAsset)"))
{
_levelScene = value;
LevelName = name.Substring(0,name.IndexOf(" (UnityEngine.SceneAsset)"));
}
}
}
}
And in editor window:
LevelScene = EditorGUILayout.ObjectField("Scene",LevelScene,typeof(Object),false);
Later, I can load the correct level using the LevelName.
Answer by 1337GameDev · Apr 20, 2012 at 11:39 PM
I'm assuming you could make a script or such and have a public variable so it can be assigned in the editor window. I am unsure ifna similar thing applies to asset packages.
I would also assume this, but I'm having trouble finding the class to use in this instance.
To make a transform field, I use a "public Transform myTransform;" field.
To make an AudioClip field, I use a "public AudioClip myClip;" field.
To make a scene file field, I have no idea what class I should be using to create the inspector field. I'm fearing that there is no such class exposed.
I am also looking for the answer to this. Are there any updates on this. And a related question. If Application.LoadLevel("aLevel") does not take any paths, how does it avoid name clashes if you have levels with the same name in different folders?
thanks
It is stated above that scenes inherit from Object. You could try using this in a public variable.
For the scene name problem, unity might grab the first name it finds (or random) so avoid sharing names of scenes. It's similar to Java's arraylist object and the find method. It returns the first occurrence. If the arraylist is unwonted you don't know which is returned. Same principle here. Just name them differently, and come up with a na$$anonymous$$g convention for your scene assets.
Thanks GameDev. I get the part about using an Object. But then how in the world do I load that object as a scene at runtime?. I can use Application.LoadLevel() which takes a string. And the only way I see of getting the LevelName from the Object is to call AssetDatabase.GetAssetPath which is an Editor class. So it will not work at runtime. This can't possibly be this hard :(
You can probably achieve what you're looking for by creating a custom inspector. In the custom inspector, you can trim and save the scene names for use at runtime. For example, something like this seems to work:
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(YourClass))]
public class YourCustomInspector : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
YourClass comp = (YourClass)target;
if(comp != null)
{
if(comp.scenes.Length > 0)
{
comp.sceneNames = new string[comp.scenes.Length];
for(int i = 0; i < comp.scenes.Length; i++)
{
string scenePath = AssetDatabase.GetAssetPath(comp.scenes[i]);
scenePath = scenePath.Substring(scenePath.LastIndexOf('/') + 1);
scenePath = scenePath.Substring(0, scenePath.LastIndexOf('.'));
comp.sceneNames[i] = scenePath;
}
}
}
}
}
Since this editor class creates an array of scene names for you, you can then use those scene names at runtime to load the scenes. I think this is kind of a dumb solution, but I don't see a better way to do it, unfortunately :-\
Your answer
Follow this Question
Related Questions
How do I change inspector values of other game objects using a custom Editor Window 0 Answers
Set Level inquiry/data from the inspector 1 Answer
How do I change inspector values of other game objects using a custom Editor Window 0 Answers
CustomEditor : DrawDefaultInspector for Class 1 Answer
Moving a inspector slider to add objects to the scene 1 Answer