使用ScriptableObjects实现更好的场景工作流程

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. 

在Unity中管理多个场景可能是一个挑战,改善此工作流程对于游戏性能和团队生产力都至关重要。 在这里,我们分享了一些技巧,这些技巧以适合更大项目的方式设置场景工作流。

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.

您可以将级别分解为一个或多个Unity场景。 寻找最佳方式来管理它们成为关键。 您可以在编辑器中并在运行时使用“ 多场景”编辑 打开多个场景 。 将级别分为多个场景还具有使团队协作更容易的优势,因为它避免了协作工具(如Git,SVN, Unity Collaborate 等)中的 合并冲突 。

管理多个场景以构成一个关卡 (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.

在下面的视频中,我们展示了如何通过将游戏逻辑和关卡的不同部分分解为几个不同的Unity场景来更有效地加载关卡。 然后, 在加载这些场景时 使用“ 添加场景加载”模式 ,我们在持久的游戏逻辑旁边加载和卸载所需的部分。 我们使用Prefabs充当“场景”的“锚”,在团队工作时,它也提供了很大的灵活性,因为每个“场景”都代表关卡的一部分,可以单独进行编辑。

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.  

现在,所有内容都在关卡内部进行了管理,您可以在该关卡之上添加一个图层以更好地管理关卡。

使用ScriptableObjects管理游戏中的多个关卡 (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. 

我们要跟踪整个游戏过程中每个关卡以及所有关卡的不同场景。 一种可行的方法是在您的MonoBehaviour脚本中使用静态变量和单例模式,但是此解决方案存在一些问题。 使用单例模式可以实现系统之间的刚性连接,因此它不是严格的模块化。 这些系统不能分开存在,并且总是相互依赖的。

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.

另一个问题涉及静态变量的使用。 由于您无法在“检查器”中看到它们,因此需要更改代码来设置它们,从而使美术师或关卡设计师更难于轻松测试游戏。 当需要在不同场景之间共享数据时,可以将静态变量与DontDestroyOnLoad结合使用,但是应尽可能避免使用后者。

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. 

要存储有关不同场景的信息,可以使用 ScriptableObject ,这是一个可序列化的类,主要用于存储数据。 与MonoBehaviour脚本(用作附加到GameObjects的组件)不同,ScriptableObjects不附加到任何GameObjects,因此可以在整个项目的不同场景之间共享。

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.

您希望能够将此级别用于游戏中的关卡以及菜单场景。 为此,创建一个GameScene类,其中包含关卡和菜单之间的不同公共属性。

1

2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

1

2
3
4
5
6
7
8
9
10
11
12
13
14
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.

请注意,该类继承自ScriptableObject而不是MonoBehaviour。 您可以根据需要添加任意数量的游戏属性。 完成此步骤后,您可以创建同时继承自刚刚创建的GameScene类的Level和Menu类-因此它们也是ScriptableObjects。

1

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

1

2
3
4
5
6
7
[ 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.

通过在顶部 添加 CreateAssetMenu 属性,可以从Unity中的“资产”菜单创建新级别。 您可以对Menu类执行相同的操作。 您还可以包含一个枚举,以便能够从Inspector中选择菜单类型。

1

2
3
4
5
6
7
8
9
10
11
12
13
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;
}

1

2
3
4
5
6
7
8
9
10
11
12
13
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.

现在您可以创建级别和菜单,让我们添加一个列出级别和菜单的数据库,以方便参考。 您还可以添加索引以跟踪播放器的当前级别。 然后,您可以添加方法来加载新游戏(在这种情况下,将加载第一个关卡),重播当前关卡以及进入下一个关卡。 请注意,在这三种方法之间只有索引会发生变化,因此您可以创建一个使用索引加载级别以多次使用它的方法。

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
[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;
    /*
     * 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);
    }

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
[ 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 ;
     /*
     * 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.

现在,您终于可以通过右键单击“项目”窗口中的“资产”菜单来创建级别,菜单或数据库ScriptableObject。

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.

从那里开始,只需继续添加所需的级别和菜单,调整设置,然后将它们添加到Scenes数据库即可。 下面的示例向您显示Level1,MainMenu和Scenes Data的外观。

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.

是时候调用这些方法了。 在此示例中,当玩家到达关卡末尾时出现在用户界面(UI)上的“下一个关卡”按钮调用NextLevel方法。 要将方法附加到按钮,请单击Button组件的On Click事件的加号按钮以添加新事件,然后将Scenes Data ScriptableObject拖放到对象字段中,然后从ScenesData中选择NextLevel方法,如下所示。

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.

现在,您可以对其他按钮执行相同的过程-重放级别或进入主菜单,依此类推。 您还可以从任何其他脚本中引用ScriptableObject来访问不同的属性,例如用于背景音乐或后处理配置文件的AudioClip,并在关卡中使用它们。

防错流程的提示 (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. 

在视频中显示的ScenePartLoader脚本中,您可以看到玩家可以多次进入和离开对撞机,从而触发场景的重复加载和卸载。 为避免这种情况,您可以在脚本中调用场景的加载和卸载方法之前添加协程,并在玩家离开触发器时停止协程。

  • 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.

另一个一般性技巧是在项目中使用实体命名约定。 团队应事先就如何命名不同类型的资产(从脚本和场景到项目中的素材和其他内容)达成一致。 这不仅使您,而且使您的团队成员更轻松地从事该项目并进行维护。 这始终是个好主意,但在这种特殊情况下,这对于使用ScriptableObjects进行场景管理至关重要。 我们的示例使用了基于场景名称的简单方法,但是有许多不同的解决方案较少依赖场景名称。 您应该避免使用基于字符串的方法,因为如果在给定的上下文中重命名Unity场景,则在游戏的另一部分中,场景将不会加载。

  • 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.

避免在游戏范围内出现名称依赖的一种方法是,将脚本设置为将场景引用为 对象 类型。 这使您可以将场景资产拖放到检查器中,然后在脚本中安全地获取其名称。 但是,由于它是一个Editor类, 因此您在运行时 无权访问 AssetDatabase 类,因此您需要将这两个数据组合在一起,以使该解决方案在Editor中起作用,防止人为错误,并且仍在运行时起作用。 您可以参考 ISerializationCallbackReceiver 接口,以获取有关如何实现对象的示例,该对象在序列化后可以从Scene资产中提取字符串路径并将其存储以在运行时使用。

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.

此外,您还可以创建一个自定义的Inspector,以使 使用按钮 将场景快速添加到“ 构建设置” 变得更加容易 ,而不必通过该菜单手动添加它们并使其保持同步。

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).

作为此类工具的示例,请查看 开发人员JohannesMP的 出色开源实现 (这不是Unity的官方资源)。

让我们知道您的想法 (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.

这篇文章仅显示了ScriptableObjects可以在结合使用多个场景和Prefabs时增强工作流的一种方法。 不同的游戏具有不同的场景管理方式–没有适用于所有游戏结构的解决方案。 实施自己的自定义工具以适合项目的组织非常有意义。

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.

如果您有任何疑问,请在评论中告诉我们。 我们很想听听您使用什么方法来管理游戏中的场景。 并且随时建议您希望我们在以后的博客文章中介绍的其他用例。

翻译自: https://blogs.unity3d.com/2020/07/01/achieve-better-scene-workflow-with-scriptableobjects/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值