Better scene workflows with Scriptable Objects

July 1, 2020 Amel Negra

Managing multiple Scenes in Unity can be a challenge, and improving this workflow is crucial for both the performance of your game and the productivity of your team. Here, we share some tips for setting up your Scene workflows in ways that scale for bigger projects.

Most games involve multiple levels, and levels often contain more than one Scene. In games where Scenes are relatively small, you can break them into different sections using Prefabs. However, to enable or instantiate them during the game you need to reference all these Prefabs. That means that as your game gets bigger and as those references take up more space in memory, it becomes more efficient to use Scenes.

You can break down your levels into one or multiple Unity Scenes. Finding the optimal way to manage them all becomes key. You can open multiple Scenes in the Editor and at runtime using Multi-Scene editing. Splitting levels into multiple Scenes also has the advantage of making teamwork easier as it avoids merge conflicts in collaboration tools such as Git, SVN, Unity Collaborate and the like.

Managing multiple Scenes to compose a level

In the video below, we show how to load a level more efficiently by breaking the game logic and the different parts of the level into several distinct Unity Scenes. Then, using Additive Scene-loading mode when loading these Scenes, we load and unload the needed parts alongside the game logic, which is persistent. We use Prefabs to act as “anchors” for the Scenes, which also offers a lot of flexibility when working in a team, as every Scene represents a part of the level and can be edited separately.

You can still load these Scenes while in Edit Mode and press Play at any time, so that you can visualize them all together when creating the level design.

We show two different methods to load those Scenes. The first one is distance-based, which is well suited for non-interior levels like an open world. This technique is also useful for some visual effects (like fog, for instance) to hide the loading and unloading process.

The second technique uses a Trigger to check which Scenes to load, which is more efficient when working with interiors.

Now that everything is managed inside the level, you can add a layer on top of it to better manage the levels.

Managing multiple levels within a game using ScriptableObjects

We want to keep track of the different Scenes for each level as well as all the levels during the entire duration of the gameplay. One possible way of doing this is to use static variables and the singleton pattern in your MonoBehaviour scripts, but there are a few problems with this solution. Using the singleton pattern allows rigid connections between your systems, so it is not strictly modular. The systems can’t exist separately and will always depend on each other.

Another issue involves the use of static variables. Since you can’t see them in the Inspector, you need to change the code to set them, making it harder for artists or level designers to test the game easily. When you need data to be shared between the different Scenes, you use static variables combined with DontDestroyOnLoad, but the latter should be avoided whenever it is possible.

To store information about the different Scenes, you can use ScriptableObject, which is a serializable class mainly used to store data. Unlike MonoBehaviour scripts, which are used as components attached to GameObjects, ScriptableObjects are not attached to any GameObjects and thus can be shared between the different Scenes of the whole project.

You want to be able to use this structure for levels but also for menu Scenes in your game. To do so, create a GameScene class that contains the different common properties between levels and menus.

			
		public class GameScene : ScriptableObject
		{
		    [Header("Information")]
		    public string sceneName;
		    public string shortDescription;

		    [Header("Sounds")]
		    public AudioClip music;
		    [Range(0.0f, 1.0f)]
		    public float musicVolume;

		    [Header("Visuals")]
		    public PostProcessProfile postprocess;
		}
    

Notice that the class inherits from ScriptableObject and not MonoBehaviour. You can add as many properties as you need for your game. After this step, you can create Level and Menu classes that both inherit from the GameScene class that was just created – so they are also ScriptableObjects.

			
		[CreateAssetMenu(fileName = "NewLevel", menuName = "Scene Data/Level")]
		public class Level : GameScene
		{
		    //Settings specific to level only
		    [Header("Level specific")]
		    public int enemiesCount;
		}
    

Adding the CreateAssetMenu attribute at the top lets you create a new level from the Assets menu in Unity. You can do the same for the Menu class. You can also include an enum to be able to choose the menu type from the Inspector.

			
		public enum Type
		{
		    Main_Menu,
		    Pause_Menu
		}

		[CreateAssetMenu(fileName = "NewMenu", menuName = "Scene Data/Menu")]
		public class Menu : GameScene
		{
		    //Settings specific to menu only
		    [Header("Menu specific")]
		    public Type type;
		}
    

