Skip to main content

VR Controller Input in Godot

Preface:

I use C# for coding in Godot due to the speed and resources (we need all the resources we can get when building with VR). Below is an example using C# but can be converted to GDScript as well.

For more information about C# in Godot:

https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_basics.html

Getting Started

Assuming you have followed the previous tutorial and confirmed VR is working with controller visuals (even cubes) and OpenXR, you can get started.

By default, after installing OpenXR within Godot enables a default OpenXR Action Map. Typically, I leave these defaults alone. There is a good article in the documentation that reviews how to create your own custom inputs, but these work great.

The event structure on how you process this input is up to you! Below is an EXAMPLE but should be structured!

1. Create subscription events for ButtonPressed in Released.

Create a script on your XRController3D node (the VR controller) and the script should inherit from XRController3D.

Inside the script, create two override functions: _EnterTree() and _Exit Tree(). Add ButtonPressed and ButtonReleased and subscribe to a method. Here is the layout:

public override void _EnterTree()
{
  ButtonPressed += OnButtonPressed;
  ButtonReleased += OnButtonReleased;
}

public override void _ExitTree()
{
  ButtonPressed -= OnButtonPressed;
  ButtonReleased -= OnButtonReleased;
}

Now, create two functions, OnButtonPressed and OnButtonReleased:

void OnButtonPressed(string name)
{

}

void OnButtonReleased(string name)
{

}

2. Process events

You will notice on the methods created above that there is a parameter called string name. This string is what is received from your OpenXR Action Map (or anything else with a button). For me, I have added a switch statement to process these inputs and invoke methods:

    void OnButtonPressed(string name)
    {
        switch (name)
        {
            case "grip_click":
                OnGripPressed();
                break;
            case "trigger_click":
                OnTriggerPressed();
                break;
            case "ax_button":
                OnAXPressed();
                break;
            case "by_button":
                OnBYPressed();
                break;
        }
    }

    void OnButtonReleased(string name)
    {
        switch (name)
        {
            case "grip_click":
                OnGripReleased();
                break;
            case "trigger_click":
                OnTriggerReleased();
                break;
            case "ax_button":
                OnAXReleased();
                break;
            case "by_button":
                OnBYReleased();
                break;
        }
    }

I'm not including every single method, but you can see that I now have a system to process input!

3. Expand (with some Object Oriented Programming)!!

Obviously, this script would be a pain to copy for both hands and alter the names of the methods slightly. You can do this if you wish to keep it simple.

Object Oriented Programming (OOP):

For me, I created a parent script called something like XRHand.cs. Then, I created two scripts called LeftHand.cs and RightHand.cs that are children of XRHand. Using the methods above, I made them virtual so in my LeftHand.cs script for example, I can configure special implementation.

Here is the final script for XRHand.cs

using Godot;
using System;

public partial class XRHand : XRController3D
{
    public override void _EnterTree()
    {
        ButtonPressed += OnButtonPressed;
        ButtonReleased += OnButtonReleased;

        InputFloatChanged += OnInputFloatChanged;

        InputVector2Changed += OnInputVector2Changed;
    }

    public override void _ExitTree()
    {
        ButtonPressed -= OnButtonPressed;
        ButtonReleased -= OnButtonReleased;

        InputFloatChanged -= OnInputFloatChanged;

        InputVector2Changed -= OnInputVector2Changed;
    }

    void OnButtonPressed(string name)
    {
        switch (name)
        {
            case "grip_click":
                OnGripPressed();
                break;
            case "trigger_click":
                OnTriggerPressed();
                break;
            case "ax_button":
                OnAXPressed();
                break;
            case "by_button":
                OnBYPressed();
                break;
        }
    }

    void OnButtonReleased(string name)
    {
        switch (name)
        {
            case "grip_click":
                OnGripReleased();
                break;
            case "trigger_click":
                OnTriggerReleased();
                break;
            case "ax_button":
                OnAXReleased();
                break;
            case "by_button":
                OnBYReleased();
                break;
        }
    }

    void OnInputFloatChanged(string name, double value)
    {

    }

    void OnInputVector2Changed(string name, Vector2 value)
    {
        switch (name)
        {
            case "primary":
                OnThumbstickMoved(value);
                break;
        }
    }


    // Digital Input
    public virtual void OnGripPressed() { }
    public virtual void OnGripReleased() { }
    public virtual void OnTriggerPressed() { }
    public virtual void OnTriggerReleased() { }
    public virtual void OnAXPressed() { }
    public virtual void OnAXReleased() { }
    public virtual void OnBYPressed() { }
    public virtual void OnBYReleased() { }

    // Axial-1D Input

    // Axial-2D Input
    public virtual void OnThumbstickMoved(Vector2 value) { }


    // Signals
    
}

And as for LeftHand.cs or RightHand.cs,  I would just override a button that I would need:

using Godot;
using System;

public partial class LeftHand : XRHand
{
    public override void OnGripPressed()
    {
         GD.Print("Right grip released!");
    }

    public override void OnGripReleased()
    {
        GD.Print("Left grip released!");
    }
}

The LeftHand.cs and RightHand.cs would now go on their respective XRController3D nodes, in the scene.