Other ways of cross referencing between scenes?

In this lecture we used an enum to cross reference scenes. What else could we have used?

We could have used a navmesh offmesh link between the scenes

Does that work between scenes? I thought it was for between two parts of a NavMesh?

Well apparently it can, https://docs.unity3d.com/Manual/nav-AdditiveLoading.html however, how effectivre it would be in loading additive, making the leap, removing the other… is another story, but I kinda like the idea. (I do confess I havent tried it, but this seems such a simple answer)

Definitely worth some further investigation. Does seem to require additive loading however.

If I understand this lecture correctly, we’re using the destination enum as a source ID as well - i.e. if the destination of a Portal in scene 1 is set to “B” then we’re also considering this to be the “B” destination from a Portal in scene 2. I.e. B links in both direction to B, C links in both directions to C, etc. If this is the case, maybe the field should not be named “destination” but “identifier” or “id” or “link” instead?

I can foresee this causing problems when more scenes are added that already use the identifiers we want to use. For example we might have two sets of scenes, one set only has the E identifier available, and the other set only has the A identifier. In this case we’d have to go through an entire set and “refactor” the linkages so that the same enum is available, or just create a new enum value I suppose.

Would a more flexible approach be to have both an identifier and a destination enum per portal, so that any portal in any scene can be connected to any other portal in any other scene without clashing?

Alternatively, allow arbitrary integer IDs (rather than enums) to create the links. I found this to work nicely, and it’s simpler than using two IDs per portal. Essentially the tuple (destSceneIndex, linkID) represents a unique portal-to-portal connection in one direction, and the opposite connection can be found with (originalSceneIndex, linkID).

What many students have done is to add two enums… a Destination and a Source…
So Scene 1 could have a Portal with Source A and Destination B, and would link to the portal Source B in Scene 2, which may link back to another Portal in Scene 1 Source C, etc.

That would work. I didn’t envisage needing this asymmetry in our game. I do agree that the naming is a little confusing. But I’m never going to be able to find the perfect names.

I am personally allergic to use identifiers that needs to be manually changed/updated. Even more so when it comes to things that are to be reusable over and over again. Using an enum is a bit limiting, but having to check the spelling of a name is, in a way, even worse.

So instead of either of those, I opted for a custom inspector which allows me to choose any world (added in the build) and select a portal from it:

image

Each Portal is then responsible for keeping track of its own name as well as what scene it should teleport the player to with (using the SceneReference package found here Unofficial Unity Package Manager Registry) and the name of the portal. When a portal is selected, the inspector gives a drop-down menu showing all available worlds and portals.

It does, of course, come with a few downsides; the most notable being if a portal changes its name. When this happens, one has to manually go through and edit all portals connecting to this portal so that the name matches again. The code for the Portal and the custom inspector can be found below.

PortalBetweenWorlds.cs

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;

namespace RPG.SceneHandling
{
    [Serializable]
    public class PortalBetweenWorlds : MonoBehaviour
    {
        [Serializable]
        public struct WorldTarget
        {
            public SceneReference world;
            public string portalName;
        }

        [SerializeField] private Transform spawnPoint;
        [SerializeField] private string portalName;

        [SerializeField, HideInInspector] private WorldTarget otherWorldTarget;

        private void OnTriggerEnter(Collider other)
        {
            if (other.CompareTag("Player"))
            {
                StartCoroutine(SceneTransition());
            }
        }

        private IEnumerator SceneTransition()
        {
            DontDestroyOnLoad(this);

            yield return SceneManager.LoadSceneAsync(otherWorldTarget.world.sceneIndex);

            PortalBetweenWorlds otherPortal = FindPortalInWorld();
            if (otherPortal != null)
                UpdatePlayer(otherPortal);

            Destroy(gameObject);
        }

        private void UpdatePlayer(PortalBetweenWorlds otherPortal)
        {
            GameObject player = GameObject.FindWithTag("Player");

            player.GetComponent<NavMeshAgent>().Warp(otherPortal.spawnPoint.position);
            player.transform.rotation = otherPortal.spawnPoint.rotation;
        }

        private PortalBetweenWorlds FindPortalInWorld()
        {
            PortalBetweenWorlds[] portalsInWorld = FindObjectsOfType<PortalBetweenWorlds>();

            foreach (PortalBetweenWorlds portal in portalsInWorld)
            {
                if (portal == this)
                    continue;

                if (portal.GetPortalName() == otherWorldTarget.portalName)
                    return portal;
            }

            return null;
        }

        public string GetPortalName()
        {
            return portalName;
        }

        public void SetPortalTarget(WorldTarget target)
        {
            otherWorldTarget = target;
        }

