Improving scene handling with ScriptableObject

Hello. Right now OTUS has opened a set for the course โ€œUnity Game Developer. Basic " . We invite you to look at the record of the open day for the course , and also traditionally share an interesting translation.










Working with multiple scenes in Unity can be challenging, and optimizing this workflow has a huge impact on both your game's performance and your team's productivity. Today, we'll share with you tips for setting up workflows with Scene that can scale to larger projects.


Most games have multiple levels, and levels often contain more than one scene. In games where scenes are relatively small, you can break them up into different parts using Prefabs. However, in order to connect or instantiate them during the game, you need to reference all of these prefabs. This means that as your game gets bigger and these links take up more memory space, it becomes more efficient to use scenes.



You can split the levels into one or more Unity Scenes. Finding the best way to manage them becomes the key point. You can open multiple scenes at once in the editor and at runtime using the Multi-Scene editing feature . Splitting layers into multiple scenes also makes teamwork easier, as it avoids merge conflicts in collaboration tools like Git, SVN, Unity Collaborate, and more.



Manage multiple scenes to create a level



In the video below, we'll show you how to load a level more efficiently by breaking the game logic and different parts of the level into multiple separate Unity scenes. Then, using the Additive Scene-loading mode when loading those scenes, we load and unload the necessary parts along with the game logic that doesn't go anywhere. We use prefabs as anchors for scenes, which also provides more flexibility when working as a team, since each scene is a part of a level and can be edited separately.



You can still load these scenes in edit mode and press Play at any time to render them all together while working on the level design.



We will show two different methods for loading these scenes. The first is based on distance, which works well for non-interior levels such as the open world. This technique is also useful for some visual effects (like fog) to hide the loading and unloading process.



The second method uses a Trigger to check which scenes need to be loaded, which is more efficient when working with interiors.





Now that we have figured out everything inside the level, we can add an additional layer on top of it to better manage the levels themselves.



Controlling multiple game levels with ScriptableObjects



We want to keep track of the different scenes in each level, as well as all the levels throughout the entire gameplay. One possible way to achieve this is to use static variables and singletones in MonoBehaviour scripts, but this solution is not so smooth. Using a singleton implies tight links between your systems, so it is not strictly modular. Systems cannot exist separately and will always depend on each other.



Another problem is related to the use of static variables. Since you cannot see them in the Inspector, you need to define them through code, which makes it harder for artists or level designers to test the game. When you need data to be shared between different scenes, you use static variables in conjunction with DontDestroyOnLoad, but the latter should be avoided whenever possible.



To store information about various scenes, you can use ScriptableObject , a serializable class that is primarily used to store data. Unlike MonoBehaviour scripts, which are used as components bound to GameObjects, ScriptableObjects are not bound to any GameObject and thus can be used by different scenes throughout the project.



It would be nice to be able to use this structure for levels as well as menu scenes in your game. To do this, create a GameScene class that contains various general properties for 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;
}


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



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


Adding the CreateAssetMenu attribute at the top allows you to create a new level from the Assets menu in Unity. You can do the same for the Menu class. You can also add an enumeration to be able to select the menu type from the inspector.



public enum Type
{
    Main_Menu,
    Pause_Menu
}
 
[CreateAssetMenu(fileName = "NewMenu", menuName = "Scene Data/Menu")]
public class Menu : GameScene
{
    // ,    
    [Header("Menu specific")]
    public Type type;
}


Now that you can create levels and menus, let's add a database that lists them (levels and menus) for convenience. You can also add an index to keep track of the player's current level. You can then add methods to load a new game (in which case the first level will be loaded), to repeat the current level, and to go to the next level. Note that only the index is changed in these three methods, so you can create a method that loads the level by index to reuse it.



[CreateAssetMenu(fileName = "sceneDB", menuName = "Scene Data/Database")]
public class ScenesData : ScriptableObject
{
    public List<Level> levels = new List<Level>();
    public List<Menu> menus = new List<Menu>();
    public int CurrentLevelIndex=1;
 
