在游戏开发中,同时实例化大量对象,容易产生卡顿,影响用户游戏体验。为此我们开发了异步实例化 功能。 团结引擎中提供了 AssetBundle.InstantiateAsync() 和 Object.InstantiateAsync() 两种异步实例化方案,可针对不同场景选择使用。
为了避免同时实例化大量 AssetBundle 中的游戏对象而导致的卡顿,用户可以利用 InstantiateAsync API 进行异步实例化,以此来提升性能并保持游戏流畅性。
当需要在场景中创建游戏对象,只需在 AssetBundle 加载完成之后,异步调用 InstantiateAsync API。 使用 InstantiateAsync API 不会影响原始 AssetBundle 的工作流程。
public class Example : MonoBehaviour
{
AssetBundle myLoadedAssetBundle;
void Start()
{
//load Assetbundle
myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle"));
}
void Update()
{
// Press the space key to start coroutine
if (Input.GetKeyDown(KeyCode.Space))
{
if (myLoadedAssetBundle == null)
{
Debug.Log("Failed to load AssetBundle!");
return;
}
// Use a coroutine to Instantiate in the background
StartCoroutine(InstantiateAsyc());
}
}
IEnumerator InstantiateAsyc()
{
yield return myLoadedAssetBundle.InstantiateAsync(myLoadedAssetBundle.GetAllAssetNames()[0]);
}
}
用户代码中调用 Instantiate 时,引擎内分为两步执行:
通过 transfer 序列化构建出对象;
通过 IntegrateMainThread 初始化新创建的对象,进入引擎事件生命周期。
使用同步 Instantiate 时,这两步会在同一帧内完成。
小游戏平台上的 InstantiateAsync 方法 通过分帧处理来优化实例化过程,将对象构建和初始化操作拆分到两帧中执行:
执行第一步 transfer 序列化构建对象时,会使用 JobSystem 来加速,但依旧会在一帧内完成;
在第二步 IntegrateMainThread 增加了一个队列。如果单帧时间内有多次的 InstantiateAsync 调用,则可根据 IntegrationTime 的时间长度,分多帧完成。
由于小游戏是单线程,使用 JobSystem 加速 transfer 序列化构建对象无法达到预期的效果。另外对于单次实例包含大量对象, 例如包含 Collider、Renderer 等的 Prefab,在完成 IntegrateMainThread 进入引擎事件生命周期后,首次更新会有较长的创建物理几何体,编译shader等耗时。
因此,从实际的测试来看,执行第二步 IntegrateMainThread 所在的那一帧总时间远高于执行第一步构建对象所在的帧。
因此在小游戏平台上,我们对第二步 IntegrateMainThread 进一步分帧处理,允许单次 InstantiateAsync 调用能够拆分成更多帧执行。 当前版本为保证 Renderer 和粒子等继承自 Behaviour 的对象运行时序正确,允许最多分成4帧执行。纹理、shader等资源以及用户的 MonoBehaviour 不受该限制影响。
实例化总耗时 | 单帧最高时长 | 7帧总耗时 | 平均每帧耗时 | |
---|---|---|---|---|
Instantiate | 73.2ms | 73.2ms | 160ms | 22.8ms |
InstantiateAsync | 84ms | 35.6ms | 135ms | 19.2ms |
从测试结果看,在实例化相同的对象时,单帧最高时长为 73.10ms。替换为 InstantiateAsync 后,降低为 35.6ms。另外,异步接口因为将工作负载拆分到了4帧完成,7帧总耗时降低为 135ms,同步接口耗时约 160ms。
InstantiateAsync()
接口时需要注意,在完成对象的实例化前,不能提前卸载对象所在的 AB 和其依赖 AB 。否则可能出现材质或 prefab 引用丢失的情况,导致游戏逻辑或渲染出错。