升级 P1 主菜单样例和使用规范
This commit is contained in:
@@ -25,8 +25,15 @@ namespace FlowScope.Samples.MainMenuP0
|
||||
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 _shutdown;
|
||||
|
||||
public int ReleasedResourceCount => _sampleBackend?.ReleaseCount ?? 0;
|
||||
public bool IsShutdown => _shutdown;
|
||||
|
||||
private async void Awake()
|
||||
{
|
||||
@@ -36,35 +43,61 @@ namespace FlowScope.Samples.MainMenuP0
|
||||
|
||||
public async Task StartupAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_started = true;
|
||||
_shutdown = false;
|
||||
_root = new ContainerType();
|
||||
_gameFlow = new GameFlow();
|
||||
|
||||
var resources = new SampleResourceService();
|
||||
resources.Register(
|
||||
_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());
|
||||
new JsonSaveSerializer(2, migrations));
|
||||
_playerData = await _saveService.LoadAsync(
|
||||
SaveKey,
|
||||
new PlayerData { Gold = new R3.ReactiveProperty<int>(defaultGold) },
|
||||
cancellationToken);
|
||||
|
||||
var uiManager = new UIManager(resources);
|
||||
uiManager.RegisterLayer("hud", ResolveHudCanvas(), 0, PanelStrategy.Cache);
|
||||
await _preloadService.PreloadAsync<MainMenuPanel>(cancellationToken);
|
||||
|
||||
var audioService = gameObject.AddComponent<AudioService>().Initialize(resources);
|
||||
_root.RegisterInstance(_root);
|
||||
_root.RegisterInstance<IConfigProvider>(
|
||||
new JsonConfigProvider(Array.Empty<JsonConfigProvider.ConfigMapping>()));
|
||||
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.RegisterFactory(_ => new MainMenuFeature(uiManager, _playerData));
|
||||
_root.RegisterFactory(_ => new MainMenuFeature(screenNavigator, _playerData));
|
||||
|
||||
await _gameFlow.StartupAsync<MainMenuFeature>(_root, cancellationToken);
|
||||
}
|
||||
@@ -80,6 +113,9 @@ namespace FlowScope.Samples.MainMenuP0
|
||||
{
|
||||
await _gameFlow.ShutdownAsync(cancellationToken);
|
||||
}
|
||||
|
||||
_preloadService?.ReleaseAll();
|
||||
_shutdown = true;
|
||||
}
|
||||
|
||||
private async void OnApplicationQuit()
|
||||
@@ -137,16 +173,19 @@ namespace FlowScope.Samples.MainMenuP0
|
||||
return mainMenuPanelPrefab;
|
||||
}
|
||||
|
||||
private sealed class SampleResourceService : IResourceService
|
||||
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<IResourceHandle<T>> LoadAsync<T>(
|
||||
public Task<T> LoadAsync<T>(
|
||||
string key,
|
||||
CancellationToken cancellationToken)
|
||||
where T : class
|
||||
@@ -155,36 +194,32 @@ namespace FlowScope.Samples.MainMenuP0
|
||||
if (typeof(T) == typeof(GameObject) &&
|
||||
_prefabs.TryGetValue(key, out var prefab))
|
||||
{
|
||||
return Task.FromResult<IResourceHandle<T>>(
|
||||
new SampleResourceHandle<T>(key, prefab as T));
|
||||
return Task.FromResult(prefab as T);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Sample resource is not registered: {key}");
|
||||
}
|
||||
|
||||
public IResourceGroup CreateGroup()
|
||||
public void Release<T>(string key, T asset)
|
||||
where T : class
|
||||
{
|
||||
return new ResourceGroup();
|
||||
ReleaseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SampleResourceHandle<T> : IResourceHandle<T> where T : class
|
||||
private sealed class SampleMenuConfig : IConfigRow
|
||||
{
|
||||
public SampleResourceHandle(string key, T asset)
|
||||
{
|
||||
Key = key;
|
||||
Asset = asset;
|
||||
}
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; }
|
||||
}
|
||||
|
||||
public string Key { get; }
|
||||
public T Asset { get; private set; }
|
||||
public bool IsDisposed { get; private set; }
|
||||
private sealed class PlayerDataV1ToV2Migration : ISaveMigration
|
||||
{
|
||||
public string Key => typeof(PlayerData).FullName;
|
||||
public int FromVersion => 1;
|
||||
public int ToVersion => 2;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
Asset = null;
|
||||
}
|
||||
public string Migrate(string json) => json;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ namespace FlowScope.Samples.MainMenuP0
|
||||
{
|
||||
public sealed class MainMenuFeature : FeatureBase
|
||||
{
|
||||
private readonly UIManager _uiManager;
|
||||
private readonly IUIScreenNavigator _screenNavigator;
|
||||
private readonly PlayerData _playerData;
|
||||
private MainMenuViewModel _viewModel;
|
||||
|
||||
public MainMenuFeature(UIManager uiManager, PlayerData playerData)
|
||||
public MainMenuFeature(IUIScreenNavigator screenNavigator, PlayerData playerData)
|
||||
{
|
||||
_uiManager = uiManager;
|
||||
_screenNavigator = screenNavigator;
|
||||
_playerData = playerData;
|
||||
}
|
||||
|
||||
@@ -25,14 +25,14 @@ namespace FlowScope.Samples.MainMenuP0
|
||||
|
||||
public override async Task EnterAsync(FeatureContext context)
|
||||
{
|
||||
await _uiManager.OpenAsync<MainMenuPanel, MainMenuViewModel>(
|
||||
await _screenNavigator.PushAsync<MainMenuPanel, MainMenuViewModel>(
|
||||
_viewModel,
|
||||
context.CancellationToken);
|
||||
}
|
||||
|
||||
public override Task ExitAsync(FeatureContext context)
|
||||
{
|
||||
_uiManager.Close<MainMenuPanel>();
|
||||
_screenNavigator.Clear();
|
||||
return base.ExitAsync(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using UnityEngine.UI;
|
||||
|
||||
namespace FlowScope.Samples.MainMenuP0
|
||||
{
|
||||
[UIPanel("hud", "FlowScope/Samples/MainMenuP0/MainMenuPanel", PanelStrategy.Cache)]
|
||||
[UIPanel("hud", "FlowScope/Samples/MainMenuP0/MainMenuPanel")]
|
||||
public sealed class MainMenuPanel : UIPanelBase<MainMenuViewModel>
|
||||
{
|
||||
[SerializeField] private Text goldText;
|
||||
|
||||
@@ -27,13 +27,11 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Bootstrap_StartClickShutdownAndRestart_RestoresSavedGold()
|
||||
public IEnumerator Bootstrap_StartsAndOpensVisibleMainMenuPanel()
|
||||
{
|
||||
DeleteSampleSave();
|
||||
var root = new GameObject("bootstrap-test");
|
||||
root.SetActive(false);
|
||||
var root = CreateInactiveBootstrap("bootstrap-visible-test", out var bootstrap);
|
||||
|
||||
var bootstrap = root.AddComponent<GameBootstrap>();
|
||||
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
||||
|
||||
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
||||
@@ -42,17 +40,49 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
var button = panel.GetComponentInChildren<Button>(true);
|
||||
Assert.That(label, Is.Not.Null);
|
||||
Assert.That(button, Is.Not.Null);
|
||||
Assert.That(panel.gameObject.activeSelf, Is.True);
|
||||
Assert.That(label.text, Is.EqualTo("0"));
|
||||
|
||||
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
||||
Object.DestroyImmediate(root);
|
||||
DeleteSampleSave();
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator ClickIncrement_UpdatesGoldTextImmediately()
|
||||
{
|
||||
DeleteSampleSave();
|
||||
var root = CreateInactiveBootstrap("bootstrap-click-test", out var bootstrap);
|
||||
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
||||
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
||||
var label = panel.GetComponentInChildren<Text>(true);
|
||||
var button = panel.GetComponentInChildren<Button>(true);
|
||||
|
||||
button.onClick.Invoke();
|
||||
|
||||
Assert.That(label.text, Is.EqualTo("1"));
|
||||
|
||||
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
||||
Object.DestroyImmediate(root);
|
||||
DeleteSampleSave();
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
var restartRoot = new GameObject("bootstrap-restart-test");
|
||||
restartRoot.SetActive(false);
|
||||
var restartedBootstrap = restartRoot.AddComponent<GameBootstrap>();
|
||||
[UnityTest]
|
||||
public IEnumerator ShutdownAndRestart_RestoresSavedGold()
|
||||
{
|
||||
DeleteSampleSave();
|
||||
var root = CreateInactiveBootstrap("bootstrap-save-test", out var bootstrap);
|
||||
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
||||
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
||||
var button = panel.GetComponentInChildren<Button>(true);
|
||||
|
||||
button.onClick.Invoke();
|
||||
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
||||
Object.DestroyImmediate(root);
|
||||
|
||||
var restartRoot = CreateInactiveBootstrap("bootstrap-restart-test", out var restartedBootstrap);
|
||||
yield return AwaitTask(restartedBootstrap.StartupAsync(CancellationToken.None));
|
||||
|
||||
var restoredPanel = Object.FindObjectOfType<MainMenuPanel>(true);
|
||||
@@ -62,6 +92,27 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
yield return AwaitTask(restartedBootstrap.ShutdownAsync(CancellationToken.None));
|
||||
Object.DestroyImmediate(restartRoot);
|
||||
DeleteSampleSave();
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator Shutdown_ReleasesPreloadedResourcesAndSubscriptions()
|
||||
{
|
||||
DeleteSampleSave();
|
||||
var root = CreateInactiveBootstrap("bootstrap-release-test", out var bootstrap);
|
||||
yield return AwaitTask(bootstrap.StartupAsync(CancellationToken.None));
|
||||
var panel = Object.FindObjectOfType<MainMenuPanel>(true);
|
||||
|
||||
yield return AwaitTask(bootstrap.ShutdownAsync(CancellationToken.None));
|
||||
yield return null;
|
||||
|
||||
Assert.That(bootstrap.IsShutdown, Is.True);
|
||||
Assert.That(bootstrap.ReleasedResourceCount, Is.EqualTo(1));
|
||||
Assert.That(panel == null, Is.True);
|
||||
|
||||
Object.DestroyImmediate(root);
|
||||
DeleteSampleSave();
|
||||
LogAssert.NoUnexpectedReceived();
|
||||
}
|
||||
|
||||
private static IEnumerator AwaitTask(Task task)
|
||||
@@ -82,6 +133,14 @@ namespace FlowScope.Tests.PlayMode.Samples
|
||||
}
|
||||
}
|
||||
|
||||
private static GameObject CreateInactiveBootstrap(string name, out GameBootstrap bootstrap)
|
||||
{
|
||||
var root = new GameObject(name);
|
||||
root.SetActive(false);
|
||||
bootstrap = root.AddComponent<GameBootstrap>();
|
||||
return root;
|
||||
}
|
||||
|
||||
private static void DeleteSampleSave()
|
||||
{
|
||||
var path = Path.Combine(
|
||||
|
||||
Reference in New Issue
Block a user