Controller HP Reverb G2 in Unity
I controller HP Motion sono un nuovo tipo di controller Windows Mixed Reality: tutte le stesse tecnologie di rilevamento con un set leggermente diverso di input disponibili:
- Touchpad è stato sostituito da due pulsanti: A e B per il controller destro e X e Y per il controller sinistro.
- Il trigger è ora un trigger che pubblica un flusso di valori compreso tra 0,0 e 1.0 anziché un pulsante con stati premuti e non premuti.
Poiché i nuovi input non sono accessibili tramite le API Windows e Unity esistenti, è necessario il pacchetto UPM Microsoft.MixedReality.Input dedicato.
Importante
Le classi in questo pacchetto non sostituiscono le API di Windows e Unity esistenti, ma le completano. Le funzionalità comunemente disponibili per i controller di Windows Mixed Reality classici e i controller di movimento HP sono accessibili tramite lo stesso percorso di codice usando le API esistenti. Solo i nuovi input richiedono l'uso del pacchetto Microsoft.MixedReality.Input aggiuntivo.
Panoramica di HP Motion Controller
Microsoft.MixedReality.Input.MotionController rappresenta un controller di movimento. Ogni istanza di MotionController ha una XR. WSA. Peer Input.InteractionSource , che può essere correlato usando la mano, l'ID fornitore, l'ID prodotto e la versione.
È possibile acquisire istanze di MotionController creando un MotionControllerWatcher e sottoscrivendone gli eventi, analogamente all'uso di eventi InteractionManager per individuare nuove istanze di InteractionSource . I metodi e le proprietà di MotionController descrivono gli input supportati dal controller, inclusi i pulsanti, i trigger, l'asse 2D e l'asse 2D. La classe MotionController espone anche metodi per l'accesso agli stati di input tramite la classe MotionControllerReading . La classe MotionControllerReading rappresenta uno snapshot dello stato del controller in un determinato momento.
Installazione di Microsoft.MixedReality.Input con lo strumento di funzionalità Realtà mista
Installare il plug-in Microsoft.MixedReality.Input con la nuova applicazione strumento di funzionalità Realtà mista. Seguire le istruzioni di installazione e utilizzo e selezionare il pacchetto di input Realtà mista nella categoria Realtà mista Toolkit:
Uso di Microsoft.MixedReality.Input
Valori di input
Un MotionController può esporre due tipi di input:
- I pulsanti e gli stati trigger vengono espressi da un valore float univoco compreso tra 0,0 e 1.0 che indica la quantità di pressione.
- Un pulsante può restituire solo 0,0 (quando non premuto) o 1.0 (quando premuto) mentre un trigger può restituire valori continui tra 0,0 (completamente rilasciato) a 1.0 (completamente premuto).
- Lo stato del pollice è espresso da vector2 i cui componenti X e Y sono compresi tra -1.0 e 1.0.
È possibile usare MotionController.GetPressableInputs() per restituire un elenco di input che restituiscono un valore premuto (pulsanti e trigger) o il metodo MotionController.GetXYInputs() per restituire un elenco di input che restituiscono un valore a 2 asse.
Un'istanza di MotionControllerReading rappresenta lo stato del controller in un determinato momento:
- GetPressedValue() recupera lo stato di un pulsante o di un trigger.
- GetXYValue() recupera lo stato di un pollice.
Creazione di una cache per mantenere una raccolta di istanze di MotionController e dei relativi stati
Iniziare creando un'istanza di MotionControllerWatcher e registrando i gestori per i relativi eventi MotionControllerAdded e MotionControllerRemoved per mantenere una cache delle istanze di MotionController disponibili. Questa cache deve essere un MonoBehavior collegato a un GameObject, come illustrato nel codice seguente:
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);
}
}
}
Lettura di nuovi input tramite polling
È possibile leggere lo stato corrente di ogni controller noto tramite MotionController.TryGetReadingAtTime durante il metodo Update della classe MonoBehavior. Si vuole passare DateTime.Now come parametro timestamp per assicurarsi che lo stato più recente del controller sia letto.
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);
}
}
}
}
È possibile acquisire il valore di input corrente dei controller usando La mano del controller:
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;
}
}
Ad esempio, per leggere il valore di afferrare analogico di un oggetto 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);
…
}
}
Generazione di eventi dai nuovi input
Anziché eseguire il polling per lo stato di un controller una volta per fotogramma, è possibile gestire tutte le modifiche dello stato come eventi, consentendo di gestire anche le azioni più rapide che durano meno di un frame. Affinché questo approccio funzioni, la cache dei controller di movimento deve elaborare tutti gli stati pubblicati da un controller dall'ultimo frame, che è possibile eseguire archiviando il timestamp dell'ultimo MotionControllerReading recuperato da un MotionControllerReadler e chiamando 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;
}
}
Dopo aver aggiornato le classi interne della cache, la classe MonoBehavior può esporre due eventi, pressati e rilasciati, e generarli dal relativo metodo 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 struttura negli esempi di codice precedenti rende la registrazione di eventi molto più leggibili:
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))
{
…
}
}