上一篇文章写了介绍了扩展方法递归查找子物体,单例基类,框架常用的消息机制的简单版本(这个会在加载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;
}
}
///
/// 已经加载但没卸载的AB
///
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的处理的,其实本质上和他一样的,没什么太大的区别。