赞
踩
在Unity中管理多个场景并不简单,改善场景工作流是改善游戏性能和团队生产力的关键。在本文中,我们将分享一些建立场景工作流的贴士,方便你管理大型项目。
大部分游戏都会有多个关卡,每个关卡又会有不止一个场景。如果场景较小,可以将其拆分为一个个预制件,在游戏运行时使用预制件来启用、激活场景。当游戏规模变大,预制件会逐渐占据更多内存,反而没有场景的效率来得高。
如果将关卡拆分为场景,就能避免Git、SVN、Unity Collaborate这类协作工具在合并工程时出现冲突。而Unity的多场景编辑(Multi-Scene editing)功能可在编辑器内打开多个场景,解决高效管理场景的问题。
组合多个场景为一个关卡
在下方视频中,我们将游戏逻辑和不同的关卡组件拆分为多个不同的场景,使得关卡加载速度更加高效。接着,使用叠加式场景加载 (LoadSceneMode.Additive) 模式在加载游戏逻辑的同时,完成关卡组件的加载和卸载。预制件在场景中扮演了“锚点”的角色,让团队能分别编辑各个场景,使团队协作变得更加灵活。
场景在编辑模式中同样可以加载、运行,在设计关卡时就实现所有场景的预览。
我们将展示两种不同的场景加载方法。第一种是基于距离的加载,适用于无内部场景的开放世界,也能借助视觉特效(像雾气)来隐藏加载和卸载流程。
第二种方法是触发式加载,适用于室内场景。
在设置好关卡元素的加载方式后,我们便能继续添加控制层,管理所有的关卡。
使用ScriptableObjects管理多个关卡
如果想要管理各个关卡的场景,或者游戏时所有的关卡,一个可能的方法是在MonoBehaviour脚本中使用静态变量和单例模式。但是使用单例模式会让系统之间产生刚性连接,无法实现模块化制作,系统不能独立存在,必定会相互依赖。
另一个问题出在静态变量上。变量无法在检视器中暴露出来,需要使用代码才能修改,使美术和关卡设计师难以测试游戏。而场景间的数据分享更是需要在DontDestroyOnLoad对象中使用静态变量,造成问题。
ScriptableObject是一种专门存储数据的序列化类,可用于储存不同场景的数据信息。MonoBehavior脚本一般作为GameObject的附属组件使用,而ScriptableObjects并不附着与任何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类,对象就也会成为ScriptableObject。
[CreateAssetMenu(fileName = "NewLevel", menuName = "Scene Data/Level")]public class Level : GameScene{ //Settings specific to level only(关卡设置) [Header("Level specific")] public int enemiesCount;}
右滑查看完整代码
在上方添加CreateAssetMenu属性可以生成Unity的Asset菜单,在菜单中创建新关卡。你也能加入一个enum(枚举)函数来实现在检视器中选择菜单类型。
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;}
右滑查看完整代码
在创建关卡和菜单后,接下来需要加入一个列出所有关卡和菜单的数据库。加入索引值来确定玩家所在的关卡。再添加加载新游戏(即加载第一关)、重玩当前关卡和进入下一关的方法。这三种方法只有索引值会更改,所以可以同个方法多次套用。
[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);
}
右滑查看完整代码
上方也包含了菜单的方法,你也能使用前边的enum类型来加载特定菜单——注意enum中的顺序需要与菜单列表的顺序相同。
这下你就能在Project窗口中右击在Asset菜单下创建关卡、菜单或数据库ScriptableObject了。
然后,你就能添加关卡、菜单,调整设置,再将其添加到场景数据库中了。下方图例展示了Level1、MainMenu和Scenes Data的检视器。接下来要做的就是调用方法。在下方例子中,当玩家到达关卡末尾时,UI中会出现Next Level(下一关)按钮,按下后就会调用NextLevel方法。要给按键添加方法,需要在Button组件的On Click事件部分点击“+”按钮来添加新事件,再将Scenes Data ScriptableObject拖到对象字段中,并选择NextLevel方法。下方为图例。
重新开始、回到主菜单等其它按键也是同样的操作。其它脚本,如背景音乐的AudioClip或后期处理设置,都能引用ScriptableObject来获取属性,在关卡中使用。
使用提示
最小化加载/卸载流程
在视频中,玩家在多次进入、离开碰撞体时,触发了重复的场景加载和卸载。要想避免此类问题,可在调用场景加载/卸载方法前运行一个协同程序,在玩家离开触发位置后再停止程序。
命名规则
另一个提示是在项目中使用靠得住的命名规则。团队最好事前就确定如何命名不同类型的资源——从脚本、场景,到材质等所有内容。这样一来,场景管理,尤其是ScriptableObject的管理都会容易很多。在示例中我们直接根据场景名称来命名。最好避免使用字符串方法,不然在重命名场景后,场景可能无法加载。
自定义工具
一种避免名称依赖的方法是让场景以Object类型被引用。如此一来,你就能在检视器中拖动场景资源到脚本,让脚本安全地取得场景名称。但由于其为编辑器类,运行时又无法访问AssetDatabase类,需要将两种数据结合起来,才能让方案在编辑器和运行时运行。你可以参考ISerializationCallbackReceiver接口示例来查看如何让对象在序列化之后从Scene资源中提取字符串路径,储存数据用在运行时中。
此外,你还能自制一个检视器,使用按键给Build Settings快速添加场景,而不必在菜单中手动添加、管理。
JohannesMP开发的开源应用是一个非常好的例子(注意这不是Unity官方资源)。
欢迎大家积极反馈
本文仅展示了ScriptableObject在使用多场景和预制件制作时的一种使用方法。不同游戏管理场景的方式大相径庭,一个方案是无法满足所有游戏结构的。请根据自己项目的组织结构来制作合适的工具。希望本文能帮助、激励你制作自己的场景管理工具。
如果你有任何疑问,也欢迎在留言区留言,我们非常希望了解大家在游戏中管理场景的方法。
文中提及的相关链接:
[1] 多场景编辑文档:
https://docs.unity3d.com/Manual/MultiSceneEditing.html
[2] 叠加式场景加载文档:
https://docs.unity3d.com/ScriptReference/SceneManagement.LoadSceneMode.Additive.html
[3] 触发式加载文档:
https://docs.unity3d.com/ScriptReference/Collider.OnTriggerEnter.html
[4] ScriptableObject 文档:
https://docs.unity3d.com/Manual/class-ScriptableObject.html
[5] CreateAssetMenu文档:
https://docs.unity3d.com/ScriptReference/CreateAssetMenuAttribute.html
[6] Object文档:
https://docs.unity3d.com/ScriptReference/Object.html
[7] AssetDatabase文档:
https://docs.unity3d.com/ScriptReference/AssetDatabase.html
[8] ISerializationCallbackReceiver文档:
https://docs.unity3d.com/ScriptReference/ISerializationCallbackReceiver.html
[9] Build Settings文档:
https://docs.unity3d.com/Manual/BuildSettings.html
[10] JohannesMP开发的开源应用(非官方资源):
https://gist.github.com/JohannesMP/ec7d3f0bcf167dab3d0d3bb480e0e07b
每一个“在看”,都是我们前进的动力
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。