MMORPG游戏核心技术-跨平台的动态资源管理(三)
序言:
早在十几年前,当时我玩的游戏都是通过文字来“眉目传情”,然而随着技术的不断进度,2D/3D图形技术高速崛起,伴随着技术的不断进步,游戏中资源管理是游戏开发者在项目启动的时候最需要考虑的事情,对于MMORPG游戏来说,庞大的资源池如何进行管理相信已经让很多开发者头疼了。当然,具体的策略跟开发者选择的平台有直接关联。
端游:将所有的资源打包到安装包,所以动则几个G的安装包再正常不过了。
页游:再浏览器中运行游戏,动态从文件服下载资源,之后保存到内存,从而避免多次下载影响玩家体验。
微端:提供几MB的运行程序,动态的从文件服下载资源,之后保存到本地,之后使用的资源从本地获取,除非资源库做了版本叠加。这里如果资源的被改变了需要从文件服获取到最新的,覆盖本地的文件。
手游:局限于网络环境,提供了首包可以运营的运行包。之后再动态的下载资源到SDK中。与微端的思路一致。
下面我将讨论如何在UNITY3D中实现动态资源管理。
正文:
庆幸的是UNITY3D在资源的动态管理方面提供了比较好的技术支持。
针对UNITY3D,提供了二种动态加载机制:一是Resource.Load, 一是AssetBundle。
1、Resource.Load:只能访问程序打包时Resource资源包里面的资源,也就是Resource目录下的资源,外部资源无法访问。
2、 AssetBundle:Unity提供将资源打包成AssetBundle的方法,之后可以运行时动态加载,可以指定路径和来源。
我们本章采用的是使用AsssetBundle来实现跨平台的资源管理。
函数介绍
WWW类:提供HTTP访问的功能。
WWW.LoadFromCacheOrDownload函数:从缓存加载一个带有特定版本号的资源包,如果资源包目前没有被缓存,将从文件服下载,并放入到本地的缓存区。
AssetBundle.LoadFromMemoryAsync函数:通过文件读取byte[]加载Assetbundle。
AssetBundle.LoadAssetAsync函数:从构建好的AssetBundle中加载资源。
AssetBundleRequest类: 资源包请求类。
StartCoroutine函数:c#提供的协程,类似于多线程的概念,意思就是启动一个辅助的线程。
整套类架构
流程
ResourceManager类
LoadResource函数:资源加载的方法调用,原则上所以的资源加载都通过它来处理。 代码里的ResInfo其实是一个结构体,包含了资源ID,资源名字,资源版本,资源路径等等信息。具体的会在下一章介绍。这里有一个CreateResource方法,方法主要是创建资源类Resource,而m_mapResource则是资源管理类键值表。至于如何管理内存资源,可能会在后续的文章中进行介绍。这里有一个核心的思想,就是对资源类Resource进行引用计数,当引用计数为0的时候则Destroy掉资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public virtual Resource LoadResource( ref ResInfo resInfo,ResourceLoaded resLoad, ResourceCancel resCancel) { var outRes = GetResource(resInfo); if ( null != outRes) { if (outRes.IsLoaded()) { return outRes; } if (outRes.GetResState() == eResourceState.eRS_NoFile) { Debug.LogError(resInfo.strName + "resource is does not exist" ); } else { outRes.AddEvent(resLoad, resCancel); } return outRes; } //加载资源 outRes = CreateResource( ref resInfo); outRes.AddEvent(resLoad, resCancel); m_mapResource[resInfo.nResID] = outRes; resInfo.uDownLoadWeigth = ( uint ) Mathf.Clamp(( int ) resInfo.uDownLoadWeigth, 0, 2); m_DelayQuestDownLoader[( int )resInfo.uDownLoadWeigth].Add(outRes); outRes.AddRef(); return outRes; } |
Update函数:将放入到下载列表m_DelayQuestDownLoader进行下载请求,将下载完成的列表m_DelayNotifyDownLoaded进行逻辑回调。比如:打开一个UI,之前一直是loading界面,加载完成之后则进行UI显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public virtual void Update(float fTime, float fDTime) { //Profiler.BeginSample("ResourceManager:Update"); //通知下载组件帮助下载资源 int iCount = m_DelayQuestDownLoader. Count ; m_isCache = true; for ( var i = 0; i
m_isCache = false;
|
OnDownLoadend函数:资源加载完之后回调处理,并保存到m_DelayNotifyDownLoaded列表中。
1 2 3 4 5 | public void OnDownLoadend(Resource res) { m_downNotCacheRef--; m_DelayNotifyDownLoaded.Add(res); } |
DownLoadHelperManager类
Init()函数:初始化DownLoadHelperManager管理类,针对于不同平台开通不同的WWW数量,在PC平台里面WWW数量为3,手机平台里WWW数量为1.具体为什么是3和1,我只能说UNITY官方是这么说的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public void Init() { if (UnityEngine.Application.platform == RuntimePlatform.WindowsPlayer || UnityEngine.Application.platform == RuntimePlatform.WindowsEditor) { m_uWWWNums = 3; } else if (UnityEngine.Application.platform == RuntimePlatform.Android || UnityEngine.Application.platform == RuntimePlatform.IPhonePlayer || UnityEngine.Application.platform == RuntimePlatform.WP8Player) { m_uWWWNums = 1; } else if (UnityEngine.Application.platform == RuntimePlatform.WindowsWebPlayer || UnityEngine.Application.platform == RuntimePlatform.WebGLPlayer) { m_uWWWNums = 1; } for ( int i = 0; i < m_uWWWNums; i++) { m_object.Add( new GameObject( "down" + i.ToString())); m_downhelpers.Add(m_object[i].AddComponent()); m_object[i].SetActive( false ); m_DelayNotify.Add( new DelayNotify()); } } |
HelpMe函数:查找列表中WWW是否有空闲的,如有有空闲的进行下载,如果都已经在忙碌状态则等待下一次下载.其中的m_DelayNotify为保存回调事件,在资源下载完成之后进行回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public bool HelpMe(Resource res, HelpEnd end) { for ( int i = 0; i < m_uWWWNums; i++) { if (m_DelayNotify[i].res == null ) { m_DelayNotify[i].finish = end; m_DelayNotify[i].res = res; m_object[i].SetActive( true ); m_downhelpers[i].InitNewDownLoad( ref res, this ); return true ; } } return false ; } |
SingletonBeforeUpdate函数:检测下载中的资源是否下载完成,如果下载完成进行逻辑回调,通知到ResourceManager类,并将当前WWW设置为可以下载资源状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 | public override void SingletonBeforeUpdate( float fTime, float fDTime) { for ( int i = 0; i < m_uWWWNums; i ++ ) { if ( null != m_DelayNotify[i].res && !m_downhelpers[i].IsStart()) { m_object[i].SetActive( false ); m_DelayNotify[i].finish(m_DelayNotify[i].res); m_DelayNotify[i].finish = null ; m_DelayNotify[i].res = null ; } } } |
DownLoadHelper类
InitNewDownLoad函数:通过协程正式下载资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public void InitNewDownLoad(ref Resource res, DownLoadHelperManager Parent) { Clear(); m_res = res; # if UNITY_EDITOR && !UNITY_WEBPLAYER && !UNITY_WEBGL && !UNITY_IOS && !UNITY_ANDROID StartCoroutine(DownLoadEditor()); #elif UNITY_WEBPLAYER || UNITY_WEBGL && !UNITY_EDITOR StartCoroutine(DownLoadWeb()); #elif UNITY_ANDROID || UNITY_IOS && !UNITY_EDITOR StartCoroutine(DownLoadMobile()); #elif UNITY_STANDALONE_WIN && !UNITY_EDITOR StartCoroutine(DownLoadWindows()); # endif } |
DownLoadMobile函数:下载移动端的资源,对于函数中使用的LoadAssetAsync、Unload(false)、 m_www.Dispose()都是内存管理中一些Unity提供的管理方式。其中涉及到Assetbundle的内存管理。如果大家存在疑问可以留言。核心思路是将本地资源提供LoadFromMemoryAsync得到Assetbundle数据,本地无法获取的资源将通过WWW得到数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | private IEnumerator DownLoadMobile() { m_bStart = true ; if (m_res.IsNeedLoadLocalFile()) { AssetBundleCreateRequest abcr = AssetBundle.LoadFromMemoryAsync(m_res.GetByte()); yield return abcr; if (abcr.isDone) { m_res.SetByte( null ); if (abcr.assetBundle != null ) { m_res.SetDownLoadProcess(1.0f); if (m_res.IsNeedLoadAll()) { m_res.SetResource(abcr.assetBundle.LoadAllAssets()); } else { AssetBundleRequest request = abcr.assetBundle.LoadAssetAsync(m_res.GetOrgName()); yield return request; m_res.SetResource(request.asset); } abcr.assetBundle.Unload( false ); } } } else { //从包里加载 m_www = new WWW(m_res.GetFullUrl()); yield return m_www; if ( null != m_www) { if ( null != m_www.error) { Debug.LogError(m_www.error + "|" + m_res.GetFullUrl()); } } if (m_www.assetBundle != null ) { m_res.SetDownLoadProcess(1.0f); if (m_res.IsNeedLoadAll()) { m_res.SetResource(m_www.assetBundle.LoadAllAssets()); } else { AssetBundleRequest request = m_www.assetBundle.LoadAssetAsync(m_res.GetOrgName()); yield return request; m_res.SetResource(request.asset); } m_www.assetBundle.Unload( false ); } } m_www.Dispose(); m_res = null ; m_bStart = false ; } |
DownLoadWeb函数:网页端资源管理,每次请求的资源都将从资源服上获取,本地硬盘不保存任何数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | private IEnumerator DownLoadWeb() { while (!Caching.ready) yield return null ; m_bStart = true ; m_www = new WWW(m_res.GetFullUrl()); if (m_www == null ) { Debug.LogWarning( "www is null" ); } yield return m_www; if ( null != m_www) { if ( null != m_www.error) { Debug.LogError(m_www.error + "|" + m_res.GetFullUrl()); } } if (m_www.assetBundle != null ) { m_res.SetDownLoadProcess(1.0f); if (m_res.IsNeedLoadAll()) { m_res.SetResource(m_www.assetBundle.LoadAllAssets()); } else { AssetBundleRequest request = m_www.assetBundle.LoadAssetAsync(m_res.GetOrgName()); yield return request; m_res.SetResource(request.asset); } m_www.assetBundle.Unload( false ); } m_www.Dispose(); m_res = null ; m_bStart = false ; } |
DownLoadWindows函数:与网页端唯一不同的是,对于有缓存标志的资源,都将使用LoadFromCacheOrDownload方法,将文件服的数据写入到本地硬盘,之后从本地硬盘获取Assetbundle数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | private IEnumerator DownLoadWindows() { m_bStart = true ; if (m_res.GetResInfo().nCacheflags >= 1) { m_www = WWW.LoadFromCacheOrDownload(m_res.GetFullUrl(), m_res.GetResInfo().iVersion); } else { m_www = new WWW(m_res.GetFullUrl()); } if (m_www == null ) { Debug.LogWarning( "www is null" ); } yield return m_www; if ( null != m_www) { if ( null != m_www.error) { Debug.LogError(m_www.error + "|" + m_res.GetFullUrl()); } } if (m_www.assetBundle != null ) { m_res.SetDownLoadProcess(1.0f); if (m_res.m_Cahcedown) { m_www.assetBundle.Unload( true ); } else { if (m_res.IsNeedLoadAll()) { m_res.SetResource(m_www.assetBundle.LoadAllAssets()); } else { AssetBundleRequest request = m_www.assetBundle.LoadAssetAsync(m_res.GetOrgName()); yield return request; m_res.SetResource(request.asset); } m_www.assetBundle.Unload( false ); m_www.Dispose(); } } m_res = null ; m_bStart = false ; } |