Unity Timeline Interaction System Integration #17

Merged
tschesky merged 4 commits from unity_timeline_on_interact into main 2025-10-07 10:57:11 +00:00
27 changed files with 3101 additions and 46 deletions
Showing only changes of commit bdec77d36f - Show all commits

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 27f5954bda3e24b48b33e5a6605eb33f
guid: 1791fd5a24a3142418ed441a2a25b374
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000

View File

@@ -41,47 +41,6 @@ MonoBehaviour:
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!114 &-7231857257271738743
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track (2)
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationTrack
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips: []
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 1
m_InfiniteClipPostExtrapolation: 1
m_InfiniteClipOffsetPosition: {x: -3.4, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: -0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: -6922421277477507865}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!74 &-7150935952859310220
AnimationClip:
m_ObjectHideFlags: 0
@@ -101,14 +60,23 @@ AnimationClip:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0.11666667
value: {x: 0, y: 0, z: 0}
time: 0
value: {x: -0.000011444092, y: 0, z: 0}
inSlope: {x: 0, y: 0, z: 0}
outSlope: {x: 0, y: 0, z: 0}
tangentMode: 0
weightedMode: 0
inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334}
outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334}
- serializedVersion: 3
time: 0.11666667
value: {x: 0, y: 0, z: 0}
inSlope: {x: 0.00024968927, y: 0, z: 0}
outSlope: {x: 0.00024968927, y: 0, z: 0}
tangentMode: 0
weightedMode: 0
inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334}
outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334}
- serializedVersion: 3
time: 0.18333334
value: {x: 3, y: 2.2000046, z: 0}
@@ -222,14 +190,23 @@ AnimationClip:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0.11666667
value: 0
time: 0
value: -0.000011444092
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.11666667
value: 0
inSlope: 0.00024968927
outSlope: 0.00024968927
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.18333334
value: 3
@@ -305,6 +282,15 @@ AnimationClip:
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.11666667
value: 0
@@ -389,6 +375,15 @@ AnimationClip:
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0.11666667
value: 0
@@ -473,145 +468,6 @@ AnimationClip:
m_HasGenericRootTransform: 1
m_HasMotionFloatCurves: 0
m_Events: []
--- !u!74 &-6922421277477507865
AnimationClip:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Recorded
serializedVersion: 7
m_Legacy: 0
m_Compressed: 0
m_UseHighQualityCurve: 1
m_RotationCurves: []
m_CompressedRotationCurves: []
m_EulerCurves: []
m_PositionCurves:
- curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: {x: -39.2, y: 92, z: 0}
inSlope: {x: 0, y: 0, z: 0}
outSlope: {x: 0, y: 0, z: 0}
tangentMode: 0
weightedMode: 0
inWeight: {x: 0, y: 0.33333334, z: 0.33333334}
outWeight: {x: 0, y: 0.33333334, z: 0.33333334}
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
path:
m_ScaleCurves: []
m_FloatCurves: []
m_PPtrCurves: []
m_SampleRate: 60
m_WrapMode: 0
m_Bounds:
m_Center: {x: 0, y: 0, z: 0}
m_Extent: {x: 0, y: 0, z: 0}
m_ClipBindingConstant:
genericBindings:
- serializedVersion: 2
path: 0
attribute: 1
script: {fileID: 0}
typeID: 4
customType: 0
isPPtrCurve: 0
isIntCurve: 0
isSerializeReferenceCurve: 0
pptrCurveMapping: []
m_AnimationClipSettings:
serializedVersion: 2
m_AdditiveReferencePoseClip: {fileID: 0}
m_AdditiveReferencePoseTime: 0
m_StartTime: 0
m_StopTime: 0
m_OrientationOffsetY: 0
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 0
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0
m_LoopBlendPositionXZ: 0
m_KeepOriginalOrientation: 0
m_KeepOriginalPositionY: 1
m_KeepOriginalPositionXZ: 0
m_HeightFromFeet: 0
m_Mirror: 0
m_EditorCurves:
- serializedVersion: 2
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: -39.2
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
attribute: m_LocalPosition.x
path:
classID: 4
script: {fileID: 0}
flags: 0
- serializedVersion: 2
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 92
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
attribute: m_LocalPosition.y
path:
classID: 4
script: {fileID: 0}
flags: 0
- serializedVersion: 2
curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 136
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
attribute: m_LocalPosition.z
path:
classID: 4
script: {fileID: 0}
flags: 0
m_EulerEditorCurves: []
m_HasGenericRootTransform: 1
m_HasMotionFloatCurves: 0
m_Events: []
--- !u!114 &-4100604165561699402
MonoBehaviour:
m_ObjectHideFlags: 1
@@ -779,7 +635,6 @@ MonoBehaviour:
m_Tracks:
- {fileID: -7584736085941489071}
- {fileID: -2395336864975438248}
- {fileID: -7231857257271738743}
- {fileID: -4100604165561699402}
m_FixedDuration: 0
m_EditorSettings:

