开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2006

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 1543|回复: 31

[士郎] 用面向对象思想,管住Unity调皮的AssetBundle

[复制链接]  [移动端链接]
排名
3
昨日变化

5733

主题

6219

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
14283
威望
30
注册时间
2013-7-29
在线时间
2869 小时
最后登录
2017-12-13

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2017-11-17 11:10:19 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
KSFramework是一个Unity 5 Asset Bundle开发框架和工具集,专注于运行时热重载,使用了SLua作为脚本引擎。
相信每一个使用Unity引擎的老司机都会被它的一个功能所困扰过——AssetBundle。

变味的易用性
Unity是一款主打易用性的游戏引擎。它的开发团队,希望开发者可以低门槛、快速、容易地使用Unity开发游戏,所以Unity在最初以类似JavaScript、类似Python的脚本语言作为主要开发语言。

在移动互联网起步阶段,小游戏是手机游戏应用的主旋律。无数的创业者需要一款跨平台、简单易用的游戏引擎,来快速开发3D游戏产品,这也是Unity近年大火的重要原因。

随着这两年手机游戏过度到端游化、大型化,可以看到的是,Unity已经成为手游开发的首选方案了,其自身的功能和各种围绕它的技术生态日催完善,C#语言也当仁不让的成为首选开发语言。

但是,她的骨子里依旧还是那个标榜易用性的游戏引擎,由于她“易用”这个特点,使用Unity开发大型游戏,开发团队如果在开发之初按照Unity标准的易用方式来制作游戏,到了后期就免不了出现各种各样的坑,需要花费大量的时间去重新重构和代码维护。最典型的情况就是:

一个开头快速功能迭代开发的游戏,直到中后期才萌生热更新的需求,而在Unity里做资源热更(AssetBundle)和代码热更(Lua)是一个不小的工作,需要耗费相当多的精力。

怎么办?加班或延期呗。

回想使用Unity这5年的搬砖经历,Unity里最令我沮丧的功能是它的AssetBundle——不论是打包还是加载,都要花费开发人员大量的时间成本去研究和应用。
起初,你以为它可以像Resource.Load那样轻松加载资源?不是的,它还要手工码代码打包;你以为打包完了就能直接加载调用了?不是的,它还要在加载时注意处理依赖关系。

从Unity的资源方式说起
如前面所说,Unity是一款主打易用性的游戏引擎。它的资源打包方式有两种。



1.png



一种是使用Resource模式,这种模式更像是端游时代的资源打包方式,把所有的游戏资源打包成一整块的文件,然后通过索引文件去记录索引,各个具体资源文件散落不同大文件里面的不同索引位置。比如说,暴雪公司的魔兽世界、魔兽争霸的mpq文件就是这样的一个思路。


这种方式完全体现了Unity的易用性,比如一个图片,不管它是PNG、TGA还是PSD,只要丢到Unity里,都会被统一转化成Unity的Texture格式。简单、傻瓜,非常适合小游戏的开发。但是它也有缺点,就是每一次发布最终编译包的时候,都会重新对资源进行一次打包,速度非常的慢,这也反映出它的致命问题,由于资源全部堆砌在一块了,要替换其中的资源变得困难——难以进行资源热更新。


另一个种模式,AssetBundle模式。相比较之下,这个模式看上去就像后来迭代版本的时候加出来的一个功能——基于原有Resource模式的不足,提供一个对资源方式更自由控制的方式。


在Resource模式中,开发者是几乎完全不用操心他们的资源管理的技术细节。直接使用编辑器进行资源编辑,用完以后开发完以后直接打包最终程序就可以了。而Asset Bundle模式则需要自己进行资源的打包加载管理。


2.png


在Unity 5.x之前的版本,3.x和4.x,AssetBundle是一个非常难用的功能。你不但要操心资源的管理规范,还要写大量的代码控制的它们的打包,更要命的是,打包不但速度慢,还有数之不尽的坑。相信不少开发团队,都在AssetBundle上花费过不少精力和时间。


我经历过了4个不同的中大型游戏规模的Asset Bundle打包,躺过其中相当多的坑,逐渐的开始掌握它的脾性。回头仔细想一下,其实很多坑完全是没有必要的,但是前提是在设计之初给予相对的重视,提炼统一的方案,就不必说导致后期失控的状况。


在Unity 5.x里,官方推出了一个全新的打包方案,对于程序员而言,可以仅用一行代码打包所有的AssetBundle。尽管它里面还是有一些坑,可是却大大减轻了开发团队的工作。打包方式变简单了,大家可以集中精力研究怎么更好的去把这些Asset Bundle加载起来了。


