Compare commits

...

5 Commits

22 changed files with 1904 additions and 1313 deletions

View File

@ -193,6 +193,7 @@ GameObject:
- component: {fileID: 1590262444720639052}
- component: {fileID: 8270483239104722155}
- component: {fileID: 4311531008057825966}
- component: {fileID: -6194537545441247702}
m_Layer: 5
m_Name: UILoadUpdateWindow
m_TagString: Untagged
@ -319,6 +320,32 @@ MonoBehaviour:
uuid: 052a4cdb-50cb-44b3-876f-90639a028620
LoopCount: 1
Name:
--- !u!114 &-6194537545441247702
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 526598954257632073}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b880940ee89d20e4fa6f2630d7a81a14, type: 3}
m_Name:
m_EditorClassIdentifier:
openPreset: 3
closePreset: 3
openEase: 4
closeEase: 3
targetRect: {fileID: 2553447206821208227}
canvasGroup: {fileID: 0}
useUnscaledTime: 1
initializeAsClosed: 1
disableInteractionWhilePlaying: 1
openDuration: 0.22
closeDuration: 0.18
slideDistance: 120
toastDistance: 40
closedScale: 0.94
--- !u!1 &541431694581587512
GameObject:
m_ObjectHideFlags: 0

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 84e81e9c2b71c5f4a8e13c52a72e3e74
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,40 @@
DarkMode Mod for Unity Editor on Windows
----------------------------------------------------------------------------
v1.1 04/08/2024
----------------------------------------------------------------------------
A fully working runtime dark mode mod for Unity Editor on Windows with:
- Dark title bar
- Dark menu bar
- Dark context menu
- And more...
> This runtime mod works on Windows 11 and Windows 10 1903+.
Tested on Unity 2019, 2020, 2021, 2022, 2023 and Unity 6.
Source code is available at: https://github.com/0x7c13/UnityEditor-DarkMode
----------------------------------------------------------------------------
MIT License
Copyright (c) 2024 Jiaqi (0x7c13) Liu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 33d8f76296a183145ae27b820db1e4b4
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,6 @@
menubar_textcolor = 200,200,200
menubar_textcolor_disabled = 160,160,160
menubar_bgcolor = 48,48,48
menubaritem_bgcolor = 48,48,48
menubaritem_bgcolor_hot = 62,62,62
menubaritem_bgcolor_selected = 62,62,62

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6c50fe9ef05bb7b49bf69b0c596fd1ed
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,63 @@
fileFormatVersion: 2
guid: c8116b2fba7c75047b30e087741eb77b
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 1
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: Windows
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: None
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,18 +1,17 @@
using System;
using System;
using UnityEngine;
using UnityEngine.InputSystem;
public static class GlyphService
{
// 缓存的设备提示数组,避免内存分配
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 string[] KeyboardGroupHints = { "keyboard", "mouse", "keyboard&mouse", "keyboardmouse", "kbm" };
private static readonly string[] XboxGroupHints = { "xbox", "xinput", "gamepad", "controller" };
private static readonly string[] PlayStationGroupHints = { "playstation", "dualshock", "dualsense", "gamepad", "controller" };
private static readonly string[] OtherGamepadGroupHints = { "gamepad", "controller", "joystick" };
private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' };
/// <summary>
/// 获取输入图标数据库实例
/// </summary>
private static InputGlyphDatabase _database;
public static InputGlyphDatabase Database
{
get
@ -26,179 +25,291 @@ public static class GlyphService
}
}
private static InputGlyphDatabase _database;
/// <summary>
/// 获取输入操作的绑定控制路径
/// </summary>
/// <param name="action">输入操作</param>
/// <param name="compositePartName">复合部分名称(可选)</param>
/// <param name="deviceOverride">设备类型覆盖(可选)</param>
/// <returns>绑定控制路径</returns>
public static string GetBindingControlPath(InputAction action, string compositePartName = null, 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, compositePartName, deviceOverride);
return binding.hasOverrides ? binding.effectivePath : binding.path;
return TryGetBindingControl(action, compositePartName, deviceOverride, out InputBinding binding)
? GetEffectivePath(binding)
: string.Empty;
}
/// <summary>
/// 尝试获取输入操作的 TextMeshPro 标签
/// </summary>
/// <param name="reference">输入操作引用</param>
/// <param name="compositePartName">复合部分名称</param>
/// <param name="device">设备类型</param>
/// <param name="tag">输出的 TMP 标签</param>
/// <param name="displayFallback">显示回退文本</param>
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetTMPTagForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null)
public static string GetBindingControlPath(
InputActionReference actionReference,
string compositePartName = null,
InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{
string path = GetBindingControlPath(reference, compositePartName, device);
return TryGetTMPTagForActionPath(path, device, out tag, out displayFallback, db);
return GetBindingControlPath(actionReference != null ? actionReference.action : null, compositePartName, deviceOverride);
}
/// <summary>
/// 尝试获取输入操作的 UI Sprite
/// </summary>
/// <param name="reference">输入操作引用</param>
/// <param name="compositePartName">复合部分名称</param>
/// <param name="device">设备类型</param>
/// <param name="sprite">输出的 Sprite</param>
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetUISpriteForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null)
public static bool TryGetTMPTagForActionPath(
InputAction action,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory device,
out string tag,
out string displayFallback,
InputGlyphDatabase db = null)
{
string path = GetBindingControlPath(reference, compositePartName, device);
return TryGetUISpriteForActionPath(path, device, out sprite, db);
string controlPath = GetBindingControlPath(action, compositePartName, device);
return TryGetTMPTagForActionPath(controlPath, device, out tag, out displayFallback, db);
}
/// <summary>
/// 根据控制路径尝试获取 TextMeshPro 标签
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <param name="tag">输出的 TMP 标签</param>
/// <param name="displayFallback">显示回退文本</param>
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetTMPTagForActionPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null)
public static bool TryGetTMPTagForActionPath(
InputActionReference actionReference,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory device,
out string tag,
out string displayFallback,
InputGlyphDatabase db = null)
{
tag = null;
displayFallback = null;
db = db ?? Database;
return TryGetTMPTagForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out tag, out displayFallback, db);
}
public static bool TryGetUISpriteForActionPath(
InputAction action,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory device,
out Sprite sprite,
InputGlyphDatabase db = null)
{
string controlPath = GetBindingControlPath(action, compositePartName, device);
return TryGetUISpriteForActionPath(controlPath, device, out sprite, db);
}
public static bool TryGetUISpriteForActionPath(
InputActionReference actionReference,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory device,
out Sprite sprite,
InputGlyphDatabase db = null)
{
return TryGetUISpriteForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out sprite, db);
}
public static bool TryGetTMPTagForActionPath(
string controlPath,
InputDeviceWatcher.InputDeviceCategory device,
out string tag,
out string displayFallback,
InputGlyphDatabase db = null)
{
displayFallback = GetDisplayNameFromControlPath(controlPath);
tag = null;
var sprite = db.FindSprite(controlPath, device) ?? db.FindSprite(controlPath, InputDeviceWatcher.InputDeviceCategory.Keyboard);
if (!TryGetUISpriteForActionPath(controlPath, device, out Sprite sprite, db))
{
return false;
}
var spriteName = sprite == null ? string.Empty : sprite.name;
tag = $"<sprite name=\"{spriteName}\">";
tag = $"<sprite name=\"{sprite.name}\">";
return true;
}
/// <summary>
/// 根据控制路径尝试获取 UI Sprite
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <param name="sprite">输出的 Sprite</param>
/// <param name="db">数据库实例(可选)</param>
/// <returns>是否成功获取</returns>
public static bool TryGetUISpriteForActionPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null)
public static bool TryGetUISpriteForActionPath(
string controlPath,
InputDeviceWatcher.InputDeviceCategory device,
out Sprite sprite,
InputGlyphDatabase db = null)
{
sprite = null;
db = db ?? Database;
if (string.IsNullOrEmpty(controlPath) || db == null) return false;
sprite = db.FindSprite(controlPath, device) ?? db.FindSprite(controlPath, InputDeviceWatcher.InputDeviceCategory.Keyboard);
return sprite != null;
db ??= Database;
return db != null && db.TryGetSprite(controlPath, device, out sprite);
}
/// <summary>
/// 获取输入操作的绑定控制
/// </summary>
static InputBinding GetBindingControl(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
public static string GetDisplayNameFromInputAction(
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 b in action.bindings)
if (!TryGetBindingControl(action, compositePartName, deviceOverride, out InputBinding binding))
{
if (!string.IsNullOrEmpty(compositePartName))
return string.Empty;
}
string display = binding.ToDisplayString();
return string.IsNullOrEmpty(display) ? GetDisplayNameFromControlPath(GetEffectivePath(binding)) : display;
}
public static string GetDisplayNameFromControlPath(string controlPath)
{
if (string.IsNullOrWhiteSpace(controlPath))
{
return string.Empty;
}
string humanReadable = InputControlPath.ToHumanReadableString(controlPath, InputControlPath.HumanReadableStringOptions.OmitDevice);
if (!string.IsNullOrWhiteSpace(humanReadable))
{
return humanReadable;
}
string[] parts = controlPath.Split('/');
string last = parts[parts.Length - 1].Trim(TrimChars);
return last;
}
public static bool TryGetBindingControl(
InputAction action,
string compositePartName,
InputDeviceWatcher.InputDeviceCategory? deviceOverride,
out InputBinding binding)
{
binding = default;
if (action == null)
{
return false;
}
InputDeviceWatcher.InputDeviceCategory category = deviceOverride ?? InputDeviceWatcher.CurrentCategory;
int bestScore = int.MinValue;
bool requireCompositePart = !string.IsNullOrEmpty(compositePartName);
for (int i = 0; i < action.bindings.Count; i++)
{
InputBinding candidate = action.bindings[i];
if (candidate.isComposite)
{
if (!b.isPartOfComposite) continue;
if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue;
continue;
}
// 替换 LINQ Any() 以避免委托分配
if (!string.IsNullOrEmpty(b.path) && ContainsAnyHint(b.path, hints)) return b;
if (!string.IsNullOrEmpty(b.effectivePath) && ContainsAnyHint(b.effectivePath, hints)) return b;
if (requireCompositePart)
{
if (!candidate.isPartOfComposite || !string.Equals(candidate.name, compositePartName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
}
else if (candidate.isPartOfComposite)
{
continue;
}
string path = GetEffectivePath(candidate);
if (string.IsNullOrWhiteSpace(path))
{
continue;
}
int score = ScoreBinding(candidate, category);
if (score > bestScore)
{
bestScore = score;
binding = candidate;
}
}
return default;
return bestScore > int.MinValue;
}
// 辅助方法,避免 LINQ Any() 的内存分配
/// <summary>
/// 检查路径是否包含任何提示字符串
/// </summary>
static bool ContainsAnyHint(string path, string[] hints)
private static int ScoreBinding(InputBinding binding, InputDeviceWatcher.InputDeviceCategory category)
{
for (int i = 0; i < hints.Length; i++)
int score = 0;
string path = GetEffectivePath(binding);
if (MatchesBindingGroups(binding.groups, category))
{
if (path.IndexOf(hints[i], StringComparison.OrdinalIgnoreCase) >= 0)
return true;
score += 100;
}
else if (!string.IsNullOrWhiteSpace(binding.groups))
{
score -= 20;
}
if (MatchesControlPath(path, category))
{
score += 60;
}
if (!binding.isPartOfComposite)
{
score += 5;
}
return score;
}
private static bool MatchesBindingGroups(string groups, InputDeviceWatcher.InputDeviceCategory category)
{
if (string.IsNullOrWhiteSpace(groups))
{
return false;
}
string[] hints = GetGroupHints(category);
string[] tokens = groups.Split(InputBinding.Separator);
for (int i = 0; i < tokens.Length; i++)
{
string token = tokens[i].Trim();
if (ContainsAny(token, hints))
{
return true;
}
}
return false;
}
/// <summary>
/// 根据设备类型获取设备提示字符串数组
/// </summary>
static string[] GetDeviceHintsForCategory(InputDeviceWatcher.InputDeviceCategory cat)
private static bool MatchesControlPath(string path, InputDeviceWatcher.InputDeviceCategory category)
{
switch (cat)
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
switch (category)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return KeyboardHints;
return StartsWithDevice(path, "<Keyboard>") || StartsWithDevice(path, "<Mouse>");
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return XboxHints;
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, XboxGroupHints);
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return PlayStationHints;
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, PlayStationGroupHints);
default:
return XboxHints;
return StartsWithDevice(path, "<Gamepad>") || StartsWithDevice(path, "<Joystick>") || ContainsAny(path, OtherGamepadGroupHints);
}
}
/// <summary>
/// 从输入操作获取显示名称
/// </summary>
/// <param name="action">输入操作</param>
/// <param name="compositePartName">复合部分名称(可选)</param>
/// <param name="deviceOverride">设备类型覆盖</param>
/// <returns>显示名称</returns>
public static string GetDisplayNameFromInputAction(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory deviceOverride = InputDeviceWatcher.InputDeviceCategory.Keyboard)
private static bool StartsWithDevice(string path, string deviceTag)
{
if (action == null) return string.Empty;
var binding = GetBindingControl(action, compositePartName, deviceOverride);
return binding.ToDisplayString();
return path.StartsWith(deviceTag, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// 从控制路径获取显示名称
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <returns>显示名称</returns>
public static string GetDisplayNameFromControlPath(string controlPath)
private static string[] GetGroupHints(InputDeviceWatcher.InputDeviceCategory category)
{
if (string.IsNullOrEmpty(controlPath)) return string.Empty;
var parts = controlPath.Split('/');
var last = parts[parts.Length - 1].Trim(TrimChars);
return last;
switch (category)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return KeyboardGroupHints;
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return XboxGroupHints;
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return PlayStationGroupHints;
default:
return OtherGamepadGroupHints;
}
}
private static bool ContainsAny(string source, string[] hints)
{
if (string.IsNullOrWhiteSpace(source) || hints == null)
{
return false;
}
for (int i = 0; i < hints.Length; i++)
{
if (source.IndexOf(hints[i], StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
private static string GetEffectivePath(InputBinding binding)
{
return string.IsNullOrWhiteSpace(binding.effectivePath) ? binding.path : binding.effectivePath;
}
}

View File

@ -1,9 +1,10 @@
using System;
using System.Text.RegularExpressions;
using AlicizaX;
using System;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
public static class InputDeviceWatcher
{
@ -15,35 +16,108 @@ public static class InputDeviceWatcher
Other
}
static readonly float DebounceWindow = 1f;
public static InputDeviceCategory CurrentCategory = InputDeviceCategory.Keyboard;
public static string CurrentDeviceName = "";
public readonly struct DeviceContext : IEquatable<DeviceContext>
{
public readonly InputDeviceCategory Category;
public readonly int DeviceId;
public readonly int VendorId;
public readonly int ProductId;
public readonly string DeviceName;
public readonly string Layout;
public DeviceContext(
InputDeviceCategory category,
int deviceId,
int vendorId,
int productId,
string deviceName,
string layout)
{
Category = category;
DeviceId = deviceId;
VendorId = vendorId;
ProductId = productId;
DeviceName = deviceName ?? string.Empty;
Layout = layout ?? string.Empty;
}
public bool Equals(DeviceContext other)
{
return Category == other.Category
&& DeviceId == other.DeviceId
&& VendorId == other.VendorId
&& ProductId == other.ProductId
&& string.Equals(DeviceName, other.DeviceName, StringComparison.Ordinal)
&& string.Equals(Layout, other.Layout, StringComparison.Ordinal);
}
public override bool Equals(object obj)
{
return obj is DeviceContext other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
int hashCode = (int)Category;
hashCode = (hashCode * 397) ^ DeviceId;
hashCode = (hashCode * 397) ^ VendorId;
hashCode = (hashCode * 397) ^ ProductId;
hashCode = (hashCode * 397) ^ (DeviceName != null ? DeviceName.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (Layout != null ? Layout.GetHashCode() : 0);
return hashCode;
}
}
}
[Serializable]
private struct DeviceCapabilityInfo
{
public int vendorId;
public int productId;
}
private const float SameCategoryDebounceWindow = 0.15f;
private const float AxisActivationThreshold = 0.5f;
private const float StickActivationThreshold = 0.25f;
private const string DefaultKeyboardDeviceName = "Keyboard&Mouse";
public static InputDeviceCategory CurrentCategory { get; private set; } = InputDeviceCategory.Keyboard;
public static string CurrentDeviceName { get; private set; } = DefaultKeyboardDeviceName;
public static int CurrentDeviceId { get; private set; } = -1;
public static int CurrentVendorId { get; private set; }
public static int CurrentProductId { get; private set; }
public static DeviceContext CurrentContext { get; private set; } = CreateDefaultContext();
private static InputAction _anyInputAction;
private static int _lastDeviceId = -1;
private static float _lastInputTime = -Mathf.Infinity;
private static InputDeviceCategory _lastEmittedCategory = InputDeviceCategory.Keyboard;
private static float _lastSwitchTime = -Mathf.Infinity;
private static DeviceContext _lastEmittedContext = CreateDefaultContext();
private static bool _initialized;
public static event Action<InputDeviceCategory> OnDeviceChanged;
private static bool initialized = false;
public static event Action<DeviceContext> OnDeviceContextChanged;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void Initialize()
{
if (initialized) return;
initialized = true;
if (_initialized)
{
return;
}
CurrentCategory = InputDeviceCategory.Keyboard;
CurrentDeviceName = "";
_lastEmittedCategory = CurrentCategory; // 初始化同步
_initialized = true;
ApplyContext(CreateDefaultContext(), false);
_lastEmittedContext = CurrentContext;
_anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough);
_anyInputAction.AddBinding("<Keyboard>/anyKey");
_anyInputAction.AddBinding("<Mouse>/leftButton");
_anyInputAction.AddBinding("<Mouse>/rightButton");
_anyInputAction.AddBinding("<Mouse>/middleButton");
_anyInputAction.AddBinding("<Mouse>/scroll");
_anyInputAction.AddBinding("<Gamepad>/*");
_anyInputAction.AddBinding("<Joystick>/*");
_anyInputAction.performed += OnAnyInputPerformed;
_anyInputAction.Enable();
@ -54,142 +128,311 @@ public static class InputDeviceWatcher
}
#if UNITY_EDITOR
static void OnPlayModeStateChanged(PlayModeStateChange state)
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.ExitingPlayMode)
{
Dispose();
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
}
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
}
#endif
public static void Dispose()
{
if (!initialized) return;
CurrentCategory = InputDeviceCategory.Keyboard;
_anyInputAction.performed -= OnAnyInputPerformed;
_anyInputAction.Disable();
_anyInputAction.Dispose();
if (!_initialized)
{
return;
}
if (_anyInputAction != null)
{
_anyInputAction.performed -= OnAnyInputPerformed;
_anyInputAction.Disable();
_anyInputAction.Dispose();
_anyInputAction = null;
}
InputSystem.onDeviceChange -= OnDeviceChange;
ApplyContext(CreateDefaultContext(), false);
_lastEmittedContext = CurrentContext;
_lastSwitchTime = -Mathf.Infinity;
OnDeviceChanged = null;
initialized = false;
_lastEmittedCategory = InputDeviceCategory.Keyboard;
OnDeviceContextChanged = null;
_initialized = false;
}
private static void OnAnyInputPerformed(InputAction.CallbackContext ctx)
private static void OnAnyInputPerformed(InputAction.CallbackContext context)
{
if (ctx.control == null || ctx.control.device == null) return;
InputControl control = context.control;
if (!IsRelevantControl(control))
{
return;
}
var device = ctx.control.device;
DeviceContext deviceContext = BuildContext(control.device);
if (deviceContext.DeviceId == CurrentDeviceId)
{
return;
}
if (!IsRelevantDevice(device)) return;
int curId = device.deviceId;
float now = Time.realtimeSinceStartup;
if (deviceContext.Category == CurrentCategory && now - _lastSwitchTime < SameCategoryDebounceWindow)
{
return;
}
if (curId == _lastDeviceId) return;
if (DebounceWindow > 0f && (now - _lastInputTime) < DebounceWindow) return;
_lastInputTime = now;
_lastDeviceId = curId;
CurrentCategory = DetermineCategoryFromDevice(device);
CurrentDeviceName = device.displayName ?? $"Device_{curId}";
EmitChange();
_lastSwitchTime = now;
SetCurrentContext(deviceContext);
}
private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
{
if (change == InputDeviceChange.Removed || change == InputDeviceChange.Disconnected)
if (device == null)
{
if (device.deviceId == _lastDeviceId)
{
_lastDeviceId = -1;
_lastInputTime = -Mathf.Infinity;
CurrentDeviceName = "";
CurrentCategory = InputDeviceCategory.Keyboard;
EmitChange();
}
return;
}
switch (change)
{
case InputDeviceChange.Removed:
case InputDeviceChange.Disconnected:
if (device.deviceId == CurrentDeviceId)
{
PromoteFallbackDevice(device.deviceId);
}
break;
case InputDeviceChange.Reconnected:
case InputDeviceChange.Added:
if (CurrentDeviceId < 0 && IsRelevantDevice(device))
{
SetCurrentContext(BuildContext(device));
}
break;
}
}
// ------------------ 分类逻辑 --------------------
private static InputDeviceCategory DetermineCategoryFromDevice(InputDevice device)
private static void PromoteFallbackDevice(int removedDeviceId)
{
if (device == null) return InputDeviceCategory.Keyboard;
// 重要:鼠标不再被视为键盘类(避免鼠标触发时回退到 Keyboard
if (device is Keyboard) return InputDeviceCategory.Keyboard;
if (device is Mouse) return InputDeviceCategory.Other; // 明确忽略鼠标
if (IsGamepadLike(device)) return GetGamepadCategory(device);
for (int i = InputSystem.devices.Count - 1; i >= 0; i--)
{
InputDevice device = InputSystem.devices[i];
if (device == null || device.deviceId == removedDeviceId || !device.added || !IsRelevantDevice(device))
{
continue;
}
string combined = $"{device.description.interfaceName} {device.layout} {device.description.product} {device.description.manufacturer} {device.displayName}".ToLower();
SetCurrentContext(BuildContext(device));
return;
}
if (combined.Contains("xbox") || combined.Contains("xinput")) return InputDeviceCategory.Xbox;
if (combined.Contains("dualshock") || combined.Contains("dualsense") || combined.Contains("playstation")) return InputDeviceCategory.PlayStation;
return InputDeviceCategory.Other;
SetCurrentContext(CreateDefaultContext());
}
private static bool IsGamepadLike(InputDevice device)
private static void SetCurrentContext(DeviceContext context)
{
if (device is Gamepad) return true;
if (device is Joystick) return true;
bool categoryChanged = CurrentCategory != context.Category;
ApplyContext(context, true);
var layout = (device.layout ?? "").ToLower();
// 这里保留 controller/gamepad/joystick 的识别,但忽略 mouse/touch 等
if (layout.Contains("mouse") || layout.Contains("touch") || layout.Contains("pen")) return false;
return layout.Contains("gamepad") || layout.Contains("controller") || layout.Contains("joystick");
if (!_lastEmittedContext.Equals(context))
{
OnDeviceContextChanged?.Invoke(context);
if (categoryChanged)
{
OnDeviceChanged?.Invoke(context.Category);
}
_lastEmittedContext = context;
}
}
private static void ApplyContext(DeviceContext context, bool log)
{
CurrentContext = context;
CurrentCategory = context.Category;
CurrentDeviceId = context.DeviceId;
CurrentVendorId = context.VendorId;
CurrentProductId = context.ProductId;
CurrentDeviceName = context.DeviceName;
#if UNITY_EDITOR
if (log)
{
AlicizaX.Log.Info($"Input device -> {CurrentCategory} name={CurrentDeviceName} vid=0x{CurrentVendorId:X} pid=0x{CurrentProductId:X} id={CurrentDeviceId}");
}
#endif
}
private static DeviceContext BuildContext(InputDevice device)
{
if (device == null)
{
return CreateDefaultContext();
}
TryParseVendorProductIds(device.description.capabilities, out int vendorId, out int productId);
string deviceName = string.IsNullOrWhiteSpace(device.displayName) ? device.name : device.displayName;
return new DeviceContext(
DetermineCategoryFromDevice(device),
device.deviceId,
vendorId,
productId,
deviceName,
device.layout);
}
private static DeviceContext CreateDefaultContext()
{
return new DeviceContext(InputDeviceCategory.Keyboard, -1, 0, 0, DefaultKeyboardDeviceName, Keyboard.current != null ? Keyboard.current.layout : string.Empty);
}
private static InputDeviceCategory DetermineCategoryFromDevice(InputDevice device)
{
if (device == null)
{
return InputDeviceCategory.Keyboard;
}
if (device is Keyboard || device is Mouse)
{
return InputDeviceCategory.Keyboard;
}
if (IsGamepadLike(device))
{
return GetGamepadCategory(device);
}
string combined = CombineDeviceDescription(device);
if (ContainsIgnoreCase(combined, "xbox") || ContainsIgnoreCase(combined, "xinput"))
{
return InputDeviceCategory.Xbox;
}
if (ContainsIgnoreCase(combined, "dualshock")
|| ContainsIgnoreCase(combined, "dualsense")
|| ContainsIgnoreCase(combined, "playstation"))
{
return InputDeviceCategory.PlayStation;
}
return InputDeviceCategory.Other;
}
private static bool IsRelevantDevice(InputDevice device)
{
if (device == null) return false;
if (device is Keyboard) return true;
if (IsGamepadLike(device)) return true;
return false;
return device is Keyboard || device is Mouse || IsGamepadLike(device);
}
private static bool IsRelevantControl(InputControl control)
{
if (control == null || control.device == null || !IsRelevantDevice(control.device) || control.synthetic)
{
return false;
}
switch (control)
{
case ButtonControl button:
return button.IsPressed();
case StickControl stick:
return stick.ReadValue().sqrMagnitude >= StickActivationThreshold;
case Vector2Control vector2:
return vector2.ReadValue().sqrMagnitude >= StickActivationThreshold;
case AxisControl axis:
return Mathf.Abs(axis.ReadValue()) >= AxisActivationThreshold;
default:
return !control.noisy;
}
}
private static bool IsGamepadLike(InputDevice device)
{
if (device is Gamepad || device is Joystick)
{
return true;
}
string layout = device.layout ?? string.Empty;
if (ContainsIgnoreCase(layout, "Mouse")
|| ContainsIgnoreCase(layout, "Touch")
|| ContainsIgnoreCase(layout, "Pen"))
{
return false;
}
return ContainsIgnoreCase(layout, "Gamepad")
|| ContainsIgnoreCase(layout, "Controller")
|| ContainsIgnoreCase(layout, "Joystick");
}
private static InputDeviceCategory GetGamepadCategory(InputDevice device)
{
if (device == null) return InputDeviceCategory.Other;
var iface = (device.description.interfaceName ?? "").ToLower();
if (iface.Contains("xinput")) return InputDeviceCategory.Xbox;
if (TryParseVidPidFromCapabilities(device.description.capabilities, out int vendorId, out int _))
if (device == null)
{
if (vendorId == 0x045E || vendorId == 1118) return InputDeviceCategory.Xbox;
if (vendorId == 0x054C || vendorId == 1356) return InputDeviceCategory.PlayStation;
return InputDeviceCategory.Other;
}
string combined = $"{device.description.interfaceName} {device.layout} {device.description.product} {device.description.manufacturer} {device.displayName}".ToLower();
if (combined.Contains("xbox")) return InputDeviceCategory.Xbox;
if (combined.Contains("dualshock") || combined.Contains("playstation")) return InputDeviceCategory.PlayStation;
string interfaceName = device.description.interfaceName ?? string.Empty;
if (ContainsIgnoreCase(interfaceName, "xinput"))
{
return InputDeviceCategory.Xbox;
}
if (TryParseVendorProductIds(device.description.capabilities, out int vendorId, out _))
{
if (vendorId == 0x045E || vendorId == 1118)
{
return InputDeviceCategory.Xbox;
}
if (vendorId == 0x054C || vendorId == 1356)
{
return InputDeviceCategory.PlayStation;
}
}
string combined = CombineDeviceDescription(device);
if (ContainsIgnoreCase(combined, "xbox"))
{
return InputDeviceCategory.Xbox;
}
if (ContainsIgnoreCase(combined, "dualshock")
|| ContainsIgnoreCase(combined, "dualsense")
|| ContainsIgnoreCase(combined, "playstation"))
{
return InputDeviceCategory.PlayStation;
}
return InputDeviceCategory.Other;
}
// ------------------ VID/PID 解析 --------------------
private static bool TryParseVidPidFromCapabilities(string capabilities, out int vendorId, out int productId)
private static string CombineDeviceDescription(InputDevice device)
{
return string.Concat(
device.description.interfaceName, " ",
device.layout, " ",
device.description.product, " ",
device.description.manufacturer, " ",
device.displayName);
}
private static bool TryParseVendorProductIds(string capabilities, out int vendorId, out int productId)
{
vendorId = 0;
productId = 0;
if (string.IsNullOrEmpty(capabilities)) return false;
if (string.IsNullOrWhiteSpace(capabilities))
{
return false;
}
try
{
var decVendor = Regex.Match(capabilities, "\"vendorId\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase);
var decProduct = Regex.Match(capabilities, "\"productId\"\\s*:\\s*(\\d+)", RegexOptions.IgnoreCase);
if (decVendor.Success) int.TryParse(decVendor.Groups[1].Value, out vendorId);
if (decProduct.Success) int.TryParse(decProduct.Groups[1].Value, out productId);
DeviceCapabilityInfo info = JsonUtility.FromJson<DeviceCapabilityInfo>(capabilities);
vendorId = info.vendorId;
productId = info.productId;
return vendorId != 0 || productId != 0;
}
catch
@ -198,45 +441,8 @@ public static class InputDeviceWatcher
}
}
private static void EmitChange()
private static bool ContainsIgnoreCase(string source, string value)
{
if (CurrentCategory == _lastEmittedCategory)
{
return;
}
int vid = GetVendorId();
int pid = GetProductId();
#if UNITY_EDITOR
Log.Info($"输入设备变更 -> {CurrentCategory} 触发设备: {CurrentDeviceName} vid=0x{vid:X} pid=0x{pid:X}");
#endif
OnDeviceChanged?.Invoke(CurrentCategory);
_lastEmittedCategory = CurrentCategory;
}
private static int GetVendorId()
{
foreach (var d in InputSystem.devices)
{
if ((d.displayName ?? "") == CurrentDeviceName &&
TryParseVidPidFromCapabilities(d.description.capabilities, out int v, out int _))
return v;
}
return 0;
}
private static int GetProductId()
{
foreach (var d in InputSystem.devices)
{
if ((d.displayName ?? "") == CurrentDeviceName &&
TryParseVidPidFromCapabilities(d.description.capabilities, out int _, out int p))
return p;
}
return 0;
return !string.IsNullOrEmpty(source) && source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
}
}

