using System; using System.Collections.Generic; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UIElements; using BaseGames.Editor; using BaseGames.UI; namespace BaseGames.Editor.Input { /// /// 按键图标管理工作台(Input Icon Studio)。 /// /// 布局: /// 工具栏 (InputReaderSO 选取 + 刷新按钮) /// 设备标签栏 (键鼠 / Xbox / PlayStation / Switch) /// TwoPaneSplitView: /// 左列:Action 列表(带覆盖指示点 + 当前绑定路径预览) /// 右列: /// ① 当前 Action × 当前设备的绑定路径 + Sprite 字段 /// ② 48px 大图预览 /// ③ 所有设备快览行(4 行) /// ④ 模拟 InteractPromptWidget 外观预览 /// /// 菜单:BaseGames / Input Icon Studio (priority=55) /// public class InputIconStudioWindow : EditorWindow { private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss"; private const string PrefDevice = "InputIconStudio.Device"; private const string PrefAction = "InputIconStudio.Action"; private const float ListWidth = 280f; // ── 静态开口 ────────────────────────────────────────────────────────── [MenuItem("BaseGames/Input Icon Studio", priority = 55)] public static void Open() { var wnd = GetWindow(); wnd.titleContent = new GUIContent("Input Icon Studio", EditorGUIUtility.IconContent("d_PreTextureRGB").image); wnd.minSize = new Vector2(780, 480); } // ── 数据 ────────────────────────────────────────────────────────────── private ScriptableObject _inputReaderAsset; private InputActionAsset _actionAsset; // 四套图标集 SO(每个设备一套,可为 null) private InputDeviceIconSetSO _kbSet, _xboxSet, _psSet, _switchSet; // 当前激活设备标签 private InputDeviceType _activeDevice = InputDeviceType.KeyboardMouse; // 当前选中的 Action 名称 private string _selectedAction; // 缓存所有 actions(仅 Gameplay map) private readonly List _actionNames = new(); // ── UI 节点引用 ─────────────────────────────────────────────────────── private VisualElement _listContainer; private VisualElement _detailPanel; private VisualElement _deviceTabBar; // ── 生命周期 ────────────────────────────────────────────────────────── public void CreateGUI() { var uss = AssetDatabase.LoadAssetAtPath(UssPath); if (uss != null) rootVisualElement.styleSheets.Add(uss); rootVisualElement.style.flexDirection = FlexDirection.Column; rootVisualElement.style.flexGrow = 1; // 恢复上次选中状态 _activeDevice = (InputDeviceType)EditorPrefs.GetInt(PrefDevice, 0); _selectedAction = EditorPrefs.GetString(PrefAction, null); BuildToolbar(); BuildDeviceTabBar(); BuildMainSplit(); LoadAssetsFromProject(); } private void OnDisable() { EditorPrefs.SetInt(PrefDevice, (int)_activeDevice); EditorPrefs.SetString(PrefAction, _selectedAction ?? string.Empty); } // ── 工具栏 ──────────────────────────────────────────────────────────── private void BuildToolbar() { var toolbar = new VisualElement(); toolbar.AddToClassList("editor-toolbar"); // 标题图标 var icon = EditorGUIUtility.IconContent("d_PreTextureRGB"); if (icon.image != null) { var iconImg = new Image { image = icon.image }; iconImg.style.width = 18; iconImg.style.height = 18; iconImg.style.marginRight = 6; toolbar.Add(iconImg); } var title = new Label("INPUT ICON STUDIO"); title.style.unityFontStyleAndWeight = FontStyle.Bold; title.style.fontSize = 12; title.style.opacity = 0.85f; title.style.marginRight = 12; toolbar.Add(title); // InputReaderSO 选取器 var readerLabel = new Label("InputReaderSO:"); readerLabel.style.opacity = 0.65f; readerLabel.style.marginRight = 4; readerLabel.style.alignSelf = Align.Center; toolbar.Add(readerLabel); var readerField = new ObjectField { objectType = typeof(ScriptableObject), allowSceneObjects = false, value = _inputReaderAsset }; readerField.style.width = 200; readerField.style.flexShrink = 0; readerField.RegisterValueChangedCallback(evt => { _inputReaderAsset = evt.newValue as ScriptableObject; ReloadActionAsset(); RebuildActionList(); RebuildDetail(); }); toolbar.Add(readerField); // 弹性填充 var spacer = new VisualElement(); spacer.style.flexGrow = 1; toolbar.Add(spacer); // 刷新按钮 var btnRefresh = new Button(RefreshAll) { text = "⟳ 刷新" }; btnRefresh.tooltip = "重新扫描项目资产"; toolbar.Add(btnRefresh); // 帮助按钮 var btnHelp = new Button(ShowHelp) { text = "?" }; btnHelp.style.width = 24; btnHelp.tooltip = "使用说明"; toolbar.Add(btnHelp); rootVisualElement.Add(toolbar); } // ── 设备标签栏 ──────────────────────────────────────────────────────── private void BuildDeviceTabBar() { _deviceTabBar = new VisualElement(); _deviceTabBar.AddToClassList("tab-bar"); _deviceTabBar.style.paddingLeft = 8; _deviceTabBar.style.paddingRight = 8; rootVisualElement.Add(_deviceTabBar); RebuildDeviceTabs(); } private void RebuildDeviceTabs() { _deviceTabBar.Clear(); var devices = new[] { (InputDeviceType.KeyboardMouse, "🖱 键鼠", "(W/A/S/D + Mouse)"), (InputDeviceType.XboxController, "🎮 Xbox", "Xbox Controller"), (InputDeviceType.PlayStationController,"🎮 PlayStation","DualShock / DualSense"), (InputDeviceType.SwitchController, "🕹 Switch", "Pro Controller / Joy-Con"), }; foreach (var (devType, label, hint) in devices) { var iconSet = GetIconSetForDevice(devType); int total = GetEntryCount(iconSet); int filled = GetFilledCount(iconSet); // 覆盖率圆点颜色 var dot = total > 0 && filled == total ? "●" : (filled == 0 ? "○" : "◑"); var dotColor = total > 0 && filled == total ? new Color(0.3f, 0.85f, 0.45f) : filled == 0 ? new Color(0.8f, 0.3f, 0.3f) : new Color(0.9f, 0.75f, 0.2f); var btn = new Button(() => SelectDevice(devType)); btn.tooltip = hint; btn.style.flexDirection = FlexDirection.Row; btn.style.alignItems = Align.Center; btn.AddToClassList("tab-button"); var dotLbl = new Label(dot); dotLbl.style.color = new StyleColor(dotColor); dotLbl.style.fontSize = 10; dotLbl.style.marginRight = 4; btn.Add(dotLbl); var nameLbl = new Label(label); btn.Add(nameLbl); if (total > 0) { var countLbl = new Label($" {filled}/{total}"); countLbl.style.fontSize = 10; countLbl.style.opacity = 0.55f; btn.Add(countLbl); } if (devType == _activeDevice) btn.AddToClassList("tab-button--active"); _deviceTabBar.Add(btn); } } private void SelectDevice(InputDeviceType device) { _activeDevice = device; RebuildDeviceTabs(); RebuildActionList(); RebuildDetail(); } // ── 主分屏 ──────────────────────────────────────────────────────────── private void BuildMainSplit() { var split = new TwoPaneSplitView(0, ListWidth, TwoPaneSplitViewOrientation.Horizontal); split.style.flexGrow = 1; rootVisualElement.Add(split); // ── 左:Action 列表 ──────────────────────────────────────────── var leftPane = new VisualElement(); leftPane.style.flexDirection = FlexDirection.Column; split.Add(leftPane); // 左列标题 var leftHeader = new VisualElement(); leftHeader.AddToClassList("pane-header"); leftHeader.style.flexDirection = FlexDirection.Row; leftHeader.style.alignItems = Align.Center; var leftTitle = new Label("Actions"); leftTitle.style.flexGrow = 1; leftHeader.Add(leftTitle); var btnSetAsset = new Button(PickIconSetForCurrentDevice) { text = "📂 指定图标集", tooltip = "为当前设备绑定一个 InputDeviceIconSetSO 资产" }; btnSetAsset.style.fontSize = 10; btnSetAsset.style.height = 20; leftHeader.Add(btnSetAsset); leftPane.Add(leftHeader); // 左列滚动区 var leftScroll = new ScrollView(ScrollViewMode.Vertical); leftScroll.style.flexGrow = 1; leftPane.Add(leftScroll); _listContainer = new VisualElement(); _listContainer.style.paddingTop = 4; _listContainer.style.paddingBottom = 4; leftScroll.Add(_listContainer); // ── 右:详情区 ───────────────────────────────────────────────── var rightScroll = new ScrollView(ScrollViewMode.Vertical); rightScroll.style.flexGrow = 1; split.Add(rightScroll); _detailPanel = new VisualElement(); _detailPanel.style.flexDirection = FlexDirection.Column; _detailPanel.style.paddingBottom = 16; rightScroll.Add(_detailPanel); } // ── Action 列表 ─────────────────────────────────────────────────────── private void RebuildActionList() { if (_listContainer == null) return; _listContainer.Clear(); if (_actionNames.Count == 0) { var empty = new Label("未加载 Action Asset\n请在上方指定 InputReaderSO"); empty.style.opacity = 0.5f; empty.style.marginTop = 20; empty.style.whiteSpace = WhiteSpace.Normal; empty.style.unityTextAlign = TextAnchor.MiddleCenter; _listContainer.Add(empty); return; } var iconSet = GetIconSetForDevice(_activeDevice); foreach (var actionName in _actionNames) { var effectivePath = GetEffectivePath(actionName); var sprite = iconSet?.GetIcon(effectivePath ?? string.Empty); bool hasIcon = sprite != null; var row = new Button(() => { _selectedAction = actionName; RebuildActionListSelection(); RebuildDetail(); }); row.style.flexDirection = FlexDirection.Row; row.style.alignItems = Align.Center; row.style.paddingLeft = 10; row.style.paddingRight = 8; row.style.paddingTop = 6; row.style.paddingBottom = 6; row.style.borderRadius(0); row.style.borderWidth(0); row.style.marginBottom = 1; row.style.backgroundColor = _selectedAction == actionName ? new StyleColor(new Color(0.35f, 0.55f, 0.85f, 0.22f)) : new StyleColor(Color.clear); // 覆盖指示点 var dot = new Label(hasIcon ? "●" : "○"); dot.style.color = new StyleColor(hasIcon ? new Color(0.3f, 0.85f, 0.4f) : new Color(0.75f, 0.3f, 0.3f)); dot.style.fontSize = 10; dot.style.width = 14; dot.style.flexShrink = 0; row.Add(dot); // Action 名称 var nameLbl = new Label(actionName); nameLbl.style.flexGrow = 1; nameLbl.style.fontSize = 11; row.Add(nameLbl); // 缩略绑定路径 if (!string.IsNullOrEmpty(effectivePath)) { var pathLbl = new Label(ShortPath(effectivePath)); pathLbl.style.fontSize = 10; pathLbl.style.opacity = 0.5f; pathLbl.style.marginLeft = 4; row.Add(pathLbl); } // 微型图标预览 if (sprite != null) { var thumb = new Image { sprite = sprite }; thumb.style.width = 18; thumb.style.height = 18; thumb.style.marginLeft = 4; thumb.style.flexShrink = 0; row.Add(thumb); } row.name = "action-row-" + actionName; _listContainer.Add(row); } } private void RebuildActionListSelection() { if (_listContainer == null) return; foreach (var child in _listContainer.Children()) { if (child is not Button btn) continue; var isSelected = btn.name == "action-row-" + _selectedAction; btn.style.backgroundColor = isSelected ? new StyleColor(new Color(0.35f, 0.55f, 0.85f, 0.22f)) : new StyleColor(Color.clear); } } // ── 详情面板 ────────────────────────────────────────────────────────── private void RebuildDetail() { if (_detailPanel == null) return; _detailPanel.Clear(); if (string.IsNullOrEmpty(_selectedAction)) { var placeholder = new Label("← 从左侧选择一个 Action"); placeholder.style.opacity = 0.4f; placeholder.style.marginTop = 60; placeholder.style.unityTextAlign = TextAnchor.MiddleCenter; _detailPanel.Add(placeholder); return; } // ① 标题栏 BuildDetailHeader(); // ② 当前设备 Mapping 编辑区 BuildCurrentDeviceMapping(); // ③ 所有设备快览 BuildAllDevicesOverview(); // ④ 交互提示模拟预览 BuildPromptPreview(); // ⑤ 图标集资产状态 BuildIconSetStatus(); } private void BuildDetailHeader() { var header = new VisualElement(); header.style.flexDirection = FlexDirection.Row; header.style.alignItems = Align.Center; header.style.paddingLeft = 12; header.style.paddingRight = 12; header.style.paddingTop = 10; header.style.paddingBottom = 10; header.style.borderBottomWidth = 1; header.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.25f)); var actionLbl = new Label(_selectedAction); actionLbl.style.fontSize = 15; actionLbl.style.unityFontStyleAndWeight = FontStyle.Bold; actionLbl.style.flexGrow = 1; header.Add(actionLbl); // 当前设备徽章 var devBadge = MakeDeviceBadge(_activeDevice); header.Add(devBadge); _detailPanel.Add(header); } private void BuildCurrentDeviceMapping() { var section = MakeSectionContainer("绑定路径与图标"); var iconSet = GetIconSetForDevice(_activeDevice); var effectivePath = GetEffectivePath(_selectedAction); // ── 绑定路径预览 ────────────────────────────────────────────── var pathRow = new VisualElement(); pathRow.style.flexDirection = FlexDirection.Row; pathRow.style.alignItems = Align.Center; pathRow.style.marginBottom = 8; var pathLabel = new Label("绑定路径"); pathLabel.style.opacity = 0.65f; pathLabel.style.width = 80; pathLabel.style.flexShrink = 0; pathRow.Add(pathLabel); var pathValue = new Label(effectivePath ?? "(未检测到绑定)"); pathValue.style.fontSize = 11; pathValue.style.flexGrow = 1; pathValue.style.opacity = string.IsNullOrEmpty(effectivePath) ? 0.4f : 1f; pathValue.style.unityFontStyleAndWeight = FontStyle.Bold; pathRow.Add(pathValue); if (!string.IsNullOrEmpty(effectivePath)) { var copyBtn = new Button(() => { GUIUtility.systemCopyBuffer = effectivePath; }) { text = "复制", tooltip = "复制路径到剪贴板" }; copyBtn.style.height = 20; copyBtn.style.fontSize = 10; pathRow.Add(copyBtn); } section.Add(pathRow); // ── 图标集 SO 字段 + 大图预览 ───────────────────────────────── if (iconSet == null) { var warn = new HelpBox($"当前设备({DeviceLabel(_activeDevice)})尚未配置图标集 SO。\n" + "请在工具栏 InputIconService 的 Inspector 中指定对应图标集,或点击左列「指定图标集」。", HelpBoxMessageType.Warning); warn.style.marginTop = 4; section.Add(warn); } else { var iconEditRow = new VisualElement(); iconEditRow.style.flexDirection = FlexDirection.Row; iconEditRow.style.alignItems = Align.Center; // 图标 ObjectField var existingSprite = string.IsNullOrEmpty(effectivePath) ? null : iconSet.GetIcon(effectivePath); var spriteField = new ObjectField("图标 Sprite") { objectType = typeof(Sprite), allowSceneObjects = false, value = existingSprite }; spriteField.style.flexGrow = 1; // 大图预览 var bigPreview = new Image(); bigPreview.style.width = 64; bigPreview.style.height = 64; bigPreview.style.marginLeft = 12; bigPreview.style.flexShrink = 0; bigPreview.style.borderRadius(4); bigPreview.style.backgroundColor = new StyleColor(new Color(0f, 0f, 0f, 0.25f)); bigPreview.scaleMode = ScaleMode.ScaleToFit; if (existingSprite != null) bigPreview.sprite = existingSprite; spriteField.RegisterValueChangedCallback(evt => { if (string.IsNullOrEmpty(effectivePath)) return; SaveIconToSet(iconSet, effectivePath, evt.newValue as Sprite); bigPreview.sprite = evt.newValue as Sprite; RebuildDeviceTabs(); RebuildActionListSelection(); }); iconEditRow.Add(spriteField); iconEditRow.Add(bigPreview); section.Add(iconEditRow); // 提示:若路径为空,说明该 Action 在此设备方案下无绑定 if (string.IsNullOrEmpty(effectivePath)) { var note = new HelpBox("该 Action 在此控制方案下没有绑定。如有需要,请在 InputActionAsset 中添加对应的 Binding。", HelpBoxMessageType.Info); note.style.marginTop = 4; section.Add(note); } } _detailPanel.Add(section); } private void BuildAllDevicesOverview() { var section = MakeSectionContainer("所有设备快览"); var devices = new[] { InputDeviceType.KeyboardMouse, InputDeviceType.XboxController, InputDeviceType.PlayStationController, InputDeviceType.SwitchController, }; foreach (var devType in devices) { var iconSet = GetIconSetForDevice(devType); var path = GetEffectivePath(_selectedAction); var sprite = (iconSet != null && path != null) ? iconSet.GetIcon(path) : null; var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.alignItems = Align.Center; row.style.paddingTop = 4; row.style.paddingBottom = 4; row.style.paddingLeft = 6; row.style.paddingRight = 6; row.style.borderBottomWidth = 1; row.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.12f)); // 设备徽章 var badge = MakeDeviceBadge(devType); badge.style.width = 110; badge.style.flexShrink = 0; row.Add(badge); // 绑定路径 var pathLbl = new Label(path != null ? ShortPath(path) : "—"); pathLbl.style.flexGrow = 1; pathLbl.style.fontSize = 10; pathLbl.style.opacity = path == null ? 0.4f : 1f; row.Add(pathLbl); // 状态点 var statusLbl = new Label(sprite != null ? "✓" : (iconSet == null ? "—" : "✗")); statusLbl.style.color = new StyleColor( sprite != null ? new Color(0.3f, 0.85f, 0.4f) : iconSet == null ? new Color(0.6f, 0.6f, 0.6f) : new Color(0.85f, 0.3f, 0.3f)); statusLbl.style.width = 20; statusLbl.style.unityTextAlign = TextAnchor.MiddleCenter; row.Add(statusLbl); // 小图预览 var thumb = new Image(); thumb.style.width = 28; thumb.style.height = 28; thumb.style.marginLeft = 6; thumb.style.flexShrink = 0; thumb.style.borderRadius(3); thumb.style.backgroundColor = new StyleColor(new Color(0f, 0f, 0f, 0.2f)); thumb.scaleMode = ScaleMode.ScaleToFit; if (sprite != null) thumb.sprite = sprite; row.Add(thumb); section.Add(row); } _detailPanel.Add(section); } private void BuildPromptPreview() { var section = MakeSectionContainer("交互提示预览(模拟)"); var iconSet = GetIconSetForDevice(_activeDevice); var effectivePath = GetEffectivePath(_selectedAction); var sprite = (iconSet != null && effectivePath != null) ? iconSet.GetIcon(effectivePath) : null; // 模拟 InteractPromptWidget 的外观 var mockup = new VisualElement(); mockup.style.flexDirection = FlexDirection.Row; mockup.style.alignItems = Align.Center; mockup.style.alignSelf = Align.FlexStart; mockup.style.paddingTop = 8; mockup.style.paddingBottom = 8; mockup.style.paddingLeft = 12; mockup.style.paddingRight = 14; mockup.style.marginTop = 4; mockup.style.marginLeft = 8; mockup.style.borderRadius(6); mockup.style.backgroundColor = new StyleColor(new Color(0.08f, 0.08f, 0.10f, 0.85f)); mockup.style.borderWidth(1); mockup.style.borderColor(new Color(0.5f, 0.5f, 0.6f, 0.35f)); // 按键图标区域 var keyBox = new VisualElement(); keyBox.style.width = 32; keyBox.style.height = 32; keyBox.style.borderRadius(4); keyBox.style.backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.25f, 0.9f)); keyBox.style.borderWidth(1); keyBox.style.borderColor(new Color(0.6f, 0.6f, 0.7f, 0.5f)); keyBox.style.justifyContent = Justify.Center; keyBox.style.alignItems = Align.Center; keyBox.style.marginRight = 10; keyBox.style.flexShrink = 0; if (sprite != null) { var keyImg = new Image { sprite = sprite }; keyImg.style.width = 24; keyImg.style.height = 24; keyImg.scaleMode = ScaleMode.ScaleToFit; keyBox.Add(keyImg); } else { var missingLbl = new Label("?"); missingLbl.style.opacity = 0.4f; missingLbl.style.unityTextAlign = TextAnchor.MiddleCenter; keyBox.Add(missingLbl); } mockup.Add(keyBox); // 文字区域 var textCol = new VisualElement(); textCol.style.flexDirection = FlexDirection.Column; var actionText = new Label("交互"); // 模拟 LabelText actionText.style.fontSize = 11; actionText.style.unityFontStyleAndWeight = FontStyle.Bold; actionText.style.color = new StyleColor(Color.white); textCol.Add(actionText); if (!string.IsNullOrEmpty(effectivePath)) { var pathTip = new Label(ShortPath(effectivePath)); pathTip.style.fontSize = 9; pathTip.style.opacity = 0.5f; pathTip.style.color = new StyleColor(Color.white); textCol.Add(pathTip); } mockup.Add(textCol); section.Add(mockup); if (sprite == null) { var note = new Label(iconSet == null ? "ⓘ 当前设备未配置图标集,无法预览。" : "ⓘ 该路径在图标集中未找到对应 Sprite,运行时图标区域将隐藏。"); note.style.fontSize = 10; note.style.opacity = 0.55f; note.style.marginTop = 4; note.style.whiteSpace = WhiteSpace.Normal; section.Add(note); } _detailPanel.Add(section); } private void BuildIconSetStatus() { var section = MakeSectionContainer("图标集资产状态"); var rows = new[] { (InputDeviceType.KeyboardMouse, "键鼠", _kbSet), (InputDeviceType.XboxController, "Xbox", _xboxSet), (InputDeviceType.PlayStationController,"PlayStation", _psSet), (InputDeviceType.SwitchController, "Switch", _switchSet), }; foreach (var (devType, label, set) in rows) { var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; row.style.alignItems = Align.Center; row.style.marginBottom = 3; var badge = MakeDeviceBadge(devType); badge.style.width = 100; badge.style.flexShrink = 0; row.Add(badge); if (set != null) { int total = GetEntryCount(set); int filled = GetFilledCount(set); var statusChip = new Label($"{filled}/{total} 图标"); statusChip.AddToClassList(filled == total && total > 0 ? "status-chip--ok" : "status-chip--missing"); statusChip.style.fontSize = 10; statusChip.style.marginLeft = 8; row.Add(statusChip); var pingBtn = new Button(() => { EditorGUIUtility.PingObject(set); Selection.activeObject = set; }) { text = "定位", tooltip = $"在 Project 窗口中高亮 {set.name}" }; pingBtn.style.height = 18; pingBtn.style.fontSize = 10; pingBtn.style.marginLeft = 6; row.Add(pingBtn); } else { var missingLbl = new Label("⚠ 未配置"); missingLbl.style.opacity = 0.6f; missingLbl.style.fontSize = 10; missingLbl.style.marginLeft = 8; row.Add(missingLbl); var createBtn = new Button(() => CreateIconSetForDevice(devType)) { text = "+ 新建", tooltip = $"创建 {label} 图标集 SO 并放入项目" }; createBtn.style.height = 18; createBtn.style.fontSize = 10; createBtn.style.marginLeft = 6; createBtn.AddToClassList("wizard-factory-btn"); row.Add(createBtn); } section.Add(row); } _detailPanel.Add(section); } // ── 加载项目资产 ────────────────────────────────────────────────────── private void LoadAssetsFromProject() { // 找 InputReaderSO if (_inputReaderAsset == null) { var guids = AssetDatabase.FindAssets("t:InputReaderSO"); if (guids.Length > 0) _inputReaderAsset = AssetDatabase.LoadAssetAtPath( AssetDatabase.GUIDToAssetPath(guids[0])); } ReloadActionAsset(); LoadIconSets(); RebuildDeviceTabs(); RebuildActionList(); RebuildDetail(); } private void ReloadActionAsset() { _actionAsset = null; _actionNames.Clear(); if (_inputReaderAsset == null) return; var so = new SerializedObject(_inputReaderAsset); var prop = so.FindProperty("_inputActions"); if (prop?.objectReferenceValue is InputActionAsset asset) { _actionAsset = asset; var map = asset.FindActionMap("Gameplay", throwIfNotFound: false); if (map != null) foreach (var action in map.actions) _actionNames.Add(action.name); } } private void LoadIconSets() { var sets = EditorScaffoldUtils.FindAllAssetsOfType(); _kbSet = _xboxSet = _psSet = _switchSet = null; // 按 DeviceType 字段匹配图标集 SO foreach (var set in sets) { switch (set.DeviceType) { case InputDeviceType.KeyboardMouse: _kbSet ??= set; break; case InputDeviceType.XboxController: _xboxSet ??= set; break; case InputDeviceType.PlayStationController:_psSet ??= set; break; case InputDeviceType.SwitchController: _switchSet??= set; break; } } } private void RefreshAll() { LoadAssetsFromProject(); } // ── 每设备绑定路径查询 ──────────────────────────────────────────────── private string GetEffectivePath(string actionName) { if (_actionAsset == null || string.IsNullOrEmpty(actionName)) return null; var action = _actionAsset.FindAction(actionName, throwIfNotFound: false); if (action == null) return null; string scheme = _activeDevice == InputDeviceType.KeyboardMouse ? "Keyboard&Mouse" : "Gamepad"; foreach (var binding in action.bindings) { if (binding.isComposite) continue; if (!string.IsNullOrEmpty(scheme) && !string.IsNullOrEmpty(binding.groups) && !binding.groups.Contains(scheme, StringComparison.OrdinalIgnoreCase)) continue; if (!string.IsNullOrEmpty(binding.effectivePath)) return binding.effectivePath; } return null; } // ── 保存图标到 SO ───────────────────────────────────────────────────── private static void SaveIconToSet(InputDeviceIconSetSO iconSet, string path, Sprite sprite) { var so = new SerializedObject(iconSet); var entries = so.FindProperty("_entries"); // 查找已有相同路径的条目 int found = -1; for (int i = 0; i < entries.arraySize; i++) { var p = entries.GetArrayElementAtIndex(i).FindPropertyRelative("BindingPath").stringValue; if (string.Equals(p, path, StringComparison.OrdinalIgnoreCase)) { found = i; break; } } if (found < 0) { // 新增 entries.InsertArrayElementAtIndex(entries.arraySize); found = entries.arraySize - 1; entries.GetArrayElementAtIndex(found).FindPropertyRelative("BindingPath").stringValue = path; } entries.GetArrayElementAtIndex(found).FindPropertyRelative("Icon").objectReferenceValue = sprite; so.ApplyModifiedProperties(); EditorUtility.SetDirty(iconSet); AssetDatabase.SaveAssets(); } // ── 工具方法 ────────────────────────────────────────────────────────── private InputDeviceIconSetSO GetIconSetForDevice(InputDeviceType device) => device switch { InputDeviceType.KeyboardMouse => _kbSet, InputDeviceType.XboxController => _xboxSet, InputDeviceType.PlayStationController => _psSet, InputDeviceType.SwitchController => _switchSet, _ => null, }; private static int GetEntryCount(InputDeviceIconSetSO set) { if (set == null) return 0; var so = new SerializedObject(set); var arr = so.FindProperty("_entries"); return arr?.arraySize ?? 0; } private static int GetFilledCount(InputDeviceIconSetSO set) { if (set == null) return 0; var so = new SerializedObject(set); var arr = so.FindProperty("_entries"); if (arr == null) return 0; int count = 0; for (int i = 0; i < arr.arraySize; i++) if (arr.GetArrayElementAtIndex(i).FindPropertyRelative("Icon").objectReferenceValue != null) count++; return count; } private void PickIconSetForCurrentDevice() { // 打开 Project 的 ObjectPicker 让用户选一个 InputDeviceIconSetSO // 简单实现:扫描项目中所有 InputDeviceIconSetSO,弹出通用 GenericMenu var all = EditorScaffoldUtils.FindAllAssetsOfType(); if (all.Count == 0) { EditorUtility.DisplayDialog("无图标集资产", "项目中未找到 InputDeviceIconSetSO 资产,请先创建。", "确定"); return; } var menu = new GenericMenu(); foreach (var set in all) { var captured = set; menu.AddItem(new GUIContent(set.name), false, () => { AssignIconSetToDevice(_activeDevice, captured); LoadIconSets(); RebuildDeviceTabs(); RebuildActionList(); RebuildDetail(); }); } menu.AddSeparator(""); menu.AddItem(new GUIContent("新建..."), false, () => CreateIconSetForDevice(_activeDevice)); menu.ShowAsContext(); } private void AssignIconSetToDevice(InputDeviceType device, InputDeviceIconSetSO set) { switch (device) { case InputDeviceType.KeyboardMouse: _kbSet = set; break; case InputDeviceType.XboxController: _xboxSet = set; break; case InputDeviceType.PlayStationController: _psSet = set; break; case InputDeviceType.SwitchController: _switchSet = set; break; } } private void CreateIconSetForDevice(InputDeviceType device) { string defaultName = $"ICN_{device}"; var asset = EditorScaffoldUtils.CreateSOAssetInteractive( "Assets/_Game/Data/UI/InputIcons", defaultName); if (asset != null) { var so = new SerializedObject(asset); var prop = so.FindProperty("_deviceType"); if (prop != null) { prop.enumValueIndex = (int)device; so.ApplyModifiedProperties(); } AssignIconSetToDevice(device, asset); LoadIconSets(); RebuildDeviceTabs(); RebuildActionList(); RebuildDetail(); } } // ── UI 构建辅助 ─────────────────────────────────────────────────────── private static VisualElement MakeSectionContainer(string title) { var section = new VisualElement(); section.style.paddingLeft = 12; section.style.paddingRight = 12; section.style.paddingTop = 10; section.style.paddingBottom = 10; section.style.borderBottomWidth = 1; section.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.18f)); var hdr = new Label(title); hdr.AddToClassList("section-header"); hdr.style.marginBottom = 6; section.Add(hdr); return section; } private static VisualElement MakeDeviceBadge(InputDeviceType device) { var (label, bg) = device switch { InputDeviceType.KeyboardMouse => ("键鼠", new Color(0.2f, 0.4f, 0.7f, 0.5f)), InputDeviceType.XboxController => ("Xbox", new Color(0.1f, 0.5f, 0.15f, 0.5f)), InputDeviceType.PlayStationController => ("PS", new Color(0.1f, 0.25f, 0.65f, 0.5f)), InputDeviceType.SwitchController => ("Switch", new Color(0.75f, 0.15f, 0.2f, 0.5f)), _ => ("Unknown", new Color(0.4f, 0.4f, 0.4f, 0.4f)), }; var badge = new Label(label); badge.style.fontSize = 10; badge.style.paddingTop = 2; badge.style.paddingBottom = 2; badge.style.paddingLeft = 7; badge.style.paddingRight = 7; badge.style.borderRadius(4); badge.style.backgroundColor = new StyleColor(bg); badge.style.borderWidth(1); badge.style.borderColor(new Color(bg.r + 0.15f, bg.g + 0.15f, bg.b + 0.15f, 0.6f)); badge.style.unityFontStyleAndWeight = FontStyle.Bold; return badge; } private static string DeviceLabel(InputDeviceType device) => device switch { InputDeviceType.KeyboardMouse => "键鼠", InputDeviceType.XboxController => "Xbox", InputDeviceType.PlayStationController => "PlayStation", InputDeviceType.SwitchController => "Switch", _ => device.ToString(), }; /// 将完整绑定路径缩短为可读格式(去掉设备前缀)。 private static string ShortPath(string path) { if (string.IsNullOrEmpty(path)) return path; var slash = path.LastIndexOf('/'); return slash >= 0 ? path[(slash + 1)..] : path; } private static void ShowHelp() { EditorUtility.DisplayDialog("Input Icon Studio — 使用说明", "1. 在顶部选取 InputReaderSO 资产(项目中有唯一实例则自动加载)\n" + "2. 通过设备标签页切换键鼠/Xbox/PS/Switch\n" + "3. 左侧列表显示所有 Gameplay Actions:\n" + " ● 绿色 = 当前设备已配置图标\n" + " ○ 红色 = 未配置\n" + "4. 选中 Action 后在右侧指定 Sprite\n" + "5. 修改实时保存到对应的 InputDeviceIconSetSO\n\n" + "💡 在 Inspector 中可对 InputDeviceIconSetSO 使用「从 Action Asset 填充路径」\n" + " 批量生成所有路径条目,再逐一指定 Sprite。", "知道了"); } } // ── 样式扩展方法(限本文件使用)───────────────────────────────────────── internal static class VisualElementStyleExtensions { internal static void borderRadius(this IStyle style, float r) { style.borderTopLeftRadius = r; style.borderTopRightRadius = r; style.borderBottomLeftRadius = r; style.borderBottomRightRadius = r; } internal static void borderWidth(this IStyle style, float w) { style.borderLeftWidth = w; style.borderRightWidth = w; style.borderTopWidth = w; style.borderBottomWidth = w; } internal static void borderColor(this IStyle style, Color c) { var sc = new StyleColor(c); style.borderLeftColor = sc; style.borderRightColor = sc; style.borderTopColor = sc; style.borderBottomColor = sc; } internal static void borderRadius(this Button btn, float r) => btn.style.borderRadius(r); internal static void borderWidth(this Button btn, float w) => btn.style.borderWidth(w); } }