MMORPG游戏核心技术-跨平台的动态资源管理(三)

发表于2016-06-07
评论0 1.8k浏览

序言:
早在十几年前,当时我玩的游戏都是通过文字来“眉目传情”,然而随着技术的不断进度,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 "" {="" if="" (m_delayquestdownloader[i].count=""> 0)
                {
m_isCache = false;
                    if (DownLoadHelperManager.Inst.HelpMe(m_DelayQuestDownLoader[i][0], OnDownLoadend))
                    {
                        m_downNotCacheRef++;
                        m_DelayQuestDownLoader[i].RemoveAt(0);
                    }
                }
            }
            //通知下载完成了
            //每帧处理衡量的回调避免出现顿卡。
            iCount = m_DelayNotifyDownLoaded.Count - 1;
            for (var i = iCount; i >= 0; i--)
            {
                var res = m_DelayNotifyDownLoaded[i];
                if (!res.IsNeedOtherResourceLoaded())
                {
                    m_DelayNotifyDownLoaded.RemoveAt(i);
                    _Loaded(res);
                    if (!(--s_nFrameNotifyNums > 0))
                    {
                        break;
                    }
                }
                else
                {
                    if (res.IsNeedResourceLoaded())
                    {
                        m_DelayNotifyDownLoaded.RemoveAt(i);
                        _Loaded(res);
                        if (!(--s_nFrameNotifyNums > 0))
                        {
                            break;
                        }
                    }
                }
            }
            //Profiler.EndSample();
        }

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.具体为什么是31,我只能说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;
}
     
总结
本文主要是通过3个类实现了动态资源管理的架构: ResourceManager类(所有资源的管理类)、DownLoadHelperManager类(下载类的管理类)、DownLoadHelper类(下载类)。开发者可能在正式项目中会遇见各种问题,欢迎大家留言讨论。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

0个评论