View File

@ -0,0 +1,33 @@
using UnityEngine;
public abstract class InputGlyphBehaviourBase : MonoBehaviour
{
protected InputDeviceWatcher.InputDeviceCategory CurrentCategory { get; private set; }
protected virtual void OnEnable()
{
CurrentCategory = InputDeviceWatcher.CurrentCategory;
InputDeviceWatcher.OnDeviceChanged += HandleDeviceChanged;
InputBindingManager.BindingsChanged += HandleBindingsChanged;
RefreshGlyph();
}
protected virtual void OnDisable()
{
InputDeviceWatcher.OnDeviceChanged -= HandleDeviceChanged;
InputBindingManager.BindingsChanged -= HandleBindingsChanged;
}
private void HandleDeviceChanged(InputDeviceWatcher.InputDeviceCategory category)
{
CurrentCategory = category;
RefreshGlyph();
}
private void HandleBindingsChanged()
{
RefreshGlyph();
}
protected abstract void RefreshGlyph();
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a16393b81b47f1844a49d492284be475
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,10 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.U2D;
[Serializable]
public sealed class GlyphEntry
@ -17,205 +14,321 @@ public sealed class GlyphEntry
public sealed class DeviceGlyphTable
{
public string deviceName;
public Texture2D spriteSheetTexture;
public Sprite platformIcons;
public List<GlyphEntry> entries = new List<GlyphEntry>();
}
[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";
private const string DeviceKeyboard = "Keyboard";
private const string DeviceXbox = "Xbox";
private const string DevicePlayStation = "PlayStation";
private const string DeviceOther = "Other";
private static readonly InputDeviceWatcher.InputDeviceCategory[] KeyboardLookupOrder = { InputDeviceWatcher.InputDeviceCategory.Keyboard };
private static readonly InputDeviceWatcher.InputDeviceCategory[] XboxLookupOrder =
{
InputDeviceWatcher.InputDeviceCategory.Xbox,
InputDeviceWatcher.InputDeviceCategory.Other,
InputDeviceWatcher.InputDeviceCategory.Keyboard,
};
private static readonly InputDeviceWatcher.InputDeviceCategory[] PlayStationLookupOrder =
{
InputDeviceWatcher.InputDeviceCategory.PlayStation,
InputDeviceWatcher.InputDeviceCategory.Other,
InputDeviceWatcher.InputDeviceCategory.Keyboard,
};
private static readonly InputDeviceWatcher.InputDeviceCategory[] OtherLookupOrder =
{
InputDeviceWatcher.InputDeviceCategory.Other,
InputDeviceWatcher.InputDeviceCategory.Xbox,
InputDeviceWatcher.InputDeviceCategory.Keyboard,
};
public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>();
// 当 FindEntryByControlPath 传空 path 时返回的占位 sprite
public Sprite placeholderSprite;
// 用于更快查找的缓存
private Dictionary<string, DeviceGlyphTable> _tableCache;
private Dictionary<(string path, InputDeviceWatcher.InputDeviceCategory device), Sprite> _spriteCache;
private Dictionary<InputDeviceWatcher.InputDeviceCategory, Dictionary<string, Sprite>> _pathLookup;
/// <summary>
/// 启用时构建缓存
/// </summary>
private void OnEnable()
{
BuildCache();
}
/// <summary>
/// 构建表和精灵的查找缓存
/// </summary>
private void BuildCache()
#if UNITY_EDITOR
private void OnValidate()
{
if (_tableCache == null)
{
_tableCache = new Dictionary<string, DeviceGlyphTable>(tables.Count);
}
else
{
_tableCache.Clear();
}
if (_spriteCache == null)
{
_spriteCache = new Dictionary<(string, InputDeviceWatcher.InputDeviceCategory), Sprite>();
}
else
{
_spriteCache.Clear();
}
for (int i = 0; i < tables.Count; i++)
{
var table = tables[i];
if (table != null && !string.IsNullOrEmpty(table.deviceName))
{
_tableCache[table.deviceName.ToLowerInvariant()] = table;
}
}
BuildCache();
}
#endif
/// <summary>
/// 根据设备名称获取设备图标表
/// </summary>
/// <param name="deviceName">设备名称</param>
/// <returns>设备图标表</returns>
public DeviceGlyphTable GetTable(string deviceName)
{
if (string.IsNullOrEmpty(deviceName)) return null;
if (tables == null) return null;
// 确保缓存已构建
if (_tableCache == null || _tableCache.Count == 0)
if (string.IsNullOrWhiteSpace(deviceName) || tables == null)
{
BuildCache();
return null;
}
// 使用缓存进行 O(1) 查找
if (_tableCache.TryGetValue(deviceName.ToLowerInvariant(), out var table))
{
return table;
}
return null;
EnsureCache();
_tableCache.TryGetValue(deviceName.ToLowerInvariant(), out DeviceGlyphTable table);
return table;
}
/// <summary>
/// 获取平台图标
/// </summary>
/// <param name="device">设备类型</param>
/// <returns>平台图标 Sprite</returns>
public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device)
{
var table = GetTable(device);
if (table == null) return null;
return table.platformIcons;
}
/// <summary>
/// 根据设备类型获取设备图标表
/// </summary>
/// <param name="device">设备类型</param>
/// <returns>设备图标表</returns>
public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device)
{
// 使用常量避免字符串分配
string name;
switch (device)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
name = DEVICE_KEYBOARD;
break;
return GetTable(DeviceKeyboard);
case InputDeviceWatcher.InputDeviceCategory.Xbox:
name = DEVICE_XBOX;
break;
return GetTable(DeviceXbox);
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
name = DEVICE_PLAYSTATION;
break;
return GetTable(DevicePlayStation);
default:
name = DEVICE_XBOX;
break;
return GetTable(DeviceOther) ?? GetTable(DeviceXbox);
}
return GetTable(name);
}
/// <summary>
/// 根据控制路径和设备类型查找 Sprite
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <returns>找到的 Sprite未找到则返回占位 Sprite</returns>
public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device)
{
DeviceGlyphTable table = GetTable(device);
return table != null ? table.platformIcons : null;
}
public bool TryGetSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite)
{
EnsureCache();
string key = NormalizeControlPath(controlPath);
if (string.IsNullOrEmpty(key))
{
sprite = placeholderSprite;
return sprite != null;
}
InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device);
for (int i = 0; i < lookupOrder.Length; i++)
{
InputDeviceWatcher.InputDeviceCategory category = lookupOrder[i];
if (_pathLookup.TryGetValue(category, out Dictionary<string, Sprite> map) && map.TryGetValue(key, out sprite) && sprite != null)
{
return true;
}
}
sprite = placeholderSprite;
return sprite != null;
}
public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{
if (string.IsNullOrEmpty(controlPath))
{
return placeholderSprite;
}
// 首先检查缓存
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;
// 缓存结果(包括 null 结果以避免重复查找)
if (_spriteCache != null)
{
_spriteCache[cacheKey] = sprite;
}
return sprite;
return TryGetSprite(controlPath, device, out Sprite sprite) ? sprite : placeholderSprite;
}
/// <summary>
/// 根据控制路径和设备类型查找图标条目
/// </summary>
/// <param name="controlPath">控制路径</param>
/// <param name="device">设备类型</param>
/// <returns>找到的图标条目,未找到则返回 null</returns>
public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{
var t = GetTable(device);
if (t != null && t.entries != null)
if (!TryGetSprite(controlPath, device, out Sprite sprite) || sprite == null)
{
for (int i = 0; i < t.entries.Count; ++i)
return null;
}
InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device);
for (int i = 0; i < lookupOrder.Length; i++)
{
DeviceGlyphTable table = GetTable(lookupOrder[i]);
if (table == null || table.entries == null)
{
var e = t.entries[i];
if (e == null) continue;
if (e.action == null) continue;
continue;
}
var bindings = e.action.bindings;
int bindingCount = bindings.Count;
if (bindingCount <= 0) continue;
for (int j = 0; j < bindingCount; j++)
for (int j = 0; j < table.entries.Count; j++)
{
GlyphEntry entry = table.entries[j];
if (entry != null && entry.Sprite == sprite)
{
var b = bindings[j];
if (!string.IsNullOrEmpty(b.path) && string.Equals(b.path, controlPath, StringComparison.OrdinalIgnoreCase))
{
return e;
}
if (!string.IsNullOrEmpty(b.effectivePath) && string.Equals(b.effectivePath, controlPath, StringComparison.OrdinalIgnoreCase))
{
return e;
}
return entry;
}
}
}
return null;
}
private void EnsureCache()
{
if (_tableCache == null || _pathLookup == null)
{
BuildCache();
}
}
private void BuildCache()
{
_tableCache ??= new Dictionary<string, DeviceGlyphTable>(StringComparer.OrdinalIgnoreCase);
_tableCache.Clear();
_pathLookup ??= new Dictionary<InputDeviceWatcher.InputDeviceCategory, Dictionary<string, Sprite>>();
_pathLookup.Clear();
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Keyboard);
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Xbox);
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.PlayStation);
InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Other);
for (int i = 0; i < tables.Count; i++)
{
DeviceGlyphTable table = tables[i];
if (table == null || string.IsNullOrWhiteSpace(table.deviceName))
{
continue;
}
_tableCache[table.deviceName.ToLowerInvariant()] = table;
InputDeviceWatcher.InputDeviceCategory category = ParseCategory(table.deviceName);
Dictionary<string, Sprite> map = _pathLookup[category];
RegisterEntries(table, map);
}
}
private void InitializeLookup(InputDeviceWatcher.InputDeviceCategory category)
{
_pathLookup[category] = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase);
}
private void RegisterEntries(DeviceGlyphTable table, Dictionary<string, Sprite> map)
{
if (table.entries == null)
{
return;
}
for (int i = 0; i < table.entries.Count; i++)
{
GlyphEntry entry = table.entries[i];
if (entry == null || entry.Sprite == null || entry.action == null)
{
continue;
}
for (int j = 0; j < entry.action.bindings.Count; j++)
{
RegisterBinding(map, entry.action.bindings[j].path, entry.Sprite);
RegisterBinding(map, entry.action.bindings[j].effectivePath, entry.Sprite);
}
}
}
private void RegisterBinding(Dictionary<string, Sprite> map, string controlPath, Sprite sprite)
{
string key = NormalizeControlPath(controlPath);
if (string.IsNullOrEmpty(key) || map.ContainsKey(key))
{
return;
}
map[key] = sprite;
}
private static string NormalizeControlPath(string controlPath)
{
if (string.IsNullOrWhiteSpace(controlPath))
{
return string.Empty;
}
return CanonicalizeDeviceLayout(controlPath.Trim().ToLowerInvariant());
}
private static string CanonicalizeDeviceLayout(string controlPath)
{
int start = controlPath.IndexOf('<');
int end = controlPath.IndexOf('>');
if (start < 0 || end <= start + 1)
{
return controlPath;
}
string layout = controlPath.Substring(start + 1, end - start - 1);
string canonicalLayout = GetCanonicalLayout(layout);
if (string.Equals(layout, canonicalLayout, StringComparison.Ordinal))
{
return controlPath;
}
return controlPath.Substring(0, start + 1) + canonicalLayout + controlPath.Substring(end);
}
private static string GetCanonicalLayout(string layout)
{
if (string.IsNullOrEmpty(layout))
{
return string.Empty;
}
if (layout.IndexOf("keyboard", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "keyboard";
}
if (layout.IndexOf("mouse", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "mouse";
}
if (layout.IndexOf("joystick", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "joystick";
}
if (layout.IndexOf("gamepad", StringComparison.OrdinalIgnoreCase) >= 0
|| layout.IndexOf("controller", StringComparison.OrdinalIgnoreCase) >= 0
|| layout.IndexOf("xinput", StringComparison.OrdinalIgnoreCase) >= 0
|| layout.IndexOf("dualshock", StringComparison.OrdinalIgnoreCase) >= 0
|| layout.IndexOf("dualsense", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "gamepad";
}
return layout;
}
private static InputDeviceWatcher.InputDeviceCategory ParseCategory(string deviceName)
{
if (string.IsNullOrWhiteSpace(deviceName))
{
return InputDeviceWatcher.InputDeviceCategory.Other;
}
if (deviceName.Equals(DeviceKeyboard, StringComparison.OrdinalIgnoreCase))
{
return InputDeviceWatcher.InputDeviceCategory.Keyboard;
}
if (deviceName.Equals(DeviceXbox, StringComparison.OrdinalIgnoreCase))
{
return InputDeviceWatcher.InputDeviceCategory.Xbox;
}
if (deviceName.Equals(DevicePlayStation, StringComparison.OrdinalIgnoreCase))
{
return InputDeviceWatcher.InputDeviceCategory.PlayStation;
}
return InputDeviceWatcher.InputDeviceCategory.Other;
}
private static InputDeviceWatcher.InputDeviceCategory[] GetLookupOrder(InputDeviceWatcher.InputDeviceCategory device)
{
switch (device)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return KeyboardLookupOrder;
case InputDeviceWatcher.InputDeviceCategory.Xbox:
return XboxLookupOrder;
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return PlayStationLookupOrder;
default:
return OtherLookupOrder;
}
}
}

View File

@ -1,72 +1,62 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public sealed class InputGlyphImage : MonoBehaviour
public sealed class InputGlyphImage : InputGlyphBehaviourBase
{
[SerializeField] private InputActionReference actionReference;
[SerializeField] private string compositePartName;
[SerializeField] private Image targetImage;
[SerializeField] private bool hideIfMissing = false;
[SerializeField] private GameObject hideTargetObject;
private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private Sprite _cachedSprite;
/// <summary>
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable()
protected override void OnEnable()
{
if (targetImage == null) targetImage = GetComponent<Image>();
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged;
_cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard;
UpdatePrompt();
}
/// <summary>
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable()
{
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
}
/// <summary>
/// 设备类型变更时的回调,更新图标显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{
if (_cachedCategory != cat)
if (targetImage == null)
{
_cachedCategory = cat;
UpdatePrompt();
}
}
/// <summary>
/// 更新输入提示图标,并根据配置控制目标对象的显示/隐藏
/// </summary>
void UpdatePrompt()
{
if (actionReference == null || actionReference.action == null || targetImage == null) return;
// 使用缓存的设备类型,避免重复查询 CurrentCategory
if (GlyphService.TryGetUISpriteForActionPath(actionReference, "", _cachedCategory, out Sprite sprite))
{
if (_cachedSprite != sprite)
{
_cachedSprite = sprite;
targetImage.sprite = sprite;
}
targetImage = GetComponent<Image>();
}
if (hideTargetObject != null)
base.OnEnable();
}
protected override void RefreshGlyph()
{
if (actionReference == null || actionReference.action == null || targetImage == null)
{
bool shouldBeActive = sprite != null && !hideIfMissing;
if (hideTargetObject.activeSelf != shouldBeActive)
if (targetImage != null && _cachedSprite != null)
{
hideTargetObject.SetActive(shouldBeActive);
_cachedSprite = null;
targetImage.sprite = null;
}
ApplyVisibility(false);
return;
}
bool hasSprite = GlyphService.TryGetUISpriteForActionPath(actionReference, compositePartName, CurrentCategory, out Sprite sprite);
if (_cachedSprite != sprite)
{
_cachedSprite = sprite;
targetImage.sprite = sprite;
}
ApplyVisibility(hasSprite && sprite != null);
}
private void ApplyVisibility(bool hasSprite)
{
if (hideTargetObject == null)
{
return;
}
bool shouldBeActive = !hideIfMissing || hasSprite;
if (hideTargetObject.activeSelf != shouldBeActive)
{
hideTargetObject.SetActive(shouldBeActive);
}
}
}

View File

@ -1,75 +1,54 @@
using System;
using System.Linq;
using AlicizaX;
using UnityEngine;
using AlicizaX;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(TextMeshProUGUI))]
public sealed class InputGlyphText : MonoBehaviour
public sealed class InputGlyphText : InputGlyphBehaviourBase
{
[SerializeField] private InputActionReference actionReference;
private TMP_Text textField;
private string _oldText;
private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
[SerializeField] private string compositePartName;
private TMP_Text _textField;
private string _templateText;
private string _cachedFormattedText;
/// <summary>
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable()
protected override void OnEnable()
{
if (textField == null) textField = GetComponent<TMP_Text>();
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged;
_oldText = textField.text;
_cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard;
UpdatePrompt();
}
/// <summary>
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable()
{
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
}
/// <summary>
/// 设备类型变更时的回调,更新文本显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{
if (_cachedCategory != cat)
if (_textField == null)
{
_cachedCategory = cat;
UpdatePrompt();
_textField = GetComponent<TMP_Text>();
}
if (string.IsNullOrEmpty(_templateText) && _textField != null)
{
_templateText = _textField.text;
}
base.OnEnable();
}
/// <summary>
/// 更新文本中的输入提示标签,使用 TextMeshPro 的 sprite 标签或文本回退
/// </summary>
void UpdatePrompt()
protected override void RefreshGlyph()
{
if (actionReference == null || actionReference.action == null || textField == null) return;
// 使用缓存的设备类型,避免重复查询 CurrentCategory
if (GlyphService.TryGetTMPTagForActionPath(actionReference, "", _cachedCategory, out string tag, out string displayFallback))
if (actionReference == null || actionReference.action == null || _textField == null)
{
string formattedText = Utility.Text.Format(_oldText, tag);
if (_cachedFormattedText != formattedText)
{
_cachedFormattedText = formattedText;
textField.text = formattedText;
}
return;
}
string fallbackText = Utility.Text.Format(_oldText, displayFallback);
if (_cachedFormattedText != fallbackText)
string formattedText;
if (GlyphService.TryGetTMPTagForActionPath(actionReference, compositePartName, CurrentCategory, out string tag, out string displayFallback))
{
_cachedFormattedText = fallbackText;
textField.text = fallbackText;
formattedText = Utility.Text.Format(_templateText, tag);
}
else
{
formattedText = Utility.Text.Format(_templateText, displayFallback);
}
if (_cachedFormattedText != formattedText)
{
_cachedFormattedText = formattedText;
_textField.text = formattedText;
}
}
}