        public WorldTarget GetPortalTarget()
        {
            return otherWorldTarget;
        }
    }
}

PortalBetweenWorldsEditor.cs

#if UNITY_EDITOR

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace RPG.SceneHandling
{
    [CustomEditor(typeof(PortalBetweenWorlds))]
    public class PortalBetweenWorldsEditor : Editor
    {
        PortalBetweenWorlds myTarget;

        private EditorBuildSettingsScene[] scenes;
        private Dictionary<SceneReference, List<string>> availablePortals;
        private Dictionary<string, SceneReference> availableWorlds;

        private int worldChoice;
        private int portalChoice;

        private void OnEnable()
        {
            myTarget = (PortalBetweenWorlds) target;

            if (!Application.isEditor || Application.isPlaying || myTarget.gameObject.scene.name == null)
                return;

            UpdatePortalList();

            PortalBetweenWorlds.WorldTarget worldTarget = myTarget.GetPortalTarget();

            worldChoice = Math.Max(GetWorldIndexFromWorld(worldTarget.world), 0);

            portalChoice = Math.Max(GetPortalIndexFromPortalName(worldTarget.world, worldTarget.portalName), 0);
        }

        public void UpdatePortalList()
        {
            availableWorlds = new Dictionary<string, SceneReference>();
            availablePortals = new Dictionary<SceneReference, List<string>>();
            scenes = EditorBuildSettings.scenes;

            Scene currentScene = EditorSceneManager.GetActiveScene();

            foreach (EditorBuildSettingsScene scene in scenes)
            {
                if (currentScene.path != scene.path)
                {
                    Scene openedScene = EditorSceneManager.OpenScene(scene.path, OpenSceneMode.Additive);
                    InsertPortalsFromWorld(openedScene);
                }
                else
                {
                    InsertPortalsFromWorld(currentScene);
                }
            }
        }

        public void InsertPortalsFromWorld(Scene openedScene)
        {
            Scene currentScene = EditorSceneManager.GetActiveScene();
            Scene sceneAsset = SceneManager.GetSceneByPath(openedScene.path);

            SceneReference reference =
                new SceneReference(Array.Find(scenes, scene => scene.path == openedScene.path).guid.ToString());
            List<string> portalsAvailableInWorld = new List<string>();

            GameObject[] gameObjectsAtRoot;

            gameObjectsAtRoot = openedScene.GetRootGameObjects();
            foreach (GameObject gameObject in gameObjectsAtRoot)
            {
                PortalBetweenWorlds[] availablePortals = gameObject.GetComponentsInChildren<PortalBetweenWorlds>();
                foreach (PortalBetweenWorlds portal in availablePortals)
                {
                    if (portalsAvailableInWorld.Contains(portal.GetPortalName()))
                        continue;
                    portalsAvailableInWorld.Add(portal.GetPortalName());
                }
            }

            availableWorlds.Add(sceneAsset.name, reference);

            if (currentScene != sceneAsset)
                EditorSceneManager.CloseScene(sceneAsset, true);

            if (portalsAvailableInWorld.Count == 0)
                return;

            availablePortals[reference] = portalsAvailableInWorld;
        }

        public string[] GetWorlds()
        {
            return availableWorlds.Keys.ToArray();
        }

        public string[] GetPortalsInWorld(string worldName)
        {
            return availablePortals[availableWorlds[worldName]].ToArray();
        }

        public PortalBetweenWorlds.WorldTarget CreateTarget(string worldName, string portalName)
        {
            SceneReference world = availableWorlds[worldName];
            PortalBetweenWorlds.WorldTarget target;
            target.world = world;
            target.portalName = portalName;

            return target;
        }

        public int GetWorldIndexFromWorld(SceneReference world)
        {
            return Array.IndexOf(availableWorlds.Values.ToArray(), world);
        }

        public int GetPortalIndexFromPortalName(SceneReference world, string name)
        {
            return Array.IndexOf(availablePortals[world].ToArray(), name);
        }

        public override void OnInspectorGUI()
        {
            DrawDefaultInspector();

            if (Application.isPlaying)
            {
                PortalBetweenWorlds.WorldTarget portalTarget = myTarget.GetPortalTarget();

                string scenePath = EditorBuildSettings.scenes[portalTarget.world.sceneIndex].path;

                string worldName = scenePath.Substring(scenePath.LastIndexOf('/') + 1);
                worldName = worldName.Substring(0, worldName.LastIndexOf('.'));

                EditorGUILayout.LabelField(
                    $"Leads to the portal named \"{portalTarget.portalName}\" in world \"{worldName}\"");

                return;
            }

            if (myTarget.gameObject.scene.name == null)
            {
                EditorGUILayout.LabelField("Cannot modify the portal's destination inside a prefab!");
                return;
            }

            EditorGUILayout.Space();


            string[] worlds = GetWorlds();
            if (worlds.Length == 0)
            {
                EditorGUILayout.HelpBox("No worlds exist. Have you added them to the build settings?",
                    MessageType.Error);
                return;
            }

            EditorGUILayout.LabelField(new GUIContent("Worlds available in build settings: ",
                "Can't find what you're looking for? Make sure the world is in the build settings!"));
            worldChoice = EditorGUILayout.Popup(worldChoice, worlds);


            EditorGUILayout.LabelField("Portals available in selected world: ");
            string[] portals = GetPortalsInWorld(worlds[worldChoice]);
            if (portals.Length == 0)
            {
                EditorGUILayout.HelpBox("No portals found in the chosen world.", MessageType.Error);
                return;
            }

            portalChoice = EditorGUILayout.Popup(portalChoice, portals);

            myTarget.SetPortalTarget(CreateTarget(worlds[worldChoice], portals[portalChoice]));
            if (GUI.changed)
            {
                EditorUtility.SetDirty(myTarget);
                EditorSceneManager.MarkSceneDirty(myTarget.gameObject.scene);
            }
        }
    }
}
#endif