Now that you can create levels and menus, let’s add a database that lists the levels and menus for easy reference. You can also add an index to track the current level of the player. Then, you can add methods to load a new game (in this case the first level will be loaded), to replay the current level, and for going to the next level. Note that only the index changes between these three methods, so you can create a method that loads the level with an index to use it multiple times.

			
		[CreateAssetMenu(fileName = "sceneDB", menuName = "Scene Data/Database")]
		public class ScenesData : ScriptableObject
		{
		    public List levels = new List();
		    public List menus = new List();
		    public int CurrentLevelIndex=1;

		    /*
		     * Levels
		     */

		    //Load a scene with a given index
		    public void LoadLevelWithIndex(int index)
		    {
		        if (index <= levels.Count)
		        {
		            //Load Gameplay scene for the level
		            SceneManager.LoadSceneAsync("Gameplay" + index.ToString());
		            //Load first part of the level in additive mode
		            SceneManager.LoadSceneAsync("Level" + index.ToString() + "Part1", LoadSceneMode.Additive);
		        }
		        //reset the index if we have no more levels
		        else CurrentLevelIndex =1;
		    }
		    //Start next level
		    public void NextLevel()
		    {
		        CurrentLevelIndex++;
		        LoadLevelWithIndex(CurrentLevelIndex);
		    }
		    //Restart current level
		    public void RestartLevel()
		    {
		        LoadLevelWithIndex(CurrentLevelIndex);
		    }
		    //New game, load level 1
		    public void NewGame()
		    {
		        LoadLevelWithIndex(1);
		    }
		   
		    /*
		     * Menus
		     */

		    //Load main Menu
		    public void LoadMainMenu()
		    {
		        SceneManager.LoadSceneAsync(menus[(int)Type.Main_Menu].sceneName);
		    }
		    //Load Pause Menu
		    public void LoadPauseMenu()
		    {
		        SceneManager.LoadSceneAsync(menus[(int)Type.Pause_Menu].sceneName);
		    }
    

There are also methods for the menus, and you can use the enum type that you created before to load the specific menu you want – just make sure that the order in the enum and the order in the list of menus is the same.

Now you can finally create a level, menu or database ScriptableObject from the Assets menu by right-clicking in the Project window.

From there, just keep adding the levels and menus you need, adjusting the settings, and then adding them to the Scenes database. The example below shows you what Level1, MainMenu and Scenes Data look like.

It’s time to call those methods. In this example, the Next Level Button on the user interface (UI) that appears when a player reaches the end of the level calls the NextLevel method. To attach the method to the button, click the plus button of the On Click event of the Button component to add a new event, then drag and drop the Scenes Data ScriptableObject into the object field and choose the NextLevel method from ScenesData, as shown below.

Now you can go through the same process for the other buttons – to replay the level or go to the main menu, and so on. You can also reference the ScriptableObject from any other script to access the different properties, like the AudioClip for the background music or the post-processing profile, and use them in the level.

Tips for error-proofing your processes

  • Minimizing loading/unloading

In the ScenePartLoader script shown in the video, you can see that a player can keep entering and leaving the collider multiple times, triggering the repeated loading and unloading of a Scene. To avoid this, you can add a coroutine before calling the loading and unloading methods of the Scene in the script, and stop the coroutine if the player leaves the trigger.

  • Naming conventions

Another general tip is to use solid naming conventions in the project. The team should agree beforehand on how to name the different types of assets – from scripts and Scenes to materials and other things in the project. This will make it easier not only for you but also for your teammates to work on the project and to maintain it. This is always a good idea, but it’s crucial for Scene management with ScriptableObjects in this particular case. Our example used a straightforward approach based on the Scene name, but there are many different solutions that rely less on the scene name. You should avoid the string-based approach because if you rename a Unity Scene in a given context, in another part of the game that Scene will not load.

  • Custom tooling

One way to avoid the name dependency game-wide is to set up your script to reference Scenes as Object type. This allows you to drag and drop a Scene asset in an Inspector and then safely get its name in a script. However, since it’s an Editor class, you don’t have access to the AssetDatabase class at runtime, so you need to combine both pieces of data for a solution that works in the Editor, prevents human error, and still works at runtime. You can refer to the ISerializationCallbackReceiver interface for an example of how to implement an object which, upon serialization, can extract the string path from the Scene asset and store it to be used at runtime.

In addition, you might also create a custom Inspector to make it easier to quickly add Scenes to the Build Settings using buttons, instead of having to add them manually through that menu and having to keep them in sync.

As an example of this type of tool, check out this great open source implementation by developer JohannesMP (this is not an official Unity resource).

Let us know what you think

This post shows just one way that ScriptableObjects can enhance your workflow when working with multiple Scenes combined with Prefabs. Different games have vastly different ways of managing Scenes – no single solution works for all game structures. It makes a lot of sense to implement your own custom tooling to fit the organization of your project.

We hope this information can help you in your project or maybe inspire you to create your own Scene management tools.

Let us know in the comments if you have any questions. We would love to hear what methods you use to manage the Scenes in your game. And feel free to suggest other use cases you would like us to cover in future blog posts.

Previous Article
 Architect your game with Scriptable Objects
Architect your game with Scriptable Objects

Keep your game code easy to change and debug by architecting it with Scriptable Objects.

Next Article
Architect your code as your project scales
Architect your code as your project scales

Effective strategies for architecting the code of a growing project, so it scales neatly and with fewer pro...