Files
beyond/Assets/Scripts/Utils/BarkPlayer.cs

490 lines
19 KiB
C#

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using PixelCrushers; // For Saver
using PixelCrushers.DialogueSystem; // REQUIRED: Added for DialogueManager events
using Sirenix.OdinInspector;
using UnityEngine;
using Random = UnityEngine.Random;
namespace Beyond
{
[RequireComponent(typeof(Collider))]
public class BarkPlayer : Saver
{
#region Inspector Fields
[Header("Bark Configuration")]
[Tooltip("The indices of the BarkEntries in BarkManager's list to play sequentially.")]
public int[] barkManagerEntryIndices = new int[0];
[Tooltip("Delay in seconds after one bark's audio finishes before starting the next bark WITHIN THE SAME entry sequence.")]
[Min(0f)]
public float delayBetweenBarks = 0.5f;
[Tooltip("Delay in seconds after ALL barks in one entry sequence finish before starting the NEXT entry sequence.")]
[Min(0f)]
public float delayBetweenEntries = 5.0f;
[Tooltip("Play barks within each entry in a random order (shuffled when the entry starts).")]
public bool shuffleOrder = false;
[Header("Dialogue Integration")] // --- NEW SECTION ---
[Tooltip("How long to wait after a dialogue ends before resuming barks.")]
public float resumeDelayAfterDialogue = 2.0f;
[Header("Trigger Settings")]
[Tooltip("Layers that can activate this bark trigger.")]
public LayerMask triggeringLayers;
[Header("Activation")]
[Tooltip("If true, the BarkPlayer will be active automatically when the game starts or when loaded without existing save data for it.")]
public bool startAutomaticallyOnLoad = false;
[Header("Runtime State (Read Only)")]
[SerializeField, ReadOnly]
private bool _runtime_IsStarted = false;
[SerializeField, ReadOnly]
private int currentEntryArrayIndex = 0;
[SerializeField, ReadOnly]
private int nextBarkSequenceIndex = 0;
[SerializeField, ReadOnly]
private bool isTriggeringObjectInside = false;
#endregion
#region Internal Variables
private List<int> playbackOrder;
private bool isInitialized = false;
private Collider triggerCollider;
private BarkManager barkManager;
private Coroutine currentPlaybackCoroutine = null;
private Coroutine resumeCoroutine = null; // To track the resume timer
private Transform currentTriggererTransform = null;
#endregion
#region Save Data Structure
[System.Serializable]
public class BarkPlayerData
{
public bool savedIsStarted;
public int savedCurrentEntryArrayIndex;
public int savedNextBarkSequenceIndex;
public bool wasShuffled;
}
private BarkPlayerData m_saveData = new BarkPlayerData();
#endregion
#region Unity Lifecycle Methods
public override void Awake()
{
_runtime_IsStarted = startAutomaticallyOnLoad;
base.Awake();
triggerCollider = GetComponent<Collider>();
if (!triggerCollider.isTrigger)
{
triggerCollider.isTrigger = true;
}
if (triggeringLayers.value == 0)
{
triggeringLayers = LayerMask.GetMask("Player");
}
}
public override void OnEnable()
{
base.OnEnable();
// Subscribe to global Dialogue System events
// We use the C# event delegates instead of the component message system for reliability
if (DialogueManager.instance != null)
{
DialogueManager.instance.conversationStarted += OnDialogueStarted;
DialogueManager.instance.conversationEnded += OnDialogueEnded;
}
}
public override void OnDisable()
{
base.OnDisable();
// Unsubscribe to prevent memory leaks
if (DialogueManager.instance != null)
{
DialogueManager.instance.conversationStarted -= OnDialogueStarted;
DialogueManager.instance.conversationEnded -= OnDialogueEnded;
}
}
private void Start()
{
barkManager = BarkManager.Instance;
if (barkManager == null)
{
enabled = false;
return;
}
currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1));
if (barkManagerEntryIndices == null || barkManagerEntryIndices.Length == 0)
{
enabled = false;
return;
}
isInitialized = true;
CheckForAutoStartIfInside();
}
private void OnDrawGizmos()
{
Collider col = GetComponent<Collider>();
if (col != null)
{
Gizmos.color = isTriggeringObjectInside ? new Color(1f, 0.5f, 0.5f, 0.4f) : new Color(0.5f, 1f, 0.5f, 0.3f);
if (col is BoxCollider box) Gizmos.DrawCube(transform.TransformPoint(box.center), Vector3.Scale(box.size, transform.lossyScale));
else if (col is SphereCollider sphere) Gizmos.DrawSphere(transform.TransformPoint(sphere.center), sphere.radius * MaxComponent(transform.lossyScale));
}
}
#endregion
#region Dialogue System Event Handlers (New Logic)
// Called automatically by Dialogue System whenever ANY conversation starts
private void OnDialogueStarted(Transform actor)
{
if (resumeCoroutine != null) StopCoroutine(resumeCoroutine);
// Halt the bark loop immediately
if (currentPlaybackCoroutine != null)
{
Debug.Log($"BarkPlayer ({gameObject.name}): Dialogue started. Halting barks.", this);
StopCoroutine(currentPlaybackCoroutine);
currentPlaybackCoroutine = null;
}
}
// Called automatically by Dialogue System whenever ANY conversation ends
private void OnDialogueEnded(Transform actor)
{
// Don't resume if we aren't supposed to be running or nobody is inside
if (_runtime_IsStarted && isTriggeringObjectInside && enabled)
{
Debug.Log($"BarkPlayer ({gameObject.name}): Dialogue ended. Resuming in {resumeDelayAfterDialogue}s.", this);
resumeCoroutine = StartCoroutine(ResumeAfterDialogueDelay());
}
}
private IEnumerator ResumeAfterDialogueDelay()
{
yield return new WaitForSeconds(resumeDelayAfterDialogue);
// Check conditions again in case player left during the wait
CheckForAutoStartIfInside();
resumeCoroutine = null;
}
#endregion
#region Trigger Handling
private void OnTriggerEnter(Collider other)
{
if (!isInitialized || !enabled) return;
int otherLayer = other.gameObject.layer;
if ((triggeringLayers.value & (1 << otherLayer)) == 0) return;
if (isTriggeringObjectInside && other.transform != currentTriggererTransform) return;
if (isTriggeringObjectInside) return;
isTriggeringObjectInside = true;
currentTriggererTransform = other.transform;
// Added check: Don't start barking if a conversation is ALREADY active
if (DialogueManager.isConversationActive)
{
Debug.Log($"BarkPlayer ({gameObject.name}): Trigger entered, but dialogue is active. Waiting for dialogue end.", this);
return;
}
if (_runtime_IsStarted && currentPlaybackCoroutine == null)
{
currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1));
currentPlaybackCoroutine = StartCoroutine(ContinuousPlaybackLoop());
}
}
private void OnTriggerExit(Collider other)
{
if (!isInitialized || !enabled) return;
int otherLayer = other.gameObject.layer;
if ((triggeringLayers.value & (1 << otherLayer)) == 0) return;
if (other.transform == currentTriggererTransform)
{
isTriggeringObjectInside = false;
currentTriggererTransform = null;
if (resumeCoroutine != null) StopCoroutine(resumeCoroutine);
if (currentPlaybackCoroutine != null)
{
StopCoroutine(currentPlaybackCoroutine);
currentPlaybackCoroutine = null;
}
}
}
#endregion
#region Playback Logic
private bool PrepareEntrySequence(int entryIndexToPrepare)
{
if (barkManager == null) return false;
int barkCount = barkManager.GetBarkCountInEntry(entryIndexToPrepare);
if (barkCount <= 0)
{
playbackOrder = new List<int>();
nextBarkSequenceIndex = 0;
return false;
}
playbackOrder = Enumerable.Range(0, barkCount).ToList();
if (shuffleOrder)
{
for (int i = playbackOrder.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
(playbackOrder[i], playbackOrder[j]) = (playbackOrder[j], playbackOrder[i]);
}
}
// If we are resuming mid-sequence (because coroutine was stopped/restarted),
// we try to respect the saved index, otherwise reset to 0.
if (currentEntryArrayIndex == m_saveData.savedCurrentEntryArrayIndex && shuffleOrder == m_saveData.wasShuffled)
{
nextBarkSequenceIndex = Mathf.Clamp(m_saveData.savedNextBarkSequenceIndex, 0, playbackOrder.Count);
}
else
{
nextBarkSequenceIndex = 0;
}
return true;
}
private IEnumerator ContinuousPlaybackLoop()
{
if (barkManagerEntryIndices == null || barkManagerEntryIndices.Length == 0)
{
currentPlaybackCoroutine = null;
yield break;
}
while (isTriggeringObjectInside && currentTriggererTransform != null && _runtime_IsStarted)
{
// Safety check: if dialogue started while we were in a yield, break immediately
if (DialogueManager.isConversationActive) break;
if (currentEntryArrayIndex >= barkManagerEntryIndices.Length)
{
currentEntryArrayIndex = 0;
}
int currentManagerEntryIndex = barkManagerEntryIndices[currentEntryArrayIndex];
bool canPlayEntry = PrepareEntrySequence(currentManagerEntryIndex);
if (!canPlayEntry)
{
currentEntryArrayIndex++;
m_saveData.savedNextBarkSequenceIndex = 0;
yield return null;
continue;
}
while (nextBarkSequenceIndex < playbackOrder.Count && isTriggeringObjectInside && currentTriggererTransform != null && _runtime_IsStarted)
{
// Wait for any other barks (or dialogue) to finish
while ((barkManager.IsPlaying || DialogueManager.isConversationActive) && isTriggeringObjectInside && _runtime_IsStarted)
{
// If dialogue becomes active during this wait, the OnDialogueStarted event
// will kill this coroutine, so this check is just a backup.
yield return null;
}
if (!isTriggeringObjectInside || currentTriggererTransform == null || !_runtime_IsStarted) break;
int barkIndexToPlay = playbackOrder[nextBarkSequenceIndex];
AudioClip playedClip = barkManager.PlayBark(currentManagerEntryIndex, currentTriggererTransform, barkIndexToPlay);
nextBarkSequenceIndex++;
// Update Save Data immediately so if we stop mid-sequence we know where we were
m_saveData.savedNextBarkSequenceIndex = nextBarkSequenceIndex;
float waitTime = delayBetweenBarks;
if (playedClip != null) { waitTime += Mathf.Max(0f, playedClip.length); }
if (waitTime > 0)
{
float timer = 0f;
while (timer < waitTime && isTriggeringObjectInside && currentTriggererTransform != null && _runtime_IsStarted)
{
if (DialogueManager.isConversationActive) break; // Break timer on dialogue
timer += Time.deltaTime;
yield return null;
}
}
else
{
yield return null;
}
if (DialogueManager.isConversationActive) break; // Exit loop if dialogue started
if (!isTriggeringObjectInside || currentTriggererTransform == null || !_runtime_IsStarted) break;
}
// If we broke out due to dialogue, exit the outer loop properly
if (DialogueManager.isConversationActive) break;
if (isTriggeringObjectInside && currentTriggererTransform != null && _runtime_IsStarted)
{
currentEntryArrayIndex++;
m_saveData.savedNextBarkSequenceIndex = 0;
// Reset save data sequence index for next entry
m_saveData.savedCurrentEntryArrayIndex = currentEntryArrayIndex;
if (currentEntryArrayIndex >= barkManagerEntryIndices.Length)
{
currentEntryArrayIndex = 0;
}
if (delayBetweenEntries > 0)
{
float timer = 0f;
while (timer < delayBetweenEntries && isTriggeringObjectInside && currentTriggererTransform != null && _runtime_IsStarted)
{
if (DialogueManager.isConversationActive) break;
timer += Time.deltaTime;
yield return null;
}
if (DialogueManager.isConversationActive) break;
if (!isTriggeringObjectInside || currentTriggererTransform == null || !_runtime_IsStarted) break;
}
}
}
if (currentPlaybackCoroutine == this.currentPlaybackCoroutine)
{
currentPlaybackCoroutine = null;
}
}
#endregion
#region Public Control Methods
[Button]
public void StartPlayer()
{
if (!_runtime_IsStarted)
{
_runtime_IsStarted = true;
if (isInitialized)
{
CheckForAutoStartIfInside();
}
}
}
[Button]
public void StopPlayer()
{
if (_runtime_IsStarted)
{
_runtime_IsStarted = false;
if (resumeCoroutine != null) StopCoroutine(resumeCoroutine);
if (currentPlaybackCoroutine != null)
{
StopCoroutine(currentPlaybackCoroutine);
currentPlaybackCoroutine = null;
}
}
}
private void CheckForAutoStartIfInside()
{
if (_runtime_IsStarted && isTriggeringObjectInside && currentTriggererTransform != null && currentPlaybackCoroutine == null && isInitialized)
{
// Double check dialogue isn't running before starting
if (!DialogueManager.isConversationActive)
{
currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1));
currentPlaybackCoroutine = StartCoroutine(ContinuousPlaybackLoop());
}
}
}
#endregion
#region Save System Integration
public override string RecordData()
{
m_saveData.savedIsStarted = this._runtime_IsStarted;
m_saveData.savedCurrentEntryArrayIndex = this.currentEntryArrayIndex;
m_saveData.savedNextBarkSequenceIndex = this.nextBarkSequenceIndex;
m_saveData.wasShuffled = this.shuffleOrder;
return SaveSystem.Serialize(m_saveData);
}
public override void ApplyData(string s)
{
if (string.IsNullOrEmpty(s))
{
_runtime_IsStarted = startAutomaticallyOnLoad;
m_saveData = new BarkPlayerData();
m_saveData.savedIsStarted = _runtime_IsStarted;
m_saveData.savedCurrentEntryArrayIndex = 0;
m_saveData.savedNextBarkSequenceIndex = 0;
m_saveData.wasShuffled = shuffleOrder;
return;
}
var loadedData = SaveSystem.Deserialize<BarkPlayerData>(s);
if (loadedData != null)
{
m_saveData = loadedData;
this._runtime_IsStarted = m_saveData.savedIsStarted;
this.currentEntryArrayIndex = m_saveData.savedCurrentEntryArrayIndex;
this.nextBarkSequenceIndex = m_saveData.savedNextBarkSequenceIndex;
}
if (!this._runtime_IsStarted && currentPlaybackCoroutine != null)
{
StopCoroutine(currentPlaybackCoroutine);
currentPlaybackCoroutine = null;
}
}
#endregion
#region Helper Methods
private float MaxComponent(Vector3 v) => Mathf.Max(Mathf.Max(v.x, v.y), v.z);
#endregion
}
}