View File

@@ -0,0 +1,197 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-7584736085941489071
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationTrack
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips: []
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 0
m_InfiniteClipPostExtrapolation: 0
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: 0}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!114 &-2395336864975438248
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d21dcc2386d650c4597f3633c75a1f98, type: 3}
m_Name: Animation Track (1)
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationTrack
m_Version: 3
m_AnimClip: {fileID: 0}
m_Locked: 0
m_Muted: 0
m_CustomPlayableFullTypename:
m_Curves: {fileID: 0}
m_Parent: {fileID: 11400000}
m_Children: []
m_Clips:
- m_Version: 1
m_Start: 0
m_ClipIn: 0
m_Asset: {fileID: 3115908604919352715}
m_Duration: 0.8333333333333334
m_TimeScale: 1
m_ParentTrack: {fileID: -2395336864975438248}
m_EaseInDuration: 0
m_EaseOutDuration: 0
m_BlendInDuration: -1
m_BlendOutDuration: -1
m_MixInCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_MixOutCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
m_BlendInCurveMode: 0
m_BlendOutCurveMode: 0
m_ExposedParameterNames: []
m_AnimationCurves: {fileID: 0}
m_Recordable: 0
m_PostExtrapolationMode: 1
m_PreExtrapolationMode: 1
m_PostExtrapolationTime: Infinity
m_PreExtrapolationTime: 0
m_DisplayName: Pulver_Cucumbatacc
m_Markers:
m_Objects: []
m_InfiniteClipPreExtrapolation: 0
m_InfiniteClipPostExtrapolation: 0
m_InfiniteClipOffsetPosition: {x: 0, y: 0, z: 0}
m_InfiniteClipOffsetEulerAngles: {x: 0, y: 0, z: 0}
m_InfiniteClipTimeOffset: 0
m_InfiniteClipRemoveOffset: 0
m_InfiniteClipApplyFootIK: 1
mInfiniteClipLoop: 0
m_MatchTargetFields: 63
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_AvatarMask: {fileID: 0}
m_ApplyAvatarMask: 1
m_TrackOffset: 0
m_InfiniteClip: {fileID: 0}
m_OpenClipOffsetRotation: {x: 0, y: 0, z: 0, w: 1}
m_Rotation: {x: 0, y: 0, z: 0, w: 1}
m_ApplyOffsets: 0
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bfda56da833e2384a9677cd3c976a436, type: 3}
m_Name: PulverCucumberSmack_empty
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.TimelineAsset
m_Version: 0
m_Tracks:
- {fileID: -7584736085941489071}
- {fileID: -2395336864975438248}
m_FixedDuration: 0
m_EditorSettings:
m_Framerate: 60
m_ScenePreview: 1
m_DurationMode: 0
m_MarkerTrack: {fileID: 0}
--- !u!114 &3115908604919352715
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 030f85c3f73729f4f976f66ffb23b875, type: 3}
m_Name: AnimationPlayableAsset
m_EditorClassIdentifier: Unity.Timeline::UnityEngine.Timeline.AnimationPlayableAsset
m_Clip: {fileID: 7400000, guid: 09d7dd4e84cbed54bb4ca4e63ad0c6fa, type: 2}
m_Position: {x: 0, y: 0, z: 0}
m_EulerAngles: {x: 0, y: 0, z: 0}
m_UseTrackMatchFields: 1
m_MatchTargetFields: 63
m_RemoveStartOffset: 1
m_ApplyFootIK: 1
m_Loop: 0
m_Version: 1
m_Rotation: {x: 0, y: 0, z: 0, w: 1}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a591b28d7cd18274b9cca9511b96261a
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -314,7 +314,8 @@ namespace Interactions
}
}
_followerController.GoToPointAndReturn(targetPosition, _playerRef.transform);
// Use the new GoToPoint method instead of GoToPointAndReturn
_followerController.GoToPoint(targetPosition);
// Await follower arrival
await tcs.Task;
@@ -356,7 +357,8 @@ namespace Interactions
}
}
_followerController.GoToPointAndReturn(targetPosition, _playerRef.transform);
// Use the new GoToPoint method instead of GoToPointAndReturn
_followerController.GoToPoint(targetPosition);
// Await follower arrival
await tcs.Task;
@@ -368,10 +370,32 @@ namespace Interactions
if (!_interactionInProgress)
return;
// Dispatch FollowerArrived event
// 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");
// After all FollowerArrived actions complete, proceed to character arrived
// 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();
}

