Skip to main content

Command Palette

Search for a command to run...

The Universal OpenXR Input System

From Inspector bindings to a personalised input system

Updated
4 min read
The Universal OpenXR Input System

In the early days of a VR project, the "Unity way" seems obvious: using inspector based input action bindings. It’s fast, it’s visual, and for a prototype, it works. But with too many of them, it is such a pain to add bindings for all the actions and recalling what OpenXR naming is for which button in the XRI Default Input Actions.

Here is how I evolved my input architecture from "Inspector-dependent" to a "Universal, code-generated" system.


Evolution of Thought

Level 1: The Inspector Trap (Direct Definition)

Initially, my workflow was purely visual. I would define [SerializeField] InputAction myAction directly in the script and configure the bindings inside the Unity Inspector.

  • The Flaw: This leads to "Inspector Bloat." Every script becomes a mess of settings. If you delete a script or move a GameObject, you lose your bindings. It’s impossible to track globally.

Level 2: The Hardcoded String Phase

My first thought for an "enhancement" was to centralize. I thought, "What if I just use the OpenXR string paths (like <XRController>{RightHand}/trigger) directly in my code?"

  • The Flaw: This felt "pro" because it was in code, but it was brittle. A single typo (writing "triger" instead of "trigger") meant a silent failure. Plus, there was zero auto-complete (Intellisense).

Level 3: The XRI Code-Gen Realisation

Then I discovered the "Generate C# Class" option under the input action asset turning the actions defined in the UI into a strongly-typed script, accessible throughout your codebase.

  • The Realisation: No more strings! I could finally call input.Activate.performed.

  • The New Problem: The naming was too opinionated. XRI samples call the A-button "Jump" or the Trigger "Activate." For my custom logic, those names were confusing and lacked clarity.

Level 4: The Universal Master Asset (Final Form)

I finally reached a professional standard: Creating my own Universal Input Action Asset. I defined my own schema with names that actually made sense—PrimaryButtonA, TriggerValue, GripButton. This ensured it remained compatible with every headset on the market that was built on OpenXR.


System Architecture: XRInputManager Service

The final result is a centralized XRInputManager that acts as the "Input Heart" of the application. It isn't just a script; it's a implementation of several SOLID principles and Design Patterns.

1. The Singleton Pattern

I used a Thread-Safe Singleton. This provides a single, global access point (XRInputManager.Instance) so that any script—whether it's a weapon system, a teleportation logic, or a menu—can access input without needing a direct reference to a GameObject.

2. The Observer Pattern (Decoupling)

This is the most critical part of the system. The XRInputManager doesn't tell other scripts what to do. Instead, it broadcasts events.

  • It exposes public event Action<float> OnTriggerPressed.

  • Why it's Pro: The Player script doesn't need to know how the input is calculated; it just "subscribes" to the event. This is the Dependency Inversion Principle in action—high-level logic shouldn't depend on low-level input details.

3. Hardware Abstraction Layer

By using the auto-generated class from my custom asset, I created a layer of abstraction.

  • The Manager talks to the generated code.

  • The Game Scripts talk to the Manager.

    If I ever decide to change the "Select" button from the Grip to the Trigger, I change it in the Asset UI. Not a single line of game logic C# changes.

4. Single Responsibility Principle (SRP)

The XRInputManager has one job: Translate hardware signals into game-ready events. It doesn't handle player movement; it doesn't handle shooting. It only handles the "delivery" of the intent.