[Opt] AppService架构 UI加载&调试 ObjectPool优化 Timer模块优化

This commit is contained in:
陈思海 2026-04-28 18:25:04 +08:00
parent 8cc5025796
commit de926116aa
44 changed files with 3344 additions and 1747 deletions

View File

@ -8,38 +8,46 @@ namespace AlicizaX.Timer.Editor
[CustomEditor(typeof(TimerComponent))]
internal sealed class TimerComponentInspector : GameFrameworkInspector
{
private const double UPDATE_INTERVAL = 0.25d;
private const int MAX_DISPLAY_COUNT = 20;
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 TimerDebugInfo[] _timerBuffer;
#if UNITY_EDITOR
private TimerDebugInfo[] _leakBuffer;
#endif
private readonly TimerDebugInfo[] _timerBuffer = new TimerDebugInfo[DISPLAY_COUNT];
private readonly TimerDebugInfo[] _staleBuffer = new TimerDebugInfo[DISPLAY_COUNT];
private double _lastUpdateTime;
private int _cachedActiveCount;
private int _cachedPoolCapacity;
private int _cachedPeakActiveCount;
private int _cachedFreeCount;
private string _cachedUsageText;
private SerializedProperty _initialCapacityProperty;
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
DrawConfiguration();
serializedObject.ApplyModifiedProperties();
DrawRuntimeDebugInfo();
RequestRuntimeRepaint();
}
if (EditorApplication.isPlaying)
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)
{
double currentTime = EditorApplication.timeSinceStartup;
if (currentTime - _lastUpdateTime >= UPDATE_INTERVAL)
{
_lastUpdateTime = currentTime;
Repaint();
}
_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()
@ -56,30 +64,31 @@ namespace AlicizaX.Timer.Editor
return;
}
if (!(timerService is ITimerDebugService timerDebugService))
{
EditorGUILayout.HelpBox("Timer debug service is not available.", MessageType.Info);
return;
}
timerService.GetStatistics(out _cachedActiveCount, out _cachedPoolCapacity, out _cachedPeakActiveCount, out _cachedFreeCount);
_cachedUsageText = _cachedPoolCapacity > 0
? Utility.Text.Format("{0:F1}%", (float)_cachedActiveCount / _cachedPoolCapacity * 100f)
: "0.0%";
timerDebugService.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount);
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);
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(timerService, _cachedActiveCount);
#if UNITY_EDITOR
DrawLeakDetection(timerService, _cachedActiveCount);
#endif
DrawTimerList(timerDebugService, activeCount);
DrawStaleTimerList(timerDebugService, activeCount);
}
private void DrawTimerList(ITimerService debug, int activeCount)
private void DrawTimerList(ITimerDebugService timerDebugService, int activeCount)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Active Timers", EditorStyles.boldLabel);
EditorGUILayout.LabelField("Active Timer Sample", EditorStyles.boldLabel);
if (activeCount <= 0)
{
@ -87,84 +96,93 @@ namespace AlicizaX.Timer.Editor
return;
}
EnsureTimerBuffer(activeCount);
int timerCount = debug.GetAllTimers(_timerBuffer);
int displayCount = Mathf.Min(timerCount, MAX_DISPLAY_COUNT);
if (displayCount < activeCount)
int timerCount = timerDebugService.GetAllTimers(_timerBuffer);
if (activeCount > DISPLAY_COUNT)
{
EditorGUILayout.HelpBox(Utility.Text.Format("Showing first {0} timers of {1}.", displayCount, activeCount), MessageType.Info);
EditorGUILayout.HelpBox(Utility.Text.Format("Showing first {0} timers of {1}.", timerCount, activeCount), MessageType.Info);
}
for (int i = 0; i < displayCount; i++)
for (int i = 0; i < timerCount; i++)
{
TimerDebugInfo timer = _timerBuffer[i];
bool isLoop = (timer.Flags & TimerDebugFlags.Loop) != 0;
bool isRunning = (timer.Flags & TimerDebugFlags.Running) != 0;
bool isUnscaled = (timer.Flags & TimerDebugFlags.Unscaled) != 0;
string label = Utility.Text.Format(
"ID {0} | {1} | {2} | {3}",
timer.TimerHandle,
isLoop ? "Loop" : "Once",
isUnscaled ? "Unscaled" : "Scaled",
isRunning ? "Running" : "Paused");
string value = Utility.Text.Format(
"Left {0:F2}s | Duration {1:F2}s",
timer.LeftTime,
timer.Duration);
EditorGUILayout.LabelField(label, value);
DrawTimerInfo(ref _timerBuffer[i]);
}
}
#if UNITY_EDITOR
private void DrawLeakDetection(ITimerService debug, int activeCount)
private void DrawStaleTimerList(ITimerDebugService timerDebugService, int activeCount)
{
if (activeCount <= 0)
{
return;
}
EnsureLeakBuffer(activeCount);
int staleCount = debug.GetStaleOneShotTimers(_leakBuffer);
if (!(timerDebugService is ITimerEditorDebugService editorDebugService))
{
return;
}
int staleCount = editorDebugService.GetStaleOneShotTimers(_staleBuffer);
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);
EditorGUILayout.HelpBox("Long-lived one-shot timers detected.", MessageType.Warning);
for (int i = 0; i < staleCount; i++)
{
TimerDebugInfo staleTimer = _leakBuffer[i];
EditorGUILayout.LabelField(
Utility.Text.Format("ID {0}", staleTimer.TimerHandle),
Utility.Text.Format("Created {0:F1}s ago", staleTimer.Age));
}
}
#endif
private void EnsureTimerBuffer(int count)
{
int capacity = count > 0 ? count : 1;
if (_timerBuffer == null || _timerBuffer.Length < capacity)
{
_timerBuffer = new TimerDebugInfo[capacity];
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));
}
}
#if UNITY_EDITOR
private void EnsureLeakBuffer(int count)
private static void DrawTimerInfo(ref TimerDebugInfo info)
{
int capacity = count > 0 ? count : 1;
if (_leakBuffer == null || _leakBuffer.Length < capacity)
{
_leakBuffer = new TimerDebugInfo[capacity];
}
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();
}
#endif
}
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using AlicizaX.Editor;
using AlicizaX.UI.Runtime;
using UnityEditor;
@ -9,9 +9,26 @@ namespace AlicizaX.UI.Editor
[CustomEditor(typeof(UIComponent))]
internal sealed class UIComponentInspector : GameFrameworkInspector
{
private const int CacheDebugInfoCapacity = 64;
private static readonly Color SectionColor = new Color(0.18f, 0.18f, 0.18f, 1f);
private static readonly Color OkColor = new Color(0.3f, 0.75f, 0.35f, 1f);
private static readonly Color WarningColor = new Color(1f, 0.72f, 0.18f, 1f);
private static readonly Color ErrorColor = new Color(1f, 0.32f, 0.28f, 1f);
private static readonly Color MutedColor = new Color(0.62f, 0.62f, 0.62f, 1f);
private readonly UIServiceDebugInfo _serviceInfo = new UIServiceDebugInfo();
private readonly UILayerDebugInfo _layerInfo = new UILayerDebugInfo();
private readonly UIWindowDebugInfo _windowInfo = new UIWindowDebugInfo();
private readonly UIWindowDebugInfo[] _cacheInfos = new UIWindowDebugInfo[CacheDebugInfoCapacity];
private SerializedProperty uiRoot;
private SerializedProperty _isOrthographic;
private bool _showRuntimeDebug = true;
private bool _showReferences;
private bool _showLayers = true;
private bool _showCache = true;
private bool _showEmptyLayers;
private Vector2 _runtimeScroll;
public override void OnInspectorGUI()
{
@ -27,7 +44,7 @@ namespace AlicizaX.UI.Editor
EditorGUILayout.BeginHorizontal();
GameObject rootPrefab = (GameObject)EditorGUILayout.ObjectField("UI根预设", uiRoot.objectReferenceValue, typeof(GameObject), false);
GameObject rootPrefab = (GameObject)EditorGUILayout.ObjectField("UI Root Prefab", uiRoot.objectReferenceValue, typeof(GameObject), false);
if (rootPrefab != uiRoot.objectReferenceValue)
{
@ -36,7 +53,7 @@ namespace AlicizaX.UI.Editor
if (uiRoot.objectReferenceValue == null)
{
if (GUILayout.Button("设置默认"))
if (GUILayout.Button("Set Default"))
{
GameObject defaultPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(UIGlobalPath.UIPrefabPath);
uiRoot.objectReferenceValue = defaultPrefab;
@ -49,14 +66,283 @@ namespace AlicizaX.UI.Editor
}
EditorGUI.EndDisabledGroup();
serializedObject.ApplyModifiedProperties();
Repaint();
}
DrawRuntimeDebugInfo();
if (EditorApplication.isPlaying)
{
Repaint();
}
}
private void OnEnable()
{
uiRoot = serializedObject.FindProperty("uiRoot");
_isOrthographic = serializedObject.FindProperty("_isOrthographic");
for (int i = 0; i < _cacheInfos.Length; i++)
{
_cacheInfos[i] = new UIWindowDebugInfo();
}
}
private void DrawRuntimeDebugInfo()
{
EditorGUILayout.Space();
_showRuntimeDebug = EditorGUILayout.Foldout(_showRuntimeDebug, "Runtime Debug", true, EditorStyles.foldoutHeader);
if (!_showRuntimeDebug)
{
return;
}
if (!EditorApplication.isPlaying)
{
EditorGUILayout.HelpBox("Enter Play Mode to inspect runtime UI state.", MessageType.Info);
return;
}
if (!AppServices.TryGet<IUIService>(out IUIService uiService) || uiService is not IUIDebugService debugService)
{
EditorGUILayout.HelpBox("UI service is not initialized.", MessageType.Warning);
return;
}
debugService.FillServiceDebugInfo(_serviceInfo);
DrawServiceSummary();
DrawRuntimeOptions();
_runtimeScroll = EditorGUILayout.BeginScrollView(_runtimeScroll, GUILayout.MinHeight(260f), GUILayout.MaxHeight(720f));
DrawReferences();
DrawLayerDebugInfo(debugService);
DrawCacheDebugInfo(debugService);
EditorGUILayout.EndScrollView();
}
private void DrawRuntimeOptions()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
_showLayers = GUILayout.Toggle(_showLayers, "Layers", EditorStyles.toolbarButton);
_showCache = GUILayout.Toggle(_showCache, "Cache", EditorStyles.toolbarButton);
_showReferences = GUILayout.Toggle(_showReferences, "References", EditorStyles.toolbarButton);
_showEmptyLayers = GUILayout.Toggle(_showEmptyLayers, "Empty Layers", EditorStyles.toolbarButton);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
}
private void DrawServiceSummary()
{
DrawSectionBegin("Service Summary");
EditorGUILayout.BeginHorizontal();
DrawCounter("Initialized", _serviceInfo.Initialized ? "Yes" : "No", _serviceInfo.Initialized ? OkColor : ErrorColor);
DrawCounter("Mode", _serviceInfo.Orthographic ? "Orthographic" : "Perspective", Color.white);
DrawCounter("Layers", _serviceInfo.LayerCount.ToString(), Color.white);
DrawCounter("Open", _serviceInfo.OpenWindowCount.ToString(), _serviceInfo.OpenWindowCount > 0 ? OkColor : MutedColor);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
DrawCounter("Cache", _serviceInfo.CacheWindowCount.ToString(), _serviceInfo.CacheWindowCount > 0 ? WarningColor : MutedColor);
DrawCounter("Update", _serviceInfo.UpdateWindowCount.ToString(), _serviceInfo.UpdateWindowCount > 0 ? OkColor : MutedColor);
DrawCounter("Block", _serviceInfo.BlockActive ? "Active" : "Inactive", _serviceInfo.BlockActive ? WarningColor : MutedColor);
DrawCounter("Block Timer", _serviceInfo.BlockTimerHandle.ToString(), _serviceInfo.BlockTimerHandle != 0UL ? WarningColor : MutedColor);
EditorGUILayout.EndHorizontal();
DrawSectionEnd();
}
private void DrawReferences()
{
if (!_showReferences)
{
return;
}
DrawSectionBegin("References");
EditorGUILayout.ObjectField("Root", _serviceInfo.Root, typeof(Transform), true);
EditorGUILayout.ObjectField("Canvas Root", _serviceInfo.CanvasRoot, typeof(Transform), true);
EditorGUILayout.ObjectField("Canvas", _serviceInfo.Canvas, typeof(Canvas), true);
EditorGUILayout.ObjectField("Camera", _serviceInfo.Camera, typeof(Camera), true);
DrawSectionEnd();
}
private void DrawLayerDebugInfo(IUIDebugService debugService)
{
if (!_showLayers)
{
return;
}
DrawSectionBegin("Open Windows");
bool hasWindow = false;
for (int layerIndex = 0; layerIndex < debugService.LayerCount; layerIndex++)
{
if (!debugService.FillLayerDebugInfo(layerIndex, _layerInfo))
{
continue;
}
if (_layerInfo.WindowCount == 0 && !_showEmptyLayers)
{
continue;
}
hasWindow |= _layerInfo.WindowCount > 0;
DrawLayerHeader(_layerInfo);
EditorGUI.indentLevel++;
for (int windowIndex = 0; windowIndex < _layerInfo.WindowCount; windowIndex++)
{
if (debugService.FillWindowDebugInfo(layerIndex, windowIndex, _windowInfo))
{
DrawWindowDebugInfo(_windowInfo, false);
}
}
EditorGUI.indentLevel--;
}
if (!hasWindow && !_showEmptyLayers)
{
DrawEmptyLabel("No open UI windows.");
}
DrawSectionEnd();
}
private void DrawCacheDebugInfo(IUIDebugService debugService)
{
if (!_showCache)
{
return;
}
DrawSectionBegin("Cached Windows");
int count = debugService.FillCacheDebugInfo(_cacheInfos, _cacheInfos.Length);
if (count == 0)
{
DrawEmptyLabel("No cached UI windows.");
}
for (int i = 0; i < count; i++)
{
DrawWindowDebugInfo(_cacheInfos[i], true);
}
if (debugService.CacheWindowCount > _cacheInfos.Length)
{
EditorGUILayout.HelpBox("Cache list is truncated by inspector buffer capacity.", MessageType.Warning);
}
DrawSectionEnd();
}
private static void DrawLayerHeader(UILayerDebugInfo info)
{
Rect rect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight + 4f);
EditorGUI.DrawRect(rect, SectionColor);
rect.x += 6f;
rect.width -= 12f;
rect.y += 2f;
EditorGUI.LabelField(
rect,
info.Layer.ToString(),
"Windows " + info.WindowCount + " | Last Fullscreen " + info.LastFullscreenIndex,
EditorStyles.boldLabel);
}
private static void DrawWindowDebugInfo(UIWindowDebugInfo info, bool cached)
{
Rect rect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight + 4f);
Color color = GetWindowColor(info, cached);
EditorGUI.DrawRect(new Rect(rect.x, rect.y + 2f, 4f, rect.height - 4f), color);
rect.x += 8f;
rect.width -= 8f;
EditorGUI.ObjectField(rect, GetWindowTitle(info, cached), info.HolderTransform, typeof(Transform), true);
EditorGUI.indentLevel++;
EditorGUILayout.LabelField("State", GetStateLine(info));
EditorGUILayout.LabelField("Flags", GetFlagLine(info));
if (cached || info.CacheTime > 0f || info.CacheTimerHandle != 0UL)
{
EditorGUILayout.LabelField("Cache", GetCacheLine(info));
}
EditorGUILayout.LabelField("Holder", string.IsNullOrEmpty(info.HolderTypeName) ? "None" : info.HolderTypeName);
EditorGUI.indentLevel--;
}
private static string GetWindowTitle(UIWindowDebugInfo info, bool cached)
{
string logicName = string.IsNullOrEmpty(info.LogicTypeName) ? "Unknown" : info.LogicTypeName;
string prefix = cached ? "[Cache] " : "";
return prefix + "#" + info.OrderIndex + " L" + info.LayerIndex + " " + logicName;
}
private static string GetStateLine(UIWindowDebugInfo info)
{
return info.State + " | Visible " + info.Visible + " | Depth " + info.Depth + " | FullScreen " + info.FullScreen;
}
private static string GetFlagLine(UIWindowDebugInfo info)
{
return "Update " + info.NeedUpdate + " | InCache " + info.InCache + " | ShowOp " + info.ShowInProgress + " | CloseOp " + info.CloseInProgress;
}
private static string GetCacheLine(UIWindowDebugInfo info)
{
return "Time " + info.CacheTime.ToString("F2") + " | Timer " + info.CacheTimerHandle;
}
private static Color GetWindowColor(UIWindowDebugInfo info, bool cached)
{
if (info.ShowInProgress || info.CloseInProgress)
{
return WarningColor;
}
if (cached || info.InCache)
{
return MutedColor;
}
if (!info.Visible)
{
return ErrorColor;
}
return info.FullScreen ? OkColor : Color.cyan;
}
private static void DrawCounter(string label, string value, Color valueColor)
{
EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.MinWidth(82f));
EditorGUILayout.LabelField(label, EditorStyles.miniLabel);
Color oldColor = GUI.color;
GUI.color = valueColor;
EditorGUILayout.LabelField(value, EditorStyles.boldLabel);
GUI.color = oldColor;
EditorGUILayout.EndVertical();
}
private static void DrawSectionBegin(string title)
{
EditorGUILayout.Space(4f);
Rect rect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight + 6f);
EditorGUI.DrawRect(rect, SectionColor);
rect.x += 6f;
rect.y += 3f;
rect.width -= 12f;
EditorGUI.LabelField(rect, title, EditorStyles.boldLabel);
EditorGUILayout.BeginVertical(GUI.skin.box);
}
private static void DrawSectionEnd()
{
EditorGUILayout.EndVertical();
}
private static void DrawEmptyLabel(string text)
{
Color oldColor = GUI.color;
GUI.color = MutedColor;
EditorGUILayout.LabelField(text, EditorStyles.miniLabel);
GUI.color = oldColor;
}
}
}

View File

@ -26,4 +26,11 @@ namespace AlicizaX
{
public int Order { get; }
}
internal enum ServiceScopeKind : byte
{
App = 0,
Scene = 1,
Gameplay = 2,
}
}

View File

