Add subtitle UI for voice playback

This commit is contained in:
2026-05-16 21:33:00 +01:00
parent 58050abded
commit 3174079e37
81 changed files with 8657 additions and 1231 deletions

View File

@@ -255,6 +255,7 @@ namespace BriarQueen.UI.Codex
}
public WindowType WindowType => WindowType.CodexWindow;
public UIPauseBehavior PauseBehavior => UIPauseBehavior.TreatAsBackRequest;
// ── IUIWindow ─────────────────────────────────────────────────
@@ -263,7 +264,7 @@ namespace BriarQueen.UI.Codex
ResetOperationCts();
gameObject.SetActive(true);
TryRegisterRaycaster();
EnsureExclusiveRaycaster();
if (_canvasGroup != null)
{
@@ -380,7 +381,7 @@ namespace BriarQueen.UI.Codex
// ── Raycaster ─────────────────────────────────────────────────
private void TryRegisterRaycaster()
private void EnsureExclusiveRaycaster()
{
Debug.Log($"[CodexWindow] TryRegisterRaycaster " +
@@ -392,10 +393,15 @@ namespace BriarQueen.UI.Codex
Debug.Log("[CodexWindow] Try register raycaster.");
if (_raycasterRegistered || _interactManager == null || _graphicRaycaster == null) return;
_interactManager.AddUIRaycaster(_graphicRaycaster);
if (_interactManager == null || _graphicRaycaster == null) return;
if (!_raycasterRegistered)
{
_interactManager.AddUIRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
Debug.Log("[CodexWindow] Registered raycaster.");
}
@@ -406,7 +412,7 @@ namespace BriarQueen.UI.Codex
if (!_raycasterRegistered || _interactManager == null || _graphicRaycaster == null) return;
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
_interactManager.ClearExclusiveRaycaster();
_interactManager.ReleaseExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = false;
Debug.Log("[CodexWindow] Raycaster unregistered.");

View File

@@ -0,0 +1,212 @@
using System;
using System.Threading;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Services.Subtitles;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
using VContainer;
namespace BriarQueen.UI.HUD
{
public class SubtitleUI : MonoBehaviour
{
[Header("UI")]
[SerializeField]
private InteractTextUI _interactTextUI;
[SerializeField]
private CanvasGroup _canvasGroup;
[Header("Tweens")]
[SerializeField]
private TweenSettings _tweenSettings = new()
{
duration = 0.2f,
ease = Ease.InOutSine,
useUnscaledTime = true
};
private EventCoordinator _eventCoordinator;
private SubtitleService _subtitleService;
private CancellationTokenSource _cancellationTokenSource;
private Sequence _sequence;
[Inject]
public void Construct(EventCoordinator eventCoordinator, SubtitleService subtitleService)
{
_eventCoordinator = eventCoordinator;
_subtitleService = subtitleService;
}
private void Awake()
{
ApplyImmediate(string.Empty, false);
}
private void OnEnable()
{
_eventCoordinator?.Subscribe<SubtitleDisplayChangedEvent>(OnSubtitleDisplayChanged);
SyncFromService();
}
private void OnDisable()
{
_eventCoordinator?.Unsubscribe<SubtitleDisplayChangedEvent>(OnSubtitleDisplayChanged);
StopTween();
}
private void OnDestroy()
{
StopTween();
}
private void SyncFromService()
{
if (_subtitleService != null && _subtitleService.IsVisible)
{
ApplyImmediate(_subtitleService.CurrentText, true);
return;
}
ApplyImmediate(string.Empty, false);
}
private void OnSubtitleDisplayChanged(SubtitleDisplayChangedEvent eventData)
{
if (!eventData.Visible || string.IsNullOrWhiteSpace(eventData.Text))
{
HideSubtitle().Forget();
return;
}
ShowSubtitle(eventData.Text).Forget();
}
private async UniTaskVoid ShowSubtitle(string text)
{
if (_canvasGroup == null || _interactTextUI == null)
return;
StopTween();
_interactTextUI.SetText(text);
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
_cancellationTokenSource = new CancellationTokenSource();
var tween = new TweenSettings<float>
{
startValue = _canvasGroup.alpha,
endValue = 1f,
settings = _tweenSettings
};
_sequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_canvasGroup, tween));
try
{
await _sequence.ToUniTask(cancellationToken: _cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
DisposeTweenState();
}
_canvasGroup.alpha = 1f;
}
private async UniTaskVoid HideSubtitle()
{
if (_canvasGroup == null || _interactTextUI == null)
return;
StopTween();
_cancellationTokenSource = new CancellationTokenSource();
var tween = new TweenSettings<float>
{
startValue = _canvasGroup.alpha,
endValue = 0f,
settings = _tweenSettings
};
_sequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_canvasGroup, tween));
try
{
await _sequence.ToUniTask(cancellationToken: _cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
DisposeTweenState();
}
ApplyImmediate(string.Empty, false);
}
private void ApplyImmediate(string text, bool visible)
{
if (_interactTextUI != null)
{
if (visible)
_interactTextUI.SetText(text);
else
_interactTextUI.ClearText();
}
if (_canvasGroup == null)
return;
_canvasGroup.alpha = visible ? 1f : 0f;
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
}
private void StopTween()
{
if (_sequence.isAlive)
_sequence.Stop();
_sequence = default;
if (_cancellationTokenSource == null)
return;
try
{
_cancellationTokenSource.Cancel();
}
catch
{
}
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
}
private void DisposeTweenState()
{
_sequence = default;
if (_cancellationTokenSource == null)
return;
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a7225bc6a3f524fb8b820162cd937c1e

View File

@@ -635,6 +635,11 @@ namespace BriarQueen.UI.Menus
group.blocksRaycasts = inputEnabled;
}
public bool CanSuspendFor(WindowType incomingWindowType)
{
return incomingWindowType == WindowType.SettingsWindow;
}
public async UniTask SuspendForOverlay()
{
if (_mainMenuGroup == null)

View File

@@ -79,18 +79,25 @@ namespace BriarQueen.UI.Menus
public bool IsModal => true;
public WindowType WindowType => WindowType.PauseMenuWindow;
private void TryRegisterRaycaster()
{
if (_raycasterRegistered)
return;
public UIPauseBehavior PauseBehavior => UIPauseBehavior.TreatAsBackRequest;
public bool CanSuspendFor(WindowType incomingWindowType)
{
return incomingWindowType == WindowType.SettingsWindow;
}
private void EnsureExclusiveRaycaster()
{
if (_interactManager == null || _graphicRaycaster == null)
return;
_interactManager.AddUIRaycaster(_graphicRaycaster);
if (!_raycasterRegistered)
{
_interactManager.AddUIRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
private void TryUnregisterRaycaster()
@@ -102,7 +109,7 @@ namespace BriarQueen.UI.Menus
return;
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
_interactManager.ClearExclusiveRaycaster();
_interactManager.ReleaseExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = false;
}
@@ -149,7 +156,7 @@ namespace BriarQueen.UI.Menus
SetLevelName();
gameObject.SetActive(true);
TryRegisterRaycaster();
EnsureExclusiveRaycaster();
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
@@ -233,6 +240,7 @@ namespace BriarQueen.UI.Menus
public async UniTask ResumeFromOverlay()
{
StopAndResetCancellation();
EnsureExclusiveRaycaster();
_buttonsGroup.blocksRaycasts = true;
_buttonsGroup.interactable = true;

View File

@@ -147,6 +147,7 @@ namespace BriarQueen.UI.Menus
// ── IUIWindow ─────────────────────────────────────────────────
public bool IsModal => true;
public WindowType WindowType => WindowType.SettingsWindow;
public UIPauseBehavior PauseBehavior => UIPauseBehavior.TreatAsBackRequest;
// ── Unity lifecycle ───────────────────────────────────────────
private void Awake()
@@ -155,9 +156,9 @@ namespace BriarQueen.UI.Menus
if (_backButton != null) _backButton.onClick.AddListener(OnBackClicked);
// Individual buttons drive panel switching via SelectionRequested
if (_gameCategoryButton != null) _gameCategoryButton.SelectionRequested += _ => SwitchCategory(Category.Game);
if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested += _ => SwitchCategory(Category.Visual);
if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested += _ => SwitchCategory(Category.Audio);
if (_gameCategoryButton != null) _gameCategoryButton.SelectionRequested += OnGameCategorySelected;
if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested += OnVisualCategorySelected;
if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested += OnAudioCategorySelected;
HookSlider(_masterVolumeSlider, _masterVolumeText, v => _draftAudio.MasterVolume = v);
HookSlider(_musicVolumeSlider, _musicVolumeText, v => _draftAudio.MusicVolume = v);
@@ -210,9 +211,9 @@ namespace BriarQueen.UI.Menus
if (_applyButton != null) _applyButton.onClick.RemoveListener(OnApplyClicked);
if (_backButton != null) _backButton.onClick.RemoveListener(OnBackClicked);
if (_gameCategoryButton != null) _gameCategoryButton.SelectionRequested -= _ => SwitchCategory(Category.Game);
if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested -= _ => SwitchCategory(Category.Visual);
if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested -= _ => SwitchCategory(Category.Audio);
if (_gameCategoryButton != null) _gameCategoryButton.SelectionRequested -= OnGameCategorySelected;
if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested -= OnVisualCategorySelected;
if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested -= OnAudioCategorySelected;
if (_popupDisplayDurationSlider != null)
_popupDisplayDurationSlider.onValueChanged.RemoveListener(OnPopupDisplayDurationChanged);
@@ -246,7 +247,7 @@ namespace BriarQueen.UI.Menus
StopAndResetCancellation();
gameObject.SetActive(true);
TryRegisterRaycaster();
EnsureExclusiveRaycaster();
_canvasGroup.alpha = 1f;
_canvasGroup.blocksRaycasts = false;
@@ -368,6 +369,21 @@ namespace BriarQueen.UI.Menus
return true;
}
private void OnGameCategorySelected(AnimatedSelectionButton _)
{
SwitchCategory(Category.Game);
}
private void OnVisualCategorySelected(AnimatedSelectionButton _)
{
SwitchCategory(Category.Visual);
}
private void OnAudioCategorySelected(AnimatedSelectionButton _)
{
SwitchCategory(Category.Audio);
}
// ── Category switching ────────────────────────────────────────
private void SwitchCategory(Category category)
@@ -909,17 +925,18 @@ namespace BriarQueen.UI.Menus
};
}
private void TryRegisterRaycaster()
private void EnsureExclusiveRaycaster()
{
if (_raycasterRegistered)
return;
if (_interactManager == null || _graphicRaycaster == null)
return;
_interactManager.AddUIRaycaster(_graphicRaycaster);
if (!_raycasterRegistered)
{
_interactManager.AddUIRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
private void TryUnregisterRaycaster()
@@ -931,7 +948,7 @@ namespace BriarQueen.UI.Menus
return;
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
_interactManager.ClearExclusiveRaycaster();
_interactManager.ReleaseExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = false;
}
}

View File

@@ -40,6 +40,9 @@ namespace BriarQueen.UI.Scopes
[SerializeField]
private InventoryBar _inventoryBar;
[SerializeField]
private SubtitleUI _subtitleUI;
protected override void Configure(IContainerBuilder builder)
@@ -70,6 +73,9 @@ namespace BriarQueen.UI.Scopes
if (_inventoryBar != null)
builder.RegisterComponent(_inventoryBar);
if (_subtitleUI != null)
builder.RegisterComponent(_subtitleUI);
builder.RegisterBuildCallback(container =>
@@ -101,4 +107,4 @@ namespace BriarQueen.UI.Scopes
});
}
}
}
}