使用ScriptableObject改善场景处理

你好。现在,为课程“ Unity Game Developer。基本的我们邀请您查看课程开放日记录,并且按照传统,也分享有趣的翻译。










在Unity中使用多个场景可能具有挑战性,并且优化此工作流程对您的游戏性能和团队的生产力都有巨大影响。今天,我们将与您分享有关使用Scene设置可扩展到更大项目的工作流的提示。


大多数游戏具有多个级别,并且每个级别通常包含多个场景。在场景相对较小的游戏中,您可以使用预制件将它们分解为不同的部分。但是,为了在游戏中连接或实例化它们,您需要引用所有这些预制件。这意味着随着您的游戏变得更大并且这些链接占用更多的内存空间,使用场景的效率也会更高。



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



管理多个场景以创建关卡



在下面的视频中,我们将向您展示如何通过将游戏逻辑和关卡的不同部分分解为多个单独的Unity场景来更有效地加载关卡。然后,在加载这些场景时使用“添加场景加载”模式,我们可以加载和卸载必要的部分,以及随处可见的游戏逻辑。我们将预制件用作场景的锚点,由于每个场景都是关卡的一部分并且可以单独编辑,因此在团队合作时还提供了很大的灵活性。



您仍可以在编辑模式下加载这些场景,并在进行关卡设计时随时按播放将它们一起渲染。



我们将展示两种不同的方法来加载这些场景。第一个基于距离,它适用于非内部级别(例如开放世界)。对于某些视觉效果(例如雾)来隐藏加载和卸载过程,此技术也很有用。



第二种方法使用触发器来检查哪些场景需要加载,这在处理室内场景时效率更高。





现在我们已经弄清了关卡中的所有内容,我们可以在其之上添加一个额外的层,以更好地管理关卡本身。



使用ScriptableObjects控制多个游戏关卡



我们想要跟踪每个关卡中的不同场景以及整个游戏中的所有关卡。实现此目的的一种可能方法是在MonoBehaviour脚本中使用静态变量和单调,但是这种解决方案并不是那么顺利。使用单例意味着系统之间的紧密链接,因此它不是严格的模块化。系统不能单独存在,并且将始终相互依赖。



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



要存储有关各种场景的信息,可以使用ScriptableObject,这是一个可序列化的类,主要用于存储数据。与用作绑定到GameObject的组件的MonoBehaviour脚本不同,ScriptableObject不绑定到任何GameObject,因此可以在整个项目中的不同场景中使用。



能够在游戏中的关卡和菜单场景中使用此结构将是很好的。为此,创建一个GameScene类,其中包含关卡和菜单的各种常规属性。



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;
}


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



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


在顶部 添加CreateAssetMenu属性可让您从Unity中的Assets菜单中创建新级别。您可以对Menu类执行相同的操作。您还可以添加枚举,以便能够从检查器中选择菜单类型。



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


现在您可以创建级别和菜单了,为了方便起见,让我们添加一个列出它们(级别和菜单)的数据库。您还可以添加索引来跟踪玩家的当前水平。然后,您可以添加方法以加载新游戏(在这种情况下将加载第一个关卡),重复当前关卡并转到下一个关卡。请注意,在这三种方法中仅更改了索引,因此您可以创建一个按索引加载级别以重新使用它的方法。



[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);
    }


还有菜单方法,您可以使用之前创建的枚举类型来加载所需的特定菜单-只需确保枚举中的顺序与菜单列表中的顺序相同即可。



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







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







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







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



最小化过程中错误的提示



最小化加载/



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



命名约定



另一个全局技巧是在项目中使用强大的命名约定。团队应事先就如何命名不同类型的资产(从脚本和场景到项目中的素材和其他内容)达成一致。这将使项目工作变得更容易,不仅为您而且为您的队友提供支持。这总是一个好主意,但是在这种特殊情况下,这对于使用ScriptableObjects管理场景非常重要。我们的示例使用了基于场景名称的简单方法,但是有许多不同的解决方案较少依赖场景名称。您应该避免使用基于字符串的方法,因为如果您在此上下文中重命名Unity场景,则该场景不会在游戏中的其他位置加载。



特殊工具



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



另外,您也可以创建自己的检查器,以使使用按钮将场景快速添加到“构建设置”变得更容易,而不是通过此菜单手动添加场景并使它们保持同步。



有关此类工具的示例,请从开发人员JohannesMP中查看这个很棒的开源实现(这不是Unity的官方资源)。



让我们知道您的想法



这篇文章仅展示了ScriptableObjects在与预制件结合使用多个场景时可以改善工作流程的一种方式。不同的游戏使用完全不同的方式来控制场景-没有任何一种解决方案可以一次适应所有游戏结构。实施适合您的项目组织的工具是很有意义的。



我们希望这些信息将对您的项目有所帮助,或者启发您创建自己的场景管理工具。



如果您有任何疑问,请在评论中告诉我们。我们很想听听您使用什么技术来操纵游戏中的场景。并随时提出您希望在以后的帖子中提出建议的其他用例。












All Articles