@ -21,8 +21,12 @@ namespace AlicizaX
return _world;
}
public static T RegisterApp<T>(T service, params Type[] extraContracts) where T : class, IService
=> RequireWorld().App.Register(service, extraContracts);
public static T RegisterAppSelf<T>(T service) where T : class, IService
=> RequireWorld().App.RegisterSelf(service);
public static TContract RegisterApp<TContract>(IService service)
where TContract : class, IService
=> RequireWorld().App.Register<TContract>(service);
public static bool TryGetApp<T>(out T service) where T : class, IService
{

View File

@ -1,10 +1,9 @@
using System;
namespace AlicizaX
namespace AlicizaX
{
public interface IServiceRegistry
{
T Register<T>(T service, params Type[] extraContracts) where T : class, IService;
T RegisterSelf<T>(T service) where T : class, IService;
TContract Register<TContract>(IService service) where TContract : class, IService;
bool TryGet<T>(out T service) where T : class, IService;
T Require<T>() where T : class, IService;
}

View File

@ -1,3 +1,5 @@
using Cysharp.Text;
namespace AlicizaX
{
internal interface IServiceLifecycle
@ -15,7 +17,7 @@ namespace AlicizaX
void IServiceLifecycle.Initialize(ServiceContext context)
{
if (IsInitialized)
throw new System.InvalidOperationException($"{GetType().FullName} is already initialized.");
throw new System.InvalidOperationException(ZString.Format("{0} is already initialized.", GetType().FullName));
Context = context;
IsInitialized = true;

View File

@ -1,78 +1,37 @@
using System;
using System.Collections.Generic;
namespace AlicizaX
{
internal static class ServiceContractUtility
{
private static readonly HashSet<Type> ExcludedContracts = new HashSet<Type>
public static ServiceContracts Create(Type serviceType)
=> new ServiceContracts(serviceType, null);
public static ServiceContracts Create(Type serviceType, Type contractType)
=> new ServiceContracts(serviceType, contractType);
}
internal readonly struct ServiceContracts
{
private readonly Type _serviceType;
private readonly Type _contractType;
public ServiceContracts(Type serviceType, Type contractType)
{
typeof(IService),
typeof(IMonoService),
typeof(IServiceTickable),
typeof(IServiceLateTickable),
typeof(IServiceFixedTickable),
typeof(IServiceGizmoDrawable),
typeof(IServiceOrder),
typeof(IServiceLifecycle),
};
// Cache for the common no-extraContracts path — contracts per concrete type never change.
private static readonly Dictionary<Type, List<Type>> _contractCache = new Dictionary<Type, List<Type>>();
public static List<Type> Collect(Type serviceType, IReadOnlyList<Type> extraContracts)
{
if (extraContracts == null || extraContracts.Count == 0)
{
if (_contractCache.TryGetValue(serviceType, out var cached))
return cached;
var result = BuildContracts(serviceType);
_contractCache[serviceType] = result;
return result;
}
// Extra contracts path: build fresh, validate, append extras.
var contracts = BuildContracts(serviceType);
contracts = new List<Type>(contracts); // don't mutate the cached list
var unique = new HashSet<Type>(contracts);
for (var i = 0; i < extraContracts.Count; i++)
{
var extraContract = extraContracts[i];
ValidateExtraContract(serviceType, extraContract);
if (unique.Add(extraContract)) contracts.Add(extraContract);
}
return contracts;
_serviceType = serviceType;
_contractType = contractType;
}
private static List<Type> BuildContracts(Type serviceType)
{
var contracts = new List<Type> { serviceType };
var unique = new HashSet<Type> { serviceType };
public int Count => _contractType == null || _contractType == _serviceType ? 1 : 2;
var interfaces = serviceType.GetInterfaces();
for (var i = 0; i < interfaces.Length; i++)
public Type this[int index]
{
get
{
var contract = interfaces[i];
if (!typeof(IService).IsAssignableFrom(contract)) continue;
if (ExcludedContracts.Contains(contract)) continue;
if (unique.Add(contract)) contracts.Add(contract);
if (index == 0) return _serviceType;
if (index == 1 && Count == 2) return _contractType;
throw new IndexOutOfRangeException();
}
return contracts;
}
private static void ValidateExtraContract(Type serviceType, Type contract)
{
if (contract == null)
throw new ArgumentNullException(nameof(contract));
if (!typeof(IService).IsAssignableFrom(contract))
throw new InvalidOperationException($"{contract.FullName} must inherit {nameof(IService)}.");
if (!contract.IsAssignableFrom(serviceType))
throw new InvalidOperationException($"{serviceType.FullName} does not implement {contract.FullName}.");
if (ExcludedContracts.Contains(contract))
throw new InvalidOperationException($"{contract.FullName} cannot be used as a service contract.");
}
}
}

View File

@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using Cysharp.Text;
namespace AlicizaX
{
internal sealed class ServiceScope : IDisposable, IServiceRegistry
{
private readonly Dictionary<Type, IService> _servicesByContract = new Dictionary<Type, IService>();
private readonly Dictionary<IService, List<Type>> _contractsByService = new Dictionary<IService, List<Type>>(ReferenceComparer<IService>.Instance);
private const int MissingIndex = -1;
private readonly Dictionary<RuntimeTypeHandle, IService> _servicesByContract = new Dictionary<RuntimeTypeHandle, IService>();
private readonly Dictionary<IService, ServiceEntry> _entriesByService = new Dictionary<IService, ServiceEntry>(ReferenceComparer<IService>.Instance);
private readonly List<IService> _registrationOrder = new List<IService>();
private readonly List<IServiceTickable> _tickables = new List<IServiceTickable>();
@ -14,101 +17,74 @@ namespace AlicizaX
private readonly List<IServiceFixedTickable> _fixedTickables = new List<IServiceFixedTickable>();
private readonly List<IServiceGizmoDrawable> _gizmoDrawables = new List<IServiceGizmoDrawable>();
private IServiceTickable[] _tickableSnapshot = Array.Empty<IServiceTickable>();
private IServiceLateTickable[] _lateTickableSnapshot = Array.Empty<IServiceLateTickable>();
private IServiceFixedTickable[] _fixedTickableSnapshot = Array.Empty<IServiceFixedTickable>();
private IServiceGizmoDrawable[] _gizmoSnapshot = Array.Empty<IServiceGizmoDrawable>();
private readonly List<PendingChange> _pendingChanges = new List<PendingChange>();
private bool _tickablesDirty;
private bool _lateTickablesDirty;
private bool _fixedTickablesDirty;
private bool _gizmoDrawablesDirty;
private bool _isIterating;
internal ServiceScope(ServiceWorld world, string name, int order)
internal ServiceScope(ServiceWorld world, ServiceScopeKind kind, string name, int order, int creationIndex)
{
World = world ?? throw new ArgumentNullException(nameof(world));
Kind = kind;
Name = string.IsNullOrWhiteSpace(name) ? throw new ArgumentException("Scope name cannot be empty.", nameof(name)) : name;
Order = order;
CreationIndex = creationIndex;
}
internal ServiceWorld World { get; }
internal ServiceScopeKind Kind { get; }
public string Name { get; }
internal int Order { get; }
internal int CreationIndex { get; }
internal bool IsDisposed { get; private set; }
public T Register<T>(T service, params Type[] extraContracts)
where T : class, IService
{
EnsureNotDisposed();
public T RegisterSelf<T>(T service) where T : class, IService
=> RegisterInternal(service, ServiceContractUtility.Create(service != null ? service.GetType() : typeof(T)));
if (service == null)
throw new ArgumentNullException(nameof(service));
if (service is not IServiceLifecycle lifecycle)
throw new InvalidOperationException($"Service {service.GetType().FullName} must implement {nameof(IServiceLifecycle)}.");
ValidateService(service);
if (_contractsByService.ContainsKey(service))
throw new InvalidOperationException($"Service {service.GetType().FullName} is already registered in scope {Name}.");
var contracts = ServiceContractUtility.Collect(service.GetType(), extraContracts);
for (var i = 0; i < contracts.Count; i++)
{
var contract = contracts[i];
if (_servicesByContract.TryGetValue(contract, out var existing))
{
throw new InvalidOperationException(
$"Scope {Name} already contains contract {contract.FullName} bound to {existing.GetType().FullName}.");
}
}
_contractsByService.Add(service, contracts);
_registrationOrder.Add(service);
for (var i = 0; i < contracts.Count; i++)
_servicesByContract.Add(contracts[i], service);
try
{
lifecycle.Initialize(new ServiceContext(World, this));
AddToLifecycleLists(service);
}
catch
{
RemoveBindings(service);
throw;
}
return service;
}
public TContract Register<TContract>(IService service)
where TContract : class, IService
=> RegisterInternal(service, ServiceContractUtility.Create(service != null ? service.GetType() : typeof(TContract), typeof(TContract))) as TContract;
public bool Unregister<T>() where T : class, IService
{
if (!_servicesByContract.TryGetValue(typeof(T), out var service))
if (!_servicesByContract.TryGetValue(typeof(T).TypeHandle, out var service))
return false;
return Unregister(service);
}
public bool Unregister(IService service)
{
if (service == null || !_contractsByService.ContainsKey(service))
if (service == null || !_entriesByService.TryGetValue(service, out var entry))
return false;
if (service is not IServiceLifecycle lifecycle)
throw new InvalidOperationException($"Service {service.GetType().FullName} must implement {nameof(IServiceLifecycle)}.");
throw new InvalidOperationException(ZString.Format("Service {0} must implement {1}.", service.GetType().FullName, nameof(IServiceLifecycle)));
RemoveFromLifecycleLists(service);
RemoveBindings(service);
if (_isIterating)
{
if (entry.PendingRemove) return true;
entry.PendingRemove = true;
_entriesByService[service] = entry;
_pendingChanges.Add(PendingChange.Remove(service));
return true;
}
RemoveEntry(service, entry, true);
lifecycle.Destroy();
return true;
}
public bool TryGet<T>(out T service) where T : class, IService
{
if (_servicesByContract.TryGetValue(typeof(T), out var raw))
if (_servicesByContract.TryGetValue(typeof(T).TypeHandle, out var raw))
{
service = raw as T;
return service != null;
@ -118,60 +94,180 @@ namespace AlicizaX
return false;
}
internal bool TryGet(Type contract, out IService service)
=> _servicesByContract.TryGetValue(contract.TypeHandle, out service);
public T Require<T>() where T : class, IService
{
if (TryGet(out T service)) return service;
Log.Error($"Scope {Name} does not contain service {typeof(T).FullName}.");
return default;
throw new InvalidOperationException(ZString.Format("Scope {0} does not contain service {1}.", Name, typeof(T).FullName));
}
public bool HasContract(Type contractType)
=> _servicesByContract.ContainsKey(contractType);
=> _servicesByContract.ContainsKey(contractType.TypeHandle);
internal void Tick(float deltaTime)
{
var snapshot = GetTickSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].Tick(deltaTime);
SortTickablesIfDirty();
_isIterating = true;
for (var i = 0; i < _tickables.Count; i++) _tickables[i].Tick(deltaTime);
_isIterating = false;
FlushPendingChanges();
}
internal void LateTick(float deltaTime)
{
var snapshot = GetLateTickSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].LateTick(deltaTime);
SortLateTickablesIfDirty();
_isIterating = true;
for (var i = 0; i < _lateTickables.Count; i++) _lateTickables[i].LateTick(deltaTime);
_isIterating = false;
FlushPendingChanges();
}
internal void FixedTick(float fixedDeltaTime)
{
var snapshot = GetFixedTickSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].FixedTick(fixedDeltaTime);
SortFixedTickablesIfDirty();
_isIterating = true;
for (var i = 0; i < _fixedTickables.Count; i++) _fixedTickables[i].FixedTick(fixedDeltaTime);
_isIterating = false;
FlushPendingChanges();
}
internal void DrawGizmos()
{
var snapshot = GetGizmoSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].DrawGizmos();
SortGizmoDrawablesIfDirty();
_isIterating = true;
for (var i = 0; i < _gizmoDrawables.Count; i++) _gizmoDrawables[i].DrawGizmos();
_isIterating = false;
FlushPendingChanges();
}
public void Dispose()
{
if (IsDisposed) return;
var snapshot = _registrationOrder.ToArray();
for (var i = snapshot.Length - 1; i >= 0; i--)
_isIterating = false;
_pendingChanges.Clear();
for (var i = _registrationOrder.Count - 1; i >= 0; i--)
{
var service = snapshot[i];
if (!_contractsByService.ContainsKey(service)) continue;
var service = _registrationOrder[i];
if (service == null || !_entriesByService.TryGetValue(service, out var entry)) continue;
if (service is not IServiceLifecycle lifecycle)
throw new InvalidOperationException($"Service {service.GetType().FullName} must implement {nameof(IServiceLifecycle)}.");
RemoveFromLifecycleLists(service);
RemoveContractBindings(service); // skip _registrationOrder.Remove — we clear below
throw new InvalidOperationException(ZString.Format("Service {0} must implement {1}.", service.GetType().FullName, nameof(IServiceLifecycle)));
RemoveEntry(service, entry, false);
lifecycle.Destroy();
}
_registrationOrder.Clear();
_tickables.Clear();
_lateTickables.Clear();
_fixedTickables.Clear();
_gizmoDrawables.Clear();
IsDisposed = true;
}
private T RegisterInternal<T>(T service, ServiceContracts contracts) where T : class, IService
{
EnsureNotDisposed();
if (service == null)
throw new ArgumentNullException(nameof(service));
if (service is not IServiceLifecycle lifecycle)
throw new InvalidOperationException(ZString.Format("Service {0} must implement {1}.", service.GetType().FullName, nameof(IServiceLifecycle)));
ValidateService(service);
if (_entriesByService.ContainsKey(service))
throw new InvalidOperationException(ZString.Format("Service {0} is already registered in scope {1}.", service.GetType().FullName, Name));
if (_isIterating)
{
ValidateContracts(contracts);
_pendingChanges.Add(PendingChange.Add(service, contracts));
return service;
}
ValidateContracts(contracts);
lifecycle.Initialize(new ServiceContext(World, this));
AddEntry(service, contracts);
return service;
}
private void ValidateContracts(ServiceContracts contracts)
{
for (var i = 0; i < contracts.Count; i++)
{
var contract = contracts[i];
if (_servicesByContract.TryGetValue(contract.TypeHandle, out var existing))
{
throw new InvalidOperationException(
ZString.Format("Scope {0} already contains contract {1} bound to {2}.", Name, contract.FullName, existing.GetType().FullName));
}
}
for (var i = 0; i < _pendingChanges.Count; i++)
{
var change = _pendingChanges[i];
if (!change.IsAdd) continue;
for (var contractIndex = 0; contractIndex < contracts.Count; contractIndex++)
{
var contract = contracts[contractIndex];
for (var pendingContractIndex = 0; pendingContractIndex < change.Contracts.Count; pendingContractIndex++)
{
if (!change.Contracts[pendingContractIndex].TypeHandle.Equals(contract.TypeHandle)) continue;
throw new InvalidOperationException(
ZString.Format("Scope {0} already contains pending contract {1}.", Name, contract.FullName));
}
}
}
}
private void AddEntry(IService service, ServiceContracts contracts)
{
var entry = new ServiceEntry(contracts, _registrationOrder.Count);
_registrationOrder.Add(service);
for (var i = 0; i < contracts.Count; i++)
{
var contract = contracts[i];
_servicesByContract.Add(contract.TypeHandle, service);
World.AddContract(this, contract, service);
}
AddToLifecycleLists(service, ref entry);
_entriesByService.Add(service, entry);
}
private void RemoveEntry(IService service, ServiceEntry entry, bool removeRegistrationOrder)
{
_entriesByService.Remove(service);
RemoveFromLifecycleLists(service, entry);
for (var i = 0; i < entry.Contracts.Count; i++)
{
var contract = entry.Contracts[i];
_servicesByContract.Remove(contract.TypeHandle);
World.RemoveContract(this, contract, service);
}
if (!removeRegistrationOrder) return;
var lastIndex = _registrationOrder.Count - 1;
var removedIndex = entry.RegistrationIndex;
var lastService = _registrationOrder[lastIndex];
_registrationOrder[removedIndex] = lastService;
_registrationOrder.RemoveAt(lastIndex);
if (removedIndex != lastIndex && lastService != null && _entriesByService.TryGetValue(lastService, out var lastEntry))
{
lastEntry.RegistrationIndex = removedIndex;
_entriesByService[lastService] = lastEntry;
}
}
private void EnsureNotDisposed()
{
if (IsDisposed) throw new ObjectDisposedException(Name);
@ -186,115 +282,202 @@ namespace AlicizaX
service is IServiceGizmoDrawable))
{
throw new InvalidOperationException(
$"Mono service {service.GetType().FullName} cannot implement tick lifecycle interfaces.");
ZString.Format("Mono service {0} cannot implement tick lifecycle interfaces.", service.GetType().FullName));
}
}
private void AddToLifecycleLists(IService service)
private void AddToLifecycleLists(IService service, ref ServiceEntry entry)
{
if (service is IServiceTickable tickable)
{
entry.TickIndex = _tickables.Count;
_tickables.Add(tickable);
_tickablesDirty = true;
}
if (service is IServiceLateTickable late)
{
entry.LateTickIndex = _lateTickables.Count;
_lateTickables.Add(late);
_lateTickablesDirty = true;
}
if (service is IServiceFixedTickable fixed_)
{
entry.FixedTickIndex = _fixedTickables.Count;
_fixedTickables.Add(fixed_);
_fixedTickablesDirty = true;
}
if (service is IServiceGizmoDrawable gizmo)
{
entry.GizmoIndex = _gizmoDrawables.Count;
_gizmoDrawables.Add(gizmo);
_gizmoDrawablesDirty = true;
}
}
private void RemoveFromLifecycleLists(IService service)
private void RemoveFromLifecycleLists(IService service, ServiceEntry entry)
{
if (service is IServiceTickable tickable && _tickables.Remove(tickable)) _tickablesDirty = true;
if (service is IServiceLateTickable late && _lateTickables.Remove(late)) _lateTickablesDirty = true;
if (service is IServiceFixedTickable fixed_ && _fixedTickables.Remove(fixed_)) _fixedTickablesDirty = true;
if (service is IServiceGizmoDrawable gizmo && _gizmoDrawables.Remove(gizmo)) _gizmoDrawablesDirty = true;
if (entry.TickIndex != MissingIndex) RemoveTickableAt(entry.TickIndex);
if (entry.LateTickIndex != MissingIndex) RemoveLateTickableAt(entry.LateTickIndex);
if (entry.FixedTickIndex != MissingIndex) RemoveFixedTickableAt(entry.FixedTickIndex);
if (entry.GizmoIndex != MissingIndex) RemoveGizmoDrawableAt(entry.GizmoIndex);
}
private void RemoveBindings(IService service)
private void RemoveTickableAt(int index)
{
if (_contractsByService.TryGetValue(service, out var contracts))
var lastIndex = _tickables.Count - 1;
var moved = _tickables[lastIndex];
_tickables[index] = moved;
_tickables.RemoveAt(lastIndex);
if (index != lastIndex) UpdateTickIndex(moved, index);
_tickablesDirty = true;
}
private void RemoveLateTickableAt(int index)
{
var lastIndex = _lateTickables.Count - 1;
var moved = _lateTickables[lastIndex];
_lateTickables[index] = moved;
_lateTickables.RemoveAt(lastIndex);
if (index != lastIndex) UpdateLateTickIndex(moved, index);
_lateTickablesDirty = true;
}
private void RemoveFixedTickableAt(int index)
{
var lastIndex = _fixedTickables.Count - 1;
var moved = _fixedTickables[lastIndex];
_fixedTickables[index] = moved;
_fixedTickables.RemoveAt(lastIndex);
if (index != lastIndex) UpdateFixedTickIndex(moved, index);
_fixedTickablesDirty = true;
}
private void RemoveGizmoDrawableAt(int index)
{
var lastIndex = _gizmoDrawables.Count - 1;
var moved = _gizmoDrawables[lastIndex];
_gizmoDrawables[index] = moved;
_gizmoDrawables.RemoveAt(lastIndex);
if (index != lastIndex) UpdateGizmoIndex(moved, index);
_gizmoDrawablesDirty = true;
}
private void UpdateTickIndex(IServiceTickable service, int index)
{
if (!_entriesByService.TryGetValue((IService)service, out var entry)) return;
entry.TickIndex = index;
_entriesByService[(IService)service] = entry;
}
private void UpdateLateTickIndex(IServiceLateTickable service, int index)
{
if (!_entriesByService.TryGetValue((IService)service, out var entry)) return;
entry.LateTickIndex = index;
_entriesByService[(IService)service] = entry;
}
private void UpdateFixedTickIndex(IServiceFixedTickable service, int index)
{
if (!_entriesByService.TryGetValue((IService)service, out var entry)) return;
entry.FixedTickIndex = index;
_entriesByService[(IService)service] = entry;
}
private void UpdateGizmoIndex(IServiceGizmoDrawable service, int index)
{
if (!_entriesByService.TryGetValue((IService)service, out var entry)) return;
entry.GizmoIndex = index;
_entriesByService[(IService)service] = entry;
}
private void FlushPendingChanges()
{
if (_pendingChanges.Count == 0) return;
for (var i = 0; i < _pendingChanges.Count; i++)
{
for (var i = 0; i < contracts.Count; i++)
_servicesByContract.Remove(contracts[i]);
var change = _pendingChanges[i];
if (change.IsAdd)
{
if (!_entriesByService.ContainsKey(change.Service))
{
((IServiceLifecycle)change.Service).Initialize(new ServiceContext(World, this));
AddEntry(change.Service, change.Contracts);
}
continue;
}
if (_entriesByService.TryGetValue(change.Service, out var entry))
{
RemoveEntry(change.Service, entry, true);
((IServiceLifecycle)change.Service).Destroy();
}
}
_contractsByService.Remove(service);
_registrationOrder.Remove(service);
_pendingChanges.Clear();
}
// Used during full Dispose — skips the O(n) _registrationOrder.Remove since we clear the list afterwards.
private void RemoveContractBindings(IService service)
{
if (_contractsByService.TryGetValue(service, out var contracts))
{
for (var i = 0; i < contracts.Count; i++)
_servicesByContract.Remove(contracts[i]);
}
_contractsByService.Remove(service);
}
private IServiceTickable[] GetTickSnapshot()
private void SortTickablesIfDirty()
{
if (_tickablesDirty)
{
_tickables.Sort(CompareByOrder);
_tickableSnapshot = _tickables.Count > 0 ? _tickables.ToArray() : Array.Empty<IServiceTickable>();
RebuildTickIndices();
_tickablesDirty = false;
}
return _tickableSnapshot;
}
private IServiceLateTickable[] GetLateTickSnapshot()
private void SortLateTickablesIfDirty()
{
if (_lateTickablesDirty)
{
_lateTickables.Sort(CompareByOrder);
_lateTickableSnapshot = _lateTickables.Count > 0 ? _lateTickables.ToArray() : Array.Empty<IServiceLateTickable>();
RebuildLateTickIndices();
_lateTickablesDirty = false;
}
return _lateTickableSnapshot;
}
private IServiceFixedTickable[] GetFixedTickSnapshot()
private void SortFixedTickablesIfDirty()
{
if (_fixedTickablesDirty)
{
_fixedTickables.Sort(CompareByOrder);
_fixedTickableSnapshot = _fixedTickables.Count > 0 ? _fixedTickables.ToArray() : Array.Empty<IServiceFixedTickable>();
RebuildFixedTickIndices();
_fixedTickablesDirty = false;
}
return _fixedTickableSnapshot;
}
private IServiceGizmoDrawable[] GetGizmoSnapshot()
private void SortGizmoDrawablesIfDirty()
{
if (_gizmoDrawablesDirty)
{
_gizmoDrawables.Sort(CompareByOrder);
_gizmoSnapshot = _gizmoDrawables.Count > 0 ? _gizmoDrawables.ToArray() : Array.Empty<IServiceGizmoDrawable>();
RebuildGizmoIndices();
_gizmoDrawablesDirty = false;
}
}
return _gizmoSnapshot;
private void RebuildTickIndices()
{
for (var i = 0; i < _tickables.Count; i++) UpdateTickIndex(_tickables[i], i);
}
private void RebuildLateTickIndices()
{
for (var i = 0; i < _lateTickables.Count; i++) UpdateLateTickIndex(_lateTickables[i], i);
}
private void RebuildFixedTickIndices()
{
for (var i = 0; i < _fixedTickables.Count; i++) UpdateFixedTickIndex(_fixedTickables[i], i);
}
private void RebuildGizmoIndices()
{
for (var i = 0; i < _gizmoDrawables.Count; i++) UpdateGizmoIndex(_gizmoDrawables[i], i);
}
private static int CompareByOrder<T>(T a, T b)
@ -303,5 +486,47 @@ namespace AlicizaX
var right = b is IServiceOrder ob ? ob.Order : 0;
return left.CompareTo(right);
}
private struct ServiceEntry
{
public readonly ServiceContracts Contracts;
public int RegistrationIndex;
public int TickIndex;
public int LateTickIndex;
public int FixedTickIndex;
public int GizmoIndex;
public bool PendingRemove;
public ServiceEntry(ServiceContracts contracts, int registrationIndex)
{
Contracts = contracts;
RegistrationIndex = registrationIndex;
TickIndex = MissingIndex;
LateTickIndex = MissingIndex;
FixedTickIndex = MissingIndex;
GizmoIndex = MissingIndex;
PendingRemove = false;
}
}
private readonly struct PendingChange
{
public readonly bool IsAdd;
public readonly IService Service;
public readonly ServiceContracts Contracts;
private PendingChange(bool isAdd, IService service, ServiceContracts contracts)
{
IsAdd = isAdd;
Service = service;
Contracts = contracts;
}
public static PendingChange Add(IService service, ServiceContracts contracts)
=> new PendingChange(true, service, contracts);
public static PendingChange Remove(IService service)
=> new PendingChange(false, service, default);
}
}
}

