天天看点

Unity游戏开发UI框架(2)

上一篇文章写了介绍了扩展方法递归查找子物体,单例基类,框架常用的消息机制的简单版本(这个会在加载loading界面用到这个机制,之后会介绍的)。这一篇先把一个简单的AB包管理器介绍一下,至于具体怎么打ab包,自行搜索引擎就行了。

Unity的PackManger提供了AB包打包相关的东西,看官网即可。

AB管理最重要的其实就是依赖项的处理,避过这个坑其实没什么好说的。

一、AB包管理的代码;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ABManager : MonoBehaviour
{
    private static ABManager instance;
    public static ABManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject go = new GameObject(typeof(ABManager).ToString());
                instance= go.AddComponent<ABManager>();
            }
            return instance;
        }
    }

    private AssetBundle single;//单一的总包;
    private AssetBundleManifest mainfest;//AB包的清单

    public string SingleName
    {
        get
        {
#if UNITY_STANDALONE
            MDebug.Log("这是当前平台的ab包名字:STANDALONE");
            return "STANDALONE";
#else
    return "ELSE";
#endif
        }
    } 
    private string abPath = "";
    public string ABPath
    {
        get
        {
            if (abPath == "")
            {
#if UNITY_STANDALONE
                abPath = Application.streamingAssetsPath + "/";
                MDebug.Log("当前平台的AB包路径:" + abPath);

#else
     abPath = Application.persistentDataPath + "/";
#endif
              
            }
            return abPath;
        }
    }
    /// <summary>
    /// 已经加载但没卸载的AB
    /// </summary>
    private Dictionary<string, AssetBundle> loadDic = new Dictionary<string, AssetBundle>();

    public T LoadAssets<T>(string assetName,string abName) where T : Object
    {
        AssetBundle ab = LoadAssetBundle(assetName);
        if (ab != null)
        {
            T asset = ab.LoadAsset<T>(abName);
            return asset;
        }
        return default(T);
    }

    public AssetBundle LoadAssetBundle(string abName)
    {
        AssetBundle ab = null;
        if (single == null)
        {
            single = AssetBundle.LoadFromFile(ABPath + SingleName);
        }
        if (mainfest == null)
        {
            mainfest = single.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        }
        //所有的依赖项
        string[] deps = mainfest.GetAllDependencies(abName);
        for (int i = 0; i < deps.Length; i++)
        {
            string depABName = deps[i];
            //是否加载过
            if (!loadDic.ContainsKey(depABName))
            {
                AssetBundle depAB = AssetBundle.LoadFromFile(ABPath + depABName);
                loadDic.Add(depABName, depAB);
            }
        }
        if(!loadDic.TryGetValue(abName,out ab))
        {
            ab = AssetBundle.LoadFromFile(ABPath + abName);
            loadDic.Add(abName, ab);
        }
        return ab;
    }

    //卸载
    public void UnLoadAB(string abName,bool unloadAllObjects = false)
    {
        AssetBundle ab = null;
        if(loadDic.TryGetValue(abName,out ab))
        {
            ab.Unload(unloadAllObjects);
            loadDic.Remove(abName);
            Debug.Log("卸载了" + abName);
        }
    }
    public void UnLoadAllAB(bool unloadAllObjects = false)
    {
        foreach (var item in loadDic.Values)
        {
            item.Unload(unloadAllObjects);
        }
        loadDic.Clear();
    }    
}      

注意的唯一一点:就是卸载AB包的时候,传入ab.Unload(true);和ab.Unload(false);的区别;然后就是加载某个资源可能存在依赖项的问题,所以需要递归加载,知道某个资源所有的依赖全部加载完成。否则的话会造成部分资源的丢失。具体的APi我就不多介绍了,自己去查看unity官网的手册就可以了。而且介绍这些东西的人太多太多了。。。

接下来就是介绍如何加载状态,也就是场景的一部分逻辑了;

二、加载场景的一些状态处理;

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using System;