View File

@ -1,21 +1,17 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
[RequireComponent(typeof(UXButton))]
public sealed class InputGlyphUXButton : MonoBehaviour
public sealed class InputGlyphUXButton : InputGlyphBehaviourBase
{
[SerializeField] private UXButton button;
[SerializeField] private string compositePartName;
[SerializeField] private Image targetImage;
private InputActionReference _actionReference;
private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private Sprite _cachedSprite;
#if UNITY_EDITOR
/// <summary>
/// 编辑器验证,自动获取 UXButton 组件
/// </summary>
private void OnValidate()
{
if (button == null)
@ -25,54 +21,45 @@ public sealed class InputGlyphUXButton : MonoBehaviour
}
#endif
/// <summary>
/// 启用时初始化组件并订阅设备变更事件
/// </summary>
void OnEnable()
protected override void OnEnable()
{
if (button == null) button = GetComponent<UXButton>();
if (targetImage == null) targetImage = GetComponent<Image>();
_actionReference = button.HotKeyRefrence;
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged;
_cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard;
UpdatePrompt();
}
/// <summary>
/// 禁用时取消订阅设备变更事件
/// </summary>
void OnDisable()
{
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
}
/// <summary>
/// 设备类型变更时的回调,更新图标显示
/// </summary>
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{
if (_cachedCategory != cat)
if (button == null)
{
_cachedCategory = cat;
UpdatePrompt();
button = GetComponent<UXButton>();
}
if (targetImage == null)
{
targetImage = GetComponent<Image>();
}
base.OnEnable();
}
/// <summary>
/// 更新按钮的输入提示图标
/// </summary>
void UpdatePrompt()
protected override void RefreshGlyph()
{
if (_actionReference == null || _actionReference.action == null || targetImage == null) return;
// 使用缓存的设备类型,避免重复查询 CurrentCategory
if (GlyphService.TryGetUISpriteForActionPath(_actionReference, "", _cachedCategory, out Sprite sprite))
InputActionReference actionReference = button != null ? button.HotKeyRefrence : null;
if (actionReference == null || actionReference.action == null || targetImage == null)
{
if (_cachedSprite != sprite)
if (targetImage != null && _cachedSprite != null)
{
_cachedSprite = sprite;
targetImage.sprite = sprite;
_cachedSprite = null;
targetImage.sprite = null;
}
return;
}
bool hasSprite = GlyphService.TryGetUISpriteForActionPath(actionReference, compositePartName, CurrentCategory, out Sprite sprite);
if (!hasSprite)
{
sprite = null;
}
if (_cachedSprite != sprite)
{
_cachedSprite = sprite;
targetImage.sprite = sprite;
}
}
}

