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

@@ -32,6 +32,9 @@ namespace BriarQueen.Framework.Effects
[SerializeField]
private bool _randomizeFlickerOffset = true;
[SerializeField]
private bool _useStartingValues;
[Header("Tween")]
[SerializeField]
@@ -81,8 +84,12 @@ namespace BriarQueen.Framework.Effects
}
CreateRuntimeMaterial();
SetLightColor(_startingColor);
SetIntensity(_startingIntensity);
if(_useStartingValues)
{
SetLightColor(_startingColor);
SetIntensity(_startingIntensity);
}
if (_randomizeFlickerOffset && _runtimeMaterial != null)
{

View File

@@ -0,0 +1,9 @@
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Events.System;
namespace BriarQueen.Framework.Events.Audio
{
public record VoicePlaybackFinishedEvent(
VoiceKey VoiceKey,
SubtitleKey SubtitleKey) : IEvent;
}

View File

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

View File

@@ -0,0 +1,10 @@
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Events.System;
namespace BriarQueen.Framework.Events.Audio
{
public record VoicePlaybackStartedEvent(
VoiceKey VoiceKey,
SubtitleKey SubtitleKey,
float ClipLengthSeconds) : IEvent;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7b2afa1c54b63413293f433a9490a998

View File

@@ -0,0 +1,6 @@
using BriarQueen.Framework.Events.System;
namespace BriarQueen.Framework.Events.UI
{
public record SubtitleDisplayChangedEvent(string Text, bool Visible) : IEvent;
}

View File

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

View File

@@ -45,7 +45,8 @@ namespace BriarQueen.Framework.Managers.Audio
private AudioSource _musicSourceB;
private AudioSource _voiceSource;
private string _activeVoiceSubtitleId;
private VoiceKey _activeVoiceKey = VoiceKey.None;
private SubtitleKey _activeSubtitleKey = SubtitleKey.None;
private AudioFileSo _currentMusicTrack;
private CancellationTokenSource _musicDuckCts;
@@ -140,7 +141,8 @@ namespace BriarQueen.Framework.Managers.Audio
_musicSourceB = null;
_voiceSource = null;
_currentMusicTrack = null;
_activeVoiceSubtitleId = null;
_activeVoiceKey = VoiceKey.None;
_activeSubtitleKey = SubtitleKey.None;
_voiceFinishedPublished = false;
Initialized = false;
}
@@ -205,8 +207,7 @@ namespace BriarQueen.Framework.Managers.Audio
if (_audioMixer == null || string.IsNullOrWhiteSpace(parameter))
return;
if (!_baseDb.TryGetValue(parameter, out var baseDb))
baseDb = 0f;
var baseDb = _baseDb.GetValueOrDefault(parameter, 0f);
var effective = baseDb;
@@ -340,10 +341,14 @@ namespace BriarQueen.Framework.Managers.Audio
_voiceCts = new CancellationTokenSource();
var token = _voiceCts.Token;
_activeVoiceSubtitleId = SubtitleIdentifiers.Get(audioData.MatchingSubtitleID);
_activeVoiceKey = audioData.VoiceKey;
_activeSubtitleKey = audioData.MatchingSubtitleID;
_voiceFinishedPublished = false;
_eventCoordinator.Publish(new VoiceLineStartedEvent(_activeVoiceSubtitleId));
_eventCoordinator.Publish(new VoicePlaybackStartedEvent(
_activeVoiceKey,
_activeSubtitleKey,
audioData.Clip.length));
_voiceSource.clip = audioData.Clip;
_voiceSource.pitch = audioData.Pitch;
@@ -369,11 +374,16 @@ namespace BriarQueen.Framework.Managers.Audio
{
if (_voiceFinishedPublished) return;
if (!string.IsNullOrEmpty(_activeVoiceSubtitleId))
_eventCoordinator.Publish(new VoiceLineFinishedEvent(_activeVoiceSubtitleId));
if (_activeVoiceKey != VoiceKey.None || _activeSubtitleKey != SubtitleKey.None)
{
_eventCoordinator.Publish(new VoicePlaybackFinishedEvent(
_activeVoiceKey,
_activeSubtitleKey));
}
_voiceFinishedPublished = true;
_activeVoiceSubtitleId = null;
_activeVoiceKey = VoiceKey.None;
_activeSubtitleKey = SubtitleKey.None;
}
public void StopVoice()
@@ -666,4 +676,4 @@ namespace BriarQueen.Framework.Managers.Audio
public float StartedAtUnscaled;
}
}
}
}