View File

@ -1,28 +1,31 @@
using System;
using System.Collections.Generic;
using Cysharp.Text;
namespace AlicizaX
{
internal sealed class ServiceWorld : IDisposable
{
private readonly List<ServiceScope> _scopes = new List<ServiceScope>();
private readonly Dictionary<Type, ServiceScope> _scopesByType = new Dictionary<Type, ServiceScope>();
private const int ScopeSlotCount = 3;
private ServiceScope[] _scopeSnapshot = Array.Empty<ServiceScope>();
private readonly ServiceScope[] _scopesByKind = new ServiceScope[ScopeSlotCount];
private readonly ServiceScope[] _activeScopes = new ServiceScope[ScopeSlotCount];
private readonly Dictionary<RuntimeTypeHandle, ContractBindings> _servicesByContract = new Dictionary<RuntimeTypeHandle, ContractBindings>();
private int _activeScopeCount;
private int _nextScopeCreationIndex;
private bool _scopesDirty;
private ServiceScope _sceneScope;
private ServiceScope _gameplayScope;
internal ServiceWorld(int appScopeOrder = ServiceDomainOrder.App)
{
App = CreateScopeInternal(typeof(AppScope), nameof(App), appScopeOrder);
App = CreateScopeInternal(ServiceScopeKind.App, nameof(App), appScopeOrder);
}
internal ServiceScope App { get; }
internal bool HasScene => _sceneScope != null && !_sceneScope.IsDisposed;
internal bool HasScene => IsAlive(ServiceScopeKind.Scene);
internal bool HasGameplay => _gameplayScope != null && !_gameplayScope.IsDisposed;
internal bool HasGameplay => IsAlive(ServiceScopeKind.Gameplay);
internal ServiceScope Scene
{
@ -30,7 +33,7 @@ namespace AlicizaX
{
if (!HasScene)
throw new InvalidOperationException("Scene scope has not been created yet.");
return _sceneScope;
return _scopesByKind[(int)ServiceScopeKind.Scene];
}
}
@ -40,18 +43,18 @@ namespace AlicizaX
{
if (!HasGameplay)
throw new InvalidOperationException("Gameplay scope has not been created yet.");
return _gameplayScope;
return _scopesByKind[(int)ServiceScopeKind.Gameplay];
}
}
internal ServiceScope EnsureScene(int order = ServiceDomainOrder.Scene)
=> _sceneScope is { IsDisposed: false }
? _sceneScope
: _sceneScope = CreateScopeInternal(typeof(SceneScope), nameof(SceneScope), order);
=> IsAlive(ServiceScopeKind.Scene)
? _scopesByKind[(int)ServiceScopeKind.Scene]
: CreateScopeInternal(ServiceScopeKind.Scene, nameof(SceneScope), order);
internal bool TryGetScene(out ServiceScope scope)
{
scope = HasScene ? _sceneScope : null;
scope = IsAlive(ServiceScopeKind.Scene) ? _scopesByKind[(int)ServiceScopeKind.Scene] : null;
return scope != null;
}
@ -62,35 +65,21 @@ namespace AlicizaX
}
internal bool DestroyScene()
{
if (!HasScene)
return false;
var scope = _sceneScope;
_sceneScope = null;
return DestroyScope(scope, typeof(SceneScope));
}
=> DestroyScope(ServiceScopeKind.Scene);
internal ServiceScope EnsureGameplay(int order = ServiceDomainOrder.Gameplay)
=> _gameplayScope is { IsDisposed: false }
? _gameplayScope
: _gameplayScope = CreateScopeInternal(typeof(GameplayScope), nameof(GameplayScope), order);
=> IsAlive(ServiceScopeKind.Gameplay)
? _scopesByKind[(int)ServiceScopeKind.Gameplay]
: CreateScopeInternal(ServiceScopeKind.Gameplay, nameof(GameplayScope), order);
internal bool TryGetGameplay(out ServiceScope scope)
{
scope = HasGameplay ? _gameplayScope : null;
scope = IsAlive(ServiceScopeKind.Gameplay) ? _scopesByKind[(int)ServiceScopeKind.Gameplay] : null;
return scope != null;
}
internal bool DestroyGameplay()
{
if (!HasGameplay)
return false;
var scope = _gameplayScope;
_gameplayScope = null;
return DestroyScope(scope, typeof(GameplayScope));
}
=> DestroyScope(ServiceScopeKind.Gameplay);
internal bool TryGet<T>(out T service) where T : class, IService
=> TryGet(null, out service);
@ -100,12 +89,10 @@ namespace AlicizaX
if (preferredScope != null && !preferredScope.IsDisposed && preferredScope.TryGet(out service))
return true;
var snapshot = GetScopeSnapshot();
for (var i = snapshot.Length - 1; i >= 0; i--)
if (_servicesByContract.TryGetValue(typeof(T).TypeHandle, out var bindings) && bindings.TryGetBest(out var raw))
{
var scope = snapshot[i];
if (ReferenceEquals(scope, preferredScope)) continue;
if (scope.TryGet(out service)) return true;
service = raw as T;
return service != null;
}
service = null;
@ -117,82 +104,209 @@ namespace AlicizaX
internal T Require<T>(ServiceScope preferredScope) where T : class, IService
{
if (TryGet(preferredScope, out T service)) return service;
throw new InvalidOperationException($"Service {typeof(T).FullName} was not found in any active scope.");
throw new InvalidOperationException(ZString.Format("Service {0} was not found in any active scope.", typeof(T).FullName));
}
internal void Tick(float deltaTime)
{
var snapshot = GetScopeSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].Tick(deltaTime);
SortScopesIfDirty();
for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].Tick(deltaTime);
}
internal void LateTick(float deltaTime)
{
var snapshot = GetScopeSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].LateTick(deltaTime);
SortScopesIfDirty();
for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].LateTick(deltaTime);
}
internal void FixedTick(float fixedDeltaTime)
{
var snapshot = GetScopeSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].FixedTick(fixedDeltaTime);
SortScopesIfDirty();
for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].FixedTick(fixedDeltaTime);
}
internal void DrawGizmos()
{
var snapshot = GetScopeSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].DrawGizmos();
SortScopesIfDirty();
for (var i = 0; i < _activeScopeCount; i++) _activeScopes[i].DrawGizmos();
}
public void Dispose()
{
var snapshot = _scopes.ToArray();
for (var i = snapshot.Length - 1; i >= 0; i--)
snapshot[i].Dispose();
for (var i = _activeScopeCount - 1; i >= 0; i--)
_activeScopes[i].Dispose();
_sceneScope = null;
_gameplayScope = null;
_scopes.Clear();
_scopesByType.Clear();
_scopeSnapshot = Array.Empty<ServiceScope>();
for (var i = 0; i < ScopeSlotCount; i++)
_scopesByKind[i] = null;
_activeScopeCount = 0;
_servicesByContract.Clear();
}
private bool DestroyScope(ServiceScope scope, Type scopeType)
internal void AddContract(ServiceScope scope, Type contract, IService service)
{
if (scope == null)
return false;
var contractHandle = contract.TypeHandle;
if (!_servicesByContract.TryGetValue(contractHandle, out var bindings))
{
bindings = default;
_servicesByContract.Add(contractHandle, bindings);
}
_scopesByType.Remove(scopeType);
_scopes.Remove(scope);
_scopesDirty = true;
bindings.Set(scope.Kind, scope, service);
_servicesByContract[contractHandle] = bindings;
}
internal void RemoveContract(ServiceScope scope, Type contract, IService service)
{
var contractHandle = contract.TypeHandle;
if (!_servicesByContract.TryGetValue(contractHandle, out var bindings)) return;
bindings.Clear(scope.Kind, service);
if (bindings.IsEmpty)
_servicesByContract.Remove(contractHandle);
else
_servicesByContract[contractHandle] = bindings;
}
private bool IsAlive(ServiceScopeKind kind)
{
var scope = _scopesByKind[(int)kind];
return scope != null && !scope.IsDisposed;
}
private bool DestroyScope(ServiceScopeKind kind)
{
if (!IsAlive(kind)) return false;
var scope = _scopesByKind[(int)kind];
_scopesByKind[(int)kind] = null;
RemoveActiveScope(scope);
scope.Dispose();
return true;
}
private ServiceScope CreateScopeInternal(Type scopeType, string scopeName, int order)
private ServiceScope CreateScopeInternal(ServiceScopeKind kind, string scopeName, int order)
{
if (_scopesByType.ContainsKey(scopeType))
throw new InvalidOperationException($"Scope {scopeType.Name} already exists.");
if (IsAlive(kind))
throw new InvalidOperationException(ZString.Format("Scope {0} already exists.", scopeName));
var scope = new ServiceScope(this, scopeName, order);
_scopes.Add(scope);
_scopesByType.Add(scopeType, scope);
_scopes.Sort(CompareScopeOrder);
var scope = new ServiceScope(this, kind, scopeName, order, _nextScopeCreationIndex++);
_scopesByKind[(int)kind] = scope;
_activeScopes[_activeScopeCount++] = scope;
_scopesDirty = true;
return scope;
}
private ServiceScope[] GetScopeSnapshot()
private void RemoveActiveScope(ServiceScope scope)
{
if (_scopesDirty)
for (var i = 0; i < _activeScopeCount; i++)
{
_scopeSnapshot = _scopes.Count > 0 ? _scopes.ToArray() : Array.Empty<ServiceScope>();
_scopesDirty = false;
if (!ReferenceEquals(_activeScopes[i], scope)) continue;
_activeScopeCount--;
_activeScopes[i] = _activeScopes[_activeScopeCount];
_activeScopes[_activeScopeCount] = null;
_scopesDirty = true;
return;
}
return _scopeSnapshot;
}
private static int CompareScopeOrder(ServiceScope left, ServiceScope right)
=> left.Order.CompareTo(right.Order);
private void SortScopesIfDirty()
{
if (!_scopesDirty) return;
for (var i = 1; i < _activeScopeCount; i++)
{
var scope = _activeScopes[i];
var index = i - 1;
while (index >= 0 && HasHigherPriority(_activeScopes[index], scope))
{
_activeScopes[index + 1] = _activeScopes[index];
index--;
}
_activeScopes[index + 1] = scope;
}
_scopesDirty = false;
}
private static bool HasHigherPriority(ServiceScope left, ServiceScope right)
=> left.Order > right.Order || left.Order == right.Order && left.CreationIndex > right.CreationIndex;
private struct ContractBindings
{
private ServiceBinding _app;
private ServiceBinding _scene;
private ServiceBinding _gameplay;
public bool IsEmpty => !_app.HasValue && !_scene.HasValue && !_gameplay.HasValue;
public void Set(ServiceScopeKind kind, ServiceScope scope, IService service)
{
var binding = new ServiceBinding(scope, service);
if (kind == ServiceScopeKind.App)
_app = binding;
else if (kind == ServiceScopeKind.Scene)
_scene = binding;
else
_gameplay = binding;
}
public void Clear(ServiceScopeKind kind, IService service)
{
if (kind == ServiceScopeKind.App)
{
if (_app.HasValue && ReferenceEquals(_app.Service, service)) _app = default;
return;
}
if (kind == ServiceScopeKind.Scene)
{
if (_scene.HasValue && ReferenceEquals(_scene.Service, service)) _scene = default;
return;
}
if (_gameplay.HasValue && ReferenceEquals(_gameplay.Service, service)) _gameplay = default;
}
public bool TryGetBest(out IService service)
{
if (_gameplay.HasValue)
{
service = _gameplay.Service;
return true;
}
if (_scene.HasValue)
{
service = _scene.Service;
return true;
}
if (_app.HasValue)
{
service = _app.Service;
return true;
}
service = null;
return false;
}
}
private struct ServiceBinding
{
public ServiceScope Scope;
public IService Service;
public bool HasValue;
public ServiceBinding(ServiceScope scope, IService service)
{
Scope = scope;
Service = service;
HasValue = true;
}
}
}
}

View File

@ -1,11 +1,10 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace AlicizaX
{
[DefaultExecutionOrder(-32000)]
[DisallowMultipleComponent]
public class AppServiceRoot : MonoBehaviour
public abstract class AppServiceRoot : MonoBehaviour
{
private static AppServiceRoot s_activeRoot;
@ -55,14 +54,13 @@ namespace AlicizaX
if (AppServices.HasWorld) AppServices.RequireWorld().DrawGizmos();
}
protected virtual async void OnDestroy()
protected virtual void OnDestroy()
{
if (s_activeRoot == this)
s_activeRoot = null;
if (_ownsWorld && AppServices.HasWorld)
{
await UniTask.Yield();
AppServices.Shutdown();
}

View File

@ -1,3 +1,4 @@
using Cysharp.Text;
using UnityEngine;
namespace AlicizaX
@ -11,7 +12,7 @@ namespace AlicizaX
void IServiceLifecycle.Initialize(ServiceContext context)
{
if (IsInitialized)
throw new System.InvalidOperationException($"{GetType().FullName} is already initialized.");
throw new System.InvalidOperationException(ZString.Format("{0} is already initialized.", GetType().FullName));
Context = context;
IsInitialized = true;
@ -55,7 +56,7 @@ namespace AlicizaX
if (_dontDestroyOnLoad)
DontDestroyOnLoad(gameObject);
scope.Register(this);
scope.RegisterSelf(this);
}
private void OnDestroy()
@ -69,36 +70,41 @@ namespace AlicizaX
private static ServiceScope ResolveOrCreateScope()
{
if (typeof(TScope) == typeof(AppScope))
return AppServices.RequireWorld().App;
if (typeof(TScope) == typeof(SceneScope))
return AppServices.EnsureScene();
if (typeof(TScope) == typeof(GameplayScope))
return AppServices.EnsureGameplay();
throw new System.InvalidOperationException($"Unsupported service scope: {typeof(TScope).FullName}.");
var kind = ScopeKindCache<TScope>.Kind;
if (kind == ServiceScopeKind.App) return AppServices.RequireWorld().App;
if (kind == ServiceScopeKind.Scene) return AppServices.EnsureScene();
return AppServices.EnsureGameplay();
}
private static bool TryResolveScope(out ServiceScope scope)
{
if (typeof(TScope) == typeof(AppScope))
var kind = ScopeKindCache<TScope>.Kind;
if (kind == ServiceScopeKind.App)
{
scope = AppServices.RequireWorld().App;
return true;
}
if (typeof(TScope) == typeof(SceneScope))
if (kind == ServiceScopeKind.Scene)
return AppServices.TryGetScene(out scope);
if (typeof(TScope) == typeof(GameplayScope))
return AppServices.TryGetGameplay(out scope);
scope = null;
return false;
return AppServices.TryGetGameplay(out scope);
}
protected virtual void OnAwake() { }
}
internal static class ScopeKindCache<TScope>
where TScope : IScope
{
public static readonly ServiceScopeKind Kind = Resolve();
private static ServiceScopeKind Resolve()
{
if (typeof(TScope) == typeof(AppScope)) return ServiceScopeKind.App;
if (typeof(TScope) == typeof(SceneScope)) return ServiceScopeKind.Scene;
if (typeof(TScope) == typeof(GameplayScope)) return ServiceScopeKind.Gameplay;
throw new System.InvalidOperationException(ZString.Format("Unsupported service scope: {0}.", typeof(TScope).FullName));
}
}
}

View File

@ -286,7 +286,7 @@ namespace AlicizaX
return;
}
_loopService = AppServices.RegisterApp(new UtilityLoopService());
_loopService = AppServices.RegisterAppSelf(new UtilityLoopService());
}
private static UtilityLoopService EnsureLoopService()

View File

@ -16,7 +16,7 @@ namespace AlicizaX.Audio.Runtime
private void Awake()
{
_audioService = AppServices.RegisterApp(new AudioService());
_audioService = AppServices.RegisterApp<IAudioService>(new AudioService());
}
private void Start()

View File

