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

548 lines
26 KiB
C#

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using PixelCrushers; // For Saver
using Sirenix.OdinInspector; // For ReadOnly/Button attributes (Optional: remove if not using Odin)
using UnityEngine;
using Random = UnityEngine.Random;
namespace Beyond // Ensure this namespace matches your project
{
[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("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;
// Runtime state fields remain private but visible for debugging if needed
[Header("Runtime State (Read Only)")]
[SerializeField, ReadOnly] // Use Sirenix ReadOnly or remove if not using Odin
private bool _runtime_IsStarted = false; // Tracks the current operational state
[SerializeField, ReadOnly]
private int currentEntryArrayIndex = 0; // Index for barkManagerEntryIndices array
[SerializeField, ReadOnly]
private int nextBarkSequenceIndex = 0; // Index for playbackOrder (within current entry)
[SerializeField, ReadOnly]
private bool isTriggeringObjectInside = false; // Is a valid object currently inside?
#endregion
#region Internal Variables
private List<int> playbackOrder; // Stores the bark order for the CURRENTLY active entry
private bool isInitialized = false;
private Collider triggerCollider;
private BarkManager barkManager; // Cached reference
private Coroutine currentPlaybackCoroutine = null;
private Transform currentTriggererTransform = null; // Transform of the object inside
#endregion
#region Save Data Structure
[System.Serializable]
public class BarkPlayerData
{
public bool savedIsStarted; // Tracks the saved state from _runtime_IsStarted
public int savedCurrentEntryArrayIndex;
public int savedNextBarkSequenceIndex;
public bool wasShuffled; // Tracks shuffle setting at time of save
}
private BarkPlayerData m_saveData = new BarkPlayerData();
#endregion
#region Unity Lifecycle Methods
public override void Awake()
{
// Set initial state based on Inspector setting *before* base.Awake potentially calls ApplyData
// If save data exists, ApplyData will overwrite this.
_runtime_IsStarted = startAutomaticallyOnLoad;
base.Awake(); // This might call ApplyData via Save System
// --- Component & Initial Setup ---
triggerCollider = GetComponent<Collider>();
if (!triggerCollider.isTrigger)
{
Debug.LogWarning($"BarkPlayer on {gameObject.name}: Collider was not set to 'Is Trigger'. Forcing it.", this);
triggerCollider.isTrigger = true;
}
// --- Default LayerMask ---
if (triggeringLayers.value == 0) // LayerMask is empty/unassigned
{
int playerLayer = LayerMask.NameToLayer("Player");
if (playerLayer != -1)
{
triggeringLayers = LayerMask.GetMask("Player"); // Default to Player layer
Debug.LogWarning($"BarkPlayer on {gameObject.name}: Triggering Layers not set in inspector, defaulting to 'Player' layer.", this);
}
else
{
Debug.LogError($"BarkPlayer ({gameObject.name}): Triggering Layers is not set in the inspector, and the default 'Player' layer was not found. This trigger will likely not activate.", this);
}
}
// Final state of _runtime_IsStarted reflects Inspector default OR loaded save data.
}
private void Start()
{
// --- Get BarkManager Instance ---
barkManager = BarkManager.Instance;
if (barkManager == null)
{
Debug.LogError($"BarkPlayer ({gameObject.name}): Could not find BarkManager instance! Disabling component.", this);
enabled = false;
return;
}
// --- Apply Loaded Indices ---
// Ensure array index is valid even if array size changed since saving
currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1));
// nextBarkSequenceIndex will be applied when PrepareEntrySequence is called
// --- Validate Entry Indices ---
if (barkManagerEntryIndices == null || barkManagerEntryIndices.Length == 0)
{
Debug.LogWarning($"BarkPlayer ({gameObject.name}): No Bark Manager Entry Indices provided. Disabling.", this);
enabled = false;
return;
}
for (int i = 0; i < barkManagerEntryIndices.Length; i++)
{
if (barkManagerEntryIndices[i] < 0 || barkManagerEntryIndices[i] >= barkManager.m_barks.Length)
{
Debug.LogError($"BarkPlayer ({gameObject.name}): Invalid Bark Manager Entry Index {barkManagerEntryIndices[i]} at array position {i}. Max index is {barkManager.m_barks.Length - 1}. Disabling.", this);
enabled = false;
return;
}
}
isInitialized = true;
// Check if we should start playing immediately after initialization
// (e.g., if loaded state is 'started' and player is already inside trigger)
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); // Red when active
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));
// Add other collider types if needed
}
}
#endregion
#region Trigger Handling
private void OnTriggerEnter(Collider other)
{
// Ignore if not initialized, not enabled, or runtime state is not 'Started'
if (!isInitialized || !enabled || !_runtime_IsStarted) return;
// --- Layer Check ---
int otherLayer = other.gameObject.layer;
if ((triggeringLayers.value & (1 << otherLayer)) == 0)
{
return; // Layer not in mask, ignore this trigger event
}
// --- Check if already processing an object ---
// This prevents multiple triggering objects from interfering. Only the first one in matters.
if (isTriggeringObjectInside) return;
// --- Valid Trigger ---
Debug.Log($"BarkPlayer ({gameObject.name}): Triggering object '{other.name}' entered.", this);
isTriggeringObjectInside = true;
currentTriggererTransform = other.transform; // Store the transform for barking
// --- Start Playback Coroutine if not already running ---
if (currentPlaybackCoroutine == null)
{
// Apply potentially loaded/saved array index before starting loop
currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1));
// nextBarkSequenceIndex will be set by PrepareEntrySequence inside the coroutine
currentPlaybackCoroutine = StartCoroutine(ContinuousPlaybackLoop());
}
}
private void OnTriggerExit(Collider other)
{
if (!isInitialized || !enabled) return;
// --- Layer Check ---
int otherLayer = other.gameObject.layer;
if ((triggeringLayers.value & (1 << otherLayer)) == 0)
{
return; // Ignore exit events from non-triggering layers
}
// --- Check if the *tracked* object is the one exiting ---
if (other.transform == currentTriggererTransform)
{
Debug.Log($"BarkPlayer ({gameObject.name}): Triggering object '{other.name}' exited.", this);
isTriggeringObjectInside = false;
currentTriggererTransform = null; // Clear the stored transform
// --- Stop the Playback Coroutine ---
if (currentPlaybackCoroutine != null)
{
StopCoroutine(currentPlaybackCoroutine);
currentPlaybackCoroutine = null;
Debug.Log($"BarkPlayer ({gameObject.name}): Stopping playback loop due to exit.", this);
// Optional: Immediately stop BarkManager audio?
// if (barkManager != null && barkManager.IsPlaying) barkManager.m_audioSource.Stop();
}
}
// Else: Some other object on a triggering layer exited, but the primary one is still inside. Do nothing.
}
#endregion
#region Playback Logic
/// <summary>
/// Sets up the playbackOrder list for a specific BarkManager entry index.
/// Resets or applies saved nextBarkSequenceIndex.
/// </summary>
/// <param name="entryIndexToPrepare">The index within BarkManager's m_barks array.</param>
/// <returns>True if preparation was successful (entry has barks), false otherwise.</returns>
private bool PrepareEntrySequence(int entryIndexToPrepare)
{
if (barkManager == null) return false;
int barkCount = barkManager.GetBarkCountInEntry(entryIndexToPrepare);
if (barkCount <= 0)
{
Debug.LogWarning($"BarkPlayer ({gameObject.name}): BarkEntry {entryIndexToPrepare} (from array index {currentEntryArrayIndex}) has no barks. Skipping entry.", this);
playbackOrder = new List<int>(); // Ensure list is empty
nextBarkSequenceIndex = 0;
return false; // Indicate nothing to play for this entry
}
// Generate sequence order (sequential or shuffled)
playbackOrder = Enumerable.Range(0, barkCount).ToList();
if (shuffleOrder)
{
// Simple Fisher-Yates shuffle
for (int i = playbackOrder.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
// Tuple swap is concise
(playbackOrder[i], playbackOrder[j]) = (playbackOrder[j], playbackOrder[i]);
}
// Debug.Log($"BarkPlayer ({gameObject.name}): Shuffled order for entry {entryIndexToPrepare}.", this);
}
// Reset sequence progress. Apply saved index ONLY if the loaded array index matches the current one.
if (currentEntryArrayIndex == m_saveData.savedCurrentEntryArrayIndex)
{
nextBarkSequenceIndex = Mathf.Clamp(m_saveData.savedNextBarkSequenceIndex, 0, playbackOrder.Count);
// Debug.Log($"BarkPlayer ({gameObject.name}): Resuming entry {entryIndexToPrepare} at bark index {nextBarkSequenceIndex}.", this);
}
else
{
nextBarkSequenceIndex = 0; // Start new entry from beginning if array index differs from saved
}
return true; // Preparation successful
}
/// <summary>
/// Main coroutine managing the playback loop through entries and barks.
/// </summary>
private IEnumerator ContinuousPlaybackLoop()
{
Debug.Log($"BarkPlayer ({gameObject.name}): Starting playback loop.", this);
// Initial check for valid configuration
if (barkManagerEntryIndices == null || barkManagerEntryIndices.Length == 0)
{
Debug.LogError($"BarkPlayer ({gameObject.name}): Cannot start loop, entry indices array is empty or null.", this);
currentPlaybackCoroutine = null; // Ensure coroutine reference is cleared
yield break;
}
// Outer loop: Continues as long as a valid triggering object is inside
while (isTriggeringObjectInside && currentTriggererTransform != null)
{
// --- Step 1: Prepare for Current Entry ---
// Safety check/loop wrap for the array index
if (currentEntryArrayIndex >= barkManagerEntryIndices.Length)
{
currentEntryArrayIndex = 0;
if (barkManagerEntryIndices.Length == 0)
{ // Should be caught earlier, but double-check
Debug.LogError($"BarkPlayer ({gameObject.name}): Entry indices array became empty during loop?", this);
yield break;
}
}
int currentManagerEntryIndex = barkManagerEntryIndices[currentEntryArrayIndex];
// Debug.Log($"BarkPlayer ({gameObject.name}): Preparing Entry Index: {currentManagerEntryIndex} (Array Pos: {currentEntryArrayIndex}).", this);
// Prepare the sequence (generate playbackOrder, set nextBarkSequenceIndex)
bool canPlayEntry = PrepareEntrySequence(currentManagerEntryIndex);
if (!canPlayEntry) // Skip this entry if it has no barks
{
// Debug.Log($"BarkPlayer ({gameObject.name}): Skipping empty BarkEntry {currentManagerEntryIndex}.", this);
currentEntryArrayIndex++; // Move immediately to the next entry index
yield return null; // Wait a frame before checking the next entry in the outer loop
continue; // Go to next iteration of the outer while loop (skips delays below)
}
// --- Step 2: Inner Loop - Play Barks within the Current Entry ---
while (nextBarkSequenceIndex < playbackOrder.Count && isTriggeringObjectInside && currentTriggererTransform != null)
{
// Wait for BarkManager's AudioSource if it's busy
while (barkManager.IsPlaying && isTriggeringObjectInside)
{
yield return null; // Wait a frame
}
// Re-check condition after waiting, player might have left
if (!isTriggeringObjectInside || currentTriggererTransform == null) break;
// Play the specific bark for this step in the sequence
int barkIndexToPlay = playbackOrder[nextBarkSequenceIndex];
Debug.Log($"BarkPlayer ({gameObject.name}): Playing Bark {nextBarkSequenceIndex + 1}/{playbackOrder.Count} (Actual Index: {barkIndexToPlay}) from Entry {currentManagerEntryIndex}.", this);
// Play using the specific index, passing the triggerer's transform
AudioClip playedClip = barkManager.PlayBark(currentManagerEntryIndex, currentTriggererTransform, barkIndexToPlay);
nextBarkSequenceIndex++; // Increment progress within this entry's sequence
// Wait for the bark's duration plus the inter-bark delay
float waitTime = delayBetweenBarks;
if (playedClip != null) { waitTime += Mathf.Max(0f, playedClip.length); } // Ensure non-negative length
// Perform the wait, checking continuously if the player leaves
if (waitTime > 0)
{
float timer = 0f;
while (timer < waitTime && isTriggeringObjectInside && currentTriggererTransform != null)
{
timer += Time.deltaTime;
yield return null; // Wait one frame
}
}
else
{
// Minimum one frame wait even if no delay/clip length
yield return null;
}
// If player left during wait, break inner loop
if (!isTriggeringObjectInside || currentTriggererTransform == null) break;
} // --- End of Inner Loop (Barks within Entry) ---
// --- Step 3: Transition to Next Entry (if player didn't exit) ---
if (isTriggeringObjectInside && currentTriggererTransform != null)
{
// Current entry sequence finished normally
Debug.Log($"BarkPlayer ({gameObject.name}): Finished sequence for Entry {currentManagerEntryIndex}.", this);
currentEntryArrayIndex++; // Move to the next entry index in the array
// Check for wrapping / end of all entries in the array
if (currentEntryArrayIndex >= barkManagerEntryIndices.Length)
{
Debug.Log($"BarkPlayer ({gameObject.name}): Finished all entries in the array. Looping back.", this);
currentEntryArrayIndex = 0; // Loop back to the start of the array
}
// Wait for the specified delay BETWEEN entries
if (delayBetweenEntries > 0)
{
Debug.Log($"BarkPlayer ({gameObject.name}): Waiting {delayBetweenEntries}s before next entry.", this);
float timer = 0f;
while (timer < delayBetweenEntries && isTriggeringObjectInside && currentTriggererTransform != null)
{
timer += Time.deltaTime;
yield return null; // Wait one frame
}
// If player left during delay, break outer loop
if (!isTriggeringObjectInside || currentTriggererTransform == null) break;
}
// The outer loop will then continue, preparing the next entry
}
} // --- End of Outer Loop (isTriggeringObjectInside) ---
Debug.Log($"BarkPlayer ({gameObject.name}): Playback loop finished (Triggering object left or component stopped).", this);
// Clear coroutine reference *if* this coroutine instance is the one finishing
if (currentPlaybackCoroutine != null && (!isTriggeringObjectInside || currentTriggererTransform == null))
{
currentPlaybackCoroutine = null;
}
}
#endregion
#region Public Control Methods
/// <summary>
/// Enables the BarkPlayer's runtime operation. It will start playing sequences when triggered.
/// If a triggering object is already inside, playback may start immediately.
/// </summary>
[Button] // Example for Odin Inspector button
public void StartPlayer()
{
if (!_runtime_IsStarted)
{
Debug.Log($"BarkPlayer ({gameObject.name}): Player Runtime Started.", this);
_runtime_IsStarted = true;
// If already initialized, check if we need to immediately start playback
if (isInitialized)
{
CheckForAutoStartIfInside();
}
}
}
/// <summary>
/// Disables the BarkPlayer's runtime operation. Stops any current playback and ignores future triggers.
/// </summary>
[Button] // Example for Odin Inspector button
public void StopPlayer()
{
if (_runtime_IsStarted)
{
Debug.Log($"BarkPlayer ({gameObject.name}): Player Runtime Stopped.", this);
_runtime_IsStarted = false;
// Stop any currently running playback coroutine
if (currentPlaybackCoroutine != null)
{
StopCoroutine(currentPlaybackCoroutine);
currentPlaybackCoroutine = null; // Clear the reference
Debug.Log($"BarkPlayer ({gameObject.name}): Stopping active playback loop.", this);
// Optional: Immediately stop BarkManager audio?
// if (barkManager != null && barkManager.IsPlaying) barkManager.m_audioSource.Stop();
}
}
}
/// <summary>
/// Checks if the player is currently inside the trigger and starts the playback loop
/// if the BarkPlayer is started (_runtime_IsStarted=true) but the loop isn't currently running.
/// Useful after initialization (in Start) or after calling StartPlayer manually.
/// </summary>
private void CheckForAutoStartIfInside()
{
if (_runtime_IsStarted && isTriggeringObjectInside && currentTriggererTransform != null && currentPlaybackCoroutine == null && isInitialized)
{
Debug.Log($"BarkPlayer ({gameObject.name}): Triggering object already inside and player is started. Starting playback loop.", this);
// Ensure indices are correctly set from save data before starting
currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1));
currentPlaybackCoroutine = StartCoroutine(ContinuousPlaybackLoop());
}
}
#endregion
#region Save System Integration (Pixel Crushers Saver)
public override string RecordData()
{
// Update save data container with current runtime state
m_saveData.savedIsStarted = this._runtime_IsStarted;
m_saveData.savedCurrentEntryArrayIndex = this.currentEntryArrayIndex;
m_saveData.savedNextBarkSequenceIndex = this.nextBarkSequenceIndex;
m_saveData.wasShuffled = this.shuffleOrder; // Record shuffle setting at time of save
// Serialize the save data container
return SaveSystem.Serialize(m_saveData);
}
public override void ApplyData(string s)
{
// Handle case where there is no save data for this component
if (string.IsNullOrEmpty(s))
{
// No save data - runtime state keeps the value set by startAutomaticallyOnLoad in Awake
_runtime_IsStarted = startAutomaticallyOnLoad;
// Reset progress indices if no save data
m_saveData.savedCurrentEntryArrayIndex = 0;
m_saveData.savedNextBarkSequenceIndex = 0;
Debug.Log($"BarkPlayer ({gameObject.name}): No save data found, using startAutomaticallyOnLoad ({startAutomaticallyOnLoad}).", this);
return;
}
// Deserialize the saved data string
var loadedData = SaveSystem.Deserialize<BarkPlayerData>(s);
if (loadedData != null)
{
m_saveData = loadedData; // Store the loaded data
// Apply the loaded runtime state
this._runtime_IsStarted = m_saveData.savedIsStarted;
// Indices will be applied in Start() / PrepareEntrySequence() using m_saveData
Debug.Log($"BarkPlayer ({gameObject.name}): Applied loaded data. Started={_runtime_IsStarted}, EntryIdx={m_saveData.savedCurrentEntryArrayIndex}, BarkIdx={m_saveData.savedNextBarkSequenceIndex}.", this);
}
else
{
Debug.LogError($"BarkPlayer ({gameObject.name}): Failed to deserialize save data. Using defaults.", this);
// Fallback: use inspector setting if deserialize failed
this._runtime_IsStarted = startAutomaticallyOnLoad;
m_saveData.savedCurrentEntryArrayIndex = 0;
m_saveData.savedNextBarkSequenceIndex = 0;
}
// After applying data, ensure consistency: if loaded state is 'stopped', stop coroutine
if (!this._runtime_IsStarted && currentPlaybackCoroutine != null)
{
StopCoroutine(currentPlaybackCoroutine);
currentPlaybackCoroutine = null;
}
// The CheckForAutoStartIfInside() call in Start() will handle restarting the
// coroutine if the loaded state is 'started' and the player is inside the trigger.
}
#endregion
#region Helper Methods
/// <summary> Helper to find the largest component of a Vector3 (for Gizmos). </summary>
private float MaxComponent(Vector3 v) => Mathf.Max(Mathf.Max(v.x, v.y), v.z);
#endregion
}
}