镜像 MainMenuP0 包样例
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Audio;
|
||||
using FlowScope.Config;
|
||||
using FlowScope.Flow;
|
||||
using FlowScope.Resources;
|
||||
using FlowScope.Save;
|
||||
using FlowScope.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using ContainerType = FlowScope.Container.Container;
|
||||
|
||||
namespace FlowScope.Samples.MainMenuP0
|
||||
{
|
||||
public sealed class GameBootstrap : MonoBehaviour
|
||||
{
|
||||
private const string SaveKey = "main-menu-player";
|
||||
|
||||
[SerializeField] private Canvas hudCanvas;
|
||||
[SerializeField] private MainMenuPanel mainMenuPanelPrefab;
|
||||
[SerializeField] private int defaultGold;
|
||||
|
||||
private ContainerType _root;
|
||||
private GameFlow _gameFlow;
|
||||
private ISaveService _saveService;
|
||||
private UIPreloadService _preloadService;
|
||||
private SampleResourceBackend _sampleBackend;
|
||||
private PlayerData _playerData;
|
||||
private CancellationTokenSource _lifetime;
|
||||
private bool _started;
|
||||
private bool _shutdownInProgress;
|
||||
private bool _shutdown;
|
||||
|
||||
public int ReleasedResourceCount => _sampleBackend?.ReleaseCount ?? 0;
|
||||
/// <summary>
|
||||
/// True after runtime cleanup has completed; shutdown sub-step failures are still reported by ShutdownAsync.
|
||||
/// </summary>
|
||||
public bool IsShutdown => _shutdown;
|
||||
public Task StartupTask { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_lifetime = new CancellationTokenSource();
|
||||
StartupTask = RunStartupWithLoggingAsync(_lifetime.Token);
|
||||
}
|
||||
|
||||
public async Task StartupAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_started = true;
|
||||
_shutdown = false;
|
||||
_root = new ContainerType();
|
||||
_gameFlow = new GameFlow();
|
||||
|
||||
_sampleBackend = new SampleResourceBackend();
|
||||
_sampleBackend.Register(
|
||||
"FlowScope/Samples/MainMenuP0/MainMenuPanel",
|
||||
ResolveMainMenuPanelPrefab().gameObject);
|
||||
var resources = new ResourceService(_sampleBackend);
|
||||
_preloadService = new UIPreloadService(resources);
|
||||
var uiManager = new UIManager(resources, _preloadService);
|
||||
uiManager.RegisterLayer("hud", ResolveHudCanvas(), 0, PanelStrategy.Destroy);
|
||||
var screenNavigator = new UIScreenNavigator(uiManager);
|
||||
|
||||
var migrations = new SaveMigrationRegistry();
|
||||
migrations.Register(new PlayerDataV1ToV2Migration());
|
||||
_saveService = new SaveService(
|
||||
new FileSaveStorage(),
|
||||
new JsonSaveSerializer(2, migrations));
|
||||
_playerData = await _saveService.LoadAsync(
|
||||
SaveKey,
|
||||
new PlayerData { Gold = new R3.ReactiveProperty<int>(defaultGold) },
|
||||
cancellationToken);
|
||||
|
||||
await _preloadService.PreloadAsync<MainMenuPanel>(cancellationToken);
|
||||
|
||||
var audioService = gameObject.AddComponent<AudioService>().Initialize(resources);
|
||||
_root.RegisterInstance(_root);
|
||||
_root.RegisterInstance<IConfigProvider>(
|
||||
new JsonConfigProvider(new[]
|
||||
{
|
||||
JsonConfigProvider.Mapping<SampleMenuConfig>(
|
||||
"main_menu.json",
|
||||
new InMemoryConfigSource(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "main_menu.json", "[{\"Id\":1,\"Title\":\"Main Menu P1\"}]" }
|
||||
},
|
||||
"MainMenuP0"),
|
||||
new JsonConfigParser())
|
||||
}));
|
||||
_root.RegisterInstance<IResourceService>(resources);
|
||||
_root.RegisterInstance<ISaveService>(_saveService);
|
||||
_root.RegisterInstance<IAudioService>(audioService);
|
||||
_root.RegisterInstance(uiManager);
|
||||
_root.RegisterInstance<IUIPreloadService>(_preloadService);
|
||||
_root.RegisterInstance<IUIScreenNavigator>(screenNavigator);
|
||||
_root.RegisterInstance(_playerData);
|
||||
_root.RegisterTransient(_ => new MainMenuFeature(screenNavigator, _playerData));
|
||||
|
||||
await _gameFlow.StartupAsync<MainMenuFeature>(_root, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_shutdown || _shutdownInProgress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_shutdownInProgress = true;
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
try
|
||||
{
|
||||
if (_saveService != null && _playerData != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _saveService.SaveAsync(SaveKey, _playerData, cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
exceptions.Add(exception);
|
||||
}
|
||||
}
|
||||
|
||||
if (_gameFlow != null && _gameFlow.State != GameFlowState.Disposed)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _gameFlow.ShutdownAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
exceptions.Add(exception);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_preloadService?.ReleaseAll();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
exceptions.Add(exception);
|
||||
}
|
||||
|
||||
_shutdown = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_shutdownInProgress = false;
|
||||
}
|
||||
|
||||
if (exceptions.Count == 1)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(exceptions[0]).Throw();
|
||||
}
|
||||
|
||||
if (exceptions.Count > 1)
|
||||
{
|
||||
throw new AggregateException("MainMenuP0 shutdown completed with failures.", exceptions);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
_lifetime?.Cancel();
|
||||
_ = RunShutdownWithLoggingAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_lifetime?.Cancel();
|
||||
_lifetime?.Dispose();
|
||||
}
|
||||
|
||||
private async Task RunStartupWithLoggingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await StartupAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunShutdownWithLoggingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ShutdownAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Canvas ResolveHudCanvas()
|
||||
{
|
||||
if (hudCanvas != null)
|
||||
{
|
||||
return hudCanvas;
|
||||
}
|
||||
|
||||
var canvasObject = new GameObject("FlowScope HUD Canvas");
|
||||
canvasObject.transform.SetParent(transform, false);
|
||||
hudCanvas = canvasObject.AddComponent<Canvas>();
|
||||
hudCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
return hudCanvas;
|
||||
}
|
||||
|
||||
private MainMenuPanel ResolveMainMenuPanelPrefab()
|
||||
{
|
||||
if (mainMenuPanelPrefab != null)
|
||||
{
|
||||
return mainMenuPanelPrefab;
|
||||
}
|
||||
|
||||
var panelObject = new GameObject("MainMenuPanel Template");
|
||||
panelObject.transform.SetParent(transform, false);
|
||||
panelObject.SetActive(false);
|
||||
|
||||
var panel = panelObject.AddComponent<MainMenuPanel>();
|
||||
|
||||
var goldObject = new GameObject("GoldText");
|
||||
goldObject.transform.SetParent(panelObject.transform, false);
|
||||
var goldText = goldObject.AddComponent<Text>();
|
||||
goldText.text = defaultGold.ToString();
|
||||
goldText.alignment = TextAnchor.MiddleCenter;
|
||||
|
||||
var buttonObject = new GameObject("IncrementGoldButton");
|
||||
buttonObject.transform.SetParent(panelObject.transform, false);
|
||||
buttonObject.AddComponent<Image>();
|
||||
var button = buttonObject.AddComponent<Button>();
|
||||
|
||||
panel.Configure(goldText, button);
|
||||
mainMenuPanelPrefab = panel;
|
||||
return mainMenuPanelPrefab;
|
||||
}
|
||||
|
||||
private sealed class SampleResourceBackend : IResourceBackend
|
||||
{
|
||||
private readonly Dictionary<string, GameObject> _prefabs = new();
|
||||
|
||||
public string Name => "MainMenuP0Sample";
|
||||
public int ReleaseCount { get; private set; }
|
||||
|
||||
public void Register(string key, GameObject prefab)
|
||||
{
|
||||
_prefabs[key] = prefab;
|
||||
}
|
||||
|
||||
public Task<T> LoadAsync<T>(
|
||||
string key,
|
||||
CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (typeof(T) == typeof(GameObject) &&
|
||||
_prefabs.TryGetValue(key, out var prefab))
|
||||
{
|
||||
return Task.FromResult(prefab as T);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Sample resource is not registered: {key}");
|
||||
}
|
||||
|
||||
public void Release<T>(string key, T asset)
|
||||
where T : class
|
||||
{
|
||||
ReleaseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SampleMenuConfig : IConfigRow
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; }
|
||||
}
|
||||
|
||||
private sealed class PlayerDataV1ToV2Migration : ISaveMigration
|
||||
{
|
||||
public string Key => typeof(PlayerData).FullName;
|
||||
public int FromVersion => 1;
|
||||
public int ToVersion => 2;
|
||||
|
||||
public string Migrate(string json) => json;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd97432f2838454cbbf2d19dbe14d7f3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Flow;
|
||||
using FlowScope.UI;
|
||||
|
||||
namespace FlowScope.Samples.MainMenuP0
|
||||
{
|
||||
public sealed class MainMenuFeature : FeatureBase
|
||||
{
|
||||
private readonly IUIScreenNavigator _screenNavigator;
|
||||
private readonly PlayerData _playerData;
|
||||
private MainMenuViewModel _viewModel;
|
||||
|
||||
public MainMenuFeature(IUIScreenNavigator screenNavigator, PlayerData playerData)
|
||||
{
|
||||
_screenNavigator = screenNavigator;
|
||||
_playerData = playerData;
|
||||
}
|
||||
|
||||
public override Task LoadAsync(FeatureContext context)
|
||||
{
|
||||
base.LoadAsync(context);
|
||||
_viewModel = new MainMenuViewModel(_playerData);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task EnterAsync(FeatureContext context)
|
||||
{
|
||||
await _screenNavigator.PushAsync<MainMenuPanel, MainMenuViewModel>(
|
||||
_viewModel,
|
||||
context.CancellationToken);
|
||||
}
|
||||
|
||||
public override Task ExitAsync(FeatureContext context)
|
||||
{
|
||||
_screenNavigator.Clear();
|
||||
return base.ExitAsync(context);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_viewModel = null;
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b015663928874766b56036f39f0aca6d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,62 @@
|
||||
using FlowScope.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace FlowScope.Samples.MainMenuP0
|
||||
{
|
||||
[UIPanel("hud", "FlowScope/Samples/MainMenuP0/MainMenuPanel")]
|
||||
public sealed class MainMenuPanel : UIPanelBase<MainMenuViewModel>
|
||||
{
|
||||
[SerializeField] private Text goldText;
|
||||
[SerializeField] private Button incrementButton;
|
||||
|
||||
public void Configure(Text goldLabel, Button incrementGoldButton)
|
||||
{
|
||||
goldText = goldLabel;
|
||||
incrementButton = incrementGoldButton;
|
||||
}
|
||||
|
||||
protected override void OnBind(MainMenuViewModel viewModel)
|
||||
{
|
||||
if (goldText == null)
|
||||
{
|
||||
goldText = GetComponentInChildren<Text>(true);
|
||||
}
|
||||
|
||||
if (incrementButton == null)
|
||||
{
|
||||
incrementButton = GetComponentInChildren<Button>(true);
|
||||
}
|
||||
|
||||
UpdateGold(viewModel.Gold.Value);
|
||||
viewModel.GoldChanged += UpdateGold;
|
||||
if (incrementButton != null)
|
||||
{
|
||||
incrementButton.onClick.AddListener(viewModel.IncrementGold);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unbind()
|
||||
{
|
||||
if (ViewModel != null)
|
||||
{
|
||||
ViewModel.GoldChanged -= UpdateGold;
|
||||
}
|
||||
|
||||
if (incrementButton != null)
|
||||
{
|
||||
incrementButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
base.Unbind();
|
||||
}
|
||||
|
||||
private void UpdateGold(int value)
|
||||
{
|
||||
if (goldText != null)
|
||||
{
|
||||
goldText.text = value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a641bd6447847f2af2cd5af40305a5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using R3;
|
||||
|
||||
namespace FlowScope.Samples.MainMenuP0
|
||||
{
|
||||
public sealed class MainMenuViewModel
|
||||
{
|
||||
private readonly PlayerData _data;
|
||||
|
||||
public MainMenuViewModel(PlayerData data)
|
||||
{
|
||||
_data = data ?? throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
public ReactiveProperty<int> Gold => _data.Gold;
|
||||
public event Action<int> GoldChanged;
|
||||
|
||||
public void IncrementGold()
|
||||
{
|
||||
Gold.Value++;
|
||||
GoldChanged?.Invoke(Gold.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 68e2343c920540a280ac2718ceae12ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
using R3;
|
||||
|
||||
namespace FlowScope.Samples.MainMenuP0
|
||||
{
|
||||
public sealed class PlayerData
|
||||
{
|
||||
public ReactiveProperty<int> Gold { get; set; } = new(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d62814450dc46fba84df1fba2a372f4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user