Edit: Found a few errors in how the custom inspector handled showing portal information in play mode and when attempted to edit destination from a prefab.

1 Like

I feel the same. I also hate having to figure out what index something is and making sure that I have the correct index. I did something similar but I used attributes instead of creating a custom inspector. I am using attributes so that I do not have to write custom inspectors every time that I want to do scene selection or portal selection.

Portal Selection Example

Portal.cs

namespace RPG.SceneManagement
{
    [System.Serializable]
    public struct DestinationPortal
    {
        [Scene] public int scene;
        [PortalIndex(scene = "scene")] public int portal;
    }

    public class Portal : MonoBehaviour
    {
        [SerializeField] private int portalIndex = 0;
        [SerializeField] private Transform spawnPoint;
        [SerializeField] private DestinationPortal destination;
    }
}

SceneAttribute.cs

using UnityEngine;

namespace RPG.SceneManagement
{
    /// <summary>
    /// Displays a dropdown list of available build settings Scenes (must be used with a 'string' or 'integer' typed field).
    /// <example>
    /// <code>
    /// [Scene] public string sceneString;
    /// [Scene] public int sceneInt;
    /// </code>
    /// </example>
    /// </summary>
    [System.AttributeUsage(System.AttributeTargets.Field)]
    public class SceneAttribute : PropertyAttribute { }
}

PortalIndexAttribute.cs

using UnityEngine;

namespace RPG.SceneManagement
{
    /// <summary>
    /// Displays a dropdown list of available portals (must be used with a 'integer' typed field).
    /// Must have an integer field for the index of the scene to select a portal from.
    /// <example>
    /// <code>
    /// [Scene] public int sceneInt;
    /// [PortalIndex(PortalIndex(scene = "sceneInt"))] public int portalIndex;
    /// </code>
    /// </example>
    /// </summary>
    [System.AttributeUsage(System.AttributeTargets.Field)]
    public class PortalIndexAttribute : PropertyAttribute
    {
        /// <summary>
        /// The name of the serialized Scene property to use.
        /// Must be an integer.
        /// </summary>
        public string scene;
    }
}

In Editor Folders

PropertyDrawerHelper.cs

using UnityEditor;

namespace RPG.Core.Editor
{
    /// <summary>
    /// Collection of helper methods when coding a PropertyDrawer editor.
    /// </summary>
    public static class PropertyDrawerHelper
    {
        /// <summary>
        /// Finds the property using the specified property
        /// </summary>
        /// <param name="property">The property to look in for the property name.</param>
        /// <param name="propertyName">The property name</param>
        /// <param name="errorMessage">The error message</param>
        /// <returns>The serialized property</returns>
        public static SerializedProperty FindProperty(SerializedProperty property, string propertyName,
                                                      out string errorMessage)
        {
            SerializedProperty prop = property.serializedObject.FindProperty(propertyName);
            errorMessage = string.Empty;
            if (prop != null) return prop;
            string propPath =
                property.propertyPath.Substring(
                    0, property.propertyPath.IndexOf($".{property.name}", System.StringComparison.Ordinal));
            prop = property.serializedObject.FindProperty($"{propPath}.{propertyName}");
            if (prop != null) return prop;
            errorMessage = $"The Field name {propertyName} cannot be found in {propPath}";
            return null;
        }
    }
}

