diff --git a/Editor/Audio/AudioComponentInspector.cs b/Editor/Audio/AudioComponentInspector.cs index b0ec020..48aca34 100644 --- a/Editor/Audio/AudioComponentInspector.cs +++ b/Editor/Audio/AudioComponentInspector.cs @@ -13,7 +13,6 @@ namespace AlicizaX.Audio.Editor private readonly AudioCategoryDebugInfo _categoryInfo = new AudioCategoryDebugInfo(); private readonly AudioAgentDebugInfo _agentInfo = new AudioAgentDebugInfo(); private readonly AudioClipCacheDebugInfo _clipCacheInfo = new AudioClipCacheDebugInfo(); - private SerializedProperty m_InstanceRoot = null; private SerializedProperty m_AudioListener = null; private SerializedProperty m_AudioMixer = null; private SerializedProperty m_AudioGroupConfigs = null; @@ -31,7 +30,6 @@ namespace AlicizaX.Audio.Editor EditorGUI.BeginDisabledGroup(EditorApplication.isPlayingOrWillChangePlaymode); { - EditorGUILayout.PropertyField(m_InstanceRoot); EditorGUILayout.PropertyField(m_AudioListener, AudioListenerLabel); EditorGUILayout.PropertyField(m_AudioMixer); EditorGUILayout.PropertyField(m_AudioGroupConfigs, GroupConfigLabel); @@ -51,7 +49,6 @@ namespace AlicizaX.Audio.Editor private void OnEnable() { - m_InstanceRoot = serializedObject.FindProperty("m_InstanceRoot"); m_AudioListener = serializedObject.FindProperty("m_AudioListener"); m_AudioMixer = serializedObject.FindProperty("m_AudioMixer"); m_AudioGroupConfigs = serializedObject.FindProperty("m_AudioGroupConfigs"); diff --git a/Editor/Timer.meta b/Editor/Timer.meta new file mode 100644 index 0000000..98ce463 --- /dev/null +++ b/Editor/Timer.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d5eec662629e81c458ff9c776e1a9e29 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Timer/TimerComponentInspector.cs b/Editor/Timer/TimerComponentInspector.cs new file mode 100644 index 0000000..31a26b6 --- /dev/null +++ b/Editor/Timer/TimerComponentInspector.cs @@ -0,0 +1,169 @@ +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.001d; + private const int MAX_DISPLAY_COUNT = 20; + + private TimerDebugInfo[] _timerBuffer; +#if UNITY_EDITOR + private TimerDebugInfo[] _leakBuffer; +#endif + private double _lastUpdateTime; + private int _cachedActiveCount; + private int _cachedPoolCapacity; + private int _cachedPeakActiveCount; + private int _cachedFreeCount; + private string _cachedUsageText; + + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + + serializedObject.Update(); + serializedObject.ApplyModifiedProperties(); + + DrawRuntimeDebugInfo(); + + if (EditorApplication.isPlaying) + { + double currentTime = EditorApplication.timeSinceStartup; + if (currentTime - _lastUpdateTime >= UPDATE_INTERVAL) + { + _lastUpdateTime = currentTime; + Repaint(); + } + } + } + + 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 not ITimerServiceDebugView debugView) + { + return; + } + + debugView.GetStatistics(out _cachedActiveCount, out _cachedPoolCapacity, out _cachedPeakActiveCount, out _cachedFreeCount); + _cachedUsageText = _cachedPoolCapacity > 0 + ? Utility.Text.Format("{0:F1}%", (float)_cachedActiveCount / _cachedPoolCapacity * 100f) + : "0.0%"; + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Runtime Debug", EditorStyles.boldLabel); + EditorGUILayout.LabelField("Active Timers", _cachedActiveCount.ToString()); + EditorGUILayout.LabelField("Pool Capacity", _cachedPoolCapacity.ToString()); + EditorGUILayout.LabelField("Peak Active Count", _cachedPeakActiveCount.ToString()); + EditorGUILayout.LabelField("Free Slots", _cachedFreeCount.ToString()); + EditorGUILayout.LabelField("Pool Usage", _cachedUsageText); + + DrawTimerList(debugView, _cachedActiveCount); +#if UNITY_EDITOR + DrawLeakDetection(debugView, _cachedActiveCount); +#endif + } + + private void DrawTimerList(ITimerServiceDebugView debugView, int activeCount) + { + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Active Timers", EditorStyles.boldLabel); + + if (activeCount <= 0) + { + EditorGUILayout.LabelField("No active timers."); + return; + } + + EnsureTimerBuffer(activeCount); + int timerCount = debugView.GetAllTimers(_timerBuffer); + int displayCount = Mathf.Min(timerCount, MAX_DISPLAY_COUNT); + + if (displayCount < timerCount) + { + EditorGUILayout.HelpBox(Utility.Text.Format("Showing first {0} timers of {1}.", displayCount, timerCount), MessageType.Info); + } + + for (int i = 0; i < displayCount; i++) + { + TimerDebugInfo timer = _timerBuffer[i]; + string label = Utility.Text.Format( + "ID {0} | {1} | {2} | {3}", + timer.TimerId, + timer.IsLoop ? "Loop" : "Once", + timer.IsUnscaled ? "Unscaled" : "Scaled", + timer.IsRunning ? "Running" : "Paused"); + + string value = Utility.Text.Format( + "Left {0:F2}s | Duration {1:F2}s", + timer.LeftTime, + timer.Duration); + + EditorGUILayout.LabelField(label, value); + } + } + +#if UNITY_EDITOR + private void DrawLeakDetection(ITimerServiceDebugView debugView, int activeCount) + { + if (activeCount <= 0) + { + return; + } + + EnsureLeakBuffer(activeCount); + int staleCount = debugView.GetStaleOneShotTimers(_leakBuffer); + if (staleCount <= 0) + { + return; + } + + EditorGUILayout.Space(); + EditorGUILayout.LabelField(Utility.Text.Format("Stale One-Shot Timers ({0})", staleCount), EditorStyles.boldLabel); + EditorGUILayout.HelpBox("Non-loop timers older than 5 minutes. This may indicate long-delay tasks or paused timers.", MessageType.Warning); + + for (int i = 0; i < staleCount; i++) + { + TimerDebugInfo staleTimer = _leakBuffer[i]; + EditorGUILayout.LabelField( + Utility.Text.Format("ID {0}", staleTimer.TimerId), + Utility.Text.Format("Created {0:F1}s ago", staleTimer.CreationTime)); + } + } +#endif + + private void EnsureTimerBuffer(int count) + { + if (_timerBuffer == null || _timerBuffer.Length < count) + { + _timerBuffer = new TimerDebugInfo[count]; + } + } + +#if UNITY_EDITOR + private void EnsureLeakBuffer(int count) + { + if (_leakBuffer == null || _leakBuffer.Length < count) + { + _leakBuffer = new TimerDebugInfo[count]; + } + } +#endif + } +} diff --git a/Editor/Timer/TimerComponentInspector.cs.meta b/Editor/Timer/TimerComponentInspector.cs.meta new file mode 100644 index 0000000..c81241a --- /dev/null +++ b/Editor/Timer/TimerComponentInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02aa1426c358e87479136bd0f17f1f1c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Utility/Utility.Text.cs b/Runtime/ABase/Utility/Utility.Text.cs index 3a1bf0d..45a82d0 100644 --- a/Runtime/ABase/Utility/Utility.Text.cs +++ b/Runtime/ABase/Utility/Utility.Text.cs @@ -1,6 +1,4 @@ -#if ZSTRING_SUPPORT -using Cysharp.Text; -#endif +using Cysharp.Text; namespace AlicizaX { @@ -21,11 +19,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg); -#else - return string.Format(format, arg); -#endif } /// @@ -43,11 +38,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2); -#else - return string.Format(format, arg1, arg2); -#endif } /// @@ -67,11 +59,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3); -#else - return string.Format(format, arg1, arg2, arg3); -#endif } /// @@ -93,11 +82,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4); -#else - return string.Format(format, arg1, arg2, arg3, arg4); -#endif } /// @@ -121,11 +107,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5); -#endif } /// @@ -151,11 +134,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6); -#endif } /// @@ -183,11 +163,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7); -#endif } /// @@ -217,11 +194,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); -#endif } /// @@ -253,11 +227,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); -#endif } /// @@ -291,11 +262,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); -#endif } /// @@ -331,11 +299,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11); -#endif } /// @@ -373,11 +338,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12); -#endif } /// @@ -417,11 +379,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13); -#endif } /// @@ -463,11 +422,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14); -#endif } /// @@ -511,11 +467,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15); -#endif } /// @@ -561,11 +514,8 @@ namespace AlicizaX { throw new GameFrameworkException("Format is invalid."); } -#if ZSTRING_SUPPORT + return ZString.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16); -#else - return string.Format(format, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16); -#endif } } } diff --git a/Runtime/AlicizaX.Framework.Runtime.asmdef b/Runtime/AlicizaX.Framework.Runtime.asmdef index c18a518..849f7f7 100644 --- a/Runtime/AlicizaX.Framework.Runtime.asmdef +++ b/Runtime/AlicizaX.Framework.Runtime.asmdef @@ -21,11 +21,6 @@ "name": "com.alicizax.unity.animationflow", "expression": "", "define": "ALICIZAX_UI_ANIMATION_SUPPORT" - }, - { - "name": "com.alicizax.unity.cysharp.zstring", - "expression": "2.3.0", - "define": "ZSTRING_SUPPORT" } ], "noEngineReferences": false diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs new file mode 100644 index 0000000..2ba2bc6 --- /dev/null +++ b/Runtime/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AlicizaX.Framework.Editor")] diff --git a/Runtime/AssemblyInfo.cs.meta b/Runtime/AssemblyInfo.cs.meta new file mode 100644 index 0000000..4efa3b5 --- /dev/null +++ b/Runtime/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc79b52770c12304bbdd80020433741e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Audio/AudioAgent.cs b/Runtime/Audio/AudioAgent.cs index 6fae4a3..8c8f021 100644 --- a/Runtime/Audio/AudioAgent.cs +++ b/Runtime/Audio/AudioAgent.cs @@ -25,6 +25,8 @@ namespace AlicizaX.Audio.Runtime private ulong _handle; private float _baseVolume; private float _pitch; + private float _fadeInTimer; + private float _fadeInDuration; private float _fadeTimer; private float _fadeDuration; private float _startedAt; @@ -37,7 +39,7 @@ namespace AlicizaX.Audio.Runtime internal ulong Handle => _handle; internal int Generation => _generation; internal bool IsFree => _state == AudioAgentRuntimeState.Free; - internal bool IsPlayingState => _state == AudioAgentRuntimeState.Playing || _state == AudioAgentRuntimeState.Loading || _state == AudioAgentRuntimeState.Paused || _state == AudioAgentRuntimeState.FadingOut; + internal bool IsPlayingState => _state == AudioAgentRuntimeState.Playing || _state == AudioAgentRuntimeState.Loading || _state == AudioAgentRuntimeState.Paused || _state == AudioAgentRuntimeState.FadingIn || _state == AudioAgentRuntimeState.FadingOut; internal float StartedAt => _startedAt; internal void Initialize(AudioService service, AudioCategory category, int index, int globalIndex, AudioSourceObject sourceObject) @@ -67,6 +69,8 @@ namespace AlicizaX.Audio.Runtime _startedAt = Time.realtimeSinceStartup; _baseVolume = Mathf.Clamp01(request.Volume); _pitch = request.Pitch <= 0f ? 1f : request.Pitch; + _fadeInDuration = request.FadeInSeconds > 0f ? request.FadeInSeconds : 0f; + _fadeInTimer = 0f; _fadeDuration = request.FadeOutSeconds > 0f ? request.FadeOutSeconds : DefaultFadeOutSeconds; _loop = request.Loop; _spatial = request.Spatial || request.FollowTarget != null || request.UseWorldPosition; @@ -169,6 +173,22 @@ namespace AlicizaX.Audio.Runtime UpdateFollowTarget(); UpdateOcclusion(); + if (_state == AudioAgentRuntimeState.FadingIn) + { + _fadeInTimer += deltaTime; + if (_fadeInTimer >= _fadeInDuration) + { + _state = AudioAgentRuntimeState.Playing; + ApplyRuntimeVolume(1f); + } + else + { + float scale = _fadeInTimer / Mathf.Max(_fadeInDuration, MinFadeOutSeconds); + ApplyRuntimeVolume(scale); + } + return; + } + if (_state == AudioAgentRuntimeState.Playing) { if (!_loop && _source != null && !_source.isPlaying) @@ -252,9 +272,20 @@ namespace AlicizaX.Audio.Runtime _source.clip = clip; _source.loop = _loop; - ApplyRuntimeVolume(1f); + + if (_fadeInDuration > 0f) + { + _fadeInTimer = 0f; + _state = AudioAgentRuntimeState.FadingIn; + ApplyRuntimeVolume(0f); + } + else + { + _state = AudioAgentRuntimeState.Playing; + ApplyRuntimeVolume(1f); + } + _source.Play(); - _state = AudioAgentRuntimeState.Playing; } private void StopImmediate(bool notifyCategory) @@ -313,6 +344,8 @@ namespace AlicizaX.Audio.Runtime _handle = 0; _baseVolume = 1f; _pitch = 1f; + _fadeInTimer = 0f; + _fadeInDuration = 0f; _fadeTimer = 0f; _fadeDuration = 0f; _startedAt = 0f; diff --git a/Runtime/Audio/AudioAgentRuntimeState.cs b/Runtime/Audio/AudioAgentRuntimeState.cs index 3867817..ec6bb45 100644 --- a/Runtime/Audio/AudioAgentRuntimeState.cs +++ b/Runtime/Audio/AudioAgentRuntimeState.cs @@ -6,6 +6,7 @@ namespace AlicizaX.Audio.Runtime Loading = 1, Playing = 2, Paused = 3, - FadingOut = 4 + FadingIn = 4, + FadingOut = 5 } } diff --git a/Runtime/Audio/AudioCategory.cs b/Runtime/Audio/AudioCategory.cs index 52d4049..6ea0343 100644 --- a/Runtime/Audio/AudioCategory.cs +++ b/Runtime/Audio/AudioCategory.cs @@ -1,5 +1,6 @@ using UnityEngine; using UnityEngine.Audio; +using Cysharp.Text; namespace AlicizaX.Audio.Runtime { @@ -51,7 +52,7 @@ namespace AlicizaX.Audio.Runtime _enabled = !config.Mute; MixerGroup = ResolveMixerGroup(audioMixer, config); - InstanceRoot = new GameObject("Audio Category - " + Type).transform; + InstanceRoot = new GameObject(ZString.Concat("Audio Category - ", Type)).transform; InstanceRoot.SetParent(service.InstanceRoot, false); int capacity = config.AgentHelperCount; @@ -321,7 +322,7 @@ namespace AlicizaX.Audio.Runtime private static AudioMixerGroup ResolveMixerGroup(AudioMixer audioMixer, AudioGroupConfig config) { - AudioMixerGroup[] groups = audioMixer.FindMatchingGroups("Master/" + config.AudioType); + AudioMixerGroup[] groups = audioMixer.FindMatchingGroups(ZString.Concat("Master/", config.AudioType)); if (groups != null && groups.Length > 0) { return groups[0]; diff --git a/Runtime/Audio/AudioComponent.cs b/Runtime/Audio/AudioComponent.cs index 7078377..2930b95 100644 --- a/Runtime/Audio/AudioComponent.cs +++ b/Runtime/Audio/AudioComponent.cs @@ -9,7 +9,6 @@ namespace AlicizaX.Audio.Runtime public sealed class AudioComponent : MonoBehaviour { [SerializeField] private AudioMixer m_AudioMixer; - [SerializeField] private Transform m_InstanceRoot; [SerializeField] private AudioListener m_AudioListener; [SerializeField] private AudioGroupConfigCollection m_AudioGroupConfigs; @@ -22,10 +21,13 @@ namespace AlicizaX.Audio.Runtime private void Start() { - EnsureInstanceRoot(); - EnsureAudioMixer(); + if (m_AudioMixer == null) + { + throw new GameFrameworkException("AudioMixer is not assigned. Please assign an AudioMixer in the inspector."); + } + AudioGroupConfig[] configs = m_AudioGroupConfigs != null ? m_AudioGroupConfigs.GroupConfigs : null; - _audioService.Initialize(configs, m_InstanceRoot, m_AudioMixer); + _audioService.Initialize(configs, transform, m_AudioMixer); if (m_AudioListener != null) { _audioService.RegisterListener(m_AudioListener); @@ -47,26 +49,5 @@ namespace AlicizaX.Audio.Runtime _audioService.UnregisterListener(m_AudioListener); } } - - private void EnsureInstanceRoot() - { - if (m_InstanceRoot != null) - { - return; - } - - m_InstanceRoot = new GameObject("[AudioService Instances]").transform; - m_InstanceRoot.SetParent(transform, false); - m_InstanceRoot.localScale = Vector3.one; - } - - private void EnsureAudioMixer() - { - if (m_AudioMixer == null) - { - m_AudioMixer = Resources.Load("AudioMixer"); - } - } - } } diff --git a/Runtime/Audio/AudioPlayRequest.cs b/Runtime/Audio/AudioPlayRequest.cs index 402fbff..ba20c7b 100644 --- a/Runtime/Audio/AudioPlayRequest.cs +++ b/Runtime/Audio/AudioPlayRequest.cs @@ -22,6 +22,7 @@ namespace AlicizaX.Audio.Runtime public float MaxDistance; public AudioRolloffMode RolloffMode; public bool OverrideSpatialSettings; + public float FadeInSeconds; public float FadeOutSeconds; public AudioPlayRequest() @@ -157,6 +158,7 @@ namespace AlicizaX.Audio.Runtime MaxDistance = 500f; RolloffMode = AudioRolloffMode.Logarithmic; OverrideSpatialSettings = false; + FadeInSeconds = 0f; FadeOutSeconds = 0.15f; } diff --git a/Runtime/Audio/AudioService.cs b/Runtime/Audio/AudioService.cs index 81f3436..1947fdd 100644 --- a/Runtime/Audio/AudioService.cs +++ b/Runtime/Audio/AudioService.cs @@ -6,6 +6,7 @@ using AlicizaX.Resource.Runtime; using UnityEngine; using UnityEngine.Audio; using YooAsset; +using Cysharp.Text; namespace AlicizaX.Audio.Runtime { @@ -31,6 +32,7 @@ namespace AlicizaX.Audio.Runtime private readonly bool[] _categoryEnables = new bool[(int)AudioType.Max]; private readonly Dictionary _clipCache = new Dictionary(DefaultCacheCapacity, StringComparer.Ordinal); private readonly AudioSourceObject[][] _sourceObjects = new AudioSourceObject[(int)AudioType.Max][]; + private readonly Dictionary _configMap = new Dictionary((int)AudioType.Max); private IResourceService _resourceService; private IObjectPool _sourcePool; @@ -109,18 +111,41 @@ namespace AlicizaX.Audio.Runtime { Shutdown(false); - _resourceService = AppServices.Require(); - IObjectPoolService objectPoolService = AppServices.Require(); - _sourcePool = objectPoolService.HasObjectPool(SourcePoolName) - ? objectPoolService.GetObjectPool(SourcePoolName) - : objectPoolService.CreatePool(new ObjectPoolCreateOptions(SourcePoolName, false, 10f, int.MaxValue, float.MaxValue, 10)); - if (audioGroupConfigs == null || audioGroupConfigs.Length == 0) { throw new GameFrameworkException("AudioGroupConfig[] is invalid."); } _configs = audioGroupConfigs; + BuildConfigMap(); + + InitializeObjectPools(); + InitializeInstanceRoot(instanceRoot); + InitializeAudioMixer(audioMixer); + + if (_unityAudioDisabled) + { + _initialized = true; + return; + } + + InitializeHandleSystem(); + InitializeCategories(); + + _initialized = true; + } + + private void InitializeObjectPools() + { + _resourceService = AppServices.Require(); + IObjectPoolService objectPoolService = AppServices.Require(); + _sourcePool = objectPoolService.HasObjectPool(SourcePoolName) + ? objectPoolService.GetObjectPool(SourcePoolName) + : objectPoolService.CreatePool(new ObjectPoolCreateOptions(SourcePoolName, false, 10f, int.MaxValue, float.MaxValue, 10)); + } + + private void InitializeInstanceRoot(Transform instanceRoot) + { if (instanceRoot != null) { _instanceRoot = instanceRoot; @@ -138,20 +163,25 @@ namespace AlicizaX.Audio.Runtime { UnityEngine.Object.DontDestroyOnLoad(_instanceRoot.gameObject); } + } + private void InitializeAudioMixer(AudioMixer audioMixer) + { _unityAudioDisabled = IsUnityAudioDisabled(); if (_unityAudioDisabled) { - _initialized = true; return; } - _audioMixer = audioMixer != null ? audioMixer : Resources.Load("AudioMixer"); + _audioMixer = audioMixer; if (_audioMixer == null) { - throw new GameFrameworkException("AudioMixer is invalid."); + throw new GameFrameworkException("AudioMixer is invalid. Please provide a valid AudioMixer."); } + } + private void InitializeHandleSystem() + { int totalAgentCount = 0; for (int i = 0; i < (int)AudioType.Max; i++) { @@ -169,7 +199,10 @@ namespace AlicizaX.Audio.Runtime MemoryPool.Add(totalAgentCount); MemoryPool.Add(totalAgentCount); MemoryPool.Add(_clipCacheCapacity); + } + private void InitializeCategories() + { int globalIndexOffset = 0; for (int i = 0; i < (int)AudioType.Max; i++) { @@ -186,8 +219,6 @@ namespace AlicizaX.Audio.Runtime globalIndexOffset += config.AgentHelperCount; ApplyMixerVolume(config, _categoryVolumes[i], _categoryEnables[i]); } - - _initialized = true; } public void Restart() @@ -439,12 +470,17 @@ namespace AlicizaX.Audio.Runtime } public void ClearCache() + { + ClearCache(false); + } + + private void ClearCache(bool force) { AudioClipCacheEntry entry = _allHead; while (entry != null) { AudioClipCacheEntry next = entry.AllNext; - if (entry.RefCount <= 0 && !entry.Loading && entry.PendingHead == null) + if (force || (entry.RefCount <= 0 && !entry.Loading && entry.PendingHead == null)) { RemoveClipEntry(entry); } @@ -1073,7 +1109,7 @@ namespace AlicizaX.Audio.Runtime private static string BuildSourceName(int typeIndex, int index) { - return "AudioSource_" + typeIndex + "_" + index; + return ZString.Concat("AudioSource_", typeIndex, "_", index); } private AudioGroupConfig GetConfig(AudioType type) @@ -1087,23 +1123,27 @@ namespace AlicizaX.Audio.Runtime return FindConfig(type); } - private AudioGroupConfig FindConfig(AudioType type) + private void BuildConfigMap() { + _configMap.Clear(); if (_configs == null) { - return null; + return; } for (int i = 0; i < _configs.Length; i++) { AudioGroupConfig config = _configs[i]; - if (config != null && config.AudioType == type) + if (config != null) { - return config; + _configMap[config.AudioType] = config; } } + } - return null; + private AudioGroupConfig FindConfig(AudioType type) + { + return _configMap.TryGetValue(type, out AudioGroupConfig config) ? config : null; } private int CountActiveAgents() @@ -1143,7 +1183,7 @@ namespace AlicizaX.Audio.Runtime } } - ClearCache(); + ClearCache(true); if (_sourcePool != null) { _sourcePool.ReleaseAllUnused(); @@ -1153,6 +1193,7 @@ namespace AlicizaX.Audio.Runtime Array.Clear(_handleGenerations, 0, _handleGenerations.Length); _handleAgents = Array.Empty(); _handleGenerations = Array.Empty(); + _configMap.Clear(); _resourceService = null; _sourcePool = null; _audioMixer = null; diff --git a/Runtime/Audio/Resources/AudioMixer.mixer b/Runtime/Audio/Resources/AudioMixer.mixer index 284455c..c5fde44 100644 --- a/Runtime/Audio/Resources/AudioMixer.mixer +++ b/Runtime/Audio/Resources/AudioMixer.mixer @@ -61,6 +61,25 @@ AudioMixerGroupController: m_Mute: 0 m_Solo: 0 m_BypassEffects: 0 +--- !u!243 &-6728185375074428080 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Ambient - 2 + m_AudioMixer: {fileID: 24100000} + m_GroupID: df8234ed398782540a4972841dfd6079 + m_Children: [] + m_Volume: 9d25aae74848d51418de553067dd0f0b + m_Pitch: 2018eb0e2888cff44bf523ce3d06fd0b + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: -2815600738436590526} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 --- !u!243 &-6280614258348125054 AudioMixerGroupController: m_ObjectHideFlags: 0 @@ -80,6 +99,25 @@ AudioMixerGroupController: m_Mute: 0 m_Solo: 0 m_BypassEffects: 0 +--- !u!243 &-5751393387862637061 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Ambient - 1 + m_AudioMixer: {fileID: 24100000} + m_GroupID: 221f1da294156a3418168c39f16ae139 + m_Children: [] + m_Volume: 3a4c4abef80a7d74090c4558f2efbe8c + m_Pitch: bade8e01a57417d49bccd39245648de8 + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: 588078238856344238} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 --- !u!244 &-4958177229083455073 AudioMixerEffectController: m_ObjectHideFlags: 3 @@ -94,6 +132,25 @@ AudioMixerEffectController: m_SendTarget: {fileID: 0} m_EnableWetMix: 0 m_Bypass: 0 +--- !u!243 &-4470511876276122591 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Ambient - 3 + m_AudioMixer: {fileID: 24100000} + m_GroupID: 62ab3b1c1e1517f4892acd6bbf63325e + m_Children: [] + m_Volume: ae513375e8726984d88a822dcc805a47 + m_Pitch: c670d6f9426d69c439bed5c1581cf095 + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: 4372151859782775079} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 --- !u!243 &-4372808504093502661 AudioMixerGroupController: m_ObjectHideFlags: 0 @@ -186,6 +243,43 @@ AudioMixerGroupController: m_Mute: 0 m_Solo: 0 m_BypassEffects: 0 +--- !u!243 &-3339031547535134654 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Ambient + m_AudioMixer: {fileID: 24100000} + m_GroupID: eea06a873d29c174d9b93c8a87f28f29 + m_Children: + - {fileID: -1635570523224726303} + - {fileID: -5751393387862637061} + - {fileID: -6728185375074428080} + - {fileID: -4470511876276122591} + m_Volume: 41dabee1ece434b46b30b1bf6008f4eb + m_Pitch: fef56cb47e7fcb845ae02cd29b60365e + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: -1709768828366853691} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 +--- !u!244 &-2815600738436590526 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Attenuation + m_EffectID: 89fdfdacd88dd56428258e46d756e1ea + m_EffectName: Attenuation + m_MixLevel: c67a0995f8d7b524ba49eeeca5216f58 + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 --- !u!243 &-2659745067392564156 AudioMixerGroupController: m_ObjectHideFlags: 0 @@ -219,6 +313,20 @@ AudioMixerEffectController: m_SendTarget: {fileID: 0} m_EnableWetMix: 0 m_Bypass: 0 +--- !u!244 &-1709768828366853691 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_EffectID: 65bb23c9d872c5347a34674e11b855f2 + m_EffectName: Attenuation + m_MixLevel: eeea8e411d955da479365f63ad7834fd + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 --- !u!243 &-1649243360580130678 AudioMixerGroupController: m_ObjectHideFlags: 0 @@ -238,6 +346,39 @@ AudioMixerGroupController: m_Mute: 0 m_Solo: 0 m_BypassEffects: 0 +--- !u!243 &-1635570523224726303 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Ambient - 0 + m_AudioMixer: {fileID: 24100000} + m_GroupID: dfba3d60853b22649ac844b8e925e0f6 + m_Children: [] + m_Volume: d292610df4aee5543b1124a0f1048e76 + m_Pitch: 2d28f5f5a88e080488060323fd5d5d55 + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: -1114540582412186105} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 +--- !u!244 &-1114540582412186105 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_EffectID: 10f22744f37e0a143adcb17b39acf14a + m_EffectName: Attenuation + m_MixLevel: c4b8d05d2d796eb46b9288ae6451e044 + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 --- !u!244 &-998299258853400712 AudioMixerEffectController: m_ObjectHideFlags: 3 @@ -359,6 +500,11 @@ AudioMixerController: - e012b6d2e0501df43a88eb6beff8ae07 - e84c25a476798ea43a2f6de217af7dba - 98657376d4096a947953ee04d82830c1 + - eea06a873d29c174d9b93c8a87f28f29 + - dfba3d60853b22649ac844b8e925e0f6 + - 221f1da294156a3418168c39f16ae139 + - df8234ed398782540a4972841dfd6079 + - 62ab3b1c1e1517f4892acd6bbf63325e name: View m_CurrentViewIndex: 0 m_TargetSnapshot: {fileID: 24500006} @@ -376,6 +522,7 @@ AudioMixerGroupController: - {fileID: 7235523536312936115} - {fileID: 7185772616558441635} - {fileID: -3395020342500439107} + - {fileID: -3339031547535134654} m_Volume: ba83e724007d7e9459f157db3a54a741 m_Pitch: a2d2b77391464bb4887f0bcd3835015b m_Send: 00000000000000000000000000000000 @@ -448,6 +595,20 @@ AudioMixerEffectController: m_SendTarget: {fileID: 0} m_EnableWetMix: 0 m_Bypass: 0 +--- !u!244 &588078238856344238 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Attenuation + m_EffectID: fe69f682bd0b54a42a947db1c2b9c800 + m_EffectName: Attenuation + m_MixLevel: ca7defaf1a72196439b28b45a8ca0dc6 + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 --- !u!244 &1413273517213151576 AudioMixerEffectController: m_ObjectHideFlags: 3 @@ -533,6 +694,20 @@ AudioMixerGroupController: m_Mute: 0 m_Solo: 0 m_BypassEffects: 0 +--- !u!244 &4372151859782775079 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Attenuation + m_EffectID: 3519e333bc2088840ad9986fa50cde94 + m_EffectName: Attenuation + m_MixLevel: 04a4d4239cebc9348a0e8e88df14e74c + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 --- !u!244 &5734415080786067514 AudioMixerEffectController: m_ObjectHideFlags: 3 diff --git a/Runtime/Debugger/DebuggerComponent.TimerInformationWindow.cs b/Runtime/Debugger/DebuggerComponent.TimerInformationWindow.cs new file mode 100644 index 0000000..6dbaba9 --- /dev/null +++ b/Runtime/Debugger/DebuggerComponent.TimerInformationWindow.cs @@ -0,0 +1,200 @@ +using AlicizaX.Timer.Runtime; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AlicizaX.Debugger.Runtime +{ + public sealed partial class DebuggerComponent + { + private sealed class TimerInformationWindow : ScrollableDebuggerWindowBase + { + private const int MAX_DISPLAY_COUNT = 50; + + private struct RowView + { + public VisualElement Root; + public Label Title; + public Label Value; + } + + private ITimerServiceDebugView m_TimerDebugView; + private TimerDebugInfo[] m_TimerInfos; + private Label m_SectionTitleLabel; + private Label m_ActiveCountLabel; + private Label m_PoolCapacityLabel; + private Label m_PeakCountLabel; + private Label m_FreeCountLabel; + private Label m_UsageLabel; + private Label m_WarningLabel; + private RowView m_EmptyRow; + private readonly RowView[] m_TimerRows = new RowView[MAX_DISPLAY_COUNT]; + + public override void Initialize(params object[] args) + { + m_TimerDebugView = AppServices.Require() as ITimerServiceDebugView; + } + + protected override void BuildWindow(VisualElement root) + { + if (m_TimerDebugView == null) + { + return; + } + + root.Add(CreateActionButton("Refresh", RefreshContent, DebuggerTheme.ButtonSurfaceActive, DebuggerTheme.PrimaryText)); + + VisualElement overview = CreateSection("Timer Pool Overview", out VisualElement overviewCard); + m_ActiveCountLabel = AddTextRow(overviewCard, "Active Timer Count").Value; + m_PoolCapacityLabel = AddTextRow(overviewCard, "Pool Capacity").Value; + m_PeakCountLabel = AddTextRow(overviewCard, "Peak Active Count").Value; + m_FreeCountLabel = AddTextRow(overviewCard, "Free Count").Value; + m_UsageLabel = AddTextRow(overviewCard, "Pool Usage").Value; + root.Add(overview); + + VisualElement section = CreateSection("Active Timers", out VisualElement timerCard); + m_SectionTitleLabel = section.ElementAt(0) as Label; + m_WarningLabel = new Label(); + m_WarningLabel.style.color = new Color(1f, 0.5f, 0f); + m_WarningLabel.style.display = DisplayStyle.None; + m_WarningLabel.style.marginBottom = 4f; + timerCard.Add(m_WarningLabel); + + m_EmptyRow = AddTextRow(timerCard, string.Empty); + m_EmptyRow.Root.style.display = DisplayStyle.None; + + for (int i = 0; i < MAX_DISPLAY_COUNT; i++) + { + m_TimerRows[i] = AddTextRow(timerCard, string.Empty); + m_TimerRows[i].Root.style.display = DisplayStyle.None; + } + + root.Add(section); + RefreshContent(); + } + + private void RefreshContent() + { + if (m_TimerDebugView == null) + { + return; + } + + m_TimerDebugView.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount); + float poolUsage = poolCapacity > 0 ? (float)activeCount / poolCapacity : 0f; + + m_ActiveCountLabel.text = activeCount.ToString(); + m_PoolCapacityLabel.text = poolCapacity.ToString(); + m_PeakCountLabel.text = peakActiveCount.ToString(); + m_FreeCountLabel.text = freeCount.ToString(); + m_UsageLabel.text = Utility.Text.Format("{0:P1}", poolUsage); + + if (activeCount <= 0) + { + m_SectionTitleLabel.text = "Active Timers"; + m_WarningLabel.style.display = DisplayStyle.None; + m_EmptyRow.Root.style.display = DisplayStyle.Flex; + m_EmptyRow.Title.text = "Status"; + m_EmptyRow.Value.text = "No active timers"; + SetTimerRowsVisible(0); + return; + } + + EnsureTimerInfoBuffer(activeCount); + int timerCount = m_TimerDebugView.GetAllTimers(m_TimerInfos); + int displayCount = timerCount > MAX_DISPLAY_COUNT ? MAX_DISPLAY_COUNT : timerCount; + + m_SectionTitleLabel.text = Utility.Text.Format("Active Timers ({0})", timerCount); + m_EmptyRow.Root.style.display = DisplayStyle.None; + + if (displayCount < timerCount) + { + m_WarningLabel.text = Utility.Text.Format("Showing first {0} timers of {1}.", displayCount, timerCount); + m_WarningLabel.style.display = DisplayStyle.Flex; + } + else + { + m_WarningLabel.style.display = DisplayStyle.None; + } + + for (int i = 0; i < displayCount; i++) + { + ref RowView row = ref m_TimerRows[i]; + TimerDebugInfo info = m_TimerInfos[i]; + row.Title.text = Utility.Text.Format("Timer #{0}", info.TimerId); + row.Value.text = Utility.Text.Format( + "{0} | {1} | {2} | Remaining: {3:F2}s | Duration: {4:F2}s", + info.IsLoop ? "Loop" : "Once", + info.IsRunning ? "Running" : "Paused", + info.IsUnscaled ? "Unscaled" : "Scaled", + info.LeftTime, + info.Duration); + row.Root.style.display = DisplayStyle.Flex; + } + + SetTimerRowsVisible(displayCount); + } + + private void SetTimerRowsVisible(int visibleCount) + { + for (int i = 0; i < MAX_DISPLAY_COUNT; i++) + { + m_TimerRows[i].Root.style.display = i < visibleCount ? DisplayStyle.Flex : DisplayStyle.None; + } + } + + private RowView AddTextRow(VisualElement parent, string title) + { + float scale = DebuggerComponent.Instance != null ? DebuggerComponent.Instance.GetUiScale() : 1f; + VisualElement row = new VisualElement(); + row.style.flexDirection = FlexDirection.Row; + row.style.alignItems = Align.Center; + row.style.minHeight = 36f * scale; + row.style.marginBottom = 4f * scale; + + Label titleLabel = new Label(title); + titleLabel.style.minWidth = 280f * scale; + titleLabel.style.maxWidth = 280f * scale; + titleLabel.style.color = DebuggerTheme.SecondaryText; + titleLabel.style.fontSize = 18f * scale; + titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold; + titleLabel.style.flexShrink = 0f; + titleLabel.style.whiteSpace = WhiteSpace.Normal; + + Label valueLabel = new Label(); + valueLabel.style.flexGrow = 1f; + valueLabel.style.color = DebuggerTheme.PrimaryText; + valueLabel.style.fontSize = 18f * scale; + valueLabel.style.whiteSpace = WhiteSpace.Normal; + + row.Add(titleLabel); + row.Add(valueLabel); + parent.Add(row); + + RowView view; + view.Root = row; + view.Title = titleLabel; + view.Value = valueLabel; + return view; + } + + private int EnsureTimerInfoBuffer(int count) + { + if (count <= 0) + { + if (m_TimerInfos == null || m_TimerInfos.Length == 0) + { + m_TimerInfos = new TimerDebugInfo[1]; + } + return 0; + } + + if (m_TimerInfos == null || m_TimerInfos.Length < count) + { + m_TimerInfos = new TimerDebugInfo[count]; + } + + return count; + } + } + } +} diff --git a/Runtime/Debugger/DebuggerComponent.TimerInformationWindow.cs.meta b/Runtime/Debugger/DebuggerComponent.TimerInformationWindow.cs.meta new file mode 100644 index 0000000..0401717 --- /dev/null +++ b/Runtime/Debugger/DebuggerComponent.TimerInformationWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8eac2550d41d14641b41a5c5523c4fde +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Debugger/DebuggerComponent.cs b/Runtime/Debugger/DebuggerComponent.cs index 975d27f..f05f460 100644 --- a/Runtime/Debugger/DebuggerComponent.cs +++ b/Runtime/Debugger/DebuggerComponent.cs @@ -93,6 +93,7 @@ namespace AlicizaX.Debugger.Runtime private ObjectPoolInformationWindow m_ObjectPoolInformationWindow = new ObjectPoolInformationWindow(); private ReferencePoolInformationWindow m_ReferencePoolInformationWindow = new ReferencePoolInformationWindow(); private AudioInformationWindow m_AudioInformationWindow = new AudioInformationWindow(); + private TimerInformationWindow m_TimerInformationWindow = new TimerInformationWindow(); private SettingsWindow m_SettingsWindow = new SettingsWindow(); private FpsCounter m_FpsCounter; @@ -502,6 +503,7 @@ namespace AlicizaX.Debugger.Runtime RegisterDebuggerWindow("Profiler/Object Pool", m_ObjectPoolInformationWindow); RegisterDebuggerWindow("Profiler/Reference Pool", m_ReferencePoolInformationWindow); RegisterDebuggerWindow("Profiler/Audio", m_AudioInformationWindow); + RegisterDebuggerWindow("Profiler/Timer", m_TimerInformationWindow); RegisterDebuggerWindow("Other/Settings", m_SettingsWindow); } diff --git a/Runtime/GameApp.cs b/Runtime/GameApp.cs index 47c68b4..854c88d 100644 --- a/Runtime/GameApp.cs +++ b/Runtime/GameApp.cs @@ -4,6 +4,7 @@ using AlicizaX.Localization.Runtime; using AlicizaX.ObjectPool; using AlicizaX.Resource.Runtime; using AlicizaX.Scene.Runtime; +using AlicizaX.Timer.Runtime; using AlicizaX.UI.Runtime; public static partial class GameApp diff --git a/Runtime/Timer/ITimerService.cs b/Runtime/Timer/ITimerService.cs index 509c20e..34605fc 100644 --- a/Runtime/Timer/ITimerService.cs +++ b/Runtime/Timer/ITimerService.cs @@ -1,19 +1,17 @@ using System; -namespace AlicizaX +namespace AlicizaX.Timer.Runtime { [UnityEngine.Scripting.Preserve] public interface ITimerService : IService { - int AddTimer(TimerHandler callback, float time, bool isLoop = false, bool isUnscaled = false, params object[] args); int AddTimer(TimerHandlerNoArgs callback, float time, bool isLoop = false, bool isUnscaled = false); - int AddTimer(Action callback, T arg, float time, bool isLoop = false, bool isUnscaled = false); + int AddTimer(Action callback, T arg, float time, bool isLoop = false, bool isUnscaled = false) where T : class; void Stop(int timerId); void Resume(int timerId); bool IsRunning(int timerId); float GetLeftTime(int timerId); void Restart(int timerId); void RemoveTimer(int timerId); - void RemoveAllTimer(); } } diff --git a/Runtime/Timer/ITimerServiceDebugView.cs b/Runtime/Timer/ITimerServiceDebugView.cs new file mode 100644 index 0000000..aab7b29 --- /dev/null +++ b/Runtime/Timer/ITimerServiceDebugView.cs @@ -0,0 +1,27 @@ +namespace AlicizaX.Timer.Runtime +{ + internal struct TimerDebugInfo + { + public int TimerId; + public float LeftTime; + public float Duration; + public bool IsLoop; + public bool IsRunning; + public bool IsUnscaled; + +#if UNITY_EDITOR + public float CreationTime; +#endif + } + + internal interface ITimerServiceDebugView + { + int GetAllTimers(TimerDebugInfo[] results); + + void GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount); + +#if UNITY_EDITOR + int GetStaleOneShotTimers(TimerDebugInfo[] results); +#endif + } +} diff --git a/Runtime/Timer/ITimerServiceDebugView.cs.meta b/Runtime/Timer/ITimerServiceDebugView.cs.meta new file mode 100644 index 0000000..396fbe4 --- /dev/null +++ b/Runtime/Timer/ITimerServiceDebugView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b7cf36ada40b944f8c506e3cd8ddd12 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Timer/TimerComponent.cs b/Runtime/Timer/TimerComponent.cs index fcdc5ac..d1ebf46 100644 --- a/Runtime/Timer/TimerComponent.cs +++ b/Runtime/Timer/TimerComponent.cs @@ -10,6 +10,11 @@ namespace AlicizaX.Timer.Runtime { private void Awake() { + if (AppServices.TryGet(out _)) + { + return; + } + AppServices.RegisterApp(new TimerService()); } } diff --git a/Runtime/Timer/TimerService.cs b/Runtime/Timer/TimerService.cs index e89a8b1..abf4910 100644 --- a/Runtime/Timer/TimerService.cs +++ b/Runtime/Timer/TimerService.cs @@ -4,473 +4,720 @@ using System.Runtime.InteropServices; using Unity.IL2CPP.CompilerServices; using UnityEngine; -namespace AlicizaX +namespace AlicizaX.Timer.Runtime { - public delegate void TimerHandler(params object[] args); public delegate void TimerHandlerNoArgs(); - internal delegate void TimerGenericInvoker(Delegate handler, object arg); + internal delegate void TimerGenericInvoker(object handler, object arg); - internal static class TimerGenericInvokerCache + internal static class TimerGenericInvokerCache where T : class { public static readonly TimerGenericInvoker Invoke = InvokeGeneric; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void InvokeGeneric(Delegate handler, object arg) + private static void InvokeGeneric(object handler, object arg) { - ((Action)handler)?.Invoke((T)arg); + ((Action)handler).Invoke((T)arg); } } [Il2CppSetOption(Option.NullChecks, false)] [Il2CppSetOption(Option.ArrayBoundsChecks, false)] [StructLayout(LayoutKind.Sequential, Pack = 4)] - internal struct TimerInfo : IMemory + internal struct TimerInfo { public int TimerId; + public int Version; + public int QueueIndex; + public int ActiveIndex; public float TriggerTime; - public float Interval; - - public TimerHandler Handler; - public TimerHandlerNoArgs HandlerNoArgs; - public Delegate HandlerGeneric; + public float Duration; + public float RemainingTime; + public TimerHandlerNoArgs NoArgsHandler; public TimerGenericInvoker GenericInvoker; - + public object GenericHandler; + public object GenericArg; public bool IsLoop; public bool IsRunning; public bool IsUnscaled; public bool IsActive; public byte HandlerType; - public object[] Args; - public object GenericArg; - - - public int Level; - public int SlotIndex; - public int NextTimerIndex; - public int PrevTimerIndex; +#if UNITY_EDITOR + public float CreationTime; +#endif [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear() { TimerId = 0; + QueueIndex = -1; + ActiveIndex = -1; TriggerTime = 0f; - Interval = 0f; - Handler = null; - HandlerNoArgs = null; - HandlerGeneric = null; + Duration = 0f; + RemainingTime = 0f; + NoArgsHandler = null; GenericInvoker = null; + GenericHandler = null; + GenericArg = null; IsLoop = false; IsRunning = false; IsUnscaled = false; - Args = null; - GenericArg = null; - HandlerType = 0; IsActive = false; - Level = -1; - SlotIndex = -1; - NextTimerIndex = -1; - PrevTimerIndex = -1; + HandlerType = 0; +#if UNITY_EDITOR + CreationTime = 0f; +#endif } } [UnityEngine.Scripting.Preserve] [Il2CppSetOption(Option.NullChecks, false)] [Il2CppSetOption(Option.ArrayBoundsChecks, false)] - internal sealed class TimerService : ServiceBase, ITimerService, IServiceTickable + internal sealed class TimerService : ServiceBase, ITimerService, IServiceTickable, ITimerServiceDebugView { - private const float TimeWheelSlotInterval = 0.001f; + private const int MAX_CAPACITY = 256; + private const int HANDLE_INDEX_BITS = 9; + private const int HANDLE_INDEX_MASK = MAX_CAPACITY - 1; + private const int HANDLE_VERSION_MASK = 0x3FFFFF; + private const float MINIMUM_DELAY = 0.0001f; + private const byte HANDLER_NO_ARGS = 0; + private const byte HANDLER_GENERIC = 1; - private int _curTimerId; - private HierarchicalTimeWheel _scaledTimeWheel; - private HierarchicalTimeWheel _unscaledTimeWheel; +#if UNITY_EDITOR + private const float LEAK_DETECTION_THRESHOLD = 300f; +#endif + + private readonly TimerInfo[] _timerPool; + private readonly int[] _freeIndices; + private readonly int[] _activeIndices; + private readonly TimerQueue _scaledQueue; + private readonly TimerQueue _unscaledQueue; - private TimerInfo[] _timerPool; - private int[] _timerIdToPoolIndex; - private int _timerPoolCapacity; - private int[] _freeIndices; private int _freeCount; + private int _activeCount; + private int _peakActiveCount; - private int[] _pendingRemoveTimers; - private int _pendingRemoveCount; - - private class HierarchicalTimeWheel + private sealed class TimerQueue { - private const int SLOT_COUNT_LEVEL0 = 256; - private const int SLOT_COUNT_LEVEL1 = 64; - private const int SLOT_COUNT_LEVEL2 = 64; - private const int SLOT_MASK_LEVEL0 = SLOT_COUNT_LEVEL0 - 1; - private const int SLOT_MASK_LEVEL1 = SLOT_COUNT_LEVEL1 - 1; - private const int SLOT_MASK_LEVEL2 = SLOT_COUNT_LEVEL2 - 1; + private readonly TimerService _owner; + private readonly int[] _heap; + private int _count; - private readonly float _slotInterval; - private readonly TimeWheelLevel[] _levels; - private float _currentTime; - private bool _isInitialized; - - private class TimeWheelLevel + public TimerQueue(TimerService owner) { - public readonly int[] SlotHeads; - public readonly int SlotCount; - public int CurrentSlot; - - public TimeWheelLevel(int slotCount) - { - SlotCount = slotCount; - SlotHeads = new int[slotCount]; - for (int i = 0; i < slotCount; i++) - SlotHeads[i] = -1; - } - } - - public HierarchicalTimeWheel(float slotInterval) - { - _slotInterval = slotInterval; - _levels = new TimeWheelLevel[3]; - _levels[0] = new TimeWheelLevel(SLOT_COUNT_LEVEL0); - _levels[1] = new TimeWheelLevel(SLOT_COUNT_LEVEL1); - _levels[2] = new TimeWheelLevel(SLOT_COUNT_LEVEL2); + _owner = owner; + _heap = new int[MAX_CAPACITY]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddTimer(int poolIndex, TimerInfo[] pool, float currentTime) + public void Add(int poolIndex) { - EnsureInitialized(currentTime); - - ref TimerInfo timer = ref pool[poolIndex]; - if (!timer.IsActive) return; - - float delta = timer.TriggerTime - currentTime; - if (delta < 0) delta = 0; - - int totalSlots = Mathf.FloorToInt(delta / _slotInterval); - int level = 0; - int slotIndex = 0; - - if (totalSlots < SLOT_COUNT_LEVEL0) - { - level = 0; - slotIndex = (_levels[0].CurrentSlot + totalSlots) & SLOT_MASK_LEVEL0; - } - else if (totalSlots < SLOT_COUNT_LEVEL0 * SLOT_COUNT_LEVEL1) - { - level = 1; - int level1Slots = totalSlots / SLOT_COUNT_LEVEL0; - slotIndex = (_levels[1].CurrentSlot + level1Slots) & SLOT_MASK_LEVEL1; - } - else - { - level = 2; - int level2Slots = totalSlots / (SLOT_COUNT_LEVEL0 * SLOT_COUNT_LEVEL1); - slotIndex = (_levels[2].CurrentSlot + level2Slots) & SLOT_MASK_LEVEL2; - } - - timer.Level = level; - timer.SlotIndex = slotIndex; - timer.NextTimerIndex = _levels[level].SlotHeads[slotIndex]; - timer.PrevTimerIndex = -1; - - if (_levels[level].SlotHeads[slotIndex] != -1) - { - pool[_levels[level].SlotHeads[slotIndex]].PrevTimerIndex = poolIndex; - } - - _levels[level].SlotHeads[slotIndex] = poolIndex; - timer.IsRunning = true; - } - - public void Advance(float currentTime, TimerInfo[] pool, Action processTimer) - { - EnsureInitialized(currentTime); - - float timeDelta = currentTime - _currentTime; - if (timeDelta <= 0) return; - - int steps = Mathf.FloorToInt(timeDelta / _slotInterval); - - for (int step = 0; step < steps; step++) - { - _currentTime += _slotInterval; - ProcessLevel(0, pool, processTimer); - - if (_levels[0].CurrentSlot == 0) - { - ProcessLevel(1, pool, processTimer); - if (_levels[1].CurrentSlot == 0) - { - ProcessLevel(2, pool, processTimer); - } - } - } + ref TimerInfo timer = ref _owner._timerPool[poolIndex]; + int index = _count++; + _heap[index] = poolIndex; + timer.QueueIndex = index; + BubbleUp(index); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void EnsureInitialized(float currentTime) + public void Remove(int poolIndex) { - if (_isInitialized) + ref TimerInfo timer = ref _owner._timerPool[poolIndex]; + int index = timer.QueueIndex; + if ((uint)index >= (uint)_count) + { + timer.QueueIndex = -1; + return; + } + + int lastIndex = --_count; + int lastPoolIndex = _heap[lastIndex]; + timer.QueueIndex = -1; + + if (index == lastIndex) { return; } - _currentTime = currentTime; - _isInitialized = true; + _heap[index] = lastPoolIndex; + _owner._timerPool[lastPoolIndex].QueueIndex = index; + + if (!BubbleUp(index)) + { + BubbleDown(index); + } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessLevel(int level, TimerInfo[] pool, Action processTimer) + public void Advance(float currentTime) { - TimeWheelLevel wheelLevel = _levels[level]; - if (level == 0) + while (_count > 0) { - wheelLevel.CurrentSlot = (wheelLevel.CurrentSlot + 1) & SLOT_MASK_LEVEL0; - } - else if (level == 1) - { - wheelLevel.CurrentSlot = (wheelLevel.CurrentSlot + 1) & SLOT_MASK_LEVEL1; - } - else - { - wheelLevel.CurrentSlot = (wheelLevel.CurrentSlot + 1) & SLOT_MASK_LEVEL2; - } + int poolIndex = _heap[0]; + ref TimerInfo timer = ref _owner._timerPool[poolIndex]; - int currentHead = wheelLevel.SlotHeads[wheelLevel.CurrentSlot]; - wheelLevel.SlotHeads[wheelLevel.CurrentSlot] = -1; - - int currentIndex = currentHead; - while (currentIndex != -1) - { - ref TimerInfo timer = ref pool[currentIndex]; - int nextIndex = timer.NextTimerIndex; - - timer.NextTimerIndex = -1; - timer.PrevTimerIndex = -1; - timer.Level = -1; - - if (timer.IsActive && timer.IsRunning) + if (!timer.IsActive || !timer.IsRunning) { - if (level == 0) - { - processTimer(currentIndex); - } - else - { - AddTimer(currentIndex, pool, _currentTime); - } + RemoveRoot(); + continue; } - currentIndex = nextIndex; + if (timer.TriggerTime > currentTime) + { + break; + } + + RemoveRoot(); + _owner.ProcessDueTimer(poolIndex, currentTime); + } + } + + public void Clear() + { + while (_count > 0) + { + int poolIndex = _heap[--_count]; + _owner._timerPool[poolIndex].QueueIndex = -1; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RemoveTimer(int poolIndex, TimerInfo[] pool) + private void RemoveRoot() { - if (poolIndex < 0 || poolIndex >= pool.Length) - return; + int rootPoolIndex = _heap[0]; + int lastIndex = --_count; + _owner._timerPool[rootPoolIndex].QueueIndex = -1; - ref TimerInfo timer = ref pool[poolIndex]; - - if (timer.Level < 0 || timer.Level >= _levels.Length) - return; - - int level = timer.Level; - int slotIndex = timer.SlotIndex; - - if (slotIndex < 0 || slotIndex >= _levels[level].SlotCount) - return; - - if (timer.PrevTimerIndex != -1) + if (lastIndex <= 0) { - if (timer.PrevTimerIndex < pool.Length) + return; + } + + int lastPoolIndex = _heap[lastIndex]; + _heap[0] = lastPoolIndex; + _owner._timerPool[lastPoolIndex].QueueIndex = 0; + BubbleDown(0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool BubbleUp(int index) + { + bool moved = false; + while (index > 0) + { + int parent = (index - 1) >> 1; + if (!Less(_heap[index], _heap[parent])) { - pool[timer.PrevTimerIndex].NextTimerIndex = timer.NextTimerIndex; + break; } - } - else - { - _levels[level].SlotHeads[slotIndex] = timer.NextTimerIndex; + + Swap(index, parent); + index = parent; + moved = true; } - if (timer.NextTimerIndex != -1) + return moved; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void BubbleDown(int index) + { + while (true) { - if (timer.NextTimerIndex < pool.Length) + int left = (index << 1) + 1; + if (left >= _count) { - pool[timer.NextTimerIndex].PrevTimerIndex = timer.PrevTimerIndex; + return; } + + int right = left + 1; + int smallest = left; + if (right < _count && Less(_heap[right], _heap[left])) + { + smallest = right; + } + + if (!Less(_heap[smallest], _heap[index])) + { + return; + } + + Swap(index, smallest); + index = smallest; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool Less(int leftPoolIndex, int rightPoolIndex) + { + ref TimerInfo left = ref _owner._timerPool[leftPoolIndex]; + ref TimerInfo right = ref _owner._timerPool[rightPoolIndex]; + + if (left.TriggerTime < right.TriggerTime) + { + return true; } - timer.Level = -1; - timer.SlotIndex = -1; - timer.NextTimerIndex = -1; - timer.PrevTimerIndex = -1; + if (left.TriggerTime > right.TriggerTime) + { + return false; + } + + return left.TimerId < right.TimerId; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Swap(int leftIndex, int rightIndex) + { + int leftPoolIndex = _heap[leftIndex]; + int rightPoolIndex = _heap[rightIndex]; + + _heap[leftIndex] = rightPoolIndex; + _heap[rightIndex] = leftPoolIndex; + + _owner._timerPool[leftPoolIndex].QueueIndex = rightIndex; + _owner._timerPool[rightPoolIndex].QueueIndex = leftIndex; } } public TimerService() { - _timerPoolCapacity = 64; - _timerPool = new TimerInfo[_timerPoolCapacity]; - _timerIdToPoolIndex = new int[1024]; - _freeIndices = new int[_timerPoolCapacity]; - _freeCount = _timerPoolCapacity; - _pendingRemoveTimers = new int[64]; - _pendingRemoveCount = 0; + _timerPool = new TimerInfo[MAX_CAPACITY]; + _freeIndices = new int[MAX_CAPACITY]; + _activeIndices = new int[MAX_CAPACITY]; + _scaledQueue = new TimerQueue(this); + _unscaledQueue = new TimerQueue(this); - InitializeTimerIdMapping(_timerIdToPoolIndex, 0); - - for (int i = 0; i < _timerPoolCapacity; i++) + _freeCount = MAX_CAPACITY; + for (int i = 0; i < MAX_CAPACITY; i++) { _freeIndices[i] = i; - _timerPool[i].NextTimerIndex = -1; - _timerPool[i].PrevTimerIndex = -1; - _timerPool[i].Level = -1; - _timerPool[i].SlotIndex = -1; + _timerPool[i].QueueIndex = -1; + _timerPool[i].ActiveIndex = -1; } - - _scaledTimeWheel = new HierarchicalTimeWheel(TimeWheelSlotInterval); - _unscaledTimeWheel = new HierarchicalTimeWheel(TimeWheelSlotInterval); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int AddTimer(TimerHandlerNoArgs callback, float time, bool isLoop = false, bool isUnscaled = false) { - int poolIndex = AcquireTimerFromPool(); - ref TimerInfo timer = ref _timerPool[poolIndex]; - - timer.TimerId = ++_curTimerId; - timer.TriggerTime = (isUnscaled ? Time.unscaledTime : Time.time) + time; - timer.Interval = isLoop ? time : 0f; - timer.Handler = null; - timer.HandlerNoArgs = callback; - timer.HandlerGeneric = null; - timer.GenericInvoker = null; - timer.HandlerType = 1; - timer.IsLoop = isLoop; - timer.IsRunning = true; - timer.IsUnscaled = isUnscaled; - timer.IsActive = true; - timer.Args = null; - timer.GenericArg = null; - - RegisterTimer(timer.TimerId, poolIndex); - - HierarchicalTimeWheel targetWheel = isUnscaled ? _unscaledTimeWheel : _scaledTimeWheel; - targetWheel.AddTimer(poolIndex, _timerPool, isUnscaled ? Time.unscaledTime : Time.time); - - return timer.TimerId; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int AddTimer(Action callback, T arg, float time, bool isLoop = false, bool isUnscaled = false) - { - int poolIndex = AcquireTimerFromPool(); - ref TimerInfo timer = ref _timerPool[poolIndex]; - - timer.TimerId = ++_curTimerId; - timer.TriggerTime = (isUnscaled ? Time.unscaledTime : Time.time) + time; - timer.Interval = isLoop ? time : 0f; - timer.Handler = null; - timer.HandlerNoArgs = null; - timer.HandlerGeneric = callback; - timer.GenericInvoker = TimerGenericInvokerCache.Invoke; - timer.GenericArg = arg; - timer.HandlerType = 2; - timer.IsLoop = isLoop; - timer.IsRunning = true; - timer.IsUnscaled = isUnscaled; - timer.IsActive = true; - timer.Args = null; - - RegisterTimer(timer.TimerId, poolIndex); - - HierarchicalTimeWheel targetWheel = isUnscaled ? _unscaledTimeWheel : _scaledTimeWheel; - targetWheel.AddTimer(poolIndex, _timerPool, isUnscaled ? Time.unscaledTime : Time.time); - - return timer.TimerId; - } - - public int AddTimer(TimerHandler callback, float time, bool isLoop = false, bool isUnscaled = false, params object[] args) - { - int poolIndex = AcquireTimerFromPool(); - ref TimerInfo timer = ref _timerPool[poolIndex]; - - timer.TimerId = ++_curTimerId; - timer.TriggerTime = (isUnscaled ? Time.unscaledTime : Time.time) + time; - timer.Interval = isLoop ? time : 0f; - timer.Handler = callback; - timer.HandlerNoArgs = null; - timer.HandlerGeneric = null; - timer.GenericInvoker = null; - timer.HandlerType = 0; - timer.IsLoop = isLoop; - timer.IsRunning = true; - timer.IsUnscaled = isUnscaled; - timer.IsActive = true; - timer.Args = args; - timer.GenericArg = null; - - RegisterTimer(timer.TimerId, poolIndex); - - HierarchicalTimeWheel targetWheel = isUnscaled ? _unscaledTimeWheel : _scaledTimeWheel; - targetWheel.AddTimer(poolIndex, _timerPool, isUnscaled ? Time.unscaledTime : Time.time); - - return timer.TimerId; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int AcquireTimerFromPool() - { - if (_freeCount > 0) + if (callback == null) { - return _freeIndices[--_freeCount]; + return 0; } - int oldCapacity = _timerPoolCapacity; - _timerPoolCapacity *= 2; - Array.Resize(ref _timerPool, _timerPoolCapacity); - Array.Resize(ref _freeIndices, _timerPoolCapacity); - - for (int i = _timerPoolCapacity - 1; i >= oldCapacity; i--) + int poolIndex = AcquireTimerIndex(); + if (poolIndex < 0) { - _freeIndices[_freeCount++] = i; - _timerPool[i].NextTimerIndex = -1; - _timerPool[i].PrevTimerIndex = -1; - _timerPool[i].Level = -1; - _timerPool[i].SlotIndex = -1; + return 0; + } + + InitializeTimer(poolIndex, time, isLoop, isUnscaled); + + ref TimerInfo timer = ref _timerPool[poolIndex]; + timer.HandlerType = HANDLER_NO_ARGS; + timer.NoArgsHandler = callback; + + AddToActiveSet(poolIndex); + ScheduleTimer(poolIndex); + return timer.TimerId; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int AddTimer(Action callback, T arg, float time, bool isLoop = false, bool isUnscaled = false) where T : class + { + if (callback == null) + { + return 0; + } + + int poolIndex = AcquireTimerIndex(); + if (poolIndex < 0) + { + return 0; + } + + InitializeTimer(poolIndex, time, isLoop, isUnscaled); + + ref TimerInfo timer = ref _timerPool[poolIndex]; + timer.HandlerType = HANDLER_GENERIC; + timer.GenericInvoker = TimerGenericInvokerCache.Invoke; + timer.GenericHandler = callback; + timer.GenericArg = arg; + + AddToActiveSet(poolIndex); + ScheduleTimer(poolIndex); + return timer.TimerId; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Stop(int timerId) + { + int poolIndex = GetPoolIndex(timerId); + if (poolIndex < 0) + { + return; + } + + ref TimerInfo timer = ref _timerPool[poolIndex]; + if (!timer.IsRunning) + { + return; + } + + if (timer.QueueIndex >= 0) + { + GetQueue(timer.IsUnscaled).Remove(poolIndex); + float currentTime = GetCurrentTime(timer.IsUnscaled); + float remainingTime = timer.TriggerTime - currentTime; + timer.RemainingTime = remainingTime > MINIMUM_DELAY ? remainingTime : MINIMUM_DELAY; + } + else + { + timer.RemainingTime = timer.IsLoop ? timer.Duration : MINIMUM_DELAY; + } + + timer.IsRunning = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Resume(int timerId) + { + int poolIndex = GetPoolIndex(timerId); + if (poolIndex < 0) + { + return; + } + + ref TimerInfo timer = ref _timerPool[poolIndex]; + if (timer.IsRunning) + { + return; + } + + float delay = timer.RemainingTime > MINIMUM_DELAY ? timer.RemainingTime : MINIMUM_DELAY; + timer.TriggerTime = GetCurrentTime(timer.IsUnscaled) + delay; + timer.RemainingTime = 0f; + timer.IsRunning = true; + ScheduleTimer(poolIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsRunning(int timerId) + { + int poolIndex = GetPoolIndex(timerId); + return poolIndex >= 0 && _timerPool[poolIndex].IsRunning; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetLeftTime(int timerId) + { + int poolIndex = GetPoolIndex(timerId); + if (poolIndex < 0) + { + return 0f; + } + + ref TimerInfo timer = ref _timerPool[poolIndex]; + if (!timer.IsRunning) + { + return timer.RemainingTime; + } + + float leftTime = timer.TriggerTime - GetCurrentTime(timer.IsUnscaled); + return leftTime > 0f ? leftTime : 0f; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Restart(int timerId) + { + int poolIndex = GetPoolIndex(timerId); + if (poolIndex < 0) + { + return; + } + + ref TimerInfo timer = ref _timerPool[poolIndex]; + if (timer.QueueIndex >= 0) + { + GetQueue(timer.IsUnscaled).Remove(poolIndex); + } + + timer.TriggerTime = GetCurrentTime(timer.IsUnscaled) + timer.Duration; + timer.RemainingTime = 0f; + timer.IsRunning = true; + ScheduleTimer(poolIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveTimer(int timerId) + { + int poolIndex = GetPoolIndex(timerId); + if (poolIndex >= 0) + { + ReleaseTimer(poolIndex); + } + } + + void IServiceTickable.Tick(float deltaTime) + { + _scaledQueue.Advance(Time.time); + _unscaledQueue.Advance(Time.unscaledTime); + } + + protected override void OnInitialize() + { + } + + protected override void OnDestroyService() + { + RemoveAllTimers(); + } + + public int Order => 0; + + void ITimerServiceDebugView.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount) + { + activeCount = _activeCount; + poolCapacity = MAX_CAPACITY; + peakActiveCount = _peakActiveCount; + freeCount = _freeCount; + } + + int ITimerServiceDebugView.GetAllTimers(TimerDebugInfo[] results) + { + if (results == null || results.Length == 0) + { + return 0; + } + + int count = _activeCount < results.Length ? _activeCount : results.Length; + float currentTime = Time.time; + float currentUnscaledTime = Time.unscaledTime; + + for (int i = 0; i < count; i++) + { + FillDebugInfo(_activeIndices[i], ref results[i], currentTime, currentUnscaledTime); + } + + return count; + } + +#if UNITY_EDITOR + int ITimerServiceDebugView.GetStaleOneShotTimers(TimerDebugInfo[] results) + { + if (results == null || results.Length == 0) + { + return 0; + } + + int count = 0; + float realtimeSinceStartup = Time.realtimeSinceStartup; + float currentTime = Time.time; + float currentUnscaledTime = Time.unscaledTime; + + for (int i = 0; i < _activeCount && count < results.Length; i++) + { + int poolIndex = _activeIndices[i]; + ref TimerInfo timer = ref _timerPool[poolIndex]; + if (timer.IsLoop) + { + continue; + } + + if (realtimeSinceStartup - timer.CreationTime <= LEAK_DETECTION_THRESHOLD) + { + continue; + } + + FillDebugInfo(poolIndex, ref results[count], currentTime, currentUnscaledTime); + count++; + } + + return count; + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int AcquireTimerIndex() + { + if (_freeCount <= 0) + { + return -1; } return _freeIndices[--_freeCount]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RegisterTimer(int timerId, int poolIndex) + private void InitializeTimer(int poolIndex, float time, bool isLoop, bool isUnscaled) { - if (timerId >= _timerIdToPoolIndex.Length) - { - int oldLength = _timerIdToPoolIndex.Length; - int newLength = oldLength; - while (timerId >= newLength) - { - newLength *= 2; - } + ref TimerInfo timer = ref _timerPool[poolIndex]; + float duration = NormalizeDelay(time); + float currentTime = GetCurrentTime(isUnscaled); - Array.Resize(ref _timerIdToPoolIndex, newLength); - InitializeTimerIdMapping(_timerIdToPoolIndex, oldLength); + int version = NextVersion(timer.Version); + timer.Version = version; + timer.TimerId = ComposeTimerId(poolIndex, version); + timer.TriggerTime = currentTime + duration; + timer.Duration = duration; + timer.RemainingTime = 0f; + timer.NoArgsHandler = null; + timer.GenericInvoker = null; + timer.GenericHandler = null; + timer.GenericArg = null; + timer.IsLoop = isLoop; + timer.IsRunning = true; + timer.IsUnscaled = isUnscaled; + timer.IsActive = true; + timer.HandlerType = HANDLER_NO_ARGS; + +#if UNITY_EDITOR + timer.CreationTime = Time.realtimeSinceStartup; +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RemoveAllTimers() + { + while (_activeCount > 0) + { + ReleaseTimer(_activeIndices[_activeCount - 1]); } - _timerIdToPoolIndex[timerId] = poolIndex; + _scaledQueue.Clear(); + _unscaledQueue.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessDueTimer(int poolIndex, float currentTime) + { + ref TimerInfo timer = ref _timerPool[poolIndex]; + if (!timer.IsActive) + { + return; + } + + bool shouldRemoveAfterCallback = !timer.IsLoop; + + switch (timer.HandlerType) + { + case HANDLER_NO_ARGS: + timer.NoArgsHandler(); + break; + + case HANDLER_GENERIC: + timer.GenericInvoker(timer.GenericHandler, timer.GenericArg); + break; + } + + if (!timer.IsActive) + { + return; + } + + if (timer.QueueIndex >= 0) + { + return; + } + + if (shouldRemoveAfterCallback) + { + ReleaseTimer(poolIndex); + } + else if (timer.IsRunning) + { + timer.TriggerTime = currentTime + timer.Duration; + ScheduleTimer(poolIndex); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ScheduleTimer(int poolIndex) + { + ref TimerInfo timer = ref _timerPool[poolIndex]; + if (!timer.IsActive || !timer.IsRunning) + { + return; + } + + if (timer.QueueIndex >= 0) + { + return; + } + + float currentTime = GetCurrentTime(timer.IsUnscaled); + if (timer.TriggerTime <= currentTime) + { + timer.TriggerTime = currentTime + MINIMUM_DELAY; + } + + GetQueue(timer.IsUnscaled).Add(poolIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReleaseTimer(int poolIndex) + { + ref TimerInfo timer = ref _timerPool[poolIndex]; + if (!timer.IsActive) + { + return; + } + + if (timer.QueueIndex >= 0) + { + GetQueue(timer.IsUnscaled).Remove(poolIndex); + } + + RemoveFromActiveSet(poolIndex); + timer.Clear(); + _freeIndices[_freeCount++] = poolIndex; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddToActiveSet(int poolIndex) + { + int position = _activeCount; + _activeIndices[position] = poolIndex; + _timerPool[poolIndex].ActiveIndex = position; + _activeCount = position + 1; + + if (_activeCount > _peakActiveCount) + { + _peakActiveCount = _activeCount; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RemoveFromActiveSet(int poolIndex) + { + ref TimerInfo timer = ref _timerPool[poolIndex]; + int position = timer.ActiveIndex; + if ((uint)position >= (uint)_activeCount) + { + return; + } + + int lastPosition = --_activeCount; + int lastPoolIndex = _activeIndices[lastPosition]; + + if (position != lastPosition) + { + _activeIndices[position] = lastPoolIndex; + _timerPool[lastPoolIndex].ActiveIndex = position; + } + + timer.ActiveIndex = -1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetPoolIndex(int timerId) { - if (timerId <= 0 || timerId >= _timerIdToPoolIndex.Length) + if (timerId <= 0) { return -1; } - int poolIndex = _timerIdToPoolIndex[timerId]; - if ((uint)poolIndex >= (uint)_timerPool.Length) + int poolIndex = (timerId & HANDLE_INDEX_MASK) - 1; + if ((uint)poolIndex >= MAX_CAPACITY) { return -1; } @@ -480,198 +727,68 @@ namespace AlicizaX } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void InitializeTimerIdMapping(int[] mapping, int startIndex) + private static int ComposeTimerId(int poolIndex, int version) { - for (int i = startIndex; i < mapping.Length; i++) - { - mapping[i] = -1; - } - } - - public void Stop(int timerId) - { - int poolIndex = GetPoolIndex(timerId); - if (poolIndex >= 0 && poolIndex < _timerPool.Length && _timerPool[poolIndex].IsActive) - _timerPool[poolIndex].IsRunning = false; - } - - public void Resume(int timerId) - { - int poolIndex = GetPoolIndex(timerId); - if (poolIndex >= 0 && poolIndex < _timerPool.Length && _timerPool[poolIndex].IsActive) - _timerPool[poolIndex].IsRunning = true; - } - - public bool IsRunning(int timerId) - { - int poolIndex = GetPoolIndex(timerId); - return poolIndex >= 0 && poolIndex < _timerPool.Length && - _timerPool[poolIndex].IsActive && _timerPool[poolIndex].IsRunning; - } - - public float GetLeftTime(int timerId) - { - int poolIndex = GetPoolIndex(timerId); - if (poolIndex < 0 || poolIndex >= _timerPool.Length || !_timerPool[poolIndex].IsActive) - return 0; - - ref TimerInfo timer = ref _timerPool[poolIndex]; - return Mathf.Max(timer.TriggerTime - (timer.IsUnscaled ? Time.unscaledTime : Time.time), 0); - } - - public void Restart(int timerId) - { - int poolIndex = GetPoolIndex(timerId); - if (poolIndex < 0 || poolIndex >= _timerPool.Length || !_timerPool[poolIndex].IsActive) - return; - - ref TimerInfo timer = ref _timerPool[poolIndex]; - - HierarchicalTimeWheel targetWheel = timer.IsUnscaled ? _unscaledTimeWheel : _scaledTimeWheel; - targetWheel.RemoveTimer(poolIndex, _timerPool); - - timer.TriggerTime = (timer.IsUnscaled ? Time.unscaledTime : Time.time) + timer.Interval; - targetWheel.AddTimer(poolIndex, _timerPool, timer.IsUnscaled ? Time.unscaledTime : Time.time); - } - - public void RemoveTimer(int timerId) - { - int poolIndex = GetPoolIndex(timerId); - if (poolIndex < 0 || poolIndex >= _timerPool.Length || !_timerPool[poolIndex].IsActive) - return; - - ref TimerInfo timer = ref _timerPool[poolIndex]; - timer.IsActive = false; - - if (timer.Level >= 0) - { - HierarchicalTimeWheel targetWheel = timer.IsUnscaled ? _unscaledTimeWheel : _scaledTimeWheel; - targetWheel.RemoveTimer(poolIndex, _timerPool); - } - - ReleaseTimerToPool(poolIndex); - } - - public void RemoveAllTimer() - { - for (int i = 0; i < _timerPoolCapacity; i++) - { - if (_timerPool[i].IsActive) - { - int timerId = _timerPool[i].TimerId; - if (timerId > 0 && timerId < _timerIdToPoolIndex.Length) - { - _timerIdToPoolIndex[timerId] = -1; - } - - _timerPool[i].Clear(); - _freeIndices[_freeCount++] = i; - } - } - - _pendingRemoveCount = 0; - _scaledTimeWheel = new HierarchicalTimeWheel(TimeWheelSlotInterval); - _unscaledTimeWheel = new HierarchicalTimeWheel(TimeWheelSlotInterval); + return (version << HANDLE_INDEX_BITS) | (poolIndex + 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReleaseTimerToPool(int poolIndex) + private static int NextVersion(int version) { - if (poolIndex >= 0 && poolIndex < _timerPool.Length) + version++; + if (version > HANDLE_VERSION_MASK) { - int timerId = _timerPool[poolIndex].TimerId; - if (timerId > 0 && timerId < _timerIdToPoolIndex.Length && _timerIdToPoolIndex[timerId] == poolIndex) - { - _timerIdToPoolIndex[timerId] = -1; - } - - _timerPool[poolIndex].Clear(); - - if (_freeCount >= _freeIndices.Length) - { - Array.Resize(ref _freeIndices, _freeIndices.Length * 2); - } - - _freeIndices[_freeCount++] = poolIndex; + version = 1; } - } - void IServiceTickable.Tick(float deltaTime) - { - _scaledTimeWheel.Advance(Time.time, _timerPool, ProcessTimer); - _unscaledTimeWheel.Advance(Time.unscaledTime, _timerPool, ProcessTimer); - - ProcessPendingRemovals(); + return version; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessTimer(int poolIndex) + private static float NormalizeDelay(float time) { - if (poolIndex < 0 || poolIndex >= _timerPool.Length) - return; + return time > MINIMUM_DELAY ? time : MINIMUM_DELAY; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetCurrentTime(bool isUnscaled) + { + return isUnscaled ? Time.unscaledTime : Time.time; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private TimerQueue GetQueue(bool isUnscaled) + { + return isUnscaled ? _unscaledQueue : _scaledQueue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FillDebugInfo(int poolIndex, ref TimerDebugInfo info, float currentTime, float currentUnscaledTime) + { ref TimerInfo timer = ref _timerPool[poolIndex]; - if (!timer.IsActive || !timer.IsRunning) - return; - - try + float leftTime; + if (timer.IsRunning) { - switch (timer.HandlerType) + leftTime = timer.TriggerTime - (timer.IsUnscaled ? currentUnscaledTime : currentTime); + if (leftTime < 0f) { - case 0: // TimerHandler - timer.Handler?.Invoke(timer.Args); - break; - case 1: // NoArgs - timer.HandlerNoArgs?.Invoke(); - break; - case 2: // Generic - timer.GenericInvoker?.Invoke(timer.HandlerGeneric, timer.GenericArg); - break; + leftTime = 0f; } } - catch (Exception e) - { - Log.Error($"Timer callback error: {e}"); - } - - if (!timer.IsActive) - return; - - if (timer.IsLoop) - { - timer.TriggerTime += timer.Interval; - HierarchicalTimeWheel targetWheel = timer.IsUnscaled ? _unscaledTimeWheel : _scaledTimeWheel; - targetWheel.AddTimer(poolIndex, _timerPool, timer.IsUnscaled ? Time.unscaledTime : Time.time); - } else { - AddPendingRemoval(timer.TimerId); + leftTime = timer.RemainingTime; } + + info.TimerId = timer.TimerId; + info.LeftTime = leftTime; + info.Duration = timer.Duration; + info.IsLoop = timer.IsLoop; + info.IsRunning = timer.IsRunning; + info.IsUnscaled = timer.IsUnscaled; +#if UNITY_EDITOR + info.CreationTime = timer.CreationTime; +#endif } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddPendingRemoval(int timerId) - { - if (_pendingRemoveCount >= _pendingRemoveTimers.Length) - { - Array.Resize(ref _pendingRemoveTimers, _pendingRemoveTimers.Length * 2); - } - _pendingRemoveTimers[_pendingRemoveCount++] = timerId; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessPendingRemovals() - { - for (int i = 0; i < _pendingRemoveCount; i++) - { - RemoveTimer(_pendingRemoveTimers[i]); - } - _pendingRemoveCount = 0; - } - - protected override void OnInitialize() { } - protected override void OnDestroyService() => RemoveAllTimer(); - - public int Order => 0; } } diff --git a/Runtime/Timer/TimerService.cs.meta b/Runtime/Timer/TimerService.cs.meta index c85d8fd..4895c0a 100644 --- a/Runtime/Timer/TimerService.cs.meta +++ b/Runtime/Timer/TimerService.cs.meta @@ -1,3 +1,11 @@ fileFormatVersion: 2 guid: 205d0803930745d7825f89aa604530a5 -timeCreated: 1741683842 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UI/Manager/UIService.Cache.cs b/Runtime/UI/Manager/UIService.Cache.cs index a1acea6..e5ce353 100644 --- a/Runtime/UI/Manager/UIService.Cache.cs +++ b/Runtime/UI/Manager/UIService.Cache.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using AlicizaX; +using AlicizaX.Timer.Runtime; namespace AlicizaX.UI.Runtime { @@ -46,8 +47,7 @@ namespace AlicizaX.UI.Runtime uiMetadata, uiMetadata.MetaInfo.CacheTime, isLoop: false, - isUnscaled: true - ); + isUnscaled: true); if (timerId <= 0) {