The Universal OpenXR Input System
From Inspector bindings to a personalised 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.