SceneAttributePropertyDrawer.cs

using System.Linq;
using System.Text.RegularExpressions;
using Unity.Mathematics;
using UnityEditor;
using UnityEngine;

namespace RPG.SceneManagement.Editor
{
    /// <summary>
    /// The scene attribute property drawer class
    /// </summary>
    /// <seealso cref="PropertyDrawer"/>
    [CustomPropertyDrawer(typeof(SceneAttribute))]
    public class SceneAttributePropertyDrawer : PropertyDrawer
    {
        /// <summary>
        /// Gets the value of the any scene in build settings
        /// </summary>
        private static bool AnySceneInBuildSettings => GetScenes()?.Length > 0;

        /// <summary>
        /// Gets the scenes
        /// </summary>
        /// <returns>The string array</returns>
        private static string[] GetScenes()
        {
            return (from scene in EditorBuildSettings.scenes
                    where scene.enabled
                    select scene.path).ToArray();
        }

        /// <summary>
        /// Gets the scene options using the specified scenes.
        /// Uses Regex to remove the path from the scene name.
        /// </summary>
        /// <param name="scenes">The scenes</param>
        /// <returns>The string array</returns>
        private static string[] GetSceneOptions(string[] scenes)
        {
            return (from scene in scenes
                    select Regex.Match(scene ?? string.Empty,
                                       @".+\/(.+).unity").Groups[1].Value).ToArray();
        }

        #region Overrides of PropertyDrawer

        /// <inheritdoc />
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            if (property == null) return;

            if (!(property.propertyType == SerializedPropertyType.String ||
                  property.propertyType == SerializedPropertyType.Integer))
            {
                Debug.LogError($"{nameof(SceneAttribute)} supports only string and int fields");
                label.text = $"{nameof(SceneAttribute)} supports only string fields and int fields";
                EditorGUI.PropertyField(position, property, label);
                return;
            }

            if (!AnySceneInBuildSettings)
            {
                EditorGUI.HelpBox(
                    position,
                    "No scenes in the build settings, Please ensure that you add your scenes to File->Build Settings->",
                    MessageType.Error);
                return;
            }

            string[] scenes = GetScenes();
            string[] sceneOptions = GetSceneOptions(scenes);

            using (new EditorGUI.PropertyScope(position, label, property))
            {
                using (EditorGUI.ChangeCheckScope changeCheck = new EditorGUI.ChangeCheckScope())
                {
                    switch (property.propertyType)
                    {
                        case SerializedPropertyType.String:
                            DrawPropertyForString(position, property, label, scenes, sceneOptions);
                            break;
                        case SerializedPropertyType.Integer:
                            DrawPropertyForInt(position, property, label, sceneOptions);
                            break;
                        default:
                            Debug.LogError(
                                $"{nameof(SceneAttribute)} supports only int or string fields! {property.propertyType}");
                            break;
                    }

                    if (changeCheck.changed)
                    {
                        property.serializedObject?.ApplyModifiedProperties();
                    }
                }
            }
        }

        #endregion

        #region Drawer Methods

        /// <summary>
        /// Draws the property for string using the specified rect
        /// </summary>
        /// <param name="rect">The rect</param>
        /// <param name="property">The property</param>
        /// <param name="label">The label</param>
        /// <param name="scenes">The scenes</param>
        /// <param name="sceneOptions">The scene options</param>
        private static void DrawPropertyForString(Rect rect, SerializedProperty property, GUIContent label,
                                                  string[] scenes, string[] sceneOptions)
        {
            if (property == null) return;
            if (scenes == null) return;

            int index = math.clamp(System.Array.IndexOf(scenes, property.stringValue), 0, scenes.Length - 1);
            int newIndex = EditorGUI.Popup(rect, label != null ? label.text : "", index, sceneOptions);
            string newScene = scenes[newIndex];

            if (property.stringValue?.Equals(newScene, System.StringComparison.Ordinal) == false)
            {
                property.stringValue = scenes[newIndex];
            }
        }

        /// <summary>
        /// Draws the property for int using the specified rect
        /// </summary>
        /// <param name="rect">The rect</param>
        /// <param name="property">The property</param>
        /// <param name="label">The label</param>
        /// <param name="sceneOptions">The scene options</param>
        private static void DrawPropertyForInt(Rect rect, SerializedProperty property, GUIContent label,
                                               string[] sceneOptions)
        {
            if (property == null) return;

            int index = property.intValue;
            int newIndex = EditorGUI.Popup(rect, label != null ? label.text : "", index, sceneOptions);

            if (property.intValue != newIndex)
            {
                property.intValue = newIndex;
            }
        }

