/* 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 { /// /// A subclass of that plays voice-over /// s for lines of dialogue. /// public sealed class VoiceOverPresenter : DialoguePresenterBase { /// /// If , the voice over view will request that the /// dialogue runner proceed to the next line when audio for the line has /// finished playing. /// [Group("Line Management")] public bool endLineWhenVoiceoverComplete = true; /// /// The fade out time when the line is interrupted during playback. /// [Group("Timing")] public float fadeOutTimeOnLineFinish = 0.05f; /// /// The amount of time to wait before starting playback of the line. /// [Group("Timing")] public float waitTimeBeforeLineStart = 0f; /// /// The amount of time after playback has completed before this view /// reports that it's finished delivering the line. /// [Group("Timing")] public float waitTimeAfterLineComplete = 0f; /// /// The that this voice over view will play /// its audio from. /// /// If this is , a new will be added at runtime. [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(); // 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(); } } /// /// Begins playing the associated audio for the specified line. /// /// /// This method is not intended to be called from /// your code. Instead, the class will call /// it at the appropriate time. /// /// /// 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(); } } } /// /// /// Stops any audio if there is still any playing. /// public override YarnTask OnDialogueCompleteAsync() { // just in case we are still playing audio we want it to stop audioSource.Stop(); return YarnTask.CompletedTask; } /// public override YarnTask OnDialogueStartedAsync() { return YarnTask.CompletedTask; } } }