前言
在前面三篇文章中,我們簡單的搭建好了ILRuntime的使用環境,然而還並沒有實現具體的功能。所以在這篇文章中我們首先簡單的實現下UI模塊相關的功能。(暫時不考慮AB包系統,對象池系統等等,同時本篇先是簡單的實現UI顯示以及跳轉,更多的功能在後續進行補充)
GitHub地址:https://github.com/luckyWjr/ILRuntimeDemo 方便代碼查看(由於代碼量比較多,下面只會貼出部分代碼,感興趣的同學還是直接看工程吧~有什麼更好的意見歡迎提出指正)
思路
在實現代碼前,我們首先要清楚我們的需求,UI系統的功能大致有加載UI,顯示UI,跳轉UI,UI層級管理等等。
通常我們會把一個完整的界面當成一個整體(一個prefab),比如登陸頁面,主頁面,商城頁面等等,它們可以都繼承於一個基類(暫定爲UIPanel),主要功能就是顯示隱藏界面,獲取界面內的組件等等。
在一些界面中往往有些組件需要我們單獨管理的,比如用戶信息欄,商城物品的Item等,它們也可以都繼承於一個基類(暫定爲UIView,同時UIPanel繼承於UIView),主要功能就是加載prefab,數據的顯示等。
然後我們建立兩個管理器,UIPanelManager和UIViewManager分別進行管理。UIPanelManager主要管理界面的顯示隱藏和跳轉,UIViewManager主要維護UIView的生命週期。
由於我們生成一個UIPanel的子類就需要加載對應的Prefab,比如要顯示登陸界面,我們要實例化LoginPanel,同時加載LoginPanel.prefab,那麼我們如何在實例化UIPanel的時候就得知其對應的Prefab是哪個呢,我們可以使用Attribute功能來實現,在每個UIPanel中進行配置對應的prefab的名稱,具體代碼看下文。
簡單效果如圖
代碼實現
由於UI系統的邏輯後期變動會比較大,也容易出BUG,更新也會很頻繁,所以我們將這部分的相關代碼全部放在Hotfix部分,以便後續維護更新。目錄結構大致如下
首先創建一個IView接口,用於生命週期
namespace Hotfix.UI
{
public interface IView
{
//初始化,只在prefab被創建的時候執行一次
void Init();
//每次界面顯示的時候執行
void Show();
void Update();
void LateUpdate();
void FixedUpdate();
//界面被隱藏的時候執行,再次顯示會調用Show方法
void Hide();
//銷燬的時候執行
void Destroy();
}
}
然後創建UIView類實現IView接口,主要功能是加載和銷燬gameobject,並在Show和Hide方法中進行顯示和隱藏。
namespace Hotfix.UI
{
public class UIView : IView
{
//需要加載的prefab的路徑,也作爲唯一標識符
public string url { private set; get; }
public GameObject gameObject { private set; get; }
public Transform transform { private set; get; }
public RectTransform rectTransform { private set; get; }
//是否加載完成
public bool isLoaded { get { return gameObject != null; } }
//是否顯示
public bool isVisible
{
get
{
return isLoaded && gameObject.activeSelf;
}
set
{
if (isLoaded)
gameObject.SetActive(value);
}
}
//若爲true,將在下一幀銷燬gameobject
internal bool isWillDestroy;
public UIView(string url)
{
this.url = url;
}
public virtual void Init()
{
isVisible = false;
}
public virtual void Show()
{
isVisible = true;
}
......
public virtual void Hide()
{
isVisible = false;
}
public virtual void Destroy()
{
isWillDestroy = true;
if (isVisible)
{
Hide();
}
}
//銷燬gameobject
public void DestroyImmediately()
{
if (!isWillDestroy)
{
Destroy();
}
GameObject.Destroy(gameObject);
gameObject = null;
transform = null;
rectTransform = null;
}
//加載prefab
public virtual void Load(Action callback = null)
{
gameObject = GameObject.Instantiate(Resources.Load(url)) as GameObject;
if (gameObject != null)
{
transform = gameObject.transform;
rectTransform = gameObject.GetComponent<RectTransform>();
Init();
callback?.Invoke();
}
}
}
}
然後編寫UIPanel,繼承於UIView
namespace Hotfix.UI
{
public class UIPanel : UIView
{
//UIPanel間的自定義傳遞數據
public object data;
//前一個UIPanel,用於隱藏自己的時候,Show前者
public UIPanel previousPanel;
public UIPanel(string url) : base(url)
{
}
public override void Destroy()
{
base.Destroy();
previousPanel = null;
}
}
}
接着我們編寫UIViewManager,用於管理UIView,主要用於生成和獲取UIView,管理所有UIView的生命週期
using Hotfix.UI;
using System;
using System.Collections.Generic;
namespace Hotfix.Manager
{
public class UIViewManager : ManagerBase<UIViewManager>
{
//存放所有在場景中的UIView
List<UIView> m_UIViewList;
public override void Init()
{
base.Init();
m_UIViewList = new List<UIView>();
}
public override void Update()
{
base.Update();
for (int i = 0; i < m_UIViewList.Count; i++)
{
//銷燬UIView
if (m_UIViewList[i].isWillDestroy)
{
m_UIViewList[i].DestroyImmediately();
m_UIViewList.RemoveAt(i);
i--;
continue;
}
if (m_UIViewList[i].isVisible)
{
m_UIViewList[i].Update();
}
}
}
......
//創建UIView
public UIView CreateView(Type type, params object[] args)
{
UIView view = Activator.CreateInstance(type, args) as UIView;
m_UIViewList.Add(view);
return view;
}
public void DestroyAll()
{
for (int i = 0; i < m_UIViewList.Count; i++)
m_UIViewList[i].Destroy();
}
}
}
接下來是比較重要的一點了,在前面的UIView當中,我們通過url這個路徑來加載prefab,那麼我們實例化一個UIPanel的時候,比如LoginPanel,MainPanel,我們如何僅僅通過Type知道每個UIPanel對應的prefab,即url的值。
我們的解決思路是使用自定義的Attribute,叫ManagerAtrribute,裏面會有個string的值用於存儲最基礎的自定義數據。同時對於用於這類Attribute的類,他們的管理器也要進行特殊處理。
using System;
namespace Hotfix.Manager
{
public interface IAttribute
{
//檢測符合IAttribute的類
void CheckType(Type type);
//獲取Attribute信息
AttributeData GetAtrributeData(string attrValue);
//生成被管理類的實例,管理類爲T,被管理的類爲T2
T2 CreateInstance<T2>(string attrValue) where T2 : class;
//獲取被管理類的構造函數參數
object[] GetInstanceParams(AttributeData data);
}
}
namespace Hotfix.Manager
{
public class AttributeData
{
public ManagerAttribute attribute;
public Type type;
}
public class ManagerAttribute : Attribute
{
public string value { get; protected set; }
public ManagerAttribute(string value)
{
this.value = value;
}
}
public class ManagerBaseWithAttr<T, V> : ManagerBase<T>, IAttribute where T : IManager, new() where V : ManagerAttribute
{
protected Dictionary<string, AttributeData> m_atrributeDataDic;
protected ManagerBaseWithAttr()
{
m_atrributeDataDic = new Dictionary<string, AttributeData>();
}
public virtual void CheckType(Type type)
{
var attrs = type.GetCustomAttributes(typeof(V), false);
if (attrs.Length > 0)
{
var attr = attrs[0];
if (attr is V)
{
var _attr = (V)attr;
SaveAttribute(_attr.value, new AttributeData() { attribute = _attr, type = type });
}
}
}
public AttributeData GetAtrributeData(string attrValue)
{
AttributeData classData = null;
m_atrributeDataDic.TryGetValue(attrValue, out classData);
return classData;
}
public void SaveAttribute(string name, AttributeData data)
{
m_atrributeDataDic[name] = data;
}
public T2 CreateInstance<T2>(string attrValue) where T2 : class
{
var data = GetAtrributeData(attrValue);
if (data == null)
{
Debug.LogError("沒有找到:" + attrValue + " -" + typeof(T2).Name);
return null;
}
if (data.type != null)
{
object[] p = GetInstanceParams(data);
if (p.Length == 0)
return Activator.CreateInstance(data.type) as T2;
else
return Activator.CreateInstance(data.type, p) as T2;
}
return null;
}
public virtual object[] GetInstanceParams(AttributeData data)
{
return new object[] { data.attribute.value };
}
}
}
然後我們的UIPanelManager會繼承於上面的ManagerBaseWithAttr,同時創建一個UIAttribute繼承於ManagerAttribute,我們的UIPanel的構造函數參數會對應UIAttribute的參數(如有需要可以在子類Attribute中添加其他自己需要的參數,然後在子類ManagerBaseWithAttr中改寫GetInstanceParams方法)
下面是我們的UIPanelManager的代碼,目前的主要功能就是生成,顯示,隱藏和銷燬UIPanel。
namespace Hotfix.Manager
{
public class UIPanelManager : ManagerBaseWithAttr<UIPanelManager, UIAttribute>
{
public UIPanel currentPanel;//當前顯示的頁面
Dictionary<string, UIPanel> m_UIPanelDic;//存放所有存在在場景中的UIPanel
Transform m_UICanvas;
public override void Init()
{
base.Init();
m_UIPanelDic = new Dictionary<string, UIPanel>();
m_UICanvas = GameObject.Find("Canvas").transform;
}
public void ShowPanel<T>() where T : UIPanel
{
ShowPanel<T>(null, null);
}
public void ShowPanel<T>(Action<T> callback) where T : UIPanel
{
ShowPanel(callback, null);
}
public void ShowPanel<T>(object data) where T : UIPanel
{
ShowPanel<T>(null, data);
}
//顯示一個UIPanel,參數爲回調和自定義傳遞數據
public void ShowPanel<T>(Action<T> callback, object data) where T : UIPanel
{
string url = GetUrl(typeof(T));
if (!string.IsNullOrEmpty(url))
{
LoadPanel(url, data, () =>
{
var panel = ShowPanel(url);
callback?.Invoke(panel as T);
});
}
}
//顯示UIPanel
UIPanel ShowPanel(string url)
{
if (m_UIPanelDic.TryGetValue(url, out UIPanel panel))
{
panel = m_UIPanelDic[url];
if (!panel.isVisible)
{
currentPanel?.Hide();
panel.previousPanel = currentPanel;
panel.Show();
currentPanel = panel;
}
else
Debug.Log("UIPanel is visible:" + url);
}
else
Debug.LogError("UIPanel not loaded:" + url);
return panel;
}
//加載UIPanel對象
public void LoadPanel(string url, object data, Action callback)
{
if (m_UIPanelDic.TryGetValue(url, out UIPanel panel))
{
if (panel.isLoaded)
callback?.Invoke();
}
else
{
panel = CreatePanel(url);
if (panel == null)
Debug.LogError("UIPanel not exist: " + url);
else
{
panel.data = data;
m_UIPanelDic[url] = panel;
panel.Load(() =>
{
if (panel.isLoaded)
{
panel.rectTransform.SetParentAndResetTrans(m_UICanvas);
callback?.Invoke();
}
else
m_UIPanelDic.Remove(url);
});
}
}
}
//實例化UIPanel對象
UIPanel CreatePanel(string url)
{
var data = GetAtrributeData(url);
if (data == null)
{
Debug.LogError("Unregistered UIPanel, unable to load: " + url);
return null;
}
var attr = data.attribute as UIAttribute;
var panel = UIViewManager.Instance.CreateView(data.type, attr.value) as UIPanel;
////或者
//var panel = CreateInstance<UIPanel>(url);
//UIViewManager.Instance.AddUIView(panel as UIView);
return panel;
}
//隱藏當前顯示的UIPanel
public void HidePanel()
{
currentPanel.Hide();
//顯示上一層頁面
if (currentPanel.previousPanel != null && currentPanel.previousPanel.isLoaded)
{
currentPanel.previousPanel.Show();
currentPanel = currentPanel.previousPanel;
}
}
public void DestroyPanel<T>()
{
UnLoadPanel(GetUrl(typeof(T)));
}
void UnLoadPanel(string url)
{
if (m_UIPanelDic.TryGetValue(url, out UIPanel panel))
{
panel.Destroy();
m_UIPanelDic.Remove(url);
}
else
Debug.LogError("UIPanel not exist: " + url);
}
void UnLoadAllPanel()
{
foreach(var panel in m_UIPanelDic.Values)
panel.Destroy();
m_UIPanelDic.Clear();
}
//根據UIPanel的Type獲取其對應的url
string GetUrl(Type t)
{
foreach (var keyPairValue in m_atrributeDataDic)
if (keyPairValue.Value.type == t)
return keyPairValue.Key;
Debug.LogError($"Cannot found type({t.Name})");
return null;
}
public override void OnApplicationQuit()
{
UnLoadAllPanel();
}
}
}
然後就是我們的UIPanel了,繼承於UIView
namespace Hotfix.UI
{
public class UIPanel : UIView
{
//UIPanel間的自定義傳遞數據
public object data;
//前一個UIPanel,用於隱藏自己的時候,Show前者
public UIPanel previousPanel;
public UIPanel(string url) : base(url)
{
}
public override void Destroy()
{
base.Destroy();
previousPanel = null;
}
}
}
最後我們就可以編寫我們需要的UI界面的對應UIPanel了,例如登陸界面的LoginPanel,我們會添加UIAttribute來配置其prefab的路徑(demo中就簡單的丟在了Resources目錄下),然後在Init方法中去找到我們需要使用到的控件,show方法中可以做一些界面每次顯示的時候需要的操作,數據的顯示。等等
using Hotfix.Manager;
using UnityEngine.UI;
namespace Hotfix.UI
{
[UI("LoginPanel")]
public class LoginPanel : UIPanel
{
Button m_loginBtn;
InputField m_userNameInput;
public LoginPanel(string url) : base(url)
{
}
public override void Init()
{
base.Init();
m_loginBtn = transform.Find("LoginButton").GetComponent<Button>();
m_userNameInput = transform.Find("UserNameInputField").GetComponent<InputField>();
m_loginBtn.onClick.AddListener(OnClick);
}
void OnClick()
{
UIPanelManager.Instance.ShowPanel<MainPanel>(m_userNameInput.text);
}
}
}
注:由於代碼量較多,剩下的代碼有興趣的還是看GitHub的工程。
由於添加了ManagerBaseWithAttr類,在HotfixLaunch類中也要進行相應的處理,同時也暫時在其中顯示第一個界面
namespace Hotfix
{
public class HotfixLaunch
{
static List<IManager> m_managerList = new List<IManager>();
public static void Start(bool isHotfix)
{
......
//獲取hotfix的管理類,並啓動
foreach (var t in allTypes)
{
try
{
if (t != null && t.BaseType != null && t.BaseType.FullName != null)
{
......
else if (t.BaseType.FullName.Contains(".ManagerBaseWithAttr`"))
{
Debug.Log("加載管理器-" + t);
var manager = t.BaseType.BaseType.GetProperty("Instance").GetValue(null, null) as IManager;
m_managerList.Add(manager);
attributeManagerList.Add(manager as IAttribute);
continue;
}
}
}
......
}
//遍歷所有類和ManagerBaseWithAttr管理器,找出對應的被ManagerBaseWithAttr管理的子類。例如UIPanelManager和LoginPane的關係
foreach (var t in allTypes)
foreach (var attr in attributeManagerList)
attr.CheckType(t);
.....
UIPanelManager.Instance.ShowPanel<LoginPanel>(null);
}
......
}
}