using System; using UnityEditor; using UnityEngine; using UnityEngine.U2D; using TMPro; using System.Reflection; using System.Collections.Generic; using UnityEngine.InputSystem; using InputGlyphsFramework; using System.IO; using System.Linq; using UnityEngine.InputSystem.Layouts; [CustomEditor(typeof(InputGlyphDatabase))] public class InputGlyphDatabaseEditor : Editor { InputGlyphDatabase db; int tabIndex = 0; string[] tabNames = new string[] { "Keyboard", "Xbox", "PlayStation", "Other" }; void OnEnable() { db = target as InputGlyphDatabase; } public override void OnInspectorGUI() { serializedObject.Update(); if (db == null) return; EditorGUILayout.Space(); tabIndex = GUILayout.Toolbar(tabIndex, tabNames, GUILayout.Height(24)); var curDevice = (InputDeviceWatcher.InputDeviceCategory)tabIndex; if (db.tables == null) db.tables = new List(); EnsureTableFor(InputDeviceWatcher.InputDeviceCategory.Keyboard); EnsureTableFor(InputDeviceWatcher.InputDeviceCategory.Xbox); EnsureTableFor(InputDeviceWatcher.InputDeviceCategory.PlayStation); EnsureTableFor(InputDeviceWatcher.InputDeviceCategory.Other); var table = db.GetTable(curDevice); if (table == null) return; EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField(curDevice.ToString(), EditorStyles.boldLabel); table.tmpAsset = EditorGUILayout.ObjectField("TMP Sprite Asset", table.tmpAsset, typeof(TMP_SpriteAsset), false) as TMP_SpriteAsset; EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Parse TMP Asset")) { ParseTMPAssetIntoTable(table); } if (GUILayout.Button("Clear")) { table.entries.Clear(); EditorUtility.SetDirty(db); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); if (table.entries != null) { for (int i = 0; i < table.entries.Count; ++i) { var e = table.entries[i]; if (e == null) continue; EditorGUILayout.BeginHorizontal(GUILayout.Height(64)); if (e.Sprite != null) { Texture2D preview = AssetPreview.GetAssetPreview(e.Sprite); if (preview == null) preview = AssetPreview.GetMiniThumbnail(e.Sprite); if (preview != null) GUILayout.Label(preview, GUILayout.Width(64), GUILayout.Height(64)); else EditorGUILayout.ObjectField(e.Sprite, typeof(Sprite), false, GUILayout.Width(64), GUILayout.Height(64)); } else { EditorGUILayout.ObjectField(null, typeof(Sprite), false, GUILayout.Width(64), GUILayout.Height(64)); } EditorGUILayout.BeginVertical(); EditorGUILayout.LabelField(e.Sprite != null ? e.Sprite.name : "", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("ControlPath", GUILayout.Width(80)); EditorGUILayout.SelectableLabel(e.controlPath ?? string.Empty, GUILayout.Height(16), GUILayout.Width(300)); Rect btnRect = GUILayoutUtility.GetRect(60, 18, GUILayout.Width(60)); if (GUI.Button(btnRect, "Bind")) { ControlPathPickerPopup.ShowDropdown(btnRect, curDevice, (selectedPath) => { if (!string.IsNullOrEmpty(selectedPath)) { e.controlPath = selectedPath; EditorUtility.SetDirty(db); AssetDatabase.SaveAssets(); } }); } if (GUILayout.Button("Clear", GUILayout.Width(48))) { e.controlPath = string.Empty; EditorUtility.SetDirty(db); AssetDatabase.SaveAssets(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(6); } } EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Add Entry")) { table.entries.Add(new GlyphEntry { Sprite = null, controlPath = string.Empty }); EditorUtility.SetDirty(db); } if (GUILayout.Button("Remove Nulls")) { table.entries.RemoveAll(x => x == null || x.Sprite == null); EditorUtility.SetDirty(db); } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); EditorGUILayout.Space(); if (GUILayout.Button("Save Asset")) { EditorUtility.SetDirty(db); AssetDatabase.SaveAssets(); } serializedObject.ApplyModifiedProperties(); } void EnsureTableFor(InputDeviceWatcher.InputDeviceCategory device) { if (db.GetTable(device) == null) { var t = new DeviceGlyphTable { deviceType = device, tmpAsset = null, entries = new List() }; db.tables.Add(t); EditorUtility.SetDirty(db); } } void ParseTMPAssetIntoTable(DeviceGlyphTable table) { if (table == null || table.tmpAsset == null) return; var asset = table.tmpAsset; table.entries = new List(); var chars = asset.spriteCharacterTable; SpriteAtlas atlas = GetSpriteAtlasFromTMP(asset); string assetPath = AssetDatabase.GetAssetPath(asset); string assetFolder = Path.GetDirectoryName(assetPath); for (int i = 0; i < chars.Count; ++i) { var ch = chars[i]; if (ch == null) continue; var name = ch.name; if (string.IsNullOrEmpty(name)) continue; Sprite s = null; try { var glyph = ch.glyph as TMP_SpriteGlyph; if (glyph != null && glyph.sprite != null) s = glyph.sprite; } catch { } if (s == null && atlas != null) { try { s = atlas.GetSprite(name); } catch { s = null; } if (s == null) { try { var m = typeof(SpriteAtlas).GetMethod("GetSprite", new System.Type[] { typeof(string) }); if (m != null) s = m.Invoke(atlas, new object[] { name }) as Sprite; } catch { } } } if (s == null) { string[] scoped = AssetDatabase.FindAssets(name + " t:Sprite", new[] { assetFolder }); if (scoped != null && scoped.Length > 0) { foreach (var g in scoped) { var p = AssetDatabase.GUIDToAssetPath(g); var sp = AssetDatabase.LoadAssetAtPath(p); if (sp != null && sp.name == name) { s = sp; break; } } } } if (s == null) { string[] all = AssetDatabase.FindAssets(name + " t:Sprite"); if (all != null && all.Length > 0) { foreach (var g in all) { var p = AssetDatabase.GUIDToAssetPath(g); var sp = AssetDatabase.LoadAssetAtPath(p); if (sp != null && sp.name == name) { s = sp; break; } } } } table.entries.Add(new GlyphEntry { Sprite = s, controlPath = string.Empty }); } EditorUtility.SetDirty(db); AssetDatabase.SaveAssets(); } SpriteAtlas GetSpriteAtlasFromTMP(TMP_SpriteAsset asset) { if (asset == null) return null; var t = asset.GetType(); foreach (var f in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { if (typeof(SpriteAtlas).IsAssignableFrom(f.FieldType)) { var val = f.GetValue(asset) as SpriteAtlas; if (val != null) return val; } } foreach (var p in t.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { if (typeof(SpriteAtlas).IsAssignableFrom(p.PropertyType)) { try { var val = p.GetValue(asset, null) as SpriteAtlas; if (val != null) return val; } catch { } } } return null; } } public class ControlPathPickerPopup : EditorWindow { InputDeviceWatcher.InputDeviceCategory device; Action onSelect; string search = ""; Vector2 scroll; Dictionary> groups = new Dictionary>(); Dictionary> displayGroups = new Dictionary>(); Dictionary foldouts = new Dictionary(); bool listening = false; IDisposable anyButtonSubscription; public static void ShowDropdown(Rect anchorRect, InputDeviceWatcher.InputDeviceCategory device, Action onSelect) { var w = CreateInstance(); w.device = device; w.onSelect = onSelect; w.titleContent = new GUIContent("Pick Control Path"); w.minSize = new Vector2(480, 420); w.InitListsDynamic(); w.ShowAsDropDown(anchorRect, new Vector2(480, 420)); } void OnEnable() { EditorApplication.update += EditorUpdate; } void OnDisable() { StopListening(); EditorApplication.update -= EditorUpdate; } void EditorUpdate() { if (listening) Repaint(); } // -------------------- 动态构建列表 -------------------- void InitListsDynamic() { groups.Clear(); displayGroups.Clear(); foldouts.Clear(); // 首先插入一些基本的静态分组占位(如果需要) var known = new[] { "Keyboard", "Gamepad", "Mouse", "Joystick", "Touchscreen", "XR Controller", "XR HMD", "Pointer", "Pen", "Other" }; foreach (var k in known) { groups[k] = new List(); displayGroups[k] = new List(); foldouts[k] = true; } // 列出所有已注册布局名称 IEnumerable layoutNames; try { layoutNames = InputSystem.ListLayouts(); } catch { layoutNames = Enumerable.Empty(); } foreach (var layoutName in layoutNames) { InputControlLayout layout = null; try { layout = InputSystem.LoadLayout(layoutName); } catch { layout = null; } if (layout == null) continue; // 遍历 layout.controls (InputControlLayout.ControlItem) try { var controls = layout.controls; if (controls.Count == 0) continue; foreach (var ci in controls) { // isModifyingExistingControl 表示这个 control 只是修改已有 control 的属性,不引入新的控制项,通常跳过 if (ci.isModifyingExistingControl) continue; string controlName = ci.name.ToString(); if (string.IsNullOrEmpty(controlName)) continue; string path = $"<{layoutName}>/{controlName}"; // 决定分组:根据 layoutName 或 controlName 做简单匹配 string group = DecideGroupForLayout(layoutName, controlName); if (!groups.ContainsKey(group)) { groups[group] = new List(); displayGroups[group] = new List(); foldouts[group] = true; } // 防重复 if (!groups[group].Contains(path)) { groups[group].Add(path); displayGroups[group].Add($"{controlName} ({path})"); } // 试着添加常见的子项(例如 stick/up/down 等),如果 controlItem 标识这些为子控制可能需要额外处理 // 但大多数情况下 layout.controls 已包含独立条目如 leftStick/up } } catch { /* 忽略个别布局异常 */ } } // 额外补充:把一些常用手动添加的路径放到 Gamepad / Keyboard 中,防止某些 layout 未定义具体按钮 AddFallbackCommonPaths(); } string DecideGroupForLayout(string layoutName, string controlName) { var lname = layoutName.ToLowerInvariant(); if (lname.Contains("keyboard")) return "Keyboard"; if (lname.Contains("gamepad") || lname.Contains("xbox") || lname.Contains("dualshock") || lname.Contains("controller")) return "Gamepad"; if (lname.Contains("mouse")) return "Mouse"; if (lname.Contains("joystick")) return "Joystick"; if (lname.Contains("touchscreen")) return "Touchscreen"; if (lname.Contains("xr") || lname.Contains("hmd")) return "XR Controller"; return "Other"; } void AddFallbackCommonPaths() { void addIfMissing(string grp, string path, string label = null) { if (!groups.ContainsKey(grp)) { groups[grp] = new List(); displayGroups[grp] = new List(); foldouts[grp] = true; } if (!groups[grp].Contains(path)) { groups[grp].Add(path); displayGroups[grp].Add(label ?? path); } } addIfMissing("Gamepad", "/buttonSouth"); addIfMissing("Gamepad", "/buttonEast"); addIfMissing("Gamepad", "/buttonWest"); addIfMissing("Gamepad", "/buttonNorth"); addIfMissing("Gamepad", "/leftShoulder"); addIfMissing("Gamepad", "/rightShoulder"); addIfMissing("Gamepad", "/leftTrigger"); addIfMissing("Gamepad", "/rightTrigger"); addIfMissing("Keyboard", "/space"); addIfMissing("Keyboard", "/enter"); addIfMissing("Keyboard", "/escape"); addIfMissing("Mouse", "/leftButton"); } // -------------------- UI -------------------- void OnGUI() { EditorGUILayout.BeginVertical(); EditorGUILayout.BeginHorizontal(); var prevColor = GUI.color; search = EditorGUILayout.TextField(search, GUILayout.ExpandWidth(true)); if (!listening) { if (GUILayout.Button("Listen", GUILayout.Width(72))) { StartListening(); } } else { GUI.color = Color.yellow; if (GUILayout.Button("Stop", GUILayout.Width(72))) { StopListening(); } GUI.color = prevColor; } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); scroll = EditorGUILayout.BeginScrollView(scroll); foreach (var kv in displayGroups.OrderBy(k => k.Key)) { string cat = kv.Key; if (!ShouldShowCategory(cat)) continue; foldouts.TryGetValue(cat, out bool f); f = EditorGUILayout.Foldout(f, cat, true); foldouts[cat] = f; if (!f) continue; var displays = kv.Value; var vals = groups[cat]; for (int i = 0; i < displays.Count; ++i) { var dsp = displays[i]; var val = vals[i]; if (!MatchSearch(dsp, val)) continue; EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Select", GUILayout.Width(64))) { onSelect?.Invoke(val); Close(); return; } EditorGUILayout.LabelField(dsp, GUILayout.ExpandWidth(true)); EditorGUILayout.EndHorizontal(); } EditorGUILayout.Space(6); } EditorGUILayout.EndScrollView(); EditorGUILayout.Space(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Close")) Close(); EditorGUILayout.EndVertical(); } bool ShouldShowCategory(string cat) { if (string.IsNullOrEmpty(search)) return true; if (cat.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true; var disp = displayGroups[cat]; for (int i = 0; i < disp.Count; ++i) if (MatchSearch(disp[i], groups[cat][i])) return true; return false; } bool MatchSearch(string display, string value) { if (string.IsNullOrEmpty(search)) return true; if (display.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true; if (value.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true; return false; } // -------------------- Listen (使用 IObserver 订阅) -------------------- class InputControlObserver : IObserver { readonly Action onNext; public InputControlObserver(Action onNext) { this.onNext = onNext; } public void OnCompleted() { } public void OnError(Exception error) { } public void OnNext(InputControl value) { try { onNext?.Invoke(value); } catch { } } } void StartListening() { if (listening) return; listening = true; try { // 使用 IObserver 订阅(通用、可靠) anyButtonSubscription = InputSystem.onAnyButtonPress.Subscribe(new InputControlObserver(control => { if (control == null) return; string path = control.path; onSelect?.Invoke(path); StopListening(); Close(); })); } catch (Exception) { // 某些版本/包装可能提供 Call/CallOnce 扩展方法(Utilities),尝试反射调用这些扩展 try { var t = typeof(InputSystem); var prop = t.GetProperty("onAnyButtonPress"); if (prop != null) { var obs = prop.GetValue(null); var callMethod = obs?.GetType().GetMethod("CallOnce") ?? obs?.GetType().GetMethod("Call"); if (callMethod != null) { // 尝试调用 CallOnce(Action) callMethod.Invoke(obs, new object[] { new Action(c => { if (c == null) return; onSelect?.Invoke(c.path); StopListening(); Close(); }) }); StopListening(); Close(); return; } } } catch { } // 如果都失败,简单关闭监听状态 listening = false; } } void StopListening() { if (!listening) return; listening = false; try { anyButtonSubscription?.Dispose(); anyButtonSubscription = null; } catch { anyButtonSubscription = null; } } }