Contrôleurs HP Reverb G2 dans Unity

Les contrôleurs de mouvement HP sont un tout nouveau type de contrôleurs de Windows Mixed Reality : toutes les mêmes technologies de suivi avec un ensemble légèrement différent d’entrées disponibles :

  • Le pavé tactile a été remplacé par deux boutons : A et B pour le contrôleur droit, et X et Y pour le contrôleur gauche.
  • Saisir est maintenant un déclencheur qui publie un flux de valeurs compris entre 0.0 et 1.0 au lieu d’un bouton avec des états appuyés et non appuyés.

Étant donné que les nouvelles entrées ne sont pas accessibles via les API Windows et Unity existantes, vous avez besoin du package UPM Microsoft.MixedReality.Input dédié.

Important

Les classes de ce package ne remplacent pas les API Windows et Unity existantes, mais les complètent. Les fonctionnalités couramment disponibles pour les contrôleurs de Windows Mixed Reality classiques et les contrôleurs de mouvement HP sont accessibles via le même chemin de code à l’aide d’API existantes. Seules les nouvelles entrées nécessitent l’utilisation du package Microsoft.MixedReality.Input supplémentaire.

Vue d’ensemble du contrôleur de mouvement HP

Microsoft.MixedReality.Input.MotionController représente un contrôleur de mouvement. Chaque instance MotionController a un XR. WSA. Peer Input.InteractionSource , qui peut être corrélé à l’aide de la remise, de l’ID du fournisseur, de l’ID de produit et de la version.

Vous pouvez récupérer des instances MotionController en créant un MotionControllerWatcher et en vous abonnant à ses événements, comme l’utilisation d’événements InteractionManager pour découvrir de nouvelles instances InteractionSource . Les méthodes et propriétés de MotionController décrivent les entrées prises en charge par le contrôleur, notamment ses boutons, ses déclencheurs, son axe 2D et son stick. La classe MotionController expose également des méthodes d’accès aux états d’entrée via la classe MotionControllerReading . La classe MotionControllerReading représente un instantané de l’état du contrôleur à un moment donné.

Installation de Microsoft.MixedReality.Input avec l’outil de fonctionnalité Mixed Reality

Installez le plug-in Microsoft.MixedReality.Input avec la nouvelle application Mixed Reality Feature Tool. Suivez les instructions d’installation et d’utilisation, puis sélectionnez le package d’entrée Mixed Reality dans la catégorie Mixed Reality Shared Computer Toolkit :

Mixed Reality Feature Tool packages window with mixed reality input highlighted

Utilisation de Microsoft.MixedReality.Input

Valeurs d’entrée

Un MotionController peut exposer deux types d’entrées :

  • Les boutons et les états de déclencheur sont exprimés par une valeur flottante unique comprise entre 0,0 et 1,0 qui indique la quantité d’appuis.
    • Un bouton ne peut retourner que 0,0 (lorsqu’il n’est pas enfoncé) ou 1,0 (lorsqu’il est enfoncé), tandis qu’un déclencheur peut retourner des valeurs continues comprises entre 0,0 (entièrement libéré) à 1,0 (entièrement enfoncé).
  • L’état du stick est exprimé par un Vector2 dont les composants X et Y sont compris entre -1.0 et 1.0.

Vous pouvez utiliser MotionController.GetPressableInputs() pour renvoyer une liste d’entrées retournant une valeur enfoncée (boutons et déclencheurs) ou la méthode MotionController.GetXYInputs() pour retourner une liste d’entrées retournant une valeur d’axe 2.

Une instance MotionControllerReading représente l’état du contrôleur à un moment donné :

  • GetPressedValue() récupère l’état d’un bouton ou d’un déclencheur.
  • GetXYValue() récupère l’état d’un stick.

Création d’un cache pour gérer une collection d’instances MotionController et leurs états

Commencez par instancier un MotionControllerWatcher et inscrire des gestionnaires pour ses événements MotionControllerAdded et MotionControllerRemoved afin de conserver un cache des instances MotionController disponibles. Ce cache doit être un MonoBehavior attaché à un GameObject, comme illustré dans le code suivant :