@ -8,83 +8,89 @@ namespace AlicizaX.Debugger.Runtime
{
private sealed class TimerInformationWindow : ScrollableDebuggerWindowBase
{
private const int MAX_DISPLAY_COUNT = 50;
private const int MAX_DISPLAY_COUNT = 64;
private const float REFRESH_INTERVAL = 0.25f;
private const string OVERFLOW_NOTE = "Showing first 50 active timers.";
private const string OVERFLOW_NOTE = "Active timer count exceeds visible sample.";
private const string EMPTY_NOTE = "No active timers.";
private const string SAMPLE_NOTE = "Zero-allocation runtime sample view.";
private const string SAMPLE_NOTE = "Runtime view uses fixed buffers and refresh-only style updates.";
private struct UsageView
{
public VisualElement Fill;
}
private struct RowView
{
public VisualElement Root;
public VisualElement Fill;
public VisualElement LoopIndicator;
public VisualElement ScaleIndicator;
public VisualElement StateIndicator;
public VisualElement Fill;
}
private ITimerService _mTimerDebug;
private readonly TimerDebugInfo[] m_TimerInfos = new TimerDebugInfo[MAX_DISPLAY_COUNT];
private readonly RowView[] m_TimerRows = new RowView[MAX_DISPLAY_COUNT];
private VisualElement m_ActiveUsageFill;
private VisualElement m_PeakUsageFill;
private VisualElement m_FreeUsageFill;
private VisualElement m_OverflowNote;
private VisualElement m_EmptyNote;
private float m_RefreshCountdown;
private ITimerService _timerService;
private ITimerDebugService _timerDebugService;
private readonly TimerDebugInfo[] _timerInfos = new TimerDebugInfo[MAX_DISPLAY_COUNT];
private readonly RowView[] _timerRows = new RowView[MAX_DISPLAY_COUNT];
private UsageView _activeUsage;
private UsageView _peakUsage;
private UsageView _freeUsage;
private VisualElement _overflowNote;
private VisualElement _emptyNote;
private float _refreshCountdown;
private int _visibleRowCount;
public override void Initialize(params object[] args)
{
_mTimerDebug = AppServices.Require<ITimerService>();
if (AppServices.TryGet(out _timerService))
{
_timerDebugService = _timerService as ITimerDebugService;
}
}
public override void OnEnter()
{
m_RefreshCountdown = 0f;
_refreshCountdown = 0f;
RefreshContent();
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
m_RefreshCountdown -= realElapseSeconds;
if (m_RefreshCountdown > 0f)
_refreshCountdown -= realElapseSeconds;
if (_refreshCountdown > 0f)
{
return;
}
m_RefreshCountdown = REFRESH_INTERVAL;
_refreshCountdown = REFRESH_INTERVAL;
RefreshContent();
}
protected override void BuildWindow(VisualElement root)
{
if (_mTimerDebug == null)
{
return;
}
root.Add(CreateActionButton("Refresh", RefreshContent, DebuggerTheme.ButtonSurfaceActive, DebuggerTheme.PrimaryText));
VisualElement overview = CreateSection("Timer Pool Overview", out VisualElement overviewCard);
overviewCard.Add(CreateUsageRow("Active Usage", DebuggerTheme.Accent, out m_ActiveUsageFill));
overviewCard.Add(CreateUsageRow("Peak Usage", DebuggerTheme.Warning, out m_PeakUsageFill));
overviewCard.Add(CreateUsageRow("Free Capacity", DebuggerTheme.Positive, out m_FreeUsageFill));
VisualElement overview = CreateSection("Timer Pool", out VisualElement overviewCard);
overviewCard.Add(CreateUsageRow("Active", DebuggerTheme.Accent, out _activeUsage));
overviewCard.Add(CreateUsageRow("Peak", DebuggerTheme.Warning, out _peakUsage));
overviewCard.Add(CreateUsageRow("Free", DebuggerTheme.Positive, out _freeUsage));
root.Add(overview);
VisualElement sample = CreateSection("Timer Sample", out VisualElement sampleCard);
sampleCard.Add(CreateNoteLabel(SAMPLE_NOTE, DebuggerTheme.SecondaryText));
m_OverflowNote = CreateNoteLabel(OVERFLOW_NOTE, new Color(1f, 0.5f, 0f));
m_OverflowNote.style.display = DisplayStyle.None;
sampleCard.Add(m_OverflowNote);
_overflowNote = CreateNoteLabel(OVERFLOW_NOTE, DebuggerTheme.Warning);
_overflowNote.style.display = DisplayStyle.None;
sampleCard.Add(_overflowNote);
m_EmptyNote = CreateNoteLabel(EMPTY_NOTE, DebuggerTheme.SecondaryText);
m_EmptyNote.style.display = DisplayStyle.None;
sampleCard.Add(m_EmptyNote);
_emptyNote = CreateNoteLabel(EMPTY_NOTE, DebuggerTheme.SecondaryText);
_emptyNote.style.display = DisplayStyle.None;
sampleCard.Add(_emptyNote);
_visibleRowCount = 0;
for (int i = 0; i < MAX_DISPLAY_COUNT; i++)
{
m_TimerRows[i] = CreateTimerRow(sampleCard);
m_TimerRows[i].Root.style.display = DisplayStyle.None;
_timerRows[i] = CreateTimerRow(sampleCard);
_timerRows[i].Root.style.display = DisplayStyle.None;
}
root.Add(sample);
@ -93,50 +99,76 @@ namespace AlicizaX.Debugger.Runtime
private void RefreshContent()
{
if (_mTimerDebug == null || m_ActiveUsageFill == null)
if (_emptyNote == null || _overflowNote == null)
{
return;
}
_mTimerDebug.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount);
if (_timerService == null)
{
if (AppServices.TryGet(out _timerService))
{
_timerDebugService = _timerService as ITimerDebugService;
}
if (_timerDebugService == null)
{
SetVisibleRows(0);
return;
}
}
_timerDebugService.GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount);
float capacity = poolCapacity > 0 ? poolCapacity : 1f;
SetFillRatio(m_ActiveUsageFill, activeCount / capacity);
SetFillRatio(m_PeakUsageFill, peakActiveCount / capacity);
SetFillRatio(m_FreeUsageFill, freeCount / capacity);
SetFillRatio(_activeUsage.Fill, activeCount / capacity);
SetFillRatio(_peakUsage.Fill, peakActiveCount / capacity);
SetFillRatio(_freeUsage.Fill, freeCount / capacity);
if (activeCount <= 0)
{
m_EmptyNote.style.display = DisplayStyle.Flex;
m_OverflowNote.style.display = DisplayStyle.None;
SetTimerRowsVisible(0);
_emptyNote.style.display = DisplayStyle.Flex;
_overflowNote.style.display = DisplayStyle.None;
SetVisibleRows(0);
return;
}
m_EmptyNote.style.display = DisplayStyle.None;
int timerCount = _mTimerDebug.GetAllTimers(m_TimerInfos);
int displayCount = timerCount > MAX_DISPLAY_COUNT ? MAX_DISPLAY_COUNT : timerCount;
m_OverflowNote.style.display = activeCount > MAX_DISPLAY_COUNT ? DisplayStyle.Flex : DisplayStyle.None;
_emptyNote.style.display = DisplayStyle.None;
_overflowNote.style.display = activeCount > MAX_DISPLAY_COUNT ? DisplayStyle.Flex : DisplayStyle.None;
int timerCount = _timerDebugService.GetAllTimers(_timerInfos);
int displayCount = timerCount < MAX_DISPLAY_COUNT ? timerCount : MAX_DISPLAY_COUNT;
for (int i = 0; i < displayCount; i++)
{
UpdateTimerRow(ref m_TimerRows[i], ref m_TimerInfos[i]);
UpdateTimerRow(ref _timerRows[i], ref _timerInfos[i]);
}
SetTimerRowsVisible(displayCount);
SetVisibleRows(displayCount);
}
private void SetTimerRowsVisible(int visibleCount)
private void SetVisibleRows(int visibleCount)
{
for (int i = 0; i < MAX_DISPLAY_COUNT; i++)
if (_visibleRowCount == visibleCount)
{
m_TimerRows[i].Root.style.display = i < visibleCount ? DisplayStyle.Flex : DisplayStyle.None;
return;
}
int minCount = _visibleRowCount < visibleCount ? _visibleRowCount : visibleCount;
for (int i = minCount; i < visibleCount; i++)
{
_timerRows[i].Root.style.display = DisplayStyle.Flex;
}
for (int i = visibleCount; i < _visibleRowCount; i++)
{
_timerRows[i].Root.style.display = DisplayStyle.None;
}
_visibleRowCount = visibleCount;
}
private static VisualElement CreateUsageRow(string title, Color fillColor, out VisualElement fill)
private static VisualElement CreateUsageRow(string title, Color fillColor, out UsageView usageView)
{
float scale = DebuggerComponent.Instance != null ? DebuggerComponent.Instance.GetUiScale() : 1f;
float scale = GetScale();
VisualElement row = new VisualElement();
row.style.flexDirection = FlexDirection.Column;
row.style.marginBottom = 8f * scale;
@ -148,80 +180,72 @@ namespace AlicizaX.Debugger.Runtime
titleLabel.style.marginBottom = 4f * scale;
row.Add(titleLabel);
VisualElement track = new VisualElement();
track.style.height = 14f * scale;
track.style.backgroundColor = DebuggerTheme.PanelSurfaceStrong;
track.style.borderTopLeftRadius = 4f * scale;
track.style.borderTopRightRadius = 4f * scale;
track.style.borderBottomLeftRadius = 4f * scale;
track.style.borderBottomRightRadius = 4f * scale;
track.style.overflow = Overflow.Hidden;
fill = new VisualElement();
VisualElement track = CreateTrack(14f * scale);
VisualElement fill = new VisualElement();
fill.style.height = Length.Percent(100f);
fill.style.width = Length.Percent(0f);
fill.style.backgroundColor = fillColor;
track.Add(fill);
row.Add(track);
usageView.Fill = fill;
return row;
}
private static VisualElement CreateNoteLabel(string text, Color color)
{
float scale = DebuggerComponent.Instance != null ? DebuggerComponent.Instance.GetUiScale() : 1f;
Label label = new Label(text);
label.style.color = color;
label.style.fontSize = 15f * scale;
label.style.marginBottom = 6f * scale;
label.style.whiteSpace = WhiteSpace.Normal;
return label;
}
private static RowView CreateTimerRow(VisualElement parent)
{
float scale = DebuggerComponent.Instance != null ? DebuggerComponent.Instance.GetUiScale() : 1f;
float scale = GetScale();
VisualElement row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.height = 20f * scale;
row.style.marginBottom = 4f * scale;
VisualElement loopIndicator = CreateIndicator(6f * scale, DebuggerTheme.Accent);
VisualElement scaleIndicator = CreateIndicator(6f * scale, DebuggerTheme.Warning);
VisualElement stateIndicator = CreateIndicator(6f * scale, DebuggerTheme.Positive);
row.Add(loopIndicator);
row.Add(scaleIndicator);
row.Add(stateIndicator);
VisualElement track = new VisualElement();
track.style.flexGrow = 1f;
track.style.height = 14f * scale;
track.style.backgroundColor = DebuggerTheme.PanelSurfaceStrong;
track.style.borderTopLeftRadius = 4f * scale;
track.style.borderTopRightRadius = 4f * scale;
track.style.borderBottomLeftRadius = 4f * scale;
track.style.borderBottomRightRadius = 4f * scale;
track.style.overflow = Overflow.Hidden;
VisualElement fill = new VisualElement();
fill.style.height = Length.Percent(100f);
fill.style.width = Length.Percent(0f);
fill.style.backgroundColor = DebuggerTheme.Positive;
track.Add(fill);
row.Add(track);
parent.Add(row);
RowView view;
view.Root = row;
view.LoopIndicator = loopIndicator;
view.ScaleIndicator = scaleIndicator;
view.StateIndicator = stateIndicator;
view.Fill = fill;
view.LoopIndicator = CreateIndicator(6f * scale, DebuggerTheme.Accent);
view.ScaleIndicator = CreateIndicator(6f * scale, DebuggerTheme.Warning);
view.StateIndicator = CreateIndicator(6f * scale, DebuggerTheme.Positive);
row.Add(view.LoopIndicator);
row.Add(view.ScaleIndicator);
row.Add(view.StateIndicator);
VisualElement track = CreateTrack(14f * scale);
view.Fill = new VisualElement();
view.Fill.style.height = Length.Percent(100f);
view.Fill.style.width = Length.Percent(0f);
view.Fill.style.backgroundColor = DebuggerTheme.Positive;
track.Add(view.Fill);
row.Add(track);
parent.Add(row);
return view;
}
private static VisualElement CreateTrack(float height)
{
VisualElement track = new VisualElement();
track.style.flexGrow = 1f;
track.style.height = height;
track.style.backgroundColor = DebuggerTheme.PanelSurfaceStrong;
track.style.borderTopLeftRadius = 4f;
track.style.borderTopRightRadius = 4f;
track.style.borderBottomLeftRadius = 4f;
track.style.borderBottomRightRadius = 4f;
track.style.overflow = Overflow.Hidden;
return track;
}
private static Label CreateNoteLabel(string text, Color color)
{
float scale = GetScale();
Label label = new Label(text);
label.style.color = color;
label.style.fontSize = 15f * scale;
label.style.marginBottom = 6f * scale;
label.style.whiteSpace = WhiteSpace.Normal;
return label;
}
private static VisualElement CreateIndicator(float size, Color color)
{
VisualElement indicator = new VisualElement();
@ -239,31 +263,24 @@ namespace AlicizaX.Debugger.Runtime
bool isRunning = (flags & TimerDebugFlags.Running) != 0;
bool isLoop = (flags & TimerDebugFlags.Loop) != 0;
bool isUnscaled = (flags & TimerDebugFlags.Unscaled) != 0;
float duration = info.Duration;
float ratio = duration > 0f ? info.LeftTime / duration : 0f;
if (ratio < 0f)
{
ratio = 0f;
}
else if (ratio > 1f)
{
ratio = 1f;
}
float ratio = info.Duration > 0f ? info.LeftTime / info.Duration : 0f;
row.Root.style.display = DisplayStyle.Flex;
row.LoopIndicator.style.opacity = isLoop ? 1f : 0.2f;
row.ScaleIndicator.style.opacity = isUnscaled ? 1f : 0.35f;
row.ScaleIndicator.style.backgroundColor = isUnscaled ? DebuggerTheme.Warning : DebuggerTheme.Accent;
row.StateIndicator.style.opacity = isRunning ? 1f : 0.45f;
row.StateIndicator.style.backgroundColor = isRunning ? DebuggerTheme.Positive : DebuggerTheme.Warning;
row.Fill.style.width = Length.Percent(ratio * 100f);
row.Fill.style.backgroundColor = isRunning
? ratio <= 0.2f ? DebuggerTheme.Warning : DebuggerTheme.Positive
: DebuggerTheme.SecondaryText;
row.Fill.style.backgroundColor = isRunning ? ratio <= 0.2f ? DebuggerTheme.Warning : DebuggerTheme.Positive : DebuggerTheme.SecondaryText;
SetFillRatio(row.Fill, ratio);
}
private static void SetFillRatio(VisualElement fill, float ratio)
{
if (fill == null)
{
return;
}
if (ratio < 0f)
{
ratio = 0f;
@ -275,6 +292,11 @@ namespace AlicizaX.Debugger.Runtime
fill.style.width = Length.Percent(ratio * 100f);
}
private static float GetScale()
{
return Instance != null ? Instance.GetUiScale() : 1f;
}
}
}
}

View File

@ -263,7 +263,7 @@ namespace AlicizaX.Debugger.Runtime
if (!AppServices.TryGetApp<IDebuggerService>(out _mDebuggerService))
{
_mDebuggerService = AppServices.RegisterApp(new DebuggerService());
_mDebuggerService = AppServices.RegisterApp<IDebuggerService>(new DebuggerService());
}
if (_mDebuggerService == null)

View File

