新增 FlowScope Git 包雏形
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Resources;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
public interface IUIPreloadService
|
||||
{
|
||||
Task PreloadAsync<TPanel>(CancellationToken cancellationToken);
|
||||
bool IsPreloaded<TPanel>();
|
||||
bool TryGetPreloadedHandle<TPanel>(out IResourceHandle<GameObject> handle);
|
||||
void Release<TPanel>();
|
||||
void ReleaseAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34f5e8174a844438842b8e839ba4bb64
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
public interface IUIScreenNavigator
|
||||
{
|
||||
Task<TPanel> PushAsync<TPanel, TViewModel>(
|
||||
TViewModel viewModel,
|
||||
CancellationToken cancellationToken)
|
||||
where TPanel : UIPanelBase<TViewModel>;
|
||||
|
||||
bool CanGoBack { get; }
|
||||
|
||||
void GoBack();
|
||||
|
||||
void Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d76da54cf887479d8448849a5ddb7dd8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
public enum PanelStrategy
|
||||
{
|
||||
Destroy,
|
||||
Cache
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be444a62bb9241da99d5a4fa2ff8be69
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
73
Packages/com.flowscope.gamecore/Runtime/UI/UILayer.cs
Normal file
73
Packages/com.flowscope.gamecore/Runtime/UI/UILayer.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
internal sealed class UILayer
|
||||
{
|
||||
private readonly Stack<UIPanelRecord> _stack = new();
|
||||
private readonly Dictionary<Type, UIPanelRecord> _cachedPanels = new();
|
||||
|
||||
public UILayer(string name, Canvas canvas, int sortOrder, PanelStrategy strategy)
|
||||
{
|
||||
Name = name;
|
||||
Canvas = canvas;
|
||||
Strategy = strategy;
|
||||
Canvas.sortingOrder = sortOrder;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public Canvas Canvas { get; }
|
||||
public PanelStrategy Strategy { get; }
|
||||
public int Count => _stack.Count;
|
||||
|
||||
public void Push(UIPanelRecord record)
|
||||
{
|
||||
record.IsOpen = true;
|
||||
_stack.Push(record);
|
||||
}
|
||||
|
||||
public UIPanelRecord Peek()
|
||||
{
|
||||
return _stack.Peek();
|
||||
}
|
||||
|
||||
public UIPanelRecord Pop()
|
||||
{
|
||||
var record = _stack.Pop();
|
||||
record.IsOpen = false;
|
||||
return record;
|
||||
}
|
||||
|
||||
public bool TryGetCached(Type panelType, out UIPanelRecord record)
|
||||
{
|
||||
return _cachedPanels.TryGetValue(panelType, out record);
|
||||
}
|
||||
|
||||
public void Cache(UIPanelRecord record)
|
||||
{
|
||||
_cachedPanels[record.PanelType] = record;
|
||||
}
|
||||
|
||||
public void RemoveCached(Type panelType)
|
||||
{
|
||||
_cachedPanels.Remove(panelType);
|
||||
}
|
||||
|
||||
public bool ContainsOpen(Type panelType, out UIPanelRecord record)
|
||||
{
|
||||
foreach (var current in _stack)
|
||||
{
|
||||
if (current.PanelType == panelType)
|
||||
{
|
||||
record = current;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
record = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/com.flowscope.gamecore/Runtime/UI/UILayer.cs.meta
Normal file
11
Packages/com.flowscope.gamecore/Runtime/UI/UILayer.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bf04ebbbc2f4785b5daf63b334d1a6a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
266
Packages/com.flowscope.gamecore/Runtime/UI/UIManager.cs
Normal file
266
Packages/com.flowscope.gamecore/Runtime/UI/UIManager.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Resources;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
public sealed class UIManager
|
||||
{
|
||||
private readonly IResourceService _resources;
|
||||
private readonly IUIPreloadService _preloadService;
|
||||
private readonly Dictionary<string, UILayer> _layers = new();
|
||||
|
||||
public UIManager(IResourceService resources, IUIPreloadService preloadService = null)
|
||||
{
|
||||
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
||||
_preloadService = preloadService;
|
||||
}
|
||||
|
||||
public void RegisterLayer(
|
||||
string name,
|
||||
Canvas canvas,
|
||||
int sortOrder,
|
||||
PanelStrategy strategy = PanelStrategy.Destroy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Layer name is required.", nameof(name));
|
||||
}
|
||||
|
||||
if (canvas == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(canvas));
|
||||
}
|
||||
|
||||
_layers[name] = new UILayer(name, canvas, sortOrder, strategy);
|
||||
}
|
||||
|
||||
public async Task<TPanel> OpenAsync<TPanel, TViewModel>(
|
||||
TViewModel viewModel,
|
||||
CancellationToken cancellationToken)
|
||||
where TPanel : UIPanelBase<TViewModel>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var attribute = GetPanelAttribute(typeof(TPanel));
|
||||
if (!_layers.TryGetValue(attribute.Layer, out var layer))
|
||||
{
|
||||
throw new InvalidOperationException($"UI layer is not registered: {attribute.Layer}");
|
||||
}
|
||||
|
||||
var strategy = attribute.OverrideStrategy ?? layer.Strategy;
|
||||
if (strategy == PanelStrategy.Cache &&
|
||||
layer.TryGetCached(typeof(TPanel), out var cached) &&
|
||||
!cached.IsOpen)
|
||||
{
|
||||
return ReopenCached<TPanel, TViewModel>(cached, layer, viewModel);
|
||||
}
|
||||
|
||||
var path = ResolvePath(typeof(TPanel), attribute);
|
||||
var handle = await LoadPanelHandleAsync<TPanel>(path, cancellationToken);
|
||||
if (handle == null || handle.Asset == null)
|
||||
{
|
||||
handle?.Dispose();
|
||||
throw new InvalidOperationException($"UI panel resource is missing: {path}");
|
||||
}
|
||||
|
||||
GameObject instance = null;
|
||||
var ownsHandle = !IsPreloadedHandle<TPanel>(handle);
|
||||
try
|
||||
{
|
||||
instance = UnityEngine.Object.Instantiate(handle.Asset, layer.Canvas.transform, false);
|
||||
var panel = instance.GetComponent<TPanel>();
|
||||
if (panel == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"UI panel prefab '{path}' does not contain component {typeof(TPanel).Name}.");
|
||||
}
|
||||
|
||||
panel.Bind(viewModel);
|
||||
instance.SetActive(true);
|
||||
|
||||
var recordHandle = ownsHandle ? (IDisposable)handle : RetainedPanelHandle.Instance;
|
||||
var record = new UIPanelRecord(typeof(TPanel), layer.Name, panel, recordHandle, strategy);
|
||||
if (strategy == PanelStrategy.Cache)
|
||||
{
|
||||
layer.Cache(record);
|
||||
}
|
||||
|
||||
layer.Push(record);
|
||||
return panel;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (instance != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(instance);
|
||||
}
|
||||
|
||||
if (ownsHandle)
|
||||
{
|
||||
handle.Dispose();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Close<TPanel>()
|
||||
{
|
||||
var panelType = typeof(TPanel);
|
||||
foreach (var layer in _layers.Values)
|
||||
{
|
||||
if (!layer.ContainsOpen(panelType, out var record))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(layer.Peek(), record))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot close non-top UI panel: {panelType.Name}");
|
||||
}
|
||||
|
||||
CloseTop(layer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseLayer(string layerName)
|
||||
{
|
||||
if (!_layers.TryGetValue(layerName, out var layer))
|
||||
{
|
||||
throw new InvalidOperationException($"UI layer is not registered: {layerName}");
|
||||
}
|
||||
|
||||
while (layer.Count > 0)
|
||||
{
|
||||
CloseTop(layer);
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseAll()
|
||||
{
|
||||
foreach (var layer in _layers.Values)
|
||||
{
|
||||
while (layer.Count > 0)
|
||||
{
|
||||
CloseTop(layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOpen<TPanel>()
|
||||
{
|
||||
var panelType = typeof(TPanel);
|
||||
foreach (var layer in _layers.Values)
|
||||
{
|
||||
if (layer.ContainsOpen(panelType, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static TPanel ReopenCached<TPanel, TViewModel>(
|
||||
UIPanelRecord record,
|
||||
UILayer layer,
|
||||
TViewModel viewModel)
|
||||
where TPanel : UIPanelBase<TViewModel>
|
||||
{
|
||||
var panel = (TPanel)record.Panel;
|
||||
panel.Bind(viewModel);
|
||||
panel.gameObject.SetActive(true);
|
||||
layer.Push(record);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static void CloseTop(UILayer layer)
|
||||
{
|
||||
var record = layer.Pop();
|
||||
record.Panel.Unbind();
|
||||
|
||||
if (record.Strategy == PanelStrategy.Cache)
|
||||
{
|
||||
record.Panel.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
layer.RemoveCached(record.PanelType);
|
||||
UnityEngine.Object.Destroy(record.Panel.gameObject);
|
||||
record.Handle.Dispose();
|
||||
}
|
||||
|
||||
private async Task<IResourceHandle<GameObject>> LoadPanelHandleAsync<TPanel>(
|
||||
string path,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_preloadService != null &&
|
||||
_preloadService.TryGetPreloadedHandle<TPanel>(out var preloadedHandle))
|
||||
{
|
||||
return preloadedHandle;
|
||||
}
|
||||
|
||||
return await _resources.LoadAsync<GameObject>(path, cancellationToken);
|
||||
}
|
||||
|
||||
private bool IsPreloadedHandle<TPanel>(IResourceHandle<GameObject> handle)
|
||||
{
|
||||
return _preloadService != null &&
|
||||
_preloadService.TryGetPreloadedHandle<TPanel>(out var preloadedHandle) &&
|
||||
ReferenceEquals(preloadedHandle, handle);
|
||||
}
|
||||
|
||||
internal static UIPanelAttribute GetPanelAttribute(Type panelType)
|
||||
{
|
||||
var attribute = (UIPanelAttribute)Attribute.GetCustomAttribute(
|
||||
panelType,
|
||||
typeof(UIPanelAttribute));
|
||||
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"UI panel must declare UIPanelAttribute: {panelType.Name}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(attribute.Layer))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"UI panel layer is required: {panelType.Name}");
|
||||
}
|
||||
|
||||
return attribute;
|
||||
}
|
||||
|
||||
internal static string ResolvePath(Type panelType, UIPanelAttribute attribute)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(attribute.Path))
|
||||
{
|
||||
return attribute.Path;
|
||||
}
|
||||
|
||||
var name = panelType.Name;
|
||||
const string suffix = "Panel";
|
||||
if (name.EndsWith(suffix, StringComparison.Ordinal))
|
||||
{
|
||||
name = name.Substring(0, name.Length - suffix.Length);
|
||||
}
|
||||
|
||||
return $"UI/{name}/Prefab";
|
||||
}
|
||||
|
||||
private sealed class RetainedPanelHandle : IDisposable
|
||||
{
|
||||
public static readonly RetainedPanelHandle Instance = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/com.flowscope.gamecore/Runtime/UI/UIManager.cs.meta
Normal file
11
Packages/com.flowscope.gamecore/Runtime/UI/UIManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c83637921703494f8fd6eb6e2eda6c70
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class UIPanelAttribute : Attribute
|
||||
{
|
||||
public UIPanelAttribute(string layer)
|
||||
: this(layer, null, null)
|
||||
{
|
||||
}
|
||||
|
||||
public UIPanelAttribute(string layer, string path)
|
||||
: this(layer, path, null)
|
||||
{
|
||||
}
|
||||
|
||||
public UIPanelAttribute(string layer, PanelStrategy overrideStrategy)
|
||||
: this(layer, null, overrideStrategy)
|
||||
{
|
||||
}
|
||||
|
||||
public UIPanelAttribute(string layer, string path, PanelStrategy overrideStrategy)
|
||||
: this(layer, path, (PanelStrategy?)overrideStrategy)
|
||||
{
|
||||
}
|
||||
|
||||
private UIPanelAttribute(
|
||||
string layer,
|
||||
string path,
|
||||
PanelStrategy? overrideStrategy)
|
||||
{
|
||||
Layer = layer;
|
||||
Path = path;
|
||||
OverrideStrategy = overrideStrategy;
|
||||
}
|
||||
|
||||
public string Layer { get; }
|
||||
public string Path { get; }
|
||||
public PanelStrategy? OverrideStrategy { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6b9266c245947bebd6cdf8c445cf712
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
31
Packages/com.flowscope.gamecore/Runtime/UI/UIPanelBase.cs
Normal file
31
Packages/com.flowscope.gamecore/Runtime/UI/UIPanelBase.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using R3;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
internal interface IUIPanel
|
||||
{
|
||||
GameObject gameObject { get; }
|
||||
Transform transform { get; }
|
||||
void Unbind();
|
||||
}
|
||||
|
||||
public abstract class UIPanelBase<TViewModel> : MonoBehaviour, IUIPanel
|
||||
{
|
||||
protected TViewModel ViewModel { get; private set; }
|
||||
protected CompositeDisposable Disposables { get; } = new();
|
||||
|
||||
public void Bind(TViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
OnBind(viewModel);
|
||||
}
|
||||
|
||||
public virtual void Unbind()
|
||||
{
|
||||
Disposables.Clear();
|
||||
}
|
||||
|
||||
protected abstract void OnBind(TViewModel viewModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c766b1543a347c2bec5d5f1b34b99ae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
28
Packages/com.flowscope.gamecore/Runtime/UI/UIPanelRecord.cs
Normal file
28
Packages/com.flowscope.gamecore/Runtime/UI/UIPanelRecord.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
internal sealed class UIPanelRecord
|
||||
{
|
||||
public UIPanelRecord(
|
||||
Type panelType,
|
||||
string layerName,
|
||||
IUIPanel panel,
|
||||
IDisposable handle,
|
||||
PanelStrategy strategy)
|
||||
{
|
||||
PanelType = panelType;
|
||||
LayerName = layerName;
|
||||
Panel = panel;
|
||||
Handle = handle;
|
||||
Strategy = strategy;
|
||||
}
|
||||
|
||||
public Type PanelType { get; }
|
||||
public string LayerName { get; }
|
||||
public IUIPanel Panel { get; }
|
||||
public IDisposable Handle { get; }
|
||||
public PanelStrategy Strategy { get; }
|
||||
public bool IsOpen { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a5aee9195e642a1a6f72d4ce5390885
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
274
Packages/com.flowscope.gamecore/Runtime/UI/UIPreloadService.cs
Normal file
274
Packages/com.flowscope.gamecore/Runtime/UI/UIPreloadService.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Resources;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
public sealed class UIPreloadService : IUIPreloadService, IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly IResourceService _resources;
|
||||
private readonly Dictionary<Type, IResourceHandle<GameObject>> _handles = new();
|
||||
private readonly Dictionary<Type, InFlightPreload> _inFlightLoads = new();
|
||||
private int _releaseGeneration;
|
||||
private bool _disposed;
|
||||
|
||||
public UIPreloadService(IResourceService resources)
|
||||
{
|
||||
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
||||
}
|
||||
|
||||
public async Task PreloadAsync<TPanel>(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var panelType = typeof(TPanel);
|
||||
InFlightPreload inFlightLoad;
|
||||
bool startLoad = false;
|
||||
string path = null;
|
||||
lock (_gate)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (TryGetPreloadedHandle(panelType, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_inFlightLoads.TryGetValue(panelType, out inFlightLoad) ||
|
||||
inFlightLoad.Invalidated ||
|
||||
inFlightLoad.ReleaseGeneration != _releaseGeneration)
|
||||
{
|
||||
var attribute = UIManager.GetPanelAttribute(panelType);
|
||||
path = UIManager.ResolvePath(panelType, attribute);
|
||||
inFlightLoad = new InFlightPreload();
|
||||
inFlightLoad.ReleaseGeneration = _releaseGeneration;
|
||||
_inFlightLoads[panelType] = inFlightLoad;
|
||||
startLoad = true;
|
||||
}
|
||||
|
||||
inFlightLoad.WaiterCount++;
|
||||
if (startLoad)
|
||||
{
|
||||
inFlightLoad.Task = LoadAndStoreAsync(panelType, path, inFlightLoad);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await WaitForSharedLoadAsync(inFlightLoad.Task, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
ForgetWaitingCaller(panelType, inFlightLoad);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPreloaded<TPanel>()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryGetPreloadedHandle(typeof(TPanel), out _);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetPreloadedHandle<TPanel>(out IResourceHandle<GameObject> handle)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
handle = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryGetPreloadedHandle(typeof(TPanel), out handle);
|
||||
}
|
||||
}
|
||||
|
||||
public void Release<TPanel>()
|
||||
{
|
||||
var panelType = typeof(TPanel);
|
||||
IResourceHandle<GameObject> handle = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InvalidateInFlightLoad(panelType);
|
||||
if (_handles.TryGetValue(panelType, out handle))
|
||||
{
|
||||
_handles.Remove(panelType);
|
||||
}
|
||||
}
|
||||
|
||||
handle?.Dispose();
|
||||
}
|
||||
|
||||
public void ReleaseAll()
|
||||
{
|
||||
List<IResourceHandle<GameObject>> handles;
|
||||
lock (_gate)
|
||||
{
|
||||
_releaseGeneration++;
|
||||
handles = new List<IResourceHandle<GameObject>>(_handles.Values);
|
||||
_handles.Clear();
|
||||
}
|
||||
|
||||
foreach (var handle in handles)
|
||||
{
|
||||
handle.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
ReleaseAll();
|
||||
}
|
||||
|
||||
private async Task LoadAndStoreAsync(
|
||||
Type panelType,
|
||||
string path,
|
||||
InFlightPreload inFlightLoad)
|
||||
{
|
||||
IResourceHandle<GameObject> handle = null;
|
||||
try
|
||||
{
|
||||
handle = await _resources.LoadAsync<GameObject>(path, CancellationToken.None);
|
||||
if (handle == null || handle.Asset == null)
|
||||
{
|
||||
handle?.Dispose();
|
||||
throw new InvalidOperationException($"UI panel resource is missing: {path}");
|
||||
}
|
||||
|
||||
var shouldCache = false;
|
||||
lock (_gate)
|
||||
{
|
||||
ForgetInFlightLoad(panelType, inFlightLoad);
|
||||
if (!_disposed &&
|
||||
!inFlightLoad.Invalidated &&
|
||||
inFlightLoad.ReleaseGeneration == _releaseGeneration &&
|
||||
inFlightLoad.WaiterCount > 0)
|
||||
{
|
||||
_handles[panelType] = handle;
|
||||
shouldCache = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldCache)
|
||||
{
|
||||
handle.Dispose();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
ForgetInFlightLoad(panelType, inFlightLoad);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ForgetInFlightLoad(Type panelType, InFlightPreload inFlightLoad)
|
||||
{
|
||||
if (_inFlightLoads.TryGetValue(panelType, out var current) &&
|
||||
ReferenceEquals(current, inFlightLoad))
|
||||
{
|
||||
_inFlightLoads.Remove(panelType);
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateInFlightLoad(Type panelType)
|
||||
{
|
||||
if (_inFlightLoads.TryGetValue(panelType, out var inFlightLoad))
|
||||
{
|
||||
inFlightLoad.Invalidated = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ForgetWaitingCaller(Type panelType, InFlightPreload inFlightLoad)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_inFlightLoads.TryGetValue(panelType, out var current) &&
|
||||
ReferenceEquals(current, inFlightLoad) &&
|
||||
inFlightLoad.WaiterCount > 0)
|
||||
{
|
||||
inFlightLoad.WaiterCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetPreloadedHandle(
|
||||
Type panelType,
|
||||
out IResourceHandle<GameObject> handle)
|
||||
{
|
||||
return _handles.TryGetValue(panelType, out handle) &&
|
||||
handle != null &&
|
||||
!handle.IsDisposed &&
|
||||
handle.Asset != null;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(UIPreloadService));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WaitForSharedLoadAsync(
|
||||
Task loadTask,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!cancellationToken.CanBeCanceled)
|
||||
{
|
||||
await loadTask;
|
||||
return;
|
||||
}
|
||||
|
||||
var cancellation = new TaskCompletionSource<bool>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
using (cancellationToken.Register(() => cancellation.TrySetResult(true)))
|
||||
{
|
||||
if (await Task.WhenAny(loadTask, cancellation.Task) == cancellation.Task)
|
||||
{
|
||||
throw new OperationCanceledException(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
await loadTask;
|
||||
}
|
||||
|
||||
private sealed class InFlightPreload
|
||||
{
|
||||
public Task Task { get; set; }
|
||||
public int WaiterCount { get; set; }
|
||||
public int ReleaseGeneration { get; set; }
|
||||
public bool Invalidated { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b2cbe6065884f82838b7c882ad5085a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
public sealed class UIScreenNavigator : IUIScreenNavigator
|
||||
{
|
||||
private readonly UIManager _uiManager;
|
||||
private readonly Stack<Action> _closeStack = new();
|
||||
|
||||
public UIScreenNavigator(UIManager uiManager)
|
||||
{
|
||||
_uiManager = uiManager ?? throw new ArgumentNullException(nameof(uiManager));
|
||||
}
|
||||
|
||||
public bool CanGoBack => _closeStack.Count > 1;
|
||||
|
||||
public async Task<TPanel> PushAsync<TPanel, TViewModel>(
|
||||
TViewModel viewModel,
|
||||
CancellationToken cancellationToken)
|
||||
where TPanel : UIPanelBase<TViewModel>
|
||||
{
|
||||
var panel = await _uiManager.OpenAsync<TPanel, TViewModel>(viewModel, cancellationToken);
|
||||
_closeStack.Push(() => _uiManager.Close<TPanel>());
|
||||
return panel;
|
||||
}
|
||||
|
||||
public void GoBack()
|
||||
{
|
||||
if (!CanGoBack)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var close = _closeStack.Pop();
|
||||
close();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
while (_closeStack.Count > 0)
|
||||
{
|
||||
var close = _closeStack.Pop();
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f991669426b4444098f1973daeb8fb58
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user