Files
ichni_Official/Packages/dev.yarnspinner.unity/Runtime/Views/VoiceOverPresenter.cs
SoulliesOfficial 021e76efe7 同步
2026-06-09 11:21:59 -04:00

252 lines
9.4 KiB
C#

/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using System;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using UnityEngine;
using Yarn.Unity.Attributes;
#nullable enable
namespace Yarn.Unity
{
/// <summary>
/// A subclass of <see cref="DialoguePresenterBase"/> that plays voice-over
/// <see cref="AudioClip"/>s for lines of dialogue.
/// </summary>
public sealed class VoiceOverPresenter : DialoguePresenterBase
{
/// <summary>
/// If <see langword="true"/>, the voice over view will request that the
/// dialogue runner proceed to the next line when audio for the line has
/// finished playing.
/// </summary>
[Group("Line Management")]
public bool endLineWhenVoiceoverComplete = true;
/// <summary>
/// The fade out time when the line is interrupted during playback.
/// </summary>
[Group("Timing")]
public float fadeOutTimeOnLineFinish = 0.05f;
/// <summary>
/// The amount of time to wait before starting playback of the line.
/// </summary>
[Group("Timing")]
public float waitTimeBeforeLineStart = 0f;
/// <summary>
/// The amount of time after playback has completed before this view
/// reports that it's finished delivering the line.
/// </summary>
[Group("Timing")]
public float waitTimeAfterLineComplete = 0f;
/// <summary>
/// The <see cref="AudioSource"/> that this voice over view will play
/// its audio from.
/// </summary>
/// <remarks>If this is <see langword="null"/>, a new <see
/// cref="AudioSource"/> will be added at runtime.</remarks>
[SerializeField]
[NotNull]
// for some reason Unity doesn't seem to respect the [NotNull] attribute
// presumably this will be fixed in a future version of Unity
#pragma warning disable CS8618
public AudioSource audioSource;
#pragma warning restore CS8618
void Awake()
{
if (audioSource == null)
{
// If we don't have an audio source, add one.
audioSource = gameObject.AddComponent<AudioSource>();
// Additionally, we'll assume that the user didn't place the
// game object that this component is attached to deliberately,
// so we'll set the spatial blend to 0 (which means the audio
// will not be positioned in 3D space.)
audioSource.spatialBlend = 0f;
}
}
void Reset()
{
if (audioSource == null)
{
audioSource = GetComponentInChildren<AudioSource>();
}
}
/// <summary>
/// Begins playing the associated audio for the specified line.
/// </summary>
/// <remarks>
/// <para style="warning">This method is not intended to be called from
/// your code. Instead, the <see cref="DialogueRunner"/> class will call
/// it at the appropriate time.</para>
/// </remarks>
/// <inheritdoc cref="DialoguePresenterBase.RunLineAsync(LocalizedLine,
/// LineCancellationToken)" path="/param"/>
/// <seealso cref="DialoguePresenterBase.RunLineAsync(LocalizedLine,
/// LineCancellationToken)"/>
public override async YarnTask RunLineAsync(LocalizedLine dialogueLine, LineCancellationToken lineCancellationToken)
{
// Get the localized voice over audio clip
AudioClip? voiceOverClip = null;
if (dialogueLine.Asset is AudioClip clip)
{
voiceOverClip = clip;
}
else if (dialogueLine.Asset is IAssetProvider provider && provider.TryGetAsset(out AudioClip? result))
{
voiceOverClip = result;
}
DialogueRunner? dialogueRunner = dialogueLine.Source as DialogueRunner;
if (voiceOverClip == null)
{
Debug.LogError($"Playing voice over failed because the localised line {dialogueLine.TextID} " +
$"either didn't have an asset, or its asset was not an {nameof(AudioClip)}.", gameObject);
if (this.endLineWhenVoiceoverComplete && dialogueRunner != null)
{
// If we didn't get a line, but we were configured to
// advance the line on end, then we should act as though
// we've reached the end of the line now and advance.
dialogueRunner.RequestNextLine();
}
return;
}
if (audioSource.isPlaying)
{
// Usually, this shouldn't happen because the DialogueRunner
// finishes and ends a line first
audioSource.Stop();
}
// If we need to wait before starting playback, do this now
if (waitTimeBeforeLineStart > 0)
{
await YarnTask.Delay(
TimeSpan.FromSeconds(waitTimeBeforeLineStart),
lineCancellationToken.NextContentToken).SuppressCancellationThrow();
}
if (!DialogueRunner.IsInPlaymode)
{
// We left play mode while waiting before starting playback
return;
}
// Start playing the audio.
audioSource.PlayOneShot(voiceOverClip);
// Playback may not begin immediately, so wait until it does (or if
// the line is interrupted.)
await YarnTask.WaitUntil(() => audioSource.isPlaying, lineCancellationToken.NextContentToken).SuppressCancellationThrow();
if (!DialogueRunner.IsInPlaymode)
{
// We left play mode before the audio started playing
return;
}
// Now wait until either the audio source finishes playing, or the
// line is interrupted.
await YarnTask.WaitUntil(() => !audioSource.isPlaying, lineCancellationToken.NextContentToken).SuppressCancellationThrow();
if (!DialogueRunner.IsInPlaymode)
{
// We left play mode while the audio was playing
return;
}
// If the line was interrupted while we were playing, we need to
// wrap up the playback as quickly as we can. We do this here with a
// fade-out to zero over fadeOutTimeOnLineFinish seconds.
if (audioSource.isPlaying && lineCancellationToken.IsNextContentRequested)
{
// Fade out voice over clip
float lerpPosition = 0f;
float volumeFadeStart = audioSource.volume;
while (audioSource.volume != 0)
{
// We'll use unscaled time here, because if time is scaled,
// we might be fading out way too slowly, and that would
// sound extremely strange.
lerpPosition += Time.unscaledDeltaTime / fadeOutTimeOnLineFinish;
audioSource.volume = Mathf.Lerp(volumeFadeStart, 0, lerpPosition);
await YarnTask.Yield();
}
// We're done fading out. Restore our audio volume to its
// original point for the next line.
audioSource.volume = volumeFadeStart;
}
audioSource.Stop();
// We've finished our playback at this point, either by waiting
// normally or by interrupting it with a fadeout. If we weren't
// interrupted, and we have additional time to wait after the audio
// finishes, wait now. (If we were interrupted, we skip this wait,
// because the user has already indicated that they're fine with
// things moving faster than sounds normal.)
if (!lineCancellationToken.IsNextContentRequested && waitTimeAfterLineComplete > 0)
{
await YarnTask.Delay(
TimeSpan.FromSeconds(waitTimeAfterLineComplete),
lineCancellationToken.NextContentToken
).SuppressCancellationThrow();
}
if (!DialogueRunner.IsInPlaymode)
{
// We left play mode while waiting after the line completed
return;
}
if (endLineWhenVoiceoverComplete)
{
if (dialogueRunner == null)
{
Debug.LogError($"Can't end line due to voice over being complete: {nameof(dialogueRunner)} is null", this);
}
else
{
dialogueRunner.RequestNextLine();
}
}
}
/// <inheritdoc />
/// <remarks>
/// Stops any audio if there is still any playing.
/// </remarks>
public override YarnTask OnDialogueCompleteAsync()
{
// just in case we are still playing audio we want it to stop
audioSource.Stop();
return YarnTask.CompletedTask;
}
/// <inheritdoc/>
public override YarnTask OnDialogueStartedAsync()
{
return YarnTask.CompletedTask;
}
}
}