        #endregion
    }
}

PortalIndexAttributePropertyDrawer.cs

using System.Collections.Generic;
using System.Linq;
using RPG.Core.Editor;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace RPG.SceneManagement.Editor
{
    /// <summary>
    /// The portal index attribute property drawer class
    /// </summary>
    /// <seealso cref="PropertyDrawer"/>
    [CustomPropertyDrawer(typeof(PortalIndexAttribute))]
    public class PortalIndexAttributePropertyDrawer : PropertyDrawer
    {
        /// <summary>
        /// The errormessage
        /// </summary>
        private string m_errorMessage;

        #region Overrides of PropertyDrawer

        /// <inheritdoc />
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            if (property == null) return;

            if (property.propertyType != SerializedPropertyType.Integer)
            {
                m_errorMessage = $"{nameof(PortalIndexAttribute)} can only be used on integer properties.";
                Debug.LogError(m_errorMessage);
                EditorGUI.LabelField(position, label.text, m_errorMessage);
                return;
            }

            PortalIndexAttribute attr = attribute as PortalIndexAttribute;

            SerializedProperty sceneProp =
                PropertyDrawerHelper.FindProperty(property, attr.scene, out m_errorMessage);
            if (sceneProp == null)
            {
                Debug.LogError(m_errorMessage);
                EditorGUI.LabelField(position, label.text, m_errorMessage);
                return;
            }

            if (sceneProp.propertyType != SerializedPropertyType.Integer)
            {
                m_errorMessage = $"{nameof(sceneProp)} must be an int field.";
                Debug.LogError(m_errorMessage);
                EditorGUI.LabelField(position, label.text, m_errorMessage);
                return;
            }

            string[] portalNames = PortalNames(sceneProp);

            using (new EditorGUI.PropertyScope(position, label, property))
            {
                using (EditorGUI.ChangeCheckScope changeCheck = new EditorGUI.ChangeCheckScope())
                {
                    DrawPortalPopup(position, property, label, portalNames);

                    if (changeCheck.changed)
                    {
                        property.serializedObject?.ApplyModifiedProperties();
                    }
                }
            }
        }

        #endregion

        private static string[] PortalNames(SerializedProperty sceneProp)
        {
            EditorBuildSettingsScene editorScene = EditorBuildSettings.scenes[sceneProp.intValue];
            Scene scene = EditorSceneManager.GetActiveScene().buildIndex != sceneProp.intValue
                ? EditorSceneManager.OpenScene(editorScene.path, OpenSceneMode.Additive)
                : EditorSceneManager.GetActiveScene();

            GameObject[] sceneObjects = scene.GetRootGameObjects();
            List<Portal> portalObjects = new List<Portal>();
            foreach (GameObject sceneObject in sceneObjects)
            {
                Portal[] portals = sceneObject.GetComponentsInChildren<Portal>();
                foreach (Portal portal in portals)
                {
                    portalObjects.Add(portal);
                }
            }

            int portalIndex = 0;
            foreach (Portal portal in portalObjects.OrderBy(a => a.Index))
            {
                if (portal.Index != portalIndex) portal.Index = portalIndex;
                portalIndex++;
            }

            string[] portalNames = portalObjects.Select(a => a.name).ToArray();
            if (scene != EditorSceneManager.GetActiveScene()) EditorSceneManager.CloseScene(scene, true);
            return portalNames;
        }

        /// <summary>
        /// Draws a popup selection for the portal index by the name of the portal.
        /// </summary>
        /// <param name="rect">The rect</param>
        /// <param name="property">The property</param>
        /// <param name="label">The label</param>
        /// <param name="portalOptions">The portal options</param>
        private static void DrawPortalPopup(Rect rect, SerializedProperty property, GUIContent label,
                                            string[] portalOptions)
        {
            if (property == null) return;

            int index = property.intValue;
            int newIndex = EditorGUI.Popup(rect, label != null ? label.text : "", index, portalOptions);

            if (property.intValue != newIndex)
            {
                property.intValue = newIndex;
            }
        }
    }
}

Edit: Found a bug when in Play Mode and inspecting a portal. InvalidOperationException: This cannot be used during play mode, please use SceneManager.LoadScene()/SceneManager.LoadSceneAsync() instead. To fix this I draw the base GUI if I am in Play Mode and just exit.

        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            if (Application.isPlaying)
            {
                // ReSharper disable once Unity.PropertyDrawerOnGUIBase
                base.OnGUI(position, property, label);
                return;
            }
1 Like

Privacy & Terms