From dfef3f519986499082cf9fa0a3efed246678bd95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Mon, 1 Dec 2025 16:44:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20=E5=A2=9E=E5=8A=A0UIContro?= =?UTF-8?q?ller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Editor/Helper/SerializedClassDrawer.cs | 51 +++ Editor/Helper/SerializedClassDrawer.cs.meta | 3 + .../{UXUIEditor.cs => UXCreateHelper.cs} | 13 +- ...IEditor.cs.meta => UXCreateHelper.cs.meta} | 0 Editor/UX/Controller.meta | 3 + Editor/UX/Controller/StateTypePopup.cs | 86 +++++ Editor/UX/Controller/StateTypePopup.cs.meta | 3 + Editor/UX/Controller/UXControllerEditor.cs | 73 ++++ .../UX/Controller/UXControllerEditor.cs.meta | 3 + .../UXControllerStateRecorderEditor.cs | 358 ++++++++++++++++++ .../UXControllerStateRecorderEditor.cs.meta | 3 + Runtime/UXComponent/Controller.meta | 8 + .../Controller/ControllerStateBase.cs | 36 ++ .../Controller/ControllerStateBase.cs.meta | 3 + Runtime/UXComponent/Controller/Property.meta | 3 + .../Property/GameObjectPropertyState.cs | 113 ++++++ .../Property/GameObjectPropertyState.cs.meta | 3 + .../UXComponent/Controller/UXController.cs | 111 ++++++ .../Controller/UXController.cs.meta | 11 + .../Controller/UXControllerStateRecorder.cs | 50 +++ .../UXControllerStateRecorder.cs.meta | 3 + Runtime/_InternalVisibleTo.cs | 4 + Runtime/_InternalVisibleTo.cs.meta | 3 + 23 files changed, 939 insertions(+), 5 deletions(-) create mode 100644 Editor/Helper/SerializedClassDrawer.cs create mode 100644 Editor/Helper/SerializedClassDrawer.cs.meta rename Editor/Inspector/{UXUIEditor.cs => UXCreateHelper.cs} (91%) rename Editor/Inspector/{UXUIEditor.cs.meta => UXCreateHelper.cs.meta} (100%) create mode 100644 Editor/UX/Controller.meta create mode 100644 Editor/UX/Controller/StateTypePopup.cs create mode 100644 Editor/UX/Controller/StateTypePopup.cs.meta create mode 100644 Editor/UX/Controller/UXControllerEditor.cs create mode 100644 Editor/UX/Controller/UXControllerEditor.cs.meta create mode 100644 Editor/UX/Controller/UXControllerStateRecorderEditor.cs create mode 100644 Editor/UX/Controller/UXControllerStateRecorderEditor.cs.meta create mode 100644 Runtime/UXComponent/Controller.meta create mode 100644 Runtime/UXComponent/Controller/ControllerStateBase.cs create mode 100644 Runtime/UXComponent/Controller/ControllerStateBase.cs.meta create mode 100644 Runtime/UXComponent/Controller/Property.meta create mode 100644 Runtime/UXComponent/Controller/Property/GameObjectPropertyState.cs create mode 100644 Runtime/UXComponent/Controller/Property/GameObjectPropertyState.cs.meta create mode 100644 Runtime/UXComponent/Controller/UXController.cs create mode 100644 Runtime/UXComponent/Controller/UXController.cs.meta create mode 100644 Runtime/UXComponent/Controller/UXControllerStateRecorder.cs create mode 100644 Runtime/UXComponent/Controller/UXControllerStateRecorder.cs.meta create mode 100644 Runtime/_InternalVisibleTo.cs create mode 100644 Runtime/_InternalVisibleTo.cs.meta diff --git a/Editor/Helper/SerializedClassDrawer.cs b/Editor/Helper/SerializedClassDrawer.cs new file mode 100644 index 0000000..8b6f538 --- /dev/null +++ b/Editor/Helper/SerializedClassDrawer.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +public static class SerializedClassDrawer +{ + public static void DrawSerializableProperty(SerializedProperty prop, string title = null) + { + if (prop == null) return; + + EditorGUILayout.BeginHorizontal(); + + EditorGUILayout.EndHorizontal(); + + if (!prop.hasVisibleChildren || IsEmptyClass(prop)) + { + EditorGUILayout.PropertyField(prop, true); + } + else + { + SerializedProperty iterator = prop.Copy(); + SerializedProperty end = iterator.GetEndProperty(); + + if (iterator.NextVisible(true)) + { + while (!SerializedProperty.EqualContents(iterator, end)) + { + if (iterator.name == "m_Script") + { + if (!iterator.NextVisible(false)) break; + else continue; + } + + GUIContent label = new GUIContent(ObjectNames.NicifyVariableName(iterator.displayName)); + + EditorGUILayout.PropertyField(iterator, label, true); + + if (!iterator.NextVisible(false)) break; + } + } + } + } + + static bool IsEmptyClass(SerializedProperty prop) + { + SerializedProperty it = prop.Copy(); + if (!it.NextVisible(true)) return true; + SerializedProperty end = prop.GetEndProperty(); + return SerializedProperty.EqualContents(it, end); + } +} diff --git a/Editor/Helper/SerializedClassDrawer.cs.meta b/Editor/Helper/SerializedClassDrawer.cs.meta new file mode 100644 index 0000000..24f3111 --- /dev/null +++ b/Editor/Helper/SerializedClassDrawer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 04d2930185f64726b690d03a849dc699 +timeCreated: 1764138440 \ No newline at end of file diff --git a/Editor/Inspector/UXUIEditor.cs b/Editor/Inspector/UXCreateHelper.cs similarity index 91% rename from Editor/Inspector/UXUIEditor.cs rename to Editor/Inspector/UXCreateHelper.cs index 1b8f084..9d739ae 100644 --- a/Editor/Inspector/UXUIEditor.cs +++ b/Editor/Inspector/UXCreateHelper.cs @@ -3,12 +3,14 @@ using System.Reflection; using TMPro; using TMPro.EditorUtilities; using UnityEditor; +using UnityEditor.SceneManagement; using UnityEditor.UI; using UnityEngine; using UnityEngine.UI; +using Random = UnityEngine.Random; -internal class UXUIEditor : Editor +public class UXCreateHelper : Editor { static object InvokeMethod(Type type, string methodName, object[] parameters = null) { @@ -41,7 +43,7 @@ internal class UXUIEditor : Editor #if TEXTMESHPRO_SUPPORT [MenuItem("GameObject/UI/UXTextMeshPro")] - private static void CreateUXTextMeshPro(MenuCommand menuCommand) + public static void CreateUXTextMeshPro(MenuCommand menuCommand) { Type MenuOptionsType = typeof(ImageEditor).Assembly.GetType("UnityEditor.UI.MenuOptions"); InvokeMethod(MenuOptionsType, "AddText", new object[] { menuCommand }); @@ -85,14 +87,15 @@ internal class UXUIEditor : Editor #endif [MenuItem("GameObject/UI/UXScrollView")] - private static void CreateUxRecyclerView() + public static void CreateUxRecyclerView() { GameObject selectionObject = Selection.activeGameObject; - if (selectionObject == null) return; + Transform parent = selectionObject != null ? selectionObject.transform : PrefabStageUtility.GetCurrentPrefabStage().prefabContentsRoot.transform; const string prefabPath = "Packages/com.alicizax.unity.ui.extension/Editor/RecyclerView/Res/ScrollView.prefab"; GameObject prefab = AssetDatabase.LoadAssetAtPath(prefabPath); - GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, selectionObject.transform); + GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent); PrefabUtility.UnpackPrefabInstance(instance, PrefabUnpackMode.Completely, InteractionMode.UserAction); + instance.name = prefab.name + Random.Range(1, 1000); Selection.activeGameObject = instance; } diff --git a/Editor/Inspector/UXUIEditor.cs.meta b/Editor/Inspector/UXCreateHelper.cs.meta similarity index 100% rename from Editor/Inspector/UXUIEditor.cs.meta rename to Editor/Inspector/UXCreateHelper.cs.meta diff --git a/Editor/UX/Controller.meta b/Editor/UX/Controller.meta new file mode 100644 index 0000000..39b6976 --- /dev/null +++ b/Editor/UX/Controller.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 03f1bde8358542e897147218725d1776 +timeCreated: 1764127120 \ No newline at end of file diff --git a/Editor/UX/Controller/StateTypePopup.cs b/Editor/UX/Controller/StateTypePopup.cs new file mode 100644 index 0000000..143e141 --- /dev/null +++ b/Editor/UX/Controller/StateTypePopup.cs @@ -0,0 +1,86 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEditor.Experimental.GraphView; +using UnityEngine; +using AlicizaX.UI; +using AlicizaX.UI.Runtime; + +public class StateTypeSearchProvider : ScriptableObject, ISearchWindowProvider +{ + private Action _onSelect; + private List _entries; + + public void Init(Action onSelect) + { + _onSelect = onSelect; + BuildEntries(); + } + + private void BuildEntries() + { + _entries = new List(); + + _entries.Add(new SearchTreeGroupEntry(new GUIContent("States"))); + + var all = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => + { + try { return a.GetTypes(); } + catch { return new Type[0]; } + }) + .Where(t => typeof(ControllerStateBase).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract) + .Select(t => + { + var attr = t.GetCustomAttribute(); + var tag = attr != null ? attr.StateName : t.Name; + return (type: t, tag); + }) + .OrderBy(x => x.tag) + .ToArray(); + + + var groups = new Dictionary>(); + foreach (var item in all) + { + string[] parts = item.tag.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + string g = parts.Length > 0 ? parts[0] : "Other"; + string child = parts.Length > 1 ? string.Join("/", parts.Skip(1)) : (parts.Length == 1 ? parts[0] : item.type.Name); + string display = $"{child}"; + + if (!groups.TryGetValue(g, out var list)) { list = new List<(Type, string, string)>(); groups[g] = list; } + list.Add((item.type, item.tag, display)); + } + + foreach (var kv in groups.OrderBy(k => k.Key)) + { + _entries.Add(new SearchTreeGroupEntry(new GUIContent(kv.Key)) { level = 1 }); + foreach (var child in kv.Value.OrderBy(c => c.fullTag)) + { + var entry = new SearchTreeEntry(new GUIContent(child.display)) { level = 2, userData = child.type }; + _entries.Add(entry); + } + } + } + + public List CreateSearchTree(SearchWindowContext context) + { + if (_entries == null) BuildEntries(); + return _entries; + } + + public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) + { + var t = entry.userData as Type; + if (t != null) + { + _onSelect?.Invoke(t); + return true; + } + return false; + } +} +#endif diff --git a/Editor/UX/Controller/StateTypePopup.cs.meta b/Editor/UX/Controller/StateTypePopup.cs.meta new file mode 100644 index 0000000..6001102 --- /dev/null +++ b/Editor/UX/Controller/StateTypePopup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 37183e7bcfe8411fba14fb9eb6bdfe9d +timeCreated: 1764136414 \ No newline at end of file diff --git a/Editor/UX/Controller/UXControllerEditor.cs b/Editor/UX/Controller/UXControllerEditor.cs new file mode 100644 index 0000000..04d5f57 --- /dev/null +++ b/Editor/UX/Controller/UXControllerEditor.cs @@ -0,0 +1,73 @@ +using UnityEditor; +using UnityEngine; +using AlicizaX.UI; +using AlicizaX.UI.Runtime; + +[CustomEditor(typeof(UXController))] +public class UXControllerEditor : Editor +{ + private SerializedProperty _controllersProp; + + private void OnEnable() + { + _controllersProp = serializedObject.FindProperty("_controllers"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.LabelField("UX Controller", EditorStyles.boldLabel); + EditorGUILayout.Space(); + EditorGUI.BeginDisabledGroup(true); + + for (int i = 0; i < _controllersProp.arraySize; i++) + { + var el = _controllersProp.GetArrayElementAtIndex(i); + var nameProp = el.FindPropertyRelative("Name"); + var lengthProp = el.FindPropertyRelative("Length"); + + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"[{i}]", GUILayout.Width(30)); + nameProp.stringValue = EditorGUILayout.TextField(nameProp.stringValue); + if (GUILayout.Button("Remove", GUILayout.Width(70))) + { + _controllersProp.DeleteArrayElementAtIndex(i); + break; + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Length", GUILayout.Width(50)); + lengthProp.intValue = Mathf.Max(1, EditorGUILayout.IntField(lengthProp.intValue)); + EditorGUILayout.EndHorizontal(); + + + var ux = target as UXController; + if (ux != null && ux.Controllers != null && i < ux.Controllers.Count) + { + EditorGUILayout.LabelField("CurrentIndex", ux.Controllers[i].CurrentIndex.ToString()); + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(); + } + + var controller = target as UXController; + using (new EditorGUILayout.VerticalScope("Box")) + { + foreach (var recorder in controller.Recorders) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"ID:{recorder.ID}", EditorStyles.helpBox); + EditorGUILayout.ObjectField(recorder, typeof(UXControllerStateRecorder), true); + EditorGUILayout.EndHorizontal(); + } + } + + EditorGUI.EndDisabledGroup(); + serializedObject.ApplyModifiedProperties(); + } +} diff --git a/Editor/UX/Controller/UXControllerEditor.cs.meta b/Editor/UX/Controller/UXControllerEditor.cs.meta new file mode 100644 index 0000000..7783ec2 --- /dev/null +++ b/Editor/UX/Controller/UXControllerEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 627b7a51244f46ce8bcbd31e6cedf4f8 +timeCreated: 1764127335 \ No newline at end of file diff --git a/Editor/UX/Controller/UXControllerStateRecorderEditor.cs b/Editor/UX/Controller/UXControllerStateRecorderEditor.cs new file mode 100644 index 0000000..29403dc --- /dev/null +++ b/Editor/UX/Controller/UXControllerStateRecorderEditor.cs @@ -0,0 +1,358 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEditor.Experimental.GraphView; +using UnityEditor.Experimental.SceneManagement; +using UnityEngine; +using AlicizaX.UI; +using AlicizaX.UI.Runtime; +using UnityEditor.SceneManagement; +using Random = UnityEngine.Random; + +[CustomEditor(typeof(UXControllerStateRecorder))] +public class UXControllerStateRecorderEditor : Editor +{ + private SerializedProperty _stateEntriesProp; + private SerializedProperty _controller; + private SerializedProperty _id; + private GUIContent _removeIcon; + private UXController _cacheController; + + private void OnEnable() + { + _controller = serializedObject.FindProperty("_controller"); + _id = serializedObject.FindProperty("_id"); + _stateEntriesProp = serializedObject.FindProperty("_stateEntries"); + _removeIcon = EditorGUIUtility.IconContent("d_winbtn_win_close"); + } + + private void OnDestroy() + { + if (target == null) + { + UnRegisterSelf(); + } + } + + private void UnRegisterSelf() + { + UXController controller = (_cacheController != null) ? _cacheController : GetPrefabStageController(); + if (controller == null) return; + var so = new SerializedObject(controller); + var recorders = so.FindProperty("_recorders"); + for (int i = recorders.arraySize - 1; i >= 0; i--) + { + var el = recorders.GetArrayElementAtIndex(i); + if (el.objectReferenceValue == null) + { + recorders.DeleteArrayElementAtIndex(i); + break; + } + } + + so.ApplyModifiedProperties(); + EditorUtility.SetDirty(controller); + } + + private void RegisterSelfToController(UXController controller) + { + var so = new SerializedObject(controller); + var recorders = so.FindProperty("_recorders"); + int newIndex = recorders.arraySize; + recorders.InsertArrayElementAtIndex(newIndex); + var el = recorders.GetArrayElementAtIndex(newIndex); + el.objectReferenceValue = target; + so.ApplyModifiedProperties(); + EditorUtility.SetDirty(controller); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.Space(); + + var recorder = target as UXControllerStateRecorder; + UXController prefabCtrl = GetPrefabStageController(); + + if (_id.intValue <= 0) + { + _id.intValue = Random.Range(10000000, 99999999); + } + + + EditorGUILayout.LabelField($"ID:{_id.intValue}", EditorStyles.helpBox); + + EditorGUILayout.BeginHorizontal(); + + + if (prefabCtrl != null && prefabCtrl != _controller.objectReferenceValue) + { + _controller.objectReferenceValue = prefabCtrl; + } + + if (!prefabCtrl.HasRecorder(recorder)) + { + RegisterSelfToController(prefabCtrl); + } + + if (_controller.objectReferenceValue != null) + { + _cacheController = _controller.objectReferenceValue as UXController; + } + + + if (_controller.objectReferenceValue != null) + { + UXController ctl = _controller.objectReferenceValue as UXController; + EditorGUILayout.LabelField($"{ctl.name} ({ctl.Controllers.Count} controllers)", EditorStyles.helpBox); + } + else + EditorGUILayout.LabelField("(None)", EditorStyles.helpBox); + + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + Rect addRect = EditorGUILayout.GetControlRect(false, GUILayout.Height(22)); + if (GUI.Button(addRect, "Add State")) + { + var provider = ScriptableObject.CreateInstance(); + provider.Init(OnTypeSelected); + var screenPos = GUIUtility.GUIToScreenPoint(new Vector2(addRect.x, addRect.yMax)); + SearchWindow.Open(new SearchWindowContext(screenPos), provider); + } + + EditorGUILayout.Space(); + + for (int i = 0; i < _stateEntriesProp.arraySize; i++) + { + var entryProp = _stateEntriesProp.GetArrayElementAtIndex(i); + if (entryProp == null) continue; + + var stateProp = entryProp.FindPropertyRelative("State"); + var controllerNameProp = entryProp.FindPropertyRelative("ControllerName"); + var controllerIndexProp = entryProp.FindPropertyRelative("ControllerIndex"); + + if (stateProp == null) + { + EditorGUILayout.HelpBox($"Unable to find 'State' on entry {i}.", MessageType.Error); + if (GUILayout.Button("Remove")) + { + _stateEntriesProp.DeleteArrayElementAtIndex(i); + break; + } + + continue; + } + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + var t = stateProp.managedReferenceValue.GetType(); + var attr = t.GetCustomAttribute(); + var tag = attr != null ? attr.StateName : t.Name; + EditorGUILayout.LabelField($"State:{tag}", EditorStyles.helpBox); + EditorGUILayout.BeginHorizontal(); + + + var controllerNames = GetControllerNamesForRecorder(recorder); + int selIdx = -1; + if (!string.IsNullOrEmpty(controllerNameProp.stringValue)) + { + selIdx = controllerNames.IndexOf(controllerNameProp.stringValue); + } + + GUILayoutOption ctrlWidth = GUILayout.Width(140); + if (controllerNames.Count > 0) + { + int newSel = EditorGUILayout.Popup(selIdx, controllerNames.ToArray(), ctrlWidth); + if (newSel >= 0 && newSel < controllerNames.Count) + controllerNameProp.stringValue = controllerNames[newSel]; + } + else + { + if (!string.IsNullOrEmpty(controllerNameProp.stringValue)) + { + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.HelpBox($"绑定控制器 [{controllerNameProp.stringValue}] 不存在", MessageType.Error); + EditorGUI.EndDisabledGroup(); + } + else + { + EditorGUILayout.HelpBox($"暂无控制器", MessageType.Warning); + } + } + + + int ctrlLen = GetSelectedControllerLength(recorder, controllerNameProp.stringValue); + if (ctrlLen <= 0) ctrlLen = 1; + controllerIndexProp.intValue = Mathf.Clamp(controllerIndexProp.intValue, 0, ctrlLen - 1); + string[] idxOptions = new string[ctrlLen]; + for (int j = 0; j < ctrlLen; j++) idxOptions[j] = j.ToString(); + int curIdx = Mathf.Clamp(controllerIndexProp.intValue, 0, ctrlLen - 1); + + if (controllerNames.Count > 0) + { + int newIdx = EditorGUILayout.Popup(curIdx, idxOptions, GUILayout.Width(60)); + if (newIdx != curIdx) controllerIndexProp.intValue = newIdx; + } + + GUILayout.FlexibleSpace(); + + + if (GUILayout.Button(_removeIcon, GUILayout.Width(28), GUILayout.Height(20))) + { + _stateEntriesProp.DeleteArrayElementAtIndex(i); + break; + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(4); + + if (stateProp.managedReferenceValue == null) + { + EditorGUILayout.HelpBox("State instance is null. Remove and re-add.", MessageType.Warning); + } + else + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.LabelField(new GUIContent("属性"), EditorStyles.boldLabel); + SerializedClassDrawer.DrawSerializableProperty(stateProp); + EditorGUILayout.EndVertical(); + } + + bool valid = true; + try + { + var inst = stateProp.managedReferenceValue as ControllerStateBase; + if (inst != null) valid = inst.Valid(recorder); + } + catch (Exception ex) + { + valid = false; + Debug.LogException(ex); + } + + if (!valid) + { + EditorGUILayout.HelpBox($"当前状态核验失败 请查看{stateProp.managedReferenceValue.GetType().FullName}脚本Valid", MessageType.Error); + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(); + } + + serializedObject.ApplyModifiedProperties(); + } + + + private void OnTypeSelected(Type type) + { + if (type == null) return; + + // 撤销支持 + Undo.RecordObject(serializedObject.targetObject, "Add Controller State"); + + serializedObject.Update(); + + int idx = _stateEntriesProp.arraySize; + _stateEntriesProp.InsertArrayElementAtIndex(idx); + var entryProp = _stateEntriesProp.GetArrayElementAtIndex(idx); + var stateProp = entryProp.FindPropertyRelative("State"); + var controllerNameProp = entryProp.FindPropertyRelative("ControllerName"); + var controllerIndexProp = entryProp.FindPropertyRelative("ControllerIndex"); + var defaultValueProp = entryProp.FindPropertyRelative("DefaultValue"); + ControllerStateBase instance = null; + try + { + instance = (ControllerStateBase)Activator.CreateInstance(type); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + + if (instance != null) + { + var recorder = serializedObject.targetObject as UXControllerStateRecorder; + instance.Init(recorder); + } + + // 把实例写进 SerializedProperty(需保证你的 State 字段用了 [SerializeReference]) + if (stateProp != null) stateProp.managedReferenceValue = instance; + if (stateProp != null) stateProp.isExpanded = true; + controllerNameProp.stringValue = ""; + controllerIndexProp.intValue = 0; + + serializedObject.ApplyModifiedProperties(); + EditorUtility.SetDirty(serializedObject.targetObject); + } + + + private UXController GetPrefabStageController() + { + var stage = PrefabStageUtility.GetCurrentPrefabStage(); + if (stage == null) return null; + var root = stage.prefabContentsRoot; + if (root == null) return null; + return root.GetComponentInChildren(); + } + + private List GetControllerNamesForRecorder(UXControllerStateRecorder recorder) + { + var names = new List(); + var prefabCtrl = GetPrefabStageController(); + if (prefabCtrl != null) + { + foreach (var cd in prefabCtrl.Controllers) names.Add(cd.Name); + return names; + } + + var assigned = recorder.Controller; + if (assigned != null) + { + foreach (var cd in assigned.Controllers) names.Add(cd.Name); + return names; + } + + var inScene = recorder.gameObject.GetComponentInParent(); + if (inScene != null) + { + foreach (var cd in inScene.Controllers) names.Add(cd.Name); + return names; + } + + return names; + } + + private int GetSelectedControllerLength(UXControllerStateRecorder recorder, string controllerName) + { + var prefabCtrl = GetPrefabStageController(); + if (prefabCtrl != null) + { + var ch = prefabCtrl.Controllers.ToList().Find(x => x.Name == controllerName); + if (ch != null) return ch.Length; + } + + var assigned = recorder.Controller; + if (assigned != null) + { + var ch = assigned.Controllers.ToList().Find(x => x.Name == controllerName); + if (ch != null) return ch.Length; + } + + var inScene = recorder.gameObject.GetComponentInParent(); + if (inScene != null) + { + var ch = inScene.Controllers.ToList().Find(x => x.Name == controllerName); + if (ch != null) return ch.Length; + } + + return 1; + } +} +#endif diff --git a/Editor/UX/Controller/UXControllerStateRecorderEditor.cs.meta b/Editor/UX/Controller/UXControllerStateRecorderEditor.cs.meta new file mode 100644 index 0000000..3fc698d --- /dev/null +++ b/Editor/UX/Controller/UXControllerStateRecorderEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a754ad6b8cb341ada13bb87279b12b78 +timeCreated: 1764134852 \ No newline at end of file diff --git a/Runtime/UXComponent/Controller.meta b/Runtime/UXComponent/Controller.meta new file mode 100644 index 0000000..32ce4b7 --- /dev/null +++ b/Runtime/UXComponent/Controller.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 821f0b8459b1fd84cb5cfaa2a256056a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/Controller/ControllerStateBase.cs b/Runtime/UXComponent/Controller/ControllerStateBase.cs new file mode 100644 index 0000000..690fb85 --- /dev/null +++ b/Runtime/UXComponent/Controller/ControllerStateBase.cs @@ -0,0 +1,36 @@ +// Assets/Scripts/AlicizaX/UI/IControllerState.cs + +using System; +using UnityEngine; + +namespace AlicizaX.UI.Runtime +{ + public abstract class ControllerStateBase + { + public abstract void Init(UXControllerStateRecorder recorder); + public abstract void Execute(UXControllerStateRecorder recorder, int entryIndex, int selectionIndex); + public abstract bool Valid(UXControllerStateRecorder recorder); + } + + [AttributeUsage(AttributeTargets.Class)] + public class ControlerStateNameAttribute : Attribute + { + public string StateName; + + public ControlerStateNameAttribute(string stateName) + { + StateName = stateName; + } + } + + [AttributeUsage(AttributeTargets.Class)] + public class ControlerStateAttachTypeAttribute : Attribute + { + public bool Repeat; + + public ControlerStateAttachTypeAttribute(bool repeat) + { + Repeat = repeat; + } + } +} diff --git a/Runtime/UXComponent/Controller/ControllerStateBase.cs.meta b/Runtime/UXComponent/Controller/ControllerStateBase.cs.meta new file mode 100644 index 0000000..fd3b016 --- /dev/null +++ b/Runtime/UXComponent/Controller/ControllerStateBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 774b49ad28de40fda18249bd9a24e38b +timeCreated: 1763705196 \ No newline at end of file diff --git a/Runtime/UXComponent/Controller/Property.meta b/Runtime/UXComponent/Controller/Property.meta new file mode 100644 index 0000000..1577bc8 --- /dev/null +++ b/Runtime/UXComponent/Controller/Property.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 97df53cd2751466dae0ab1d774d48f12 +timeCreated: 1763705022 \ No newline at end of file diff --git a/Runtime/UXComponent/Controller/Property/GameObjectPropertyState.cs b/Runtime/UXComponent/Controller/Property/GameObjectPropertyState.cs new file mode 100644 index 0000000..003fb8a --- /dev/null +++ b/Runtime/UXComponent/Controller/Property/GameObjectPropertyState.cs @@ -0,0 +1,113 @@ +using System; +using UnityEngine; +using UnityEngine.Serialization; +using UnityEngine.UI; + +namespace AlicizaX.UI.Runtime +{ + [Serializable] + [ControlerStateName("GameObject/Visiblity")] + public class GameObjectPropertyStateBase : ControllerStateBase + { + public override void Init(UXControllerStateRecorder recorder) + { + } + + public override void Execute(UXControllerStateRecorder recorder, int entryIndex, int selectionIndex) + { + if (recorder != null && recorder.gameObject != null) + { + var visible = entryIndex == selectionIndex; + recorder.gameObject.SetActive(visible); + } + } + + public override bool Valid(UXControllerStateRecorder recorder) + { + return recorder != null; + } + } + + [Serializable] + [ControlerStateName("Transform/Position")] + public class TransformPostionPropertyStateBase : ControllerStateBase + { + [SerializeField] private Vector2 _pos; + [HideInInspector] [SerializeField] private Vector2 _cachePos; + + public override void Init(UXControllerStateRecorder recorder) + { + _cachePos = recorder.gameObject.GetComponent().anchoredPosition; + } + + public override void Execute(UXControllerStateRecorder recorder, int entryIndex, int selectionIndex) + { + if (recorder != null && recorder.gameObject != null) + { + var pos = entryIndex != selectionIndex ? _cachePos : _pos; + recorder.GetComponent().anchoredPosition = pos; + } + } + + public override bool Valid(UXControllerStateRecorder recorder) + { + return recorder != null; + } + } + + [Serializable] + [ControlerStateName("Transform/Scale")] + public class TransformScalePropertyStateBase : ControllerStateBase + { + [SerializeField] private Vector3 _scale; + [HideInInspector] [SerializeField] private Vector3 _cacheScale; + + + public override void Init(UXControllerStateRecorder recorder) + { + _cacheScale = recorder.gameObject.GetComponent().localScale; + } + + public override void Execute(UXControllerStateRecorder recorder, int entryIndex, int selectionIndex) + { + if (recorder != null && recorder.gameObject != null) + { + var scale = entryIndex != selectionIndex ? _cacheScale : _scale; + recorder.GetComponent().localScale = scale; + } + } + + public override bool Valid(UXControllerStateRecorder recorder) + { + return recorder != null; + } + } + + [Serializable] + [ControlerStateName("Text/Color")] + public class TextColorPropertyStateBase : ControllerStateBase + { + [SerializeField] private Color _color = Color.white; + [HideInInspector] [SerializeField] private Color _cacheColor; + + public override void Init(UXControllerStateRecorder recorder) + { + _cacheColor = recorder.GetComponent().color; + } + + public override void Execute(UXControllerStateRecorder recorder, int entryIndex, int selectionIndex) + { + var t = recorder.GetComponent(); + if (t != null) + { + var color = entryIndex != selectionIndex ? _cacheColor : _color; + t.color = color; + } + } + + public override bool Valid(UXControllerStateRecorder recorder) + { + return recorder != null && recorder.GetComponent() != null; + } + } +} diff --git a/Runtime/UXComponent/Controller/Property/GameObjectPropertyState.cs.meta b/Runtime/UXComponent/Controller/Property/GameObjectPropertyState.cs.meta new file mode 100644 index 0000000..5907c5a --- /dev/null +++ b/Runtime/UXComponent/Controller/Property/GameObjectPropertyState.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e3b8cb1ae89c4c7b8dfb1c5797d4e8a0 +timeCreated: 1763705161 \ No newline at end of file diff --git a/Runtime/UXComponent/Controller/UXController.cs b/Runtime/UXComponent/Controller/UXController.cs new file mode 100644 index 0000000..3c453d3 --- /dev/null +++ b/Runtime/UXComponent/Controller/UXController.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace AlicizaX.UI.Runtime +{ + [DisallowMultipleComponent] + [AddComponentMenu("Controller/控制器")] + public sealed class UXController : MonoBehaviour + { + [Serializable] + public class ControllerData + { + public string Name = "Controller"; + public int Length = 2; + [NonSerialized] public int CurrentIndex = 0; + } + + [SerializeField] private List _controllers = new List(); + + [SerializeField] private List _recorders = new List(); + + internal IReadOnlyList Recorders => _recorders; + + #region Runtime API + + internal IReadOnlyList Controllers => _controllers; + + private void Awake() + { + Initialized(); + } + + private void OnValidate() + { + Initialized(); + } + + private void Initialized() + { + for (int i = 0; i < _controllers.Count; i++) + { + SetControllerIndex(i, 0); + } + } + + public ControllerHandle GetController(string name) + { + var idx = _controllers.FindIndex(c => c.Name == name); + if (idx < 0) return null; + return new ControllerHandle(this, idx); + } + + internal void SetControllerIndex(int controllerIndex, int selectedIndex) + { + if (controllerIndex < 0 || controllerIndex >= _controllers.Count) return; + var cd = _controllers[controllerIndex]; + if (selectedIndex < 0) selectedIndex = 0; + if (selectedIndex >= cd.Length) selectedIndex = cd.Length - 1; + if (cd.CurrentIndex == selectedIndex) return; + cd.CurrentIndex = selectedIndex; + + foreach (var r in _recorders) + { + if (r != null) r.OnControllerIndexChanged(cd.Name, selectedIndex); + } + } + + internal bool HasRecorder(UXControllerStateRecorder recorder) + { + return _recorders.Find(t => t.ID == recorder.ID) != null; + } + + #endregion + + [Serializable] + public class ControllerHandle + { + private readonly UXController _owner; + private readonly int _controllerIndex; + + public ControllerHandle(UXController owner, int controllerIndex) + { + _owner = owner; + _controllerIndex = controllerIndex; + } + + public int SelectedIndex + { + get + { + if (_controllerIndex < 0 || _controllerIndex >= _owner._controllers.Count) return 0; + return _owner._controllers[_controllerIndex].CurrentIndex; + } + set { _owner.SetControllerIndex(_controllerIndex, value); } + } + + public int Length + { + get + { + if (_controllerIndex < 0 || _controllerIndex >= _owner._controllers.Count) return 0; + return _owner._controllers[_controllerIndex].Length; + } + } + + public string Name => (_controllerIndex >= 0 && _controllerIndex < _owner._controllers.Count) ? _owner._controllers[_controllerIndex].Name : null; + } + } +} diff --git a/Runtime/UXComponent/Controller/UXController.cs.meta b/Runtime/UXComponent/Controller/UXController.cs.meta new file mode 100644 index 0000000..a7cf0d9 --- /dev/null +++ b/Runtime/UXComponent/Controller/UXController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5552ea6a0a020954781d0ca2e1944512 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UXComponent/Controller/UXControllerStateRecorder.cs b/Runtime/UXComponent/Controller/UXControllerStateRecorder.cs new file mode 100644 index 0000000..00b5ff3 --- /dev/null +++ b/Runtime/UXComponent/Controller/UXControllerStateRecorder.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Serialization; +using Random = UnityEngine.Random; + + +namespace AlicizaX.UI.Runtime +{ + [DisallowMultipleComponent] + [AddComponentMenu("Controller/控制器状态")] + public class UXControllerStateRecorder : MonoBehaviour + { + [SerializeField] private int _id; + [SerializeField] private UXController _controller; + + internal int ID => _id; + internal UXController Controller => _controller; + + [Serializable] + public class StateEntry + { + [SerializeReference] public ControllerStateBase State = null; + public string ControllerName = string.Empty; + public int ControllerIndex = 0; + } + + [SerializeField] private List _stateEntries = new List(); + + // private void Awake() + // { + // foreach (var entry in _stateEntries) + // { + // entry.State.Init(this); + // } + // } + + internal void OnControllerIndexChanged(string controllerName, int selectedIndex) + { + var entrys = _stateEntries.FindAll(t => t.ControllerName.Equals(controllerName)); + if (entrys != null) + { + foreach (var entry in entrys) + { + entry.State.Execute(this, entry.ControllerIndex, selectedIndex); + } + } + } + } +} diff --git a/Runtime/UXComponent/Controller/UXControllerStateRecorder.cs.meta b/Runtime/UXComponent/Controller/UXControllerStateRecorder.cs.meta new file mode 100644 index 0000000..9560517 --- /dev/null +++ b/Runtime/UXComponent/Controller/UXControllerStateRecorder.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 35829408994740a08c32ac2f519442f0 +timeCreated: 1763705324 \ No newline at end of file diff --git a/Runtime/_InternalVisibleTo.cs b/Runtime/_InternalVisibleTo.cs new file mode 100644 index 0000000..a3d477b --- /dev/null +++ b/Runtime/_InternalVisibleTo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AlicizaX.UI.Extension.Editor")] +[assembly: InternalsVisibleTo("AlicizaUXTool.Editor")] diff --git a/Runtime/_InternalVisibleTo.cs.meta b/Runtime/_InternalVisibleTo.cs.meta new file mode 100644 index 0000000..3947c50 --- /dev/null +++ b/Runtime/_InternalVisibleTo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e33eadf8eacb4c799ebce6f6178d4659 +timeCreated: 1764221559 \ No newline at end of file