Unity
5.3中新增加了多场景编辑功能,允许用户将一个大场景以某种逻辑分割成多个小场景并方便的编辑和管理。这在某些情况下会比较有用,是对Unity编辑器
对场景编辑能力的一个重要提升。本文将由Unity官方工程师张为,为大家介绍一些多场景编辑的基本功能以及一些实例。
什么是多场景编辑
多场景编辑就是允许用户在Unity编辑器中同时打开多个场景,并对它们进行编辑。Unity提供了一系列的UI和Scripting APIs来管理这些场景。以下就是一个在Unity编辑器中进行多场景编辑的一个实例。
什么是场景
在进一步了解多场景编辑之前,我们先了解下什么是场景(Scene)。简而言之场景就是包含了游戏对象的一个文件,比如Game objects,Components等。不过有一些对象可能一些用户并不会太在意,那就是Scene Game managers。
Unity中有两类Game Managers:
一类是Global Game Managers。它们是全局的Game
managers,包括AudioManager、InputManager、PhysicsManager等。当你在Unity5.3中发布你的游戏的
时候,你会发现在输出目录或者发布包里面会有个globalgamemanagers文件,它包含了所有全局Game managers。
另一类是Scene game managers。如果你将Editor Settings中的Asset Serialization选择为”Force Text”模式,并打开一个已经保存的*.unity场景文件,你会发现前保存在文件最前面的就是SceneSettings、RenderSettings、LightMapSettings和NavMeshSettings,它们就是每个场景都会有的Game managers。
为什么需要多场景编辑
首先将大场景分割成多个场景,可以更好的支持场景的流式加载(Scene streaming);
其次可以更好地支持协同合作,尤其是在有源代码版本管理的时候可以允许多人同时编辑而不会产生冲突;
再者支持卸载场景(Scene unloading),在5.3之前用户可以通过Application.LoadLevelAddtive()和Application.LoadLevelAdditiveAsync()动态加载场景,但没有对应的Application.UnloadScene()。而5.3中提供了场景卸载,让用户可以更灵活的管理多个场景。
多场景编辑基本功能介绍
接下来我们看看Unity在5.3中具体提供了哪些多场景编辑的功能。
Scene结构
在5.3之前,Unity中只有概念上的场景,而到5.3中我们引入了真正的Scene结构。它包含了name、path、isLoaded等变量,同时也提供了IsValid()以及GetRootGameObjects()方法。
Active Scene
在5.3中,我们引入了Active Scene(当前场景)的概念。引入它的目的在于:
如果有多个场景同时打开,我们会选择Active Scene的Scene game managers作为当前的Scene game managers。比如在bake lightmapping的时候,我们会使用Active scene的LightMapSettings来bake当前打开的所有场景。
在创建Game object的时候,会默认加入到Active scene。
SceneManager
SceneManager在UnityEngine.SceneManagement之下,它是Runtime中的Scene manager,提供了以下方法:
LoadScene() / LoadSceneAsync()
它们允许用户通过name、build index来加载场景。用户可以在Build Settings窗口查看name和build index。通过这两个方法加载的场景,要么被加到了Build Settings,要么存在于AssetBundle之中。如果是从AssetBundle中加载场景,则只能通过名字加载。
要说明的是如果有多个场景同名但位于不同的目录之下,可以使用完整的路径(不带.unity后缀名)来加载不同的场景。
用户可以通过LoadSceneMode来指定不同的加载模式。LoadSceneMode.Single在加载之前会卸载其他所有的场景,LoadSceneMode.Additive则是加载的时候不关闭之前的场景。
还有一点很重要的是LoadScene()并不是完全同步的,它只能保证在下一帧开始之前加载完毕。所以在此推荐大家使用LoadSceneAsync()这个异步的加载方法。
UnLoadScene()
目前5.3中用户只能通过name和build index来同步的卸载一个Scene。在后续的版本中我们会提供通过Scene结构来卸载一个Scene,并且提供异步卸载的方法。
GetActiveScene() / SetActiveScene()
获取和设置Active scene。
GetSceneAt() / GetSceneByName() / GetSceneByPath()
我们也提供了一组方法来查询Scene。
其它
EditorSceneManager
EditorSceneManager在UnityEditor.SceneManagement之下,它是Editor中的Scene manager,提供了以下方法:
OpenScene()
它是一个同步的方法,用户只能通过path来打开场景。不同于LoadScene() / LoadSceneAsync(),它可以直接打开一个存在于Assets目录下的场景,不管它是否被添加到Build Settings。
用户可以通过OpenSceneMode来指定不同的打开模式,相比较LoadSceneMode,它多了一个AdditiveWithoutLoading模式,允许用户增加一个场景但并不真正加载它。
CloseScene()
顾名思义,它可以关闭一个场景,同时它提供了一个bool参数来指定关闭的时候是否将场景从Scene manager中移除。
SaveScene() / SaveScenes()
通过它们可以保存一个或多个Scene。
MarkSceneDirty() / MarkAllScenesDirty()
通过它们可以将某个指定的场景或者所有场景标记为Dirty。大部分情况Unity内部通过Undo系统来实现场景的Dirty跟踪,但是有些模块并没有完全支持Undo,比如Terrain在设置某些参数的时候就不支持Undo。所以我们提供了这两个方法支持直接将场景设置为Dirty。
其它
API的使用限制
在Editor mode下,UnityEngine.SceneManagement.SceneManager的某些方法是不能使用的。
LoadScene()
LoadSceneAsync()
CreateScene()
UnloadScene()
同样在Play mode下,UnityEditor.SceneManagement.EditorSceneManager的某些方法也不能使用。
OpenScene()
NewScene()
CloseScene()
SaveScene() / SaveScenes() / …
MarkSceneDirty() / MarkAllScenesDirty()
…
如果用户在不同的模式下使用了错误的方法,我们会在Console输出对应的错误信息引导用户使用正确的方法。
DontDestroyOnLoad Scene
在Unity 5.3中,如果用户通过Object.DontDestroyOnLoad()方法将某个Game object标记成DontDestroyOnLoad,在进入Play mode的时候会发现Hierarchy窗口中会多出一个DontDestroyOnLoad场景,它包含了之前标记成DontDestroyOnLoad的Game object。
为什么需要DontDestroyOnLoad Scene
在Unity 5.3中,所有的Game objects必须隶属于某一个场景。这样我们必须有一个特别的场景来管理这些被标记为DontDestroyOnLoad的Game objects,否则在Unload这些Game objects所属的场景的时候,这些Game objects也会被删除掉。这显然不是我们想要的结果。
因此我们引入了DontDestroyOnLoad Scene,当进入Play mode时候,我们会把所有标记为DontDestroyOnLoad的Game objects从所属的场景移入到这个特别的场景之中。
DontDestroyOnLoad Scene的特点
DontDestroyOnLoad Scene仅仅存在于Runtime,或者是Play mode。它不能从外部访问,仅仅是Unity内部用于管理标记为DontDestroyOnLoad的Game objects。
事实上从Unity 5.3开始,我们并不推荐用户使用DontDestroyOnLoad这一功能,它使得我们内部的代码逻辑复杂度增加了不少。5.3之前因为没有多场景的支持,所以并没有很好地办法绕开它并实现相同的功能。而从5.3开始我们推荐用户创建一个Manager场景,由它负责加载/卸载其它所有的游戏场景。它从游戏开始便存在一直到游戏退出,这样所有需要被标记为DontDestroyOnLoad的Game objects都应该属于这个场景。
多场景编辑的进阶
接下来我们介绍一些关于多场景编辑的进阶以及一些小Tips。
Scene Manager Setup
Scene Manager Setup可以用来保存并恢复当前的Scene hierarchy。EditorSceneManager上提供了GetSceneManagerSetup() / RestoreSceneManagerSetup()来获取和恢复Scene hierarchy。
我们可以通过ScriptableObject来保存Scene hierarchy,如下代码所示:
[AppleScript]纯文本查看复制代码
1
2
3
4
5publicclassSceneSetupSerialization:ScriptableObject
{
[SerializeField]
public SceneSetup[] SceneSetups;
}
以下的代码展示了如何保存以及读取Scene manager setup。
[C#]纯文本查看复制代码
string sceneSetupAssetPath = "Assets/SceneSetup.asset";
public static void SaveSceneSetups()
{
// Create SceneSetupSerialization
var sceneSetupSerialization = ScriptableObject.CreateInstance();
// Set SceneManagerSetup.
var sceneSetups = EditorSceneManager.GetSceneManagerSetup();
sceneSetupSerialization.SceneSetups = sceneSetups;
// Create and save the asset.
AssetDatabase.CreateAsset(sceneSetupSerialization, sceneSetupAssetPath);
AssetDatabase.SaveAssets();
}
public static void LoadSceneSetups()
{
// Restore SceneManagerSetup from the asset.
var sceneSetupSerialization = AssetDatabase.LoadMainAssetAtPath(sceneSetupAssetPath) as SceneSetupSerialization;
EditorSceneManager.RestoreSceneManagerSetup(sceneSetupSerialization.SceneSetups);
}
Lightmap & NavMesh Baking
在Unity 5.3中,Lightmap和NavMesh的烘焙都同时支持多个场景,它们之间的不同之处在于如何管理和划分烘焙的结果。
对于Lightmap Baking,我们会根据Scenes划分Lightmaps和Realtime GI数据。每个场景都只会加载和自己相关的那部分数据。
对于NavMesh Baking,因为它烘焙的结果很小,所以我们将NavMesh的数据保存在一个asset当中,每个场景都会引用到这个asset并能够找到自己所关联的那部分数据。
另外用户也可以通过脚本进行Baking,Lightmapping.BakeMultipleScenes()和NavMeshBuilder.BuildNavMeshForMultipleScenes()都支持一次烘焙多个场景。
Scene Dirty Track
Unity内部大多通过Undo系统来实现Scene dirty追踪。Unity 5.3为了支持多场景编辑,我们通过在Undo操作中保存Scene handles来扩展Undo系统。
另外我们增加了Undo.MoveGameObjectToScene()方法来支持场景之间Game object移动的Undo。同时Scene结构上面也有一个Scene.isDirty属性用于查询某个Scene是否被修改。
“Ctrl + S”的行为
在这里有一个问题想跟大家讨论的是“Ctrl + S”的行为。在5.3以前,无论场景是否Dirty,只要用户按下”Ctrl + S”,我们一定会保存该场景。而在5.3中,因为多场景编辑的引入,我们改变了这一行为。
从5.3开始,”Ctrl + S”只会保存Dirty的场景。试想如果用户打开了上百个场景,只修改了其中某一个场景,如果“Ctrl + S”还是保存非Dirty的场景,保存速度会受到比较大的影响。
这个改动会影响到一些Editor的工具。比如某个Editor的工具创建了一个Game object,由于没有使用Undo系统(Undo.RegisterCreatedObjectUndo),使得场景未标记成Dirty。这样当在保存的时候,Unity并不会去真正保存这个场景。
在此推荐大家使用Undo系统来注册Undo操作,从而能够正确的将受影响的场景标记为Dirty。我们也乐于听到大家的反馈,来看看我们是否有办法更好的处理这个问题。
Scene加载的延迟Awaking
在多场景的使用中,一个比较有意思的地方就是Scene加载过程中的Delay awaking。在介绍它之前我们来看看Unity内部加载一个Scene所需的步骤。
Scene加载的两个步骤
Unity内部场景的加载分为两步:
Loading。是指从文件、内存(主要是Streamed scene
AssetBundle)中加载Scene的内容,创建并读取所有相关的Game objects、Assets以及Scene game
managers。所有的IO操作都在这一步完成,所以它是比较耗时的过程。当这一步完成的时候,我们内部会将加载进度标记为90%。
Awaking。主要是一些轻量级的操作,比如在Transform的Awaking的时候,我们会将Game objects加入到它所属于的Scene。
我们这里所说的Scene加载过程中的Delay awaking就是指第二步。
比如用户有一个大场景划分成了若干个子场景,在所有场景加载完毕我们才会开始Game play。这时我们就可以推迟所有子场景的Awaking。当所有的加载第一步完成了,我们才进行所有场景的Awaking。
用户可以通过将AsyncOperation.allowSceneActivation设置成false来阻止Scene的Awaking,示例如下:
[C#]纯文本查看复制代码
1
2
3stringname = “TestScene”;
AsyncOperation operation = SceneManager.LoadSceneAsync(name, LoadSceneMode.Additive);
operation.allowSceneActivation =false;
当加载进度AsyncOperation.progress到达90%的时候,就可以将allowSceneActivation设置成true来允许Scene awaking。