CinemachineCore.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. using UnityEngine;
  2. using System.Collections.Generic;
  3. using UnityEngine.Assertions;
  4. namespace Cinemachine
  5. {
  6. internal static class Documentation
  7. {
  8. /// <summary>This must be used like
  9. /// [HelpURL(Documentation.BaseURL + "api/some-page.html")]
  10. /// or
  11. /// [HelpURL(Documentation.BaseURL + "manual/some-page.html")]
  12. /// It cannot support String.Format nor string interpolation</summary>
  13. public const string BaseURL = "https://docs.unity3d.com/Packages/com.unity.cinemachine@2.9/";
  14. }
  15. /// <summary>A singleton that manages complete lists of CinemachineBrain and,
  16. /// Cinemachine Virtual Cameras, and the priority queue. Provides
  17. /// services to keeping track of whether Cinemachine Virtual Cameras have
  18. /// been updated each frame.</summary>
  19. public sealed class CinemachineCore
  20. {
  21. /// <summary>Data version string. Used to upgrade from legacy projects</summary>
  22. public static readonly int kStreamingVersion = 20170927;
  23. /// <summary>
  24. /// Stages in the Cinemachine Component pipeline, used for
  25. /// UI organization>. This enum defines the pipeline order.
  26. /// </summary>
  27. public enum Stage
  28. {
  29. /// <summary>Second stage: position the camera in space</summary>
  30. Body,
  31. /// <summary>Third stage: orient the camera to point at the target</summary>
  32. Aim,
  33. /// <summary>Final pipeline stage: apply noise (this is done separately, in the
  34. /// Correction channel of the CameraState)</summary>
  35. Noise,
  36. /// <summary>Post-correction stage. This is invoked on all virtual camera
  37. /// types, after the pipeline is complete</summary>
  38. Finalize
  39. };
  40. private static CinemachineCore sInstance = null;
  41. /// <summary>Get the singleton instance</summary>
  42. public static CinemachineCore Instance
  43. {
  44. get
  45. {
  46. if (sInstance == null)
  47. sInstance = new CinemachineCore();
  48. return sInstance;
  49. }
  50. }
  51. /// <summary>
  52. /// If true, show hidden Cinemachine objects, to make manual script mapping possible.
  53. /// </summary>
  54. public static bool sShowHiddenObjects = false;
  55. /// <summary>Delegate for overriding Unity's default input system. Returns the value
  56. /// of the named axis.</summary>
  57. public delegate float AxisInputDelegate(string axisName);
  58. /// <summary>Delegate for overriding Unity's default input system.
  59. /// If you set this, then your delegate will be called instead of
  60. /// System.Input.GetAxis(axisName) whenever in-game user input is needed.</summary>
  61. #if ENABLE_LEGACY_INPUT_MANAGER
  62. public static AxisInputDelegate GetInputAxis = UnityEngine.Input.GetAxis;
  63. #else
  64. public static AxisInputDelegate GetInputAxis = delegate { return 0; };
  65. #endif
  66. /// <summary>
  67. /// If non-negative, cinemachine will update with this uniform delta time.
  68. /// Usage is for timelines in manual update mode.
  69. /// </summary>
  70. public static float UniformDeltaTimeOverride = -1;
  71. /// <summary>
  72. /// Replacement for Time.deltaTime, taking UniformDeltaTimeOverride into account.
  73. /// </summary>
  74. public static float DeltaTime => UniformDeltaTimeOverride >= 0 ? UniformDeltaTimeOverride : Time.deltaTime;
  75. /// <summary>
  76. /// If non-negative, cinemachine willuse this value whenever it wants current game time.
  77. /// Usage is for master timelines in manual update mode, for deterministic behaviour.
  78. /// </summary>
  79. public static float CurrentTimeOverride = -1;
  80. /// <summary>
  81. /// Replacement for Time.time, taking CurrentTimeTimeOverride into account.
  82. /// </summary>
  83. public static float CurrentTime => CurrentTimeOverride >= 0 ? CurrentTimeOverride : Time.time;
  84. /// <summary>
  85. /// Delegate for overriding a blend that is about to be applied to a transition.
  86. /// A handler can either return the default blend, or a new blend specific to
  87. /// current conditions.
  88. /// </summary>
  89. /// <param name="fromVcam">The outgoing virtual camera</param>
  90. /// <param name="toVcam">Yhe incoming virtual camera</param>
  91. /// <param name="defaultBlend">The blend that would normally be applied</param>
  92. /// <param name="owner">The context in which the blend is taking place.
  93. /// Can be a CinemachineBrain, or CinemachineStateDrivenCamera, or other manager
  94. /// object that can initiate a blend</param>
  95. /// <returns>The blend definition to use for this transition.</returns>
  96. public delegate CinemachineBlendDefinition GetBlendOverrideDelegate(
  97. ICinemachineCamera fromVcam, ICinemachineCamera toVcam,
  98. CinemachineBlendDefinition defaultBlend,
  99. MonoBehaviour owner);
  100. /// <summary>
  101. /// Delegate for overriding a blend that is about to be applied to a transition.
  102. /// A handler can either return the default blend, or a new blend specific to
  103. /// current conditions.
  104. /// </summary>
  105. public static GetBlendOverrideDelegate GetBlendOverride;
  106. /// <summary>This event will fire after a brain updates its Camera</summary>
  107. public static CinemachineBrain.BrainEvent CameraUpdatedEvent = new CinemachineBrain.BrainEvent();
  108. /// <summary>This event will fire after a brain updates its Camera</summary>
  109. public static CinemachineBrain.BrainEvent CameraCutEvent = new CinemachineBrain.BrainEvent();
  110. /// <summary>List of all active CinemachineBrains.</summary>
  111. private List<CinemachineBrain> mActiveBrains = new List<CinemachineBrain>();
  112. /// <summary>Access the array of active CinemachineBrains in the scene</summary>
  113. public int BrainCount { get { return mActiveBrains.Count; } }
  114. /// <summary>Enables frame delta compensation for not updated frames. False is useful for deterministic test results. </summary>
  115. internal static bool FrameDeltaCompensationEnabled = true;
  116. /// <summary>Access the array of active CinemachineBrains in the scene
  117. /// without generating garbage</summary>
  118. /// <param name="index">Index of the brain to access, range 0-BrainCount</param>
  119. /// <returns>The brain at the specified index</returns>
  120. public CinemachineBrain GetActiveBrain(int index)
  121. {
  122. return mActiveBrains[index];
  123. }
  124. /// <summary>Called when a CinemachineBrain is enabled.</summary>
  125. internal void AddActiveBrain(CinemachineBrain brain)
  126. {
  127. // First remove it, just in case it's being added twice
  128. RemoveActiveBrain(brain);
  129. mActiveBrains.Insert(0, brain);
  130. }
  131. /// <summary>Called when a CinemachineBrain is disabled.</summary>
  132. internal void RemoveActiveBrain(CinemachineBrain brain)
  133. {
  134. mActiveBrains.Remove(brain);
  135. }
  136. /// <summary>List of all active ICinemachineCameras.</summary>
  137. private List<CinemachineVirtualCameraBase> mActiveCameras = new List<CinemachineVirtualCameraBase>();
  138. private bool m_ActiveCamerasAreSorted;
  139. private int m_ActivationSequence;
  140. /// <summary>
  141. /// List of all active Cinemachine Virtual Cameras for all brains.
  142. /// This list is kept sorted by priority.
  143. /// </summary>
  144. public int VirtualCameraCount { get { return mActiveCameras.Count; } }
  145. /// <summary>Access the priority-sorted array of active ICinemachineCamera in the scene
  146. /// without generating garbage</summary>
  147. /// <param name="index">Index of the camera to access, range 0-VirtualCameraCount</param>
  148. /// <returns>The virtual camera at the specified index</returns>
  149. public CinemachineVirtualCameraBase GetVirtualCamera(int index)
  150. {
  151. if (!m_ActiveCamerasAreSorted && mActiveCameras.Count > 1)
  152. {
  153. mActiveCameras.Sort((x, y) =>
  154. x.Priority == y.Priority ? y.m_ActivationId.CompareTo(x.m_ActivationId) : y.Priority.CompareTo(x.Priority));
  155. m_ActiveCamerasAreSorted = true;
  156. }
  157. return mActiveCameras[index];
  158. }
  159. /// <summary>Called when a Cinemachine Virtual Camera is enabled.</summary>
  160. internal void AddActiveCamera(CinemachineVirtualCameraBase vcam)
  161. {
  162. Assert.IsFalse(mActiveCameras.Contains(vcam));
  163. vcam.m_ActivationId = m_ActivationSequence++;
  164. mActiveCameras.Add(vcam);
  165. m_ActiveCamerasAreSorted = false;
  166. }
  167. /// <summary>Called when a Cinemachine Virtual Camera is disabled.</summary>
  168. internal void RemoveActiveCamera(CinemachineVirtualCameraBase vcam)
  169. {
  170. if (mActiveCameras.Contains(vcam))
  171. mActiveCameras.Remove(vcam);
  172. }
  173. /// <summary>Called when a Cinemachine Virtual Camera is destroyed.</summary>
  174. internal void CameraDestroyed(CinemachineVirtualCameraBase vcam)
  175. {
  176. if (mActiveCameras.Contains(vcam))
  177. mActiveCameras.Remove(vcam);
  178. if (mUpdateStatus != null && mUpdateStatus.ContainsKey(vcam))
  179. mUpdateStatus.Remove(vcam);
  180. }
  181. // Registry of all vcams that are present, active or not
  182. private List<List<CinemachineVirtualCameraBase>> mAllCameras
  183. = new List<List<CinemachineVirtualCameraBase>>();
  184. /// <summary>Called when a vcam is enabled.</summary>
  185. internal void CameraEnabled(CinemachineVirtualCameraBase vcam)
  186. {
  187. int parentLevel = 0;
  188. for (ICinemachineCamera p = vcam.ParentCamera; p != null; p = p.ParentCamera)
  189. ++parentLevel;
  190. while (mAllCameras.Count <= parentLevel)
  191. mAllCameras.Add(new List<CinemachineVirtualCameraBase>());
  192. mAllCameras[parentLevel].Add(vcam);
  193. }
  194. /// <summary>Called when a vcam is disabled.</summary>
  195. internal void CameraDisabled(CinemachineVirtualCameraBase vcam)
  196. {
  197. for (int i = 0; i < mAllCameras.Count; ++i)
  198. mAllCameras[i].Remove(vcam);
  199. if (mRoundRobinVcamLastFrame == vcam)
  200. mRoundRobinVcamLastFrame = null;
  201. }
  202. CinemachineVirtualCameraBase mRoundRobinVcamLastFrame = null;
  203. static float s_LastUpdateTime;
  204. static int s_FixedFrameCount; // Current fixed frame count
  205. /// <summary>Update all the active vcams in the scene, in the correct dependency order.</summary>
  206. internal void UpdateAllActiveVirtualCameras(int layerMask, Vector3 worldUp, float deltaTime)
  207. {
  208. // Setup for roundRobin standby updating
  209. var filter = m_CurrentUpdateFilter;
  210. bool canUpdateStandby = (filter != UpdateFilter.SmartFixed); // never in smart fixed
  211. CinemachineVirtualCameraBase currentRoundRobin = mRoundRobinVcamLastFrame;
  212. // Update the fixed frame count
  213. float now = CinemachineCore.CurrentTime;
  214. if (now != s_LastUpdateTime)
  215. {
  216. s_LastUpdateTime = now;
  217. if ((filter & ~UpdateFilter.Smart) == UpdateFilter.Fixed)
  218. ++s_FixedFrameCount;
  219. }
  220. // Update the leaf-most cameras first
  221. for (int i = mAllCameras.Count-1; i >= 0; --i)
  222. {
  223. var sublist = mAllCameras[i];
  224. for (int j = sublist.Count - 1; j >= 0; --j)
  225. {
  226. var vcam = sublist[j];
  227. if (canUpdateStandby && vcam == mRoundRobinVcamLastFrame)
  228. currentRoundRobin = null; // update the next roundrobin candidate
  229. if (vcam == null)
  230. {
  231. sublist.RemoveAt(j);
  232. continue; // deleted
  233. }
  234. if (vcam.m_StandbyUpdate == CinemachineVirtualCameraBase.StandbyUpdateMode.Always
  235. || IsLive(vcam))
  236. {
  237. // Skip this vcam if it's not on the layer mask
  238. if (((1 << vcam.gameObject.layer) & layerMask) != 0)
  239. UpdateVirtualCamera(vcam, worldUp, deltaTime);
  240. }
  241. else if (currentRoundRobin == null
  242. && mRoundRobinVcamLastFrame != vcam
  243. && canUpdateStandby
  244. && vcam.m_StandbyUpdate != CinemachineVirtualCameraBase.StandbyUpdateMode.Never
  245. && vcam.isActiveAndEnabled)
  246. {
  247. // Do the round-robin update
  248. m_CurrentUpdateFilter &= ~UpdateFilter.Smart; // force it
  249. UpdateVirtualCamera(vcam, worldUp, deltaTime);
  250. m_CurrentUpdateFilter = filter;
  251. currentRoundRobin = vcam;
  252. }
  253. }
  254. }
  255. // Did we manage to update a roundrobin?
  256. if (canUpdateStandby)
  257. {
  258. if (currentRoundRobin == mRoundRobinVcamLastFrame)
  259. currentRoundRobin = null; // take the first candidate
  260. mRoundRobinVcamLastFrame = currentRoundRobin;
  261. }
  262. }
  263. /// <summary>
  264. /// Update a single Cinemachine Virtual Camera if and only if it
  265. /// hasn't already been updated this frame. Always update vcams via this method.
  266. /// Calling this more than once per frame for the same camera will have no effect.
  267. /// </summary>
  268. internal void UpdateVirtualCamera(
  269. CinemachineVirtualCameraBase vcam, Vector3 worldUp, float deltaTime)
  270. {
  271. if (vcam == null)
  272. return;
  273. bool isSmartUpdate = (m_CurrentUpdateFilter & UpdateFilter.Smart) == UpdateFilter.Smart;
  274. UpdateTracker.UpdateClock updateClock
  275. = (UpdateTracker.UpdateClock)(m_CurrentUpdateFilter & ~UpdateFilter.Smart);
  276. // If we're in smart update mode and the target moved, then we must examine
  277. // how the target has been moving recently in order to figure out whether to
  278. // update now
  279. if (isSmartUpdate)
  280. {
  281. Transform updateTarget = GetUpdateTarget(vcam);
  282. if (updateTarget == null)
  283. return; // vcam deleted
  284. if (UpdateTracker.GetPreferredUpdate(updateTarget) != updateClock)
  285. return; // wrong clock
  286. }
  287. // Have we already been updated this frame?
  288. if (mUpdateStatus == null)
  289. mUpdateStatus = new Dictionary<CinemachineVirtualCameraBase, UpdateStatus>();
  290. if (!mUpdateStatus.TryGetValue(vcam, out UpdateStatus status))
  291. {
  292. status = new UpdateStatus
  293. {
  294. lastUpdateDeltaTime = -2,
  295. lastUpdateMode = UpdateTracker.UpdateClock.Late,
  296. lastUpdateFrame = Time.frameCount + 2, // so that frameDelta ends up negative
  297. lastUpdateFixedFrame = s_FixedFrameCount + 2
  298. };
  299. mUpdateStatus.Add(vcam, status);
  300. }
  301. int frameDelta = (updateClock == UpdateTracker.UpdateClock.Late)
  302. ? Time.frameCount - status.lastUpdateFrame
  303. : s_FixedFrameCount - status.lastUpdateFixedFrame;
  304. if (deltaTime >= 0)
  305. {
  306. if (frameDelta == 0 && status.lastUpdateMode == updateClock
  307. && status.lastUpdateDeltaTime == deltaTime)
  308. return; // already updated
  309. if (FrameDeltaCompensationEnabled && frameDelta > 0)
  310. deltaTime *= frameDelta; // try to catch up if multiple frames
  311. }
  312. //Debug.Log((vcam.ParentCamera == null ? "" : vcam.ParentCamera.Name + ".") + vcam.Name + ": frame " + Time.frameCount + "/" + status.lastUpdateFixedFrame + ", " + CurrentUpdateFilter + ", deltaTime = " + deltaTime);
  313. vcam.InternalUpdateCameraState(worldUp, deltaTime);
  314. status.lastUpdateFrame = Time.frameCount;
  315. status.lastUpdateFixedFrame = s_FixedFrameCount;
  316. status.lastUpdateMode = updateClock;
  317. status.lastUpdateDeltaTime = deltaTime;
  318. }
  319. class UpdateStatus
  320. {
  321. public int lastUpdateFrame;
  322. public int lastUpdateFixedFrame;
  323. public UpdateTracker.UpdateClock lastUpdateMode;
  324. public float lastUpdateDeltaTime;
  325. }
  326. Dictionary<CinemachineVirtualCameraBase, UpdateStatus> mUpdateStatus;
  327. [RuntimeInitializeOnLoadMethod]
  328. static void InitializeModule()
  329. {
  330. CinemachineCore.Instance.mUpdateStatus = new Dictionary<CinemachineVirtualCameraBase, UpdateStatus>();
  331. }
  332. /// <summary>Internal use only</summary>
  333. internal enum UpdateFilter
  334. {
  335. Fixed = UpdateTracker.UpdateClock.Fixed,
  336. Late = UpdateTracker.UpdateClock.Late,
  337. Smart = 8, // meant to be or'ed with the others
  338. SmartFixed = Smart | Fixed,
  339. SmartLate = Smart | Late
  340. }
  341. internal UpdateFilter m_CurrentUpdateFilter;
  342. private static Transform GetUpdateTarget(CinemachineVirtualCameraBase vcam)
  343. {
  344. if (vcam == null || vcam.gameObject == null)
  345. return null;
  346. Transform target = vcam.LookAt;
  347. if (target != null)
  348. return target;
  349. target = vcam.Follow;
  350. if (target != null)
  351. return target;
  352. // If no target, use the vcam itself
  353. return vcam.transform;
  354. }
  355. /// <summary>Internal use only - inspector</summary>
  356. internal UpdateTracker.UpdateClock GetVcamUpdateStatus(CinemachineVirtualCameraBase vcam)
  357. {
  358. UpdateStatus status;
  359. if (mUpdateStatus == null || !mUpdateStatus.TryGetValue(vcam, out status))
  360. return UpdateTracker.UpdateClock.Late;
  361. return status.lastUpdateMode;
  362. }
  363. /// <summary>
  364. /// Is this virtual camera currently actively controlling any Camera?
  365. /// </summary>
  366. /// <param name="vcam">The virtual camea in question</param>
  367. /// <returns>True if the vcam is currently driving a Brain</returns>
  368. public bool IsLive(ICinemachineCamera vcam)
  369. {
  370. if (vcam != null)
  371. {
  372. for (int i = 0; i < BrainCount; ++i)
  373. {
  374. CinemachineBrain b = GetActiveBrain(i);
  375. if (b != null && b.IsLive(vcam))
  376. return true;
  377. }
  378. }
  379. return false;
  380. }
  381. /// <summary>
  382. /// Checks if the vcam is live as part of an outgoing blend in any active CinemachineBrain.
  383. /// Does not check whether the vcam is also the current active vcam.
  384. /// </summary>
  385. /// <param name="vcam">The virtual camera to check</param>
  386. /// <returns>True if the virtual camera is part of a live outgoing blend, false otherwise</returns>
  387. public bool IsLiveInBlend(ICinemachineCamera vcam)
  388. {
  389. if (vcam != null)
  390. {
  391. for (int i = 0; i < BrainCount; ++i)
  392. {
  393. CinemachineBrain b = GetActiveBrain(i);
  394. if (b != null && b.IsLiveInBlend(vcam))
  395. return true;
  396. }
  397. }
  398. return false;
  399. }
  400. /// <summary>
  401. /// Signal that the virtual has been activated.
  402. /// If the camera is live, then all CinemachineBrains that are showing it will
  403. /// send an activation event.
  404. /// </summary>
  405. /// <param name="vcam">The virtual camera being activated</param>
  406. /// <param name="vcamFrom">The previously-active virtual camera (may be null)</param>
  407. public void GenerateCameraActivationEvent(ICinemachineCamera vcam, ICinemachineCamera vcamFrom)
  408. {
  409. if (vcam != null)
  410. {
  411. for (int i = 0; i < BrainCount; ++i)
  412. {
  413. CinemachineBrain b = GetActiveBrain(i);
  414. if (b != null && b.IsLive(vcam))
  415. b.m_CameraActivatedEvent.Invoke(vcam, vcamFrom);
  416. }
  417. }
  418. }
  419. /// <summary>
  420. /// Signal that the virtual camera's content is discontinuous WRT the previous frame.
  421. /// If the camera is live, then all CinemachineBrains that are showing it will send a cut event.
  422. /// </summary>
  423. /// <param name="vcam">The virtual camera being cut to</param>
  424. public void GenerateCameraCutEvent(ICinemachineCamera vcam)
  425. {
  426. if (vcam != null)
  427. {
  428. for (int i = 0; i < BrainCount; ++i)
  429. {
  430. CinemachineBrain b = GetActiveBrain(i);
  431. if (b != null && b.IsLive(vcam))
  432. {
  433. if (b.m_CameraCutEvent != null)
  434. b.m_CameraCutEvent.Invoke(b);
  435. if (CameraCutEvent != null)
  436. CameraCutEvent.Invoke(b);
  437. }
  438. }
  439. }
  440. }
  441. /// <summary>
  442. /// Try to find a CinemachineBrain to associate with a
  443. /// Cinemachine Virtual Camera. The first CinemachineBrain
  444. /// in which this Cinemachine Virtual Camera is live will be used.
  445. /// If none, then the first active CinemachineBrain with the correct
  446. /// layer filter will be used.
  447. /// Brains with OutputCamera == null will not be returned.
  448. /// Final result may be null.
  449. /// </summary>
  450. /// <param name="vcam">Virtual camera whose potential brain we need.</param>
  451. /// <returns>First CinemachineBrain found that might be
  452. /// appropriate for this vcam, or null</returns>
  453. public CinemachineBrain FindPotentialTargetBrain(CinemachineVirtualCameraBase vcam)
  454. {
  455. if (vcam != null)
  456. {
  457. int numBrains = BrainCount;
  458. for (int i = 0; i < numBrains; ++i)
  459. {
  460. CinemachineBrain b = GetActiveBrain(i);
  461. if (b != null && b.OutputCamera != null && b.IsLive(vcam))
  462. return b;
  463. }
  464. int layer = 1 << vcam.gameObject.layer;
  465. for (int i = 0; i < numBrains; ++i)
  466. {
  467. CinemachineBrain b = GetActiveBrain(i);
  468. if (b != null && b.OutputCamera != null && (b.OutputCamera.cullingMask & layer) != 0)
  469. return b;
  470. }
  471. }
  472. return null;
  473. }
  474. /// <summary>Call this to notify all virtual camewras that may be tracking a target
  475. /// that the target's position has suddenly warped to somewhere else, so that
  476. /// the virtual cameras can update their internal state to make the camera
  477. /// warp seamlessy along with the target.
  478. ///
  479. /// All virtual cameras are iterated so this call will work no matter how many
  480. /// are tracking the target, and whether they are active or inactive.
  481. /// </summary>
  482. /// <param name="target">The object that was warped</param>
  483. /// <param name="positionDelta">The amount the target's position changed</param>
  484. public void OnTargetObjectWarped(Transform target, Vector3 positionDelta)
  485. {
  486. int numVcams = VirtualCameraCount;
  487. for (int i = 0; i < numVcams; ++i)
  488. GetVirtualCamera(i).OnTargetObjectWarped(target, positionDelta);
  489. }
  490. }
  491. }