更好的方式去加载Asset Bundle

Asset Bundle加载资源的API非常的简单,核心其实只是两个函数,一个同步和一个异步。

[AppleScript] 纯文本查看 复制代码
// 同步加载,直接返回AssetBundle
AssetBundle.LoadFromFile(path);
// 异步加载,返回AssetBundleCreateRequest
AssetBundle.LoadFromFileAsync(path);



相信每一个游戏开发团队都在官方的这些AssetBundle加载API基础上,封装出自己的加载管理类,这几乎是必须的。封装的方式千奇百怪,怎么样去封装,去封装的比较好?


接下来我所讲述的是一种模仿面向对象的AssetBundle加载管理类封装方式,实现方便加载的同时,又可以更容易的进行实时调试。

这种基于面向对象的方式来设计的AssetBundle加载管理器,我们给他一个名字叫ResourceModule,方便下文讲述。它的主要目的是为了让开发者在方便的加载资源的同时,提供方便的实时调试功能,并且你会在过程中了解到资源文件的热更新策略。

下边将会分成五个部分来介绍ResourceModule:加载、调试、异步、垃圾回收、路由。

加载器——基于追踪对象
[AppleScript] 纯文本查看 复制代码
// 同步加载,return 直接返回AssetBundle对象[/color][/size][/font]
[font=微软雅黑][size=3][color=#696969]
AssetBundle.LoadFromFile(path);  
// 异步加载,return 返回AssetBundleCreateRequest对象
AssetBundle.LoadFromFileAsync(path);



在Unity的标准Asset Bundle加载接口中,同步加载返回了行为结果,异步加载则返回了行为追踪对象。具体来说,同步加载,直接就返回了资源的AssetBundle;异步加载,则返回了异步加载的追踪对象AssetBundleCreateRequest。追踪对象,用于之后进行资源异步加载情况跟踪,被协程轮询判断是否已经异步加载完毕,若完成了可从追踪对象里获取加载资源。


由于同步和异步的加载API不一样,在项目实际应用时,往往没有统一的加载接口。要避免这种情况,可以统一加载行为,都返回追踪对象。这也是ResourceModule加载方式的核心。


函数式接口

在ResourceModule里,提供了两个最简化的加载Asset Bundle API,看上去就跟Resources.Load一样简单。

[AppleScript] 纯文本查看 复制代码
// 同步方式
var loader = ResourceModule.LoadBundle(path);
var ab = loader.Asset; // get UnityEngine.Object sync

// 异步方式
var loaderAsync = ResourceModule.LoadBundleAsync(path);
var abAsync = loaderAsync.Asset; // null asset, async loading.
while(!loaderAsync.IsFinished)
{
    yield return null;    // 协程等待
}
abAsync = loaderAsync.Asset; // get UnityEngine.Object async


这里的函数式接口跟官方的是不一样的地方:ResourceModule的函数式接口的返回值,将始终还是一个AbstractResourceLoader对象,也就是“追踪对象”——对于异步加载,使用追踪对象,可以判断异步加载的进度并获取加载后的资源;对于同步加载,使用追踪对象立即获取资源;它也提供错误处理信息;并且后续所讲及的实时调试,也是基于这个追踪对象。

两个接口的返回值类型是一样的。

[AppleScript] 纯文本查看 复制代码
abstract class AbstractResourceLoader {
    bool IsComplete {get; set;}
    bool IsError {get; set;}
    object ResultObject;
    // .......
}

看上去,Asset Bundle的加载接口很简单。但本质来说,这只是接下来Loader对象式加载的一个使用简化。

Loader对象

ResourceModule.LoadBundle的本质,是使用AssetFileLoader进行加载行为,并把自己作为追踪对象返回。AssetFileLoader本身是对UnityEngine.Object进行处理,它自身可以通过配置,修改成使用Resources.Load模式或AssetBundle模式。


3.png

当AssetFileLoader配置成AssetBundle加载模式,它就会调用AssetBundleLoader进行AssetBundle加载行为,而AssetBundle本身则使用HotBytesLoader进行AssetBundle文件字节码进行加载。



4.png



HotBytesLoader是一个热更新桥接器——根据资源“相对路径”和“热更新资源目录”,当热更新资源目录存在对应路径的文件时,使用热更新目录的资源。
所以说,一次加载行为,会有4个Loader产生,它们之间形成链式关系。即AssetFileLoader -> AssetBundleLoader -> HotBytesLoader -> WWWLoader。
如前所说,ResourceModule函数式加载其实Loader对象式加载的一个简化。每一次加载行为都会对应一个Loader对象。那么基于AssetFileLoader,由于它是一个单独的解耦对象,我们还可以针对它一些特定需求的功能扩展


5.png


在不同类型的资源加载中,不同的行为被划分成不同的Loader对象。来给资源加载代码赋予更好的维护性和可读性。同时,由于链式关系的存在,指定的AssetBundle文件,永远只会被加载一次——这样来避免一些项目中常见的AssetBundle文件被重复加载问题。


每一个Loader对象,都有一个静态.Load函数,这是一个工厂函数,每一个Loader对象通过自身的Load静态函数生成Loader,来确保引用计数、状态 的正确。

[AppleScript] 纯文本查看 复制代码
AssetFileLoader.Load(path);  //...
StaticAssetLoader.Load(path); // ...


对象式调试

6.jpg



Unity的Profiler可以方便的提供各种Unity运行时资源的调试功能。它采用快照的方式,捕捉当前运行时状态。只要你对Profiler足够的熟悉,大部分运行性能问题都从中发现。

对于AssetBundle加载,Profiler一是不能实时获取动态,二是即使发现了AssetBundle残留,也难以发现具体是哪部分代码残留了。而这类调试的事情,是可以通过我们游戏里的统一加载接口来更好的发现的。

由于加载所使用的每一次行为都对应着一个Loader追踪对象,所以当我们要对资源加载行为,进行实时调试,简单来说就是对这些追踪对象进行监视。这里用了一个偷懒的方式:Unity引擎编辑器本身就是基于游戏对象的。

那好吧,我们把加载对象,以游戏对象的方式,显示在编辑器上,每创建一个Loader,就紧跟着一个GameObject,达到可视化实时调试的目的。


7.jpg



从上图可知,每一个Loader追踪对象(加载行为)都被一个静态的全局列表包存起来,因此可以额方便的在Unity编辑器上显示它们的具体数量,我们把这些称为“调试对象”,点击后右边还能显示其引用计数和资源路径。

异步风格

Unity的协程是一个非常好用的单线程异步编程方式,让普通开发者在没有线程编程、异步编程的基础下,也可以方便的进行异步编程。

另一种常见的单线程异步编程方式是回调Callback风格,是非阻塞IO语言NodeJS的主要异步方式。

无论是协程还是或者回调,它们都有一个共同的特点,都可以做到是基于单线程进行了异步编程。关于异步编程这个话题可以引申出很长的篇幅,这里就不多介绍了。

Unity开发中,两者各有优点。协程可以让看起来同步的代码实现了异步,但在Unity中它的一个蹩脚的地方是需要另写一个IEnumerator ()函数; 而Callback风格则由于C#中强大的匿名函数语法,使得让异步代码写起来更加的方便。

ResourceModule中两种异步风格并存,可以根据喜好使用。

协程式
[AppleScript] 纯文本查看 复制代码
IEnumerator LoadSomething()
{
    var loader = StaticAssetLoader.Load(path);
    while (!loader.IsFinished)
    {
        yield return null;
    }
    if (loader.IsError)
    {
        // error
    }
    var asset = loader.Asset; // get asset...
    // ...
}


这种协程可谓在Unity中最为常见、舒服的异步方式了。使用起来跟Unity原生的WWW差不多。


回调式




[AppleScript] 纯文本查看 复制代码
StaticAssetLoader.Load(path, (isOk, asset) => {

    // get asset async... do something....

});


相比而言,匿名函数回调的异步风格,可以写更好的代码,并且调用代码更紧密连接。

垃圾回收——基于引用计数

我们都知道Java/C#语言的核心是面向对象,他们之所以那么的强大还有一个杀手锏,就是完全自动垃圾回收机制。因其基于对象的设计,所有对象的生命期都是可以被监视和管理的。


做过iOS开发的同学也知道,Objective-C语言的内存管理使用引用计数的方式来实现的。
由于ResourceModule的加载行为都是基于对象,多个Loader对象有互相引用的关系,ResourceModule模仿了Objective-C引用计数的方式来实现AssetBundle对象的管理。

8.jpg


点开调试对象的GameObject,就能看到调试对象的引用计数信息和加载所耗费的时间。

资源的释放
如要对加载Loader追踪对象进行引用计数递减,可以调用每个Loader里的Release函数:

[AppleScript] 纯文本查看 复制代码
loader.Release(); // 引用计数-1

当一个Loader的引用计数为0时,它就会进入到释放队列,待几秒后释放。

为什么不像java那样能全自动的判断对象是否无用自动释放?

嗯,ResourceModule的加载器需要手工释放引用计数。

因为没法捕捉GameObject对象删除事件,Unity并没有提供这样的事件出来监视游戏对象的删除事件,所以无法捕捉说什么时候去把这一个对象的引用递减,所以只能手动的去,进行引用计数的管理。

延迟清理

9.jpg


当一个加载对象被引用计数减为0的时候,他不会被立刻释放。因为存在这样一种场景:当引用变成0的同一时间,同样的资源又被创建一份新的,引用计数立刻变回1。所以如果说当他引用计数为0时候,立刻就被清理了,同时又被创建,这里,就会造成了重复的对这份内存资源创建和释放。

路由——管理资源加载的路径

Unity是一个跨平台的游戏引擎,每一个平台都会有它特殊的处理资源的路径方式,在Unity中一般我们常见的是StreamingAssets和PersistentDataPath两种路径。


可是这里面,也隐含有不少的坑,比如说,在windows平台里面,路径URL,斜杠必须得3个///。安卓平台下,StreamingAssets目录是不能同步读取的(APK内目录),但是包括iOS在内的其他所有平台都是可以通过同步File.ReadAllBytes读取的。


不仅如此,由于Asset Bundle的打包是平台定向性的:打出的Asset Bundle,不能再iOS下使用;反之亦然。因此,AssetBundleLoader加载器在实际运行时,需要一个路由管理器来告诉它什么样的平台,使用哪里的Asset Bundle目录。我把这叫作“路由”。


10.jpg



所以在ResourceModule中,路由管理器做了很多路径的识别的工作,来统筹各种不同平台下的资源路径,来整个Asset Bundle模块的开箱即用。

热更新
我们使用AssetBundle,无非最想解决的就是一个需求——热更新。

热更新的两个核心要素,资源路径读取与下载更新。

资源路由管理器,除了平台差异化路径处理,另外的核心功能就是热更新路径处理了——即此前所说的,优先判断PersistentDataPath路径是否存在指定的热更文件。

后记

11.jpg


以上我们分别从加载、调试、异步、垃圾回收、路由5个方面,介绍了这种基于面向对象的思想设计的用来进行AssetBundle加载的ResourceModule管理器。
它的本质是将行为进行对象化。概括来说就是把加载行为以对象的方式保存起来。

它的代码开源放在 「Github ResourceModule」 ,是Unity开发框架KSFramework的核心部分。对于很多使用者来说,ResourceModule就像一个黑箱子,虽然一直能用,但是一直不好理解它的内部构思,所以就有了本文。

Unity的资源管理是一个很大的话题,本文仅仅从它的加载方式着手提出一种方案,更多深入的细节,更多的坑,还得伴随项目的进度而慢慢积累经验。更多的经验,可以私信跟我交流,我也愿意跟你分享。

一不小心洋洋洒洒的写了4000多字,篇幅稍长,如果对Asset Bundle机制没有太深入了解的话,当中有一些地方可能不好读懂。出现这种情况,那肯定不是你的问题,而是我没有写清楚。希望在评论处留下宝贵的建议!




知乎:陈小霖


评分

参与人数 3鲜花 +9 收起 理由
subc_manew + 5 赞一个!
Ra蹊 + 2 很给力!
a258539 + 2

查看全部评分

本帖被以下淘专辑推荐:

  • · 场景|主题: 2, 订阅: 0

跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

7日久生情
3280/5000
排名
4934
昨日变化
36

4

主题

2740

帖子

3280

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
209186
好友
5
蛮牛币
3787
威望
0
注册时间
2017-3-1
在线时间
306 小时
最后登录
2017-12-13
发表于 2017-11-17 11:15:46 | 显示全部楼层
谢谢分享

回复

使用道具 举报

7日久生情
3218/5000
排名
92
昨日变化

5

主题

109

帖子

3218

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
12951
好友
2
蛮牛币
9820
威望
0
注册时间
2014-1-19
在线时间
882 小时
最后登录
2017-12-13
发表于 2017-11-17 12:38:38 | 显示全部楼层
楼主果真是对ab管理有着深刻的理解, 感谢分享好文, 受教 了.

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1055/1500
排名
2341
昨日变化
8

13

主题

155

帖子

1055

积分

Rank: 6Rank: 6Rank: 6

UID
158398
好友
2
蛮牛币
1068
威望
0
注册时间
2016-8-30
在线时间
393 小时
最后登录
2017-12-12
发表于 2017-11-17 13:57:43 | 显示全部楼层
站长妹纸萌萌哒!

回复

使用道具 举报

6蛮牛粉丝
1036/1500
排名
3034
昨日变化
17

24

主题

205

帖子

1036

积分

Rank: 6Rank: 6Rank: 6

UID
89579
好友
3
蛮牛币
1957
威望
0
注册时间
2015-4-5
在线时间
419 小时
最后登录
2017-12-13
发表于 2017-11-17 14:27:26 | 显示全部楼层
姐姐好厉害

回复

使用道具 举报

排名
17372
昨日变化
568

0

主题

36

帖子

88

积分

Rank: 2Rank: 2

UID
248353
好友
0
蛮牛币
20
威望
0
注册时间
2017-10-12
在线时间
28 小时
最后登录
2017-12-13

一贫如洗

发表于 2017-11-17 16:18:48 | 显示全部楼层
厉害谢谢分享

回复

使用道具 举报

5熟悉之中
659/1000
排名
3675
昨日变化
4

0

主题

163

帖子

659

积分

Rank: 5Rank: 5

UID
136635
好友
0
蛮牛币
536
威望
0
注册时间
2016-2-15
在线时间
174 小时
最后登录
2017-12-13
发表于 2017-11-17 17:28:28 | 显示全部楼层
很好,学习了
[发帖际遇]: hed_tan 乐于助人,奖励 2 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

排名
40323
昨日变化
31

0

主题

9

帖子

18

积分

Rank: 1

UID
250899
好友
0
蛮牛币
7
威望
0
注册时间
2017-10-26
在线时间
5 小时
最后登录
2017-12-1
发表于 2017-11-17 19:38:29 | 显示全部楼层
顶。。。。。。。。。。。。。。。

回复

使用道具 举报

排名
11387
昨日变化
175

0

主题

4

帖子

93

积分

Rank: 2Rank: 2

UID
252847
好友
0
蛮牛币
240
威望
0
注册时间
2017-11-6
在线时间
25 小时
最后登录
2017-12-13
发表于 2017-11-18 06:51:27 | 显示全部楼层
谢谢分享

回复

使用道具 举报

5熟悉之中
782/1000
排名
3455
昨日变化
20

1

主题

251

帖子

782

积分

Rank: 5Rank: 5

UID
206337
好友
2
蛮牛币
2805
威望
0
注册时间
2017-6-5
在线时间
190 小时
最后登录
2017-12-13
发表于 2017-11-18 08:35:51 | 显示全部楼层
确实没读懂。。。

回复

使用道具 举报

7日久生情
3269/5000
排名
173
昨日变化

0

主题

355

帖子

3269

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
2484
好友
2
蛮牛币
2933
威望
0
注册时间
2013-8-23
在线时间
1072 小时
最后登录
2017-12-13
发表于 2017-11-18 10:12:56 | 显示全部楼层
我只能说,牛逼!

回复

使用道具 举报

排名
180
昨日变化

1

主题

530

帖子

3165

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
16458
好友
7
蛮牛币
4090
威望
0
注册时间
2014-3-5
在线时间
814 小时
最后登录
2017-12-13
发表于 2017-11-18 11:36:50 | 显示全部楼层
本帖最后由 lhy_ps 于 2017-11-18 12:05 编辑

楼主能发一下代码或者demo工程吗。--不好意思,看到 github上 有了
https://github.com/mr-kelly/KSFramework

回复 支持 反对

使用道具 举报

3偶尔光临
290/300
排名
8486
昨日变化
74

0

主题

117

帖子

290

积分

Rank: 3Rank: 3Rank: 3

UID
229748
好友
0
蛮牛币
381
威望
0
注册时间
2017-7-1
在线时间
67 小时
最后登录
2017-12-12
发表于 2017-11-18 13:41:58 | 显示全部楼层
谢谢分享

回复

使用道具 举报

3偶尔光临
256/300
排名
10878
昨日变化
148

1

主题

121

帖子

256

积分

Rank: 3Rank: 3Rank: 3

UID
252485
好友
0
蛮牛币
300
威望
0
注册时间
2017-11-4
在线时间
64 小时
最后登录
2017-12-12
发表于 2017-11-18 16:25:00 | 显示全部楼层
关注一下,666666

回复

使用道具 举报

5熟悉之中
563/1000
排名
3874
昨日变化
3

1

主题

114

帖子

563

积分

Rank: 5Rank: 5

UID
79201
好友
0
蛮牛币
1331
威望
0
注册时间
2015-3-12
在线时间
144 小时
最后登录
2017-11-20
发表于 2017-11-18 22:40:04 | 显示全部楼层
感谢分享

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表