public class MotionControllerStateCache : MonoBehaviour 
{ 
    /// <summary> 
    /// Internal helper class which associates a Motion Controller 
    /// and its known state 
    /// </summary> 
    private class MotionControllerState 
    { 
        /// <summary> 
        /// Construction 
        /// </summary> 
        /// <param name="mc">motion controller</param>` 
        public MotionControllerState(MotionController mc) 
        { 
            this.MotionController = mc; 
        } 

        /// <summary> 
        /// Motion Controller that the state represents 
        /// </summary> 
        public MotionController MotionController { get; private set; } 
        … 
    } 

    private MotionControllerWatcher _watcher; 
    private Dictionary<Handedness, MotionControllerState> 
        _controllers = new Dictionary<Handedness, MotionControllerState>(); 

    /// <summary> 
    /// Starts monitoring controller's connections and disconnections 
    /// </summary> 
    public void Start() 
    { 
        _watcher = new MotionControllerWatcher(); 
        _watcher.MotionControllerAdded += _watcher_MotionControllerAdded; 
        _watcher.MotionControllerRemoved += _watcher_MotionControllerRemoved; 
        var nowait = _watcher.StartAsync(); 
    } 

    /// <summary> 
    /// Stops monitoring controller's connections and disconnections 
    /// </summary> 
    public void Stop() 
    { 
        if (_watcher != null) 
        { 
            _watcher.MotionControllerAdded -= _watcher_MotionControllerAdded; 
            _watcher.MotionControllerRemoved -= _watcher_MotionControllerRemoved; 
            _watcher.Stop(); 
        } 
    }

    /// <summary> 
    /// called when a motion controller has been removed from the system: 
    /// Remove a motion controller from the cache 
    /// </summary> 
    /// <param name="sender">motion controller watcher</param> 
    /// <param name="e">motion controller </param> 
    private void _watcher_MotionControllerRemoved(object sender, MotionController e) 
    { 
        lock (_controllers) 
        { 
            _controllers.Remove(e.Handedness); 
        } 
    }

    /// <summary> 
    /// called when a motion controller has been added to the system: 
    /// Remove a motion controller from the cache 
    /// </summary> 
    /// <param name="sender">motion controller watcher</param> 
    /// <param name="e">motion controller </param> 
    private void _watcher_MotionControllerAdded(object sender, MotionController e) 
    { 
        lock (_controllers) 
        { 
            _controllers[e.Handedness] = new MotionControllerState(e); 
        } 
    } 
} 

Lecture de nouvelles entrées en interrogeant

Vous pouvez lire l’état actuel de chaque contrôleur connu via MotionController.TryGetReadingAtTime pendant la méthode Update de la classe MonoBehavior. Vous souhaitez passer DateTime.Now comme paramètre timestamp pour vous assurer que l’état le plus récent du contrôleur est lu.

public class MotionControllerStateCache : MonoBehaviour 
{ 
    … 

    private class MotionControllerState 
    {
        … 

        /// <summary> 
        /// Update the current state of the motion controller 
        /// </summary> 
        /// <param name="when">time of the reading</param> 
        public void Update(DateTime when) 
        { 
            this.CurrentReading = this.MotionController.TryGetReadingAtTime(when); 
        } 

        /// <summary> 
        /// Last reading from the controller 
        /// </summary> 
        public MotionControllerReading CurrentReading { get; private set; } 
    } 

    /// <summary> 
    /// Updates the input states of the known motion controllers 
    /// </summary> 
    public void Update() 
    { 
        var now = DateTime.Now; 

        lock (_controllers) 
        { 
            foreach (var controller in _controllers) 
            { 
                controller.Value.Update(now); 
            } 
        } 
    } 
} 

Vous pouvez récupérer la valeur d’entrée actuelle des contrôleurs à l’aide de la remise du contrôleur :

public class MotionControllerStateCache : MonoBehaviour 
{ 
    … 
    /// <summary> 
    /// Returns the current value of a controller input such as button or trigger 
    /// </summary> 
    /// <param name="handedness">Handedness of the controller</param> 
    /// <param name="input">Button or Trigger to query</param> 
    /// <returns>float value between 0.0 (not pressed) and 1.0 
    /// (fully pressed)</returns> 
    public float GetValue(Handedness handedness, ControllerInput input) 
    { 
        MotionControllerReading currentReading = null; 

        lock (_controllers) 
        { 
            if (_controllers.TryGetValue(handedness, out MotionControllerState mc)) 
            { 
                currentReading = mc.CurrentReading; 
            } 
        } 

        return (currentReading == null) ? 0.0f : currentReading.GetPressedValue(input); 
    } 

