多局战略

作为客户成功团队的顾问,在项目评审期间,我经常与创建游戏转换应用程序的客户打交道。这些应用程序有一个主菜单或主题菜单,提供多种游戏供玩家选择。在这些设置中,主要关注的是如何确保切换游戏之间的时间尽可能短,以及如何确保各游戏的最佳性能。在这篇博文中,我们将根据项目需求探讨不同的方法,以及一些对任何游戏环境都有用的最佳实践,无论是否有游戏切换设置。
在规划多应用环境(无论是游戏、娱乐还是工业仿真)时,最重要的决定是如何管理游戏可执行文件。影响这一决定的因素有很多:
- 该平台能处理多少游戏?
- 游戏规模有多大?
- 这些游戏是用相同的 Unity 版本制作的吗?应用程序的瓶颈是什么?
- 其他因素包括目标硬件、内存和 CPU 以及磁盘速度(固态硬盘 vs 硬盘 vs SD 卡)。
回答这些问题并决定如何处理可执行文件,对于了解我们是否需要为每款游戏提供单独的可执行文件、为多款游戏提供一个共享可执行文件,或将两者结合起来以确保应用程序的最佳性能至关重要。
拥有多个可执行文件是处理使用不同 Unity 版本制作的游戏的绝佳选择。通过这种方法,可以将可执行文件缓存在内存中,并将每个实例留在后台,从而减少游戏之间的切换时间。不过,将所有可执行文件保存在内存中并不总是最佳选择,因为这会对内存造成压力。在单个游戏占用内存较多和/或游戏切换程序中有许多游戏的情况下,应避免使用这种方法。
为了缓解内存限制,游戏可以共享一个可执行文件。这些游戏可以在一个 Unity 项目中,也可以各自拥有自己的项目,只要游戏共享相同的 Unity 版本即可。在 Windows 中使用 Unity 2022 LTS 后,可以使用 -datafolder 参数通过命令行传递变量路径(-datafolder <path_to_folder>),指定选定的游戏数据文件夹,以便切换更改。这种方法的一个潜在缺点是游戏切换时间较慢,因此必须遵循加载最佳实践来减少这一缺点。
无论我们开发的是什么性质的游戏,也无论在哪个平台上开发,重要的是从选择游戏的那一刻起到游戏完全加载到屏幕上所花费的时间越短越好。这一目标对于游戏切换应用尤为重要。
使用Addressables 是处理加载的好方法。Addressables 可根据需要下载和发布内容。这种延迟加载策略是缩短游戏加载时间的最有效方法,因为它限制了初始启动时必须加载的数据量。此外,它还有助于防止与后台游戏有关的任何 CPU 后台活动,因为这些活动会导致 CPU 瓶颈。Addressables:规划和最佳实践 博文是了解 Addressables 以及它们如何帮助提高游戏水平的绝佳起点。
无论我们使用多少可执行文件,都可以通过异步加载 API 来确保更快的加载速度。异步加载时,Unity 主线程将执行一个名为 "主线程集成 "的进程,负责以时间分割的方式初始化本地对象和托管对象。由于该进程会执行一些非线程安全的操作,因此会发生在主线程上,而执行主线程整合的时间是有限的,以防止游戏长时间冻结。可用于集成的时间由Application.backgroundLoadingPriority属性定义。我们建议在加载屏幕时将 backgroundLoadingPriority 设置为高或 50 毫秒,然后在加载完成后将其恢复为BelowNormal (4 毫秒)或低 (2 毫秒)。
加速加载的另一种方法是异步纹理上传。异步纹理加载可以通过协调将纹理和网格上传到 GPU 设置所使用的时间和内存来减少加载时间。了解异步上传管道》博文详细介绍了这一流程的工作原理。
这些做法将有助于加快加载时间:
- 尽可能减少场景内容。使用引导场景,只加载游戏进入可玩状态所需的内容,然后在需要时加载其他场景。
- 在加载屏幕时禁用摄像头。
- 在加载过程中填充 UI 画布时禁用 UI 画布。
- 并行处理网络请求
- 避免复杂的 "唤醒/启动 "实现,利用工作线程。
- 始终使用纹理压缩。
- 流式传输大型媒体文件(如音频文件和纹理),而不是将其保存在内存中。
- 避免使用 JSON 序列化器,而应使用二进制序列化器。
如前所述,内存并不是多游戏环境中唯一需要考虑的问题,后台 CPU 活动也会影响玩家的游戏体验。当游戏不在运行时,其 CPU 仍在运行,从而造成 CPU 饥饿,使运行中的游戏性能达不到最佳状态。防止当前游戏和任何其他 Backend 平台进程出现 CPU 空耗的方法是在 Unity 设置中将 " 后台运行"设置为false 。在后台运行会使 Unity 游戏循环在游戏未对焦时停止。也可通过脚本动态更改设置
public class ExampleClass : MonoBehaviour
{
void Example()
{
Application.runInBackground = false;
}
}
需要注意的是,"后台运行 "设置不会阻止任何自定义脚本线程的运行,因此必须通过Thread.SleepC# 方法将非游戏线程设置为休眠状态。请记住,在 Unity 中使用后台线程需要谨慎编程。由于这些线程无法直接访问 Unity 的 API,因此产生死锁和竞赛条件等问题的几率会更大。要防止出现这种情况,需要与 Unity 主线程进行适当的同步。要正确实现多线程,请查看" Unity 中的.NET 概述 " 手册页面中的 "异步和等待任务的限制"部分,以及MSDN中有关使用线程和线程的文章。Unity 6 引入了Awaitable 类,为异步/等待提供了更好的支持。
识别和修复内存泄漏的原因既困难又耗时,尤其是在开发的后期阶段。虽然听起来很老套,但预防永远胜于治疗。以下是一些有助于在任何游戏环境中防止泄漏的建议:
- 在内存中创建新对象/资产时,确保在不需要时将其删除。如果使用 Addressable,请确保释放未使用的资产。
- 加载/卸载场景时,应适当从内存中移除资产。Unity 不会在关卡卸载时自动卸载资产,因此必须确保从内存中删除任何访问。Resources.UnloadUnusedAssets API 可以帮助清理资产。不过,它可能会导致 CPU 峰值,因为它会返回一个对象,在操作完成前一直屈服,因此应在对性能不敏感的地方使用。
- 避免频繁使用"实例化 "和"销毁 GameObject"。这样做可能会导致不必要的管理分配,同时也是一项昂贵的 CPU 操作。不过,在必须使用 Destroy 的情况下,请确保移除对象的所有引用,以避免Shell 对象泄露。当对象或其父对象通过 Destroy 销毁时,C# 代码会持有一个 Unity 对象的引用,将托管封装对象--其托管外壳--保留在内存中。一旦它所在的场景被卸载,或者它所连接的 GameObject 或它的父对象通过 Destroy 被销毁,它的本地内存就会被卸载。因此,如果其他未卸载的内容仍然引用它,托管内存可能会作为泄漏 Shell 对象继续存在。
- 使用Singletons 实现事件时要注意。单例持有对已订阅其事件的所有对象的引用。如果这些对象的寿命没有单例实例长,而且它们没有取消订阅这些事件,那么它们就会留在内存中,导致内存泄漏。如果事件源在监听器之前被处理掉,那么引用就会被清除,如果监听器被正确地取消注册,那么也就没有引用了。为了解决和防止这个问题,我们建议在所有监听单例事件的对象中实施弱事件模式或IDisposable,并确保在代码中正确处理这些事件。弱事件模式是一种设计模式,可帮助您在事件驱动编程中管理内存和垃圾收集,尤其是在涉及长寿命对象时。当你的订阅者寿命较短,而出版商寿命较长时,它尤其有用。请记住,这些都是 C# 特定的解决方案,仅适用于 C# 事件,UnityEvents 或 Unity UI 工具包并不直接支持这些解决方案。因此,我们建议只在非 MonoBehaviour 脚本中实施这些解决方案。
最后,从早期开发阶段就进行剖析、执行 CI/CD 测试和压力测试可以真正节省时间,因为在出现漏洞时及时发现,就能及时解决问题,节省调试时间,并确保最佳性能。