Unity SDK Docs 1.5.0-beta.6
Loading...
Searching...
No Matches
Wand.cs
1/*
2 * Copyright (C) 2020-2023 Tilt Five, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17using System;
18using System.Collections.Generic;
19using UnityEngine;
21
22#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
23using UnityEngine.InputSystem;
24using UnityEngine.InputSystem.Users;
25using UnityEngine.InputSystem.Controls;
26#endif
27
28using WandButton = TiltFive.Input.WandButton;
29
30namespace TiltFive
31{
36 [System.Serializable]
38 {
39 public ControllerIndex controllerIndex;
40
41 public GameObject GripPoint;
42 public GameObject FingertipPoint;
43 public GameObject AimPoint;
44
45 // TODO: Think about some accessors for physical attributes of the wand (length, distance to tip, etc)?
46 internal WandSettings Copy()
47 {
48 return (WandSettings)MemberwiseClone();
49 }
50 }
51
55 public class Wand : Singleton<Wand>
56 {
57 #region Private Fields
58
62 private Dictionary<GlassesHandle, WandPair> wandCores = new Dictionary<GlassesHandle, WandPair>();
63
70 private static readonly Vector3 DEFAULT_WAND_POSITION_GAME_BOARD_SPACE = new Vector3(0f, 0.25f, -0.25f);
71
75 private static readonly Vector3 DEFAULT_WAND_HANDEDNESS_OFFSET_GAME_BOARD_SPACE = new Vector3(0.125f, 0f, 0f);
76
84 private static readonly Quaternion DEFAULT_WAND_ROTATION_GAME_BOARD_SPACE = Quaternion.Euler(new Vector3(-33f, 0f, 0f));
85
86
87 // Handles for Glasses that have just connected
88 private HashSet<GlassesHandle> incomingHandles = new HashSet<GlassesHandle>();
89 // Handles for Glasses that have just disconnected
90 private HashSet<GlassesHandle> lostHandles = new HashSet<GlassesHandle>();
91
92 private HashSet<WandCore> lostWands = new HashSet<WandCore>();
93
94
95 // Scan for new wands every half second.
96 private static DateTime lastScanAttempt = System.DateTime.MinValue;
97
98 // This should likely become a query into the native library.
99 private static readonly double wandScanInterval = 0.5d;
100
101 private static bool wandAvailabilityErroredOnce = false;
102
103
104 // Used to guard against certain functions executing more than once per frame
105 private static int currentFrame = -1;
106
107 #endregion Private Fields
108
109
110 #region Private Structs
111
112 private struct WandPair
113 {
114 public WandCore RightWand;
115 public WandCore LeftWand;
116
117 public WandPair(WandCore right, WandCore left)
118 {
119 RightWand = right;
120 LeftWand = left;
121 }
122
123 public WandCore this[ControllerIndex controllerIndex]
124 {
125 get
126 {
127 switch (controllerIndex)
128 {
129 case ControllerIndex.Right:
130 return RightWand;
131 case ControllerIndex.Left:
132 return LeftWand;
133 default:
134 // TODO: If we get an unexpected value here, should we fail silently or throw an exception?
135 return null;
136 }
137 }
138 }
139
140 public bool TryGet(ControllerIndex controllerIndex, out WandCore wandCore)
141 {
142 wandCore = this[controllerIndex];
143 return wandCore != null;
144 }
145 }
146
147 #endregion Private Structs
148
149
150 #region Public Functions
151
159 public static Vector3 GetPosition(
160 ControllerIndex controllerIndex = ControllerIndex.Right,
161 ControllerPosition controllerPosition = ControllerPosition.Grip,
162 PlayerIndex playerIndex = PlayerIndex.One)
163 {
164 if(!Player.TryGetGlassesHandle(playerIndex, out var glassesHandle)
165 || !Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
166 || !wandPair.TryGet(controllerIndex, out var wandCore))
167 {
168 return Vector3.zero;
169 }
170
171 switch (controllerPosition)
172 {
173 case ControllerPosition.Fingertips:
174 return wandCore.fingertipsPose_UnityWorldSpace.position;
175 case ControllerPosition.Aim:
176 return wandCore.aimPose_UnityWorldSpace.position;
177 case ControllerPosition.Grip:
178 return wandCore.Pose_UnityWorldSpace.position;
179 default:
180 return Vector3.zero;
181 }
182 }
183
190 public static Quaternion GetRotation(ControllerIndex controllerIndex = ControllerIndex.Right, PlayerIndex playerIndex = PlayerIndex.One)
191 {
192 if (!Player.TryGetGlassesHandle(playerIndex, out var glassesHandle)
193 || !Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
194 || !wandPair.TryGet(controllerIndex, out var wandCore))
195 {
196 return Quaternion.identity;
197 }
198
199 return wandCore.Pose_UnityWorldSpace.rotation;
200 }
201
202 public static bool IsTracked(ControllerIndex controllerIndex = ControllerIndex.Right, PlayerIndex playerIndex = PlayerIndex.One)
203 {
204 if (!Player.TryGetGlassesHandle(playerIndex, out var glassesHandle)
205 || !Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
206 || !wandPair.TryGet(controllerIndex, out var wandCore))
207 {
208 return false;
209 }
210
211 return wandCore.IsTracked;
212 }
213
221 public static bool TryCheckConnected(out bool connected, PlayerIndex playerIndex, ControllerIndex controllerIndex = ControllerIndex.Right)
222 {
223 if(!Player.TryGetGlassesHandle(playerIndex, out var glassesHandle)
224 || !Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
225 || !wandPair.TryGet(controllerIndex, out var wandCore))
226 {
227 connected = false;
228 return true;
229 }
230
231 connected = wandCore.IsConnected;
232 return true;
233 }
234
235#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
246 public static bool TryGetWandDevice(PlayerIndex playerIndex, ControllerIndex controllerIndex, out WandDevice wandDevice)
247 {
248 if(!Player.TryGetGlassesHandle(playerIndex, out var glassesHandle) || !Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
249 || !wandPair.TryGet(controllerIndex, out var wandCore) || !(wandCore is WandDeviceCore wandDeviceCore))
250 {
251 wandDevice = null;
252 return false;
253 }
254
255 wandDevice = wandDeviceCore.wandDevice;
256 return true;
257 }
258#endif
259
268 public static bool TrySendImpulse(float amplitude, float duration, PlayerIndex playerIndex = PlayerIndex.One, ControllerIndex controllerIndex = ControllerIndex.Right)
269 {
270 if (!Player.TryGetGlassesHandle(playerIndex, out var glassesHandle)
271 || !Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
272 || !wandPair.TryGet(controllerIndex, out var wandCore))
273 {
274 return false;
275 }
276
277 try
278 {
279 if (NativePlugin.SendImpulse(glassesHandle, controllerIndex, amplitude, (UInt16)(Mathf.Clamp(duration, 0.0f, 0.320f) * 1000)) == 0)
280 {
281 return true;
282 }
283 else
284 {
285 return false;
286 }
287 }
288 catch (DllNotFoundException e)
289 {
290 Log.Info("Tilt Five library missing. Could not connect to wand: {0}", e.Message);
291 return false;
292 }
293 catch (Exception e)
294 {
295 Log.Error(
296 "Failed to send impulse to wand: {0}",
297 e.ToString());
298 return false;
299 }
300 }
301
302 #region Deprecated Public Functions
303
304 // Update is called once per frame
305 [Obsolete("Converted to be an internal function.", true)]
306 public static void Update(WandSettings wandSettings, ScaleSettings scaleSettings, GameBoardSettings gameBoardSettings, PlayerIndex playerIndex = PlayerIndex.One)
307 {
308 Update(playerIndex, wandSettings, scaleSettings, gameBoardSettings);
309 }
310
311 [Obsolete("Converted to be an internal function.", true)]
312 public static void ScanForWands()
313 {
314 Scan();
315 }
316
317 #endregion Deprecated Public Functions
318
319 #endregion Public Functions
320
321
322 #region Internal Functions
323
324 internal static void Update(PlayerIndex playerIndex, WandSettings wandSettings, ScaleSettings scaleSettings, GameBoardSettings gameBoardSettings)
325 {
326 // Update the relevant WandCore
327 if (Player.TryGetGlassesHandle(playerIndex, out var glassesHandle))
328 {
329 Update(glassesHandle, wandSettings, scaleSettings, gameBoardSettings);
330 }
331 }
332
333 internal static void Update(GlassesHandle glassesHandle, WandSettings wandSettings, ScaleSettings scaleSettings, GameBoardSettings gameBoardSettings)
334 {
335 if (Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
336 && wandPair.TryGet(wandSettings.controllerIndex, out var wandCore))
337 {
338 wandCore.Update(wandSettings, scaleSettings, gameBoardSettings);
339 }
340 }
341
342 internal static void OnDisable()
343 {
344 foreach (var wandPair in Instance.wandCores.Values)
345 {
346 wandPair.RightWand?.Dispose();
347 wandPair.LeftWand?.Dispose();
348 }
349 Instance.wandCores.Clear();
350 }
351
352 internal static void Scan()
353 {
354 // Tell the native plugin to search for wands.
355 TryScanForWands();
356
357 // Obtain the latest set of connected glasses
358 var connectedGlassesHandles = Glasses.GetAllConnectedGlassesHandles();
359
360 // Add/Remove entries from the wandCores dictionary depending on which glasses appeared/disappeared
361 // ---------------------------------------------------
362 var wandCores = Instance.wandCores;
363 var incomingHandles = Instance.incomingHandles;
364 var lostHandles = Instance.lostHandles;
365 var lostWands = Instance.lostWands;
366
367 incomingHandles.Clear();
368 lostHandles.Clear();
369 lostWands.Clear();
370
371 // Add newly connected wands
372 for (int i = 0; i < connectedGlassesHandles.Length; i++)
373 {
374 var glassesHandle = connectedGlassesHandles[i];
375 incomingHandles.Add(glassesHandle);
376
377 var rightWandCore = Instance.ObtainWandCore(glassesHandle, ControllerIndex.Right);
378 var leftWandCore = Instance.ObtainWandCore(glassesHandle, ControllerIndex.Left);
379
380 // Obtain and store a pair of WandCores to associate with this GlassesHandle.
381 // If either wand is unavailable, we'll just store null.
382 // This will also clear any WandCores that represented a now-disconnected wand.
383 wandCores[glassesHandle] = new WandPair(rightWandCore, leftWandCore);
384 }
385
386 // Prune disconnected wands
387 foreach (var glassesHandle in wandCores.Keys)
388 {
389 // If a pair of glasses disconnects, save its handle for the next foreach loop below
390 if (!incomingHandles.Contains(glassesHandle))
391 {
392 lostHandles.Add(glassesHandle);
393 }
394 // otherwise, check if its wands are still connected.
395 else
396 {
397 if (wandCores.TryGetValue(glassesHandle, out var wandPair))
398 {
399 if (wandPair.TryGet(ControllerIndex.Left, out var leftWandCore)
400 && (!TryGetWandAvailability(out bool leftWandConnected, glassesHandle, ControllerIndex.Left) || !leftWandConnected))
401 {
402 lostWands.Add(leftWandCore);
403 }
404 if (wandPair.TryGet(ControllerIndex.Right, out var rightWandCore)
405 && (!TryGetWandAvailability(out bool rightWandConnected, glassesHandle, ControllerIndex.Right) || !rightWandConnected))
406 {
407 lostWands.Add(rightWandCore);
408 }
409 }
410 }
411 }
412
413 foreach (var lostHandle in lostHandles)
414 {
415 var lostWandPair = wandCores[lostHandle];
416
417 if (lostWandPair.TryGet(ControllerIndex.Right, out var lostRightWand))
418 {
419 lostWands.Add(lostRightWand);
420 }
421
422 if (lostWandPair.TryGet(ControllerIndex.Left, out var lostLeftWand))
423 {
424 lostWands.Add(lostLeftWand);
425 }
426 wandCores.Remove(lostHandle);
427 }
428
429 foreach (var lostWand in lostWands)
430 {
431 lostWand.Dispose();
432 }
433 }
434
435 internal static void GetLatestInputs()
436 {
437 // If the previous/current wand states are shuffled more than once per frame,
438 // our button rising/falling edge detection breaks.
439 // Detect this scenario and return early to prevent this.
440 if (Time.frameCount == currentFrame)
441 {
442 return;
443 }
444 currentFrame = Time.frameCount;
445
446 foreach (var wandPair in Instance.wandCores.Values)
447 {
448 wandPair.RightWand?.GetLatestInputs();
449 wandPair.LeftWand?.GetLatestInputs();
450 }
451 }
452
453 internal static bool GetButton(WandButton button, GlassesHandle glassesHandle,
454 ControllerIndex controllerIndex = ControllerIndex.Right)
455 {
456 if(!Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
457 || !wandPair.TryGet(controllerIndex, out var wandCore))
458 {
459 return false;
460 }
461 return wandCore.GetButton(button);
462 }
463
464 internal static bool TryGetButton(WandButton button, out bool pressed,
465 GlassesHandle glassesHandle, ControllerIndex controllerIndex = ControllerIndex.Right)
466 {
467 if (!Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
468 || !wandPair.TryGet(controllerIndex, out var wandCore))
469 {
470 pressed = false;
471 return false;
472 }
473
474 return wandCore.TryGetButton(button, out pressed);
475 }
476
477 internal static bool GetButtonDown(WandButton button,
478 GlassesHandle glassesHandle, ControllerIndex controllerIndex = ControllerIndex.Right)
479 {
480 if (!Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
481 || !wandPair.TryGet(controllerIndex, out var wandCore))
482 {
483 return false;
484 }
485
486 return wandCore.GetButtonDown(button);
487 }
488
489 internal static bool TryGetButtonDown(WandButton button, out bool buttonDown,
490 GlassesHandle glassesHandle, ControllerIndex controllerIndex = ControllerIndex.Right)
491 {
492 if (!Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
493 || !wandPair.TryGet(controllerIndex, out var wandCore))
494 {
495 buttonDown = false;
496 return false;
497 }
498
499 return wandCore.TryGetButtonDown(button, out buttonDown);
500 }
501
502 internal static bool GetButtonUp(WandButton button,
503 GlassesHandle glassesHandle, ControllerIndex controllerIndex = ControllerIndex.Right)
504 {
505 if (Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
506 || !wandPair.TryGet(controllerIndex, out var wandCore))
507 {
508 // TODO: Handle corner case in which wandCore existed during the previous frame, and the button was pressed
509 // We'd want to return true in that scenario.
510 return false;
511 }
512
513 return wandCore.GetButtonUp(button);
514 }
515
516 internal static bool TryGetButtonUp(WandButton button, out bool buttonUp,
517 GlassesHandle glassesHandle, ControllerIndex controllerIndex = ControllerIndex.Right)
518 {
519 if (!Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
520 || !wandPair.TryGet(controllerIndex, out var wandCore))
521 {
522 // TODO: Handle corner case in which wandCore existed during the previous frame, and the button was pressed.
523 // We'd want to return true in that scenario.
524 buttonUp = false;
525 return false;
526 }
527
528 return wandCore.TryGetButtonUp(button, out buttonUp);
529 }
530
531 internal static Vector2 GetStickTilt(GlassesHandle glassesHandle, ControllerIndex controllerIndex = ControllerIndex.Right)
532 {
533 if (Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
534 || !wandPair.TryGet(controllerIndex, out var wandCore))
535 {
536 return Vector2.zero;
537 }
538
539 return wandCore.GetStickTilt();
540 }
541
542 internal static bool TryGetStickTilt(out Vector2 stickTilt,
543 GlassesHandle glassesHandle, ControllerIndex controllerIndex = ControllerIndex.Right)
544 {
545 if (!Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
546 || !wandPair.TryGet(controllerIndex, out var wandCore))
547 {
548 stickTilt = Vector2.zero;
549 return false;
550 }
551
552 return wandCore.TryGetStickTilt(out stickTilt);
553 }
554
555 internal static float GetTrigger(GlassesHandle glassesHandle, ControllerIndex controllerIndex = ControllerIndex.Right)
556 {
557 if (!Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
558 || !wandPair.TryGet(controllerIndex, out var wandCore))
559 {
560 return 0f;
561 }
562
563 return wandCore.GetTrigger();
564 }
565
566 internal static bool TryGetTrigger(out float triggerDisplacement,
567 GlassesHandle glassesHandle, ControllerIndex controllerIndex = ControllerIndex.Right)
568 {
569 if (!Instance.wandCores.TryGetValue(glassesHandle, out var wandPair)
570 || !wandPair.TryGet(controllerIndex, out var wandCore))
571 {
572 triggerDisplacement = 0f;
573 return false;
574 }
575
576 return wandCore.TryGetTrigger(out triggerDisplacement);
577 }
578
579 internal static bool TryGetWandControlsState(GlassesHandle glassesHandle, out T5_ControllerState? controllerState,
580 ControllerIndex controllerIndex = ControllerIndex.Right)
581 {
582 int result = NativePlugin.T5_RESULT_UNKNOWN_ERROR;
583
584 try
585 {
586 T5_ControllerState state = new T5_ControllerState();
587 result = NativePlugin.GetControllerState(glassesHandle, controllerIndex, ref state);
588 controllerState = (result == NativePlugin.T5_RESULT_SUCCESS)
589 ? state
590 : (T5_ControllerState?)null;
591 }
592 catch (Exception e)
593 {
594 controllerState = null;
595 Log.Error(e.Message);
596 }
597
598 return result == NativePlugin.T5_RESULT_SUCCESS;
599 }
600
601 #endregion Internal Functions
602
603
604 #region Private Functions
605
606 private static bool TryScanForWands()
607 {
608 var currentTime = System.DateTime.Now;
609 var timeSinceLastScan = currentTime - lastScanAttempt;
610
611 // Scan for wands if the scan interval has elapsed to catch newly connected wands.
612 if (timeSinceLastScan.TotalSeconds >= wandScanInterval)
613 {
614 int result = NativePlugin.T5_RESULT_UNKNOWN_ERROR;
615
616 try
617 {
618 result = NativePlugin.ScanForWands();
619 }
620 catch (System.DllNotFoundException e)
621 {
622 Log.Info(
623 "Could not connect to Tilt Five plugin to scan for wands: {0}",
624 e);
625 }
626 catch (Exception e)
627 {
628 Log.Error(e.Message);
629 }
630
631 lastScanAttempt = currentTime;
632 return (result == NativePlugin.T5_RESULT_SUCCESS);
633 }
634
635 return false;
636 }
637
638 private static bool TryGetWandAvailability(out bool connected, GlassesHandle glassesHandle, ControllerIndex controllerIndex)
639 {
640 if (!wandAvailabilityErroredOnce)
641 {
642 try
643 {
644 T5_Bool wandAvailable = false;
645 int result = NativePlugin.GetWandAvailability(glassesHandle, ref wandAvailable, controllerIndex);
646
647 if (result == NativePlugin.T5_RESULT_SUCCESS)
648 {
649 connected = wandAvailable;
650 return true;
651 }
652 }
653 catch (DllNotFoundException e)
654 {
655 Log.Info("Could not connect to Tilt Five plugin for wand: {0}", e.Message);
656 wandAvailabilityErroredOnce = true;
657 }
658 catch (Exception e)
659 {
660 Log.Error(
661 "Failed to connect to Tilt Five plugin for wand availability: {0}",
662 e.ToString());
663 wandAvailabilityErroredOnce = true;
664 }
665 }
666
667 connected = false;
668 return false;
669 }
670
671 private WandCore ObtainWandCore(GlassesHandle glassesHandle, ControllerIndex controllerIndex)
672 {
673 WandCore wandCore = null;
674 var glassesAlreadyMonitored = wandCores.TryGetValue(glassesHandle, out var wandPair);
675
676 // Ask the native plugin whether this wand is currently connected.
677 if (TryGetWandAvailability(out var wandConnected, glassesHandle, controllerIndex) && wandConnected)
678 {
679 // If we're not already monitoring this wand, go ahead and create a corresponding WandCore.
680 if (!glassesAlreadyMonitored || !wandPair.TryGet(controllerIndex, out wandCore))
681 {
682#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
683 wandCore = new WandDeviceCore(glassesHandle, controllerIndex);
684#else
685 wandCore = new WandCore(glassesHandle, controllerIndex);
686#endif
687 }
688 return wandCore;
689 }
690 // If this wand is disconnected and we were previously monitoring it, mark it as disconnected
691 else if(glassesAlreadyMonitored && wandPair.TryGet(controllerIndex, out var lostWandCore))
692 {
693 lostWands.Add(lostWandCore);
694 }
695
696 return wandCore;
697 }
698
699 #endregion Private Functions
700
701
702 #region Private Classes
703
707 private class WandCore : TrackableCore<WandSettings, T5_ControllerState>, IDisposable
708 {
709 #region Public Fields
710
711 public readonly GlassesHandle glassesHandle;
712 public readonly ControllerIndex controllerIndex;
713
714 public Pose fingertipsPose_GameboardSpace = new Pose(DEFAULT_WAND_POSITION_GAME_BOARD_SPACE, Quaternion.identity);
715 public Pose aimPose_GameboardSpace = new Pose(DEFAULT_WAND_POSITION_GAME_BOARD_SPACE, Quaternion.identity);
716
717 public Pose gripPose_UnityWorldSpace => pose_UWRLD;
718 public Pose fingertipsPose_UnityWorldSpace;
719 public Pose aimPose_UnityWorldSpace;
720
721 protected T5_ControllerState? currentState;
722 protected T5_ControllerState? previousState;
723
724 #endregion Public Fields
725
726
727 #region Public Functions
728
729 public WandCore(GlassesHandle glassesHandle, ControllerIndex controllerIndex)
730 {
731 this.glassesHandle = glassesHandle;
732 this.controllerIndex = controllerIndex;
733
734 Glasses.TryGetFriendlyName(glassesHandle, out var friendlyName);
735 Log.Info($"Glasses {glassesHandle} (\"{friendlyName}\") {Enum.GetName(typeof(ControllerIndex), controllerIndex)} Wand connected");
736 }
737
738 public virtual void GetLatestInputs()
739 {
740 previousState = currentState;
741
742 try
743 {
744 T5_ControllerState state = new T5_ControllerState();
745
746 var result = NativePlugin.GetControllerState(glassesHandle, controllerIndex, ref state);
747
748 currentState = (result == NativePlugin.T5_RESULT_SUCCESS)
749 ? state
750 : (T5_ControllerState?)null;
751 }
752 catch (Exception e)
753 {
754 currentState = null;
755 Log.Error(e.Message);
756 }
757 }
758
759 public bool GetButton(WandButton button)
760 {
761 // If the wand isn't connected, GetButton() should return a default value of false.
762 return currentState?.GetButton(button) ?? false;
763 }
764
765 public bool TryGetButton(WandButton button, out bool pressed)
766 {
767 pressed = currentState?.GetButton(button) ?? false;
768
769 // If the wand isn't connected, TryGetButton() should fail.
770 return currentState.HasValue;
771 }
772
773 public bool GetButtonDown(WandButton button)
774 {
775
776 // If the current wand state is null, the wand isn't connected.
777 // If so, let the application assume the user isn't pressing the button currently.
778 var pressed = currentState?.GetButton(button) ?? false;
779
780 // If the previous wand state is null, the wand wasn't connected.
781 // If so, let the application assume the user wasn't pressing the button last frame.
782 var previouslyPressed = previousState?.GetButton(button) ?? false;
783
784 // The wand could potentially connect while the user is holding a button, so just report the button state.
785 if (!previousState.HasValue && currentState.HasValue)
786 {
787 return pressed;
788 }
789 // Return true if the button is currently pressed, but was unpressed on the previous frame.
790 return pressed && !previouslyPressed;
791 }
792
793 public bool TryGetButtonDown(WandButton button, out bool buttonDown)
794 {
795 // Even if this operation fails, give buttonDown a default value.
796 buttonDown = GetButtonDown(button);
797 return currentState.HasValue;
798 }
799
800 public bool GetButtonUp(WandButton button)
801 {
802 // If the current wand state is null, the wand isn't connected.
803 // If so, let the application assume the user isn't pressing the button currently.
804 var pressed = currentState?.GetButton(button) ?? false;
805
806 // If the previous wand state is null, the wand wasn't connected.
807 // If so, let the application assume the user wasn't pressing the button last frame.
808 var previouslyPressed = previousState?.GetButton(button) ?? false;
809
810 // Return true if the button is currently released, but was pressed on the previous frame.
811 return previousState.HasValue
812 ? !pressed && previouslyPressed
813 // If the current state exists but the previous state was null, the wand has just connected.
814 // There's no way for the button to be pressed during the previous frame,
815 // so there's no way for the button to have been released this frame. Always return false.
816 : false;
817 }
818
819 public bool TryGetButtonUp(WandButton button, out bool buttonUp)
820 {
821 // Even if this operation fails, give buttonUp a default value.
822 buttonUp = GetButtonUp(button);
823 return currentState.HasValue;
824 }
825
826 public Vector2 GetStickTilt()
827 {
828 return currentState?.Stick ?? Vector2.zero;
829 }
830
831 public bool TryGetStickTilt(out Vector2 stickTilt)
832 {
833 stickTilt = GetStickTilt();
834 return currentState.HasValue;
835 }
836
837 public float GetTrigger()
838 {
839 return currentState?.Trigger ?? 0.0f;
840 }
841
842 public bool TryGetTrigger(out float triggerDisplacement)
843 {
844 triggerDisplacement = GetTrigger();
845 return currentState.HasValue;
846 }
847
848 #endregion Public Functions
849
850
851 #region Overrides
852
853 public new void Reset(WandSettings wandSettings)
854 {
855 base.Reset(wandSettings);
856 }
857
858 public new virtual void Update(WandSettings wandSettings, ScaleSettings scaleSettings, GameBoardSettings gameBoardSettings)
859 {
860 if (wandSettings == null)
861 {
862 Log.Error("WandSettings configuration required for Wand tracking updates.");
863 return;
864 }
865
866 base.Update(wandSettings, scaleSettings, gameBoardSettings);
867 }
868
869 protected override void SetDefaultPoseGameboardSpace(WandSettings settings)
870 {
871 pose_USTAGE = GetDefaultPoseGameboardSpace(settings);
872 // We don't have a good offset that we can use for default fingertips/aim poses, so just use the default pose for everything
873 fingertipsPose_GameboardSpace = pose_USTAGE;
874 aimPose_GameboardSpace = pose_USTAGE;
875 }
876
877 protected static Pose GetDefaultPoseGameboardSpace(WandSettings settings)
878 {
879 Vector3 defaultPosition = DEFAULT_WAND_POSITION_GAME_BOARD_SPACE;
880
881 defaultPosition += DEFAULT_WAND_HANDEDNESS_OFFSET_GAME_BOARD_SPACE
882 * (settings.controllerIndex == ControllerIndex.Right ? 1f : -1f);
883 return new Pose(defaultPosition, DEFAULT_WAND_ROTATION_GAME_BOARD_SPACE);
884 }
885
886 protected override void SetPoseUnityWorldSpace(ScaleSettings scaleSettings, GameBoardSettings gameBoardSettings)
887 {
888 pose_UWRLD = GameboardToWorldSpace(pose_USTAGE, scaleSettings, gameBoardSettings);
889 fingertipsPose_UnityWorldSpace = GameboardToWorldSpace(fingertipsPose_GameboardSpace, scaleSettings, gameBoardSettings);
890 aimPose_UnityWorldSpace = GameboardToWorldSpace(aimPose_GameboardSpace, scaleSettings, gameBoardSettings);
891 }
892
893 protected override bool TryCheckConnected(out bool connected)
894 {
895 if (!wandAvailabilityErroredOnce)
896 {
897 try
898 {
899 T5_Bool wandAvailable = false;
900 int result = NativePlugin.GetWandAvailability(glassesHandle, ref wandAvailable, controllerIndex);
901
902 if (result == NativePlugin.T5_RESULT_SUCCESS)
903 {
904 isConnected = wandAvailable;
905 connected = wandAvailable;
906 return true;
907 }
908 }
909 catch (DllNotFoundException e)
910 {
911 Log.Info("Could not connect to Tilt Five plugin for wand: {0}", e.Message);
912 wandAvailabilityErroredOnce = true;
913 }
914 catch (Exception e)
915 {
916 Log.Error(
917 "Failed to connect to Tilt Five plugin for wand availability: {0}",
918 e.ToString());
919 wandAvailabilityErroredOnce = true;
920 }
921 }
922
923 isConnected = false;
924 connected = false;
925 return false;
926 }
927
928 protected override bool TryGetStateFromPlugin(out T5_ControllerState controllerState, out bool poseIsValid)
929 {
930 if (!TryGetWandControlsState(glassesHandle, out var controllerStateResult, controllerIndex))
931 {
932 poseIsValid = false;
933 controllerState = new T5_ControllerState();
934
935 return false;
936 }
937
938 controllerState = controllerStateResult.Value;
939 poseIsValid = Glasses.IsTracked(glassesHandle) && controllerState.PoseValid;
940
941 return true;
942 }
943
944 protected override void SetPoseGameboardSpace(in T5_ControllerState controllerState,
945 WandSettings wandSettings, ScaleSettings scaleSettings, GameBoardSettings gameboardSettings)
946 {
947 // Unity reference frames:
948 //
949 // UWND - Unity WaND local space.
950 // +x right, +y up, +z forward
951 // USTAGE - Unity Stage space.
952 // +x right, +y up, +z forward
953 //
954 // Tilt Five reference frames:
955 //
956 // DW - Our right-handed version of Unity's default wand space.
957 // +x right, +y down, +z forward
958 // STAGE - Stage space.
959 // +x right, +y forward, +z up
960
961 Vector3 gripPosition_USTAGE = ConvertPosSTAGEToUSTAGE(controllerState.GripPos_STAGE);
962 Vector3 fingertipsPosition_USTAGE = ConvertPosSTAGEToUSTAGE(controllerState.FingertipsPos_STAGE);
963 Vector3 aimPosition_USTAGE = ConvertPosSTAGEToUSTAGE(controllerState.AimPos_STAGE);
964 var rotation_USTAGE = CalculateRotation(controllerState.RotToWND_STAGE);
965
966 ProcessTrackingData(gripPosition_USTAGE, fingertipsPosition_USTAGE, aimPosition_USTAGE,
967 rotation_USTAGE, wandSettings, scaleSettings, gameboardSettings,
968 out pose_USTAGE, out fingertipsPose_GameboardSpace, out aimPose_GameboardSpace);
969 }
970
978 protected override void SetInvalidPoseGameboardSpace(in T5_ControllerState t5_ControllerState, WandSettings settings, ScaleSettings scaleSettings, GameBoardSettings gameboardSettings)
979 {
980 SetPoseGameboardSpace(t5_ControllerState, settings, scaleSettings, gameboardSettings);
981 }
982
983 protected override void SetDrivenObjectTransform(WandSettings wandSettings, ScaleSettings scaleSettings, GameBoardSettings gameBoardSettings)
984 {
985 if (wandSettings.GripPoint != null)
986 {
987 wandSettings.GripPoint.transform.SetPositionAndRotation(gripPose_UnityWorldSpace.position, gripPose_UnityWorldSpace.rotation);
988 }
989
990 if (wandSettings.FingertipPoint != null)
991 {
992 wandSettings.FingertipPoint.transform.SetPositionAndRotation(fingertipsPose_UnityWorldSpace.position, fingertipsPose_UnityWorldSpace.rotation);
993 }
994
995 if (wandSettings.AimPoint != null)
996 {
997 wandSettings.AimPoint.transform.SetPositionAndRotation(aimPose_UnityWorldSpace.position, aimPose_UnityWorldSpace.rotation);
998 }
999 }
1000
1001 public virtual void Dispose()
1002 {
1003 Log.Info($"Glasses {glassesHandle} {controllerIndex} Wand disconnected");
1004 }
1005
1006 #endregion Overrides
1007
1008
1009 #region Private Helper Functions
1010
1011 protected Quaternion CalculateRotation(Quaternion rotToWND_STAGE)
1012 {
1013 Quaternion rotToDW_STAGE = Quaternion.AngleAxis(90f, Vector3.right);
1014 Quaternion rotToSTAGE_DW = Quaternion.Inverse(rotToDW_STAGE);
1015 Quaternion rotToWND_DW = rotToWND_STAGE * rotToSTAGE_DW;
1016 Quaternion rotToUSTAGE_UWND = new Quaternion(rotToWND_DW.x, -rotToWND_DW.y, rotToWND_DW.z, rotToWND_DW.w);
1017 return rotToUSTAGE_UWND;
1018 }
1019
1020 protected void ProcessTrackingData(Vector3 gripPosition_USTAGE, Vector3 fingertipsPosition_USTAGE, Vector3 aimPosition_USTAGE, Quaternion rotToUSTAGE_WND,
1021 WandSettings wandSettings, ScaleSettings scaleSettings, GameBoardSettings gameBoardSettings,
1022 out Pose gripPose_USTAGE, out Pose fingertipsPose_USTAGE, out Pose aimPose_USTAGE)
1023 {
1024 var incomingGripPose_USTAGE = new Pose(gripPosition_USTAGE, rotToUSTAGE_WND);
1025 var incomingFingertipsPose_USTAGE = new Pose(fingertipsPosition_USTAGE, rotToUSTAGE_WND);
1026 var incomingAimPose_USTAGE = new Pose(aimPosition_USTAGE, rotToUSTAGE_WND);
1027
1028 // Get the distance between the tracking points and the grip point.
1029 // Currently, when a pose is considered invalid, the position and rotation reported by the native plugin are completely zero'd out.
1030 // This crunches the tracking points together unless we consider the stale positions from the previous frame.
1031 // It also means that we don't get the desired behavior for TrackingFailureMode.FreezePosition,
1032 // in which the wand position freezes while still showing rotation values reported by the IMU.
1033 // TODO: In the native plugin, even for invalid poses (both wand and glasses, which are also affected),
1034 // include an offset for the tracking points, and pass through rotation data.
1035 var staleGripPose_USTAGE = pose_USTAGE;
1036 var staleFingertipsPose_USTAGE = fingertipsPose_GameboardSpace;
1037 var staleAimPose_USTAGE = aimPose_GameboardSpace;
1038
1039 var gripPointOffsetDistance = 0f;
1040 var fingertipsPointOffsetDistance = Mathf.Max((fingertipsPosition_USTAGE - gripPosition_USTAGE).magnitude,
1041 (staleFingertipsPose_USTAGE.position - staleGripPose_USTAGE.position).magnitude);
1042 var aimPointOffsetDistance = Mathf.Max((aimPosition_USTAGE - gripPosition_USTAGE).magnitude,
1043 (staleAimPose_USTAGE.position - staleGripPose_USTAGE.position).magnitude);
1044
1045 // Handle invalid poses
1046 gripPose_USTAGE = FilterTrackingPointPose(staleGripPose_USTAGE, staleGripPose_USTAGE, incomingGripPose_USTAGE, gripPointOffsetDistance, wandSettings);
1047 fingertipsPose_USTAGE = FilterTrackingPointPose(staleGripPose_USTAGE, staleFingertipsPose_USTAGE, incomingFingertipsPose_USTAGE, fingertipsPointOffsetDistance, wandSettings);
1048 aimPose_USTAGE = FilterTrackingPointPose(staleGripPose_USTAGE, staleAimPose_USTAGE, incomingAimPose_USTAGE, aimPointOffsetDistance, wandSettings);
1049
1050 }
1051
1052 protected Pose FilterTrackingPointPose(Pose staleGripPointPose, Pose staleTrackingPointPose,
1053 Pose newTrackingPointPose, float trackingPointOffsetDistance, WandSettings settings)
1054 {
1055 if (!isTracked && settings.RejectUntrackedPositionData)
1056 {
1057 switch (settings.FailureMode)
1058 {
1059 default:
1060 // If we have an undefined FailureMode for some reason, fall through to FreezePosition
1061 case TrackableSettings.TrackingFailureMode.FreezePosition:
1062 // We need to determine where to put each tracking point.
1063 // We only want to freeze the position of the grip point while allowing the other
1064 // points to rotate around it, colinear to the grip point's forward vector.
1065 var extrapolatedPosition = staleGripPointPose.position +
1066 Quaternion.Inverse(newTrackingPointPose.rotation) * Vector3.forward * trackingPointOffsetDistance;
1067 return new Pose(extrapolatedPosition, newTrackingPointPose.rotation);
1068 case TrackableSettings.TrackingFailureMode.FreezePositionAndRotation:
1069 return staleTrackingPointPose;
1070 case TrackableSettings.TrackingFailureMode.SnapToDefault:
1071 return GetDefaultPoseGameboardSpace(settings);
1072 }
1073 }
1074 else
1075 {
1076 return newTrackingPointPose;
1077 }
1078 }
1079
1080 #endregion Private Helper Functions
1081 }
1082
1083#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
1084 private class WandDeviceCore : WandCore
1085 {
1086 internal WandDevice wandDevice;
1087
1088 private enum TrackingState : int
1089 {
1090 None,
1091 Limited,
1092 Tracking
1093 }
1094
1095 public WandDeviceCore(GlassesHandle glassesHandle, ControllerIndex controllerIndex) : base(glassesHandle, controllerIndex)
1096 {
1097 if(Glasses.TryGetGlassesDevice(glassesHandle, out var glassesDevice))
1098 {
1099 //Add the new Wand Device to the Input System only when a new Wand Core is being created
1100 Input.AddWandDevice(glassesDevice.PlayerIndex, controllerIndex);
1101 wandDevice = Input.GetWandDevice(glassesDevice.PlayerIndex, controllerIndex);
1102 InputSystem.QueueConfigChangeEvent(wandDevice);
1103 if(controllerIndex == ControllerIndex.Right)
1104 {
1105 glassesDevice.RightWand = wandDevice;
1106 }
1107 else
1108 {
1109 glassesDevice.LeftWand = wandDevice;
1110 }
1111 if(TiltFiveManager2.IsInstantiated){
1112 TiltFiveManager2.Instance.RefreshInputDevicePairings();
1113 }
1114 }
1115 }
1116
1117 public override void Dispose()
1118 {
1119 base.Dispose();
1120
1121 if (Player.TryGetPlayerIndex(glassesHandle, out var playerIndex))
1122 {
1123 Input.RemoveWandDevice(playerIndex, controllerIndex);
1124 }
1125 }
1126
1127 public override void GetLatestInputs()
1128 {
1129 base.GetLatestInputs();
1130
1131 // If the wandDevice isn't added to the input system for any reason,
1132 // don't bother queueing any delta state events.
1133 if (!wandDevice.added || !wandDevice.enabled)
1134 {
1135 return;
1136 }
1137
1138 InputSystem.QueueDeltaStateEvent(wandDevice.TiltFive, currentState.HasValue && currentState.Value.TryGetButton(WandButton.T5));
1139 InputSystem.QueueDeltaStateEvent(wandDevice.One, currentState.HasValue && currentState.Value.TryGetButton(WandButton.One));
1140 InputSystem.QueueDeltaStateEvent(wandDevice.Two, currentState.HasValue && currentState.Value.TryGetButton(WandButton.Two));
1141 InputSystem.QueueDeltaStateEvent(wandDevice.Three, currentState.HasValue && currentState.Value.TryGetButton(WandButton.Three));
1142 InputSystem.QueueDeltaStateEvent(wandDevice.A, currentState.HasValue && currentState.Value.TryGetButton(WandButton.A));
1143 InputSystem.QueueDeltaStateEvent(wandDevice.B, currentState.HasValue && currentState.Value.TryGetButton(WandButton.B));
1144 InputSystem.QueueDeltaStateEvent(wandDevice.X, currentState.HasValue && currentState.Value.TryGetButton(WandButton.X));
1145 InputSystem.QueueDeltaStateEvent(wandDevice.Y, currentState.HasValue && currentState.Value.TryGetButton(WandButton.Y));
1146
1147 InputSystem.QueueDeltaStateEvent(wandDevice.Stick, currentState.HasValue ? currentState.Value.TryGetStick() : Vector2.zero);
1148 InputSystem.QueueDeltaStateEvent(wandDevice.Trigger, currentState.HasValue ? currentState.Value.TryGetTrigger() : 0f);
1149 }
1150
1151 protected override void SetDrivenObjectTransform(WandSettings wandSettings, ScaleSettings scaleSettings, GameBoardSettings gameBoardSettings)
1152 {
1153 base.SetDrivenObjectTransform(wandSettings, scaleSettings, gameBoardSettings);
1154
1155 // If the wandDevice isn't added to the input system for any reason,
1156 // don't bother queueing any delta state events.
1157 if (!wandDevice.added || !wandDevice.enabled)
1158 {
1159 return;
1160 }
1161
1162 // Time to inject our wand state into the Input System.
1163 QueueDeltaStateEvent(wandDevice.devicePosition, pose_UWRLD.position);
1164 QueueDeltaStateEvent(wandDevice.FingertipsPosition, fingertipsPose_UnityWorldSpace.position);
1165 QueueDeltaStateEvent(wandDevice.AimPosition, aimPose_UnityWorldSpace.position);
1166
1167 QueueDeltaStateEvent(wandDevice.RawGripPosition, pose_USTAGE.position);
1168 QueueDeltaStateEvent(wandDevice.RawFingertipsPosition, fingertipsPose_GameboardSpace.position);
1169 QueueDeltaStateEvent(wandDevice.RawAimPosition, aimPose_GameboardSpace.position);
1170
1171 QueueDeltaStateEvent(wandDevice.deviceRotation, pose_UWRLD.rotation);
1172 QueueDeltaStateEvent(wandDevice.RawRotation, pose_USTAGE.rotation);
1173
1174 InputSystem.QueueDeltaStateEvent(wandDevice.isTracked, isTracked);
1175
1176 var trackingState = TrackingState.Tracking;
1177 if (!isTracked)
1178 {
1179 trackingState = wandSettings.FailureMode == TrackableSettings.TrackingFailureMode.FreezePosition
1180 ? TrackingState.Limited
1181 : TrackingState.None;
1182 }
1183
1184 InputSystem.QueueDeltaStateEvent(wandDevice.trackingState, (int)trackingState);
1185 }
1186
1187 private static void QueueDeltaStateEvent(Vector3Control vector3Control, Vector3 delta)
1188 {
1189 InputSystem.QueueDeltaStateEvent(vector3Control.x, delta.x);
1190 InputSystem.QueueDeltaStateEvent(vector3Control.y, delta.y);
1191 InputSystem.QueueDeltaStateEvent(vector3Control.z, delta.z);
1192 }
1193
1194 // For some reason, using QueueDeltaStateEvent on a QuaternionControl with a Quaternion as the delta state doesn't work.
1195 // As a workaround, let's do it component-wise, since we know floats seem fine.
1196 private static void QueueDeltaStateEvent(QuaternionControl quaternionControl, Quaternion delta)
1197 {
1198 InputSystem.QueueDeltaStateEvent(quaternionControl.w, delta.w);
1199 InputSystem.QueueDeltaStateEvent(quaternionControl.x, delta.x);
1200 InputSystem.QueueDeltaStateEvent(quaternionControl.y, delta.y);
1201 InputSystem.QueueDeltaStateEvent(quaternionControl.z, delta.z);
1202 }
1203 }
1204#endif
1205
1206 #endregion Private Classes
1207
1208
1209 #region Public Classes
1210
1246 public class WaitUntilWandConnected : CustomYieldInstruction
1247 {
1248 public override bool keepWaiting => !Player.IsConnected(playerIndex) // Keep waiting if the player isn't connected,
1249 || !Wand.TryCheckConnected(out bool wandConnected, playerIndex, controllerIndex) // or if their wand can't be checked,
1250 || !wandConnected; // or if their wand isn't connected.
1251 private PlayerIndex playerIndex = PlayerIndex.One;
1252 private ControllerIndex controllerIndex = ControllerIndex.Right;
1253
1254 public WaitUntilWandConnected(PlayerIndex playerIndex, ControllerIndex controllerIndex)
1255 {
1256 this.playerIndex = playerIndex;
1257 this.controllerIndex = controllerIndex;
1258 }
1259 }
1260
1261 #endregion
1262 }
1263}
The Logger.
Definition Log.cs:42
static void Info(string m, params object[] list)
INFO logging function call.
Definition Log.cs:140
static void Error(string m, params object[] list)
ERROR logging function call.
Definition Log.cs:127
Provides access to player settings and functionality.
Definition Player.cs:31
static bool IsConnected(PlayerIndex playerIndex)
Determines whether the specified player has an associated pair of glasses connected.
Definition Player.cs:48
ScaleSettings contains the scale data used to translate between Unity units and the user's physical s...
The Wand API and runtime.
Definition Wand.cs:56
static bool TryCheckConnected(out bool connected, PlayerIndex playerIndex, ControllerIndex controllerIndex=ControllerIndex.Right)
Gets the connection status of the indicated wand.
Definition Wand.cs:221
static Quaternion GetRotation(ControllerIndex controllerIndex=ControllerIndex.Right, PlayerIndex playerIndex=PlayerIndex.One)
Gets the rotation of the wand in world space.
Definition Wand.cs:190
static bool TrySendImpulse(float amplitude, float duration, PlayerIndex playerIndex=PlayerIndex.One, ControllerIndex controllerIndex=ControllerIndex.Right)
Try to send a haptics impulse to the specified wand.
Definition Wand.cs:268
static Vector3 GetPosition(ControllerIndex controllerIndex=ControllerIndex.Right, ControllerPosition controllerPosition=ControllerPosition.Grip, PlayerIndex playerIndex=PlayerIndex.One)
Gets the position of the wand in world space.
Definition Wand.cs:159
Wand Settings encapsulates all configuration data used by the Wand's tracking runtime to compute the ...
Definition Wand.cs:38
ControllerPosition
Points of interest along the wand controller, such as the handle position or wand tip.
ControllerIndex
Since wands are all physically identical (they have no "handedness"), it doesn't make sense to addres...
PlayerIndex
The Player index (e.g. Player One, Player Two, etc)