First commit for private source control. Older commits available on Github.

This commit is contained in:
2026-03-26 12:52:52 +00:00
parent a04c602626
commit 2d449c4a17
2176 changed files with 408185 additions and 0 deletions

View File

@@ -0,0 +1,685 @@
using System;
using System.Collections.Generic;
using System.Threading;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Input;
using BriarQueen.Game.Cinematics.Data;
using Cysharp.Threading.Tasks;
using PrimeTween;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using VContainer;
namespace BriarQueen.Game.Cinematics
{
public abstract class BaseCinematic : MonoBehaviour
{
[Header("Cinematic Settings")]
[SerializeField]
protected List<CinematicPanel> _cinematicPanels = new();
[Header("Crossfade Settings")]
[Tooltip("Max crossfade duration. Actual fade is clamped so EACH panel still lasts exactly DisplayTime.")]
[SerializeField]
[Min(0f)]
protected float _maxCrossfadeDuration = 0.75f;
[Header("Subtitles")]
[SerializeField]
protected bool _logSubtitleTimingWarnings = true;
[Tooltip("If true, clears subtitle text when a panel ends (recommended for VO sync).")]
[SerializeField]
protected bool _clearSubtitlesBetweenPanels = true;
[Header("UI Elements")]
[SerializeField]
protected CanvasGroup _cinematicCanvasGroup;
[SerializeField]
protected TextMeshProUGUI _cinematicText;
[SerializeField]
protected TextMeshProUGUI _skipText;
[Header("Cinematic Images (Two Layers For Crossfade)")]
[SerializeField]
protected Image _imageA;
[SerializeField]
protected Image _imageB;
[Header("Input")]
[SerializeField]
protected string _skipActionName = "Cancel";
[Header("Tween Settings")]
[SerializeField]
protected float _canvasFadeInDuration = 0.25f;
[SerializeField]
protected float _canvasFadeOutDuration = 0.25f;
[SerializeField]
protected float _finalImageFadeOutDuration = 0.25f;
[Header("Skip Text Tween (Yoyo Alpha)")]
[SerializeField]
protected bool _animateSkipText = true;
[SerializeField]
[Min(0.05f)]
protected float _skipTextHalfCycleSeconds = 0.75f;
[SerializeField]
[Range(0f, 1f)]
protected float _skipTextMinAlpha = 0.25f;
[SerializeField]
[Range(0f, 1f)]
protected float _skipTextMaxAlpha = 1f;
[SerializeField]
protected Ease _skipTextEase = Ease.InOutSine;
[Header("Test/Debug (No Injection Needed)")]
[Tooltip("If true, Escape will skip even when InputManager isn't injected (handy for editor tests).")]
[SerializeField]
private bool _allowKeyboardEscapeSkipFallback = true;
private CancellationTokenSource _cinematicCts;
private bool _endingStarted;
private bool _isPlaying;
private bool _skipRequested;
private Sequence _skipTextSequence;
protected EventCoordinator EventCoordinator;
protected InputManager InputManager;
protected virtual void Awake()
{
if (_cinematicCanvasGroup != null)
_cinematicCanvasGroup.alpha = 0f;
SetImageAlpha(_imageA, 0f);
SetImageAlpha(_imageB, 0f);
ClearSubtitleText();
StopSkipTextTween();
}
private void Update()
{
if (!_isPlaying) return;
if (!_allowKeyboardEscapeSkipFallback) return;
if (InputManager != null) return;
var kb = Keyboard.current;
if (kb != null && kb.escapeKey.wasPressedThisFrame)
RequestSkip();
}
protected virtual void OnEnable()
{
BindSkipAction();
}
protected virtual void OnDisable()
{
UnbindSkipAction();
CancelAndDisposeCts();
StopSkipTextTween();
_isPlaying = false;
}
protected virtual void OnDestroy()
{
UnbindSkipAction();
CancelAndDisposeCts();
StopSkipTextTween();
}
protected async UniTask PlayCinematic(CancellationToken destroyToken)
{
if (_cinematicCanvasGroup == null || _imageA == null || _imageB == null)
{
Debug.LogWarning("[BaseCinematic] Missing CanvasGroup or Images.");
return;
}
_isPlaying = true;
if (_cinematicPanels == null || _cinematicPanels.Count == 0)
{
await EndCinematicFlow();
return;
}
CancelAndDisposeCts();
_cinematicCts = CancellationTokenSource.CreateLinkedTokenSource(destroyToken);
var playbackToken = _cinematicCts.Token;
_skipRequested = false;
_endingStarted = false;
_cinematicCanvasGroup.blocksRaycasts = false;
_cinematicCanvasGroup.interactable = false;
_cinematicCanvasGroup.alpha = 0f;
ClearSubtitleText();
SetImageAlpha(_imageA, 0f);
SetImageAlpha(_imageB, 0f);
StartSkipTextTweenIfNeeded();
await FadeCanvasGroup(_cinematicCanvasGroup, 0f, 1f, _canvasFadeInDuration, destroyToken);
_cinematicCanvasGroup.blocksRaycasts = true;
_cinematicCanvasGroup.interactable = true;
try
{
for (var i = 0; i < _cinematicPanels.Count; i++)
{
if (_skipRequested)
break;
var panel = GetPanel(i);
if (panel == null || panel.CinematicImage == null)
continue;
var currentTime = Mathf.Max(0f, panel.DisplayTime);
var nextTime = i + 1 < _cinematicPanels.Count
? Mathf.Max(0f, GetPanel(i + 1)?.DisplayTime ?? 0f)
: 0f;
var isFirst = i == 0;
var isLast = i == _cinematicPanels.Count - 1;
await RunPanelWithSubtitles(i, currentTime, nextTime, isFirst, isLast, playbackToken);
if (_clearSubtitlesBetweenPanels && !isLast)
ClearSubtitleText();
}
}
catch (OperationCanceledException)
{
}
var safeExitToken = this.GetCancellationTokenOnDestroy();
await FadeOutAtEnd(safeExitToken);
await UniTask.Delay(TimeSpan.FromSeconds(2f), DelayType.UnscaledDeltaTime,
cancellationToken: safeExitToken);
await EndCinematicFlow();
}
protected virtual UniTask OnCinematicEnd()
{
return UniTask.CompletedTask;
}
protected void RequestSkip()
{
Debug.Log("Skip Requested");
if (_skipRequested || _endingStarted)
return;
_skipRequested = true;
if (_cinematicCts != null && !_cinematicCts.IsCancellationRequested)
try
{
_cinematicCts.Cancel();
}
catch
{
}
}
protected virtual void BindSkipAction()
{
if (InputManager == null)
return;
InputManager.BindPauseForSkip(OnSkipPerformed);
if (_skipText != null)
{
var inputType = InputManager.DeviceInputType;
switch (inputType)
{
case DeviceInputType.KeyboardAndMouse:
_skipText.text = "Press Escape to Skip.";
break;
case DeviceInputType.XboxController:
_skipText.text = "Press B to Skip.";
break;
case DeviceInputType.PlaystationController:
_skipText.text = "Press Circle to Skip.";
break;
case DeviceInputType.SwitchProController:
_skipText.text = "Press B to Skip.";
break;
default:
_skipText.text = "Press Cancel to Skip.";
break;
}
}
}
protected virtual void UnbindSkipAction()
{
if (InputManager == null)
return;
InputManager.ResetPauseBind(OnSkipPerformed);
}
private void OnSkipPerformed(InputAction.CallbackContext _)
{
RequestSkip();
}
private void CancelAndDisposeCts()
{
if (_cinematicCts == null)
return;
try
{
_cinematicCts.Cancel();
}
catch
{
}
_cinematicCts.Dispose();
_cinematicCts = null;
}
private async UniTask EndCinematicFlow()
{
if (_endingStarted)
return;
_endingStarted = true;
_isPlaying = false;
StopSkipTextTween();
ClearSubtitleText();
await OnCinematicEnd();
}
private void StartSkipTextTweenIfNeeded()
{
if (!_animateSkipText || _skipText == null)
return;
StopSkipTextTween();
var c = _skipText.color;
c.a = Mathf.Clamp01(_skipTextMaxAlpha);
_skipText.color = c;
_skipTextSequence = Sequence.Create(
useUnscaledTime: true,
cycleMode: Sequence.SequenceCycleMode.Yoyo,
cycles: -1
)
.Group(Tween.Alpha(_skipText, new TweenSettings<float>
{
startValue = _skipTextMaxAlpha,
endValue = _skipTextMinAlpha,
settings = new TweenSettings
{
duration = _skipTextHalfCycleSeconds,
ease = _skipTextEase,
useUnscaledTime = true
}
}));
}
private void StopSkipTextTween()
{
if (_skipTextSequence.isAlive)
_skipTextSequence.Stop();
_skipTextSequence = default;
}
private CinematicPanel GetPanel(int index)
{
if (_cinematicPanels == null) return null;
if (index < 0 || index >= _cinematicPanels.Count) return null;
return _cinematicPanels[index];
}
private async UniTask RunPanelWithSubtitles(int panelIndex, float currentTime, float nextTime, bool isFirst,
bool isLast, CancellationToken token)
{
using var panelCts = CancellationTokenSource.CreateLinkedTokenSource(token);
var panelToken = panelCts.Token;
await UniTask.WhenAll(
ShowPanelExact(panelIndex, currentTime, nextTime, isFirst, isLast, panelToken),
RunSubtitlesForPanel(panelIndex, currentTime, panelToken)
);
}
private async UniTask RunSubtitlesForPanel(int panelIndex, float panelTime, CancellationToken token)
{
if (_cinematicText == null)
return;
var panel = GetPanel(panelIndex);
if (panel == null)
return;
var subs = panel.Subtitles;
if (subs == null || subs.Count == 0)
{
ClearSubtitleText();
if (panelTime > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(panelTime), DelayType.UnscaledDeltaTime,
cancellationToken: token);
return;
}
var sum = 0f;
for (var i = 0; i < subs.Count; i++)
{
if (subs[i].Equals(default))
continue;
sum += Mathf.Max(0f, subs[i].DisplayTime);
}
if (_logSubtitleTimingWarnings && sum > panelTime + 0.001f)
Debug.LogWarning(
$"[BaseCinematic] Panel '{panel.name}' subtitle times sum to {sum:0.###}s but panel DisplayTime is {panelTime:0.###}s. Subtitles will be cut short.");
var remaining = panelTime;
for (var i = 0; i < subs.Count; i++)
{
token.ThrowIfCancellationRequested();
var sub = subs[i];
if (sub.Equals(default))
continue;
var subTime = Mathf.Max(0f, sub.DisplayTime);
if (subTime <= 0f)
continue;
var actual = Mathf.Min(subTime, remaining);
if (actual <= 0f)
break;
_cinematicText.text = sub.Subtitle ?? string.Empty;
await UniTask.Delay(TimeSpan.FromSeconds(actual), DelayType.UnscaledDeltaTime,
cancellationToken: token);
remaining -= actual;
if (remaining <= 0f)
break;
}
if (remaining > 0f)
{
_cinematicText.text = string.Empty;
await UniTask.Delay(TimeSpan.FromSeconds(remaining), DelayType.UnscaledDeltaTime,
cancellationToken: token);
}
}
private void ClearSubtitleText()
{
if (_cinematicText != null)
_cinematicText.text = string.Empty;
}
private async UniTask ShowPanelExact(int panelIndex, float currentTime, float nextTime, bool isFirst,
bool isLast, CancellationToken token)
{
var panel = GetPanel(panelIndex);
if (panel == null || panel.CinematicImage == null)
return;
var boundaryFade = 0f;
if (!isLast)
boundaryFade = Mathf.Min(_maxCrossfadeDuration, currentTime, nextTime);
var introFade = 0f;
if (isFirst)
{
introFade = isLast
? Mathf.Min(_maxCrossfadeDuration, currentTime)
: Mathf.Min(_maxCrossfadeDuration, currentTime, nextTime);
_imageA.sprite = panel.CinematicImage;
SetImageAlpha(_imageA, 0f);
SetImageAlpha(_imageB, 0f);
if (introFade > 0f)
await FadeImage(_imageA, 0f, 1f, introFade, token);
else
SetImageAlpha(_imageA, 1f);
var hold = Mathf.Max(0f, currentTime - introFade - boundaryFade);
if (hold > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(hold), DelayType.UnscaledDeltaTime,
cancellationToken: token);
if (!isLast && boundaryFade > 0f)
await CrossfadeToNext(panelIndex + 1, boundaryFade, token);
return;
}
var holdTime = Mathf.Max(0f, currentTime - boundaryFade);
if (holdTime > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(holdTime), DelayType.UnscaledDeltaTime,
cancellationToken: token);
if (!isLast && boundaryFade > 0f)
await CrossfadeToNext(panelIndex + 1, boundaryFade, token);
}
private async UniTask CrossfadeToNext(int nextPanelIndex, float duration, CancellationToken token)
{
var nextPanel = GetPanel(nextPanelIndex);
if (nextPanel == null || nextPanel.CinematicImage == null)
return;
var current = GetMoreVisible(_imageA, _imageB);
var next = current == _imageA ? _imageB : _imageA;
next.sprite = nextPanel.CinematicImage;
var currentAlpha = GetImageAlpha(current);
SetImageAlpha(next, 0f);
var seq = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(current, new TweenSettings<float>
{
startValue = currentAlpha,
endValue = 0f,
settings = new TweenSettings { duration = duration, ease = Ease.Linear, useUnscaledTime = true }
}))
.Group(Tween.Alpha(next, new TweenSettings<float>
{
startValue = 0f,
endValue = 1f,
settings = new TweenSettings { duration = duration, ease = Ease.Linear, useUnscaledTime = true }
}));
await seq.ToUniTask(cancellationToken: token);
}
private async UniTask FadeOutAtEnd(CancellationToken token)
{
StopSkipTextTween();
if (EventCoordinator != null)
{
if (_cinematicCanvasGroup != null)
{
_cinematicCanvasGroup.blocksRaycasts = false;
_cinematicCanvasGroup.interactable = false;
_cinematicCanvasGroup.alpha = 1f;
}
EventCoordinator.PublishImmediate(new FadeEvent(false, 1f));
return;
}
var img = GetMoreVisible(_imageA, _imageB);
var imgFrom = GetImageAlpha(img);
if ((_cinematicCanvasGroup == null || _cinematicCanvasGroup.alpha <= 0.001f) && imgFrom <= 0.001f)
return;
var canvasFrom = _cinematicCanvasGroup != null ? _cinematicCanvasGroup.alpha : 0f;
var imgDur = Mathf.Max(0f, _finalImageFadeOutDuration);
var canvasDur = Mathf.Max(0f, _canvasFadeOutDuration);
var seq = Sequence.Create(useUnscaledTime: true);
if (img != null && imgDur > 0f && imgFrom > 0.001f)
seq.Group(Tween.Alpha(img, new TweenSettings<float>
{
startValue = imgFrom,
endValue = 0f,
settings = new TweenSettings { duration = imgDur, ease = Ease.OutQuad, useUnscaledTime = true }
}));
else if (img != null) SetImageAlpha(img, 0f);
if (_cinematicCanvasGroup != null && canvasDur > 0f && canvasFrom > 0.001f)
{
_cinematicCanvasGroup.blocksRaycasts = false;
_cinematicCanvasGroup.interactable = false;
seq.Group(Tween.Alpha(_cinematicCanvasGroup, new TweenSettings<float>
{
startValue = canvasFrom,
endValue = 0f,
settings = new TweenSettings { duration = canvasDur, ease = Ease.OutQuad, useUnscaledTime = true }
}));
}
else if (_cinematicCanvasGroup != null)
{
_cinematicCanvasGroup.alpha = 0f;
_cinematicCanvasGroup.blocksRaycasts = false;
_cinematicCanvasGroup.interactable = false;
}
try
{
if (seq.isAlive)
await seq.ToUniTask(cancellationToken: token);
}
catch (OperationCanceledException)
{
if (img != null) SetImageAlpha(img, 0f);
if (_cinematicCanvasGroup != null) _cinematicCanvasGroup.alpha = 0f;
}
}
private static async UniTask FadeCanvasGroup(CanvasGroup group, float from, float to, float duration,
CancellationToken token)
{
if (group == null)
return;
if (duration <= 0f)
{
group.alpha = to;
return;
}
group.alpha = from;
var seq = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(group, new TweenSettings<float>
{
startValue = from,
endValue = to,
settings = new TweenSettings { duration = duration, ease = Ease.OutQuad, useUnscaledTime = true }
}));
await seq.ToUniTask(cancellationToken: token);
}
private static async UniTask FadeImage(Image image, float from, float to, float duration,
CancellationToken token)
{
if (image == null)
return;
if (duration <= 0f)
{
SetImageAlpha(image, to);
return;
}
SetImageAlpha(image, from);
var seq = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(image, new TweenSettings<float>
{
startValue = from,
endValue = to,
settings = new TweenSettings { duration = duration, ease = Ease.OutQuad, useUnscaledTime = true }
}));
await seq.ToUniTask(cancellationToken: token);
}
private static float GetImageAlpha(Image image)
{
if (image == null)
return 0f;
return image.color.a;
}
private static void SetImageAlpha(Image image, float alpha)
{
if (image == null)
return;
var c = image.color;
c.a = Mathf.Clamp01(alpha);
image.color = c;
}
private static Image GetMoreVisible(Image a, Image b)
{
var aa = GetImageAlpha(a);
var bb = GetImageAlpha(b);
return aa >= bb ? a : b;
}
[Inject]
public void Construct(EventCoordinator eventCoordinator, InputManager inputManager)
{
EventCoordinator = eventCoordinator;
InputManager = inputManager;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 28c2bf5f2cf04ab7ac3c1613e6164f9b
timeCreated: 1770666923

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3ec5cb96d11346c6b66b58e2c33d6539
timeCreated: 1773830225

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using BriarQueen.Framework.Managers.Audio.Data;
using NaughtyAttributes;
using UnityEngine;
namespace BriarQueen.Game.Cinematics.Data
{
[CreateAssetMenu(menuName = "Briar Queen/Cinematics/New Cinematic Panel", fileName = "New Cinematic Panel")]
public class CinematicPanel : ScriptableObject
{
public Sprite CinematicImage;
public float DisplayTime = 3f;
public AudioFileSo AudioFile;
[ReorderableList]
public List<CinematicSubtitle> Subtitles;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9ce2fc8cc1f54667a902fed5e8df23c4
timeCreated: 1769970619

View File

@@ -0,0 +1,11 @@
using System;
namespace BriarQueen.Game.Cinematics.Data
{
[Serializable]
public struct CinematicSubtitle
{
public string Subtitle;
public float DisplayTime;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6f5c998a34174e81a18833d43fd32548
timeCreated: 1769978310

View File

@@ -0,0 +1,115 @@
using System;
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Assets;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Managers.Input;
using BriarQueen.Framework.Managers.IO;
using BriarQueen.Framework.Managers.Levels;
using BriarQueen.Framework.Registries;
using BriarQueen.Framework.Services.Game;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using VContainer;
namespace BriarQueen.Game.Cinematics
{
public class OpeningCinematic : BaseCinematic
{
private AddressableManager _addressableManager;
private AssetRegistry _assetRegistry;
private GameService _gameService;
private LevelManager _levelManager;
private SaveManager _saveManager;
private SceneInstance _loadedScene;
private AsyncOperationHandle<SceneInstance> _loadedSceneHandle;
protected async UniTaskVoid Start()
{
var destroyToken = this.GetCancellationTokenOnDestroy();
try
{
if (_assetRegistry == null ||
!_assetRegistry.TryGetReference(
AssetKeyIdentifiers.Get(SceneKey.GameScene),
out var loadedSceneRef))
{
Debug.LogWarning("[OpeningCinematic] Missing GameScene reference in AssetRegistry.");
return;
}
_loadedSceneHandle = await _addressableManager.LoadSceneAsync(
loadedSceneRef,
autoLoad: false,
cancellationToken: destroyToken);
if (_loadedSceneHandle.IsValid())
_loadedScene = _loadedSceneHandle.Result;
else
{
Debug.LogWarning("[OpeningCinematic] Failed to preload GameScene.");
return;
}
await PlayCinematic(destroyToken);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
protected override async UniTask OnCinematicEnd()
{
if (!_loadedSceneHandle.IsValid())
{
Debug.LogWarning("[OpeningCinematic] Game scene handle invalid; cannot transition.");
await _gameService.LoadMainMenu();
return;
}
gameObject.SetActive(false);
await _loadedScene.ActivateAsync().ToUniTask();
await _gameService.SwapGameSceneHandle(_loadedSceneHandle);
if (_saveManager.CurrentSave != null)
_saveManager.CurrentSave.OpeningCinematicPlayed = true;
var levelLoaded = await _levelManager.LoadLevel(
AssetKeyIdentifiers.Get(LevelKey.ChapterOneVillageEdge));
if (!levelLoaded)
{
Debug.LogError(
"[OpeningCinematic] Failed to load ChapterOneVillageEdge after cinematic. Returning to main menu.");
await _gameService.LoadMainMenu();
}
}
[Inject]
public void Construct(
AddressableManager addressableManager,
AssetRegistry assetRegistry,
EventCoordinator eventCoordinator,
LevelManager levelManager,
GameService gameService,
InputManager inputManager,
SaveManager saveManager)
{
base.Construct(eventCoordinator, inputManager);
_addressableManager = addressableManager;
_assetRegistry = assetRegistry;
_levelManager = levelManager;
_gameService = gameService;
_saveManager = saveManager;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 60a9bb3284c148b89edd6f46df6284dc
timeCreated: 1769970270

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2f22ccda226c4e07bcdb00ff5983d62b
timeCreated: 1773833525

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace BriarQueen.Game.Cinematics.Scopes
{
public class CinematicScope : LifetimeScope
{
[SerializeField]
private List<BaseCinematic> _cinematics;
protected override void Configure(IContainerBuilder builder)
{
if (_cinematics.Count > 0)
foreach (var component in _cinematics)
builder.RegisterComponent(component);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: eb0a6d9650bf4c759ebcef1ed4f1eba1
timeCreated: 1770666441

View File

@@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Threading;
using BriarQueen.Framework.Services.Game;
using BriarQueen.Game.Cinematics.Data;
using Cysharp.Threading.Tasks;
using NaughtyAttributes;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using VContainer;
namespace BriarQueen.Game.Cinematics
{
public class SplashScreens : MonoBehaviour
{
[SerializeField]
[ReorderableList]
private List<CinematicPanel> _splashScreens = new();
[SerializeField]
private Image _activeSplashScreen;
[Header("Playback")]
[Tooltip("Small fade for polish. Set to 0 for hard cuts.")]
[SerializeField]
private float _fadeDuration = 0.15f;
[SerializeField]
private bool _skipInvalidPanels = true;
private CancellationTokenSource _cts;
private GameService _gameService;
private bool _isPlaying;
[Inject]
public void Construct(GameService gameService)
{
_gameService = gameService;
}
private void OnDisable()
{
Stop();
}
public void Play()
{
if (_isPlaying)
return;
Debug.Log("[SplashScreens] Playing splash screens.");
_isPlaying = true;
_cts?.Dispose();
_cts = new CancellationTokenSource();
RunSequence(_cts.Token).Forget();
}
public void Stop()
{
if (!_isPlaying)
return;
_isPlaying = false;
try
{
_cts?.Cancel();
}
catch
{
}
finally
{
_cts?.Dispose();
_cts = null;
}
}
private async UniTaskVoid RunSequence(CancellationToken token)
{
try
{
Debug.Log("[SplashScreens] Splash screen sequence started.");
if (_activeSplashScreen == null)
{
Debug.LogWarning("[SplashScreens] Active splash Image is not assigned.");
await EndSplashScreens();
return;
}
var cg = _activeSplashScreen.GetComponent<CanvasGroup>();
if (cg == null)
cg = _activeSplashScreen.gameObject.AddComponent<CanvasGroup>();
cg.alpha = 0f;
_activeSplashScreen.enabled = true;
if (_splashScreens == null || _splashScreens.Count == 0)
{
Debug.LogWarning("[SplashScreens] No splash panels configured.");
await EndSplashScreens();
return;
}
for (var i = 0; i < _splashScreens.Count; i++)
{
token.ThrowIfCancellationRequested();
var panel = _splashScreens[i];
if (panel == null || panel.CinematicImage == null)
{
if (_skipInvalidPanels)
continue;
Debug.LogWarning($"[SplashScreens] Splash panel at index {i} is null or missing image.");
continue;
}
_activeSplashScreen.sprite = panel.CinematicImage;
if (_fadeDuration > 0f)
await FadeTo(cg, 1f, _fadeDuration, token);
else
cg.alpha = 1f;
var hold = Mathf.Max(0f, panel.DisplayTime);
if (hold > 0f)
await UniTask.Delay(TimeSpan.FromSeconds(hold), cancellationToken: token);
if (_fadeDuration > 0f)
await FadeTo(cg, 0f, _fadeDuration, token);
else
cg.alpha = 0f;
}
await EndSplashScreens();
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
Debug.LogError($"[SplashScreens] Sequence failed: {ex}");
try
{
await EndSplashScreens();
}
catch
{
}
}
finally
{
_isPlaying = false;
}
}
private async UniTask FadeTo(CanvasGroup cg, float target, float duration, CancellationToken token)
{
var start = cg.alpha;
if (Mathf.Approximately(start, target))
{
cg.alpha = target;
return;
}
var t = 0f;
while (t < duration)
{
token.ThrowIfCancellationRequested();
t += Time.deltaTime;
var p = Mathf.Clamp01(t / duration);
cg.alpha = Mathf.Lerp(start, target, p);
await UniTask.NextFrame(token);
}
cg.alpha = target;
}
private async UniTask EndSplashScreens()
{
if (_gameService != null)
{
await _gameService.LoadUIAndMainMenuScene();
}
else
{
Debug.LogWarning("[SplashScreens] GameService is null; cannot load UI/MainMenu scene.");
}
const string bootstrapSceneName = "00_Bootstrap";
var scene = SceneManager.GetSceneByName(bootstrapSceneName);
if (scene.IsValid() && scene.isLoaded)
await SceneManager.UnloadSceneAsync(bootstrapSceneName);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 21a1c3d08d6f49a8b1c36082ed1f0a16
timeCreated: 1770136340