using AlicizaX.Editor; using AlicizaX.Timer.Runtime; using UnityEditor; using UnityEngine; namespace AlicizaX.Timer.Editor { [CustomEditor(typeof(TimerComponent))] internal sealed class TimerComponentInspector : GameFrameworkInspector { private const double UPDATE_INTERVAL = 0.02d; private const int DISPLAY_COUNT = 32; private const int MIN_INITIAL_CAPACITY = 256; private const int MAX_INITIAL_CAPACITY = 16384; private const int CAPACITY_STEP = 256; private readonly TimerDebugInfo[] _timerBuffer = new TimerDebugInfo[DISPLAY_COUNT]; private readonly TimerDebugInfo[] _staleBuffer = new TimerDebugInfo[DISPLAY_COUNT]; private double _lastUpdateTime; private SerializedProperty _initialCapacityProperty; public override void OnInspectorGUI() { base.OnInspectorGUI(); serializedObject.Update(); DrawConfiguration(); serializedObject.ApplyModifiedProperties(); DrawRuntimeDebugInfo(); RequestRuntimeRepaint(); } private void OnEnable() { _initialCapacityProperty = serializedObject.FindProperty("_initialCapacity"); } private void DrawConfiguration() { EditorGUILayout.Space(); EditorGUILayout.LabelField("Configuration", EditorStyles.boldLabel); int capacity = _initialCapacityProperty.intValue; int sliderValue = EditorGUILayout.IntSlider("Initial Capacity", capacity, MIN_INITIAL_CAPACITY, MAX_INITIAL_CAPACITY); sliderValue = AlignCapacity(sliderValue); if (sliderValue != capacity) { _initialCapacityProperty.intValue = sliderValue; } EditorGUILayout.HelpBox(Utility.Text.Format("Rounded by {0}. Runtime allocates timer pages during Awake/prewarm.", CAPACITY_STEP), MessageType.None); } private void DrawRuntimeDebugInfo() { if (!EditorApplication.isPlaying) { EditorGUILayout.HelpBox("Available during runtime only.", MessageType.Info); return; } if (!AppServices.TryGet(out ITimerService timerService)) { EditorGUILayout.HelpBox("Timer service is not initialized.", MessageType.Info); return; } if (!(timerService is ITimerDebugService timerDebugService)) { EditorGUILayout.HelpBox("Timer debug service is not available.", MessageType.Info); return; } timerDebugService.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount); EditorGUILayout.Space(); EditorGUILayout.LabelField("Runtime Debug", EditorStyles.boldLabel); DrawStatistic("Active Timers", activeCount); DrawStatistic("Pool Capacity", poolCapacity); DrawStatistic("Peak Active Count", peakActiveCount); DrawStatistic("Free Slots", freeCount); DrawUsageBar("Active Usage", activeCount, poolCapacity); DrawUsageBar("Peak Usage", peakActiveCount, poolCapacity); DrawTimerList(timerDebugService, activeCount); DrawStaleTimerList(timerDebugService, activeCount); } private void DrawTimerList(ITimerDebugService timerDebugService, int activeCount) { EditorGUILayout.Space(); EditorGUILayout.LabelField("Active Timer Sample", EditorStyles.boldLabel); if (activeCount <= 0) { EditorGUILayout.LabelField("No active timers."); return; } int timerCount = timerDebugService.GetAllTimers(_timerBuffer); if (activeCount > DISPLAY_COUNT) { EditorGUILayout.HelpBox(Utility.Text.Format("Showing first {0} timers of {1}.", timerCount, activeCount), MessageType.Info); } for (int i = 0; i < timerCount; i++) { DrawTimerInfo(ref _timerBuffer[i]); } } private void DrawStaleTimerList(ITimerDebugService timerDebugService, int activeCount) { if (activeCount <= 0) { return; } if (!(timerDebugService is ITimerEditorDebugService editorDebugService)) { return; } int staleCount = editorDebugService.GetStaleOneShotTimers(_staleBuffer); if (staleCount <= 0) { return; } EditorGUILayout.Space(); EditorGUILayout.HelpBox("Long-lived one-shot timers detected.", MessageType.Warning); for (int i = 0; i < staleCount; i++) { TimerDebugInfo info = _staleBuffer[i]; EditorGUILayout.LabelField(Utility.Text.Format("ID {0}", info.TimerHandle), Utility.Text.Format("Age {0:F1}s | Left {1:F2}s", info.Age, info.LeftTime)); } } private static void DrawTimerInfo(ref TimerDebugInfo info) { byte flags = info.Flags; string mode = (flags & TimerDebugFlags.Loop) != 0 ? "Loop" : "Once"; string scale = (flags & TimerDebugFlags.Unscaled) != 0 ? "Unscaled" : "Scaled"; string state = (flags & TimerDebugFlags.Running) != 0 ? "Running" : "Paused"; EditorGUILayout.LabelField( Utility.Text.Format("ID {0} | {1} | {2} | {3}", info.TimerHandle, mode, scale, state), Utility.Text.Format("Left {0:F2}s | Duration {1:F2}s", info.LeftTime, info.Duration)); } private static void DrawStatistic(string label, int value) { EditorGUILayout.LabelField(label, value.ToString(), EditorStyles.boldLabel); } private static void DrawUsageBar(string label, int value, int capacity) { float ratio = capacity > 0 ? (float)value / capacity : 0f; EditorGUILayout.Slider(label, ratio, 0f, 1f); } private static int AlignCapacity(int value) { int aligned = ((value + CAPACITY_STEP - 1) / CAPACITY_STEP) * CAPACITY_STEP; if (aligned < MIN_INITIAL_CAPACITY) { return MIN_INITIAL_CAPACITY; } return aligned > MAX_INITIAL_CAPACITY ? MAX_INITIAL_CAPACITY : aligned; } private void RequestRuntimeRepaint() { if (!EditorApplication.isPlaying) { return; } double currentTime = EditorApplication.timeSinceStartup; if (currentTime - _lastUpdateTime < UPDATE_INTERVAL) { return; } _lastUpdateTime = currentTime; Repaint(); } } }