- Home /
How to get VR button press input on only the first frame it is pressed - XR Toolkit
I've recently learned that detecting a button press on a VR controller using:
public class ReadInput : MonoBehaviour
{
//Assigned to the RightHand Controller
public XRController controller = null;
InputDevice device;
void Start (){
device = Input.GetDeviceAtXRNode(controller.controllerNode);
}
void Update()
{
if(device.TryGetFeatureValue(CommonUsages.primaryButton, out bool buttonValue) && buttonValue)
{
Debug.Log(" pressing a");
}
}
Returns true every frame that the button is pressed. I expected this code to work like normal keyboard input i.e. Input.GetKeyDown(KeyCode.Space)
, but while using that line of code would work fine in my project, getting input from a VR button press does not.
How can I fix this code to work more like Input.GetKeyDown(KeyCode.Space)
where it only returns true on the first frame the button was pressed? Is there a different function I could use that would achieve what I'm looking for, or how can I ensure it doesn't keep returning true every single frame?
Answer by wpetillo · Jun 26, 2020 at 05:22 AM
Attach this to an object in your scene, connect the exposed UnityEvents to public methods in your game logic that respond to button presses, and never worry about XR input again. Supports touch and full press as well as first frame, last frame, and continuous. For an understanding of how to get the first frame of a binary state change, see the Update method in XRBinding. For example usage, see https://github.com/Will9371/Character-Template, an open-source project which also includes touchpad input, keyboard input, 1st and 3rd person character control, and 3rd person VR character control, and lots of other things.
using System;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.XR;
using UnityEngine.XR.Interaction.Toolkit;
public class XRInput : MonoBehaviour
{
#pragma warning disable 0649
[SerializeField] XRController controller;
[SerializeField] XRBinding[] bindings;
#pragma warning restore 0649
private void Update()
{
foreach (var binding in bindings)
binding.Update(controller.inputDevice);
}
}
[Serializable]
public class XRBinding
{
#pragma warning disable 0649
[SerializeField] XRButton button;
[SerializeField] PressType pressType;
[SerializeField] UnityEvent OnActive;
#pragma warning restore 0649
bool isPressed;
bool wasPressed;
public void Update(InputDevice device)
{
device.TryGetFeatureValue(XRStatics.GetFeature(button), out isPressed);
bool active = false;
switch (pressType)
{
case PressType.Continuous: active = isPressed; break;
case PressType.Begin: active = isPressed && !wasPressed; break;
case PressType.End: active = !isPressed && wasPressed; break;
}
if (active) OnActive.Invoke();
wasPressed = isPressed;
}
}
public enum XRButton
{
Trigger,
Grip,
Primary,
PrimaryTouch,
Secondary,
SecondaryTouch,
Primary2DAxisClick,
Primary2DAxisTouch
}
public enum PressType
{
Begin,
End,
Continuous
}
public static class XRStatics
{
public static InputFeatureUsage<bool> GetFeature(XRButton button)
{
switch (button)
{
case XRButton.Trigger: return CommonUsages.triggerButton;
case XRButton.Grip: return CommonUsages.gripButton;
case XRButton.Primary: return CommonUsages.primaryButton;
case XRButton.PrimaryTouch: return CommonUsages.primaryTouch;
case XRButton.Secondary: return CommonUsages.secondaryButton;
case XRButton.SecondaryTouch: return CommonUsages.secondaryTouch;
case XRButton.Primary2DAxisClick: return CommonUsages.primary2DAxisClick;
case XRButton.Primary2DAxisTouch: return CommonUsages.primary2DAxisTouch;
default: Debug.LogError("button " + button + " not found"); return CommonUsages.triggerButton;
}
}
}
Wow, talk about not having seen the forest for the trees. This is a very solid solution! Once the setup's done (i.e. all intended/desired buttons), this looks like it handles everything very cleanly.
Thank you for this!!! I am attempting to get away from the Oculus OVR package and use the slimmed down/ less bloated XR Interaction Toolkit. Trying to figure out the input methods are confounding me at the moment and this solution for button presses is working great! Now I am going to tackle actually moving in VR using your script...
I made an empty game object and added this code to it but I'm unable to add XR controllers into the XRController field. I thought I'd just drag a left or right hand controller right onto it and it would work but it's not working. The left/right hand controllers in my scene are Action Based. Please let me know what I'm doing wrong. Thank you.
@tbgames3000 Hard to say without seeing the full context, but I am guessing it is because of the action-based system--one of the main reasons I made this was to avoid dealing with all the over-engineered nonsense out there. In any case, the best way to troubleshoot (this, or pretty much anything else) would be to create a new project, import the .UnityPackage file (from here: https://github.com/Will9371/Character-Template) for quick setup, and check out one of the example scenes to see how it all works in context. Once you understand how it works, observe what is different about your project and consider if it meets your needs.
@wpetillo I'll try what you told me to do and see if I can make it work in my project. Thanks a lot!
Answer by Eno-Khaon · Jun 24, 2020 at 11:34 PM
One way to handle it would simply be to track it yourself. If the button wasn't pressed as of the previous frame, but it is on the current one, there's your GetKeyDown() equivalent.
Dictionary<CommonUsages, bool> rightHandState;
// ...
void Start()
{
rightHandState = new Dictionary<CommonUsages, bool>();
rightHandState[CommonUsages.primaryButton] = false; // Assumed not held when starting game, while also initializing the value
}
// ...
void Update()
{
if(device.TryGetFeatureValue(CommonUsages.primaryButton, out bool buttonValue))
{
if(buttonValue != rightHandState[CommonUsages.primaryButton])
{
// Button was pressed or released this frame
// Do something with it here...
// ... then make sure to set the last known state of it for reference next Update()
rightHandState[CommonUsages.primaryButton] = buttonValue;
}
}
}
This setup should be easy enough to adapt to support more button types, where the Dictionary will automatically be able to support as many of them as are needed.
That said, if you have a very small/finite number of buttons you intend to allow, it would be cheaper/viable to simply define a Boolean to keep track of the last known state of each given button, rather than using the Dictionary.
Thank you, but this code gives me some errors. 'CommonUsages': static types cannot be used as type arguments
and Argument1: cannot convert from 'UnityEngine.XR.InputFeatureUsage<bool>' to 'UnityEngine.XR.CommonUsages'
Whoops, I should've done my research better. I thought, given the presentation and usage, that CommonUsages was an enumerator.
Well, then, in that case, I guess you'd just want to use an independent Boolean to keep track of any given value ins$$anonymous$$d, then.
bool rightHandLastState = false;
// ...
rightHandLastState = buttonValue;
$$anonymous$$y code currently looks like this:
bool rightHandLastState = false;
// ...
void Start()
{
rightHandLastState = false; // Assumed not held when starting game, while also initializing the value
}
// ...
void Update()
{
if(device.TryGetFeatureValue(CommonUsages.primaryButton, out bool buttonValue))
{
if(buttonValue != rightHandLastState)
{
// Button was pressed or released this frame
// Do something with it here...
// ... then make sure to set the last known state of it for reference next Update()
rightHandLastState = buttonValue;
}
}
}
But this doesn't seem to ever enter the second if statement