Files
2025-09-23 11:18:38 +08:00

741 lines
24 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
URL: https://github.com/Misaka-Mikoto-Tech/UIControlBinding
使用方法:
UE: 将此脚本添加到UI根节点与程序协商好需要绑定的控件及其变量名后将需要绑定的控件拖到脚本上
程序: 点此脚本右上角的齿轮,点 "复制代码到剪贴板" 按钮
UIManager 加载示例:
`` C#
IBindableUI uiA = Activator.CreateInstance(Type.GetType("UIA")) as IBindableUI;
GameObject prefab = Resources.Load<GameObject>("UI/UIA"); // you can get ui config from config file
GameObject go = Instantiate(prefab);
UIControlData ctrlData = go.GetComponent<UIControlData>();
if(ctrlData != null)
{
ctrlData.BindDataTo(uiA);
}
``
*/
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
using System.Text;
#if XLUA
using XLua;
#endif
using UnityEngine.Profiling;
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine.Playables;
#endif
namespace Stary.Evo
{
/// <summary>
/// 单个控件数据
/// </summary>
[Serializable]
public class CtrlItemData
{
public string name = string.Empty;
#if UNITY_EDITOR
[HideInInspector]
public string type = string.Empty;
#endif
public UnityEngine.Object[] targets = new UnityEngine.Object[1];
public override string ToString()
{
return name;
}
}
/// <summary>
/// 单个子UI数据
/// </summary>
[Serializable]
public class SubUIItemData
{
public string name = string.Empty;
public UIControlData subUIData = null;
public override string ToString()
{
return name;
}
}
/// <summary>
/// 被绑定的UI类字段信息
/// </summary>
public class UIFieldsInfo
{
public Type type;
public List<FieldInfo> controls = new List<FieldInfo>(10);
public List<FieldInfo> subUIs = new List<FieldInfo>();
}
/// <summary>
/// 当前UI所有的绑定数据以及子UI指定
/// </summary>
[DisallowMultipleComponent]
public class UIControlData : MonoBehaviour
{
/// <summary>
/// 所有绑定的组件,不允许重名
/// </summary>
public List<CtrlItemData> ctrlItemDatas;
/// <summary>
/// 子UI数据
/// </summary>
public List<SubUIItemData> subUIItemDatas;
/// <summary>
/// 被绑定的UI
/// </summary>
public List<WeakReference<IBindableUI>> bindUIRefs;
/// <summary>
/// 缓存所有打开过的UI类型的字段数据如果有需求可以在特定时机清理以节约内存
/// </summary>
public static Dictionary<Type, UIFieldsInfo> s_uiFieldsCache = new Dictionary<Type, UIFieldsInfo>();
#region Editor
#if UNITY_EDITOR
/// <summary>
/// 已知类型列表,自定义类型可以添加到下面指定区域
/// </summary>
private static Dictionary<string, Type> _typeMap = new Dictionary<string, Type>()
{
{ "TextMeshProUGUI", typeof(TMPro.TextMeshProUGUI) },
{ "TextMeshPro", typeof(TMPro.TextMeshPro) },
{ "TMP_InputField", typeof(TMPro.TMP_InputField) },
{ "TMP_Dropdown", typeof(TMPro.TMP_Dropdown) },
{ "Text", typeof(Text)},
{ "RawImage", typeof(RawImage)},
{ "Button", typeof(Button)},
{ "Toggle", typeof(Toggle)},
{ "Slider", typeof(Slider)},
{ "Scrollbar", typeof(Scrollbar)},
{ "Dropdown", typeof(Dropdown)},
{ "InputField", typeof(InputField)},
{ "Canvas", typeof(Canvas)},
{ "UIScrollView", typeof(UIScrollView) },
{ "ScrollRect", typeof(ScrollRect)},
{ "SpriteRenderer", typeof(SpriteRenderer)},
{ "GridLayoutGroup", typeof(GridLayoutGroup) },
{ "Animation", typeof(Animation) },
{ "VideoPlayer", typeof(UnityEngine.Video.VideoPlayer) },
{ "CanvasGroup", typeof(CanvasGroup) },
{ "PlayableDirector", typeof(PlayableDirector) },
{ "UITweener", typeof(UITweener) },
////////自定义控件类型请放这里////////
//////////////////////////////////////
{ "Image", typeof(Image)},
{ "RectTransform", typeof(RectTransform)},
{ "Transform", typeof(Transform)},
{ "GameObject", typeof(GameObject)},
};
public static string[] GetAllTypeNames()
{
string[] keys = new string[_typeMap.Count + 1];
keys[0] = "自动";
_typeMap.Keys.CopyTo(keys, 1);
return keys;
}
public static Type[] GetAllTypes()
{
Type[] types = new Type[_typeMap.Count + 1];
types[0] = typeof(UnityEngine.Object);
_typeMap.Values.CopyTo(types, 1);
return types;
}
#endif
#endregion
#region BindDataToC#UI
/// <summary>
/// 将当前数据绑定到某窗口类实例的字段UI 加载后必须被执行
/// </summary>
/// <param name="ui">需要绑定数据的 UI</param>
public void BindDataTo(IBindableUI ui)
{
if (ui == null)
return;
#if DEBUG_LOG
float time = Time.realtimeSinceStartup;
Profiler.BeginSample("BindDataTo");
#endif
UIFieldsInfo fieldInfos = GetUIFieldsInfo(ui.GetType());
var controls = fieldInfos.controls;
for (int i = 0, imax = controls.Count; i < imax; i++)
{
try
{
BindCtrl(ui, controls[i]);
}
catch (Exception e)
{
Debug.LogError(e);
}
}
var subUIs = fieldInfos.subUIs;
for (int i = 0, imax = subUIs.Count; i < imax; i++)
BindSubUI(ui, subUIs[i]);
if (bindUIRefs == null)
bindUIRefs = new List<WeakReference<IBindableUI>>();
bindUIRefs.Add(new WeakReference<IBindableUI>(ui));
#if DEBUG_LOG
Profiler.EndSample();
float span = Time.realtimeSinceStartup - time;
if (span > 0.002f)
Debug.LogWarningFormat("BindDataTo {0} 耗时{1}ms", ui.GetType().Name, span * 1000f);
#endif
}
private void BindCtrl(IBindableUI ui, FieldInfo fi)
{
int itemIdx = GetCtrlIndex(fi.Name);
if (itemIdx == -1)
{
Debug.LogWarningFormat("can not find binding control of name [{0}] in prefab", fi.Name);
return;
}
var objs = ctrlItemDatas[itemIdx];
Type fieldType = fi.FieldType;
if (fieldType.IsArray)
{
Array arrObj = Array.CreateInstance(fieldType.GetElementType(), objs.targets.Length);
// 给数组元素设置数据
for (int j = 0, jmax = objs.targets.Length; j < jmax; j++)
{
if (objs.targets[j] != null)
arrObj.SetValue(objs.targets[j], j);
else
Debug.LogErrorFormat("Component {0}[{1}] is null", objs.name, j);
}
fi.SetValue(ui, arrObj);
}
else
{
UnityEngine.Object component = GetComponent(itemIdx);
if (component != null)
fi.SetValue(ui, component);
else
Debug.LogErrorFormat("Component {0} is null", objs.name);
}
}
private void BindSubUI(IBindableUI ui, FieldInfo fi)
{
int subUIIdx = GetSubUIIndex(fi.Name);
if(subUIIdx == -1)
{
Debug.LogErrorFormat("can not find binding subUI of name [{0}] in prefab", fi.Name);
return;
}
fi.SetValue(ui, subUIItemDatas[subUIIdx].subUIData);
}
/// <summary>
/// 获取指定UI类的字段信息
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static UIFieldsInfo GetUIFieldsInfo(Type type)
{
UIFieldsInfo uIFieldsInfo;
if (s_uiFieldsCache.TryGetValue(type, out uIFieldsInfo))
return uIFieldsInfo;
uIFieldsInfo = new UIFieldsInfo() { type = type };
FieldInfo[] fis = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
for(int i = 0, imax = fis.Length; i < imax; i++)
{
FieldInfo fi = fis[i];
if (fi.IsDefined(typeof(ControlBindingAttribute), false))
uIFieldsInfo.controls.Add(fi);
else if (fi.IsDefined(typeof(SubUIBindingAttribute), false))
uIFieldsInfo.subUIs.Add(fi);
}
s_uiFieldsCache.Add(type, uIFieldsInfo);
return uIFieldsInfo;
}
#endregion
#region UnBind
private static List<UIControlData> s_tmpControlDataForUnbind = new List<UIControlData>();
/// <summary>
/// 解除指定UI及其子节点自动绑定字段的引用
/// </summary>
/// <param name="uiGo"></param>
public static void UnBindUI(GameObject uiGo)
{
if (uiGo == null)
return;
#if DEBUG_LOG
float time = Time.realtimeSinceStartup;
Profiler.BeginSample("UnBindUI");
#endif
uiGo.GetComponentsInChildren(true, s_tmpControlDataForUnbind);
for (int i = 0, imax = s_tmpControlDataForUnbind.Count; i < imax; i++)
{
UIControlData controlData = s_tmpControlDataForUnbind[i];
if (controlData.bindUIRefs == null)
continue;
List<WeakReference<IBindableUI>> bindUIRefs = controlData.bindUIRefs;
for (int j = 0, jmax = bindUIRefs.Count; j < jmax; j++)
{
WeakReference<IBindableUI> bindUIRef = bindUIRefs[j];
IBindableUI bindUI;
if (!bindUIRef.TryGetTarget(out bindUI))
continue;
}
controlData.bindUIRefs = null;
}
s_tmpControlDataForUnbind.Clear();
#if DEBUG_LOG
Profiler.EndSample();
float span = Time.realtimeSinceStartup - time;
if (span > 0.002f)
Debug.LogWarningFormat("UnBindUI {0} 耗时{1}ms", uiGo.name, span * 1000f);
#endif
}
#endregion
#region Get,使
/// <summary>
/// 找到指定名称的第一个组件, 不存在返回 null
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns></returns>
public T GetComponent<T>(string name) where T : Component
{
int idx = GetCtrlIndex(name);
if (idx == -1)
return null;
var targets = ctrlItemDatas[idx].targets;
if (targets.Length == 0)
return null;
return targets[0] as T;
}
public new UnityEngine.Object GetComponent(string name)
{
int idx = GetCtrlIndex(name);
if (idx == -1)
return null;
var targets = ctrlItemDatas[idx].targets;
if (targets.Length == 0)
return null;
return targets[0];
}
public UnityEngine.Object GetComponent(int idx)
{
if (idx == -1 || idx >= ctrlItemDatas.Count)
return null;
var targets = ctrlItemDatas[idx].targets;
if (targets.Length == 0)
return null;
return targets[0];
}
public UnityEngine.Object[] GetComponents(string name)
{
int idx = GetCtrlIndex(name);
if (idx == -1)
return null;
return ctrlItemDatas[idx].targets;
}
public UnityEngine.Object[] GetComponents(int idx)
{
if (idx == -1 || idx >= ctrlItemDatas.Count)
return null;
return ctrlItemDatas[idx].targets;
}
private int GetCtrlIndex(string name)
{
for (int i = 0, imax = ctrlItemDatas.Count; i < imax; i++)
{
CtrlItemData item = ctrlItemDatas[i];
if (item.name == name)
return i;
}
return -1;
}
private int GetSubUIIndex(string name)
{
for(int i = 0, imax = subUIItemDatas.Count; i < imax; i++)
{
SubUIItemData item = subUIItemDatas[i];
if (item.name == name)
return i;
}
return -1;
}
#endregion
#region For Editor
#if UNITY_EDITOR
public bool dataHasChanged = false;
public bool CorrectComponents()
{
if (ctrlItemDatas == null) return true;
bool isOK = true;
for(int i = 0, imax = ctrlItemDatas.Count; i < imax; i++)
{
if (string.IsNullOrEmpty(ctrlItemDatas[i].name)) // TODO Check if is a valid varible name
{
Debug.LogErrorFormat("[{1}]第 {0} 个控件没有名字,请修正", i + 1, gameObject.name);
return false;
}
for (int j = ctrlItemDatas.Count - 1; j >= 0; j--)
{
if(ctrlItemDatas[i].name == ctrlItemDatas[j].name && i != j)
{
Debug.LogErrorFormat("[{3}]控件名字 [{0}] 第 {1} 项与第 {2} 项重复,请修正", ctrlItemDatas[i].name, i + 1, j + 1, gameObject.name);
return false;
}
}
}
isOK = ReplaceTargetsToUIComponent();
if(isOK)
Debug.LogFormat("[{0}]控件绑定修正完毕", gameObject.name);
return isOK;
}
public bool CheckSubUIs()
{
for (int i = 0, imax = subUIItemDatas.Count; i < imax; i++)
{
var subUI = subUIItemDatas[i];
if(subUI != null)
{
if (string.IsNullOrEmpty(subUI.name))
{
Debug.LogErrorFormat("[{0}]第 {1} 个子UI没有设置名字, 请修正", gameObject.name, i + 1);
return false;
}
if(subUI.subUIData == null)
{
Debug.LogErrorFormat("[{0}]第 {1} 个子UI没有赋值, 请修正", gameObject.name, i + 1);
return false;
}
// 必须拖当前 Prefab 下的子UI
if (!IsInCurrentPrefab(subUI.subUIData.transform))
{
Debug.LogErrorFormat("[{0}]第 {1} 个子UI [{2}]不是当前 Prefab 下的对象,请修正", gameObject.name, i + 1, subUI.name);
return false;
}
}
else
{
Debug.LogError("internal error at ControlBinding, pls contact author");
return false;
}
}
return true;
}
/// <summary>
/// 由于自动拖上去的对象永远都是 GameObject所以我们需要把它修正为正确的对象类型
/// </summary>
private bool ReplaceTargetsToUIComponent()
{
for (int i = 0, imax = ctrlItemDatas.Count; i < imax; i++)
{
var objs = ctrlItemDatas[i].targets;
Type type = null;
for(int j = 0, jmax = objs.Length; j < jmax; j++)
{
if(objs[j] == null)
{
Debug.LogErrorFormat("[{2}]控件名字 [{0}] 第 {1} 项为空,请修正", ctrlItemDatas[i].name, j + 1, gameObject.name);
return false;
}
GameObject go = objs[j] as GameObject;
if (go == null)
go = (objs[j] as Component).gameObject;
// 必须拖当前 Prefab 下的控件
if (!IsInCurrentPrefab(go.transform))
{
Debug.LogErrorFormat("[{2}]控件名字 [{0}] 第 {1} 项不是当前 Prefab 下的控件,请修正", ctrlItemDatas[i].name, j + 1, gameObject.name);
return false;
}
UnityEngine.Object correctComponent = FindCorrectComponent(go, ctrlItemDatas[i].type);
if(correctComponent == null)
{
Debug.LogErrorFormat("[{3}]控件 [{0}] 第 {1} 项不是 {2} 类型,请修正", ctrlItemDatas[i].name, j + 1, ctrlItemDatas[i].type, gameObject.name);
return false;
}
if (type == null) // 当前变量的第一个控件时执行
{
if (string.IsNullOrEmpty(ctrlItemDatas[i].type))
{
type = correctComponent.GetType();
}else
{
if(!_typeMap.TryGetValue(ctrlItemDatas[i].type, out type))
{
Debug.LogError("Internal Error, pls contact author");
return false;
}
}
}
else if(correctComponent.GetType() != type && !correctComponent.GetType().IsSubclassOf(type))
{
Debug.LogErrorFormat("[{2}]控件名字 [{0}] 第 {1} 项与第 1 项的类型不同,请修正", ctrlItemDatas[i].name, j + 1, gameObject.name);
return false;
}
if (objs[j] != correctComponent)
dataHasChanged = true;
objs[j] = correctComponent;
}
if(type.Name != ctrlItemDatas[i].type)
{
ctrlItemDatas[i].type = type.Name;
//#if UNITY_2019_1_OR_NEWER
// EditorUtility.ClearDirty(this);
//#endif
EditorUtility.SetDirty(this);
PrefabUtility.RecordPrefabInstancePropertyModifications(this);
}
ctrlItemDatas[i].type = type.Name;
}
return true;
}
private bool IsInCurrentPrefab(Transform t)
{
do
{
if (t == transform)
return true;
t = t.parent;
} while (t != null);
return false;
}
private UnityEngine.Object FindCorrectComponent(GameObject go, string typename)
{
if (typename == "GameObject")
return go;
List<Component> components = new List<Component>();
go.GetComponents(components);
Func<Type, Component> getSpecialTypeComp = (Type t) =>
{
foreach (var comp in components)
{
Type compType = comp.GetType();
if (compType == t || compType.IsSubclassOf(t))
{
return comp;
}
}
return null;
};
Component newComp = null;
if (string.IsNullOrEmpty(typename))
{
// 类型名为空则为自动类型,在 _typeMap 里从上往下找
foreach (var kv in _typeMap)
{
newComp = getSpecialTypeComp(kv.Value);
if (newComp != null)
break;
}
}
else
{// 指定了类型名则只找指定类型的控件
Type type = null;
if (_typeMap.TryGetValue(typename, out type))
{
newComp = getSpecialTypeComp(type);
}
}
return newComp;
}
private bool IsNeedSave()
{
foreach(var ctrl in ctrlItemDatas)
{
if (string.IsNullOrEmpty(ctrl.type))
return true;
}
return false;
}
[ContextMenu("复制代码到剪贴板(Private)")]
public void CopyCodeToClipBoardPrivate()
{
CopyCodeToClipBoardImpl("private");
}
[ContextMenu("复制代码到剪贴板(Protected)")]
public void CopyCodeToClipBoardProtected()
{
CopyCodeToClipBoardImpl("protected");
}
[ContextMenu("复制代码到剪贴板(Public)")]
public void CopyCodeToClipBoardPublic()
{
CopyCodeToClipBoardImpl("public");
}
private void CopyCodeToClipBoardImpl(string accessLevel)
{
// 调用保存资源会导致 prefab 发生变化,因此只有有需要时才保存
if (IsNeedSave())
UIBindingPrefabSaveHelper.SavePrefab(gameObject);
StringBuilder sb = new StringBuilder(1024);
sb.AppendLine("#region 控件绑定变量声明,自动生成请勿手改");
sb.AppendLine("\t\t#pragma warning disable 0649"); // 变量未赋值
foreach (var ctrl in ctrlItemDatas)
{
if (ctrl.targets.Length == 0)
continue;
if (ctrl.targets.Length == 1)
sb.AppendFormat("\t\t[ControlBinding]\r\n\t\t{0} {1} {2};\r\n", accessLevel, ctrl.type, ctrl.name);
else
sb.AppendFormat("\t\t[ControlBinding]\r\n\t\t{0} {1}[] {2};\r\n", accessLevel, ctrl.type, ctrl.name);
}
sb.AppendLine();
foreach(var subUI in subUIItemDatas)
{
sb.AppendFormat("\t\t[SubUIBinding]\r\n\t\t{0} UIControlData {1};\r\n", accessLevel, subUI.name);
}
sb.AppendLine("\t\t#pragma warning restore 0649");
sb.Append("#endregion\r\n\r\n");
UnityEngine.GUIUtility.systemCopyBuffer = sb.ToString();
}
[ContextMenu("复制代码到剪贴板(Lua)")]
public void CopyCodeToClipBoardLua()
{
// 调用保存资源会导致 prefab 发生变化,因此只有有需要时才保存
if (IsNeedSave())
UIBindingPrefabSaveHelper.SavePrefab(gameObject);
StringBuilder sb = new StringBuilder(1024);
sb.Append("-- 控件绑定变量声明,自动生成请勿手改\r\n");
foreach (var ctrl in ctrlItemDatas)
{
if (ctrl.targets.Length == 0)
continue;
sb.AppendFormat("local {0}\r\n", ctrl.name);
}
sb.AppendFormat("\r\n");
sb.AppendFormat("-- SubUI\r\n");
foreach (var subUI in subUIItemDatas)
{
sb.AppendFormat("local {0}\r\n", subUI.name);
}
sb.Append("-- 控件绑定定义结束\r\n\r\n");
UnityEngine.GUIUtility.systemCopyBuffer = sb.ToString();
}
public void SetDirty()
{
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(gameObject);
#if UNITY_2021_1_OR_NEWER
var prefabStage = UnityEditor.SceneManagement.PrefabStageUtility.GetPrefabStage(gameObject);
#else
var prefabStage = UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetPrefabStage(gameObject);
#endif
if (prefabStage != null)
{
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(prefabStage.scene);
}
#endif
}
#endif
#endregion
}
}