Unity Timeline Interaction System Integration (#17)
- Added InteractionTimelineAction component for timeline-driven interactions - Implemented custom editor for timeline event mapping - Updated interaction event flow to support timeline actions - Enhanced character move target configuration - Improved inspector UI for interactable components - Added technical documentation for interaction system - Refactored interaction action base classes for extensibility - Fixed issues with character binding in timelines Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #17
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user