View File

@@ -449,13 +449,12 @@ namespace BriarQueen.Framework.Managers.Input
private void OnPause(InputAction.CallbackContext ctx)
{
var isMainMenu = _gameService != null && _gameService.IsMainMenuSceneLoaded;
if (isMainMenu || _isAnyUIOpen)
if (isMainMenu)
{
_eventCoordinator?.PublishImmediate(new UIBackRequestedEvent());
return;
}
_isPaused = true;
_eventCoordinator?.Publish(new PauseButtonClickedEvent());
}

View File

@@ -163,6 +163,22 @@ namespace BriarQueen.Framework.Managers.Interaction
Debug.Log($"[InteractManager] SetExclusiveRaycaster set to {raycaster.gameObject.name}.");
}
public void ReleaseExclusiveRaycaster(GraphicRaycaster raycaster)
{
if (raycaster == null)
return;
if (_exclusiveRaycaster != raycaster)
return;
_exclusiveRaycaster = null;
if (_currentHovered != null)
ClearHover().Forget();
Debug.Log($"[InteractManager] Released exclusive raycaster {raycaster.gameObject.name}.");
}
/// <summary>
/// Clear exclusive mode and return to using all registered raycasters.
/// </summary>
@@ -453,4 +469,4 @@ namespace BriarQueen.Framework.Managers.Interaction
_selectedItem = evt.Item;
}
}
}
}

View File

@@ -15,10 +15,15 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
[Header("Codex")]
[SerializeField]
private CodexEntrySo _codexEntry;
[SerializeField]
private bool _removeTrigger;
[Header("Events")]
[SerializeField]
private SFXKey _soundEffect;
[SerializeField]
private VoiceKey _voiceLine;
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Inspect;
public override string InteractableName =>
@@ -40,12 +45,23 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
return;
}
PlayerManager.UnlockCodexEntry(_codexEntry);
if (_removeTrigger)
{
await Remove();
}
if (_soundEffect != SFXKey.None)
{
AudioManager.Play(AudioNameIdentifiers.Get(_soundEffect));
}
if (_voiceLine != VoiceKey.None)
{
AudioManager.Play(AudioNameIdentifiers.Get(_voiceLine));
}
}
protected override void UpdateSaveGameOnRemoval()

View File

@@ -4,6 +4,7 @@ namespace BriarQueen.Framework.Managers.UI.Base
{
public interface IUIOverlayHost
{
bool CanSuspendFor(WindowType incomingWindowType);
UniTask SuspendForOverlay();
UniTask ResumeFromOverlay();
}

View File

@@ -1,16 +1,19 @@
using System.Threading;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
namespace BriarQueen.Framework.Managers.UI.Base
{
public enum UIPauseBehavior
{
TreatAsBackRequest,
OpenPauseOverlay
}
public interface IUIWindow
{
UniTask Show();
UniTask Hide();
WindowType WindowType { get; }
UIPauseBehavior PauseBehavior { get; }
}
}
}

View File

@@ -4,6 +4,7 @@ namespace BriarQueen.Framework.Managers.UI.Base
{
PauseMenuWindow,
SettingsWindow,
CodexWindow
CodexWindow,
AshwickGateKeypadWindow
}
}
}

View File