    /// <summary> 
    /// Returns the current value of a controller input such as button or trigger 
    /// </summary> 
    /// <param name="handedness">Handedness of the controller</param> 
    /// <param name="input">Button or Trigger to query</param> 
    /// <returns>float value between 0.0 (not pressed) and 1.0 
    /// (fully pressed)</returns> 
    public float GetValue(UnityEngine.XR.WSA.Input.InteractionSourceHandedness handedness, ControllerInput input) 
    { 
        return GetValue(Convert(handedness), input); 
    } 

    /// <summary> 
    /// Returns a boolean indicating whether a controller input such as button or trigger is pressed 
    /// </summary> 
    /// <param name="handedness">Handedness of the controller</param> 
    /// <param name="input">Button or Trigger to query</param> 
    /// <returns>true if pressed, false if not pressed</returns> 
    public bool IsPressed(Handedness handedness, ControllerInput input) 
    { 
        return GetValue(handedness, input) >= PressedThreshold; 
    } 
} 

Par exemple, pour lire la valeur de saisie analogique d’une InteractionSource :

/// Read the analog grasp value of all connected interaction sources 
void Update() 
{ 
    … 
    var stateCache = gameObject.GetComponent<MotionControllerStateCache>(); 
    foreach (var sourceState in InteractionManager.GetCurrentReading()) 
    { 
        float graspValue = stateCache.GetValue(sourceState.source.handedness, 
            Microsoft.MixedReality.Input.ControllerInput.Grasp);
        … 
    }
} 

Génération d’événements à partir des nouvelles entrées

Au lieu d’interroger l’état d’un contrôleur une fois par image, vous avez la possibilité de gérer toutes les modifications d’état en tant qu’événements, ce qui vous permet de gérer même les actions les plus rapides durables moins qu’une image. Pour que cette approche fonctionne, le cache des contrôleurs de mouvement doit traiter tous les états publiés par un contrôleur depuis le dernier frame, que vous pouvez faire en stockant l’horodatage du dernier MotionControllerReading récupéré à partir d’un MotionController et appelant MotionController.TryGetReadingAfterTime() :

private class MotionControllerState 
{ 
    … 
    /// <summary> 
    /// Returns an array representng buttons which are pressed 
    /// </summary> 
    /// <param name="reading">motion controller reading</param> 
    /// <returns>array of booleans</returns> 
    private bool[] GetPressed(MotionControllerReading reading) 
    { 
        if (reading == null) 
        { 
            return null; 
        } 
        else 
        { 
            bool[] ret = new bool[this.pressableInputs.Length]; 
            for (int i = 0; i < pressableInputs.Length; ++i) 
            { 
                ret[i] = reading.GetPressedValue(pressableInputs[i]) >= PressedThreshold; 
            } 

            return ret; 
        } 
    } 

    /// <summary> 
    /// Get the next available state of the motion controller 
    /// </summary> 
    /// <param name="lastReading">previous reading</param> 
    /// <param name="newReading">new reading</param> 
    /// <returns>true is a new reading was available</returns> 
    private bool GetNextReading(MotionControllerReading lastReading, out MotionControllerReading newReading) 
    { 
        if (lastReading == null) 
        { 
            // Get the first state published by the controller 
            newReading = this.MotionController.TryGetReadingAfterSystemRelativeTime(TimeSpan.FromSeconds(0.0)); 
        } 
        else 
        { 
            // Get the next state published by the controller 
            newReading = this.MotionController.TryGetReadingAfterTime(lastReading.InputTime); 
        } 

        return newReading != null; 
    } 

    /// <summary> 
    /// Processes all the new states published by the controller since the last call 
    /// </summary> 
    public IEnumerable<MotionControllerEventArgs> GetNextEvents() 
    {
        MotionControllerReading lastReading = this.CurrentReading; 
        bool[] lastPressed = GetPressed(lastReading); 
        MotionControllerReading newReading; 
        bool[] newPressed; 

        while (GetNextReading(lastReading, out newReading)) 
        { 
            newPressed = GetPressed(newReading); 

            // If we have two readings, compare and generate events 
            if (lastPressed != null) 
            { 
                for (int i = 0; i < pressableInputs.Length; ++i) 
                { 
                    if (newPressed[i] != lastPressed[i]) 
                    { 
                        yield return new MotionControllerEventArgs(this.MotionController.Handedness, newPressed[i], this.pressableInputs[i], newReading.InputTime); 
                    } 
                } 
            } 

            lastPressed = newPressed; 
            lastReading = newReading; 
        } 

        // No more reading 
        this.CurrentReading = lastReading; 
    } 
} 