@ -55,7 +55,7 @@ namespace AlicizaX.Localization.Runtime
{
if (!AppServices.TryGetApp<ILocalizationService>(out _mLocalizationService))
{
_mLocalizationService = AppServices.RegisterApp(new LocalizationService());
_mLocalizationService = AppServices.RegisterApp<ILocalizationService>(new LocalizationService());
}
if (_mLocalizationService == null)

View File

@ -615,13 +615,11 @@ namespace AlicizaX
{
MemoryPool<BenchmarkMemory>.ClearAll();
MemoryPool<BenchmarkMemory>.Prewarm(objectCount);
Type type = typeof(BenchmarkMemory);
RestartCaseMeasure();
for (int i = 0; i < loopCount; i++)
{
IMemory item = MemoryPool.Acquire(type);
MemoryPool.Release(item);
BenchmarkMemory item = MemoryPool.Acquire<BenchmarkMemory>();
MemoryPool.Release<BenchmarkMemory>(item);
}
StopCaseMeasure();

View File

@ -14,7 +14,7 @@ namespace AlicizaX
private void Awake()
{
_mObjectPoolService = AppServices.RegisterApp(new ObjectPoolService());
_mObjectPoolService = AppServices.RegisterApp<IObjectPoolService>(new ObjectPoolService());
Application.lowMemory += OnLowMemory;
}

View File

@ -32,16 +32,25 @@ namespace AlicizaX.ObjectPool
}
}
private ObjectSlot[] m_Slots;
private int m_SlotCount;
private int[] m_FreeStack;
private int m_FreeTop;
private ObjectSlot[][] m_Pages;
private int[][] m_PageFreeStacks;
private int[] m_PageAliveCounts;
private int[] m_PageFreeTops;
private byte[] m_PageFlags;
private int m_PageCount;
private int[] m_FreePageStack;
private int m_FreePageTop;
private int[] m_EmptyPageStack;
private int m_EmptyPageTop;
private int[] m_ReleasedPageStack;
private int m_ReleasedPageTop;
private ReferenceOpenHashMap m_TargetMap;
private StringOpenHashMap m_AllNameHeadMap;
private StringOpenHashMap m_NameCursorMap;
private readonly bool m_AllowMultiSpawn;
private readonly MemoryPoolHandle m_ObjectMemoryPoolHandle;
private float m_AutoReleaseInterval;
private int m_Capacity;
private float m_ExpireTime;
@ -59,18 +68,37 @@ namespace AlicizaX.ObjectPool
private const int DefaultReleasePerFrame = 8;
private const int InitSlotCapacity = 16;
private const int InitPageCapacity = 4;
private const int PageBits = 8;
private const int PageSize = 1 << PageBits;
private const int PageMask = PageSize - 1;
private const byte PageAllocated = 1;
private const byte PageInFreeStack = 2;
private const byte PageInEmptyStack = 4;
private const int EmptyPageReleaseBudget = 1;
public ObjectPool(string name, bool allowMultiSpawn,
float autoReleaseInterval, int capacity, float expireTime, int priority)
: base(name)
{
int initCap = Math.Min(Math.Max(capacity, 1), InitSlotCapacity);
m_Slots = SlotArrayPool<ObjectSlot>.Rent(initCap);
m_FreeStack = SlotArrayPool<int>.Rent(initCap);
m_Pages = SlotArrayPool<ObjectSlot[]>.Rent(InitPageCapacity);
m_PageFreeStacks = SlotArrayPool<int[]>.Rent(InitPageCapacity);
m_PageAliveCounts = SlotArrayPool<int>.Rent(InitPageCapacity);
m_PageFreeTops = SlotArrayPool<int>.Rent(InitPageCapacity);
m_PageFlags = SlotArrayPool<byte>.Rent(InitPageCapacity);
m_FreePageStack = SlotArrayPool<int>.Rent(InitPageCapacity);
m_EmptyPageStack = SlotArrayPool<int>.Rent(InitPageCapacity);
m_ReleasedPageStack = SlotArrayPool<int>.Rent(InitPageCapacity);
m_TargetMap = new ReferenceOpenHashMap(initCap);
m_AllNameHeadMap = new StringOpenHashMap(initCap);
m_NameCursorMap = new StringOpenHashMap(initCap);
m_PageCount = 0;
m_FreePageTop = 0;
m_EmptyPageTop = 0;
m_ReleasedPageTop = 0;
m_AllowMultiSpawn = allowMultiSpawn;
m_ObjectMemoryPoolHandle = MemoryPool.GetHandle(typeof(T));
m_AutoReleaseInterval = autoReleaseInterval;
m_Capacity = capacity;
m_ExpireTime = expireTime;
@ -155,7 +183,7 @@ namespace AlicizaX.ObjectPool
if (obj == null) return;
if (obj.Target == null) return;
if (m_TargetMap.TryGetValue(obj.Target, out int existingIdx) && m_Slots[existingIdx].IsAlive())
if (m_TargetMap.TryGetValue(obj.Target, out int existingIdx) && GetSlotRef(existingIdx).IsAlive())
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Target '{obj.Target.GetType().FullName}' is already registered in pool '{FullName}'.");
@ -175,7 +203,7 @@ namespace AlicizaX.ObjectPool
if (idx < 0)
return;
ref var slot = ref m_Slots[idx];
ref var slot = ref GetSlotRef(idx);
slot.Obj = obj;
slot.SpawnCount = spawned ? 1 : 0;
slot.LastUseTime = Time.realtimeSinceStartup;
@ -190,7 +218,7 @@ namespace AlicizaX.ObjectPool
string objectName = obj.Name ?? string.Empty;
if (m_AllNameHeadMap.TryGetValue(objectName, out int existingHead))
{
m_Slots[existingHead].PrevByName = idx;
GetSlotRef(existingHead).PrevByName = idx;
slot.NextByName = existingHead;
}
else
@ -221,7 +249,7 @@ namespace AlicizaX.ObjectPool
int head = FindAvailableByName(name);
if (head < 0) return null;
ref var slot = ref m_Slots[head];
ref var slot = ref GetSlotRef(head);
if (!slot.IsAlive() || slot.SpawnCount != 0 || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
{
#if UNITY_EDITOR
@ -253,7 +281,7 @@ namespace AlicizaX.ObjectPool
if (!m_AllNameHeadMap.TryGetValue(name, out int head))
return -1;
ref var headSlot = ref m_Slots[head];
ref var headSlot = ref GetSlotRef(head);
if (headSlot.IsAlive()
&& headSlot.SpawnCount == 0
&& string.Equals(headSlot.Obj.Name, name, StringComparison.Ordinal))
@ -269,7 +297,7 @@ namespace AlicizaX.ObjectPool
do
{
ref var slot = ref m_Slots[current];
ref var slot = ref GetSlotRef(current);
if (slot.IsAlive()
&& slot.SpawnCount == 0
&& string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
@ -288,11 +316,9 @@ namespace AlicizaX.ObjectPool
private int GetValidNameCursor(string name, int head)
{
if (m_NameCursorMap.TryGetValue(name, out int cursor)
&& cursor >= 0
&& cursor < m_SlotCount)
if (m_NameCursorMap.TryGetValue(name, out int cursor) && IsValidIndex(cursor))
{
ref var slot = ref m_Slots[cursor];
ref var slot = ref GetSlotRef(cursor);
if (slot.IsAlive() && string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
return cursor;
}
@ -339,8 +365,8 @@ namespace AlicizaX.ObjectPool
int current = m_UnusedHead;
while (current >= 0)
{
int next = m_Slots[current].NextUnused;
ref var slot = ref m_Slots[current];
int next = GetSlotRef(current).NextUnused;
ref var slot = ref GetSlotRef(current);
if (CanReleaseSlot(ref slot))
{
ReleaseSlot(current, false);
@ -352,8 +378,7 @@ namespace AlicizaX.ObjectPool
if (released > 0)
{
m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released);
TrimSlotCountTail();
ShrinkStorageIfEmpty();
ReleaseEmptyPages(EmptyPageReleaseBudget);
UpdateActiveState();
ValidateState();
}
@ -400,66 +425,39 @@ namespace AlicizaX.ObjectPool
return;
m_ShrinkCounter = 0;
TrimSlotCountTail();
int slotArrayLen = m_Slots.Length;
int aliveCount = m_TargetMap.Count;
if (aliveCount == 0 || slotArrayLen <= InitSlotCapacity)
return;
float usageRatio = (float)aliveCount / slotArrayLen;
if (usageRatio < 0.25f)
{
int targetCapacity = Math.Max(NextPowerOf2(Math.Max(m_SlotCount, aliveCount)), InitSlotCapacity);
if (targetCapacity < slotArrayLen && m_SlotCount <= targetCapacity)
{
var newSlots = SlotArrayPool<ObjectSlot>.Rent(targetCapacity);
var newFreeStack = SlotArrayPool<int>.Rent(targetCapacity);
Array.Copy(m_Slots, 0, newSlots, 0, m_SlotCount);
int newFreeTop = 0;
for (int i = 0; i < m_FreeTop; i++)
{
if (m_FreeStack[i] < targetCapacity)
newFreeStack[newFreeTop++] = m_FreeStack[i];
}
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = newSlots;
m_FreeStack = newFreeStack;
m_FreeTop = newFreeTop;
}
}
ReleaseEmptyPages(EmptyPageReleaseBudget);
}
internal override void Shutdown()
{
m_IsShuttingDown = true;
for (int i = 0; i < m_SlotCount; i++)
for (int page = 0; page < m_PageCount; page++)
{
ref var slot = ref m_Slots[i];
if (!slot.IsAlive()) continue;
slot.Obj.Release(true);
MemoryPool.Release(slot.Obj);
slot.Obj = null;
slot.SetAlive(false);
ObjectSlot[] slots = m_Pages[page];
if (slots == null) continue;
for (int offset = 0; offset < PageSize; offset++)
{
ref var slot = ref slots[offset];
if (!slot.IsAlive()) continue;
slot.Obj.Release(true);
m_ObjectMemoryPoolHandle.Release(slot.Obj);
slot.Obj = null;
slot.SetAlive(false);
}
}
m_TargetMap.Clear();
m_AllNameHeadMap.Clear();
m_NameCursorMap.Clear();
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = null;
m_FreeStack = null;
ReleaseAllPages();
ReturnPageStorage();
m_SlotCount = 0;
m_FreeTop = 0;
m_PageCount = 0;
m_FreePageTop = 0;
m_EmptyPageTop = 0;
m_ReleasedPageTop = 0;
m_PendingReleaseCount = 0;
m_UnusedHead = -1;
m_UnusedTail = -1;
@ -480,19 +478,25 @@ namespace AlicizaX.ObjectPool
int write = 0;
int capacity = results.Length;
for (int i = 0; i < m_SlotCount; i++)
for (int page = 0; page < m_PageCount; page++)
{
ref var slot = ref m_Slots[i];
if (!slot.IsAlive()) continue;
ObjectSlot[] slots = m_Pages[page];
if (slots == null) continue;
if (write < capacity)
for (int offset = 0; offset < PageSize; offset++)
{
results[write] = new ObjectInfo(slot.Obj.Name, slot.Obj.Locked,
slot.Obj.CustomCanReleaseFlag,
slot.Obj.LastUseTime, slot.SpawnCount);
}
ref var slot = ref slots[offset];
if (!slot.IsAlive()) continue;
write++;
if (write < capacity)
{
results[write] = new ObjectInfo(slot.Obj.Name, slot.Obj.Locked,
slot.Obj.CustomCanReleaseFlag,
slot.Obj.LastUseTime, slot.SpawnCount);
}
write++;
}
}
return write;
@ -506,7 +510,7 @@ namespace AlicizaX.ObjectPool
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SpawnSlot(int idx, float now)
{
ref var slot = ref m_Slots[idx];
ref var slot = ref GetSlotRef(idx);
if (slot.SpawnCount == 0)
MarkSlotUnavailable(idx);
@ -519,7 +523,7 @@ namespace AlicizaX.ObjectPool
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UnspawnSlot(int idx)
{
ref var slot = ref m_Slots[idx];
ref var slot = ref GetSlotRef(idx);
float now = Time.realtimeSinceStartup;
slot.LastUseTime = now;
slot.Obj.LastUseTime = now;
@ -548,43 +552,65 @@ namespace AlicizaX.ObjectPool
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int AllocSlot()
{
if (m_FreeTop > 0)
return m_FreeStack[--m_FreeTop];
int page = GetWritablePage();
if (page < 0)
return -1;
if (m_SlotCount >= m_Slots.Length)
{
GrowSlots();
if (m_SlotCount >= m_Slots.Length)
return -1;
}
int freeTop = m_PageFreeTops[page] - 1;
int offset = m_PageFreeStacks[page][freeTop];
m_PageFreeTops[page] = freeTop;
m_PageAliveCounts[page]++;
return m_SlotCount++;
if (freeTop == 0)
RemoveFreePage(page);
return MakeIndex(page, offset);
}
private void GrowSlots()
private int GetWritablePage()
{
int currentCap = m_Slots.Length;
int maxCap = m_Capacity == int.MaxValue ? int.MaxValue : Math.Max(m_Capacity, InitSlotCapacity);
int newCap = Math.Min(Math.Max(currentCap * 2, InitSlotCapacity), maxCap);
if (newCap <= currentCap)
return;
while (m_FreePageTop > 0)
{
int page = m_FreePageStack[m_FreePageTop - 1];
if (IsAllocatedPage(page) && m_PageFreeTops[page] > 0)
return page;
var newSlots = SlotArrayPool<ObjectSlot>.Rent(newCap);
var newFreeStack = SlotArrayPool<int>.Rent(newCap);
m_FreePageTop--;
if (page >= 0 && page < m_PageFlags.Length)
m_PageFlags[page] = (byte)(m_PageFlags[page] & ~PageInFreeStack);
}
Array.Copy(m_Slots, 0, newSlots, 0, m_SlotCount);
Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop);
return AllocatePage();
}
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
private int AllocatePage()
{
int page;
if (m_ReleasedPageTop > 0)
{
page = m_ReleasedPageStack[--m_ReleasedPageTop];
}
else
{
page = m_PageCount++;
EnsurePageStorageCapacity(m_PageCount);
}
m_Slots = newSlots;
m_FreeStack = newFreeStack;
m_Pages[page] = SlotArrayPool<ObjectSlot>.Rent(PageSize);
m_PageFreeStacks[page] = SlotArrayPool<int>.Rent(PageSize);
for (int offset = 0; offset < PageSize; offset++)
m_PageFreeStacks[page][offset] = offset;
m_PageAliveCounts[page] = 0;
m_PageFreeTops[page] = PageSize;
m_PageFlags[page] = PageAllocated;
AddFreePage(page);
return page;
}
private void ReleaseSlot(int idx, bool compactStorage = true)
{
ref var slot = ref m_Slots[idx];
ref var slot = ref GetSlotRef(idx);
if (!slot.IsAlive()) return;
if (slot.SpawnCount > 0) return;
@ -595,7 +621,7 @@ namespace AlicizaX.ObjectPool
m_TargetMap.Remove(obj.Target);
obj.Release(false);
MemoryPool.Release(obj);
m_ObjectMemoryPoolHandle.Release(obj);
slot.Obj = null;
slot.SetAlive(false);
@ -605,21 +631,19 @@ namespace AlicizaX.ObjectPool
slot.PrevUnused = -1;
slot.NextUnused = -1;
if (m_FreeTop >= m_FreeStack.Length)
{
int newCap = m_FreeStack.Length * 2;
var newFreeStack = SlotArrayPool<int>.Rent(newCap);
Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_FreeStack = newFreeStack;
}
m_FreeStack[m_FreeTop++] = idx;
int page = PageOf(idx);
int offset = OffsetOf(idx);
m_PageFreeStacks[page][m_PageFreeTops[page]++] = offset;
m_PageAliveCounts[page]--;
if (m_PageFreeTops[page] == 1)
AddFreePage(page);
if (m_PageAliveCounts[page] == 0)
AddEmptyPageCandidate(page);
if (compactStorage)
{
TrimSlotCountTail();
ShrinkStorageIfEmpty();
}
ReleaseEmptyPages(EmptyPageReleaseBudget);
}
private bool EnsureRegisterCapacity()
@ -637,36 +661,194 @@ namespace AlicizaX.ObjectPool
return false;
}
private void TrimSlotCountTail()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int MakeIndex(int page, int offset)
{
while (m_SlotCount > 0 && !m_Slots[m_SlotCount - 1].IsAlive())
m_SlotCount--;
int write = 0;
for (int i = 0; i < m_FreeTop; i++)
{
int freeIndex = m_FreeStack[i];
if (freeIndex < m_SlotCount)
m_FreeStack[write++] = freeIndex;
}
m_FreeTop = write;
return (page << PageBits) | offset;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int NextPowerOf2(int value)
private static int PageOf(int index)
{
value--;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return value + 1;
return index >> PageBits;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int OffsetOf(int index)
{
return index & PageMask;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref ObjectSlot GetSlotRef(int index)
{
return ref m_Pages[index >> PageBits][index & PageMask];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsAllocatedPage(int page)
{
return page >= 0
&& page < m_PageCount
&& (m_PageFlags[page] & PageAllocated) != 0
&& m_Pages[page] != null;
}
private void EnsurePageStorageCapacity(int required)
{
if (required <= m_Pages.Length)
return;
int newCap = Math.Max(required, m_Pages.Length * 2);
var newPages = SlotArrayPool<ObjectSlot[]>.Rent(newCap);
var newPageFreeStacks = SlotArrayPool<int[]>.Rent(newCap);
var newPageAliveCounts = SlotArrayPool<int>.Rent(newCap);
var newPageFreeTops = SlotArrayPool<int>.Rent(newCap);
var newPageFlags = SlotArrayPool<byte>.Rent(newCap);
var newFreePageStack = SlotArrayPool<int>.Rent(newCap);
var newEmptyPageStack = SlotArrayPool<int>.Rent(newCap);
var newReleasedPageStack = SlotArrayPool<int>.Rent(newCap);
Array.Copy(m_Pages, 0, newPages, 0, m_PageCount);
Array.Copy(m_PageFreeStacks, 0, newPageFreeStacks, 0, m_PageCount);
Array.Copy(m_PageAliveCounts, 0, newPageAliveCounts, 0, m_PageCount);
Array.Copy(m_PageFreeTops, 0, newPageFreeTops, 0, m_PageCount);
Array.Copy(m_PageFlags, 0, newPageFlags, 0, m_PageCount);
Array.Copy(m_FreePageStack, 0, newFreePageStack, 0, m_FreePageTop);
Array.Copy(m_EmptyPageStack, 0, newEmptyPageStack, 0, m_EmptyPageTop);
Array.Copy(m_ReleasedPageStack, 0, newReleasedPageStack, 0, m_ReleasedPageTop);
SlotArrayPool<ObjectSlot[]>.Return(m_Pages, true);
SlotArrayPool<int[]>.Return(m_PageFreeStacks, true);
SlotArrayPool<int>.Return(m_PageAliveCounts, true);
SlotArrayPool<int>.Return(m_PageFreeTops, true);
SlotArrayPool<byte>.Return(m_PageFlags, true);
SlotArrayPool<int>.Return(m_FreePageStack, true);
SlotArrayPool<int>.Return(m_EmptyPageStack, true);
SlotArrayPool<int>.Return(m_ReleasedPageStack, true);
m_Pages = newPages;
m_PageFreeStacks = newPageFreeStacks;
m_PageAliveCounts = newPageAliveCounts;
m_PageFreeTops = newPageFreeTops;
m_PageFlags = newPageFlags;
m_FreePageStack = newFreePageStack;
m_EmptyPageStack = newEmptyPageStack;
m_ReleasedPageStack = newReleasedPageStack;
}
private static void EnsurePageIndexStackCapacity(ref int[] stack, int required)
{
if (required <= stack.Length)
return;
int newCap = Math.Max(required, stack.Length * 2);
var newStack = SlotArrayPool<int>.Rent(newCap);
Array.Copy(stack, 0, newStack, 0, stack.Length);
SlotArrayPool<int>.Return(stack, true);
stack = newStack;
}
private void AddFreePage(int page)
{
if ((m_PageFlags[page] & PageInFreeStack) != 0)
return;
m_PageFlags[page] = (byte)(m_PageFlags[page] | PageInFreeStack);
EnsurePageIndexStackCapacity(ref m_FreePageStack, m_FreePageTop + 1);
m_FreePageStack[m_FreePageTop++] = page;
}
private void RemoveFreePage(int page)
{
if ((m_PageFlags[page] & PageInFreeStack) == 0)
return;
m_PageFlags[page] = (byte)(m_PageFlags[page] & ~PageInFreeStack);
}
private void AddEmptyPageCandidate(int page)
{
if ((m_PageFlags[page] & PageInEmptyStack) != 0)
return;
m_PageFlags[page] = (byte)(m_PageFlags[page] | PageInEmptyStack);
EnsurePageIndexStackCapacity(ref m_EmptyPageStack, m_EmptyPageTop + 1);
m_EmptyPageStack[m_EmptyPageTop++] = page;
}
private void ReleaseEmptyPages(int budget)
{
while (budget > 0 && m_EmptyPageTop > 0)
{
int page = m_EmptyPageStack[--m_EmptyPageTop];
m_PageFlags[page] = (byte)(m_PageFlags[page] & ~PageInEmptyStack);
if (!IsAllocatedPage(page) || m_PageAliveCounts[page] != 0)
continue;
ReleasePage(page);
AddReleasedPage(page);
budget--;
}
}
private void AddReleasedPage(int page)
{
EnsurePageIndexStackCapacity(ref m_ReleasedPageStack, m_ReleasedPageTop + 1);
m_ReleasedPageStack[m_ReleasedPageTop++] = page;
}
private void ReleasePage(int page)
{
SlotArrayPool<ObjectSlot>.Return(m_Pages[page], true);
SlotArrayPool<int>.Return(m_PageFreeStacks[page], true);
m_Pages[page] = null;
m_PageFreeStacks[page] = null;
m_PageAliveCounts[page] = 0;
m_PageFreeTops[page] = 0;
m_PageFlags[page] = 0;
}
private void ReleaseAllPages()
{
for (int page = 0; page < m_PageCount; page++)
{
if (m_Pages[page] != null)
SlotArrayPool<ObjectSlot>.Return(m_Pages[page], true);
if (m_PageFreeStacks[page] != null)
SlotArrayPool<int>.Return(m_PageFreeStacks[page], true);
m_Pages[page] = null;
m_PageFreeStacks[page] = null;
m_PageAliveCounts[page] = 0;
m_PageFreeTops[page] = 0;
m_PageFlags[page] = 0;
}
}
private void ReturnPageStorage()
{
SlotArrayPool<ObjectSlot[]>.Return(m_Pages, true);
SlotArrayPool<int[]>.Return(m_PageFreeStacks, true);
SlotArrayPool<int>.Return(m_PageAliveCounts, true);
SlotArrayPool<int>.Return(m_PageFreeTops, true);
SlotArrayPool<byte>.Return(m_PageFlags, true);
SlotArrayPool<int>.Return(m_FreePageStack, true);
SlotArrayPool<int>.Return(m_EmptyPageStack, true);
SlotArrayPool<int>.Return(m_ReleasedPageStack, true);
m_Pages = null;
m_PageFreeStacks = null;
m_PageAliveCounts = null;
m_PageFreeTops = null;
m_PageFlags = null;
m_FreePageStack = null;
m_EmptyPageStack = null;
m_ReleasedPageStack = null;
}
private void RemoveFromAllNameChain(int idx)
{
ref var slot = ref m_Slots[idx];
ref var slot = ref GetSlotRef(idx);
string objectName = slot.Obj.Name ?? string.Empty;
if (!m_AllNameHeadMap.TryGetValue(objectName, out int head))
return;
@ -675,7 +857,7 @@ namespace AlicizaX.ObjectPool
int next = slot.NextByName;
if (prev >= 0)
{
m_Slots[prev].NextByName = next;
GetSlotRef(prev).NextByName = next;
}
else
{
@ -701,7 +883,7 @@ namespace AlicizaX.ObjectPool
}
if (next >= 0)
m_Slots[next].PrevByName = prev;
GetSlotRef(next).PrevByName = prev;
slot.PrevByName = -1;
slot.NextByName = -1;
@ -714,7 +896,7 @@ namespace AlicizaX.ObjectPool
while (current >= 0 && released < maxReleaseCount)
{
ref var slot = ref m_Slots[current];
ref var slot = ref GetSlotRef(current);
int next = slot.NextUnused;
if (requireExpired && slot.LastUseTime > expireThreshold)
@ -738,10 +920,7 @@ namespace AlicizaX.ObjectPool
}
if (released > 0)
{
TrimSlotCountTail();
ShrinkStorageIfEmpty();
}
ReleaseEmptyPages(EmptyPageReleaseBudget);
return released;
}
@ -756,7 +935,7 @@ namespace AlicizaX.ObjectPool
{
if (m_LastBudgetScanStart >= 0)
{
ref var slot = ref m_Slots[m_LastBudgetScanStart];
ref var slot = ref GetSlotRef(m_LastBudgetScanStart);
if (slot.IsAlive() && slot.SpawnCount == 0)
{
return m_LastBudgetScanStart;
@ -775,23 +954,12 @@ namespace AlicizaX.ObjectPool
&& slot.Obj.CustomCanReleaseFlag;
}
private void ShrinkStorageIfEmpty()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsValidIndex(int index)
{
if (m_TargetMap.Count > 0 || m_Slots.Length <= InitSlotCapacity)
return;
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = SlotArrayPool<ObjectSlot>.Rent(InitSlotCapacity);
m_FreeStack = SlotArrayPool<int>.Rent(InitSlotCapacity);
m_AllNameHeadMap.Clear();
m_NameCursorMap.Clear();
m_SlotCount = 0;
m_FreeTop = 0;
m_UnusedHead = -1;
m_UnusedTail = -1;
m_LastBudgetScanStart = -1;
int page = index >> PageBits;
int offset = index & PageMask;
return offset < PageSize && IsAllocatedPage(page);
}
[Conditional("UNITY_EDITOR")]
@ -800,53 +968,60 @@ namespace AlicizaX.ObjectPool
#if UNITY_EDITOR && ENABLE_OBJECTPOOL_VALIDATION
int aliveCount = 0;
int unusedCount = 0;
for (int i = 0; i < m_SlotCount; i++)
for (int page = 0; page < m_PageCount; page++)
{
ref var slot = ref m_Slots[i];
if (!slot.IsAlive())
continue;
ObjectSlot[] slots = m_Pages[page];
if (slots == null) continue;
aliveCount++;
object target = slot.Obj.Target;
if (!m_TargetMap.TryGetValue(target, out int mappedIdx) || mappedIdx != i)
for (int offset = 0; offset < PageSize; offset++)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' target index map is inconsistent.");
continue;
}
int idx = MakeIndex(page, offset);
ref var slot = ref slots[offset];
if (!slot.IsAlive())
continue;
string objectName = slot.Obj.Name ?? string.Empty;
if (!m_AllNameHeadMap.TryGetValue(objectName, out int head))
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name head is missing.");
continue;
}
aliveCount++;
if (slot.PrevByName < 0 && head != i)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain head is inconsistent.");
}
if (slot.NextByName >= 0 && m_Slots[slot.NextByName].PrevByName != i)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain link is inconsistent.");
}
bool inUnusedList = m_UnusedHead == i || slot.PrevUnused >= 0 || slot.NextUnused >= 0;
if (slot.SpawnCount == 0)
{
unusedCount++;
if (!inUnusedList)
object target = slot.Obj.Target;
if (!m_TargetMap.TryGetValue(target, out int mappedIdx) || mappedIdx != idx)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused list is inconsistent.");
UnityEngine.Debug.LogError($"Object pool '{FullName}' target index map is inconsistent.");
continue;
}
}
else
{
if (inUnusedList)
string objectName = slot.Obj.Name ?? string.Empty;
if (!m_AllNameHeadMap.TryGetValue(objectName, out int head))
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in unused list.");
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name head is missing.");
continue;
}
if (slot.PrevByName < 0 && head != idx)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain head is inconsistent.");
}
if (slot.NextByName >= 0 && GetSlotRef(slot.NextByName).PrevByName != idx)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain link is inconsistent.");
}
bool inUnusedList = m_UnusedHead == idx || slot.PrevUnused >= 0 || slot.NextUnused >= 0;
if (slot.SpawnCount == 0)
{
unusedCount++;
if (!inUnusedList)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused list is inconsistent.");
}
}
else
{
if (inUnusedList)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in unused list.");
}
}
}
}
@ -861,7 +1036,7 @@ namespace AlicizaX.ObjectPool
int prevUnused = -1;
while (current >= 0)
{
ref var slot = ref m_Slots[current];
ref var slot = ref GetSlotRef(current);
if (!slot.IsAlive() || slot.SpawnCount != 0)
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain contains invalid slot.");
@ -886,7 +1061,7 @@ namespace AlicizaX.ObjectPool
private void MarkSlotAvailable(int idx)
{
AddToUnusedListTail(idx);
ref var slot = ref m_Slots[idx];
ref var slot = ref GetSlotRef(idx);
if (slot.IsAlive())
m_NameCursorMap.AddOrUpdate(slot.Obj.Name ?? string.Empty, idx);
}
@ -898,7 +1073,7 @@ namespace AlicizaX.ObjectPool
private void AddToUnusedListTail(int idx)
{
ref var slot = ref m_Slots[idx];
ref var slot = ref GetSlotRef(idx);
if (m_UnusedHead == idx || slot.PrevUnused >= 0 || slot.NextUnused >= 0)
return;
@ -906,7 +1081,7 @@ namespace AlicizaX.ObjectPool
slot.NextUnused = -1;
if (m_UnusedTail >= 0)
m_Slots[m_UnusedTail].NextUnused = idx;
GetSlotRef(m_UnusedTail).NextUnused = idx;
else
m_UnusedHead = idx;
@ -915,7 +1090,7 @@ namespace AlicizaX.ObjectPool
private void RemoveFromUnusedList(int idx)
{
ref var slot = ref m_Slots[idx];
ref var slot = ref GetSlotRef(idx);
if (m_UnusedHead != idx && slot.PrevUnused < 0 && slot.NextUnused < 0)
return;
@ -923,12 +1098,12 @@ namespace AlicizaX.ObjectPool
int next = slot.NextUnused;
if (prev >= 0)
m_Slots[prev].NextUnused = next;
GetSlotRef(prev).NextUnused = next;
else
m_UnusedHead = next;
if (next >= 0)
m_Slots[next].PrevUnused = prev;
GetSlotRef(next).PrevUnused = prev;
else
m_UnusedTail = prev;
@ -945,7 +1120,7 @@ namespace AlicizaX.ObjectPool
return null;
float now = Time.realtimeSinceStartup;
ref var slot = ref m_Slots[head];
ref var slot = ref GetSlotRef(head);
if (!slot.IsAlive() || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
return null;

View File

@ -151,7 +151,7 @@ namespace AlicizaX.Resource.Runtime
private void Awake()
{
_resourceService = AppServices.RegisterApp(new ResourceService());
_resourceService = AppServices.RegisterApp<IResourceService>(new ResourceService());
Application.lowMemory += OnLowMemory;
}

View File

@ -11,7 +11,7 @@ namespace AlicizaX.Scene.Runtime
{
if (!AppServices.TryGetApp<ISceneService>(out _))
{
AppServices.RegisterApp(new SceneService());
AppServices.RegisterApp<ISceneService>(new SceneService());
}
AppServices.EnsureScene();

View File

@ -292,7 +292,7 @@ namespace AlicizaX.Scene.Runtime
var sceneScope = Context.EnsureScene();
if (!sceneScope.TryGet<SceneDomainStateService>(out var sceneState))
{
sceneState = sceneScope.Register(new SceneDomainStateService());
sceneState = (SceneDomainStateService)sceneScope.Register<ISceneStateService>(new SceneDomainStateService());
}
return sceneState;
@ -301,7 +301,7 @@ namespace AlicizaX.Scene.Runtime
private SceneDomainStateService PrepareSceneStateForMainSceneLoad(string location)
{
var sceneScope = Context.ResetScene();
var sceneState = sceneScope.Register(new SceneDomainStateService());
var sceneState = (SceneDomainStateService)sceneScope.Register<ISceneStateService>(new SceneDomainStateService());
sceneState.MarkMainSceneLoading(location);
return sceneState;
}

View File

@ -29,11 +29,27 @@ namespace AlicizaX.Timer.Runtime
float GetLeftTime(ulong timerHandle);
void Restart(ulong timerHandle);
void RemoveTimer(ulong timerHandle);
}
[UnityEngine.Scripting.Preserve]
public interface ITimerCapacityService
{
void Prewarm(int capacity);
}
[UnityEngine.Scripting.Preserve]
public interface ITimerDebugService
{
int GetAllTimers(TimerDebugInfo[] results);
void GetStatistics(out int activeCount, out int poolCapacity, out int peakActiveCount, out int freeCount);
}
#if UNITY_EDITOR
[UnityEngine.Scripting.Preserve]
public interface ITimerEditorDebugService
{
int GetStaleOneShotTimers(TimerDebugInfo[] results);
}
#endif
}

View File

@ -8,6 +8,12 @@ namespace AlicizaX.Timer.Runtime
[UnityEngine.Scripting.Preserve]
public sealed class TimerComponent : MonoBehaviour
{
private const int MIN_INITIAL_CAPACITY = 256;
[SerializeField]
[Min(MIN_INITIAL_CAPACITY)]
private int _initialCapacity = 1024;
private void Awake()
{
if (AppServices.TryGet<ITimerService>(out _))
@ -15,7 +21,15 @@ namespace AlicizaX.Timer.Runtime
return;
}
AppServices.RegisterApp(new TimerService());
AppServices.RegisterApp<ITimerService>(new TimerService(_initialCapacity));
}
private void OnValidate()
{
if (_initialCapacity < MIN_INITIAL_CAPACITY)
{
_initialCapacity = MIN_INITIAL_CAPACITY;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using Cysharp.Text;
public static class InstanceFactory
{
@ -10,19 +11,19 @@ public static class InstanceFactory
{
if (!_constructorCache.TryGetValue(type, out var constructor))
{
// 验证是否存在公共无参构造函数
// 楠岃瘉鏄惁瀛樺湪鍏叡鏃犲弬鏋勯€犲嚱鏁?
var ctor = type.GetConstructor(Type.EmptyTypes);
if (ctor == null)
{
throw new MissingMethodException($"类型 {type.Name} 缺少公共无参构造函数");
throw new MissingMethodException(ZString.Format("Type {0} missing public parameterless constructor", type.Name));
}
// 构建表达式树new T()
// 鏋勫缓琛ㄨ揪寮忔爲锛歯ew T()
var newExpr = Expression.New(ctor);
var lambda = Expression.Lambda<Func<object>>(newExpr);
constructor = lambda.Compile();
// 缓存委托
// 缂撳瓨濮旀墭
_constructorCache[type] = constructor;
}

View File

@ -1,6 +1,7 @@
using System;
using AlicizaX.Resource.Runtime;
using AlicizaX;
using Cysharp.Text;
using Cysharp.Threading.Tasks;
using UnityEngine;
using Object = UnityEngine.Object;
@ -10,7 +11,6 @@ namespace AlicizaX.UI.Runtime
public static class UIHolderFactory
{
private static IResourceService ResourceService => AppServices.Require<IResourceService>();
private static bool AllowLegacyResourcesFallback => UnityEngine.Application.isEditor || UnityEngine.Debug.isDebugBuild;
public static async UniTask<T> CreateUIHolderAsync<T>(Transform parent) where T : UIHolderObjectBase
{
@ -90,65 +90,23 @@ namespace AlicizaX.UI.Runtime
private static async UniTask<GameObject> LoadResourceWithFallbackAsync(string location, Transform parent)
{
try
{
var managedObject = await ResourceService.LoadGameObjectAsync(location, parent);
if (managedObject != null)
{
return managedObject;
}
}
catch
{
if (!AllowLegacyResourcesFallback)
{
throw;
}
}
if (!AllowLegacyResourcesFallback)
{
throw new NullReferenceException($"UI resource load failed via IResourceService: {location}");
}
return await InstantiateResourceAsync(location, parent);
}
private static GameObject LoadResourceWithFallbackSync(string location, Transform parent)
{
try
{
var managedObject = ResourceService.LoadGameObject(location, parent);
if (managedObject != null)
{
return managedObject;
}
}
catch
{
if (!AllowLegacyResourcesFallback)
{
throw;
}
}
if (!AllowLegacyResourcesFallback)
{
throw new NullReferenceException($"UI resource load failed via IResourceService: {location}");
}
return InstantiateResourceSync(location, parent);
}
private static void ValidateAndBind(UIMetadata meta, GameObject holderObject, UIBase owner)
{
if (!holderObject) throw new NullReferenceException($"UI resource load failed: {meta.ResInfo.Location}");
if (!holderObject) throw new NullReferenceException(ZString.Format("UI resource load failed: {0}", meta.ResInfo.Location));
var holder = (UIHolderObjectBase)holderObject.GetComponent(meta.View.UIHolderType);
if (holder == null)
{
throw new InvalidCastException($"资源{holderObject.name}上不存在{meta.View.UIHolderType.FullName}");
throw new InvalidCastException(ZString.Format("UI resource {0} missing holder component {1}", holderObject.name, meta.View.UIHolderType.FullName));
}
meta.View?.BindUIHolder(holder, owner);

View File

@ -15,8 +15,9 @@ namespace AlicizaX.UI.Runtime
public readonly bool FullScreen;
public readonly int CacheTime;
public readonly bool NeedUpdate;
public readonly int TypeId;
public UIMetaInfo(RuntimeTypeHandle runtimeTypeHandle, RuntimeTypeHandle holderRuntimeTypeHandle, UILayer windowLayer, bool fullScreen, int cacheTime, bool needUpdate)
public UIMetaInfo(RuntimeTypeHandle runtimeTypeHandle, RuntimeTypeHandle holderRuntimeTypeHandle, UILayer windowLayer, bool fullScreen, int cacheTime, bool needUpdate, int typeId)
{
RuntimeTypeHandle = runtimeTypeHandle;
HolderRuntimeTypeHandle = holderRuntimeTypeHandle;
@ -24,18 +25,22 @@ namespace AlicizaX.UI.Runtime
FullScreen = fullScreen;
CacheTime = cacheTime;
NeedUpdate = needUpdate;
TypeId = typeId;
}
}
private static readonly Dictionary<RuntimeTypeHandle, UIMetaInfo> _typeHandleMap = new();
private static readonly Dictionary<string, RuntimeTypeHandle> _stringHandleMap = new();
private static int _nextTypeId;
public static int TypeCount => _nextTypeId;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Register(Type uiType, Type holderType, UILayer layer = UILayer.UI, bool fullScreen = false, int cacheTime = 0, bool needUpdate = false)
{
RuntimeTypeHandle holderHandle = holderType.TypeHandle;
RuntimeTypeHandle uiHandle = uiType.TypeHandle;
_typeHandleMap[uiHandle] = new UIMetaInfo(uiHandle, holderHandle, layer, fullScreen, cacheTime, needUpdate);
int typeId = _typeHandleMap.TryGetValue(uiHandle, out UIMetaInfo oldInfo) ? oldInfo.TypeId : _nextTypeId++;
_typeHandleMap[uiHandle] = new UIMetaInfo(uiHandle, holderHandle, layer, fullScreen, cacheTime, needUpdate, typeId);
_stringHandleMap[uiType.Name] = uiHandle;
}

View File

@ -2,6 +2,7 @@
using System.Runtime.CompilerServices;
using System.Threading;
using AlicizaX;
using Cysharp.Text;
using Cysharp.Threading.Tasks;
namespace AlicizaX.UI.Runtime
@ -12,10 +13,21 @@ namespace AlicizaX.UI.Runtime
public readonly UIMetaRegistry.UIMetaInfo MetaInfo;
public readonly UIResRegistry.UIResInfo ResInfo;
public readonly Type UILogicType;
public readonly string UILogicTypeName;
public readonly string UIHolderTypeName;
public bool InCache = false;
private CancellationTokenSource _cancellationTokenSource;
public CancellationToken CancellationToken => _cancellationTokenSource?.Token ?? CancellationToken.None;
private int _operationVersion;
private bool _cancelRequested;
private bool _showInProgress;
private bool _closeInProgress;
public int OperationVersion => _operationVersion;
public bool CancelRequested => _cancelRequested;
public bool ShowInProgress => _showInProgress;
public bool CloseInProgress => _closeInProgress;
public UIState State
{
@ -48,8 +60,64 @@ namespace AlicizaX.UI.Runtime
}
}
public bool BeginShowOperation()
{
if (_showInProgress)
{
return false;
}
RequestCancelCurrentOperation();
EnsureCancellationToken();
_operationVersion++;
_cancelRequested = false;
_showInProgress = true;
_closeInProgress = false;
return true;
}
public bool BeginCloseOperation()
{
if (_closeInProgress)
{
return false;
}
RequestCancelCurrentOperation();
EnsureCancellationToken();
_operationVersion++;
_cancelRequested = false;
_closeInProgress = true;
_showInProgress = false;
return true;
}
public void EndShowOperation(int operationVersion)
{
if (_operationVersion == operationVersion)
{
_showInProgress = false;
}
}
public void EndCloseOperation(int operationVersion)
{
if (_operationVersion == operationVersion)
{
_closeInProgress = false;
}
}
public void CancelAsyncOperations()
{
RequestCancelCurrentOperation();
_showInProgress = false;
_closeInProgress = false;
}
public void RequestCancelCurrentOperation()
{
_cancelRequested = true;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
@ -90,16 +158,19 @@ namespace AlicizaX.UI.Runtime
}
UILogicType = uiType;
UILogicTypeName = uiType.Name;
if (!UIMetaRegistry.TryGet(UILogicType.TypeHandle, out MetaInfo))
{
throw new InvalidOperationException($"[UI] Metadata not registered for {UILogicType.FullName}");
throw new InvalidOperationException(ZString.Format("[UI] Metadata not registered for {0}", UILogicType.FullName));
}
if (!UIResRegistry.TryGet(MetaInfo.HolderRuntimeTypeHandle, out ResInfo))
{
throw new InvalidOperationException($"[UI] Resource metadata not registered for holder of {UILogicType.FullName}");
throw new InvalidOperationException(ZString.Format("[UI] Resource metadata not registered for holder of {0}", UILogicType.FullName));
}
UIHolderTypeName = Type.GetTypeFromHandle(MetaInfo.HolderRuntimeTypeHandle)?.Name;
}
}
}

View File

@ -12,14 +12,6 @@ namespace AlicizaX.UI.Runtime
return obj;
}
public static UIMetadataObject Create(UIMetadata target, RuntimeTypeHandle handle)
{
UIMetadataObject obj = MemoryPool.Acquire<UIMetadataObject>();
obj.Initialize(handle.GetHashCode().ToString(), target);
return obj;
}
protected internal override void Release(bool isShutdown)
{
UIMetadata metadata = Target;

View File

@ -0,0 +1,105 @@
using System;
using UnityEngine;
namespace AlicizaX.UI.Runtime
{
internal interface IUIDebugService
{
int LayerCount { get; }
int CacheWindowCount { get; }
void FillServiceDebugInfo(UIServiceDebugInfo info);
bool FillLayerDebugInfo(int layerIndex, UILayerDebugInfo info);
bool FillWindowDebugInfo(int layerIndex, int windowIndex, UIWindowDebugInfo info);
int FillCacheDebugInfo(UIWindowDebugInfo[] infos, int capacity);
}
internal sealed class UIServiceDebugInfo
{
public bool Initialized;
public bool Orthographic;
public int LayerCount;
public int OpenWindowCount;
public int CacheWindowCount;
public int UpdateWindowCount;
public ulong BlockTimerHandle;
public bool BlockActive;
public Camera Camera;
public Canvas Canvas;
public Transform Root;
public Transform CanvasRoot;
public void Clear()
{
Initialized = false;
Orthographic = false;
LayerCount = 0;
OpenWindowCount = 0;
CacheWindowCount = 0;
UpdateWindowCount = 0;
BlockTimerHandle = 0UL;
BlockActive = false;
Camera = null;
Canvas = null;
Root = null;
CanvasRoot = null;
}
}
internal sealed class UILayerDebugInfo
{
public int LayerIndex;
public UILayer Layer;
public int WindowCount;
public int LastFullscreenIndex;
public RectTransform RectTransform;
public void Clear()
{
LayerIndex = 0;
Layer = UILayer.Background;
WindowCount = 0;
LastFullscreenIndex = -1;
RectTransform = null;
}
}
internal sealed class UIWindowDebugInfo
{
public int LayerIndex;
public int OrderIndex;
public RuntimeTypeHandle RuntimeTypeHandle;
public string LogicTypeName;
public string HolderTypeName;
public UIState State;
public bool Visible;
public bool FullScreen;
public bool InCache;
public bool NeedUpdate;
public bool ShowInProgress;
public bool CloseInProgress;
public int Depth;
public float CacheTime;
public ulong CacheTimerHandle;
public Transform HolderTransform;
public void Clear()
{
LayerIndex = 0;
OrderIndex = 0;
RuntimeTypeHandle = default;
LogicTypeName = null;
HolderTypeName = null;
State = UIState.Uninitialized;
Visible = false;
FullScreen = false;
InCache = false;
NeedUpdate = false;
ShowInProgress = false;
CloseInProgress = false;
Depth = 0;
CacheTime = 0f;
CacheTimerHandle = 0UL;
HolderTransform = null;
}
}
}

View File

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

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using AlicizaX;
using AlicizaX.Timer.Runtime;
using Cysharp.Text;
namespace AlicizaX.UI.Runtime
{
@ -19,7 +19,9 @@ namespace AlicizaX.UI.Runtime
}
}
private readonly Dictionary<RuntimeTypeHandle, CacheEntry> m_CacheWindow = new();
private CacheEntry[] m_CacheWindow = new CacheEntry[8];
private int[] m_CacheTypeIdToIndex = CreateCacheIndexArray(8);
private int m_CacheWindowCount;
private void CacheWindow(UIMetadata uiMetadata, bool force)
{
@ -35,7 +37,7 @@ namespace AlicizaX.UI.Runtime
return;
}
RemoveFromCache(uiMetadata.MetaInfo.RuntimeTypeHandle);
RemoveFromCache(uiMetadata.MetaInfo.TypeId);
ulong timerHandle = 0UL;
uiMetadata.View.Holder.transform.SetParent(UICacheLayer);
@ -51,36 +53,115 @@ namespace AlicizaX.UI.Runtime
if (timerHandle == 0UL)
{
Log.Warning($"Failed to create cache timer for {uiMetadata.UILogicType.Name}");
Log.Warning(ZString.Format("Failed to create cache timer for {0}", uiMetadata.UILogicType.Name));
}
}
uiMetadata.InCache = true;
m_CacheWindow.Add(uiMetadata.MetaInfo.RuntimeTypeHandle, new CacheEntry(uiMetadata, timerHandle));
AddToCache(uiMetadata, timerHandle);
}
private void OnTimerDisposeWindow(UIMetadata meta)
{
if (meta != null)
{
RemoveFromCache(meta.MetaInfo.RuntimeTypeHandle);
RemoveFromCache(meta.MetaInfo.TypeId);
meta.DisposeImmediate();
}
}
private void RemoveFromCache(RuntimeTypeHandle typeHandle)
{
if (m_CacheWindow.TryGetValue(typeHandle, out var entry))
if (UIMetaRegistry.TryGet(typeHandle, out UIMetaRegistry.UIMetaInfo metaInfo))
{
m_CacheWindow.Remove(typeHandle);
entry.Metadata.InCache = false;
if (entry.TimerHandle != 0UL && _timerService != null)
{
_timerService.RemoveTimer(entry.TimerHandle);
}
RemoveFromCache(metaInfo.TypeId);
}
}
private void RemoveFromCache(int typeId)
{
if ((uint)typeId >= (uint)m_CacheTypeIdToIndex.Length)
{
return;
}
int index = m_CacheTypeIdToIndex[typeId];
if (index < 0 || index >= m_CacheWindowCount)
{
return;
}
CacheEntry entry = m_CacheWindow[index];
int lastIndex = m_CacheWindowCount - 1;
CacheEntry last = m_CacheWindow[lastIndex];
m_CacheWindow[index] = last;
m_CacheWindow[lastIndex] = default;
m_CacheWindowCount = lastIndex;
m_CacheTypeIdToIndex[typeId] = -1;
if (index != lastIndex && last.Metadata != null)
{
m_CacheTypeIdToIndex[last.Metadata.MetaInfo.TypeId] = index;
}
entry.Metadata.InCache = false;
if (entry.TimerHandle != 0UL && _timerService != null)
{
_timerService.RemoveTimer(entry.TimerHandle);
}
}
private void AddToCache(UIMetadata metadata, ulong timerHandle)
{
int typeId = metadata.MetaInfo.TypeId;
EnsureCacheIndexCapacity(typeId);
EnsureCacheCapacity();
int index = m_CacheWindowCount++;
m_CacheWindow[index] = new CacheEntry(metadata, timerHandle);
m_CacheTypeIdToIndex[typeId] = index;
}
private void EnsureCacheCapacity()
{
if (m_CacheWindowCount < m_CacheWindow.Length)
{
return;
}
Array.Resize(ref m_CacheWindow, m_CacheWindow.Length << 1);
}
private void EnsureCacheIndexCapacity(int typeId)
{
if ((uint)typeId < (uint)m_CacheTypeIdToIndex.Length)
{
return;
}
int oldLength = m_CacheTypeIdToIndex.Length;
int newLength = oldLength;
while (newLength <= typeId)
{
newLength <<= 1;
}
Array.Resize(ref m_CacheTypeIdToIndex, newLength);
for (int i = oldLength; i < newLength; i++)
{
m_CacheTypeIdToIndex[i] = -1;
}
}
private static int[] CreateCacheIndexArray(int capacity)
{
int[] values = new int[capacity];
for (int i = 0; i < values.Length; i++)
{
values[i] = -1;
}
return values;
}
private ITimerService GetTimerService()
{
if (_timerService != null)

View File

@ -0,0 +1,149 @@
namespace AlicizaX.UI.Runtime
{
internal sealed partial class UIService
{
int IUIDebugService.LayerCount => _openUI.Length;
int IUIDebugService.CacheWindowCount => m_CacheWindowCount;
void IUIDebugService.FillServiceDebugInfo(UIServiceDebugInfo info)
{
if (info == null)
{
return;
}
int openWindowCount = 0;
int updateWindowCount = 0;
for (int i = 0; i < _openUI.Length; i++)
{
LayerData layer = _openUI[i];
if (layer == null)
{
continue;
}
int count = layer.Count;
openWindowCount += count;
for (int j = 0; j < count; j++)
{
UIMetadata metadata = layer.Items[j];
if (metadata != null && metadata.MetaInfo.NeedUpdate)
{
updateWindowCount++;
}
}
}
info.Initialized = UIRoot != null && UICanvas != null;
info.Orthographic = _isOrthographic;
info.LayerCount = _openUI.Length;
info.OpenWindowCount = openWindowCount;
info.CacheWindowCount = m_CacheWindowCount;
info.UpdateWindowCount = updateWindowCount;
info.BlockTimerHandle = m_LastCountDownHandle;
info.BlockActive = m_LayerBlock != null && m_LayerBlock.activeSelf;
info.Camera = UICamera;
info.Canvas = UICanvas;
info.Root = UIRoot;
info.CanvasRoot = UICanvasRoot;
}
bool IUIDebugService.FillLayerDebugInfo(int layerIndex, UILayerDebugInfo info)
{
if (info == null || (uint)layerIndex >= (uint)_openUI.Length)
{
return false;
}
LayerData layer = _openUI[layerIndex];
if (layer == null)
{
info.Clear();
info.LayerIndex = layerIndex;
info.Layer = (UILayer)layerIndex;
return false;
}
info.LayerIndex = layerIndex;
info.Layer = (UILayer)layerIndex;
info.WindowCount = layer.Count;
info.LastFullscreenIndex = layer.LastFullscreenIndex;
info.RectTransform = m_AllWindowLayer[layerIndex];
return true;
}
bool IUIDebugService.FillWindowDebugInfo(int layerIndex, int windowIndex, UIWindowDebugInfo info)
{
if (info == null || (uint)layerIndex >= (uint)_openUI.Length)
{
return false;
}
LayerData layer = _openUI[layerIndex];
if (layer == null || (uint)windowIndex >= (uint)layer.Count)
{
info?.Clear();
return false;
}
FillWindowDebugInfo(layer.Items[windowIndex], layerIndex, windowIndex, 0UL, info);
return true;
}
int IUIDebugService.FillCacheDebugInfo(UIWindowDebugInfo[] infos, int capacity)
{
if (infos == null || capacity <= 0)
{
return 0;
}
int index = 0;
for (int i = 0; i < m_CacheWindowCount; i++)
{
if (index >= capacity || index >= infos.Length)
{
break;
}
UIWindowDebugInfo info = infos[index];
if (info != null)
{
CacheEntry entry = m_CacheWindow[i];
FillWindowDebugInfo(entry.Metadata, entry.Metadata.MetaInfo.UILayer, index, entry.TimerHandle, info);
}
index++;
}
return index;
}
private static void FillWindowDebugInfo(UIMetadata metadata, int layerIndex, int orderIndex, ulong timerHandle, UIWindowDebugInfo info)
{
if (metadata == null)
{
info.Clear();
return;
}
UIBase view = metadata.View;
UIHolderObjectBase holder = view?.Holder;
info.LayerIndex = layerIndex;
info.OrderIndex = orderIndex;
info.RuntimeTypeHandle = metadata.MetaInfo.RuntimeTypeHandle;
info.LogicTypeName = metadata.UILogicTypeName;
info.HolderTypeName = metadata.UIHolderTypeName;
info.State = metadata.State;
info.Visible = view != null && view.Visible;
info.FullScreen = metadata.MetaInfo.FullScreen;
info.InCache = metadata.InCache;
info.NeedUpdate = metadata.MetaInfo.NeedUpdate;
info.ShowInProgress = metadata.ShowInProgress;
info.CloseInProgress = metadata.CloseInProgress;
info.Depth = view != null ? view.Depth : 0;
info.CacheTime = metadata.MetaInfo.CacheTime;
info.CacheTimerHandle = timerHandle;
info.HolderTransform = holder != null ? holder.transform : null;
}
}
}

View File

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

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Cysharp.Text;
using System.Collections.Generic;
using UnityEngine;
namespace AlicizaX.UI.Runtime
@ -57,7 +58,7 @@ namespace AlicizaX.UI.Runtime
private void AddLayer(int layer)
{
var layerObject = new GameObject($"Layer{layer}-{(UILayer)layer}");
var layerObject = new GameObject(ZString.Format("Layer{0}-{1}", layer, (UILayer)layer));
var rect = layerObject.AddComponent<RectTransform>();
rect.SetParent(UICanvasRoot);
rect.localScale = Vector3.one;

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using AlicizaX;
@ -9,16 +8,54 @@ namespace AlicizaX.UI.Runtime
{
sealed class LayerData
{
public readonly List<UIMetadata> OrderList;
public readonly Dictionary<RuntimeTypeHandle, int> IndexMap;
public UIMetadata[] Items;
public int[] TypeIdToIndex;
public int Count;
public int LastFullscreenIndex;
public LayerData(int initialCapacity)
{
OrderList = new List<UIMetadata>(initialCapacity);
IndexMap = new Dictionary<RuntimeTypeHandle, int>(initialCapacity);
Items = new UIMetadata[initialCapacity];
TypeIdToIndex = new int[initialCapacity];
for (int i = 0; i < TypeIdToIndex.Length; i++)
{
TypeIdToIndex[i] = -1;
}
Count = 0;
LastFullscreenIndex = -1;
}
public void EnsureTypeCapacity(int typeId)
{
if ((uint)typeId < (uint)TypeIdToIndex.Length)
{
return;
}
int oldLength = TypeIdToIndex.Length;
int newLength = oldLength;
while (newLength <= typeId)
{
newLength <<= 1;
}
Array.Resize(ref TypeIdToIndex, newLength);
for (int i = oldLength; i < newLength; i++)
{
TypeIdToIndex[i] = -1;
}
}
public void EnsureItemCapacity()
{
if (Count < Items.Length)
{
return;
}
Array.Resize(ref Items, Items.Length << 1);
}
}
internal sealed partial class UIService
@ -28,30 +65,51 @@ namespace AlicizaX.UI.Runtime
private async UniTask<UIBase> ShowUIImplAsync(UIMetadata metaInfo, params object[] userDatas)
{
CreateMetaUI(metaInfo);
EnsureMetaCanOpen(metaInfo);
await UIHolderFactory.CreateUIResourceAsync(metaInfo, UICacheLayer);
if (metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed)
if (!metaInfo.BeginShowOperation())
{
metaInfo.View?.RefreshParams(userDatas);
return metaInfo.View;
}
int operationVersion = metaInfo.OperationVersion;
await UIHolderFactory.CreateUIResourceAsync(metaInfo, UICacheLayer);
if (operationVersion != metaInfo.OperationVersion || metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed)
{
metaInfo.EndShowOperation(operationVersion);
return null;
}
FinalizeShow(metaInfo, userDatas);
await UpdateVisualState(metaInfo, metaInfo.CancellationToken);
return metaInfo.View;
bool showResult = await UpdateVisualState(metaInfo, metaInfo.CancellationToken);
metaInfo.EndShowOperation(operationVersion);
return showResult ? metaInfo.View : null;
}
private UIBase ShowUIImplSync(UIMetadata metaInfo, params object[] userDatas)
{
CreateMetaUI(metaInfo);
EnsureMetaCanOpen(metaInfo);
UIHolderFactory.CreateUIResourceSync(metaInfo, UICacheLayer);
if (metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed)
if (!metaInfo.BeginShowOperation())
{
metaInfo.View?.RefreshParams(userDatas);
return metaInfo.View;
}
int operationVersion = metaInfo.OperationVersion;
UIHolderFactory.CreateUIResourceSync(metaInfo, UICacheLayer);
if (operationVersion != metaInfo.OperationVersion || metaInfo.View == null || metaInfo.State == UIState.Uninitialized || metaInfo.State == UIState.Destroyed)
{
metaInfo.EndShowOperation(operationVersion);
return null;
}
FinalizeShow(metaInfo, userDatas);
UpdateVisualState(metaInfo).Forget();
bool showResult = UpdateVisualStateSync(metaInfo);
metaInfo.EndShowOperation(operationVersion);
if (!showResult)
{
return null;
}
return metaInfo.View;
}
@ -62,29 +120,35 @@ namespace AlicizaX.UI.Runtime
return;
}
if (!meta.BeginCloseOperation())
{
return;
}
int operationVersion = meta.OperationVersion;
if (meta.State == UIState.CreatedUI)
{
meta.CancelAsyncOperations();
await meta.DisposeAsync();
meta.EndCloseOperation(operationVersion);
return;
}
if (meta.State == UIState.Loaded || meta.State == UIState.Initialized)
{
meta.CancelAsyncOperations();
var popResult = Pop(meta);
SortWindowVisible(meta.MetaInfo.UILayer, popResult.previousFullscreenIndex);
SortWindowDepth(meta.MetaInfo.UILayer, popResult.removedIndex >= 0 ? popResult.removedIndex : 0);
meta.View.Visible = false;
CacheWindow(meta, force);
meta.EndCloseOperation(operationVersion);
return;
}
meta.CancelAsyncOperations();
meta.EnsureCancellationToken();
await meta.View.InternalClose();
if (meta.State != UIState.Closed)
bool closeResult = await meta.View.InternalClose(meta.CancellationToken);
if (!closeResult || meta.State != UIState.Closed)
{
meta.EndCloseOperation(operationVersion);
return;
}
@ -92,6 +156,7 @@ namespace AlicizaX.UI.Runtime
SortWindowVisible(meta.MetaInfo.UILayer, closedPopResult.previousFullscreenIndex);
SortWindowDepth(meta.MetaInfo.UILayer, closedPopResult.removedIndex >= 0 ? closedPopResult.removedIndex : 0);
CacheWindow(meta, force);
meta.EndCloseOperation(operationVersion);
}
@ -106,16 +171,6 @@ namespace AlicizaX.UI.Runtime
if (meta.State == UIState.Uninitialized) meta.CreateUI();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void EnsureMetaCanOpen(UIMetadata meta)
{
if (meta.State == UIState.Closed)
{
meta.EnsureCancellationToken();
}
}
private void FinalizeShow(UIMetadata meta, object[] userDatas)
{
if (meta.InCache)
@ -145,11 +200,14 @@ namespace AlicizaX.UI.Runtime
private void Push(UIMetadata meta)
{
var layer = _openUI[meta.MetaInfo.UILayer];
if (!layer.IndexMap.ContainsKey(meta.MetaInfo.RuntimeTypeHandle))
int typeId = meta.MetaInfo.TypeId;
layer.EnsureTypeCapacity(typeId);
if (layer.TypeIdToIndex[typeId] < 0)
{
int index = layer.OrderList.Count;
layer.OrderList.Add(meta);
layer.IndexMap[meta.MetaInfo.RuntimeTypeHandle] = index;
layer.EnsureItemCapacity();
int index = layer.Count++;
layer.Items[index] = meta;
layer.TypeIdToIndex[typeId] = index;
if (meta.MetaInfo.FullScreen)
{
layer.LastFullscreenIndex = index;
@ -164,17 +222,27 @@ namespace AlicizaX.UI.Runtime
{
var layer = _openUI[meta.MetaInfo.UILayer];
int previousFullscreenIndex = layer.LastFullscreenIndex;
if (layer.IndexMap.TryGetValue(meta.MetaInfo.RuntimeTypeHandle, out int index))
int typeId = meta.MetaInfo.TypeId;
if ((uint)typeId < (uint)layer.TypeIdToIndex.Length)
{
layer.OrderList.RemoveAt(index);
layer.IndexMap.Remove(meta.MetaInfo.RuntimeTypeHandle);
for (int i = index; i < layer.OrderList.Count; i++)
int index = layer.TypeIdToIndex[typeId];
if (index < 0)
{
var item = layer.OrderList[i];
layer.IndexMap[item.MetaInfo.RuntimeTypeHandle] = i;
return (-1, previousFullscreenIndex);
}
int lastIndex = layer.Count - 1;
for (int i = index; i < lastIndex; i++)
{
UIMetadata item = layer.Items[i + 1];
layer.Items[i] = item;
layer.TypeIdToIndex[item.MetaInfo.TypeId] = i;
}
layer.Items[lastIndex] = null;
layer.Count = lastIndex;
layer.TypeIdToIndex[typeId] = -1;
UpdateFullscreenIndexAfterRemove(layer, meta, index);
return (index, previousFullscreenIndex);
}
@ -196,23 +264,25 @@ namespace AlicizaX.UI.Runtime
private void MoveToTop(UIMetadata meta)
{
var layer = _openUI[meta.MetaInfo.UILayer];
int lastIdx = layer.OrderList.Count - 1;
int lastIdx = layer.Count - 1;
int typeId = meta.MetaInfo.TypeId;
if (!layer.IndexMap.TryGetValue(meta.MetaInfo.RuntimeTypeHandle, out int currentIdx))
if ((uint)typeId >= (uint)layer.TypeIdToIndex.Length)
return;
int currentIdx = layer.TypeIdToIndex[typeId];
if (currentIdx != lastIdx && currentIdx >= 0)
{
layer.OrderList.RemoveAt(currentIdx);
layer.OrderList.Add(meta);
for (int i = currentIdx; i < lastIdx; i++)
{
var item = layer.OrderList[i];
layer.IndexMap[item.MetaInfo.RuntimeTypeHandle] = i;
UIMetadata item = layer.Items[i + 1];
layer.Items[i] = item;
layer.TypeIdToIndex[item.MetaInfo.TypeId] = i;
}
layer.IndexMap[meta.MetaInfo.RuntimeTypeHandle] = lastIdx;
layer.Items[lastIdx] = meta;
layer.TypeIdToIndex[typeId] = lastIdx;
UpdateFullscreenIndexAfterMove(layer, meta, currentIdx, lastIdx);
SortWindowDepth(meta.MetaInfo.UILayer, currentIdx);
@ -220,41 +290,55 @@ namespace AlicizaX.UI.Runtime
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private async UniTask UpdateVisualState(UIMetadata meta, CancellationToken cancellationToken = default)
private async UniTask<bool> UpdateVisualState(UIMetadata meta, CancellationToken cancellationToken = default)
{
SortWindowVisible(meta.MetaInfo.UILayer);
SortWindowDepth(meta.MetaInfo.UILayer);
if (meta.State == UIState.Loaded)
{
await meta.View.InternalInitlized(cancellationToken);
if (!await meta.View.InternalInitlized(cancellationToken))
{
return false;
}
}
await meta.View.InternalOpen(cancellationToken);
return await meta.View.InternalOpen(cancellationToken);
}
private bool UpdateVisualStateSync(UIMetadata meta)
{
SortWindowVisible(meta.MetaInfo.UILayer);
SortWindowDepth(meta.MetaInfo.UILayer);
if (meta.State == UIState.Loaded)
{
if (!meta.View.InternalInitlizedSync()) return false;
}
return meta.View.InternalOpenSync();
}
private void SortWindowVisible(int layer, int previousFullscreenIndex = int.MinValue)
{
var layerData = _openUI[layer];
var list = layerData.OrderList;
int count = list.Count;
int count = layerData.Count;
int fullscreenIdx = layerData.LastFullscreenIndex;
if (fullscreenIdx >= count || (fullscreenIdx >= 0 && !IsDisplayFullscreen(list[fullscreenIdx])))
if (fullscreenIdx >= count || (fullscreenIdx >= 0 && !IsDisplayFullscreen(layerData.Items[fullscreenIdx])))
{
fullscreenIdx = FindLastFullscreenIndex(list, count - 1);
fullscreenIdx = FindLastFullscreenIndex(layerData, count - 1);
layerData.LastFullscreenIndex = fullscreenIdx;
}
int oldFullscreenIndex = previousFullscreenIndex == int.MinValue ? fullscreenIdx : previousFullscreenIndex;
if (oldFullscreenIndex == fullscreenIdx)
{
ApplyVisibilityRange(list, fullscreenIdx >= 0 ? fullscreenIdx : 0, count, fullscreenIdx);
ApplyVisibilityRange(layerData, fullscreenIdx >= 0 ? fullscreenIdx : 0, count, fullscreenIdx);
return;
}
if (oldFullscreenIndex == -1 && fullscreenIdx == -1)
{
ApplyVisibilityRange(list, 0, count, -1);
ApplyVisibilityRange(layerData, 0, count, -1);
return;
}
@ -265,21 +349,21 @@ namespace AlicizaX.UI.Runtime
? count
: Math.Max(oldFullscreenIndex, fullscreenIdx) + 1;
ApplyVisibilityRange(list, start, endExclusive, fullscreenIdx);
ApplyVisibilityRange(layerData, start, endExclusive, fullscreenIdx);
}
private void SortWindowDepth(int layer, int startIndex = 0)
{
var list = _openUI[layer].OrderList;
var layerData = _openUI[layer];
int baseDepth = layer * LAYER_DEEP;
for (int i = startIndex; i < list.Count; i++)
for (int i = startIndex; i < layerData.Count; i++)
{
int newDepth = baseDepth + i * WINDOW_DEEP;
if (list[i].View.Depth != newDepth)
if (layerData.Items[i].View.Depth != newDepth)
{
list[i].View.Depth = newDepth;
layerData.Items[i].View.Depth = newDepth;
}
}
}
@ -289,11 +373,11 @@ namespace AlicizaX.UI.Runtime
return meta.MetaInfo.FullScreen && UIStateMachine.IsDisplayActive(meta.State);
}
private static int FindLastFullscreenIndex(List<UIMetadata> list, int startIndex)
private static int FindLastFullscreenIndex(LayerData layer, int startIndex)
{
for (int i = Math.Min(startIndex, list.Count - 1); i >= 0; i--)
for (int i = Math.Min(startIndex, layer.Count - 1); i >= 0; i--)
{
if (IsDisplayFullscreen(list[i]))
if (IsDisplayFullscreen(layer.Items[i]))
{
return i;
}
@ -302,28 +386,28 @@ namespace AlicizaX.UI.Runtime
return -1;
}
private static void ApplyVisibilityRange(List<UIMetadata> list, int startInclusive, int endExclusive, int fullscreenIdx)
private static void ApplyVisibilityRange(LayerData layer, int startInclusive, int endExclusive, int fullscreenIdx)
{
if (startInclusive < 0)
{
startInclusive = 0;
}
if (endExclusive > list.Count)
if (endExclusive > layer.Count)
{
endExclusive = list.Count;
endExclusive = layer.Count;
}
bool showAll = fullscreenIdx < 0;
for (int i = startInclusive; i < endExclusive; i++)
{
list[i].View.Visible = showAll || i >= fullscreenIdx;
layer.Items[i].View.Visible = showAll || i >= fullscreenIdx;
}
}
private static void UpdateFullscreenIndexAfterRemove(LayerData layer, UIMetadata removedMeta, int removedIndex)
{
if (layer.OrderList.Count == 0)
if (layer.Count == 0)
{
layer.LastFullscreenIndex = -1;
return;
@ -331,7 +415,7 @@ namespace AlicizaX.UI.Runtime
if (removedMeta.MetaInfo.FullScreen && layer.LastFullscreenIndex == removedIndex)
{
layer.LastFullscreenIndex = FindLastFullscreenIndex(layer.OrderList, removedIndex - 1);
layer.LastFullscreenIndex = FindLastFullscreenIndex(layer, removedIndex - 1);
return;
}

View File

@ -6,7 +6,7 @@ using UnityEngine;
namespace AlicizaX.UI.Runtime
{
internal sealed partial class UIService : ServiceBase, IUIService, IServiceTickable
internal sealed partial class UIService : ServiceBase, IUIService, IUIDebugService, IServiceTickable
{
private ITimerService _timerService;
@ -24,16 +24,16 @@ namespace AlicizaX.UI.Runtime
for (int layerIndex = 0; layerIndex < _openUI.Length; layerIndex++)
{
var layer = _openUI[layerIndex];
int count = layer.OrderList.Count;
int count = layer.Count;
if (count == 0) continue;
for (int i = 0; i < count; i++)
{
if (layer.OrderList.Count != count)
if (layer.Count != count)
{
break;
}
var window = layer.OrderList[i];
var window = layer.Items[i];
if (window.MetaInfo.NeedUpdate)
window.View.InternalUpdate();
}
@ -110,10 +110,10 @@ namespace AlicizaX.UI.Runtime
continue;
}
int count = layer.OrderList.Count;
int count = layer.Count;
for (int i = count - 1; i >= 0; i--)
{
UIMetadata meta = layer.OrderList[i];
UIMetadata meta = layer.Items[i];
if (meta == null)
{
continue;
@ -123,27 +123,21 @@ namespace AlicizaX.UI.Runtime
meta.DisposeImmediate();
}
layer.OrderList.Clear();
layer.IndexMap.Clear();
Array.Clear(layer.Items, 0, layer.Count);
for (int i = 0; i < layer.TypeIdToIndex.Length; i++)
{
layer.TypeIdToIndex[i] = -1;
}
layer.Count = 0;
layer.LastFullscreenIndex = -1;
}
if (m_CacheWindow.Count > 0)
if (m_CacheWindowCount > 0)
{
RuntimeTypeHandle[] handles = new RuntimeTypeHandle[m_CacheWindow.Count];
int writeIndex = 0;
foreach (var pair in m_CacheWindow)
for (int i = m_CacheWindowCount - 1; i >= 0; i--)
{
handles[writeIndex++] = pair.Key;
}
for (int i = 0; i < writeIndex; i++)
{
if (!m_CacheWindow.TryGetValue(handles[i], out CacheEntry entry))
{
continue;
}
CacheEntry entry = m_CacheWindow[i];
if (entry.TimerHandle != 0UL && _timerService != null)
{
_timerService.RemoveTimer(entry.TimerHandle);
@ -152,9 +146,15 @@ namespace AlicizaX.UI.Runtime
entry.Metadata.InCache = false;
entry.Metadata.CancelAsyncOperations();
entry.Metadata.DisposeImmediate();
m_CacheWindow[i] = default;
}
m_CacheWindow.Clear();
for (int i = 0; i < m_CacheTypeIdToIndex.Length; i++)
{
m_CacheTypeIdToIndex[i] = -1;
}
m_CacheWindowCount = 0;
}
if (m_LastCountDownHandle != 0UL && _timerService != null)

View File

@ -1,6 +1,4 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Threading;
using AlicizaX;
using Cysharp.Threading.Tasks;
@ -10,12 +8,15 @@ namespace AlicizaX.UI.Runtime
{
public abstract partial class UIBase
{
private readonly Dictionary<UIBase, UIMetadata> _children = new();
private readonly List<UIMetadata> _updateableChildren = new();
private UIMetadata[] _children = new UIMetadata[8];
private int[] _childTypeIdToIndex = CreateIndexArray(8);
private int _childCount;
private UIMetadata[] _updateableChildren = new UIMetadata[4];
private int _updateableChildCount;
private void UpdateChildren()
{
for (int i = 0; i < _updateableChildren.Count; i++)
for (int i = 0; i < _updateableChildCount; i++)
{
var meta = _updateableChildren[i];
if (meta.View.State == UIState.Opened)
@ -27,72 +28,44 @@ namespace AlicizaX.UI.Runtime
private async UniTask DestroyAllChildren()
{
var temp = ArrayPool<UIMetadata>.Shared.Rent(_children.Count);
try
while (_childCount > 0)
{
int i = 0;
foreach (var kvp in _children)
UIMetadata metadata = _children[--_childCount];
_children[_childCount] = null;
ClearChildIndex(metadata);
if (metadata.View.Visible)
{
temp[i++] = kvp.Value;
metadata.CancelAsyncOperations();
metadata.EnsureCancellationToken();
await metadata.View.InternalClose(metadata.CancellationToken);
}
for (int j = 0; j < i; j++)
{
UIMetadata metadata = temp[j];
if (metadata.View.Visible)
{
metadata.CancelAsyncOperations();
metadata.EnsureCancellationToken();
await metadata.View.InternalClose(metadata.CancellationToken);
}
await metadata.DisposeAsync();
UIMetadataFactory.ReturnToPool(metadata);
temp[j] = null;
}
}
finally
{
ArrayPool<UIMetadata>.Shared.Return(temp, true);
await metadata.DisposeAsync();
UIMetadataFactory.ReturnToPool(metadata);
}
_children.Clear();
_updateableChildren.Clear();
_updateableChildCount = 0;
}
private void DestroyAllChildrenImmediate()
{
var temp = ArrayPool<UIMetadata>.Shared.Rent(_children.Count);
try
while (_childCount > 0)
{
int i = 0;
foreach (var kvp in _children)
{
temp[i++] = kvp.Value;
}
for (int j = 0; j < i; j++)
{
UIMetadata metadata = temp[j];
metadata.DisposeImmediate();
UIMetadataFactory.ReturnToPool(metadata);
temp[j] = null;
}
}
finally
{
ArrayPool<UIMetadata>.Shared.Return(temp, true);
UIMetadata metadata = _children[--_childCount];
_children[_childCount] = null;
ClearChildIndex(metadata);
metadata.DisposeImmediate();
UIMetadataFactory.ReturnToPool(metadata);
}
_children.Clear();
_updateableChildren.Clear();
_updateableChildCount = 0;
}
private void ChildVisible(bool value)
{
foreach (KeyValuePair<UIBase, UIMetadata> kvp in _children)
for (int i = 0; i < _childCount; i++)
{
UIBase view = kvp.Value.View;
UIBase view = _children[i].View;
if (view.State == UIState.Opened)
{
view.Visible = value;
@ -112,7 +85,7 @@ namespace AlicizaX.UI.Runtime
{
metadata.CreateUI();
UIHolderFactory.CreateUIResourceSync(metadata, parent, this);
ProcessWidget(metadata, visible).Forget();
ProcessWidgetSync(metadata, visible);
return (UIBase)metadata.View;
}
@ -167,7 +140,7 @@ namespace AlicizaX.UI.Runtime
metadata.CreateUI();
UIBase widget = (UIBase)metadata.View;
widget.BindUIHolder(holder, this);
ProcessWidget(metadata, true).Forget();
ProcessWidgetSync(metadata, true);
return (T)widget;
}
@ -185,7 +158,11 @@ namespace AlicizaX.UI.Runtime
return;
}
await meta.View.InternalInitlized(cancellationToken);
if (!await meta.View.InternalInitlized(cancellationToken))
{
return;
}
meta.View.Visible = visible;
if (meta.View.Visible)
{
@ -195,15 +172,23 @@ namespace AlicizaX.UI.Runtime
private bool AddWidget(UIMetadata meta)
{
if (!_children.TryAdd(meta.View, meta))
int typeId = meta.MetaInfo.TypeId;
EnsureChildIndexCapacity(typeId);
if (_childTypeIdToIndex[typeId] >= 0)
{
Log.Warning("Already has widget:{0}", meta.View);
return false;
}
EnsureChildCapacity();
int index = _childCount++;
_children[index] = meta;
_childTypeIdToIndex[typeId] = index;
if (meta.MetaInfo.NeedUpdate)
{
_updateableChildren.Add(meta);
EnsureUpdateableChildCapacity();
_updateableChildren[_updateableChildCount++] = meta;
}
return true;
@ -211,7 +196,12 @@ namespace AlicizaX.UI.Runtime
public async UniTask RemoveWidget(UIBase widget)
{
if (_children.Remove(widget, out var meta))
if (!TryRemoveChild(widget, out var meta))
{
return;
}
if (meta != null)
{
meta.CancelAsyncOperations();
meta.EnsureCancellationToken();
@ -229,18 +219,141 @@ namespace AlicizaX.UI.Runtime
private void RemoveUpdateableChild(UIMetadata meta)
{
for (int i = 0; i < _updateableChildren.Count; i++)
for (int i = 0; i < _updateableChildCount; i++)
{
if (_updateableChildren[i] != meta)
{
continue;
}
int lastIndex = _updateableChildren.Count - 1;
int lastIndex = _updateableChildCount - 1;
_updateableChildren[i] = _updateableChildren[lastIndex];
_updateableChildren.RemoveAt(lastIndex);
_updateableChildren[lastIndex] = null;
_updateableChildCount = lastIndex;
return;
}
}
private void ProcessWidgetSync(UIMetadata meta, bool visible)
{
if (!AddWidget(meta))
{
meta.DisposeImmediate();
UIMetadataFactory.ReturnToPool(meta);
return;
}
if (!meta.View.InternalInitlizedSync())
{
return;
}
meta.View.Visible = visible;
if (meta.View.Visible)
{
meta.View.InternalOpenSync();
}
}
private bool TryRemoveChild(UIBase widget, out UIMetadata meta)
{
meta = null;
if (widget == null)
{
return false;
}
int typeId = widget.UITypeId;
if ((uint)typeId >= (uint)_childTypeIdToIndex.Length)
{
return false;
}
int index = _childTypeIdToIndex[typeId];
if (index < 0 || index >= _childCount)
{
return false;
}
meta = _children[index];
int lastIndex = _childCount - 1;
UIMetadata last = _children[lastIndex];
_children[index] = last;
_children[lastIndex] = null;
_childCount = lastIndex;
_childTypeIdToIndex[typeId] = -1;
if (index != lastIndex && last != null)
{
_childTypeIdToIndex[last.MetaInfo.TypeId] = index;
}
return true;
}
private void ClearChildIndex(UIMetadata metadata)
{
if (metadata == null)
{
return;
}
int typeId = metadata.MetaInfo.TypeId;
if ((uint)typeId < (uint)_childTypeIdToIndex.Length)
{
_childTypeIdToIndex[typeId] = -1;
}
}
private void EnsureChildCapacity()
{
if (_childCount < _children.Length)
{
return;
}
Array.Resize(ref _children, _children.Length << 1);
}
private void EnsureChildIndexCapacity(int typeId)
{
if ((uint)typeId < (uint)_childTypeIdToIndex.Length)
{
return;
}
int oldLength = _childTypeIdToIndex.Length;
int newLength = oldLength;
while (newLength <= typeId)
{
newLength <<= 1;
}
Array.Resize(ref _childTypeIdToIndex, newLength);
for (int i = oldLength; i < newLength; i++)
{
_childTypeIdToIndex[i] = -1;
}
}
private void EnsureUpdateableChildCapacity()
{
if (_updateableChildCount < _updateableChildren.Length)
{
return;
}
Array.Resize(ref _updateableChildren, _updateableChildren.Length << 1);
}
private static int[] CreateIndexArray(int capacity)
{
int[] values = new int[capacity];
for (int i = 0; i < values.Length; i++)
{
values[i] = -1;
}
return values;
}
}
}

View File

@ -32,6 +32,7 @@ namespace AlicizaX.UI.Runtime
protected System.Object[] UserDatas => _userDatas;
private RuntimeTypeHandle _runtimeTypeHandle;
private int _uiTypeId = -1;
internal RuntimeTypeHandle RuntimeTypeHandler
{
@ -106,6 +107,19 @@ namespace AlicizaX.UI.Runtime
Dispose(true);
}
internal int UITypeId
{
get
{
if (_uiTypeId < 0 && UIMetaRegistry.TryGet(RuntimeTypeHandler, out UIMetaRegistry.UIMetaInfo metaInfo))
{
_uiTypeId = metaInfo.TypeId;
}
return _uiTypeId;
}
}
private void Dispose(bool disposing)
{
if (_disposed) return;
@ -204,81 +218,137 @@ namespace AlicizaX.UI.Runtime
internal abstract void BindUIHolder(UIHolderObjectBase holder, UIBase owner);
internal async UniTask InternalInitlized(CancellationToken cancellationToken = default)
internal async UniTask<bool> InternalInitlized(CancellationToken cancellationToken = default)
{
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Initialized))
return;
return false;
_state = UIState.Initialized;
Holder.OnWindowInitEvent?.Invoke();
await OnInitializeAsync();
if (cancellationToken.IsCancellationRequested)
return false;
OnRegisterEvent(EventListenerProxy);
return true;
}
internal async UniTask InternalOpen(CancellationToken cancellationToken = default)
internal bool InternalInitlizedSync()
{
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Initialized))
return false;
_state = UIState.Initialized;
Holder.OnWindowInitEvent?.Invoke();
OnInitialize();
OnRegisterEvent(EventListenerProxy);
return true;
}
internal async UniTask<bool> InternalOpen(CancellationToken cancellationToken = default)
{
if (_state == UIState.Opened || _state == UIState.Opening)
return;
return _state == UIState.Opened;
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Opening))
return;
return false;
int lifecycleVersion = BeginLifecycleTransition();
_state = UIState.Opening;
Visible = true;
Holder.OnWindowBeforeShowEvent?.Invoke();
try
await OnOpenAsync();
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening) || cancellationToken.IsCancellationRequested)
{
await OnOpenAsync();
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening))
return;
cancellationToken.ThrowIfCancellationRequested();
await Holder.PlayOpenTransitionAsync(cancellationToken);
RollbackOpeningState(lifecycleVersion);
return false;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
bool openCanceled = await Holder.PlayOpenTransitionAsync(cancellationToken).SuppressCancellationThrow();
if (openCanceled || cancellationToken.IsCancellationRequested)
{
return;
RollbackOpeningState(lifecycleVersion);
return false;
}
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening))
return;
return false;
_state = UIState.Opened;
Holder.OnWindowAfterShowEvent?.Invoke();
return true;
}
internal async UniTask InternalClose(CancellationToken cancellationToken = default)
internal bool InternalOpenSync()
{
if (_state == UIState.Opened || _state == UIState.Opening)
return _state == UIState.Opened;
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Opening))
return false;
int lifecycleVersion = BeginLifecycleTransition();
_state = UIState.Opening;
Visible = true;
Holder.OnWindowBeforeShowEvent?.Invoke();
OnOpen();
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening))
return false;
_state = UIState.Opened;
Holder.OnWindowAfterShowEvent?.Invoke();
return true;
}
internal async UniTask<bool> InternalClose(CancellationToken cancellationToken = default)
{
if (_state == UIState.Closed || _state == UIState.Closing)
return;
return _state == UIState.Closed;
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Closing))
return;
return false;
int lifecycleVersion = BeginLifecycleTransition();
_state = UIState.Closing;
Holder.OnWindowBeforeClosedEvent?.Invoke();
try
{
await OnCloseAsync();
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing))
return;
await OnCloseAsync();
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing) || cancellationToken.IsCancellationRequested)
return false;
cancellationToken.ThrowIfCancellationRequested();
await Holder.PlayCloseTransitionAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
bool closeCanceled = await Holder.PlayCloseTransitionAsync(cancellationToken).SuppressCancellationThrow();
if (closeCanceled || cancellationToken.IsCancellationRequested)
{
return;
return false;
}
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing))
return;
return false;
Visible = false;
_state = UIState.Closed;
Holder.OnWindowAfterClosedEvent?.Invoke();
return true;
}
internal bool InternalCloseSync()
{
if (_state == UIState.Closed || _state == UIState.Closing)
return _state == UIState.Closed;
if (!UIStateMachine.ValidateTransition(GetType().Name, _state, UIState.Closing))
return false;
int lifecycleVersion = BeginLifecycleTransition();
_state = UIState.Closing;
Holder.OnWindowBeforeClosedEvent?.Invoke();
OnClose();
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Closing))
return false;
Visible = false;
_state = UIState.Closed;
Holder.OnWindowAfterClosedEvent?.Invoke();
return true;
}
internal void InternalUpdate()
@ -342,6 +412,15 @@ namespace AlicizaX.UI.Runtime
return lifecycleVersion == _lifecycleVersion && _state == state;
}
private void RollbackOpeningState(int lifecycleVersion)
{
if (!IsCurrentLifecycleTransition(lifecycleVersion, UIState.Opening))
return;
Visible = false;
_state = UIState.Initialized;
}
#endregion
}
}