View File

@@ -94,7 +94,7 @@ namespace Interactions
}
_currentMapping = mapping;
_currentTimelineIndex = 0;
// _currentTimelineIndex = 0;
return await PlayTimelineSequence(player, follower);
}
@@ -155,7 +155,7 @@ namespace Interactions
if (timelineAsset == null)
{
Debug.LogWarning("[InteractionTimelineAction] Timeline asset is null");
return true;
return true; // Return true to continue the interaction flow
}
// Set the timeline asset
@@ -205,14 +205,26 @@ namespace Interactions
// 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));
// Wait for the timeline to complete (or timeout)
// 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;
@@ -220,10 +232,13 @@ namespace Interactions
private void OnPlayableDirectorStopped(PlayableDirector director)
{
if (director == playableDirector && _currentPlaybackTCS != null)
{
_currentPlaybackTCS.TrySetResult(true);
}
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)

View File

@@ -268,25 +268,22 @@ public class FollowerController: MonoBehaviour
}
}
// Command follower to go to a specific point (pathfinding mode)
/// <summary>
/// Command follower to go to a specific point (pathfinding mode).
/// Make the follower move to a specific point only. Will not automatically return.
/// </summary>
/// <param name="worldPosition">The world position to move to.</param>
public void GoToPoint(Vector2 worldPosition)
/// <param name="targetPosition">The position to move to.</param>
public void GoToPoint(Vector2 targetPosition)
{
_isManualFollowing = false;
if (_pickupCoroutine != null)
StopCoroutine(_pickupCoroutine);
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = new Vector3(worldPosition.x, worldPosition.y, 0);
}
_pickupCoroutine = StartCoroutine(GoToPointSequence(targetPosition));
}
// Command follower to go to a specific point and return to player
/// <summary>
/// Command follower to go to a specific point and return to player.
/// Command follower to go to a specific point and return to player after a brief delay.
/// Legacy method that combines GoToPoint and ReturnToPlayer for backward compatibility.
/// </summary>
/// <param name="itemPosition">The position of the item to pick up.</param>
/// <param name="playerTransform">The transform of the player.</param>
@@ -299,6 +296,19 @@ public class FollowerController: MonoBehaviour
_pickupCoroutine = StartCoroutine(PickupSequence(itemPosition, playerTransform));
}
/// <summary>
/// Make the follower return to the player after it has reached a point.
/// </summary>
/// <param name="playerTransform">The transform of the player to return to.</param>
public void ReturnToPlayer(Transform playerTransform)
{
if (_pickupCoroutine != null)
StopCoroutine(_pickupCoroutine);
if (_aiPath != null)
_aiPath.maxSpeed = _followerMaxSpeed;
_pickupCoroutine = StartCoroutine(ReturnToPlayerSequence(playerTransform));
}
private System.Collections.IEnumerator PickupSequence(Vector2 itemPosition, Transform playerTransform)
{
_isManualFollowing = false;
@@ -340,6 +350,63 @@ public class FollowerController: MonoBehaviour
_aiPath.enabled = false;
_pickupCoroutine = null;
}
private System.Collections.IEnumerator GoToPointSequence(Vector2 targetPosition)
{
_isManualFollowing = false;
_isReturningToPlayer = false;
if (_aiPath != null)
{
_aiPath.enabled = true;
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = new Vector3(targetPosition.x, targetPosition.y, 0);
}
// Wait until follower reaches target
while (Vector2.Distance(new Vector2(transform.position.x, transform.position.y),
new Vector2(targetPosition.x, targetPosition.y)) > GameManager.Instance.StopThreshold)
{
yield return null;
}
// Signal arrival
OnPickupArrived?.Invoke();
_pickupCoroutine = null;
}
private System.Collections.IEnumerator ReturnToPlayerSequence(Transform playerTransform)
{
if (_aiPath != null && playerTransform != null)
{
_aiPath.maxSpeed = _followerMaxSpeed;
_aiPath.destination = playerTransform.position;
}
_isReturningToPlayer = true;
// Wait until follower returns to player
while (playerTransform != null &&
Vector2.Distance(new Vector2(transform.position.x, transform.position.y),
new Vector2(playerTransform.position.x, playerTransform.position.y)) > GameManager.Instance.StopThreshold)
{
yield return null;
}
_isReturningToPlayer = false;
OnPickupReturned?.Invoke();
// Reset follower speed to normal after pickup
_followerMaxSpeed = _defaultFollowerMaxSpeed;
if (_aiPath != null)
_aiPath.maxSpeed = _followerMaxSpeed;
_isManualFollowing = true;
if (_aiPath != null)
_aiPath.enabled = false;
_pickupCoroutine = null;
}
#endregion Movement
#region ItemInteractions