View File

@ -28,6 +28,7 @@ public class TestRebindScript : MonoBehaviour
{
if (btn != null) btn.onClick.AddListener(OnBtnClicked);
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged;
InputBindingManager.BindingsChanged += OnBindingsChanged;
UpdateBindingText();
if (InputBindingManager.Instance != null)
@ -47,6 +48,7 @@ public class TestRebindScript : MonoBehaviour
{
if (btn != null) btn.onClick.RemoveListener(OnBtnClicked);
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
InputBindingManager.BindingsChanged -= OnBindingsChanged;
if (InputBindingManager.Instance != null)
{
@ -120,6 +122,11 @@ public class TestRebindScript : MonoBehaviour
UpdateBindingText();
}
private void OnBindingsChanged()
{
UpdateBindingText();
}
/// <summary>
/// 获取当前的输入操作
/// </summary>
@ -207,6 +214,7 @@ public class TestRebindScript : MonoBehaviour
private void UpdateBindingText()
{
var action = GetAction();
var deviceCat = InputDeviceWatcher.CurrentCategory;
if (action == null)
{
bindKeyText.text = "<no action>";
@ -215,15 +223,12 @@ public class TestRebindScript : MonoBehaviour
}
bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action, compositePartName);
bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action, compositePartName, deviceCat);
try
{
var deviceCat = InputDeviceWatcher.CurrentCategory;
InputActionReference refr=default;
// string controlPath = GlyphService.GetBindingControlPath(action, compositePartName, deviceCat);
if ( GlyphService.TryGetUISpriteForActionPath(action,compositePartName, deviceCat, out Sprite sprite))
if (GlyphService.TryGetUISpriteForActionPath(action, compositePartName, deviceCat, out Sprite sprite))
{
if (targetImage != null) targetImage.sprite = sprite;
}

@ -1 +1 @@
Subproject commit c692b47a6249a4364c87ad2ebb6619ff7bf9f80d
Subproject commit b9c79e16a50080ba10b08a9b70bb513b36f51d1e

View File

@ -14,8 +14,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_PixelRect:
serializedVersion: 2
x: 0
y: 43
x: -4
y: 51
width: 1920
height: 997
m_ShowMode: 4
@ -23,7 +23,7 @@ MonoBehaviour:
m_RootView: {fileID: 4}
m_MinSize: {x: 875, y: 300}
m_MaxSize: {x: 10000, y: 10000}
m_Maximized: 1
m_Maximized: 0
--- !u!114 &2
MonoBehaviour:
m_ObjectHideFlags: 52
@ -34,23 +34,23 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0}
m_Name: GameView
m_Name: AnimatorControllerTool
m_EditorClassIdentifier:
m_Children: []
m_Position:
serializedVersion: 2
x: 0
y: 566
width: 497
width: 614
height: 381
m_MinSize: {x: 51, y: 71}
m_MinSize: {x: 101, y: 121}
m_MaxSize: {x: 4001, y: 4021}
m_ActualView: {fileID: 14}
m_ActualView: {fileID: 15}
m_Panes:
- {fileID: 14}
- {fileID: 15}
m_Selected: 0
m_LastSelected: 1
m_Selected: 1
m_LastSelected: 0
--- !u!114 &3
MonoBehaviour:
m_ObjectHideFlags: 52
@ -70,12 +70,12 @@ MonoBehaviour:
serializedVersion: 2
x: 0
y: 0
width: 497
width: 614
height: 947
m_MinSize: {x: 100, y: 100}
m_MaxSize: {x: 8096, y: 16192}
vertical: 1
controlID: 18
controlID: 398
draggingID: 0
--- !u!114 &4
MonoBehaviour:
@ -174,7 +174,7 @@ MonoBehaviour:
m_MinSize: {x: 400, y: 100}
m_MaxSize: {x: 32384, y: 16192}
vertical: 0
controlID: 17
controlID: 144
draggingID: 0
--- !u!114 &8
MonoBehaviour:
@ -193,10 +193,10 @@ MonoBehaviour:
serializedVersion: 2
x: 0
y: 0
width: 497
width: 614
height: 566
m_MinSize: {x: 200, y: 200}
m_MaxSize: {x: 4000, y: 4000}
m_MinSize: {x: 201, y: 221}
m_MaxSize: {x: 4001, y: 4021}
m_ActualView: {fileID: 16}
m_Panes:
- {fileID: 16}
@ -219,9 +219,9 @@ MonoBehaviour:
- {fileID: 11}
m_Position:
serializedVersion: 2
x: 497
x: 614
y: 0
width: 289
width: 363
height: 947
m_MinSize: {x: 100, y: 100}
m_MaxSize: {x: 8096, y: 16192}
@ -245,7 +245,7 @@ MonoBehaviour:
serializedVersion: 2
x: 0
y: 0
width: 289
width: 363
height: 409
m_MinSize: {x: 202, y: 221}
m_MaxSize: {x: 4002, y: 4021}
@ -271,7 +271,7 @@ MonoBehaviour:
serializedVersion: 2
x: 0
y: 409
width: 289
width: 363
height: 538
m_MinSize: {x: 102, y: 121}
m_MaxSize: {x: 4002, y: 4021}
@ -295,9 +295,9 @@ MonoBehaviour:
m_Children: []
m_Position:
serializedVersion: 2
x: 786
x: 977
y: 0
width: 474
width: 369
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: 1260
x: 1346
y: 0
width: 660
width: 574
height: 947
m_MinSize: {x: 276, y: 71}
m_MaxSize: {x: 4001, y: 4021}
@ -450,10 +450,10 @@ MonoBehaviour:
m_Tooltip:
m_Pos:
serializedVersion: 2
x: 0
y: 280
width: 441
height: 719
x: -4
y: 647
width: 613
height: 360
m_SerializedDataModeController:
m_DataMode: 0
m_PreferredDataMode: 0
@ -520,9 +520,9 @@ MonoBehaviour:
m_Tooltip:
m_Pos:
serializedVersion: 2
x: 0
y: 73
width: 496
x: -4
y: 81
width: 613
height: 545
m_SerializedDataModeController:
m_DataMode: 0
@ -1114,7 +1114,7 @@ MonoBehaviour:
m_Rotation:
m_Target: {x: -0.21037178, y: -0.10913931, z: 0.02363893, w: -0.97122556}
speed: 2
m_Value: {x: -0.2103712, y: -0.10913901, z: 0.023638865, w: -0.9712229}
m_Value: {x: -0.21037178, y: -0.10913931, z: 0.023638932, w: -0.9712256}
m_Size:
m_Target: 1.0281526
speed: 2
@ -1163,9 +1163,9 @@ MonoBehaviour:
m_Tooltip:
m_Pos:
serializedVersion: 2
x: 497
y: 73
width: 287
x: 610
y: 81
width: 361
height: 388
m_SerializedDataModeController:
m_DataMode: 0
@ -1180,9 +1180,9 @@ MonoBehaviour:
m_SceneHierarchy:
m_TreeViewState:
scrollPos: {x: 0, y: 0}
m_SelectedIDs: 521b0000
m_SelectedIDs: d6700000
m_LastClickedID: 0
m_ExpandedIDs: c058ffff
m_ExpandedIDs: 28fbffff
m_RenameOverlay:
m_UserAcceptedRename: 0
m_Name:
@ -1226,9 +1226,9 @@ MonoBehaviour:
m_Tooltip:
m_Pos:
serializedVersion: 2
x: 497
y: 482
width: 287
x: 610
y: 490
width: 361
height: 517
m_SerializedDataModeController:
m_DataMode: 0
@ -1260,9 +1260,9 @@ MonoBehaviour:
m_Tooltip:
m_Pos:
serializedVersion: 2
x: 786
y: 73
width: 472
x: 973
y: 81
width: 367
height: 926
m_SerializedDataModeController:
m_DataMode: 0
@ -1285,7 +1285,7 @@ MonoBehaviour:
m_SkipHidden: 0
m_SearchArea: 2
m_Folders:
- Packages/com.alicizax.unity.framework/Runtime/UI/Manager
- Assets/Plugins/UnityEditorDarkMode
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: 00000000c40a000044a8000046a8000048a800004aa800004ca800004ea8000050a8000052a8000054a8000056a8000058a800005aa800005ca800005ea8000060a8000062a8000064a8000066a8000068a800006aa800006ca800006ea8000070a8000072a8000074a8000076a8000078a800007aa800007ca800007ea8000080a8000082a8000084a8000086a8000088a800008aa800008ca800008ea8000090a8000092a8000094a8000096a8000098a800009aa800009ca800009ea80000a0a80000a2a80000a4a80000
m_ExpandedIDs: 000000007e0200008a0d0000786d00007a6d00007c6d00007e6d0000806d0000826d0000846d0000866d0000886d00008a6d00008c6d00008e6d0000906d0000926d0000946d0000966d0000986d00009a6d00009c6d00009e6d0000a06d0000a26d0000a46d0000a66d0000a86d0000aa6d0000ac6d0000ae6d0000b06d0000b26d0000b46d0000b66d0000b86d0000ba6d0000bc6d0000be6d0000c06d0000c26d0000c46d0000c66d0000c86d0000ca6d0000cc6d0000ce6d0000d06d0000d26d0000d46d0000d66d0000d86d0000da6d0000dc6d0000de6d0000e06d0000e26d0000e46d0000e66d0000e86d0000ea6d0000ec6d0000ee6d0000f06d0000f26d0000f46d0000
m_RenameOverlay:
m_UserAcceptedRename: 0
m_Name:
@ -1326,24 +1326,24 @@ MonoBehaviour:
m_Icon: {fileID: 0}
m_ResourceFile:
m_AssetTreeState:
scrollPos: {x: 0, y: 300}
scrollPos: {x: 0, y: 0}
m_SelectedIDs:
m_LastClickedID: 0
m_ExpandedIDs: ffffffff000000007e020000c40a000044a8000046a8000048a800004aa800004ca800004ea8000050a8000052a8000054a8000056a800005aa800005ca800005ea8000060a8000062a8000064a8000066a8000068a800006aa800006ca800006ea8000070a8000072a8000074a8000076a8000078a800007aa800007ca800007ea8000080a8000082a8000084a8000086a8000088a800008aa800008ca800008ea8000090a8000092a8000094a8000096a8000098a800009aa800009ca80000a0a80000b0a8000048a90000e2a90000e6a900000aaa00000eaa000010aa000012aa000024aa000036aa000040aa00005eaa000060aa000066ab000072ad000074ad000092ad000094ad0000ffffff7f
m_ExpandedIDs: ffffffff000000007e020000786d00007a6d00007c6d00007e6d0000806d0000826d0000846d0000866d0000886d00008a6d00008c6d00008e6d0000906d0000926d0000946d0000966d0000986d00009a6d00009c6d00009e6d0000a06d0000a26d0000a46d0000a66d0000a86d0000aa6d0000ac6d0000ae6d0000b06d0000b46d0000b66d0000b86d0000ba6d0000bc6d0000be6d0000c06d0000c26d0000c46d0000c66d0000c86d0000ca6d0000cc6d0000ce6d0000d06d0000d46d0000d66d0000d86d0000da6d0000dc6d0000de6d0000e06d0000e26d0000e66d0000e86d0000ea6d0000ec6d0000ee6d0000f06d0000f26d0000f46d0000f06f000008700000cc700000
m_RenameOverlay:
m_UserAcceptedRename: 0
m_Name: TimerModule
m_OriginalName: TimerModule
m_Name:
m_OriginalName:
m_EditFieldRect:
serializedVersion: 2
x: 0
y: 0
width: 0
height: 0
m_UserData: 5676
m_UserData: 0
m_IsWaitingForDelay: 0
m_IsRenaming: 0
m_OriginalEventType: 0
m_OriginalEventType: 11
m_IsRenamingFilename: 1
m_ClientGUIView: {fileID: 12}
m_SearchString:
@ -1405,9 +1405,9 @@ MonoBehaviour:
m_Tooltip:
m_Pos:
serializedVersion: 2
x: 1260
y: 73
width: 659
x: 1342
y: 81
width: 573
height: 926
m_SerializedDataModeController:
m_DataMode: 0