View File

@ -1,28 +1,26 @@
using System;
using System.Collections.Generic;
namespace AlicizaX.UI.Runtime
{
internal static class UIStateMachine
{
private static readonly Dictionary<UIState, HashSet<UIState>> _validTransitions = new()
private static readonly ushort[] ValidTransitionMasks =
{
[UIState.Uninitialized] = new() { UIState.CreatedUI },
[UIState.CreatedUI] = new() { UIState.Loaded, UIState.Destroying },
[UIState.Loaded] = new() { UIState.Initialized, UIState.Destroying },
[UIState.Initialized] = new() { UIState.Opening, UIState.Destroying },
[UIState.Opening] = new() { UIState.Opened, UIState.Closing, UIState.Destroying },
[UIState.Opened] = new() { UIState.Closing, UIState.Destroying },
[UIState.Closing] = new() { UIState.Opening, UIState.Closed, UIState.Destroying },
[UIState.Closed] = new() { UIState.Opening, UIState.Destroying },
[UIState.Destroying] = new() { UIState.Destroyed },
[UIState.Destroyed] = new() { }
Mask(UIState.CreatedUI),
Mask(UIState.Loaded, UIState.Destroying),
Mask(UIState.Initialized, UIState.Destroying),
Mask(UIState.Opening, UIState.Destroying),
Mask(UIState.Opened, UIState.Closing, UIState.Destroying),
Mask(UIState.Closing, UIState.Destroying),
Mask(UIState.Opening, UIState.Closed, UIState.Destroying),
Mask(UIState.Opening, UIState.Destroying),
Mask(UIState.Destroyed),
0,
};
public static bool IsValidTransition(UIState from, UIState to)
{
return _validTransitions.TryGetValue(from, out var validStates) && validStates.Contains(to);
int fromIndex = (int)from;
return (uint)fromIndex < (uint)ValidTransitionMasks.Length && (ValidTransitionMasks[fromIndex] & Mask(to)) != 0;
}
public static bool ValidateTransition(string uiName, UIState from, UIState to)
@ -30,16 +28,15 @@ namespace AlicizaX.UI.Runtime
if (IsValidTransition(from, to))
return true;
Log.Error($"[UI] Invalid state transition for {uiName}: {from} -> {to}");
Log.Error(Cysharp.Text.ZString.Format("[UI] Invalid state transition for {0}: {1} -> {2}", uiName, from, to));
return false;
}
public static HashSet<UIState> GetValidNextStates(UIState currentState)
public static ushort GetValidNextStateMask(UIState currentState)
{
return _validTransitions.TryGetValue(currentState, out var states)
? states
: new HashSet<UIState>();
int stateIndex = (int)currentState;
return (uint)stateIndex < (uint)ValidTransitionMasks.Length ? ValidTransitionMasks[stateIndex] : (ushort)0;
}
public static string GetStateDescription(UIState state)
@ -64,5 +61,20 @@ namespace AlicizaX.UI.Runtime
{
return state == UIState.Opening || state == UIState.Opened || state == UIState.Closing;
}
private static ushort Mask(UIState state)
{
return (ushort)(1 << (int)state);
}
private static ushort Mask(UIState stateA, UIState stateB)
{
return (ushort)(Mask(stateA) | Mask(stateB));
}
private static ushort Mask(UIState stateA, UIState stateB, UIState stateC)
{
return (ushort)(Mask(stateA) | Mask(stateB) | Mask(stateC));
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using AlicizaX;
using Cysharp.Text;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
@ -98,25 +99,19 @@ namespace AlicizaX.UI.Runtime
private async UniTask StartAsyncLoading(RuntimeTypeHandle typeHandle, params System.Object[] userDatas)
{
_loadingFlags[typeHandle] = true;
UIMetadata metadata = UIMetadataFactory.GetWidgetMetadata(typeHandle);
Transform parent = _tabCache[typeHandle];
try
UIBase widget = await CreateWidgetUIAsync(metadata, parent, false);
_loadingFlags.Remove(typeHandle);
if (widget is UIWidget tabWidget)
{
UIMetadata metadata = UIMetadataFactory.GetWidgetMetadata(typeHandle);
Transform parent = _tabCache[typeHandle];
UIBase widget = await CreateWidgetUIAsync(metadata, parent, false);
if (widget is not UIWidget tabWidget) return;
_loadedTabs[typeHandle] = tabWidget;
SwitchToLoadedTab(tabWidget, userDatas);
}
catch (Exception e)
else
{
Debug.LogError($"Tab load failed: {e}");
}
finally
{
_loadingFlags.Remove(typeHandle);
Debug.LogError(ZString.Format("Tab load failed: {0}", Type.GetTypeFromHandle(typeHandle)?.Name));
}
}
@ -133,7 +128,7 @@ namespace AlicizaX.UI.Runtime
{
if (index >= 0 && index < _typeOrder.Count) return true;
Debug.LogError($"Invalid tab index: {index}");
Debug.LogError(ZString.Format("Invalid tab index: {0}", index));
return false;
}

View File

@ -13,12 +13,12 @@ namespace AlicizaX.UI.Runtime
public void Open(params System.Object[] userDatas)
{
RefreshParams(userDatas);
InternalOpen().Forget();
InternalOpenSync();
}
public void Close()
{
InternalClose().Forget();
InternalCloseSync();
}
public void Destroy()

View File

@ -15,6 +15,7 @@ namespace AlicizaX.UI.Runtime
[SerializeField] private GameObject uiRoot = null;
[SerializeField] private bool _isOrthographic = true;
private Transform _instanceRoot = null;
private const string CanvasScalerMissingMessage = "Not found CanvasScaler !";
private IUIService _uiService;
@ -24,7 +25,7 @@ namespace AlicizaX.UI.Runtime
private void Awake()
{
_uiService = AppServices.RegisterApp(new UIService());
_uiService = AppServices.RegisterApp<IUIService>(new UIService());
if (uiRoot == null)
{
throw new GameFrameworkException("UIRoot Prefab is invalid.");
@ -54,7 +55,7 @@ namespace AlicizaX.UI.Runtime
CanvasScaler scaler = _uiService.UICanvasRoot.GetComponent<CanvasScaler>();
if (scaler == null)
{
Log.Error($"Not found {nameof(CanvasScaler)} !");
Log.Error(CanvasScalerMissingMessage);
return;
}