欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Unity游戏开发UI框架(2)

程序员文章站 2022-04-03 16:45:29
...
上一篇文章写了介绍了扩展方法递归查找子物体,单例基类,框架常用的消息机制的简单版本(这个会在加载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);
        }
    }
}

五、总结:
到这里位置,所有控制状态的类,如果没落下的话,应该是简短的说完了。其实难度不高,也都很简单。其中有部分小坑,看明白这些代码的人可以自己轻松解决,但是影响的不大。
总体来说核心内容就在这个文章里。一定要理清楚他的逻辑,是怎么调用 各个方法实现场景(状态)的切换的。看明白了就很简单。下面就是关于UI的处理的,其实本质上和他一样的,没什么太大的区别。