@@ -41,12 +41,14 @@ namespace BriarQueen.Framework.Managers.UI
public bool Initialized { get; private set; }
private sealed record OverlayResumeContext(WindowType OverlayWindowType, IUIOverlayHost Host);
private IHud _hudContainer;
private IPopup _infoPopup;
private IPopup _tutorialPopup;
private IScreenFader _screenFader;
private IUIOverlayHost _mainMenuOverlayHost;
private IUIOverlayHost _activeSettingsOverlayHost;
private readonly Stack<OverlayResumeContext> _overlayResumeStack = new();
[Inject]
public UIManager(
@@ -127,6 +129,18 @@ namespace BriarQueen.Framework.Managers.UI
window.Hide().Forget();
}
public void UnregisterWindow(IUIWindow window)
{
if (window == null)
return;
if (_windows.TryGetValue(window.WindowType, out var registered) && ReferenceEquals(registered, window))
_windows.Remove(window.WindowType);
if (window is IUIOverlayHost overlayHost)
RemoveOverlayResumeContextsForHost(overlayHost);
}
public void RegisterHUD(IHud hudContainer)
{
_hudContainer = hudContainer;
@@ -169,9 +183,7 @@ namespace BriarQueen.Framework.Managers.UI
if (!ReferenceEquals(_mainMenuOverlayHost, host))
return;
if (ReferenceEquals(_activeSettingsOverlayHost, host))
_activeSettingsOverlayHost = null;
RemoveOverlayResumeContextsForHost(host);
_mainMenuOverlayHost = null;
}
@@ -186,6 +198,51 @@ namespace BriarQueen.Framework.Managers.UI
return target != null && _windowStack.Contains(target);
}
private void RemoveOverlayResumeContextsForHost(IUIOverlayHost host)
{
if (host == null || _overlayResumeStack.Count == 0)
return;
var contextsToKeep = new List<OverlayResumeContext>();
foreach (var context in _overlayResumeStack)
{
if (!ReferenceEquals(context.Host, host))
contextsToKeep.Add(context);
}
_overlayResumeStack.Clear();
for (var i = contextsToKeep.Count - 1; i >= 0; i--)
_overlayResumeStack.Push(contextsToKeep[i]);
}
private async UniTask<bool> TrySuspendActiveWindowFor(WindowType incomingWindowType)
{
if (ActiveWindow is IUIOverlayHost overlayHost &&
overlayHost.CanSuspendFor(incomingWindowType))
{
await overlayHost.SuspendForOverlay();
_overlayResumeStack.Push(new OverlayResumeContext(incomingWindowType, overlayHost));
return true;
}
return false;
}
private async UniTask RestoreUnderlyingUi(WindowType closedWindowType)
{
if (_overlayResumeStack.Count > 0 &&
_overlayResumeStack.Peek().OverlayWindowType == closedWindowType)
{
var resumeContext = _overlayResumeStack.Pop();
await resumeContext.Host.ResumeFromOverlay();
return;
}
if (ActiveWindow != null)
await ActiveWindow.Show();
}
private async UniTask ApplyHudVisibility(bool visible)
{
if (_disposed || _hudContainer == null)
@@ -206,13 +263,25 @@ namespace BriarQueen.Framework.Managers.UI
private void OnPauseClickReceived(PauseButtonClickedEvent _)
{
if (_windowStack.Count > 0)
if (ActiveWindow == null)
{
OpenWindow(WindowType.PauseMenuWindow);
return;
}
if (ActiveWindow.WindowType == WindowType.PauseMenuWindow)
{
TryHandleBackRequest();
return;
}
OpenWindow(WindowType.PauseMenuWindow);
if (ActiveWindow.PauseBehavior == UIPauseBehavior.OpenPauseOverlay)
{
OpenWindow(WindowType.PauseMenuWindow);
return;
}
TryHandleBackRequest();
}
private void OnBackRequested(UIBackRequestedEvent _)
@@ -350,28 +419,22 @@ namespace BriarQueen.Framework.Managers.UI
if (_windowStack.Contains(window))
return;
_activeSettingsOverlayHost = null;
var suspended = false;
var openingSettingsOverPause =
source == SettingsOpenSource.PauseMenu &&
ActiveWindow?.WindowType == WindowType.PauseMenuWindow &&
ActiveWindow is IUIOverlayHost;
var openingSettingsOverMainMenu =
source == SettingsOpenSource.MainMenu &&
_mainMenuOverlayHost != null;
if (openingSettingsOverPause)
if (source == SettingsOpenSource.MainMenu &&
_mainMenuOverlayHost != null &&
_mainMenuOverlayHost.CanSuspendFor(WindowType.SettingsWindow))
{
_activeSettingsOverlayHost = (IUIOverlayHost)ActiveWindow;
await _activeSettingsOverlayHost.SuspendForOverlay();
await _mainMenuOverlayHost.SuspendForOverlay();
_overlayResumeStack.Push(new OverlayResumeContext(WindowType.SettingsWindow, _mainMenuOverlayHost));
suspended = true;
}
else if (openingSettingsOverMainMenu)
else
{
_activeSettingsOverlayHost = _mainMenuOverlayHost;
await _activeSettingsOverlayHost.SuspendForOverlay();
suspended = await TrySuspendActiveWindowFor(WindowType.SettingsWindow);
}
else if (ActiveWindow != null)
if (!suspended && ActiveWindow != null)
{
await ActiveWindow.Hide();
}
@@ -407,7 +470,9 @@ namespace BriarQueen.Framework.Managers.UI
if (_windowStack.Contains(window))
return;
if (ActiveWindow != null)
var suspended = await TrySuspendActiveWindowFor(windowType);
if (!suspended && ActiveWindow != null)
await ActiveWindow.Hide();
_windowStack.Push(window);
@@ -452,15 +517,7 @@ namespace BriarQueen.Framework.Managers.UI
break;
}
if (target.WindowType == WindowType.SettingsWindow && _activeSettingsOverlayHost != null)
{
await _activeSettingsOverlayHost.ResumeFromOverlay();
_activeSettingsOverlayHost = null;
}
else if (ActiveWindow != null)
{
await ActiveWindow.Show();
}
await RestoreUnderlyingUi(target.WindowType);
NotifyUIStackChanged();
}
@@ -506,17 +563,8 @@ namespace BriarQueen.Framework.Managers.UI
NotifyWindowStateChanged(top.WindowType, false);
}
if (top != null &&
top.WindowType == WindowType.SettingsWindow &&
_activeSettingsOverlayHost != null)
{
await _activeSettingsOverlayHost.ResumeFromOverlay();
_activeSettingsOverlayHost = null;
}
else if (ActiveWindow != null)
{
await ActiveWindow.Show();
}
if (top != null)
await RestoreUnderlyingUi(top.WindowType);
NotifyUIStackChanged();
}
@@ -536,17 +584,12 @@ namespace BriarQueen.Framework.Managers.UI
await _windowTransitionGate.WaitAsync();
try
{
var shouldResumeSettingsHost = false;
while (_windowStack.Count > 0)
{
var window = _windowStack.Pop();
if (window == null)
continue;
if (window.WindowType == WindowType.SettingsWindow && _activeSettingsOverlayHost != null)
shouldResumeSettingsHost = true;
try
{
await window.Hide();
@@ -557,18 +600,7 @@ namespace BriarQueen.Framework.Managers.UI
}
}
if (shouldResumeSettingsHost)
{
try
{
await _activeSettingsOverlayHost.ResumeFromOverlay();
}
catch
{
}
}
_activeSettingsOverlayHost = null;
_overlayResumeStack.Clear();
if (_tutorialPopup != null)
{
@@ -621,7 +653,7 @@ namespace BriarQueen.Framework.Managers.UI
faderComponent.gameObject.SetActive(false);
_windowStack.Clear();
_activeSettingsOverlayHost = null;
_overlayResumeStack.Clear();
_mainMenuOverlayHost = null;
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1973a703eae6841b6b53b6f33b951713
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,148 @@
using System;
using System.Threading;
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.Audio;
using BriarQueen.Framework.Events.UI;
using Cysharp.Threading.Tasks;
using VContainer;
namespace BriarQueen.Framework.Services.Subtitles
{
public class SubtitleService : IDisposable
{
private readonly EventCoordinator _eventCoordinator;
private CancellationTokenSource _subtitleCts;
private SubtitleKey _activeSubtitleKey = SubtitleKey.None;
private string _currentText = string.Empty;
public bool IsVisible => _activeSubtitleKey != SubtitleKey.None && !string.IsNullOrWhiteSpace(_currentText);
public string CurrentText => _currentText;
[Inject]
public SubtitleService(EventCoordinator eventCoordinator)
{
_eventCoordinator = eventCoordinator;
}
public void Initialize()
{
_eventCoordinator.Subscribe<VoicePlaybackStartedEvent>(OnVoicePlaybackStarted);
_eventCoordinator.Subscribe<VoicePlaybackFinishedEvent>(OnVoicePlaybackFinished);
}
public void Dispose()
{
_eventCoordinator.Unsubscribe<VoicePlaybackStartedEvent>(OnVoicePlaybackStarted);
_eventCoordinator.Unsubscribe<VoicePlaybackFinishedEvent>(OnVoicePlaybackFinished);
CancelCurrentSubtitle();
ClearSubtitle();
}
public void PlayScriptedSubtitle(SubtitleKey subtitleKey, float durationOverrideSeconds = 0f)
{
if (subtitleKey == SubtitleKey.None)
{
ClearScriptedSubtitle();
return;
}
if (!SubtitleIdentifiers.TryGet(subtitleKey, out var entry) || string.IsNullOrWhiteSpace(entry.Text))
return;
var duration = durationOverrideSeconds > 0f
? durationOverrideSeconds
: entry.PreferredDurationSeconds;
ShowSubtitle(subtitleKey, entry.Text, duration).Forget();
}
public void ClearScriptedSubtitle()
{
CancelCurrentSubtitle();
ClearSubtitle();
}
private void OnVoicePlaybackStarted(VoicePlaybackStartedEvent evt)
{
if (evt.SubtitleKey == SubtitleKey.None)
return;
if (!SubtitleIdentifiers.TryGet(evt.SubtitleKey, out var entry) || string.IsNullOrWhiteSpace(entry.Text))
return;
var duration = entry.PreferredDurationSeconds > 0f
? entry.PreferredDurationSeconds
: evt.ClipLengthSeconds;
ShowSubtitle(evt.SubtitleKey, entry.Text, duration).Forget();
}
private void OnVoicePlaybackFinished(VoicePlaybackFinishedEvent evt)
{
if (evt.SubtitleKey == SubtitleKey.None)
return;
if (_activeSubtitleKey != evt.SubtitleKey)
return;
CancelCurrentSubtitle();
ClearSubtitle();
}
private async UniTaskVoid ShowSubtitle(SubtitleKey subtitleKey, string text, float durationSeconds)
{
CancelCurrentSubtitle();
_activeSubtitleKey = subtitleKey;
_currentText = text;
_subtitleCts = new CancellationTokenSource();
_eventCoordinator.PublishImmediate(new SubtitleDisplayChangedEvent(text, true));
var safeDuration = Math.Max(0f, durationSeconds);
if (safeDuration <= 0f)
return;
try
{
await UniTask.Delay(
TimeSpan.FromSeconds(safeDuration),
DelayType.UnscaledDeltaTime,
cancellationToken: _subtitleCts.Token);
}
catch (OperationCanceledException)
{
return;
}
if (_activeSubtitleKey == subtitleKey)
ClearSubtitle();
}
private void CancelCurrentSubtitle()
{
if (_subtitleCts == null)
return;
try
{
_subtitleCts.Cancel();
}
catch
{
}
_subtitleCts.Dispose();
_subtitleCts = null;
}
private void ClearSubtitle()
{
_activeSubtitleKey = SubtitleKey.None;
_currentText = string.Empty;
_eventCoordinator.PublishImmediate(new SubtitleDisplayChangedEvent(string.Empty, false));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 61b6e034a002e4319a76ebe6bd90f50f