public delegate void LoadSceneComplete(params object[] args);
public class LoadSceneMsg : AutoSingleton<LoadSceneMsg>
{
    AsyncOperation async = null;
    IEnumerator loadScene = null;
    public void LoadScene(string sceneName,LoadSceneComplete loadComp,params object[] args)
    {
        loadScene = IELoadScene(sceneName, loadComp, args);
        StartCoroutine(loadScene);
    }
    IEnumerator IELoadScene(string sceneName,LoadSceneComplete callBack,params object[] args)
    {
        async = SceneManager.LoadSceneAsync(sceneName);
        async.allowSceneActivation = false;  
        //进度条加载的值:如果不使用while循环的形式进行传参的话,那么progress的值是无法动态的传入的;
        while (async.progress < 0.9f)
        {
            MDebug.Log("async.progress" + async.progress);
            //这个执行的方法,实在Loadingpanel的代码中注册的。之后我会贴出来Loading的代码;
            MessageManager.DoFunc("loading", async.progress);
            yield return null;
        }


        async.allowSceneActivation = true;

        yield return new WaitForSeconds(0.5f);
        
        yield return async;
        MDebug.Log("加载场景" + sceneName);

        MessageManager.RemoveFunc("loading");
        callBack(args);
        Resources.UnloadUnusedAssets();
        StopCoroutine(loadScene);
        async = null;
        loadScene = null;
        GC.Collect();
    }   
}      

其实这里面需要介绍的不多,就是一个使用协程去加载了一个场景。至于具体如何调用上面的这两个类,会在状态的管理类中看到。要点,容易踩坑的地方就是想要动态获取加载进度的也就是async.progress的值,必须放在循环里。否则直接传过去,只会是固定值。关于所有的UI加载的界面,后面我会详细贴出来,目前还是把状态管理这方面的东西弄完;

三、最核心的部分,控制整个状态(场景)加载的流程。这里面我用了反射,如果有思路的小伙伴可以把这一整个所有的框架都用反射的形式,实现完全的代码和预制体分离。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Reflection;

public class MySceneManager : AutoSingleton<MySceneManager>
{
	//管理加载的过的状态,实际上看自己需求决定是否要这个。
    private Dictionary<string, BaseScene> sceneDic = new Dictionary<string, BaseScene>();
    //游戏当前所在的场景
    private BaseScene m_currentScene = null;
    private void Start()
    {
    	//这个我为了演示方便,直接就在这里声明周期函数直接调用了加载方法;
        LoadScene("LoginStateScene"); 
    }
	//核心逻辑处理;
    public void LoadScene(string sceneName)
    {
        //加载场景;
        BaseScene target = null;
        //如果字典中没有,那说明这个没加载过或者被卸载了。
        if (!sceneDic.TryGetValue(sceneName, out target))
        {
        	//反射,得到管理该场景类名字,通过类名得到类;
            target = Assembly.GetExecutingAssembly().CreateInstance(sceneName) as BaseScene;
            sceneDic.Add(sceneName, target);
        }
        if (target == null)
        {
            MDebug.Log("场景不存在:" + sceneName);
        }
        target = sceneDic[sceneName];
		//执行切换场景的工作,就是从m_currentScene——>变成想要切换的场景;
        ChangeScene(target);
        //注意这里:把场景中的函数之一:LoadComplete作为参数穿进去了。详细看F12跳转到这个函数具体的实现逻辑。
        LoadSceneMsg.Instance.LoadScene(sceneName, target.LoadComplete);
    }
    public void ChangeScene(BaseScene scene)
    {
    	//当是进入游戏第一个场景的时候,也就是游戏刚开始,不存在场景,所以就是null。例如一进入游戏,直接loginScene,那么m_currentScene就不存在了。但是当从LoginScene->CityScene的时候,m_currentScene就是LogiScene,就需要执行执行 m_currentScene.Stop();了
        if (m_currentScene != null)
        {
            m_currentScene.Stop();
        }
        m_currentScene = scene;
        m_currentScene.Start();
    }
}      

四、MDebug

有的可能会疑惑MDebug这个东西怎么来的。我之前忘记说了。这个是自己写的一个类,就是把Debug封装了一下,就是方便关闭Debug而已。实际上用处不大,缺点就是在Console控制台点击显示文字不能直接跳转到当前代码行(可以通过堆栈查看),优点就是不用一行一行的去删Debug信息。直接控制isDebug的值,就可以控制是否显示所有的Log信息了,页游开发。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MDebug
{
   static bool isDebug = true;
    public static void Log(object content)
    {
        if (isDebug)
        {
            Debug.Log(content);
        }
    }
    public static void LogError(object content)
    {
        if (isDebug)
        {
            Debug.LogError(content);
        }
    }
    public static void LogWranning(object centent)
    {
        if (isDebug)
        {
            Debug.LogWarning(centent);
        }
    }
}      

五、总结:

到这里位置,所有控制状态的类,如果没落下的话,应该是简短的说完了。其实难度不高,也都很简单。其中有部分小坑,看明白这些代码的人可以自己轻松解决,但是影响的不大。

继续阅读