Files
zeling_v2/Assets/_Game/Scripts/Editor/Input/InputIconStudioWindow.cs
Joywayer e879efaa89 Add InputDeviceIconSetSO configuration guide and related documentation
- Created a new markdown file detailing the configuration of InputDeviceIconSetSO.
- Included sections on system architecture, field explanations, image specifications, and complete workflow from setup to runtime.
- Documented the automatic device recognition logic and provided troubleshooting for common issues.
- Added references to relevant files and scripts for easier navigation.
2026-05-23 00:10:23 +08:00

1096 lines
45 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 按键图标管理工作台Input Icon Studio
///
/// 布局:
/// 工具栏 (InputReaderSO 选取 + 刷新按钮)
/// 设备标签栏 (键鼠 / Xbox / PlayStation / Switch)
/// TwoPaneSplitView:
/// 左列Action 列表(带覆盖指示点 + 当前绑定路径预览)
/// 右列:
/// ① 当前 Action × 当前设备的绑定路径 + Sprite 字段
/// ② 48px 大图预览
/// ③ 所有设备快览行4 行)
/// ④ 模拟 InteractPromptWidget 外观预览
///
/// 菜单BaseGames / Input Icon Studio (priority=55)
/// </summary>
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<InputIconStudioWindow>();
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<string> _actionNames = new();
// ── UI 节点引用 ───────────────────────────────────────────────────────
private VisualElement _listContainer;
private VisualElement _detailPanel;
private VisualElement _deviceTabBar;
// ── 生命周期 ──────────────────────────────────────────────────────────
public void CreateGUI()
{
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(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<ScriptableObject>(
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<InputDeviceIconSetSO>();
_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<InputDeviceIconSetSO>();
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<InputDeviceIconSetSO>(
"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(),
};
/// <summary>将完整绑定路径缩短为可读格式(去掉设备前缀)。</summary>
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);
}
}