Passion & UI
This commit is contained in:
8
Assets/Scripts/SLSUtilities/UI/Editor.meta
Normal file
8
Assets/Scripts/SLSUtilities/UI/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b894c601fdace7f41965eb41b68165d7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,416 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using TMPro;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TextCore;
|
||||
|
||||
namespace Cielonos.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 一键式 Editor 工具:将 Assets/Sprites/Icon/InputIcons/ 下的所有按键图标
|
||||
/// 打包为 atlas 纹理,并自动生成 TMP_SpriteAsset 供 TMP 富文本使用。
|
||||
/// 使用方式:Tools → Cielonos → Create InputGlyphs Sprite Asset
|
||||
/// </summary>
|
||||
public static class InputGlyphsSpriteAssetCreator
|
||||
{
|
||||
private const string KeyboardFolder = "Assets/Sprites/Icon/InputIcons/keyboard";
|
||||
private const string MouseFolder = "Assets/Sprites/Icon/InputIcons/mouse";
|
||||
private const string AtlasOutputFolder = "Assets/Scripts/SLSUtilities/UI/SpriteAssets";
|
||||
private const string AssetOutputFolder = "Assets/TextMesh Pro/Resources/Sprite Assets";
|
||||
private const string AtlasFileName = "InputGlyphs_Atlas.png";
|
||||
private const string AssetFileName = "InputGlyphs.asset";
|
||||
|
||||
/// <summary>打包前将每张源图缩放到此尺寸(像素),降低 atlas 体积。</summary>
|
||||
private const int SpriteSize = 128;
|
||||
|
||||
/// <summary>Atlas 最大边长。</summary>
|
||||
private const int MaxAtlasSize = 2048;
|
||||
|
||||
/// <summary>打包时各图之间的像素间距。</summary>
|
||||
private const int Padding = 2;
|
||||
|
||||
/// <summary>鼠标文件名 → Token 名映射表。</summary>
|
||||
private static readonly Dictionary<string, string> MouseNameMap = new()
|
||||
{
|
||||
{ "mouse-left", "LMB" },
|
||||
{ "mouse-right", "RMB" },
|
||||
{ "mouse-middle", "MMB" },
|
||||
{ "mouse-g1", "Mouse4" },
|
||||
{ "mouse-g2", "Mouse5" },
|
||||
{ "mouse-move-hor", "MouseMoveH" },
|
||||
{ "mouse-move-vert", "MouseMoveV" },
|
||||
};
|
||||
|
||||
private struct SpriteEntry
|
||||
{
|
||||
public string filePath;
|
||||
public string tokenName;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 入口
|
||||
// ================================================================
|
||||
|
||||
[MenuItem("Tools/Cielonos/Create InputGlyphs Sprite Asset")]
|
||||
public static void Execute()
|
||||
{
|
||||
var entries = new List<SpriteEntry>();
|
||||
CollectEntries(KeyboardFolder, entries, MapKeyboardName);
|
||||
CollectEntries(MouseFolder, entries, MapMouseName);
|
||||
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
Debug.LogError("[InputGlyphs] InputIcons 文件夹中未找到任何图片。");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[InputGlyphs] 发现 {entries.Count} 张图标,开始打包...");
|
||||
|
||||
// ── 1. 加载并缩放所有图标 ──
|
||||
LoadAndResizeResult loaded = LoadAndResize(entries);
|
||||
|
||||
// ── 2. 打包为 atlas ──
|
||||
var atlas = new Texture2D(MaxAtlasSize, MaxAtlasSize, TextureFormat.RGBA32, false);
|
||||
Rect[] rects = atlas.PackTextures(loaded.Textures.ToArray(), Padding, MaxAtlasSize, false);
|
||||
|
||||
if (rects == null)
|
||||
{
|
||||
Debug.LogError("[InputGlyphs] PackTextures 失败,请增大 MaxAtlasSize。");
|
||||
CleanupTextures(loaded.Textures);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 3. 保存 atlas PNG 到磁盘 ──
|
||||
string atlasPath = $"{AtlasOutputFolder}/{AtlasFileName}";
|
||||
SaveAtlasToDisk(atlas, atlasPath);
|
||||
|
||||
// ── 4. 配置 atlas 导入器,写入每个子 Sprite 的元数据 ──
|
||||
ConfigureAtlasImporter(atlasPath, atlas, rects, loaded.Names);
|
||||
|
||||
// ── 5. 从已导入的 atlas 构建 TMP_SpriteAsset(放入 Resources 以便 TMP 按名查找) ──
|
||||
string assetPath = $"{AssetOutputFolder}/{AssetFileName}";
|
||||
BuildSpriteAsset(atlasPath, assetPath);
|
||||
|
||||
// ── 6. 清理临时纹理 ──
|
||||
CleanupTextures(loaded.Textures);
|
||||
UnityEngine.Object.DestroyImmediate(atlas);
|
||||
|
||||
Debug.Log($"[InputGlyphs] SpriteAsset 已创建:{assetPath}({entries.Count} 个图标)。" +
|
||||
$"\n在 TMP 富文本中使用:<sprite=\"InputGlyphs\" name=\"Q\"> 或 <sprite name=\"Q\">");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 收集文件
|
||||
// ================================================================
|
||||
|
||||
private static void CollectEntries(
|
||||
string folder,
|
||||
List<SpriteEntry> entries,
|
||||
Func<string, string> nameMapper)
|
||||
{
|
||||
string[] guids = AssetDatabase.FindAssets("t:Texture2D", new[] { folder });
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string fileName = Path.GetFileNameWithoutExtension(assetPath);
|
||||
entries.Add(new SpriteEntry
|
||||
{
|
||||
filePath = Path.GetFullPath(assetPath),
|
||||
tokenName = nameMapper(fileName)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 键盘文件名 → Token 名。
|
||||
/// 规则:单字母大写,单数字保留,Fn 键大写,连字符转 PascalCase,其余首字母大写。
|
||||
/// </summary>
|
||||
private static string MapKeyboardName(string fileName)
|
||||
{
|
||||
// 单字母 → 大写
|
||||
if (fileName.Length == 1 && char.IsLetter(fileName[0]))
|
||||
return fileName.ToUpperInvariant();
|
||||
|
||||
// 单数字 → 原样
|
||||
if (fileName.Length == 1 && char.IsDigit(fileName[0]))
|
||||
return fileName;
|
||||
|
||||
// 功能键 f1-f12 → 全大写
|
||||
if (fileName.Length >= 2 && fileName[0] == 'f' &&
|
||||
int.TryParse(fileName.Substring(1), out _))
|
||||
return fileName.ToUpperInvariant();
|
||||
|
||||
// 含连字符 → PascalCase (arrow-down → ArrowDown)
|
||||
if (fileName.Contains('-'))
|
||||
{
|
||||
string[] parts = fileName.Split('-');
|
||||
var sb = new StringBuilder();
|
||||
foreach (string part in parts)
|
||||
{
|
||||
if (part.Length > 0)
|
||||
sb.Append(char.ToUpperInvariant(part[0]))
|
||||
.Append(part, 1, part.Length - 1);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// 其余 → 首字母大写
|
||||
return char.ToUpperInvariant(fileName[0]) + fileName.Substring(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 鼠标文件名 → Token 名。通过静态映射表查找。
|
||||
/// </summary>
|
||||
private static string MapMouseName(string fileName)
|
||||
{
|
||||
return MouseNameMap.TryGetValue(fileName, out string mapped) ? mapped : fileName;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 加载 & 缩放
|
||||
// ================================================================
|
||||
|
||||
private struct LoadAndResizeResult
|
||||
{
|
||||
public List<Texture2D> Textures;
|
||||
public List<string> Names;
|
||||
}
|
||||
|
||||
private static LoadAndResizeResult LoadAndResize(List<SpriteEntry> entries)
|
||||
{
|
||||
var textures = new List<Texture2D>(entries.Count);
|
||||
var names = new List<string>(entries.Count);
|
||||
|
||||
foreach (SpriteEntry entry in entries)
|
||||
{
|
||||
byte[] bytes = File.ReadAllBytes(entry.filePath);
|
||||
var src = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
||||
ImageConversion.LoadImage(src, bytes);
|
||||
|
||||
Texture2D dst = Resize(src, SpriteSize, SpriteSize);
|
||||
UnityEngine.Object.DestroyImmediate(src);
|
||||
|
||||
textures.Add(dst);
|
||||
names.Add(entry.tokenName);
|
||||
}
|
||||
|
||||
return new LoadAndResizeResult { Textures = textures, Names = names };
|
||||
}
|
||||
|
||||
private static Texture2D Resize(Texture2D source, int width, int height)
|
||||
{
|
||||
RenderTexture rt = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32);
|
||||
rt.filterMode = FilterMode.Bilinear;
|
||||
Graphics.Blit(source, rt);
|
||||
|
||||
RenderTexture prev = RenderTexture.active;
|
||||
RenderTexture.active = rt;
|
||||
|
||||
var dst = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||||
dst.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
||||
dst.Apply();
|
||||
|
||||
RenderTexture.active = prev;
|
||||
RenderTexture.ReleaseTemporary(rt);
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Atlas 保存 & 导入
|
||||
// ================================================================
|
||||
|
||||
private static void SaveAtlasToDisk(Texture2D atlas, string atlasPath)
|
||||
{
|
||||
string dir = Path.GetDirectoryName(atlasPath);
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
File.WriteAllBytes(atlasPath, atlas.EncodeToPNG());
|
||||
AssetDatabase.ImportAsset(atlasPath, ImportAssetOptions.ForceUpdate);
|
||||
}
|
||||
|
||||
private static void ConfigureAtlasImporter(
|
||||
string atlasPath,
|
||||
Texture2D atlas,
|
||||
Rect[] rects,
|
||||
List<string> names)
|
||||
{
|
||||
var importer = AssetImporter.GetAtPath(atlasPath) as TextureImporter;
|
||||
if (importer == null)
|
||||
{
|
||||
Debug.LogError($"[InputGlyphs] 无法获取 TextureImporter:{atlasPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
importer.textureType = TextureImporterType.Sprite;
|
||||
importer.spriteImportMode = SpriteImportMode.Multiple;
|
||||
importer.isReadable = false;
|
||||
importer.mipmapEnabled = false;
|
||||
importer.textureCompression = TextureImporterCompression.Uncompressed;
|
||||
importer.maxTextureSize = MaxAtlasSize;
|
||||
|
||||
int atlasW = atlas.width;
|
||||
int atlasH = atlas.height;
|
||||
|
||||
var spritesheet = new SpriteMetaData[rects.Length];
|
||||
for (int i = 0; i < rects.Length; i++)
|
||||
{
|
||||
float x = rects[i].x * atlasW;
|
||||
float y = rects[i].y * atlasH;
|
||||
float w = rects[i].width * atlasW;
|
||||
float h = rects[i].height * atlasH;
|
||||
|
||||
spritesheet[i] = new SpriteMetaData
|
||||
{
|
||||
name = names[i],
|
||||
rect = new Rect(x, y, w, h),
|
||||
alignment = (int)SpriteAlignment.Center,
|
||||
pivot = new Vector2(0.5f, 0.5f)
|
||||
};
|
||||
}
|
||||
|
||||
importer.spritesheet = spritesheet;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// TMP_SpriteAsset 构建
|
||||
// ================================================================
|
||||
|
||||
private static void BuildSpriteAsset(string atlasPath, string assetPath)
|
||||
{
|
||||
// 加载持久化后的 atlas 纹理
|
||||
Texture2D atlasTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(atlasPath);
|
||||
if (atlasTexture == null)
|
||||
{
|
||||
Debug.LogError($"[InputGlyphs] 无法加载 atlas:{atlasPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 atlas 加载所有子 Sprite
|
||||
Sprite[] sprites = AssetDatabase.LoadAllAssetsAtPath(atlasPath)
|
||||
.OfType<Sprite>()
|
||||
.OrderByDescending(s => s.rect.y)
|
||||
.ThenBy(s => s.rect.x)
|
||||
.ToArray();
|
||||
|
||||
if (sprites.Length == 0)
|
||||
{
|
||||
Debug.LogError("[InputGlyphs] Atlas 中未找到子 Sprite,请检查导入设置。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建 TMP_SpriteAsset 实例
|
||||
TMP_SpriteAsset spriteAsset = ScriptableObject.CreateInstance<TMP_SpriteAsset>();
|
||||
|
||||
string dir = Path.GetDirectoryName(assetPath);
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
// 如果已存在旧资产则删除
|
||||
if (AssetDatabase.LoadAssetAtPath<TMP_SpriteAsset>(assetPath) != null)
|
||||
AssetDatabase.DeleteAsset(assetPath);
|
||||
|
||||
AssetDatabase.CreateAsset(spriteAsset, assetPath);
|
||||
|
||||
// 设置 sprite sheet 引用
|
||||
spriteAsset.spriteSheet = atlasTexture;
|
||||
|
||||
// 通过反射设置版本号(internal setter)
|
||||
SetFieldValue(spriteAsset, "m_Version", "1.1.0");
|
||||
|
||||
// 设置 hash code
|
||||
spriteAsset.hashCode = TMP_TextUtilities.GetSimpleHashCode(spriteAsset.name);
|
||||
|
||||
// 构建 Glyph 和 Character 表
|
||||
var glyphTable = new List<TMP_SpriteGlyph>(sprites.Length);
|
||||
var charTable = new List<TMP_SpriteCharacter>(sprites.Length);
|
||||
|
||||
for (int i = 0; i < sprites.Length; i++)
|
||||
{
|
||||
Sprite sprite = sprites[i];
|
||||
|
||||
var glyph = new TMP_SpriteGlyph(
|
||||
index: (uint)i,
|
||||
metrics: new GlyphMetrics(
|
||||
sprite.rect.width,
|
||||
sprite.rect.height,
|
||||
0f,
|
||||
sprite.rect.height * 0.8f,
|
||||
sprite.rect.width),
|
||||
glyphRect: new GlyphRect(sprite.rect),
|
||||
scale: 1.0f,
|
||||
atlasIndex: 0,
|
||||
sprite: sprite
|
||||
);
|
||||
glyphTable.Add(glyph);
|
||||
|
||||
var character = new TMP_SpriteCharacter(0xFFFE, glyph)
|
||||
{
|
||||
name = sprite.name,
|
||||
scale = 1.0f
|
||||
};
|
||||
charTable.Add(character);
|
||||
}
|
||||
|
||||
// 通过反射设置内部字段(setter 为 internal)
|
||||
SetFieldValue(spriteAsset, "m_GlyphTable", glyphTable);
|
||||
SetFieldValue(spriteAsset, "m_SpriteCharacterTable", charTable);
|
||||
|
||||
// 创建并嵌入 Material
|
||||
Shader shader = Shader.Find("TextMeshPro/Sprite");
|
||||
if (shader == null)
|
||||
{
|
||||
Debug.LogWarning("[InputGlyphs] Shader 'TextMeshPro/Sprite' 未找到。");
|
||||
return;
|
||||
}
|
||||
|
||||
var material = new Material(shader);
|
||||
material.SetTexture(ShaderUtilities.ID_MainTex, atlasTexture);
|
||||
material.name = spriteAsset.name + " Material";
|
||||
spriteAsset.material = material;
|
||||
AssetDatabase.AddObjectToAsset(material, spriteAsset);
|
||||
|
||||
// 重建查找表并保存
|
||||
spriteAsset.UpdateLookupTables();
|
||||
EditorUtility.SetDirty(spriteAsset);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.ImportAsset(assetPath);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 工具方法
|
||||
// ================================================================
|
||||
|
||||
private static void SetFieldValue(object target, string fieldName, object value)
|
||||
{
|
||||
FieldInfo field = target.GetType().GetField(
|
||||
fieldName,
|
||||
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
|
||||
|
||||
if (field != null)
|
||||
{
|
||||
field.SetValue(target, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[InputGlyphs] 反射未找到字段 '{fieldName}'(类型:{target.GetType().Name})。");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CleanupTextures(List<Texture2D> textures)
|
||||
{
|
||||
foreach (Texture2D tex in textures)
|
||||
{
|
||||
if (tex != null)
|
||||
UnityEngine.Object.DestroyImmediate(tex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c0f978bf51ac504a82aaa25578a31cb
|
||||
249
Assets/Scripts/SLSUtilities/UI/InputBindingResolver.cs
Normal file
249
Assets/Scripts/SLSUtilities/UI/InputBindingResolver.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace SLSUtilities.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 从 <see cref="InputActionAsset"/> 读取当前按键绑定,
|
||||
/// 将 Input Action 名映射为 InputGlyphs SpriteAsset 中的 glyph token 名。
|
||||
/// <para>
|
||||
/// 初始化后,<see cref="InputGlyphParser"/> 自动使用本解析器,
|
||||
/// 使得本地化文本可以使用 Action 名作为 Token(如 <c>[Interact]</c>),
|
||||
/// 无需硬编码具体按键名(如 <c>[R]</c>)。
|
||||
/// 当玩家重新绑定按键时,重新调用 <see cref="Initialize"/> 即可自动更新图标。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class InputBindingResolver
|
||||
{
|
||||
private static readonly Dictionary<string, string> ActionGlyphMap = new();
|
||||
private static bool isInitialized;
|
||||
|
||||
/// <summary>是否已初始化。</summary>
|
||||
public static bool IsInitialized => isInitialized;
|
||||
|
||||
// ================================================================
|
||||
// Control Path → Glyph Token 特殊映射
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 无法通过通用规则推导的控制路径,手动映射到 glyph token。
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> ControlPathOverrides = new()
|
||||
{
|
||||
// 鼠标
|
||||
{ "<Mouse>/leftButton", "LMB" },
|
||||
{ "<Mouse>/rightButton", "RMB" },
|
||||
{ "<Mouse>/middleButton", "MMB" },
|
||||
{ "<Mouse>/forwardButton", "Mouse4" },
|
||||
{ "<Mouse>/backButton", "Mouse5" },
|
||||
|
||||
// 修饰键(左右合并为同一图标)
|
||||
{ "<Keyboard>/leftCtrl", "Ctrl" },
|
||||
{ "<Keyboard>/rightCtrl", "Ctrl" },
|
||||
{ "<Keyboard>/leftShift", "Shift" },
|
||||
{ "<Keyboard>/rightShift", "Shift" },
|
||||
{ "<Keyboard>/leftAlt", "Alt" },
|
||||
{ "<Keyboard>/rightAlt", "Alt" },
|
||||
|
||||
// 名称差异较大的按键
|
||||
{ "<Keyboard>/escape", "Esc" },
|
||||
{ "<Keyboard>/delete", "Del" },
|
||||
{ "<Keyboard>/insert", "Ins" },
|
||||
{ "<Keyboard>/pageUp", "Pgup" },
|
||||
{ "<Keyboard>/pageDown", "Pgdn" },
|
||||
{ "<Keyboard>/printScreen","Prtsc" },
|
||||
{ "<Keyboard>/scrollLock", "Scrlk" },
|
||||
{ "<Keyboard>/capsLock", "Caps" },
|
||||
{ "<Keyboard>/numpadEnter","Enter" },
|
||||
{ "<Keyboard>/contextMenu","Context" },
|
||||
{ "<Keyboard>/leftMeta", "Windows" },
|
||||
{ "<Keyboard>/rightMeta", "Windows" },
|
||||
{ "<Keyboard>/backquote", "Tilde" },
|
||||
{ "<Keyboard>/minus", "Hyphen" },
|
||||
{ "<Keyboard>/equals", "Equals" },
|
||||
{ "<Keyboard>/leftBracket","BracketOpen" },
|
||||
{ "<Keyboard>/rightBracket","BracketClose" },
|
||||
{ "<Keyboard>/backslash", "BackwardSlash" },
|
||||
{ "<Keyboard>/semicolon", "SemiColon" },
|
||||
{ "<Keyboard>/quote", "Quote" },
|
||||
{ "<Keyboard>/comma", "Comma" },
|
||||
{ "<Keyboard>/period", "Dot" },
|
||||
{ "<Keyboard>/slash", "ForwardSlash" },
|
||||
{ "<Keyboard>/numpadPlus", "Plus" },
|
||||
|
||||
// 箭头键
|
||||
{ "<Keyboard>/upArrow", "ArrowUp" },
|
||||
{ "<Keyboard>/downArrow", "ArrowDown" },
|
||||
{ "<Keyboard>/leftArrow", "ArrowLeft" },
|
||||
{ "<Keyboard>/rightArrow", "ArrowRight" },
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// 初始化
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 从 InputActionAsset 构建 Action 名 → Glyph Token 的映射表。
|
||||
/// 仅处理指定 controlScheme 的非 composite 绑定。
|
||||
/// </summary>
|
||||
/// <param name="inputActions">包含所有 ActionMap 的 InputActionAsset。</param>
|
||||
/// <param name="controlScheme">
|
||||
/// 要匹配的 Control Scheme 名称(如 "KeyboardMouse")。
|
||||
/// 为 null 时取第一个匹配的绑定。
|
||||
/// </param>
|
||||
public static void Initialize(InputActionAsset inputActions, string controlScheme = "KeyboardMouse")
|
||||
{
|
||||
ActionGlyphMap.Clear();
|
||||
|
||||
if (inputActions == null)
|
||||
{
|
||||
Debug.LogWarning("[InputBindingResolver] InputActionAsset 为 null,无法初始化。");
|
||||
isInitialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (InputActionMap map in inputActions.actionMaps)
|
||||
{
|
||||
foreach (InputAction action in map.actions)
|
||||
{
|
||||
string glyph = ResolveFirstBinding(action, controlScheme);
|
||||
if (glyph != null)
|
||||
{
|
||||
ActionGlyphMap[action.name] = glyph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ActionGlyphMap["M0"] = ActionGlyphMap["MainWeaponPrimary"];
|
||||
ActionGlyphMap["M1"] = ActionGlyphMap["MainWeaponSecondary"];
|
||||
ActionGlyphMap["MA"] = ActionGlyphMap["MainWeaponSpecialA"];
|
||||
ActionGlyphMap["MB"] = ActionGlyphMap["MainWeaponSpecialB"];
|
||||
ActionGlyphMap["MC"] = ActionGlyphMap["MainWeaponSpecialC"];
|
||||
ActionGlyphMap["S0"] = ActionGlyphMap["SupportEquipment0"];
|
||||
ActionGlyphMap["S1"] = ActionGlyphMap["SupportEquipment1"];
|
||||
ActionGlyphMap["S2"] = ActionGlyphMap["SupportEquipment2"];
|
||||
ActionGlyphMap["S3"] = ActionGlyphMap["SupportEquipment3"];
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Token 解析为 glyph token 名。
|
||||
/// 如果 token 匹配已注册的 Action 名,则返回对应按键的 glyph token;
|
||||
/// 否则原样返回(可能是直接按键名,如 "LMB"、"Q")。
|
||||
/// </summary>
|
||||
public static string ResolveToken(string token)
|
||||
{
|
||||
if (isInitialized && ActionGlyphMap.TryGetValue(token, out string glyph))
|
||||
{
|
||||
return glyph;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定 Action 名当前绑定的 glyph token。
|
||||
/// 未找到时返回 null。
|
||||
/// </summary>
|
||||
public static string GetGlyphForAction(string actionName)
|
||||
{
|
||||
if (isInitialized && ActionGlyphMap.TryGetValue(actionName, out string glyph))
|
||||
{
|
||||
return glyph;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 内部实现
|
||||
// ================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 取 Action 的第一个匹配 controlScheme 的非 composite 绑定,
|
||||
/// 解析其控制路径为 glyph token。
|
||||
/// </summary>
|
||||
private static string ResolveFirstBinding(InputAction action, string controlScheme)
|
||||
{
|
||||
foreach (InputBinding binding in action.bindings)
|
||||
{
|
||||
if (binding.isComposite || binding.isPartOfComposite)
|
||||
continue;
|
||||
|
||||
if (!IsBindingMatchingScheme(binding, controlScheme))
|
||||
continue;
|
||||
|
||||
string path = binding.effectivePath;
|
||||
string glyph = ControlPathToGlyphToken(path);
|
||||
if (glyph != null)
|
||||
return glyph;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查绑定是否属于指定的 Control Scheme。
|
||||
/// controlScheme 为 null 时始终匹配。
|
||||
/// </summary>
|
||||
private static bool IsBindingMatchingScheme(InputBinding binding, string controlScheme)
|
||||
{
|
||||
if (string.IsNullOrEmpty(controlScheme))
|
||||
return true;
|
||||
|
||||
// binding.groups 格式为 "KeyboardMouse" 或 "KeyboardMouse;Gamepad" 等
|
||||
if (string.IsNullOrEmpty(binding.groups))
|
||||
return false;
|
||||
|
||||
return binding.groups.Contains(controlScheme);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Input System 控制路径转换为 InputGlyphs SpriteAsset 中的 token 名。
|
||||
/// </summary>
|
||||
private static string ControlPathToGlyphToken(string controlPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(controlPath))
|
||||
return null;
|
||||
|
||||
// 优先查找特殊映射
|
||||
if (ControlPathOverrides.TryGetValue(controlPath, out string token))
|
||||
return token;
|
||||
|
||||
// 通用键盘规则
|
||||
const string keyboardPrefix = "<Keyboard>/";
|
||||
if (controlPath.StartsWith(keyboardPrefix))
|
||||
{
|
||||
string key = controlPath.Substring(keyboardPrefix.Length);
|
||||
return KeyNameToGlyphToken(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Input System 的键盘 key 名转换为 glyph token 名。
|
||||
/// 规则与 <c>InputGlyphsSpriteAssetCreator.MapKeyboardName</c> 保持一致。
|
||||
/// </summary>
|
||||
private static string KeyNameToGlyphToken(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return null;
|
||||
|
||||
// 单字母 → 大写
|
||||
if (key.Length == 1 && char.IsLetter(key[0]))
|
||||
return key.ToUpperInvariant();
|
||||
|
||||
// 单数字 → 原样
|
||||
if (key.Length == 1 && char.IsDigit(key[0]))
|
||||
return key;
|
||||
|
||||
// 功能键 f1-f12 → 全大写
|
||||
if (key.Length >= 2 && key[0] == 'f' && int.TryParse(key.Substring(1), out _))
|
||||
return key.ToUpperInvariant();
|
||||
|
||||
// 其余 → 首字母大写(space → Space, tab → Tab, enter → Enter, etc.)
|
||||
return char.ToUpperInvariant(key[0]) + key.Substring(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b61fa6c05168e034fa306c4f9b81ad8f
|
||||
80
Assets/Scripts/SLSUtilities/UI/InputGlyphParser.cs
Normal file
80
Assets/Scripts/SLSUtilities/UI/InputGlyphParser.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SLSUtilities.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 将文本中的 [Token] 标记替换为 TMP <sprite> 富文本标签,
|
||||
/// 用于在 TMP_Text 中内联显示按键图标。
|
||||
/// <para>
|
||||
/// Token 支持两种形式:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>直接按键名</b>:[Q]、[LMB]、[ArrowDown] — 直接映射到 SpriteAsset 中的 sprite name</item>
|
||||
/// <item><b>Input Action 名</b>:[Interact]、[MainWeaponPrimary] — 通过 <see cref="InputBindingResolver"/>
|
||||
/// 解析为当前绑定的按键 glyph token(需先调用 <see cref="InputBindingResolver.Initialize"/>)</item>
|
||||
/// </list>
|
||||
/// 语法示例:
|
||||
/// <list type="bullet">
|
||||
/// <item>[Q]+[RMB] → Q 键 + 右键图标(+ 作为普通文本保留)</item>
|
||||
/// <item>Hold [E] → "Hold " 为普通文本,E 键为图标</item>
|
||||
/// <item>按 [Interact] 拾取 → 自动解析为当前绑定的按键图标</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class InputGlyphParser
|
||||
{
|
||||
/// <summary>默认关联的 TMP SpriteAsset 名称。</summary>
|
||||
public const string DefaultSpriteAsset = "InputGlyphs";
|
||||
|
||||
/// <summary>
|
||||
/// 匹配 [Token] 模式。
|
||||
/// \w+ 涵盖字母、数字、下划线,足以覆盖所有 Token 名(Q, LMB, ArrowDown, F12, Interact 等)。
|
||||
/// </summary>
|
||||
private static readonly Regex TokenPattern =
|
||||
new Regex(@"\[(\w+)\]", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// 将文本中的 [Token] 标记解析为 TMP sprite 富文本标签。
|
||||
/// 如果 <see cref="InputBindingResolver"/> 已初始化,Token 会先尝试解析为 Action 名对应的按键。
|
||||
/// </summary>
|
||||
/// <param name="rawText">
|
||||
/// 原始文本,可包含 [Token] 标记。
|
||||
/// 例如 "[Q]+[RMB]"、"Hold [E]"、"按 [Interact] 拾取"。
|
||||
/// </param>
|
||||
/// <param name="spriteAssetName">
|
||||
/// 要引用的 TMP SpriteAsset 名称。
|
||||
/// 传入 null 或空字符串时,生成不指定 asset 的短格式标签
|
||||
/// (需在 TMP Settings 中配置默认 Sprite Asset)。
|
||||
/// </param>
|
||||
/// <returns>替换后的富文本字符串。输入为 null 或空时原样返回。</returns>
|
||||
public static string Parse(string rawText, string spriteAssetName = DefaultSpriteAsset)
|
||||
{
|
||||
if (string.IsNullOrEmpty(rawText))
|
||||
return rawText;
|
||||
|
||||
bool useResolver = InputBindingResolver.IsInitialized;
|
||||
bool hasAssetName = !string.IsNullOrEmpty(spriteAssetName);
|
||||
|
||||
return TokenPattern.Replace(rawText, match =>
|
||||
{
|
||||
string token = match.Groups[1].Value;
|
||||
|
||||
// 通过 InputBindingResolver 解析 Action 名 → glyph token
|
||||
string resolvedToken = useResolver
|
||||
? InputBindingResolver.ResolveToken(token)
|
||||
: token;
|
||||
|
||||
return hasAssetName
|
||||
? $"<sprite=\"{spriteAssetName}\" name=\"{resolvedToken}\">"
|
||||
: $"<sprite name=\"{resolvedToken}\">";
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查文本是否包含任何 [Token] 标记。
|
||||
/// </summary>
|
||||
public static bool ContainsGlyphs(string text)
|
||||
{
|
||||
return !string.IsNullOrEmpty(text) && TokenPattern.IsMatch(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SLSUtilities/UI/InputGlyphParser.cs.meta
Normal file
2
Assets/Scripts/SLSUtilities/UI/InputGlyphParser.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d652c4cf02601bc438c848b5f0a87c5f
|
||||
8
Assets/Scripts/SLSUtilities/UI/SpriteAssets.meta
Normal file
8
Assets/Scripts/SLSUtilities/UI/SpriteAssets.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf1cf4d63ccd2f349805b51366ab6177
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Scripts/SLSUtilities/UI/SpriteAssets/InputGlyphs_Atlas.png
LFS
Normal file
BIN
Assets/Scripts/SLSUtilities/UI/SpriteAssets/InputGlyphs_Atlas.png
LFS
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,9 @@ namespace SLSUtilities.UI
|
||||
public class UIPageBase : UIElementBase
|
||||
{
|
||||
/// <summary>当前页面是否处于打开状态。</summary>
|
||||
public bool IsOpen { get; private set; }
|
||||
[ShowInInspector]
|
||||
[ReadOnly]
|
||||
public bool IsOpen { get; protected set; }
|
||||
|
||||
/// <summary>是否允许按 ESC 关闭此页面。</summary>
|
||||
public virtual bool CloseOnEsc => true;
|
||||
@@ -41,7 +43,6 @@ namespace SLSUtilities.UI
|
||||
Show();
|
||||
UIPageManager.Instance.RegisterPage(this);
|
||||
OnPageOpened();
|
||||
PageOpened?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -56,13 +57,18 @@ namespace SLSUtilities.UI
|
||||
UIPageManager.Instance.UnregisterPage(this);
|
||||
Hide();
|
||||
OnPageClosed();
|
||||
PageClosed?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>页面打开后的可覆盖回调。</summary>
|
||||
protected virtual void OnPageOpened() { }
|
||||
protected virtual void OnPageOpened()
|
||||
{
|
||||
PageOpened?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>页面关闭后的可覆盖回调。</summary>
|
||||
protected virtual void OnPageClosed() { }
|
||||
protected virtual void OnPageClosed()
|
||||
{
|
||||
PageClosed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace SLSUtilities.UI
|
||||
/// </summary>
|
||||
public class UIPageManager : Singleton<UIPageManager>
|
||||
{
|
||||
// ──────────────────── 动态页面 ────────────────────
|
||||
[Header("Dynamic Pages")]
|
||||
[Tooltip("确认 / 提示弹窗的 Prefab(需挂载 ConfirmUIPage 组件)。")]
|
||||
[SerializeField] private GameObject confirmPagePrefab;
|
||||
|
||||
[Tooltip("动态页面实例化的父节点(通常是 Canvas 下的某个 Transform)。")]
|
||||
[SerializeField] private Transform dynamicPageParent;
|
||||
|
||||
// ──────────────────── 页面栈 ────────────────────
|
||||
private readonly List<UIPageBase> openPages = new List<UIPageBase>();
|
||||
|
||||
/// <summary>
|
||||
@@ -28,6 +37,11 @@ namespace SLSUtilities.UI
|
||||
/// <summary>当前栈顶页面,若无则返回 null。</summary>
|
||||
public UIPageBase TopPage => openPages.Count > 0 ? openPages[^1] : null;
|
||||
|
||||
/// <summary>确认页面的 Prefab 引用(以 GameObject 形式暴露,避免跨 Assembly 类型依赖)。</summary>
|
||||
public GameObject ConfirmPagePrefab => confirmPagePrefab;
|
||||
|
||||
// ──────────────────── 页面注册 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 将页面注册到栈顶。若为首个页面则触发 OnInputBlockChanged(true)。
|
||||
/// 由 UIPageBase.Open() 内部调用,不要手动调用。
|
||||
@@ -59,6 +73,8 @@ namespace SLSUtilities.UI
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────── 页面关闭 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 关闭栈顶页面(若其允许 ESC 关闭)。由游戏层的 ESC 处理逻辑调用。
|
||||
/// </summary>
|
||||
@@ -85,5 +101,25 @@ namespace SLSUtilities.UI
|
||||
openPages[i].Close();
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────── 动态页面实例化 ────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 在 <see cref="dynamicPageParent"/> 下实例化一个动态页面 Prefab。
|
||||
/// 用于创建运行时弹窗(确认框、提示框等)。
|
||||
/// </summary>
|
||||
/// <param name="prefab">要实例化的 Prefab。</param>
|
||||
/// <returns>实例化后的 GameObject。</returns>
|
||||
public GameObject InstantiateDynamicPage(GameObject prefab)
|
||||
{
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError("[UIPageManager] Cannot instantiate a null prefab.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Transform parent = dynamicPageParent != null ? dynamicPageParent : transform;
|
||||
return Instantiate(prefab, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user