
基于 ECS 设计下的加载管理
基于 ECS 设计下的加载管理
之前在 Addressable 迁移 YooAsset 这篇文章中做了分层设计的相关介绍,本文为分层中详细的解析,以及为什么要这么设计
目标 & 背景
我们当前使用的框架,需要设计成一个基础库,以 Package
的形式进行使用,方便公司中其他项目后续的接入,所以泛用性要求很高,此外,开发者的水平可能会参差不齐,可能会有应届生刚刚接触 Unity 等现实因素的考量,因此我们需要达成如下目标
- 任何新的设计不可对业务逻辑造成开发负担
- 如:加载和卸载必须成对出现,否则会造成资源泄露,在 一些关于代码积累的记录 中有介绍
- 业务逻辑完全不需要知道
YooAsset
的具体内容,甚至连 dll 都不引用 - 如果出现 AB 管理框架无法正常工作,带来灾难级问题,整体更换时,对业务逻辑影响需要降到最小
- 加载逻辑是整体框架的基石,要保证框架内所有的加载均走同一套设计
这里框架设计最开始使用的是
Addressable
,考虑到其名声比较糟糕,后续存在整体换掉的可能,所以从最开始的设计就考虑到了这个问题,这也是为什么整体迁移到YooAsset
非常快的原因
分层设计介绍
为了解决上述所有问题,我们将框架设计成下图这样,每个组件理论上都只做一件事
AutoLoaderComponent
基于 ECS 设计下的资源释放,我们可以做到完全不需要考虑资源对象的 Release,业务逻辑仅需要 Load,而且每个 Release 是绝对精准且及时的(如果出现了资源泄露,大概是其他代码导致的异常,中断了 Release)
那么想实现这个功能就非常简单了,我们创建一个 AutoLoaderComponent
,这个组件没有任何声明的变量,仅在 Dispose
时,调用一下 Release
方法,并给外部提供加载逻辑的扩展方法
此组件的职责仅仅是提供与加载的实体绑定 Dispose 生命周期,并自动释放这个实体加载的所有内容
注意观察这里的 API
权限,针对 GameObject 加载的 API
是 internal 的,我们需要向外部提供 LRU 组的定义,因为当前代码写在 Pacakge
中,而真正的 LUR 组,需要写在业务逻辑中,所以只能用 string
来接,但是!我们并不希望业务逻辑手敲 LRU 组的名字,最终呈现必须是一个 enum
所以这个 API
必须是 internal 权限,同时,我们在这个域中提供一个友元扩展,比如 View.Base.Friend
,我们在业务逻辑这个域中二次扩展这里的 InternalInstantiate
注意!此时 API 就变了,从 string lru_group
变成了 ResGroupType lru_group
,而这个 ResGroupType 就是业务逻辑定义的枚举
手敲
string
对我来说就像写lua
一样,所以必须考虑如何解决这个问题
最终在业务逻辑中就存在两个 View
的域,一个是真正的显示层,另一个是对底层 AutoLoaderComponent
internal 方法有访问权限的 View.Base.Friend
友元层,因为业务逻辑连 AutoLoaderComponent
的访问权限都没有,所以还需要提供一个移除的扩展
基于这样的设计,我们几乎达成了上面所有的目标,甚至做到了业务逻辑连加载组件都看不到的结果
当然,如果你项目不需要这么细致,不用友元,甚至不用
Package
也无妨
YooAssetComponent
这个组件需要做的就是提供一套 LRU 加载规则,并且因为很多时候不同机器因为性能不一样,我们还需要对组进行最大激活数量的限制,同时为了方便上面 AutoLoaderComponent
加载还需要提供 hash 和资源之间的映射关系
这里我们需要先从业务逻辑的组定义开始看起,上面的 ResGroupType
中有一个受击特效的组,在这个 enum
上,绑定了两个 Attribute
,一个标记 LRU 最大数量,另一个标记 最大特效激活数量
那么为了方便外部传入配置,我们需要在底层定义 YooAssetComponent
的配置,也可以理解为初始化参数,具体定义如下
这里你会注意到有一个 EditorPrefs
混入了配置中,这是因为我希望提供一个可以在 Editor
快速切换到底用哪种 YooAssets.EPlayMode
来加载的 GUI 工具
这个工具是 git 上开源的仓库 unity-toolbar-extender 具体不做介绍,感兴趣可以自行了解
基于现有设计,我们就可以按照下发的过程进行实现
- 对 hash 进行分组,存放、查询、删除等功能
- 对当前 LRU 组,检查最大激活数量,如果超过限制,直接给一个 null
- 对当前 LRU 组,进行维护,超出 LRU 最大数量的,交由下一次进行 Release
更具体的代码这里就不贴了,可以自己搜搜 LRU 的具体实现,下图为暴露在外部的 API 参考
YooAssetsShim
这个组件是对现有 YooAsset API
的垫片实现,因为个人比较喜欢 Addressable
中直接 Release(Object)
实现对具体 handle 的释放,以及针对 SA
图集加载中,子图集的配置方式 Assets/Res/xxx.spriteatlas[1]
这种图集配置方式,
Luban
中 path 校验器也是支持的!
那么这个组件的职责就非常简单了,提供 Object
和 handle
的索引关系,以及上述 SA
加载的解析,这里的代码在 Addressable 迁移 YooAsset 贴了一次,下面再重复贴一遍
最后
就像 一些关于代码积累的记录 这篇文章中,我希望传达的 重要的永远不是怎么做,而是为什么要这么做 一样,这里的代码没有任何高深的东西,底层框架设计更多的是思考如何降低理解成本,以及如何降低开发成本
而这些是无法通过复制代码学到的
- 感谢你赐予我前进的力量