Maintenant que vous avez mis à jour les classes internes du cache, la classe MonoBehavior peut exposer deux événements ( Pressed and Released) et les déclencher à partir de sa méthode Update() :

/// <summary> 
/// Event argument class for InputPressed and InputReleased events 
/// </summary> 
public class MotionControllerEventArgs : EventArgs 
{ 
    public MotionControllerEventArgs(Handedness handedness, bool isPressed, rollerInput input, DateTime inputTime) 
    { 
        this.Handedness = handedness; 
        this.Input = input; 
        this.InputTime = inputTime; 
        this.IsPressed = isPressed; 
    } 

    /// <summary> 
    /// Handedness of the controller raising the event 
    /// </summary> 
    public Handedness Handedness { get; private set; } 

    /// <summary> 
    /// Button pressed or released 
    /// </summary> 
    public ControllerInput Input { get; private set; } 

    /// <summary> 
    /// Time of the event 
    /// </summary> 
    public DateTime InputTime { get; private set; } 

    /// <summary> 
    /// true if button is pressed, false otherwise 
    /// </summary> 
    public bool IsPressed { get; private set; } 
} 

/// <summary> 
/// Event raised when a button is pressed 
/// </summary> 
public event EventHandler<MotionControllerEventArgs> InputPressed; 

/// <summary> 
/// Event raised when a button is released 
/// </summary> 
public event EventHandler<MotionControllerEventArgs> InputReleased; 

/// <summary> 
/// Updates the input states of the known motion controllers 
/// </summary> 
public void Update() 
{ 
    // If some event handler has been registered, we need to process all states  
    // since the last update, to avoid missing a quick press / release 
    if ((InputPressed != null) || (InputReleased != null)) 
    { 
        List<MotionControllerEventArgs> events = new <MotionControllerEventArgs>(); 

        lock (_controllers) 
        { 
            foreach (var controller in _controllers) 
            { 
                events.AddRange(controller.Value.GetNextEvents()); 
            } 
        } 
 
        // Sort the events by time 
        events.Sort((e1, e2) => DateTime.Compare(e1.InputTime, e2.InputTime)); 

        foreach (MotionControllerEventArgs evt in events) 
        { 
            if (evt.IsPressed && (InputPressed != null)) 
            { 
                InputPressed(this, evt); 
            } 
            else if (!evt.IsPressed && (InputReleased != null)) 
            { 
                InputReleased(this, evt); 
            } 
        } 
    } 
    else 
    { 
        // As we do not predict button presses and the timestamp of the next e is in the future 
        // DateTime.Now is correct in this context as it will return the latest e of controllers 
        // which is the best we have at the moment for the frame. 
        var now = DateTime.Now; 
        lock (_controllers) 
        { 
            foreach (var controller in _controllers) 
            { 
                controller.Value.Update(now); 
            } 
        } 
    } 
} 

La structure des exemples de code ci-dessus rend l’inscription des événements beaucoup plus lisible :

public InteractionSourceHandedness handedness; 
public Microsoft.MixedReality.Input.ControllerInput redButton;

// Start of the Mono Behavior: register handlers for events from cache 
void Start() 
{ 
    var stateCache = gameObject.GetComponent<MotionControllerStateCache>(); 
    stateCache.InputPressed += stateCache_InputPressed; 
    stateCache.InputReleased += stateCache_InputReleased; 
    … 
} 

// Called when a button is released 
private void stateCache_InputReleased(object sender, MotionControllerStateCache.MotionControllerEventArgs e) 
{ 
    if ((e.SourceHandedness == handedness) && (e.Input == redButton)) 
    { 
        … 
    } 
} 

// Called when a button is pressed 
private void stateCache_InputPressed(object sender, MotionControllerStateCache.MotionControllerEventArgs e) 
{ 
    if ((e.SourceHandedness == handedness) && (e.Input == redButton)) 
    { 
        … 
    } 
} 

Voir aussi