英文原文:
https://asmaloney.com/2020/01/code/using-unitys-shadervariantcollection/
最近我一直致力于将现有的 Unity 游戏移植到 macOS。这是一款名为 Blue Rider 的射击游戏,由来自阿根廷科尔多瓦的 Ravegan 开发。它最初在 PC (Steam)、Xbox、Playstation 4 和 Switch 上发布。
要将 Blue Rider 带到 macOS,我需要做的主要工作是菜单系统、分辨率处理、输入处理以及一些优化以实现流畅的帧速率。
(注意:我使用的是 Unity 5.6.7f1,因为 Blue Rider 是使用 5.x 版本编写的。我不是 100% 确定,但根据当前文档,我认为我在这里所做的仍然适用于最近版本。)
问题
其中一项优化与着色器有关。当敌人出现和爆炸时,游戏有时会出现问题。它似乎只发生在某些时候,并且发生在一些敌人身上。
我将解释我在分析 profiler 时发现了什么,然后我将提供一种使用 ShaderVariantCollection 的可能解决方案。
分析
我启动了 profiler 并运行了整个游戏,直到我看到敌人出现和死亡时的尖峰。
分析器将罪魁祸首确定为 Shader.EditorLoadVariant。
具体来说,它是着色器变体的编译——Shader.CreateGPUProgram。
游戏要求使用尚未编译的着色器变体,因此它会立即对其进行编译。因为这个过程很慢,它不能适应一帧,所以游戏感觉就像它暂停然后试图赶上。这被称为“hitching”。
这通常仅在第一次编译着色器变体时发生。同一运行期间的后续使用要么在内存中,要么被缓存。不幸的是,Unity 似乎没有在运行之间缓存这些编译的着色器,所以每次运行游戏时都会发生这种情况。
经过大量的在线搜索和阅读,这篇文章中的问题似乎没有任何答案。
一个办法
我正在开发的游戏有数百个对象以及许多着色器和着色器变体,因此每个对象的解决方案似乎具有挑战性。我想出的解决方案是为每个游戏关卡使用 ShaderVariantCollection。
(这似乎是关于这些的全部官方文档——如果我遗漏了什么,请告诉我)。
生成集合
从文档:
ShaderVariantCollection 的典型用途是从编辑器(在图形设置下)记录play期间使用的着色器变体,将它们保存为资产,并添加到预加载着色器列表(再次在图形设置中)。此外,您可以手动在 ShaderVariantCollection 对象上调用 WarmUp。
我选择为每个关卡创建一个着色器变体集合,然后加载它们并在每个关卡开始时调用 WarmUp。首先,我在 Assets/Resources 中创建了一个文件夹来保存这些文件。将其放入 Resources 可确保 Unity 将这些文件包含在构建中。我选择将它们放在 Assets/Resources/Shaders/Levels 中。
在 graphics 设置中,我们想要的工具位于面板的最底部:
对于每个关卡:
- 就在您开始关卡之前,单击Clear按钮。
- 玩关卡,触发尽可能多的事情。您希望游戏为关卡加载所有可能的着色器和着色器变体。
- 完成后,单击保存到资产… 按钮。
- 将关卡的集合保存到我们在上面创建的文件夹中(Assets/Resources/Shaders/Levels) 使用关卡名称,以便我们稍后可以找到它。在我的项目中,这将是“map_01.shadervariants”、“map_02.shadervariants”等。
完成此操作后,每个关卡应该有一个“.shadervariants”文件。
请记住,如果您更改关卡,您可能需要为该关卡重新执行此操作。 (这就是为什么,在我看来,这是一个 hacky 解决方案。Unity 没有提供任何真正的工具来更智能地管理这些集合。)
使用集合
现在我们有了文件,我们只需在加载每个关卡后加载它们。
在负责加载关卡的 MonoBehaviour 上,我创建了一个名为 OnSceneFinishedLoading 的方法,在场景加载完成时调用,并将其添加到 SceneManager 的 sceneLoaded 事件中:
SceneManager.sceneLoaded += OnSceneFinishedLoading;
这是 OnSceneFinishedLoading 方法:
void OnSceneFinishedLoading( Scene scene, LoadSceneMode mode )
{
Debug.Log( "Scene loaded: " + scene.name );
var path = "Shaders/Levels/" + scene.name;
var collection = Resources.Load<ShaderVariantCollection>( path );
if ( collection != null )
{
Debug.Log( "Shaders/variants: " + collection.shaderCount + "/" + collection.variantCount );
collection.WarmUp();
Resources.UnloadAsset( collection );
}
}
我正在根据关卡的名称从资源中加载集合。如果找到资源并且资源类型正确,我会在着色器变体集合上调用 WarmUp 来编译着色器变体。最后,我卸载了集合,因为我们不再需要它了。
不幸的是,ShaderVariantCollection 类是非常基础的,并不能让我们以任何重要的方式访问内容。例如,我们不能为它创建一个加载栏,获取它包含的变体列表,或者获取变体的详细信息来对它们做任何事情。如果我们拥有这些功能,就有可能构建一个更智能的编辑器工具来使用它们。
结果
以这种方式做事可能会导致关卡开始时有一点延迟,但这不太明显,因为我们大约在同一时间加载所有其他关卡;它还可以防止在最重要的游戏过程中"hitching"。
这可能不是唯一的解决方案!作为 Unity 和 C# 的新手,我很可能错过了一些东西。如果您有任何建议或更正,请随时与我联系或在下面发表评论。