This commit is contained in:
Michal Pikulski
2025-10-07 12:57:23 +02:00
48 changed files with 3062 additions and 254 deletions

View File

@@ -0,0 +1,58 @@
using UnityEngine;
namespace Interactions
{
/// <summary>
/// Defines a target position for character movement during interaction.
/// Attach this to an interactable object's child to specify where
/// characters should move during interaction rather than using the default calculations.
/// </summary>
public class CharacterMoveToTarget : MonoBehaviour
{
[Tooltip("Which character this target position is for")]
public CharacterToInteract characterType = CharacterToInteract.Pulver;
[Tooltip("Optional offset from this transform's position")]
public Vector3 positionOffset = Vector3.zero;
/// <summary>
/// Get the target position for this character to move to
/// </summary>
public Vector3 GetTargetPosition()
{
return transform.position + positionOffset;
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
// Draw a different colored sphere based on which character this target is for
switch (characterType)
{
case CharacterToInteract.Trafalgar:
Gizmos.color = new Color(0f, 0.5f, 1f, 0.8f); // Blue for player
break;
case CharacterToInteract.Pulver:
Gizmos.color = new Color(1f, 0.5f, 0f, 0.8f); // Orange for follower
break;
case CharacterToInteract.Both:
Gizmos.color = new Color(0.7f, 0f, 0.7f, 0.8f); // Purple for both
break;
default:
Gizmos.color = new Color(0.5f, 0.5f, 0.5f, 0.8f); // Gray for none
break;
}
Vector3 targetPos = GetTargetPosition();
Gizmos.DrawSphere(targetPos, 0.2f);
// Draw a line from the parent interactable to this target
Interactable parentInteractable = GetComponentInParent<Interactable>();
if (parentInteractable != null)
{
Gizmos.DrawLine(parentInteractable.transform.position, targetPos);
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2bcf343b3ef74f0fb3c64be6fd2893b6
timeCreated: 1759744130

View File

@@ -1,22 +1,27 @@
using Input;
using UnityEngine;
using System;
using System.Collections.Generic;
using UnityEngine.Events;
using System.Threading.Tasks;
namespace Interactions
{
public enum CharacterToInteract
{
None,
Trafalgar,
Pulver
Pulver,
Both
}
/// <summary>
/// Represents an interactable object that can respond to tap input events.
/// </summary>
public class Interactable : MonoBehaviour, ITouchInputConsumer
{
[Header("Interaction Settings")]
public bool isOneTime = false;
public bool isOneTime;
public float cooldown = -1f;
public CharacterToInteract characterToInteract = CharacterToInteract.Pulver;
@@ -30,15 +35,64 @@ namespace Interactions
private bool _interactionInProgress;
private PlayerTouchController _playerRef;
private FollowerController _followerController;
private bool _isActive = true;
private InteractionEventType _currentEventType;
// Action component system
private List<InteractionActionBase> _registeredActions = new List<InteractionActionBase>();
private void Awake()
{
// Subscribe to interactionComplete event
interactionComplete.AddListener(OnInteractionComplete);
}
/// <summary>
/// Register an action component with this interactable
/// </summary>
public void RegisterAction(InteractionActionBase action)
{
if (!_registeredActions.Contains(action))
{
_registeredActions.Add(action);
}
}
/// <summary>
/// Unregister an action component from this interactable
/// </summary>
public void UnregisterAction(InteractionActionBase action)
{
_registeredActions.Remove(action);
}
/// <summary>
/// Dispatch an interaction event to all registered actions and await their completion
/// </summary>
private async Task DispatchEventAsync(InteractionEventType eventType)
{
_currentEventType = eventType;
// Collect all tasks from actions that want to respond
List<Task<bool>> tasks = new List<Task<bool>>();
foreach (var action in _registeredActions)
{
Task<bool> task = action.OnInteractionEvent(eventType, _playerRef, _followerController);
if (task != null)
{
tasks.Add(task);
}
}
if (tasks.Count > 0)
{
// Wait for all tasks to complete
await Task.WhenAll(tasks);
}
// If no tasks were added, the method will complete immediately (no need for await)
}
/// <summary>
/// Handles tap input. Triggers interaction logic.
/// </summary>
@@ -50,11 +104,12 @@ namespace Interactions
return;
}
Debug.Log($"[Interactable] OnTap at {worldPosition} on {gameObject.name}");
// Broadcast interaction started event
TryInteract();
// Start the interaction process asynchronously
_ = TryInteractAsync();
}
public void TryInteract()
private async Task TryInteractAsync()
{
_interactionInProgress = true;
@@ -63,68 +118,302 @@ namespace Interactions
interactionStarted?.Invoke(_playerRef, _followerController);
// Dispatch the InteractionStarted event to action components
await DispatchEventAsync(InteractionEventType.InteractionStarted);
// After all InteractionStarted actions complete, proceed to player movement
await StartPlayerMovementAsync();
}
private async Task StartPlayerMovementAsync()
{
if (_playerRef == null)
{
Debug.Log($"[Interactable] Player character could not be found. Aborting interaction.");
interactionInterrupted.Invoke();
await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
return;
}
// If characterToInteract is None, immediately trigger the characterArrived event
if (characterToInteract == CharacterToInteract.None)
{
await BroadcastCharacterArrivedAsync();
return;
}
// Compute closest point on the interaction radius
Vector3 interactablePos = transform.position;
Vector3 playerPos = _playerRef.transform.position;
float stopDistance = characterToInteract == CharacterToInteract.Pulver
? GameManager.Instance.PlayerStopDistance
: GameManager.Instance.PlayerStopDistanceDirectInteraction;
Vector3 toPlayer = (playerPos - interactablePos).normalized;
Vector3 stopPoint = interactablePos + toPlayer * stopDistance;
// Check for a CharacterMoveToTarget component for Trafalgar (player) or Both
Vector3 stopPoint;
bool customTargetFound = false;
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
// Target is valid if it matches Trafalgar specifically or is set to Both
if (target.characterType == CharacterToInteract.Trafalgar || target.characterType == CharacterToInteract.Both)
{
stopPoint = target.GetTargetPosition();
customTargetFound = true;
// We need to wait for the player to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Use local functions instead of circular lambda references
void OnPlayerArrivedLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then continue with the interaction flow
OnPlayerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
void OnPlayerMoveCancelledLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then handle the cancellation
OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false));
}
// Unsubscribe previous handlers (if any)
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
// Subscribe our new handlers
_playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal;
// Start the player movement
_playerRef.MoveToAndNotify(stopPoint);
// Await player arrival
await tcs.Task;
return;
}
}
// If no custom target was found, use the default behavior
if (!customTargetFound)
{
// Compute closest point on the interaction radius
Vector3 interactablePos = transform.position;
Vector3 playerPos = _playerRef.transform.position;
float stopDistance = characterToInteract == CharacterToInteract.Pulver
? GameManager.Instance.PlayerStopDistance
: GameManager.Instance.PlayerStopDistanceDirectInteraction;
Vector3 toPlayer = (playerPos - interactablePos).normalized;
stopPoint = interactablePos + toPlayer * stopDistance;
// Unsubscribe previous to avoid duplicate calls
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
_playerRef.OnArrivedAtTarget += OnPlayerArrived;
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelled;
_playerRef.MoveToAndNotify(stopPoint);
// We need to wait for the player to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Use local functions instead of circular lambda references
void OnPlayerArrivedLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then continue with the interaction flow
OnPlayerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
void OnPlayerMoveCancelledLocal()
{
// First remove both event handlers to prevent memory leaks
if (_playerRef != null)
{
_playerRef.OnArrivedAtTarget -= OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelledLocal;
}
// Then handle the cancellation
OnPlayerMoveCancelledAsync().ContinueWith(_ => tcs.TrySetResult(false));
}
// Unsubscribe previous handlers (if any)
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
_playerRef.OnMoveToCancelled -= OnPlayerMoveCancelled;
// Subscribe our new handlers
_playerRef.OnArrivedAtTarget += OnPlayerArrivedLocal;
_playerRef.OnMoveToCancelled += OnPlayerMoveCancelledLocal;
// Start the player movement
_playerRef.MoveToAndNotify(stopPoint);
// Await player arrival
await tcs.Task;
}
}
private void OnPlayerMoveCancelled()
private async Task OnPlayerMoveCancelledAsync()
{
_interactionInProgress = false;
interactionInterrupted?.Invoke();
await DispatchEventAsync(InteractionEventType.InteractionInterrupted);
}
private void OnPlayerArrived()
private async Task OnPlayerArrivedAsync()
{
if (!_interactionInProgress)
return;
// Unsubscribe to avoid memory leaks
_playerRef.OnArrivedAtTarget -= OnPlayerArrived;
// Dispatch PlayerArrived event
await DispatchEventAsync(InteractionEventType.PlayerArrived);
// After all PlayerArrived actions complete, proceed to character interaction
await HandleCharacterInteractionAsync();
}
private async Task HandleCharacterInteractionAsync()
{
if (characterToInteract == CharacterToInteract.Pulver)
{
_followerController.OnPickupArrived -= OnFollowerArrived;
_followerController.OnPickupArrived += OnFollowerArrived;
_followerController.GoToPointAndReturn(transform.position, _playerRef.transform);
// We need to wait for the follower to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Create a proper local function for the event handler
void OnFollowerArrivedLocal()
{
// First remove the event handler to prevent memory leaks
if (_followerController != null)
{
_followerController.OnPickupArrived -= OnFollowerArrivedLocal;
}
// Then continue with the interaction flow
OnFollowerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
// Register our new local function handler
_followerController.OnPickupArrived += OnFollowerArrivedLocal;
// Check for a CharacterMoveToTarget component for Pulver or Both
Vector3 targetPosition = transform.position;
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both)
{
targetPosition = target.GetTargetPosition();
break;
}
}
// Use the new GoToPoint method instead of GoToPointAndReturn
_followerController.GoToPoint(targetPosition);
// Await follower arrival
await tcs.Task;
}
else if (characterToInteract == CharacterToInteract.Trafalgar)
{
BroadcastCharacterArrived();
await BroadcastCharacterArrivedAsync();
}
else if (characterToInteract == CharacterToInteract.Both)
{
// We need to wait for the follower to arrive, so use a TaskCompletionSource
var tcs = new TaskCompletionSource<bool>();
// Create a proper local function for the event handler
void OnFollowerArrivedLocal()
{
// First remove the event handler to prevent memory leaks
if (_followerController != null)
{
_followerController.OnPickupArrived -= OnFollowerArrivedLocal;
}
// Then continue with the interaction flow
OnFollowerArrivedAsync().ContinueWith(_ => tcs.TrySetResult(true));
}
// Register our new local function handler
_followerController.OnPickupArrived += OnFollowerArrivedLocal;
// Check for a CharacterMoveToTarget component for Pulver or Both
Vector3 targetPosition = transform.position;
CharacterMoveToTarget[] moveTargets = GetComponentsInChildren<CharacterMoveToTarget>();
foreach (var target in moveTargets)
{
if (target.characterType == CharacterToInteract.Pulver || target.characterType == CharacterToInteract.Both)
{
targetPosition = target.GetTargetPosition();
break;
}
}
// Use the new GoToPoint method instead of GoToPointAndReturn
_followerController.GoToPoint(targetPosition);
// Await follower arrival
await tcs.Task;
}
}
private void OnFollowerArrived()
private async Task OnFollowerArrivedAsync()
{
if (!_interactionInProgress)
return;
// Unsubscribe to avoid memory leaks
_followerController.OnPickupArrived -= OnFollowerArrived;
// Dispatch InteractingCharacterArrived event and WAIT for all actions to complete
// This ensures we wait for any timeline animations to finish before proceeding
Debug.Log("[Interactable] Follower arrived, dispatching InteractingCharacterArrived event and waiting for completion");
await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
Debug.Log("[Interactable] All InteractingCharacterArrived actions completed, proceeding with interaction");
BroadcastCharacterArrived();
// Check if we have any components that might have paused the interaction flow
bool hasTimelineActions = false;
foreach (var action in _registeredActions)
{
if (action is InteractionTimelineAction timelineAction &&
timelineAction.respondToEvents.Contains(InteractionEventType.InteractingCharacterArrived) &&
timelineAction.pauseInteractionFlow)
{
hasTimelineActions = true;
break;
}
}
// Tell the follower to return to the player
if (_followerController != null && _playerRef != null)
{
_followerController.ReturnToPlayer(_playerRef.transform);
}
// After all InteractingCharacterArrived actions complete, proceed to character arrived
await BroadcastCharacterArrivedAsync();
}
private void BroadcastCharacterArrived()
// Legacy non-async method to maintain compatibility with existing code
private void OnPlayerArrived()
{
// This is now just a wrapper for the async version
_ = OnPlayerArrivedAsync();
}
// Legacy non-async method to maintain compatibility with existing code
private void OnPlayerMoveCancelled()
{
// This is now just a wrapper for the async version
_ = OnPlayerMoveCancelledAsync();
}
private async Task BroadcastCharacterArrivedAsync()
{
// Check for ObjectiveStepBehaviour and lock state
var step = GetComponent<PuzzleS.ObjectiveStepBehaviour>();
@@ -138,16 +427,24 @@ namespace Interactions
_followerController = null;
return;
}
// Dispatch CharacterArrived event
// await DispatchEventAsync(InteractionEventType.InteractingCharacterArrived);
// Broadcast appropriate event
characterArrived?.Invoke();
// Reset variables for next time
_interactionInProgress = false;
_playerRef = null;
_followerController = null;
}
private void OnInteractionComplete(bool success)
private async void OnInteractionComplete(bool success)
{
// Dispatch InteractionComplete event
await DispatchEventAsync(InteractionEventType.InteractionComplete);
if (success)
{
if (isOneTime)

View File

@@ -0,0 +1,91 @@
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.Threading.Tasks;
using Input;
namespace Interactions
{
/// <summary>
/// Base class for all interaction action components
/// These components respond to interaction events and can control the interaction flow
/// </summary>
public abstract class InteractionActionBase : MonoBehaviour
{
[Tooltip("Which interaction events this action should respond to")]
public List<InteractionEventType> respondToEvents = new List<InteractionEventType>();
[Tooltip("Whether the interaction flow should wait for this action to complete")]
public bool pauseInteractionFlow = true;
protected Interactable parentInteractable;
protected virtual void Awake()
{
// Get the parent interactable component
parentInteractable = GetComponentInParent<Interactable>();
if (parentInteractable == null)
{
Debug.LogError($"[{GetType().Name}] Cannot find parent Interactable component!");
enabled = false;
return;
}
}
protected virtual void OnEnable()
{
if (parentInteractable != null)
{
parentInteractable.RegisterAction(this);
}
}
protected virtual void OnDisable()
{
if (parentInteractable != null)
{
parentInteractable.UnregisterAction(this);
}
}
/// <summary>
/// Called when an interaction event occurs that this action is registered for
/// </summary>
/// <param name="eventType">The type of event that occurred</param>
/// <returns>A task that completes when the action is finished, or null if action won't execute</returns>
public Task<bool> OnInteractionEvent(InteractionEventType eventType, PlayerTouchController player, FollowerController follower)
{
if (respondToEvents.Contains(eventType) && ShouldExecute(eventType, player, follower))
{
if (pauseInteractionFlow)
{
return ExecuteAsync(eventType, player, follower);
}
else
{
// If we don't need to pause the flow, execute in the background
// and return a completed task
_ = ExecuteAsync(eventType, player, follower);
return Task.FromResult(false);
}
}
return null;
}
/// <summary>
/// Execute the action for the given event asynchronously
/// </summary>
protected abstract Task<bool> ExecuteAsync(InteractionEventType eventType, PlayerTouchController player, FollowerController follower);
/// <summary>
/// Called to determine if this action should execute for the given event
/// Override this to add additional conditions for execution
/// </summary>
protected virtual bool ShouldExecute(InteractionEventType eventType, PlayerTouchController player, FollowerController follower)
{
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5cf351d32dac4169a9db20609727a70f
timeCreated: 1759746705

View File

@@ -0,0 +1,16 @@
using System;
namespace Interactions
{
/// <summary>
/// Defines the different types of events that can occur during an interaction
/// </summary>
public enum InteractionEventType
{
InteractionStarted,
PlayerArrived,
InteractingCharacterArrived,
InteractionComplete,
InteractionInterrupted
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 78684d31bd4d4636834a494c7cb74f48
timeCreated: 1759746690

View File

@@ -0,0 +1,258 @@
using System;
using UnityEngine;
using UnityEngine.Playables;
using System.Linq;
using System.Threading.Tasks;
using Input;
namespace Interactions
{
/// <summary>
/// Component that plays timeline animations in response to interaction events
/// </summary>
[RequireComponent(typeof(PlayableDirector))]
public class InteractionTimelineAction : InteractionActionBase
{
[System.Serializable]
public class TimelineEventMapping
{
public InteractionEventType eventType;
public PlayableAsset[] timelines;
[Tooltip("Whether to bind the player character to the track named 'Player'")]
public bool bindPlayerCharacter = false;
[Tooltip("Whether to bind the follower character to the track named 'Pulver'")]
public bool bindPulverCharacter = false;
[Tooltip("Custom track name for player character binding")]
public string playerTrackName = "Player";
[Tooltip("Custom track name for follower character binding")]
public string pulverTrackName = "Pulver";
[Tooltip("Time in seconds before the timeline is automatically completed (safety feature)")]
public float timeoutSeconds = 30f;
[Tooltip("Whether to loop the last timeline in the sequence")]
public bool loopLast = false;
[Tooltip("Whether to loop through all timelines in the sequence")]
public bool loopAll = false;
// Helper property to check if we have valid timelines
public bool HasValidTimelines => timelines != null && timelines.Length > 0 && timelines[0] != null;
}
[Header("Timeline Configuration")] [SerializeField]
private PlayableDirector playableDirector;
[SerializeField] private TimelineEventMapping[] timelineMappings;
private TaskCompletionSource<bool> _currentPlaybackTCS;
private int _currentTimelineIndex = 0;
private TimelineEventMapping _currentMapping = null;
protected override void Awake()
{
base.Awake();
if (playableDirector == null)
{
playableDirector = GetComponent<PlayableDirector>();
}
if (playableDirector == null)
{
Debug.LogError("[InteractionTimelineAction] PlayableDirector component is missing!");
enabled = false;
return;
}
// Subscribe to the director's stopped event
playableDirector.stopped += OnPlayableDirectorStopped;
}
private void OnDestroy()
{
if (playableDirector != null)
{
playableDirector.stopped -= OnPlayableDirectorStopped;
}
}
protected override async Task<bool> ExecuteAsync(InteractionEventType eventType, PlayerTouchController player,
FollowerController follower)
{
// Find the timeline for this event type
TimelineEventMapping mapping = Array.Find(timelineMappings, m => m.eventType == eventType);
if (mapping == null || !mapping.HasValidTimelines)
{
// No timeline configured for this event
return true;
}
_currentMapping = mapping;
// _currentTimelineIndex = 0;
return await PlayTimelineSequence(player, follower);
}
private async Task<bool> PlayTimelineSequence(PlayerTouchController player, FollowerController follower)
{
if (_currentMapping == null || !_currentMapping.HasValidTimelines)
{
return true;
}
follower.DropHeldItemAt(follower.transform.position);
// Play the current timeline in the sequence
bool result = await PlaySingleTimeline(_currentMapping.timelines[_currentTimelineIndex], _currentMapping, player, follower);
// Return false if the playback failed
if (!result)
{
return false;
}
// Increment the timeline index for next playback
_currentTimelineIndex++;
// Check if we've reached the end of the sequence
if (_currentTimelineIndex >= _currentMapping.timelines.Length)
{
// If loop all is enabled, start over
if (_currentMapping.loopAll)
{
_currentTimelineIndex = 0;
// Don't continue automatically, wait for next interaction
return true;
}
// If loop last is enabled, replay the last timeline
else if (_currentMapping.loopLast)
{
_currentTimelineIndex = _currentMapping.timelines.Length - 1;
// Don't continue automatically, wait for next interaction
return true;
}
// Otherwise, we're done with the sequence
else
{
_currentTimelineIndex = 0;
_currentMapping = null;
return true;
}
}
// If we have more timelines in the sequence, we're done for now
// Next interaction will pick up where we left off
return true;
}
private async Task<bool> PlaySingleTimeline(PlayableAsset timelineAsset, TimelineEventMapping mapping,
PlayerTouchController player, FollowerController follower)
{
if (timelineAsset == null)
{
Debug.LogWarning("[InteractionTimelineAction] Timeline asset is null");
return true; // Return true to continue the interaction flow
}
// Set the timeline asset
playableDirector.playableAsset = timelineAsset;
// Bind characters if needed
if (mapping.bindPlayerCharacter && player != null)
{
try
{
var trackOutput = playableDirector.playableAsset.outputs.FirstOrDefault(o => o.streamName == mapping.playerTrackName);
if (trackOutput.sourceObject != null)
{
playableDirector.SetGenericBinding(trackOutput.sourceObject, player.gameObject);
}
else
{
Debug.LogWarning($"[InteractionTimelineAction] Could not find track named '{mapping.playerTrackName}' for player binding");
}
}
catch (Exception ex)
{
Debug.LogError($"[InteractionTimelineAction] Error binding player to timeline: {ex.Message}");
}
}
if (mapping.bindPulverCharacter && follower != null)
{
try
{
var trackOutput = playableDirector.playableAsset.outputs.FirstOrDefault(o => o.streamName == mapping.pulverTrackName);
if (trackOutput.sourceObject != null)
{
playableDirector.SetGenericBinding(trackOutput.sourceObject, follower.gameObject);
}
else
{
Debug.LogWarning($"[InteractionTimelineAction] Could not find track named '{mapping.pulverTrackName}' for follower binding");
}
}
catch (Exception ex)
{
Debug.LogError($"[InteractionTimelineAction] Error binding follower to timeline: {ex.Message}");
}
}
// Create a task completion source to await the timeline completion
_currentPlaybackTCS = new TaskCompletionSource<bool>();
// Register for the stopped event if not already registered
playableDirector.stopped -= OnPlayableDirectorStopped;
playableDirector.stopped += OnPlayableDirectorStopped;
// Log the timeline playback
Debug.Log($"[InteractionTimelineAction] Playing timeline {timelineAsset.name} for event {mapping.eventType}");
// Play the timeline
playableDirector.Play();
// Start a timeout coroutine for safety using the mapping's timeout
StartCoroutine(TimeoutCoroutine(mapping.timeoutSeconds));
// Await the timeline completion (will be signaled by the OnPlayableDirectorStopped callback)
bool result = await _currentPlaybackTCS.Task;
// Log completion
Debug.Log($"[InteractionTimelineAction] Timeline {timelineAsset.name} playback completed with result: {result}");
// Clear the task completion source
_currentPlaybackTCS = null;
return result;
}
private void OnPlayableDirectorStopped(PlayableDirector director)
{
if (director != playableDirector || _currentPlaybackTCS == null)
return;
Debug.Log($"[InteractionTimelineAction] PlayableDirector stopped. Signaling completion.");
// Signal completion when the director stops
_currentPlaybackTCS.TrySetResult(true);
}
private System.Collections.IEnumerator TimeoutCoroutine(float timeoutDuration)
{
yield return new WaitForSeconds(timeoutDuration);
// If the TCS still exists after timeout, complete it with failure
if (_currentPlaybackTCS != null)
{
Debug.LogWarning($"[InteractionTimelineAction] Timeline playback timed out after {timeoutDuration} seconds");
_currentPlaybackTCS.TrySetResult(false);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 42e77a0c97604b6eb7674e58726c831a
timeCreated: 1759746720