/* 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("UI/UIA"); // you can get ui config from config file GameObject go = Instantiate(prefab); UIControlData ctrlData = go.GetComponent(); 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 { /// /// 单个控件数据 /// [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; } } /// /// 单个子UI数据 /// [Serializable] public class SubUIItemData { public string name = string.Empty; public UIControlData subUIData = null; public override string ToString() { return name; } } /// /// 被绑定的UI类字段信息 /// public class UIFieldsInfo { public Type type; public List controls = new List(10); public List subUIs = new List(); } /// /// 当前UI所有的绑定数据以及子UI指定 /// [DisallowMultipleComponent] public class UIControlData : MonoBehaviour { /// /// 所有绑定的组件,不允许重名 /// public List ctrlItemDatas; /// /// 子UI数据 /// public List subUIItemDatas; /// /// 被绑定的UI /// public List> bindUIRefs; /// /// 缓存所有打开过的UI类型的字段数据(如果有需求可以在特定时机清理以节约内存) /// public static Dictionary s_uiFieldsCache = new Dictionary(); #region Editor #if UNITY_EDITOR /// /// 已知类型列表,自定义类型可以添加到下面指定区域 /// private static Dictionary _typeMap = new Dictionary() { { "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 /// /// 将当前数据绑定到某窗口类实例的字段,UI 加载后必须被执行 /// /// 需要绑定数据的 UI 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>(); bindUIRefs.Add(new WeakReference(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); } /// /// 获取指定UI类的字段信息 /// /// /// 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 s_tmpControlDataForUnbind = new List(); /// /// 解除指定UI及其子节点自动绑定字段的引用 /// /// 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> bindUIRefs = controlData.bindUIRefs; for (int j = 0, jmax = bindUIRefs.Count; j < jmax; j++) { WeakReference 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,不建议使用 /// /// 找到指定名称的第一个组件, 不存在返回 null /// /// /// /// public T GetComponent(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; } /// /// 由于自动拖上去的对象永远都是 GameObject,所以我们需要把它修正为正确的对象类型 /// 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 components = new List(); go.GetComponents(components); Func 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 } }