diff --git a/Client/Assets/InputGlyph/GlyphService.cs b/Client/Assets/InputGlyph/GlyphService.cs index 056a72c..4247964 100644 --- a/Client/Assets/InputGlyph/GlyphService.cs +++ b/Client/Assets/InputGlyph/GlyphService.cs @@ -1,15 +1,15 @@ using System; -using System.Linq; -using AlicizaX.InputGlyph; using UnityEngine; using UnityEngine.InputSystem; public static class GlyphService { - /// - /// 可选的全局数据库引用。你可以通过场景内的启动组件在 Awake 时赋值, - /// 或者在调用每个方法时传入 InputGlyphDatabase 参数(见方法签名)。 - /// + // Cached device hint arrays to avoid allocations + private static readonly string[] KeyboardHints = { "Keyboard", "Mouse" }; + private static readonly string[] XboxHints = { "XInput", "Xbox", "Gamepad" }; + private static readonly string[] PlayStationHints = { "DualShock", "DualSense", "PlayStation", "Gamepad" }; + private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' }; + public static InputGlyphDatabase Database { get @@ -26,22 +26,22 @@ public static class GlyphService private static InputGlyphDatabase _database; - public static string GetBindingControlPath(InputAction action, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) + public static string GetBindingControlPath(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) { if (action == null) return string.Empty; - var binding = GetBindingControl(action, deviceOverride); + var binding = GetBindingControl(action, compositePartName, deviceOverride); return binding.hasOverrides ? binding.effectivePath : binding.path; } - public static bool TryGetTMPTagForActionPath(InputActionReference reference, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null) + public static bool TryGetTMPTagForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null) { - string path = GetBindingControlPath(reference, device); + string path = GetBindingControlPath(reference, compositePartName, device); return TryGetTMPTagForActionPath(path, device, out tag, out displayFallback, db); } - public static bool TryGetUISpriteForActionPath(InputActionReference reference, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null) + public static bool TryGetUISpriteForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null) { - string path = GetBindingControlPath(reference, device); + string path = GetBindingControlPath(reference, compositePartName, device); return TryGetUISpriteForActionPath(path, device, out sprite, db); } @@ -70,52 +70,68 @@ public static class GlyphService } - static InputBinding GetBindingControl(InputAction action, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) + static InputBinding GetBindingControl(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) { if (action == null) return default; var curCategory = deviceOverride ?? InputDeviceWatcher.CurrentCategory; var hints = GetDeviceHintsForCategory(curCategory); - foreach (var binding in action.bindings) + foreach (var b in action.bindings) { - var deviceName = binding.path ?? string.Empty; - if (hints.Any(h => deviceName.IndexOf(h, StringComparison.OrdinalIgnoreCase) >= 0)) + if (!string.IsNullOrEmpty(compositePartName)) { - return binding; + if (!b.isPartOfComposite) continue; + if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue; } + + // Replace LINQ Any() to avoid delegate allocation + if (!string.IsNullOrEmpty(b.path) && ContainsAnyHint(b.path, hints)) return b; + if (!string.IsNullOrEmpty(b.effectivePath) && ContainsAnyHint(b.effectivePath, hints)) return b; } return default; } + // Helper method to avoid LINQ Any() allocation + static bool ContainsAnyHint(string path, string[] hints) + { + for (int i = 0; i < hints.Length; i++) + { + if (path.IndexOf(hints[i], StringComparison.OrdinalIgnoreCase) >= 0) + return true; + } + return false; + } + static string[] GetDeviceHintsForCategory(InputDeviceWatcher.InputDeviceCategory cat) { switch (cat) { case InputDeviceWatcher.InputDeviceCategory.Keyboard: - return new[] { "Keyboard", "Mouse" }; + return KeyboardHints; case InputDeviceWatcher.InputDeviceCategory.Xbox: - return new[] { "XInput", "Xbox", "Gamepad" }; + return XboxHints; case InputDeviceWatcher.InputDeviceCategory.PlayStation: - return new[] { "DualShock", "DualSense", "PlayStation", "Gamepad" }; + return PlayStationHints; default: - return new[] { "XInput", "Xbox", "Gamepad" }; + return XboxHints; } } - public static string GetDisplayNameFromInputAction(InputAction reference) + public static string GetDisplayNameFromInputAction(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory deviceOverride = InputDeviceWatcher.InputDeviceCategory.Keyboard) { - string controlPath = GetBindingControlPath(reference, InputDeviceWatcher.CurrentCategory); - return GetDisplayNameFromControlPath(controlPath); + if (action == null) return string.Empty; + var binding = GetBindingControl(action, compositePartName, deviceOverride); + return binding.ToDisplayString(); } public static string GetDisplayNameFromControlPath(string controlPath) { if (string.IsNullOrEmpty(controlPath)) return string.Empty; var parts = controlPath.Split('/'); - var last = parts[parts.Length - 1].Trim(new char[] { '{', '}', '<', '>', '\'', '"' }); + var last = parts[parts.Length - 1].Trim(TrimChars); return last; } } diff --git a/Client/Assets/InputGlyph/InputBindingManager.cs b/Client/Assets/InputGlyph/InputBindingManager.cs index 5dd84c7..2b226a5 100644 --- a/Client/Assets/InputGlyph/InputBindingManager.cs +++ b/Client/Assets/InputGlyph/InputBindingManager.cs @@ -9,54 +9,78 @@ using System.Threading.Tasks; using UnityEngine; using UnityEngine.InputSystem; using System.Reactive.Subjects; -using AlicizaX.InputGlyph; +using AlicizaX; + +using Cysharp.Threading.Tasks; using RxUnit = System.Reactive.Unit; -namespace InputRemapper -{ - public class InputBindingManager : MonoBehaviour + + public class InputBindingManager : MonoSingleton { public const string NULL_BINDING = "__NULL__"; + private const string KEYBOARD_DEVICE = ""; + private const string MOUSE_DELTA = "/delta"; + private const string MOUSE_SCROLL = "/scroll"; + private const string MOUSE_SCROLL_X = "/scroll/x"; + private const string MOUSE_SCROLL_Y = "/scroll/y"; + private const string KEYBOARD_ESCAPE = "/escape"; [Tooltip("InputActionAsset to manage")] public InputActionAsset actions; - [SerializeField] private InputGlyphDatabase inputGlyphDatabase; - public string fileName = "input_bindings.json"; public bool debugMode = false; public Dictionary actionMap = new Dictionary(); - public List preparedRebinds = new List(); + public HashSet preparedRebinds = new HashSet(); internal InputActionRebindingExtensions.RebindingOperation rebindOperation; - private readonly List pressedActions = new List(); private bool isApplyPending = false; private string defaultBindingsJson = string.Empty; + private string cachedSavePath; + private Dictionary actionLookup = new Dictionary(); - public ReplaySubject OnInputsInit = new ReplaySubject(1); - public Subject OnApply = new Subject(); - public Subject OnRebindPrepare = new Subject(); - public Subject OnRebindStart = new Subject(); - public Subject OnRebindEnd = new Subject(); - public Subject<(RebindContext prepared, RebindContext conflict)> OnRebindConflict = new Subject<(RebindContext, RebindContext)>(); + public readonly ReplaySubject OnInputsInit = new ReplaySubject(1); + public readonly Subject<(bool success, HashSet appliedContexts)> OnApply = new Subject<(bool, HashSet)>(); + public readonly Subject OnRebindPrepare = new Subject(); + public readonly Subject OnRebindStart = new Subject(); + public readonly Subject<(bool success, RebindContext context)> OnRebindEnd = new Subject<(bool, RebindContext)>(); + public readonly Subject<(RebindContext prepared, RebindContext conflict)> OnRebindConflict = new Subject<(RebindContext, RebindContext)>(); public string SavePath { get { + if (!string.IsNullOrEmpty(cachedSavePath)) + return cachedSavePath; + #if UNITY_EDITOR string folder = Application.dataPath; #else string folder = Application.persistentDataPath; #endif - if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); - return Path.Combine(folder, fileName); + cachedSavePath = Path.Combine(folder, fileName); + return cachedSavePath; } } - private void Awake() + private void EnsureSaveDirectoryExists() { + var directory = Path.GetDirectoryName(SavePath); + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory); + } + + protected override void Awake() + { + if (_instance != null && _instance != this) + { + Destroy(gameObject); + return; + } + + _instance = this; + if (actions == null) { Debug.LogError("InputBindingManager: InputActionAsset not assigned."); @@ -69,8 +93,9 @@ namespace InputRemapper { defaultBindingsJson = actions.SaveBindingOverridesAsJson(); } - catch + catch (Exception ex) { + Debug.LogWarning($"[InputBindingManager] Failed to save default bindings: {ex.Message}"); defaultBindingsJson = string.Empty; } @@ -83,7 +108,10 @@ namespace InputRemapper { actions.LoadBindingOverridesFromJson(json); RefreshBindingPathsFromActions(); - if (debugMode) Debug.Log($"Loaded overrides from {SavePath}"); + if (debugMode) + { + Debug.Log($"Loaded overrides from {SavePath}"); + } } } catch (Exception ex) @@ -96,8 +124,13 @@ namespace InputRemapper actions.Enable(); } - private void OnDestroy() + protected override void OnDestroy() { + if (_instance == this) + { + _instance = null; + } + rebindOperation?.Dispose(); rebindOperation = null; @@ -111,18 +144,45 @@ namespace InputRemapper private void BuildActionMap() { + // Pre-allocate with known capacity to avoid resizing + int mapCount = actions.actionMaps.Count; actionMap.Clear(); + actionLookup.Clear(); + + // Estimate total action count for better allocation + int estimatedActionCount = 0; foreach (var map in actions.actionMaps) - actionMap.Add(map.name, new ActionMap(map)); + { + estimatedActionCount += map.actions.Count; + } + + // Ensure capacity to avoid rehashing + if (actionMap.Count == 0) + { + actionMap = new Dictionary(mapCount); + actionLookup = new Dictionary(estimatedActionCount); + } + + foreach (var map in actions.actionMaps) + { + var actionMapObj = new ActionMap(map); + actionMap.Add(map.name, actionMapObj); + + // Build lookup dictionary for O(1) action access + foreach (var actionPair in actionMapObj.actions) + { + actionLookup[actionPair.Key] = (actionMapObj, actionPair.Value); + } + } } private void RefreshBindingPathsFromActions() { - foreach (var mapPair in actionMap) + foreach (var mapPair in actionMap.Values) { - foreach (var actionPair in mapPair.Value.actions) + foreach (var actionPair in mapPair.actions.Values) { - var a = actionPair.Value; + var a = actionPair; foreach (var bpair in a.bindings) { bpair.Value.bindingPath.EffectivePath = a.action.bindings[bpair.Key].effectivePath; @@ -131,26 +191,33 @@ namespace InputRemapper } } - public class ActionMap + public sealed class ActionMap { public string name; - public Dictionary actions = new Dictionary(); + public Dictionary actions; public ActionMap(InputActionMap map) { name = map.name; - foreach (var action in map.actions) actions.Add(action.name, new Action(action)); + int actionCount = map.actions.Count; + actions = new Dictionary(actionCount); + foreach (var action in map.actions) + { + actions.Add(action.name, new Action(action)); + } } - public class Action + public sealed class Action { public InputAction action; - public Dictionary bindings = new Dictionary(); + public Dictionary bindings; public Action(InputAction action) { this.action = action; int count = action.bindings.Count; + bindings = new Dictionary(count); + for (int i = 0; i < count; i++) { if (action.bindings[i].isComposite) @@ -170,28 +237,39 @@ namespace InputRemapper void AddBinding(InputBinding binding, int bindingIndex) { - bindings.Add(bindingIndex, new Binding - { - name = binding.name, - parentAction = action.name, - compositePart = binding.name, - bindingIndex = bindingIndex, - group = binding.groups?.Split(InputBinding.Separator) ?? Array.Empty(), - bindingPath = new BindingPath(binding.path, binding.overridePath), - inputBinding = binding - }); + bindings.Add(bindingIndex, new Binding( + binding.name, + action.name, + binding.name, + bindingIndex, + binding.groups?.Split(InputBinding.Separator) ?? Array.Empty(), + new BindingPath(binding.path, binding.overridePath), + binding + )); } } - public struct Binding + public readonly struct Binding { - public string name; - public string parentAction; - public string compositePart; - public int bindingIndex; - public string[] group; - public BindingPath bindingPath; - public InputBinding inputBinding; + public readonly string name; + public readonly string parentAction; + public readonly string compositePart; + public readonly int bindingIndex; + public readonly string[] group; + public readonly BindingPath bindingPath; + public readonly InputBinding inputBinding; + + public Binding(string name, string parentAction, string compositePart, int bindingIndex, + string[] group, BindingPath bindingPath, InputBinding inputBinding) + { + this.name = name; + this.parentAction = parentAction; + this.compositePart = compositePart; + this.bindingIndex = bindingIndex; + this.group = group; + this.bindingPath = bindingPath; + this.inputBinding = inputBinding; + } } } } @@ -200,7 +278,7 @@ namespace InputRemapper { public string bindingPath; public string overridePath; - private readonly Subject observer = new Subject(); + private Subject observer; public BindingPath(string bindingPath, string overridePath) { @@ -214,18 +292,33 @@ namespace InputRemapper set { overridePath = (value == bindingPath) ? string.Empty : value; - observer.OnNext(RxUnit.Default); + observer?.OnNext(RxUnit.Default); } } - public IObservable EffectivePathObservable => observer.Select(_ => EffectivePath); + public IObservable EffectivePathObservable + { + get + { + observer ??= new Subject(); + return observer.Select(_ => EffectivePath); + } + } + + public void Dispose() + { + observer?.OnCompleted(); + observer?.Dispose(); + observer = null; + } } - public class RebindContext + public sealed class RebindContext { public InputAction action; public int bindingIndex; public string overridePath; + private string cachedToString; public RebindContext(InputAction action, int bindingIndex, string overridePath) { @@ -242,16 +335,28 @@ namespace InputRemapper } public override int GetHashCode() => (action?.name ?? string.Empty, bindingIndex).GetHashCode(); - public override string ToString() => $"{action?.name ?? ""}:{bindingIndex}"; + + public override string ToString() + { + if (cachedToString == null && action != null) + { + cachedToString = $"{action.name}:{bindingIndex}"; + } + + return cachedToString ?? ""; + } } /* ---------------- Public API ---------------- */ public static InputAction Action(string actionName) { - foreach (var map in Instance.actionMap) + var instance = Instance; + if (instance == null) return null; + + if (instance.actionLookup.TryGetValue(actionName, out var result)) { - if (map.Value.actions.TryGetValue(actionName, out var a)) return a.action; + return result.action.action; } Debug.LogError($"[InputBindingManager] Could not find action '{actionName}'"); @@ -272,42 +377,61 @@ namespace InputRemapper } Instance.actions.Disable(); - Instance.PerformInteractiveRebinding(action, bindingIndex, "", true); + Instance.PerformInteractiveRebinding(action, bindingIndex, KEYBOARD_DEVICE, true); Instance.OnRebindStart.OnNext(RxUnit.Default); - if (Instance.debugMode) Debug.Log("[InputBindingManager] Rebind started"); + if (Instance.debugMode) + { + Debug.Log("[InputBindingManager] Rebind started"); + } } public static void CancelRebind() => Instance.rebindOperation?.Cancel(); - public static async Task ConfirmApply(bool clearConflicts = true) + public static async UniTask ConfirmApply(bool clearConflicts = true) { if (!Instance.isApplyPending) return false; try { + // Create a copy of the prepared rebinds before clearing + var appliedContexts = new HashSet(Instance.preparedRebinds); + foreach (var ctx in Instance.preparedRebinds) { - if (string.IsNullOrEmpty(ctx.overridePath)) + if (!string.IsNullOrEmpty(ctx.overridePath)) { + if (ctx.overridePath == NULL_BINDING) + { + ctx.action.RemoveBindingOverride(ctx.bindingIndex); + } + else + { + ctx.action.ApplyBindingOverride(ctx.bindingIndex, ctx.overridePath); + } } - else if (ctx.overridePath == NULL_BINDING) ctx.action.RemoveBindingOverride(ctx.bindingIndex); - else ctx.action.ApplyBindingOverride(ctx.bindingIndex, ctx.overridePath); var bp = GetBindingPath(ctx.action.name, ctx.bindingIndex); - if (bp != null) bp.EffectivePath = (ctx.overridePath == NULL_BINDING) ? string.Empty : ctx.overridePath; + if (bp != null) + { + bp.EffectivePath = (ctx.overridePath == NULL_BINDING) ? string.Empty : ctx.overridePath; + } } Instance.preparedRebinds.Clear(); await Instance.WriteOverridesToDiskAsync(); - Instance.OnApply.OnNext(true); + Instance.OnApply.OnNext((true, appliedContexts)); Instance.isApplyPending = false; - if (Instance.debugMode) Debug.Log("[InputBindingManager] Apply confirmed and saved."); + if (Instance.debugMode) + { + Debug.Log("[InputBindingManager] Apply confirmed and saved."); + } + return true; } catch (Exception ex) { Debug.LogError("[InputBindingManager] Failed to apply binds: " + ex); - Instance.OnApply.OnNext(false); + Instance.OnApply.OnNext((false, new HashSet())); return false; } } @@ -315,23 +439,34 @@ namespace InputRemapper public static void DiscardPrepared() { if (!Instance.isApplyPending) return; + + // Create a copy of the prepared rebinds before clearing (for event notification) + var discardedContexts = new HashSet(Instance.preparedRebinds); + Instance.preparedRebinds.Clear(); Instance.isApplyPending = false; - Instance.OnApply.OnNext(false); - if (Instance.debugMode) Debug.Log("[InputBindingManager] Prepared rebinds discarded."); + Instance.OnApply.OnNext((false, discardedContexts)); + if (Instance.debugMode) + { + Debug.Log("[InputBindingManager] Prepared rebinds discarded."); + } } private void PerformInteractiveRebinding(InputAction action, int bindingIndex, string deviceMatchPath = null, bool excludeMouseMovementAndScroll = true) { var op = action.PerformInteractiveRebinding(bindingIndex); - if (!string.IsNullOrEmpty(deviceMatchPath)) op = op.WithControlsHavingToMatchPath(deviceMatchPath); + if (!string.IsNullOrEmpty(deviceMatchPath)) + { + op = op.WithControlsHavingToMatchPath(deviceMatchPath); + } + if (excludeMouseMovementAndScroll) { - op = op.WithControlsExcluding("/delta"); - op = op.WithControlsExcluding("/scroll"); - op = op.WithControlsExcluding("/scroll/x"); - op = op.WithControlsExcluding("/scroll/y"); + op = op.WithControlsExcluding(MOUSE_DELTA) + .WithControlsExcluding(MOUSE_SCROLL) + .WithControlsExcluding(MOUSE_SCROLL_X) + .WithControlsExcluding(MOUSE_SCROLL_Y); } rebindOperation = op @@ -356,19 +491,27 @@ namespace InputRemapper }) .OnComplete(opc => { - if (debugMode) Debug.Log("[InputBindingManager] Rebind completed"); + if (debugMode) + { + Debug.Log("[InputBindingManager] Rebind completed"); + } + actions.Enable(); - OnRebindEnd.OnNext(true); + OnRebindEnd.OnNext((true, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath))); CleanRebindOperation(); }) .OnCancel(opc => { - if (debugMode) Debug.Log("[InputBindingManager] Rebind cancelled"); + if (debugMode) + { + Debug.Log("[InputBindingManager] Rebind cancelled"); + } + actions.Enable(); - OnRebindEnd.OnNext(false); + OnRebindEnd.OnNext((false, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath))); CleanRebindOperation(); }) - .WithCancelingThrough("/escape") + .WithCancelingThrough(KEYBOARD_ESCAPE) .Start(); } @@ -395,17 +538,21 @@ namespace InputRemapper private bool AnyBindingPath(string bindingPath, InputAction currentAction, int currentIndex, out (InputAction action, int bindingIndex) duplicate) { - foreach (var map in actionMap) + foreach (var map in actionMap.Values) { - foreach (var actionPair in map.Value.actions) + foreach (var actionPair in map.actions.Values) { - foreach (var bindingPair in actionPair.Value.bindings) + bool isSameAction = actionPair.action == currentAction; + + foreach (var bindingPair in actionPair.bindings) { - if (actionPair.Value.action == currentAction && bindingPair.Key == currentIndex) continue; - var eff = bindingPair.Value.bindingPath.EffectivePath; - if (eff == bindingPath) + // Skip if it's the same action and same binding index + if (isSameAction && bindingPair.Key == currentIndex) + continue; + + if (bindingPair.Value.bindingPath.EffectivePath == bindingPath) { - duplicate = (actionPair.Value.action, bindingPair.Key); + duplicate = (actionPair.action, bindingPair.Key); return true; } } @@ -418,7 +565,8 @@ namespace InputRemapper private void PrepareRebind(RebindContext context) { - preparedRebinds.RemoveAll(x => x.Equals(context)); + // Remove existing rebind for same action/binding if exists + preparedRebinds.Remove(context); if (string.IsNullOrEmpty(context.overridePath)) { @@ -434,19 +582,24 @@ namespace InputRemapper preparedRebinds.Add(context); isApplyPending = true; OnRebindPrepare.OnNext(context); - if (debugMode) Debug.Log($"Prepared rebind: {context} -> {context.overridePath}"); + if (debugMode) + { + Debug.Log($"Prepared rebind: {context} -> {context.overridePath}"); + } } } - private async Task WriteOverridesToDiskAsync() + private async UniTask WriteOverridesToDiskAsync() { try { var json = actions.SaveBindingOverridesAsJson(); - var dir = Path.GetDirectoryName(SavePath); - if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); + EnsureSaveDirectoryExists(); using (var sw = new StreamWriter(SavePath, false)) await sw.WriteAsync(json); - if (debugMode) Debug.Log($"Overrides saved to {SavePath}"); + if (debugMode) + { + Debug.Log($"Overrides saved to {SavePath}"); + } } catch (Exception ex) { @@ -455,22 +608,34 @@ namespace InputRemapper } } - public async Task ResetToDefaultAsync() + public async UniTask ResetToDefaultAsync() { try { - if (!string.IsNullOrEmpty(defaultBindingsJson)) actions.LoadBindingOverridesFromJson(defaultBindingsJson); + if (!string.IsNullOrEmpty(defaultBindingsJson)) + { + actions.LoadBindingOverridesFromJson(defaultBindingsJson); + } else { - foreach (var map in actionMap) - foreach (var a in map.Value.actions) - for (int b = 0; b < a.Value.action.bindings.Count; b++) - a.Value.action.RemoveBindingOverride(b); + foreach (var map in actionMap.Values) + { + foreach (var a in map.actions.Values) + { + for (int b = 0; b < a.action.bindings.Count; b++) + { + a.action.RemoveBindingOverride(b); + } + } + } } RefreshBindingPathsFromActions(); await WriteOverridesToDiskAsync(); - if (debugMode) Debug.Log("Reset to default and saved."); + if (debugMode) + { + Debug.Log("Reset to default and saved."); + } } catch (Exception ex) { @@ -480,17 +645,21 @@ namespace InputRemapper public static BindingPath GetBindingPath(string actionName, int bindingIndex = 0) { - foreach (var map in Instance.actionMap) + var instance = Instance; + if (instance == null) return null; + + if (instance.actionLookup.TryGetValue(actionName, out var result)) { - if (map.Value.actions.TryGetValue(actionName, out var action)) + if (result.action.bindings.TryGetValue(bindingIndex, out var binding)) { - if (action.bindings.TryGetValue(bindingIndex, out var binding)) return binding.bindingPath; + return binding.bindingPath; } } return null; } + // choose best binding index for keyboard; if compositePartName != null then look for part public int FindBestBindingIndexForKeyboard(InputAction action, string compositePartName = null) { @@ -498,36 +667,51 @@ namespace InputRemapper int fallbackPart = -1; int fallbackNonComposite = -1; + bool searchingForCompositePart = !string.IsNullOrEmpty(compositePartName); for (int i = 0; i < action.bindings.Count; i++) { var b = action.bindings[i]; - if (!string.IsNullOrEmpty(compositePartName)) + // If searching for a specific composite part, skip non-matching bindings + if (searchingForCompositePart) { if (!b.isPartOfComposite) continue; if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue; } + // Check if this binding is for keyboard + bool isKeyboardBinding = (!string.IsNullOrEmpty(b.path) && b.path.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)) || + (!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)); + if (b.isPartOfComposite) { if (fallbackPart == -1) fallbackPart = i; - if (!string.IsNullOrEmpty(b.path) && b.path.StartsWith("", StringComparison.OrdinalIgnoreCase)) return i; - if (!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith("", StringComparison.OrdinalIgnoreCase)) return i; + if (isKeyboardBinding) return i; } else { if (fallbackNonComposite == -1) fallbackNonComposite = i; - if (!string.IsNullOrEmpty(b.path) && b.path.StartsWith("", StringComparison.OrdinalIgnoreCase)) return i; - if (!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith("", StringComparison.OrdinalIgnoreCase)) return i; + if (isKeyboardBinding) return i; } } - if (fallbackNonComposite >= 0) return fallbackNonComposite; - return fallbackPart; + return fallbackNonComposite >= 0 ? fallbackNonComposite : fallbackPart; + } + + public static InputBindingManager Instance + { + get + { + if (_instance == null) + { + _instance = FindObjectOfType(); + } + + return _instance; + } } - public static InputBindingManager Instance => _instance ??= FindObjectOfType(); private static InputBindingManager _instance; } -} + diff --git a/Client/Assets/InputGlyph/InputGlyphDatabase.cs b/Client/Assets/InputGlyph/InputGlyphDatabase.cs index 4c5a8d8..81ad70c 100644 --- a/Client/Assets/InputGlyph/InputGlyphDatabase.cs +++ b/Client/Assets/InputGlyph/InputGlyphDatabase.cs @@ -5,101 +5,184 @@ using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.U2D; -namespace AlicizaX.InputGlyph + +[Serializable] +public sealed class GlyphEntry { - [Serializable] - public class GlyphEntry + public Sprite Sprite; + public InputAction action; +} + +[Serializable] +public sealed class DeviceGlyphTable +{ + public string deviceName; + + public Texture2D spriteSheetTexture; + + public Sprite platformIcons; + + public List entries = new List(); +} + +[CreateAssetMenu(fileName = "InputGlyphDatabase", menuName = "InputGlyphs/InputGlyphDatabase", order = 400)] +public sealed class InputGlyphDatabase : ScriptableObject +{ + private const string DEVICE_KEYBOARD = "Keyboard"; + private const string DEVICE_XBOX = "Xbox"; + private const string DEVICE_PLAYSTATION = "PlayStation"; + + public List tables = new List(); + + // 当 FindEntryByControlPath 传空 path 时返回的占位 sprite + public Sprite placeholderSprite; + + // Cache for faster lookups + private Dictionary _tableCache; + private Dictionary<(string path, InputDeviceWatcher.InputDeviceCategory device), Sprite> _spriteCache; + + private void OnEnable() { - public Sprite Sprite; - public InputAction action; + BuildCache(); } - [Serializable] - public class DeviceGlyphTable + private void BuildCache() { - // Table 名称(序列化) - public string deviceName; + if (_tableCache == null) + { + _tableCache = new Dictionary(tables.Count); + } + else + { + _tableCache.Clear(); + } - // 支持三种来源: - // 1) TMP Sprite Asset(TextMeshPro sprite asset) - public TMP_SpriteAsset tmpAsset; + if (_spriteCache == null) + { + _spriteCache = new Dictionary<(string, InputDeviceWatcher.InputDeviceCategory), Sprite>(); + } + else + { + _spriteCache.Clear(); + } - // 2) Unity SpriteAtlas(可选) - public SpriteAtlas spriteAtlas; - - // 3) Texture2D(Sprite Mode = Multiple),在 Sprite Editor 切好的切片 - public Texture2D spriteSheetTexture; - - public List entries = new List(); + for (int i = 0; i < tables.Count; i++) + { + var table = tables[i]; + if (table != null && !string.IsNullOrEmpty(table.deviceName)) + { + _tableCache[table.deviceName.ToLowerInvariant()] = table; + } + } } - [CreateAssetMenu(fileName = "InputGlyphDatabase", menuName = "InputGlyphs/InputGlyphDatabase", order = 400)] - public class InputGlyphDatabase : ScriptableObject + public DeviceGlyphTable GetTable(string deviceName) { - public List tables = new List(); + if (string.IsNullOrEmpty(deviceName)) return null; + if (tables == null) return null; - // 当 FindEntryByControlPath 传空 path 时返回的占位 sprite - public Sprite placeholderSprite; - - public DeviceGlyphTable GetTable(string deviceName) + // Ensure cache is built + if (_tableCache == null || _tableCache.Count == 0) { - if (string.IsNullOrEmpty(deviceName)) return null; - if (tables == null) return null; - for (int i = 0; i < tables.Count; ++i) - { - var t = tables[i]; - if (t == null) continue; - if (string.Equals(t.deviceName, deviceName, StringComparison.OrdinalIgnoreCase)) - return t; - } - return null; + BuildCache(); } - // 兼容枚举版本(示例) - public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device) + // Use cache for O(1) lookup + if (_tableCache.TryGetValue(deviceName.ToLowerInvariant(), out var table)) { - string name = "Other"; - switch (device) - { - case InputDeviceWatcher.InputDeviceCategory.Keyboard: name = "Keyboard"; break; - case InputDeviceWatcher.InputDeviceCategory.Xbox: name = "Xbox"; break; - case InputDeviceWatcher.InputDeviceCategory.PlayStation: name = "PlayStation"; break; - default: name = "Xbox"; break; - } - return GetTable(name); + return table; } - public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device) + return null; + } + + public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device) + { + var table = GetTable(device); + if (table == null) return null; + return table.platformIcons; + } + + public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device) + { + // Use constants to avoid string allocations + string name; + switch (device) { - var entry = FindEntryByControlPath(controlPath, device); - if (string.IsNullOrEmpty(controlPath) || entry == null) - { - return placeholderSprite; - } - return entry.Sprite; + case InputDeviceWatcher.InputDeviceCategory.Keyboard: + name = DEVICE_KEYBOARD; + break; + case InputDeviceWatcher.InputDeviceCategory.Xbox: + name = DEVICE_XBOX; + break; + case InputDeviceWatcher.InputDeviceCategory.PlayStation: + name = DEVICE_PLAYSTATION; + break; + default: + name = DEVICE_XBOX; + break; } - public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device) + return GetTable(name); + } + + public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device) + { + if (string.IsNullOrEmpty(controlPath)) { - var t = GetTable(device); - if (t != null) + return placeholderSprite; + } + + // Check cache first + var cacheKey = (controlPath, device); + if (_spriteCache != null && _spriteCache.TryGetValue(cacheKey, out var cachedSprite)) + { + return cachedSprite ?? placeholderSprite; + } + + var entry = FindEntryByControlPath(controlPath, device); + var sprite = entry?.Sprite ?? placeholderSprite; + + // Cache the result (including null results to avoid repeated lookups) + if (_spriteCache != null) + { + _spriteCache[cacheKey] = sprite; + } + + return sprite; + } + + public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device) + { + var t = GetTable(device); + if (t != null && t.entries != null) + { + for (int i = 0; i < t.entries.Count; ++i) { - for (int i = 0; i < t.entries.Count; ++i) + var e = t.entries[i]; + if (e == null) continue; + if (e.action == null) continue; + + var bindings = e.action.bindings; + int bindingCount = bindings.Count; + if (bindingCount <= 0) continue; + + for (int j = 0; j < bindingCount; j++) { - var e = t.entries[i]; - if (e == null) continue; - if (e.action == null) continue; - if (e.action.bindings.Count <= 0) continue; - foreach (var binding in e.action.bindings) + var b = bindings[j]; + if (!string.IsNullOrEmpty(b.path) && string.Equals(b.path, controlPath, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(controlPath, binding.path, StringComparison.OrdinalIgnoreCase)) - { - return e; - } + return e; + } + + if (!string.IsNullOrEmpty(b.effectivePath) && string.Equals(b.effectivePath, controlPath, StringComparison.OrdinalIgnoreCase)) + { + return e; } } } - return null; } + + return null; } } diff --git a/Client/Assets/InputGlyph/InputGlyphDatabaseEditor.cs b/Client/Assets/InputGlyph/InputGlyphDatabaseEditor.cs index 9baa8b0..ea71041 100644 --- a/Client/Assets/InputGlyph/InputGlyphDatabaseEditor.cs +++ b/Client/Assets/InputGlyph/InputGlyphDatabaseEditor.cs @@ -1,12 +1,7 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Reflection; -using AlicizaX.InputGlyph; using UnityEditor; using UnityEngine; -using UnityEngine.U2D; -using TMPro; [CustomEditor(typeof(InputGlyphDatabase))] public class InputGlyphDatabaseEditor : Editor @@ -22,6 +17,9 @@ public class InputGlyphDatabaseEditor : Editor List searchStrings = new List(); List currentPages = new List(); + // per-table temporary fields for adding single entry (only sprite now) + List newEntrySprites = new List(); + const int itemsPerPage = 10; const int previewSize = 52; @@ -97,9 +95,11 @@ public class InputGlyphDatabaseEditor : Editor var nameProp = t.FindPropertyRelative("deviceName"); if (nameProp != null && string.Equals(nameProp.stringValue, trimmed, StringComparison.OrdinalIgnoreCase)) { - exists = true; break; + exists = true; + break; } } + if (exists) { EditorUtility.DisplayDialog("Duplicate", "A table with that name already exists.", "OK"); @@ -111,10 +111,6 @@ public class InputGlyphDatabaseEditor : Editor var newTable = tablesProp.GetArrayElementAtIndex(newIndex); var nameProp = newTable.FindPropertyRelative("deviceName"); if (nameProp != null) nameProp.stringValue = trimmed; - var tmpAssetProp = newTable.FindPropertyRelative("tmpAsset"); - if (tmpAssetProp != null) tmpAssetProp.objectReferenceValue = null; - var atlasProp = newTable.FindPropertyRelative("spriteAtlas"); - if (atlasProp != null) atlasProp.objectReferenceValue = null; var sheetProp = newTable.FindPropertyRelative("spriteSheetTexture"); if (sheetProp != null) sheetProp.objectReferenceValue = null; var entriesProp = newTable.FindPropertyRelative("entries"); @@ -130,6 +126,7 @@ public class InputGlyphDatabaseEditor : Editor } } } + if (GUILayout.Button("Cancel", EditorStyles.toolbarButton, GUILayout.Width(80))) { showAddField = false; @@ -157,7 +154,7 @@ public class InputGlyphDatabaseEditor : Editor if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(22))) { if (EditorUtility.DisplayDialog("Delete Table?", - $"Delete table '{name}' and all its entries? This cannot be undone.", "Delete", "Cancel")) + $"Delete table '{name}' and all its entries? This cannot be undone.", "Delete", "Cancel")) { tablesProp.DeleteArrayElementAtIndex(i); serializedObject.ApplyModifiedProperties(); @@ -208,6 +205,11 @@ public class InputGlyphDatabaseEditor : Editor EnsureEditorListsLength(); + // compute deviceName & runtime index for this table (used when deleting single entry) + var nameProp = tableProp.FindPropertyRelative("deviceName"); + string deviceName = nameProp != null ? nameProp.stringValue : ""; + int runtimeTableIndex = MapSerializedTableToRuntimeIndex(deviceName); + GUILayout.BeginHorizontal(); GUIStyle searchStyle = EditorStyles.toolbarSearchField ?? EditorStyles.textField; searchStrings[tabIndex] = GUILayout.TextField(searchStrings[tabIndex] ?? "", searchStyle); @@ -215,59 +217,6 @@ public class InputGlyphDatabaseEditor : Editor EditorGUILayout.Space(6); - // TMP Asset row - var tmpAssetProp = tableProp.FindPropertyRelative("tmpAsset"); - EditorGUILayout.BeginHorizontal(); - GUILayout.Label("TMP Sprite Asset", GUILayout.Width(140)); - EditorGUILayout.PropertyField(tmpAssetProp, GUIContent.none, GUILayout.ExpandWidth(true)); - if (GUILayout.Button("Parse TMP Asset", GUILayout.Width(120))) - { - ParseTMPAssetIntoTableSerialized(tableProp); - } - if (GUILayout.Button("Clear", GUILayout.Width(80))) - { - var entriesProp = tableProp.FindPropertyRelative("entries"); - if (entriesProp != null) entriesProp.arraySize = 0; - var nameProp = tableProp.FindPropertyRelative("deviceName"); - if (nameProp != null && db != null) - { - var deviceName = nameProp.stringValue; - var table = db.GetTable(deviceName); - if (table != null) table.entries.Clear(); - } - serializedObject.ApplyModifiedProperties(); - EditorUtility.SetDirty(db); - currentPages[tabIndex] = 0; - } - EditorGUILayout.EndHorizontal(); - - // SpriteAtlas row - var atlasProp = tableProp.FindPropertyRelative("spriteAtlas"); - EditorGUILayout.BeginHorizontal(); - GUILayout.Label("Sprite Atlas", GUILayout.Width(140)); - EditorGUILayout.PropertyField(atlasProp, GUIContent.none, GUILayout.ExpandWidth(true)); - if (GUILayout.Button("Parse Sprite Atlas", GUILayout.Width(120))) - { - ParseSpriteAtlasIntoTableSerialized(tableProp); - } - if (GUILayout.Button("Clear", GUILayout.Width(80))) - { - var entriesProp = tableProp.FindPropertyRelative("entries"); - if (entriesProp != null) entriesProp.arraySize = 0; - var nameProp = tableProp.FindPropertyRelative("deviceName"); - if (nameProp != null && db != null) - { - var deviceName = nameProp.stringValue; - var table = db.GetTable(deviceName); - if (table != null) table.entries.Clear(); - } - serializedObject.ApplyModifiedProperties(); - EditorUtility.SetDirty(db); - currentPages[tabIndex] = 0; - } - EditorGUILayout.EndHorizontal(); - - // SpriteSheet (Texture2D with Multiple) row var sheetProp = tableProp.FindPropertyRelative("spriteSheetTexture"); EditorGUILayout.BeginHorizontal(); GUILayout.Label("Sprite Sheet (Texture2D)", GUILayout.Width(140)); @@ -276,23 +225,74 @@ public class InputGlyphDatabaseEditor : Editor { ParseSpriteSheetIntoTableSerialized(tableProp); } + + if (GUILayout.Button("Clear", GUILayout.Width(80))) { var entriesProp = tableProp.FindPropertyRelative("entries"); if (entriesProp != null) entriesProp.arraySize = 0; - var nameProp = tableProp.FindPropertyRelative("deviceName"); - if (nameProp != null && db != null) + if (runtimeTableIndex >= 0 && db != null) { - var deviceName = nameProp.stringValue; - var table = db.GetTable(deviceName); + var table = db.tables[runtimeTableIndex]; if (table != null) table.entries.Clear(); } + serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(db); currentPages[tabIndex] = 0; } + EditorGUILayout.EndHorizontal(); + var platformProp = tableProp.FindPropertyRelative("platformIcons"); + EditorGUILayout.PropertyField(platformProp, new GUIContent("Platforms Icons")); + Sprite placeholder = platformProp.objectReferenceValue as Sprite; + EditorGUILayout.Space(6); + EditorGUILayout.LabelField("Preview", EditorStyles.miniBoldLabel); + if (placeholder != null) + { + Texture2D preview = AssetPreview.GetAssetPreview(placeholder); + if (preview == null) preview = AssetPreview.GetMiniThumbnail(placeholder); + if (preview != null) GUILayout.Label(preview, GUILayout.Width(previewSize), GUILayout.Height(previewSize)); + else EditorGUILayout.ObjectField(placeholder, typeof(Sprite), false, GUILayout.Width(previewSize), GUILayout.Height(previewSize)); + } + else + { + EditorGUILayout.HelpBox("No PlatformIcons.", MessageType.Info); + } + + EditorGUILayout.Space(6); + + // ---- 新增:单个新增 Entry 的 UI(只支持 Sprite) ---- + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.LabelField("Add Single Entry", EditorStyles.boldLabel); + EditorGUILayout.BeginHorizontal(); + GUILayout.Label("Sprite", GUILayout.Width(50)); + newEntrySprites[tabIndex] = (Sprite)EditorGUILayout.ObjectField(newEntrySprites[tabIndex], typeof(Sprite), false, GUILayout.Width(80), GUILayout.Height(80)); + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(6); + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Add Entry", GUILayout.Width(110))) + { + if (newEntrySprites[tabIndex] == null) + { + EditorUtility.DisplayDialog("Missing Sprite", "Please assign a Sprite to add.", "OK"); + } + else + { + AddEntryToTableSerialized(tableProp, newEntrySprites[tabIndex]); + // reset temp field + newEntrySprites[tabIndex] = null; + currentPages[tabIndex] = 0; + } + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + // ---- end add-single-entry UI ---- + EditorGUILayout.Space(6); var entries = tableProp.FindPropertyRelative("entries"); @@ -319,15 +319,30 @@ public class InputGlyphDatabaseEditor : Editor currentPages[tabIndex] = Mathf.Clamp(currentPages[tabIndex], 0, totalPages - 1); EditorGUILayout.BeginHorizontal(); - if (GUILayout.Button("<<", EditorStyles.miniButtonLeft, GUILayout.Width(36))) { currentPages[tabIndex] = 0; } - if (GUILayout.Button("<", EditorStyles.miniButtonMid, GUILayout.Width(36))) { currentPages[tabIndex] = Mathf.Max(0, currentPages[tabIndex] - 1); } + if (GUILayout.Button("<<", EditorStyles.miniButtonLeft, GUILayout.Width(36))) + { + currentPages[tabIndex] = 0; + } + + if (GUILayout.Button("<", EditorStyles.miniButtonMid, GUILayout.Width(36))) + { + currentPages[tabIndex] = Mathf.Max(0, currentPages[tabIndex] - 1); + } GUILayout.FlexibleSpace(); EditorGUILayout.LabelField(string.Format("Page {0}/{1}", currentPages[tabIndex] + 1, totalPages), GUILayout.Width(120)); GUILayout.FlexibleSpace(); - if (GUILayout.Button(">", EditorStyles.miniButtonMid, GUILayout.Width(36))) { currentPages[tabIndex] = Mathf.Min(totalPages - 1, currentPages[tabIndex] + 1); } - if (GUILayout.Button(">>", EditorStyles.miniButtonRight, GUILayout.Width(36))) { currentPages[tabIndex] = totalPages - 1; } + if (GUILayout.Button(">", EditorStyles.miniButtonMid, GUILayout.Width(36))) + { + currentPages[tabIndex] = Mathf.Min(totalPages - 1, currentPages[tabIndex] + 1); + } + + if (GUILayout.Button(">>", EditorStyles.miniButtonRight, GUILayout.Width(36))) + { + currentPages[tabIndex] = totalPages - 1; + } + EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(4); @@ -341,8 +356,10 @@ public class InputGlyphDatabaseEditor : Editor var eProp = entries.GetArrayElementAtIndex(i); if (eProp == null) continue; + // display one entry with a small remove button on the right using (new EditorGUILayout.HorizontalScope("box")) { + // left: preview column using (new EditorGUILayout.VerticalScope(GUILayout.Width(80))) { var spriteProp = eProp.FindPropertyRelative("Sprite"); @@ -368,11 +385,51 @@ public class InputGlyphDatabaseEditor : Editor } } + // middle: action column EditorGUILayout.BeginVertical(); var actionProp = eProp.FindPropertyRelative("action"); EditorGUILayout.Space(2); EditorGUILayout.PropertyField(actionProp, GUIContent.none, GUILayout.ExpandWidth(true)); EditorGUILayout.EndVertical(); + + // right: small remove button + GUILayout.Space(6); + if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(24))) + { + string spriteName = null; + var sProp = eProp.FindPropertyRelative("Sprite"); + if (sProp != null) spriteName = (sProp.objectReferenceValue as Sprite)?.name; + + if (EditorUtility.DisplayDialog("Remove Entry?", + $"Remove entry '{(string.IsNullOrEmpty(spriteName) ? "" : spriteName)}' from table '{deviceName}'?", "Remove", "Cancel")) + { + // remove from serialized array + var entriesProp = tableProp.FindPropertyRelative("entries"); + if (entriesProp != null && i >= 0 && i < entriesProp.arraySize) + { + entriesProp.DeleteArrayElementAtIndex(i); + // apply then remove from runtime to keep both in sync + serializedObject.ApplyModifiedProperties(); + } + + // remove from runtime list (db.tables) + if (runtimeTableIndex >= 0 && db != null && db.tables != null && runtimeTableIndex < db.tables.Count) + { + var runtimeTable = db.tables[runtimeTableIndex]; + if (runtimeTable != null && i >= 0 && i < runtimeTable.entries.Count) + { + runtimeTable.entries.RemoveAt(i); + } + } + + EditorUtility.SetDirty(db); + AssetDatabase.SaveAssets(); + + // reset paging and return to avoid continuing to iterate mutated serialized array + currentPages[tabIndex] = 0; + return; + } + } } EditorGUILayout.Space(4); @@ -409,10 +466,6 @@ public class InputGlyphDatabaseEditor : Editor var newTable = tablesProp.GetArrayElementAtIndex(idx); var deviceNameProp = newTable.FindPropertyRelative("deviceName"); if (deviceNameProp != null) deviceNameProp.stringValue = name; - var tmpAssetProp = newTable.FindPropertyRelative("tmpAsset"); - if (tmpAssetProp != null) tmpAssetProp.objectReferenceValue = null; - var atlasProp = newTable.FindPropertyRelative("spriteAtlas"); - if (atlasProp != null) atlasProp.objectReferenceValue = null; var sheetProp = newTable.FindPropertyRelative("spriteSheetTexture"); if (sheetProp != null) sheetProp.objectReferenceValue = null; var entriesProp = newTable.FindPropertyRelative("entries"); @@ -427,12 +480,15 @@ public class InputGlyphDatabaseEditor : Editor int count = tablesProp != null ? tablesProp.arraySize : 0; if (searchStrings == null) searchStrings = new List(); if (currentPages == null) currentPages = new List(); + if (newEntrySprites == null) newEntrySprites = new List(); while (searchStrings.Count < count) searchStrings.Add(""); while (currentPages.Count < count) currentPages.Add(0); + while (newEntrySprites.Count < count) newEntrySprites.Add(null); while (searchStrings.Count > count) searchStrings.RemoveAt(searchStrings.Count - 1); while (currentPages.Count > count) currentPages.RemoveAt(currentPages.Count - 1); + while (newEntrySprites.Count > count) newEntrySprites.RemoveAt(newEntrySprites.Count - 1); } void EnsureEditorListsLength() @@ -441,247 +497,58 @@ public class InputGlyphDatabaseEditor : Editor SyncEditorListsWithTables(); } - // ----- Parse TMP SpriteAsset(增强版) ----- - void ParseTMPAssetIntoTableSerialized(SerializedProperty tableProp) + // ----- 新增:把单个 Sprite 加入到序列化表和 runtime 表 ----- + void AddEntryToTableSerialized(SerializedProperty tableProp, Sprite sprite) { if (tableProp == null) return; - var tmpAssetProp = tableProp.FindPropertyRelative("tmpAsset"); - var asset = tmpAssetProp.objectReferenceValue as TMP_SpriteAsset; - if (asset == null) + var entriesProp = tableProp.FindPropertyRelative("entries"); + if (entriesProp == null) return; + + int insertIndex = entriesProp.arraySize; + entriesProp.InsertArrayElementAtIndex(insertIndex); + var newE = entriesProp.GetArrayElementAtIndex(insertIndex); + if (newE != null) { - Debug.LogWarning("[InputGlyphDatabase] TMP Sprite Asset is null for table."); - return; - } + var spriteProp = newE.FindPropertyRelative("Sprite"); + var actionProp = newE.FindPropertyRelative("action"); - var nameProp = tableProp.FindPropertyRelative("deviceName"); - string deviceName = nameProp != null ? nameProp.stringValue : ""; + if (spriteProp != null) spriteProp.objectReferenceValue = sprite; - int tableIndex = MapSerializedTableToRuntimeIndex(deviceName); - if (tableIndex < 0) - { - Debug.LogError($"[InputGlyphDatabase] Could not map serialized table '{deviceName}' to runtime db.tables."); - return; - } - - var tableObj = db.tables[tableIndex]; - tableObj.entries.Clear(); - - var chars = asset.spriteCharacterTable; - SpriteAtlas atlas = GetSpriteAtlasFromTMP(asset); - string assetPath = AssetDatabase.GetAssetPath(asset); - string assetFolder = !string.IsNullOrEmpty(assetPath) ? Path.GetDirectoryName(assetPath) : null; - - int foundCount = 0; - for (int i = 0; i < chars.Count; ++i) - { - var ch = chars[i]; - if (ch == null) continue; - string name = ch.name; - if (string.IsNullOrEmpty(name)) name = $"glyph_{i}"; - - Sprite s = null; - - // 1) 尝试从 glyph / TMP_SpriteGlyph 中取 sprite - try - { - var glyph = ch.glyph as TMP_SpriteGlyph; - if (glyph != null) - { - var possible = typeof(TMP_SpriteGlyph).GetProperty("sprite", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (possible != null) - { - s = possible.GetValue(glyph, null) as Sprite; - } - else - { - var f = typeof(TMP_SpriteGlyph).GetField("sprite", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (f != null) s = f.GetValue(glyph) as Sprite; - } - } - } - catch { s = null; } - - // 2) atlas 查找 - if (s == null && atlas != null) - { - try { s = atlas.GetSprite(name); } catch { s = null; } - if (s == null) - { - try - { - var m = typeof(SpriteAtlas).GetMethod("GetSprite", new Type[] { typeof(string) }); - if (m != null) s = m.Invoke(atlas, new object[] { name }) as Sprite; - } - catch { s = null; } - } - } - - // 3) asset folder scope 查找 - if (s == null && !string.IsNullOrEmpty(assetFolder)) + // leave action serialized as-is (most projects can't serialize InputAction directly here) + if (actionProp != null) { try { - 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; - } - } - } - } - catch { s = null; } - } - - // 4) 全项目查找 - if (s == null) - { - try - { - 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; - } - } - } - } - catch { s = null; } - } - - // 5) LoadAllAssetsAtPath (TMP asset 本身) 作为最后手段 - if (s == null && !string.IsNullOrEmpty(assetPath)) - { - try - { - var allAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath); - if (allAssets != null) - { - foreach (var obj in allAssets) - { - if (obj is Sprite sp && sp.name == name) - { - s = sp; break; - } - } - } - } - catch { s = null; } - } - - GlyphEntry entry = new GlyphEntry(); - entry.Sprite = s; - entry.action = null; - tableObj.entries.Add(entry); - - if (s != null) foundCount++; - else Debug.LogWarning($"[InputGlyphDatabase] Failed to resolve sprite '{name}' for TMP asset '{asset.name}' (table '{deviceName}')."); - } - - // 按名字逐字符排序(不区分大小写) - tableObj.entries.Sort((a, b) => CompareSpriteNames(a?.Sprite?.name, b?.Sprite?.name)); - - EditorUtility.SetDirty(db); - serializedObject.Update(); - serializedObject.ApplyModifiedProperties(); - AssetDatabase.SaveAssets(); - - Debug.Log($"[InputGlyphDatabase] Parsed TMP '{asset.name}' into table '{deviceName}'. chars={chars.Count}, resolvedSprites={foundCount}"); - } - - // ----- Parse SpriteAtlas(Unity SpriteAtlas) ----- - void ParseSpriteAtlasIntoTableSerialized(SerializedProperty tableProp) - { - if (tableProp == null) return; - var atlasProp = tableProp.FindPropertyRelative("spriteAtlas"); - var atlas = atlasProp != null ? atlasProp.objectReferenceValue as SpriteAtlas : null; - if (atlas == null) - { - Debug.LogWarning("[InputGlyphDatabase] SpriteAtlas is null for table."); - return; - } - - var nameProp = tableProp.FindPropertyRelative("deviceName"); - string deviceName = nameProp != null ? nameProp.stringValue : ""; - - int tableIndex = MapSerializedTableToRuntimeIndex(deviceName); - if (tableIndex < 0) - { - Debug.LogError($"[InputGlyphDatabase] Could not map serialized table '{deviceName}' to runtime db.tables."); - return; - } - - var tableObj = db.tables[tableIndex]; - tableObj.entries.Clear(); - - string[] guids = AssetDatabase.FindAssets("t:Sprite"); - int added = 0; - try - { - for (int gi = 0; gi < guids.Length; ++gi) - { - var guid = guids[gi]; - var path = AssetDatabase.GUIDToAssetPath(guid); - var sp = AssetDatabase.LoadAssetAtPath(path); - if (sp == null) continue; - bool belongs = false; - try - { - var got = atlas.GetSprite(sp.name); - if (got != null) belongs = true; + actionProp.objectReferenceValue = null; } catch { - try - { - var m = typeof(SpriteAtlas).GetMethod("GetSprite", new Type[] { typeof(string) }); - if (m != null) - { - var got2 = m.Invoke(atlas, new object[] { sp.name }) as Sprite; - if (got2 != null) belongs = true; - } - } - catch { } - } - - if (belongs) - { - GlyphEntry e = new GlyphEntry(); - e.Sprite = sp; - e.action = null; - tableObj.entries.Add(e); - added++; } } } - catch (Exception ex) + + serializedObject.ApplyModifiedProperties(); + + // also add to runtime list + var nameProp = tableProp.FindPropertyRelative("deviceName"); + string deviceName = nameProp != null ? nameProp.stringValue : ""; + int tableIndex = MapSerializedTableToRuntimeIndex(deviceName); + if (tableIndex >= 0 && db != null && db.tables != null && tableIndex < db.tables.Count) { - Debug.LogError("[InputGlyphDatabase] Exception while scanning sprites for atlas: " + ex); + var tableObj = db.tables[tableIndex]; + GlyphEntry e = new GlyphEntry(); + e.Sprite = sprite; + e.action = null; // runtime only: none provided here + tableObj.entries.Add(e); } - // 按名字逐字符排序(不区分大小写) - tableObj.entries.Sort((a, b) => CompareSpriteNames(a?.Sprite?.name, b?.Sprite?.name)); - EditorUtility.SetDirty(db); - serializedObject.Update(); - serializedObject.ApplyModifiedProperties(); AssetDatabase.SaveAssets(); - - Debug.Log($"[InputGlyphDatabase] Parsed SpriteAtlas '{atlas.name}' into table '{deviceName}'. foundSprites={added}"); } - // ----- Parse Sprite Sheet (Texture2D with Multiple) ----- + +// ----- Parse Sprite Sheet (Texture2D with Multiple) ----- +// 只用名称匹配覆盖 Sprite,不改变已有 action void ParseSpriteSheetIntoTableSerialized(SerializedProperty tableProp) { if (tableProp == null) return; @@ -705,7 +572,11 @@ public class InputGlyphDatabaseEditor : Editor } var tableObj = db.tables[tableIndex]; - tableObj.entries.Clear(); + if (tableObj == null) + { + Debug.LogError($"[InputGlyphDatabase] Runtime table object is null for '{deviceName}'."); + return; + } string path = AssetDatabase.GetAssetPath(tex); if (string.IsNullOrEmpty(path)) @@ -721,35 +592,107 @@ public class InputGlyphDatabaseEditor : Editor return; } + // 收集 sprites(按照文件内顺序;你如果想按名字排序可以在这里加) List sprites = new List(); foreach (var a in assets) { - if (a is Sprite sp) + if (a is Sprite sp) sprites.Add(sp); + } + + var entriesProp = tableProp.FindPropertyRelative("entries"); + if (entriesProp == null) + { + Debug.LogWarning("[InputGlyphDatabase] entries property not found on table."); + return; + } + + // 构建序列化表名 -> 索引 映射(忽略大小写) + var serializedNameToIndex = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < entriesProp.arraySize; ++i) + { + var eProp = entriesProp.GetArrayElementAtIndex(i); + if (eProp == null) continue; + var sProp = eProp.FindPropertyRelative("Sprite"); + var sRef = sProp != null ? sProp.objectReferenceValue as Sprite : null; + if (sRef != null && !serializedNameToIndex.ContainsKey(sRef.name)) { - sprites.Add(sp); + serializedNameToIndex[sRef.name] = i; } } - // 之前按视觉位置排序,改为先按名字逐字符排序(不区分大小写) - sprites.Sort((a, b) => CompareSpriteNames(a?.name, b?.name)); + // runtime 名称 -> 索引 映射 + var runtimeNameToIndex = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < tableObj.entries.Count; ++i) + { + var re = tableObj.entries[i]; + if (re != null && re.Sprite != null) + { + var rn = re.Sprite.name; + if (!runtimeNameToIndex.ContainsKey(rn)) + runtimeNameToIndex[rn] = i; + } + } + + int replaced = 0, added = 0; foreach (var sp in sprites) { - GlyphEntry e = new GlyphEntry(); - e.Sprite = sp; - e.action = null; - tableObj.entries.Add(e); + if (sp == null) continue; + string nm = sp.name; + + // -- 序列化层:同名则替换 Sprite 引用(不触碰 action),否则新增元素并把 action 设为 null -- + if (serializedNameToIndex.TryGetValue(nm, out int sIndex)) + { + var eProp = entriesProp.GetArrayElementAtIndex(sIndex); + if (eProp != null) + { + var spriteProp = eProp.FindPropertyRelative("Sprite"); + if (spriteProp != null) spriteProp.objectReferenceValue = sp; + // 不修改 actionProp,保持原有 action(如果有的话) + } + + replaced++; + } + else + { + int insertIndex = entriesProp.arraySize; + entriesProp.InsertArrayElementAtIndex(insertIndex); + var newE = entriesProp.GetArrayElementAtIndex(insertIndex); + if (newE != null) + { + var spriteProp = newE.FindPropertyRelative("Sprite"); + var actionProp = newE.FindPropertyRelative("action"); + if (spriteProp != null) spriteProp.objectReferenceValue = sp; + if (actionProp != null) actionProp.objectReferenceValue = null; // 新增项 action 为空 + } + + serializedNameToIndex[nm] = insertIndex; + added++; + } + + // -- 运行时层:同名则替换 Sprite,否则新增 runtime entry(action 设 null,保持之前 runtime entry 的 action 不变) -- + if (runtimeNameToIndex.TryGetValue(nm, out int rIndex)) + { + var runtimeEntry = tableObj.entries[rIndex]; + if (runtimeEntry != null) runtimeEntry.Sprite = sp; + } + else + { + GlyphEntry ge = new GlyphEntry(); + ge.Sprite = sp; + ge.action = null; + tableObj.entries.Add(ge); + runtimeNameToIndex[nm] = tableObj.entries.Count - 1; + } } - // 额外在 runtime list 里也用相同排序(上面已经排序过 sprites) - tableObj.entries.Sort((a, b) => CompareSpriteNames(a?.Sprite?.name, b?.Sprite?.name)); - + // 应用并保存修改(序列化层与 runtime 层保持同步) EditorUtility.SetDirty(db); serializedObject.Update(); serializedObject.ApplyModifiedProperties(); AssetDatabase.SaveAssets(); - Debug.Log($"[InputGlyphDatabase] Parsed sprite sheet '{tex.name}' into table '{deviceName}'. foundSprites={sprites.Count}"); + Debug.Log($"[InputGlyphDatabase] Merged sprite sheet '{tex.name}' into table '{deviceName}'. spritesFound={sprites.Count}, replaced={replaced}, added={added}, totalEntries={tableObj.entries.Count}"); } int MapSerializedTableToRuntimeIndex(string deviceName) @@ -760,56 +703,7 @@ public class InputGlyphDatabaseEditor : Editor if (string.Equals(db.tables[ti].deviceName, deviceName, StringComparison.OrdinalIgnoreCase)) return ti; } + return -1; } - - 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; - } - - int CompareSpriteNames(string a, string b) - { - // normalize null/empty - bool aEmpty = string.IsNullOrEmpty(a); - bool bEmpty = string.IsNullOrEmpty(b); - if (aEmpty && bEmpty) return 0; - if (aEmpty) return -1; - if (bEmpty) return 1; - - int la = a.Length; - int lb = b.Length; - int n = Math.Min(la, lb); - for (int i = 0; i < n; ++i) - { - char ca = char.ToUpperInvariant(a[i]); - char cb = char.ToUpperInvariant(b[i]); - if (ca != cb) return ca - cb; - } - return la - lb; - } } diff --git a/Client/Assets/InputGlyph/InputGlyphImage.cs b/Client/Assets/InputGlyph/InputGlyphImage.cs index fbf38da..d237fe6 100644 --- a/Client/Assets/InputGlyph/InputGlyphImage.cs +++ b/Client/Assets/InputGlyph/InputGlyphImage.cs @@ -1,20 +1,22 @@ using UnityEngine; using UnityEngine.UI; using UnityEngine.InputSystem; -using AlicizaX.InputGlyph; -[RequireComponent(typeof(Image))] -public class InputGlyphImage : MonoBehaviour +public sealed class InputGlyphImage : MonoBehaviour { [SerializeField] private InputActionReference actionReference; [SerializeField] private Image targetImage; [SerializeField] private bool hideIfMissing = false; [SerializeField] private GameObject hideTargetObject; + private InputDeviceWatcher.InputDeviceCategory _cachedCategory; + private Sprite _cachedSprite; + void OnEnable() { if (targetImage == null) targetImage = GetComponent(); InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; + _cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard; UpdatePrompt(); } @@ -25,18 +27,34 @@ public class InputGlyphImage : MonoBehaviour void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) { - UpdatePrompt(); + if (_cachedCategory != cat) + { + _cachedCategory = cat; + UpdatePrompt(); + } } void UpdatePrompt() { if (actionReference == null || actionReference.action == null || targetImage == null) return; - InputDeviceWatcher.InputDeviceCategory deviceCategory = InputDeviceWatcher.CurrentCategory; - if (GlyphService.TryGetUISpriteForActionPath(actionReference, deviceCategory, out Sprite sprite)) + + // Use cached category instead of re-querying CurrentCategory + if (GlyphService.TryGetUISpriteForActionPath(actionReference, "", _cachedCategory, out Sprite sprite)) { - targetImage.sprite = sprite; + if (_cachedSprite != sprite) + { + _cachedSprite = sprite; + targetImage.sprite = sprite; + } } - if (hideTargetObject) hideTargetObject.SetActive(sprite != null && !hideIfMissing); + if (hideTargetObject != null) + { + bool shouldBeActive = sprite != null && !hideIfMissing; + if (hideTargetObject.activeSelf != shouldBeActive) + { + hideTargetObject.SetActive(shouldBeActive); + } + } } } diff --git a/Client/Assets/InputGlyph/InputGlyphText.cs b/Client/Assets/InputGlyph/InputGlyphText.cs index 672192d..078088f 100644 --- a/Client/Assets/InputGlyph/InputGlyphText.cs +++ b/Client/Assets/InputGlyph/InputGlyphText.cs @@ -1,22 +1,25 @@ using System; using System.Linq; using AlicizaX; -using AlicizaX.InputGlyph; using UnityEngine; using TMPro; using UnityEngine.InputSystem; [RequireComponent(typeof(TextMeshProUGUI))] -public class InputGlyphText : MonoBehaviour +public sealed class InputGlyphText : MonoBehaviour { [SerializeField] private InputActionReference actionReference; private TMP_Text textField; private string _oldText; + private InputDeviceWatcher.InputDeviceCategory _cachedCategory; + private string _cachedFormattedText; + void OnEnable() { if (textField == null) textField = GetComponent(); InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; - _oldText=textField.text; + _oldText = textField.text; + _cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard; UpdatePrompt(); } @@ -25,22 +28,36 @@ public class InputGlyphText : MonoBehaviour InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; } - void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) { - UpdatePrompt(); + if (_cachedCategory != cat) + { + _cachedCategory = cat; + UpdatePrompt(); + } } void UpdatePrompt() { if (actionReference == null || actionReference.action == null || textField == null) return; - var device = InputDeviceWatcher.CurrentCategory; - if (GlyphService.TryGetTMPTagForActionPath(actionReference, device, out string tag, out string displayFallback)) + + // Use cached category instead of re-querying CurrentCategory + if (GlyphService.TryGetTMPTagForActionPath(actionReference, "", _cachedCategory, out string tag, out string displayFallback)) { - textField.text = Utility.Text.Format(_oldText, tag); + string formattedText = Utility.Text.Format(_oldText, tag); + if (_cachedFormattedText != formattedText) + { + _cachedFormattedText = formattedText; + textField.text = formattedText; + } return; } - textField.text = Utility.Text.Format(_oldText, displayFallback);; + string fallbackText = Utility.Text.Format(_oldText, displayFallback); + if (_cachedFormattedText != fallbackText) + { + _cachedFormattedText = fallbackText; + textField.text = fallbackText; + } } } diff --git a/Client/Assets/InputGlyph/InputGlyphUXButton.cs b/Client/Assets/InputGlyph/InputGlyphUXButton.cs new file mode 100644 index 0000000..9aa1b9c --- /dev/null +++ b/Client/Assets/InputGlyph/InputGlyphUXButton.cs @@ -0,0 +1,63 @@ +using System; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.InputSystem; + +[RequireComponent(typeof(UXButton))] +public sealed class InputGlyphUXButton : MonoBehaviour +{ + [SerializeField] private UXButton button; + [SerializeField] private Image targetImage; + private InputActionReference _actionReference; + private InputDeviceWatcher.InputDeviceCategory _cachedCategory; + private Sprite _cachedSprite; + +#if UNITY_EDITOR + private void OnValidate() + { + if (button == null) + { + button = GetComponent(); + } + } +#endif + + void OnEnable() + { + if (button == null) button = GetComponent(); + if (targetImage == null) targetImage = GetComponent(); + _actionReference = button.HotKeyRefrence; + InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; + _cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard; + UpdatePrompt(); + } + + void OnDisable() + { + InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; + } + + void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) + { + if (_cachedCategory != cat) + { + _cachedCategory = cat; + UpdatePrompt(); + } + } + + void UpdatePrompt() + { + if (_actionReference == null || _actionReference.action == null || targetImage == null) return; + + // Use cached category instead of re-querying CurrentCategory + if (GlyphService.TryGetUISpriteForActionPath(_actionReference, "", _cachedCategory, out Sprite sprite)) + { + if (_cachedSprite != sprite) + { + _cachedSprite = sprite; + targetImage.sprite = sprite; + } + } + } +} diff --git a/Client/Assets/InputGlyph/InputGlyphUXButton.cs.meta b/Client/Assets/InputGlyph/InputGlyphUXButton.cs.meta new file mode 100644 index 0000000..9b528e2 --- /dev/null +++ b/Client/Assets/InputGlyph/InputGlyphUXButton.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f24ff96430d042109462f373aed1f2cc +timeCreated: 1765870646 \ No newline at end of file diff --git a/Client/Assets/InputGlyph/TestRebindScript.cs b/Client/Assets/InputGlyph/TestRebindScript.cs index 0c24f91..f6b059d 100644 --- a/Client/Assets/InputGlyph/TestRebindScript.cs +++ b/Client/Assets/InputGlyph/TestRebindScript.cs @@ -1,16 +1,10 @@ -// TestRebindScript.cs - using System; using System.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.InputSystem; -using InputRemapper; using UnityEngine.UI; -/// -/// 测试用不需要繁琐处理 -/// public class TestRebindScript : MonoBehaviour { [Header("UI")] public UXButton btn; @@ -29,6 +23,7 @@ public class TestRebindScript : MonoBehaviour private IDisposable prepareSub; private IDisposable applySub; private IDisposable rebindEndSub; + private IDisposable conflictSub; private void Start() { @@ -38,6 +33,7 @@ public class TestRebindScript : MonoBehaviour if (InputBindingManager.Instance != null) { + // Subscribe to prepare events - already filtered by IsTargetContext prepareSub = InputBindingManager.Instance.OnRebindPrepare.Subscribe(ctx => { if (IsTargetContext(ctx)) @@ -48,8 +44,44 @@ public class TestRebindScript : MonoBehaviour } }); - applySub = InputBindingManager.Instance.OnApply.Subscribe(_ => UpdateBindingText()); - rebindEndSub = InputBindingManager.Instance.OnRebindEnd.Subscribe(_ => UpdateBindingText()); + // Subscribe to apply events - only update if this instance's binding was applied or discarded + applySub = InputBindingManager.Instance.OnApply.Subscribe(evt => + { + var (success, appliedContexts) = evt; + if (appliedContexts != null) + { + // Only update if any of the applied/discarded contexts match this instance + foreach (var ctx in appliedContexts) + { + if (IsTargetContext(ctx)) + { + UpdateBindingText(); + break; + } + } + } + }); + + // Subscribe to rebind end events - only update if this instance's binding ended + rebindEndSub = InputBindingManager.Instance.OnRebindEnd.Subscribe(evt => + { + var (success, context) = evt; + if (IsTargetContext(context)) + { + UpdateBindingText(); + } + }); + + // Subscribe to conflict events - update if this instance is involved in conflict + conflictSub = InputBindingManager.Instance.OnRebindConflict.Subscribe(evt => + { + var (prepared, conflict) = evt; + // Update if either the prepared or conflict context matches this instance + if (IsTargetContext(prepared) || IsTargetContext(conflict)) + { + UpdateBindingText(); + } + }); } } @@ -60,6 +92,7 @@ public class TestRebindScript : MonoBehaviour prepareSub?.Dispose(); applySub?.Dispose(); rebindEndSub?.Dispose(); + conflictSub?.Dispose(); } private void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory _) @@ -73,12 +106,30 @@ public class TestRebindScript : MonoBehaviour } - private bool IsTargetContext(InputRemapper.InputBindingManager.RebindContext ctx) + private bool IsTargetContext(InputBindingManager.RebindContext ctx) { if (ctx == null || ctx.action == null) return false; var action = GetAction(); if (action == null) return false; - return ctx.action == action; + + // Must match the action + if (ctx.action != action) return false; + + // If we have a composite part specified, we need to match the binding index + if (!string.IsNullOrEmpty(compositePartName)) + { + // Get the binding at the context's index + if (ctx.bindingIndex < 0 || ctx.bindingIndex >= action.bindings.Count) + return false; + + var binding = action.bindings[ctx.bindingIndex]; + + // Check if the binding's name matches our composite part + return string.Equals(binding.name, compositePartName, StringComparison.OrdinalIgnoreCase); + } + + // If no composite part specified, just matching the action is enough + return true; } private void OnBtnClicked() @@ -98,7 +149,6 @@ public class TestRebindScript : MonoBehaviour try { var task = InputBindingManager.ConfirmApply(); - if (task == null) return false; return await task; } catch (Exception ex) @@ -111,7 +161,7 @@ public class TestRebindScript : MonoBehaviour public void CancelPrepared() { InputBindingManager.DiscardPrepared(); - UpdateBindingText(); + // UpdateBindingText will be called automatically via OnApply event } private void UpdateBindingText() @@ -125,15 +175,15 @@ public class TestRebindScript : MonoBehaviour } - string disp = GlyphService.GetBindingControlPath(action, InputDeviceWatcher.CurrentCategory); - bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action); + bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action, compositePartName); try { var deviceCat = InputDeviceWatcher.CurrentCategory; - string controlPath = GlyphService.GetBindingControlPath(action, deviceCat); - if (!string.IsNullOrEmpty(controlPath) && GlyphService.TryGetUISpriteForActionPath(controlPath, deviceCat, out Sprite sprite)) + InputActionReference refr=default; + // string controlPath = GlyphService.GetBindingControlPath(action, compositePartName, deviceCat); + if ( GlyphService.TryGetUISpriteForActionPath(action,compositePartName, deviceCat, out Sprite sprite)) { if (targetImage != null) targetImage.sprite = sprite; } diff --git a/Client/Assets/Test/GameLogic.dll.bytes b/Client/Assets/Test/GameLogic.dll.bytes index c640215..edef161 100644 Binary files a/Client/Assets/Test/GameLogic.dll.bytes and b/Client/Assets/Test/GameLogic.dll.bytes differ diff --git a/Client/Assets/Test/GameLogic.pdb.bytes b/Client/Assets/Test/GameLogic.pdb.bytes index 0aa63f4..40b6014 100644 Binary files a/Client/Assets/Test/GameLogic.pdb.bytes and b/Client/Assets/Test/GameLogic.pdb.bytes differ diff --git a/Client/Assets/TestAudioPlay.cs b/Client/Assets/TestAudioPlay.cs index 7f4618b..8a98ba1 100644 --- a/Client/Assets/TestAudioPlay.cs +++ b/Client/Assets/TestAudioPlay.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using AlicizaX.InputGlyph; -using AlicizaX.UI; -using AlicizaX.UI.Extension; + using UnityEngine; using UnityEngine.UI; diff --git a/Client/Packages/com.alicizax.unity.entry b/Client/Packages/com.alicizax.unity.entry index 91cd9d2..90291aa 160000 --- a/Client/Packages/com.alicizax.unity.entry +++ b/Client/Packages/com.alicizax.unity.entry @@ -1 +1 @@ -Subproject commit 91cd9d2981e046ce0b03e3bbbd6c6a0294cd1150 +Subproject commit 90291aa4a969a022e7a02d855748d55af03828b0 diff --git a/Client/Packages/com.alicizax.unity.framework b/Client/Packages/com.alicizax.unity.framework index 0a5ef91..7431888 160000 --- a/Client/Packages/com.alicizax.unity.framework +++ b/Client/Packages/com.alicizax.unity.framework @@ -1 +1 @@ -Subproject commit 0a5ef9135ccb240227cf655e2251a63bf2d5da7a +Subproject commit 74318889a2a1f3d93d1dbd7e35b610fce575043e diff --git a/Client/Packages/com.alicizax.unity.ui.extension b/Client/Packages/com.alicizax.unity.ui.extension index f92e919..b3f3f26 160000 --- a/Client/Packages/com.alicizax.unity.ui.extension +++ b/Client/Packages/com.alicizax.unity.ui.extension @@ -1 +1 @@ -Subproject commit f92e91920dc4c31b41e36e3fa40058d4e6aace34 +Subproject commit b3f3f268bf27686916f963b219ffda4e8392b914 diff --git a/Client/UserSettings/EditorUserSettings.asset b/Client/UserSettings/EditorUserSettings.asset index 51d7654..fc153a9 100644 --- a/Client/UserSettings/EditorUserSettings.asset +++ b/Client/UserSettings/EditorUserSettings.asset @@ -30,14 +30,14 @@ EditorUserSettings: value: 56060350000d5b5a5908597a48255a44174e4d797a7d7e6475794f61e7b3643e flags: 0 RecentlyUsedSceneGuid-7: - value: 50500404540c580d0f0b5e7543725b44424f4c7a7b7c7734747e4f36e4b1676d - flags: 0 - RecentlyUsedSceneGuid-8: value: 015450045700505d0f0a5f2313260a444e164b2e757b76652c2d4d32bab0313a flags: 0 - RecentlyUsedSceneGuid-9: + RecentlyUsedSceneGuid-8: value: 5a07065703500c59585e0e7748770d44444f4a737d2d7f35787d4f63e0b26668 flags: 0 + RecentlyUsedSceneGuid-9: + value: 50500404540c580d0f0b5e7543725b44424f4c7a7b7c7734747e4f36e4b1676d + flags: 0 vcSharedLogLevel: value: 0d5e400f0650 flags: 0 diff --git a/Client/UserSettings/Layouts/default-2022.dwlt b/Client/UserSettings/Layouts/default-2022.dwlt index 5bd092c..9f4637c 100644 --- a/Client/UserSettings/Layouts/default-2022.dwlt +++ b/Client/UserSettings/Layouts/default-2022.dwlt @@ -19,7 +19,7 @@ MonoBehaviour: width: 1920 height: 997 m_ShowMode: 4 - m_Title: Project + m_Title: Console m_RootView: {fileID: 4} m_MinSize: {x: 875, y: 300} m_MaxSize: {x: 10000, y: 10000} @@ -41,7 +41,7 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 696 - width: 421 + width: 266 height: 251 m_MinSize: {x: 51, y: 71} m_MaxSize: {x: 4001, y: 4021} @@ -70,7 +70,7 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 0 - width: 421 + width: 266 height: 947 m_MinSize: {x: 100, y: 100} m_MaxSize: {x: 8096, y: 16192} @@ -174,7 +174,7 @@ MonoBehaviour: m_MinSize: {x: 400, y: 100} m_MaxSize: {x: 32384, y: 16192} vertical: 0 - controlID: 69 + controlID: 24 draggingID: 0 --- !u!114 &8 MonoBehaviour: @@ -193,7 +193,7 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 0 - width: 421 + width: 266 height: 696 m_MinSize: {x: 201, y: 221} m_MaxSize: {x: 4001, y: 4021} @@ -219,9 +219,9 @@ MonoBehaviour: - {fileID: 11} m_Position: serializedVersion: 2 - x: 421 + x: 266 y: 0 - width: 284 + width: 388 height: 947 m_MinSize: {x: 100, y: 100} m_MaxSize: {x: 8096, y: 16192} @@ -245,8 +245,8 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 0 - width: 284 - height: 249 + width: 388 + height: 409 m_MinSize: {x: 202, y: 221} m_MaxSize: {x: 4002, y: 4021} m_ActualView: {fileID: 17} @@ -270,9 +270,9 @@ MonoBehaviour: m_Position: serializedVersion: 2 x: 0 - y: 249 - width: 284 - height: 698 + y: 409 + width: 388 + height: 538 m_MinSize: {x: 102, y: 121} m_MaxSize: {x: 4002, y: 4021} m_ActualView: {fileID: 18} @@ -295,9 +295,9 @@ MonoBehaviour: m_Children: [] m_Position: serializedVersion: 2 - x: 705 + x: 654 y: 0 - width: 431 + width: 479 height: 947 m_MinSize: {x: 232, y: 271} m_MaxSize: {x: 10002, y: 10021} @@ -321,9 +321,9 @@ MonoBehaviour: m_Children: [] m_Position: serializedVersion: 2 - x: 1136 + x: 1133 y: 0 - width: 784 + width: 787 height: 947 m_MinSize: {x: 276, y: 71} m_MaxSize: {x: 4001, y: 4021} @@ -354,7 +354,7 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 769 - width: 420 + width: 265 height: 230 m_SerializedDataModeController: m_DataMode: 0 @@ -372,7 +372,7 @@ MonoBehaviour: m_ShowGizmos: 0 m_TargetDisplay: 0 m_ClearColor: {r: 0, g: 0, b: 0, a: 0} - m_TargetSize: {x: 420, y: 209} + m_TargetSize: {x: 265, y: 209} m_TextureFilterMode: 0 m_TextureHideFlags: 61 m_RenderIMGUI: 1 @@ -387,8 +387,8 @@ MonoBehaviour: m_VRangeLocked: 0 hZoomLockedByDefault: 0 vZoomLockedByDefault: 0 - m_HBaseRangeMin: -210 - m_HBaseRangeMax: 210 + m_HBaseRangeMin: -132.5 + m_HBaseRangeMax: 132.5 m_VBaseRangeMin: -104.5 m_VBaseRangeMax: 104.5 m_HAllowExceedBaseRangeMin: 1 @@ -408,23 +408,23 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 21 - width: 420 + width: 265 height: 209 m_Scale: {x: 1, y: 1} - m_Translation: {x: 210, y: 104.5} + m_Translation: {x: 132.5, y: 104.5} m_MarginLeft: 0 m_MarginRight: 0 m_MarginTop: 0 m_MarginBottom: 0 m_LastShownAreaInsideMargins: serializedVersion: 2 - x: -210 + x: -132.5 y: -104.5 - width: 420 + width: 265 height: 209 m_MinimalGUI: 1 m_defaultScale: 1 - m_LastWindowPixelSize: {x: 420, y: 230} + m_LastWindowPixelSize: {x: 265, y: 230} m_ClearInEditMode: 1 m_NoCameraWarning: 1 m_LowResolutionForAspectRatios: 01000000000000000000 @@ -522,7 +522,7 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 73 - width: 420 + width: 265 height: 675 m_SerializedDataModeController: m_DataMode: 0 @@ -1163,10 +1163,10 @@ MonoBehaviour: m_Tooltip: m_Pos: serializedVersion: 2 - x: 421 + x: 266 y: 73 - width: 282 - height: 228 + width: 386 + height: 388 m_SerializedDataModeController: m_DataMode: 0 m_PreferredDataMode: 0 @@ -1180,9 +1180,9 @@ MonoBehaviour: m_SceneHierarchy: m_TreeViewState: scrollPos: {x: 0, y: 0} - m_SelectedIDs: 626f0000 + m_SelectedIDs: 2e1c0000 m_LastClickedID: 0 - m_ExpandedIDs: eefafffff6fafffff8faffffd66c0000 + m_ExpandedIDs: 28fbffff m_RenameOverlay: m_UserAcceptedRename: 0 m_Name: @@ -1226,10 +1226,10 @@ MonoBehaviour: m_Tooltip: m_Pos: serializedVersion: 2 - x: 421 - y: 322 - width: 282 - height: 677 + x: 266 + y: 482 + width: 386 + height: 517 m_SerializedDataModeController: m_DataMode: 0 m_PreferredDataMode: 0 @@ -1260,9 +1260,9 @@ MonoBehaviour: m_Tooltip: m_Pos: serializedVersion: 2 - x: 705 + x: 654 y: 73 - width: 429 + width: 477 height: 926 m_SerializedDataModeController: m_DataMode: 0 @@ -1285,7 +1285,7 @@ MonoBehaviour: m_SkipHidden: 0 m_SearchArea: 2 m_Folders: - - Packages/com.alicizax.unity.ui.extension/Editor/Res/ComponentIcon + - Assets/Plugins/PrimeTween/Demo/Scripts/MeasureAllocations m_Globs: [] m_OriginalText: m_ImportLogFlags: 0 @@ -1301,7 +1301,7 @@ MonoBehaviour: scrollPos: {x: 0, y: 0} m_SelectedIDs: e48c0000 m_LastClickedID: 36068 - m_ExpandedIDs: 00000000226f0000246f0000266f0000286f00002a6f00002c6f00002e6f0000306f0000326f0000346f0000366f0000386f00003a6f00003c6f00003e6f0000406f0000426f0000446f0000466f0000486f00004a6f00004c6f00004e6f0000506f0000526f0000546f0000566f0000586f00005a6f00005c6f00005e6f0000606f0000626f0000646f0000666f0000686f00006a6f0000 + m_ExpandedIDs: 00000000860d0000746d0000766d0000786d00007a6d00007c6d00007e6d0000806d0000826d0000846d0000866d0000886d00008a6d00008c6d00008e6d0000906d0000926d0000946d0000966d0000986d00009a6d00009c6d00009e6d0000a06d0000a26d0000a46d0000a66d0000a86d0000aa6d0000ac6d0000ae6d0000b06d0000b26d0000b46d0000b66d0000b86d0000ba6d0000bc6d0000 m_RenameOverlay: m_UserAcceptedRename: 0 m_Name: @@ -1326,10 +1326,10 @@ MonoBehaviour: m_Icon: {fileID: 0} m_ResourceFile: m_AssetTreeState: - scrollPos: {x: 0, y: 720} + scrollPos: {x: 0, y: 528} m_SelectedIDs: m_LastClickedID: 0 - m_ExpandedIDs: ffffffff00000000226f0000246f0000266f0000286f00002a6f00002e6f0000306f0000326f0000346f0000366f0000386f00003a6f00003c6f00003e6f0000406f0000426f0000446f0000466f0000486f00004a6f00004e6f0000506f0000526f0000546f0000566f0000586f00005a6f00005c6f00005e6f0000606f0000626f0000646f0000666f0000686f00006a6f0000ffffff7f + m_ExpandedIDs: ffffffff00000000860d0000746d0000766d0000786d00007a6d00007c6d00007e6d0000806d0000826d0000846d0000866d0000886d00008a6d00008c6d00008e6d0000906d0000926d0000946d0000966d0000986d00009a6d00009c6d00009e6d0000a06d0000a26d0000a46d0000a66d0000a86d0000aa6d0000ac6d0000ae6d0000b06d0000b26d0000b46d0000b66d0000b86d0000ba6d0000bc6d0000c46f0000f47000001c710000 m_RenameOverlay: m_UserAcceptedRename: 0 m_Name: @@ -1405,9 +1405,9 @@ MonoBehaviour: m_Tooltip: m_Pos: serializedVersion: 2 - x: 1136 + x: 1133 y: 73 - width: 783 + width: 786 height: 926 m_SerializedDataModeController: m_DataMode: 0 @@ -1426,7 +1426,7 @@ MonoBehaviour: m_ControlHash: 1412526313 m_PrefName: Preview_InspectorPreview m_LastInspectedObjectInstanceID: -1 - m_LastVerticalScrollValue: 432 + m_LastVerticalScrollValue: 0 m_GlobalObjectId: m_InspectorMode: 0 m_LockTracker: