// Copyright (c) Pixel Crushers. All rights reserved. using UnityEngine; using System.Collections; using System.Collections.Generic; namespace PixelCrushers.DialogueSystem { /// /// Specifies the orderings that can be used for a list of barks. /// public enum BarkOrder { /// /// Play barks in random order, avoiding sequential repeats if possible. /// Random, /// /// Play barks in sequential order. /// Sequential, /// /// Stop evaluating dialogue entries after finding the first valid entry. /// FirstValid } /// /// Keeps track of a character's current bark. This allows the BarkController to iterate /// through a list of barks. /// public class BarkHistory { public BarkOrder order; public int index = 0; public List entries = null; public BarkHistory(BarkOrder order) { this.order = order; this.index = 0; this.entries = null; } public int GetNextIndex(int numEntries) { if (order == BarkOrder.Random) { if (numEntries == 0) return 0; var isNewList = entries == null; if (entries == null) entries = new List(); // If the entries have changed or we've reached the end of the shuffled list, remake the list: if (entries.Count != numEntries || index >= entries.Count) { // Remember the last entry we used: var lastEntry = (entries.Count > 0) ? entries[entries.Count - 1] : 0; // Reshuffle the list: entries.Clear(); for (int i = 0; i < numEntries; i++) { entries.Add(i); } entries.Shuffle(); if (entries[0] == lastEntry && !isNewList) { // If the first entry of new list is the same as the last entry used, move it to the end: entries.RemoveAt(0); entries.Add(lastEntry); } index = 0; } return (0 <= index && index < entries.Count) ? entries[index++] : 0; //---Was: return entries[Random.Range(0, numEntries); } else { int result = (index % numEntries); index = ((index + 1) % numEntries); return result; } } /// /// Resets the current index to the beginning. /// public void Reset() { index = 0; } } /// /// Specifies how to handle bark subtitles: /// /// - SameAsDialogueManager: Use the same setting as the dialogue UI currently assigned to the /// DialogueManager. /// - Show: Always show using the bark UI on the character. (See IBarkUI) /// - Hide: Never show. /// public enum BarkSubtitleSetting { SameAsDialogueManager, Show, Hide } /// /// BarkController is a static utility class provides a method to make characters bark. /// public static class BarkController { private static Dictionary currentBarkPriority = new Dictionary(); /// /// Gets the last sequencer created by BarkController.Bark(). /// /// The last sequencer. public static Sequencer LastSequencer { get; private set; } #if UNITY_2019_3_OR_NEWER && UNITY_EDITOR [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void InitStaticVariables() { currentBarkPriority = new Dictionary(); } #endif static BarkController() { LastSequencer = null; } private static int GetSpeakerCurrentBarkPriority(Transform speaker) { return currentBarkPriority.ContainsKey(speaker) ? currentBarkPriority[speaker] : 0; } private static void SetSpeakerCurrentBarkPriority(Transform speaker, int priority) { if (currentBarkPriority.ContainsKey(speaker)) { currentBarkPriority[speaker] = priority; } else { currentBarkPriority.Add(speaker, priority); } } private static int GetEntryBarkPriority(DialogueEntry entry) { return (entry == null) ? 0 : Field.LookupInt(entry.fields, DialogueSystemFields.Priority); } /// /// Attempts to make a character bark. This is a coroutine; you must start it using /// StartCoroutine() or Unity will hang. Shows a line from the named conversation, plays /// the sequence, and sends OnBarkStart/OnBarkEnd messages to the participants. /// /// /// Title of conversation to pull bark lines from. /// /// /// Speaker performing the bark. /// /// /// Listener that the bark is directed to; may be null. /// /// /// Bark history used to keep track of the most recent bark so this method can iterate /// through them in a specified order. /// /// /// The dialogue database to use. If null, uses DialogueManager.MasterDatabase. /// public static IEnumerator Bark(string conversationTitle, Transform speaker, Transform listener, BarkHistory barkHistory, DialogueDatabase database = null, bool stopAtFirstValid = false) { if (CheckDontBarkDuringConversation()) yield break; bool barked = false; if (string.IsNullOrEmpty(conversationTitle) && DialogueDebug.logWarnings) Debug.Log(string.Format("{0}: Bark (speaker={1}, listener={2}): conversation title is blank", new System.Object[] { DialogueDebug.Prefix, speaker, listener }), speaker); if (speaker == null) speaker = DialogueManager.instance.FindActorTransformFromConversation(conversationTitle, "Actor"); if ((speaker == null) && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' speaker is null", new System.Object[] { DialogueDebug.Prefix, speaker, listener, conversationTitle })); if (string.IsNullOrEmpty(conversationTitle) || (speaker == null)) yield break; IBarkUI barkUI = DialogueActor.GetBarkUI(speaker); //speaker.GetComponentInChildren(typeof(IBarkUI)) as IBarkUI; if ((barkUI == null) && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' speaker has no bark UI", new System.Object[] { DialogueDebug.Prefix, speaker, listener, conversationTitle }), speaker); var firstValid = stopAtFirstValid || ((barkHistory == null) ? false : barkHistory.order == (BarkOrder.FirstValid)); ConversationModel conversationModel = new ConversationModel(database ?? DialogueManager.masterDatabase, conversationTitle, speaker, listener, DialogueManager.allowLuaExceptions, DialogueManager.isDialogueEntryValid, -1, firstValid, false, DialogueManager.useLinearGroupMode); ConversationState firstState = conversationModel.firstState; if ((firstState == null) && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' has no START entry", new System.Object[] { DialogueDebug.Prefix, speaker, listener, conversationTitle }), speaker); else if (DialogueManager.useLinearGroupMode) conversationModel.UpdateResponses(firstState); // In linear mode, conversation model doesn't check responses on creation since they're evaluated when the subtitle/bark ends. if ((firstState != null) && !firstState.hasAnyResponses && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' has no valid bark at this time", new System.Object[] { DialogueDebug.Prefix, speaker, listener, conversationTitle }), speaker); if ((firstState != null) && firstState.hasAnyResponses) { try { Response[] responses = firstState.hasNPCResponse ? firstState.npcResponses : firstState.pcResponses; int index = (barkHistory ?? new BarkHistory(BarkOrder.Random)).GetNextIndex(responses.Length); DialogueEntry barkEntry = responses[index].destinationEntry; if ((barkEntry == null) && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' bark entry is null", new System.Object[] { DialogueDebug.Prefix, speaker, listener, conversationTitle }), speaker); if (barkEntry != null) { var priority = GetEntryBarkPriority(barkEntry); if (priority < GetSpeakerCurrentBarkPriority(speaker)) { if (DialogueDebug.logInfo) Debug.Log(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' currently barking a higher priority bark", new System.Object[] { DialogueDebug.Prefix, speaker, listener, conversationTitle }), speaker); yield break; } SetSpeakerCurrentBarkPriority(speaker, priority); barked = true; InformParticipants(DialogueSystemMessages.OnBarkStart, speaker, listener); ConversationState barkState = conversationModel.GetState(barkEntry, false); if (barkState == null) { if (DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' can't find a valid dialogue entry", new System.Object[] { DialogueDebug.Prefix, speaker, listener, conversationTitle }), speaker); yield break; } //--- Was: (swapping speaker & listener no longer appropriate) //if (firstState.hasNPCResponse) //{ // CharacterInfo tempInfo = barkState.subtitle.speakerInfo; // barkState.subtitle.speakerInfo = barkState.subtitle.listenerInfo; // barkState.subtitle.listenerInfo = tempInfo; //} if (DialogueDebug.logInfo) Debug.Log(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}'", new System.Object[] { DialogueDebug.Prefix, speaker, listener, barkState.subtitle.formattedText.text }), speaker); InformParticipantsLine(DialogueSystemMessages.OnBarkLine, speaker, barkState.subtitle); // Show the bark subtitle: if (((barkUI == null) || !(barkUI as MonoBehaviour).enabled) && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' bark UI is null or disabled", new System.Object[] { DialogueDebug.Prefix, speaker, listener, barkState.subtitle.formattedText.text }), speaker); if ((barkUI != null) && (barkUI as MonoBehaviour).enabled) { CheckCancelPreviousBarkSequence(speaker, barkUI); barkUI.Bark(barkState.subtitle); } // Start the sequence: var sequencer = PlayBarkSequence(barkState.subtitle, speaker, listener); LastSequencer = sequencer; // Wait until the sequence and subtitle are done: while (((sequencer != null) && sequencer.isPlaying) || ((barkUI != null) && barkUI.isPlaying)) { yield return null; } if (sequencer != null) GameObject.Destroy(sequencer); } } finally { if (barked) { InformParticipants(DialogueSystemMessages.OnBarkEnd, speaker, listener); SetSpeakerCurrentBarkPriority(speaker, 0); } } } } private static void CheckCancelPreviousBarkSequence(Transform speaker, IBarkUI barkUI) { if (barkUI.isPlaying && (barkUI is StandardBarkUI)) { var standardBarkUI = barkUI as StandardBarkUI; if (standardBarkUI.waitUntilSequenceEnds && standardBarkUI.cancelWaitUntilSequenceEndsIfReplacingBark) { standardBarkUI.Hide(); } } } private static Sequencer PlayBarkSequence(Subtitle subtitle, Transform speaker, Transform listener) { return PlayBarkSequence(subtitle.formattedText.text, subtitle.sequence, subtitle.entrytag, speaker, listener); } private static Sequencer PlayBarkSequence(string barkText, string sequence, string entrytag, Transform speaker, Transform listener) { if (string.IsNullOrEmpty(sequence)) { sequence = DialogueManager.displaySettings.barkSettings.defaultBarkSequence; } if (!string.IsNullOrEmpty(sequence)) { sequence = Sequencer.ReplaceShortcuts(sequence); if (sequence.Contains(SequencerKeywords.End)) { var text = barkText; int numCharacters = string.IsNullOrEmpty(text) ? 0 : Tools.StripRPGMakerCodes(Tools.StripTextMeshProTags(text)).Length; var endDuration = Mathf.Max(DialogueManager.displaySettings.GetMinSubtitleSeconds(), numCharacters / Mathf.Max(1, DialogueManager.displaySettings.GetSubtitleCharsPerSecond())); sequence = sequence.Replace(SequencerKeywords.End, endDuration.ToString(System.Globalization.CultureInfo.InvariantCulture)); } return DialogueManager.PlaySequence(sequence, speaker, listener, false, false, entrytag); } else { return null; } } /// /// Attempts to make a character bark. This is a coroutine; you must start it using /// StartCoroutine() or Unity will hang. Shows a specific subtitle and plays the sequence, /// but does not send OnBarkStart/OnBarkEnd messages to the participants. This optimized version /// /// /// Subtitle to bark. /// /// /// Speaker performing the bark. /// /// /// Listener that the bark is directed to; may be null. /// /// /// The bark UI to bark with. /// public static IEnumerator Bark(Subtitle subtitle, Transform speaker, Transform listener, IBarkUI barkUI) { if (CheckDontBarkDuringConversation()) yield break; if ((subtitle == null) || (subtitle.speakerInfo == null)) yield break; var priority = GetEntryBarkPriority(subtitle.dialogueEntry); if (priority < GetSpeakerCurrentBarkPriority(speaker)) { if (DialogueDebug.logInfo) Debug.Log(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' currently barking a higher priority bark", new System.Object[] { DialogueDebug.Prefix, speaker, listener, subtitle.formattedText.text }), speaker); yield break; } SetSpeakerCurrentBarkPriority(speaker, priority); InformParticipants(DialogueSystemMessages.OnBarkStart, speaker, listener); InformParticipantsLine(DialogueSystemMessages.OnBarkLine, speaker, subtitle); if ((barkUI == null) && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' speaker has no bark UI", new System.Object[] { DialogueDebug.Prefix, speaker, listener, subtitle.formattedText.text }), speaker); if (((barkUI == null) || !(barkUI as MonoBehaviour).enabled) && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' bark UI is null or disabled", new System.Object[] { DialogueDebug.Prefix, speaker, listener, subtitle.formattedText.text }), speaker); CheckCancelPreviousBarkSequence(speaker, barkUI); // Show the bark subtitle: if ((barkUI != null) && (barkUI as MonoBehaviour).enabled) { barkUI.Bark(subtitle); } // Start the sequence: Sequencer sequencer = PlayBarkSequence(subtitle, speaker, listener); LastSequencer = sequencer; // Wait until the sequence and subtitle are done: while (((sequencer != null) && sequencer.isPlaying) || ((barkUI != null) && barkUI.isPlaying)) { yield return null; } if (sequencer != null) GameObject.Destroy(sequencer); InformParticipants(DialogueSystemMessages.OnBarkEnd, speaker, listener); SetSpeakerCurrentBarkPriority(speaker, 0); } /// /// Attempts to make a character bark. This is a coroutine; you must start it using /// StartCoroutine() or Unity will hang. Shows a specific subtitle and plays the sequence, /// but does not send OnBarkStart/OnBarkEnd messages to the participants. /// /// /// Subtitle to bark. /// /// /// If `true`, don't play the sequence associated with the subtitle. /// public static IEnumerator Bark(Subtitle subtitle, bool skipSequence = false) { if (CheckDontBarkDuringConversation()) yield break; if ((subtitle == null) || (subtitle.speakerInfo == null)) yield break; Transform speaker = subtitle.speakerInfo.transform; Transform listener = (subtitle.listenerInfo != null) ? subtitle.listenerInfo.transform : null; var priority = GetEntryBarkPriority(subtitle.dialogueEntry); if (priority < GetSpeakerCurrentBarkPriority(speaker)) { if (DialogueDebug.logInfo) Debug.Log(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' currently barking a higher priority bark", new System.Object[] { DialogueDebug.Prefix, speaker, listener, subtitle.formattedText.text }), speaker); yield break; } SetSpeakerCurrentBarkPriority(speaker, priority); InformParticipants(DialogueSystemMessages.OnBarkStart, speaker, listener); InformParticipantsLine(DialogueSystemMessages.OnBarkLine, speaker, subtitle); IBarkUI barkUI = DialogueActor.GetBarkUI(speaker); // speaker.GetComponentInChildren(typeof(IBarkUI)) as IBarkUI; if ((barkUI == null) && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' speaker has no bark UI", new System.Object[] { DialogueDebug.Prefix, speaker, listener, subtitle.formattedText.text }), speaker); if (((barkUI == null) || !(barkUI as MonoBehaviour).enabled) && DialogueDebug.logWarnings) Debug.LogWarning(string.Format("{0}: Bark (speaker={1}, listener={2}): '{3}' bark UI is null or disabled", new System.Object[] { DialogueDebug.Prefix, speaker, listener, subtitle.formattedText.text }), speaker); CheckCancelPreviousBarkSequence(speaker, barkUI); // Show the bark subtitle: if ((barkUI != null) && (barkUI as MonoBehaviour).enabled) { barkUI.Bark(subtitle); } // Start the sequence: Sequencer sequencer = null; if (!skipSequence) { sequencer = PlayBarkSequence(subtitle, speaker, listener); } LastSequencer = sequencer; // Wait until the sequence and subtitle are done: while (((sequencer != null) && sequencer.isPlaying) || ((barkUI != null) && barkUI.isPlaying)) { yield return null; } if (sequencer != null) GameObject.Destroy(sequencer); InformParticipants(DialogueSystemMessages.OnBarkEnd, speaker, listener); SetSpeakerCurrentBarkPriority(speaker, 0); } private static bool CheckDontBarkDuringConversation() { return DialogueManager.isConversationActive && DialogueManager.displaySettings != null && DialogueManager.displaySettings.barkSettings != null && !DialogueManager.displaySettings.barkSettings.allowBarksDuringConversations; } /// /// Broadcasts a message to the participants in a bark. Used to send the OnBarkStart and /// OnBarkEnd messages to the speaker and listener. Also sends to the Dialogue Manager. /// /// /// Message (i.e., OnBarkStart or OnBarkEnd). /// /// /// Speaker. /// /// /// Listener. /// private static void InformParticipants(string message, Transform speaker, Transform listener) { if (speaker != null) { speaker.BroadcastMessage(message, speaker, SendMessageOptions.DontRequireReceiver); if ((listener != null) && (listener != speaker)) { listener.BroadcastMessage(message, speaker, SendMessageOptions.DontRequireReceiver); } } var dialogueManagerTransform = DialogueManager.instance.transform; if (dialogueManagerTransform != speaker && dialogueManagerTransform != listener) { var actor = (speaker != null) ? speaker : ((listener != null) ? listener : dialogueManagerTransform); DialogueManager.instance.BroadcastMessage(message, actor, SendMessageOptions.DontRequireReceiver); } } /// /// Broadcasts a message to the participants in a bark. Used to send the OnBarkStart and /// OnBarkEnd messages to the speaker and listener. Also sent to Dialogue Manager. /// /// /// Message (i.e., OnBarkStart or OnBarkEnd). /// /// /// Speaker. /// /// /// Listener. /// private static void InformParticipantsLine(string message, Transform speaker, Subtitle subtitle) { if (speaker != null) { speaker.BroadcastMessage(message, subtitle, SendMessageOptions.DontRequireReceiver); } var dialogueManagerTransform = DialogueManager.instance.transform; if (dialogueManagerTransform != speaker) { DialogueManager.instance.BroadcastMessage(message, subtitle, SendMessageOptions.DontRequireReceiver); } } } }