    /*
 	* 
 	*/
 
    //     
    public void LoadLevelWithIndex(int index)
    {
        if (index <= levels.Count)
        {
            //     
            SceneManager.LoadSceneAsync("Gameplay" + index.ToString());
            //       
            SceneManager.LoadSceneAsync("Level" + index.ToString() + "Part1", LoadSceneMode.Additive);
        }
        //  ,      
        else CurrentLevelIndex =1;
    }
    //   
    public void NextLevel()
    {
        CurrentLevelIndex++;
        LoadLevelWithIndex(CurrentLevelIndex);
    }
    //   
    public void RestartLevel()
    {
        LoadLevelWithIndex(CurrentLevelIndex);
    }
    //  ,   
    public void NewGame()
    {
        LoadLevelWithIndex(1);
    }
  
    /*
 	* 
    */
 
    //   
    public void LoadMainMenu()
    {
        SceneManager.LoadSceneAsync(menus[(int)Type.Main_Menu].sceneName);
    }
    //   
    public void LoadPauseMenu()
    {
        SceneManager.LoadSceneAsync(menus[(int)Type.Pause_Menu].sceneName);
    }


There are also menu methods, and you can use the type of enumeration you created earlier to load the specific menu you want - just make sure the order in the enumeration and the order in the menu list are the same.



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







From there, just keep adding the levels and menus you want, adjusting the parameters and then adding them to the scene database. The example below shows what the Level1, MainMenu and Scenes data looks like.







It's time to call these methods. In this example, the Next Level button in the user interface (UI) that appears when the player reaches the end of the level calls the NextLevel method. To bind a method to a button, click the Button with the On Click event plus of the Button component to add a new event, then drag the Scene Data ScriptableObject into the object field and select the NextLevel method from ScenesData as shown below.







Now you can do the same process for other buttons - replay the level or go to the main menu and so on. You can also refer to ScriptableObject from any other script to access various properties such as AudioClip for background music or post-processing profile and use them at the level.



Tips to Minimize Errors in Your Processes



Minimizing Loading /



Unloading In the ScenePartLoader script shown in the video, you can see that the player can keep going in and out of the collider multiple times, causing the scene to reload and unload. To avoid this, you can add a coroutine before calling the scene loading and unloading methods in the script and stop the coroutine if the player leaves the trigger.



Naming conventions



Another global tip is to use strong naming conventions in your project. The team should agree in advance 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 to work on the project and support it not only for you, but also for your teammates. It's always a good idea, but in this particular case it is very important for managing scenes with ScriptableObjects. Our example used a simple scene name based approach, but there are many different solutions that rely less on the scene name. You should avoid a string-based approach because if you rename a Unity scene in this context, that scene will not load elsewhere in the game.



Special tools



One way to avoid relying on names throughout the game is to configure your script to refer to scenes as being of type Object . This allows you to drag and drop a scene resource in the inspector and then quietly get its name in the script. However, since it is an Editor class, you do not 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 that, after serialization, can extract the string path from the Scene asset and store it for runtime use.



Alternatively, you can also create your own inspector to make it easier to quickly add scenes to Build Settings using buttons, instead of manually adding them through this menu and keeping them in sync.



For an example of this type of tool, check out this awesome open source implementation from developer JohannesMP (this is not an official Unity resource).



Let us know what you think



This post only shows one way that ScriptableObjects can improve your workflow when working with multiple scenes in combination with prefabs. Different games use completely different ways of controlling scenes - no single solution fits all game structures at once. It makes sense to implement your own tools to suit your project organization.



We hope this information will help you with your project, or perhaps inspire you to create your own scene management tools.



Let us know in the comments if you have any questions. We'd love to hear what techniques you use to manipulate scenes in your game. And feel free to suggest other use cases that you would like to suggest for consideration in future posts.












All Articles