Files
FlowScope/Packages/com.flowscope.gamecore/Samples~/MainMenuP0/Scripts/GameBootstrap.cs
2026-06-04 20:16:28 +08:00

304 lines
10 KiB
C#

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;
}
}
}