Controller Setup Tutorial
This tutorial will guide you through setting up and customizing controller input in your XR application using the QWR SDK. You'll learn how to configure controller inputs, create custom input actions, and implement haptic feedback.
Prerequisites
Before starting this tutorial, make sure you have:
- Completed the Quick Start Tutorial
- A VR device with controllers (e.g., Meta Quest, Valve Index)
- QWR SDK properly installed in your Unity project
Step 1: Create a New Scene
- Create a new scene (File > New Scene)
- Save the scene as "ControllerSetupDemo"
- Add a QWR XR Origin prefab to your scene (from
Packages/QWR Core/Prefabs/QWR_XROrigin) - Add a ground plane (3D Object > Plane) and position it at (0, 0, 0)
Step 2: Explore Default Controller Setup
Let's first examine the default controller setup in the QWR SDK:
- In the Hierarchy, expand the QWR XR Origin > Camera Offset
- Notice the Left Controller and Right Controller objects
- Select one of the controllers and examine its components in the Inspector:
- QWRControllerVisualizer: Handles the visual representation of the controller
- QWRControllerInteractor: Handles interactions with objects
- QWRUIInteractor: Handles interactions with UI elements
Step 3: Customize Controller Visualization
Let's customize how the controllers look:
-
Select the Left Controller in the Hierarchy
-
In the Inspector, find the QWRControllerVisualizer component
-
Customize the following properties:
- Ray Color: Change to a color of your choice
- Ray Thickness: Set to 0.002
- Show Ray On Hover Only: Enable this option
- Button Highlight Color: Change to a color of your choice
-
Repeat for the Right Controller with different settings if desired
Step 4: Create Controller Input Monitor
Let's create a script to monitor controller input:
- Create a new C# script named "ControllerInputMonitor" with the following code:
using UnityEngine;
using UnityEngine.UI;
using QWR.Core.Input;
using QWR.Core.Tracking;
public class ControllerInputMonitor : MonoBehaviour
{
[Header("Left Controller UI")]
[SerializeField] private Slider leftTriggerSlider;
[SerializeField] private Slider leftGripSlider;
[SerializeField] private RectTransform leftThumbstickIndicator;
[SerializeField] private Image leftPrimaryButtonImage;
[SerializeField] private Image leftSecondaryButtonImage;
[SerializeField] private Image leftThumbstickClickImage;
[Header("Right Controller UI")]
[SerializeField] private Slider rightTriggerSlider;
[SerializeField] private Slider rightGripSlider;
[SerializeField] private RectTransform rightThumbstickIndicator;
[SerializeField] private Image rightPrimaryButtonImage;
[SerializeField] private Image rightSecondaryButtonImage;
[SerializeField] private Image rightThumbstickClickImage;
[Header("Colors")]
[SerializeField] private Color normalColor = Color.gray;
[SerializeField] private Color pressedColor = Color.green;
private QWRInputManager inputManager;
private void Start()
{
// Get input manager
inputManager = QWRInputManager.Instance;
if (inputManager == null)
{
Debug.LogError("QWRInputManager not found!");
return;
}
// Subscribe to input events
inputManager.OnPrimaryButtonPressed += HandlePrimaryButtonPressed;
inputManager.OnPrimaryButtonReleased += HandlePrimaryButtonReleased;
inputManager.OnSecondaryButtonPressed += HandleSecondaryButtonPressed;
inputManager.OnSecondaryButtonReleased += HandleSecondaryButtonReleased;
inputManager.OnThumbstickClicked += HandleThumbstickClicked;
inputManager.OnThumbstickReleased += HandleThumbstickReleased;
// Initialize UI elements
ResetButtonColors();
}
private void OnDestroy()
{
// Unsubscribe from input events
if (inputManager != null)
{
inputManager.OnPrimaryButtonPressed -= HandlePrimaryButtonPressed;
inputManager.OnPrimaryButtonReleased -= HandlePrimaryButtonReleased;
inputManager.OnSecondaryButtonPressed -= HandleSecondaryButtonPressed;
inputManager.OnSecondaryButtonReleased -= HandleSecondaryButtonReleased;
inputManager.OnThumbstickClicked -= HandleThumbstickClicked;
inputManager.OnThumbstickReleased -= HandleThumbstickReleased;
}
}
private void Update()
{
if (inputManager == null)
return;
// Update trigger values
if (leftTriggerSlider != null)
leftTriggerSlider.value = inputManager.GetTriggerValue(HandType.Left);
if (rightTriggerSlider != null)
rightTriggerSlider.value = inputManager.GetTriggerValue(HandType.Right);
// Update grip values
if (leftGripSlider != null)
leftGripSlider.value = inputManager.GetGripValue(HandType.Left);
if (rightGripSlider != null)
rightGripSlider.value = inputManager.GetGripValue(HandType.Right);
// Update thumbstick positions
if (leftThumbstickIndicator != null)
{
Vector2 leftThumbstick = inputManager.GetThumbstickValue(HandType.Left);
leftThumbstickIndicator.anchoredPosition = leftThumbstick * 50f; // Scale for UI
}
if (rightThumbstickIndicator != null)
{
Vector2 rightThumbstick = inputManager.GetThumbstickValue(HandType.Right);
rightThumbstickIndicator.anchoredPosition = rightThumbstick * 50f; // Scale for UI
}
}
private void ResetButtonColors()
{
if (leftPrimaryButtonImage != null) leftPrimaryButtonImage.color = normalColor;
if (leftSecondaryButtonImage != null) leftSecondaryButtonImage.color = normalColor;
if (leftThumbstickClickImage != null) leftThumbstickClickImage.color = normalColor;
if (rightPrimaryButtonImage != null) rightPrimaryButtonImage.color = normalColor;
if (rightSecondaryButtonImage != null) rightSecondaryButtonImage.color = normalColor;
if (rightThumbstickClickImage != null) rightThumbstickClickImage.color = normalColor;
}
private void HandlePrimaryButtonPressed(HandType handType)
{
if (handType == HandType.Left && leftPrimaryButtonImage != null)
leftPrimaryButtonImage.color = pressedColor;
else if (handType == HandType.Right && rightPrimaryButtonImage != null)
rightPrimaryButtonImage.color = pressedColor;
}
private void HandlePrimaryButtonReleased(HandType handType)
{
if (handType == HandType.Left && leftPrimaryButtonImage != null)
leftPrimaryButtonImage.color = normalColor;
else if (handType == HandType.Right && rightPrimaryButtonImage != null)
rightPrimaryButtonImage.color = normalColor;
}
private void HandleSecondaryButtonPressed(HandType handType)
{
if (handType == HandType.Left && leftSecondaryButtonImage != null)
leftSecondaryButtonImage.color = pressedColor;
else if (handType == HandType.Right && rightSecondaryButtonImage != null)
rightSecondaryButtonImage.color = pressedColor;
}
private void HandleSecondaryButtonReleased(HandType handType)
{
if (handType == HandType.Left && leftSecondaryButtonImage != null)
leftSecondaryButtonImage.color = normalColor;
else if (handType == HandType.Right && rightSecondaryButtonImage != null)
rightSecondaryButtonImage.color = normalColor;
}
private void HandleThumbstickClicked(HandType handType)
{
if (handType == HandType.Left && leftThumbstickClickImage != null)
leftThumbstickClickImage.color = pressedColor;
else if (handType == HandType.Right && rightThumbstickClickImage != null)
rightThumbstickClickImage.color = pressedColor;
}
private void HandleThumbstickReleased(HandType handType)
{
if (handType == HandType.Left && leftThumbstickClickImage != null)
leftThumbstickClickImage.color = normalColor;
else if (handType == HandType.Right && rightThumbstickClickImage != null)
rightThumbstickClickImage.color = normalColor;
}
}
- Create a UI Canvas in your scene (if you don't already have one)
- Add a QWRUICanvas component to it
- Create two panels on the Canvas, one for each controller
- Add the following UI elements to each panel:
- Two sliders for trigger and grip
- A circular background with a small dot in the center for the thumbstick
- Three images for the primary button, secondary button, and thumbstick click
- Create an empty GameObject named "ControllerInputMonitor"
- Add the ControllerInputMonitor script to it
- Assign all the UI elements to the corresponding fields in the Inspector
Step 5: Create Haptic Feedback Examples
Let's create objects that provide different haptic feedback when grabbed:
- Create a new C# script named "HapticFeedbackObject" with the following code:
using UnityEngine;
using QWR.Core.Input;
using QWR.Core.Interaction;
using QWR.Core.Tracking;
public class HapticFeedbackObject : MonoBehaviour
{
public enum HapticPattern
{
Single,
Double,
Long,
Ramp,
Custom
}
[SerializeField] private HapticPattern pattern = HapticPattern.Single;
[SerializeField] private float amplitude = 0.5f;
[SerializeField] private float duration = 0.1f;
[SerializeField] private Material normalMaterial;
[SerializeField] private Material activeMaterial;
private QWRInteractable interactable;
private Renderer objectRenderer;
private QWRInputManager inputManager;
private void Awake()
{
interactable = GetComponent<QWRInteractable>();
objectRenderer = GetComponent<Renderer>();
inputManager = QWRInputManager.Instance;
if (interactable == null)
{
Debug.LogError("QWRInteractable component required!");
return;
}
// Subscribe to grab events
interactable.OnGrabbed += HandleGrabbed;
interactable.OnReleased += HandleReleased;
}
private void OnDestroy()
{
// Unsubscribe from events
if (interactable != null)
{
interactable.OnGrabbed -= HandleGrabbed;
interactable.OnReleased -= HandleReleased;
}
}
private void HandleGrabbed(QWRInteractor interactor)
{
// Change material
if (objectRenderer != null && activeMaterial != null)
{
objectRenderer.material = activeMaterial;
}
// Get hand type
HandType handType = HandType.Right;
QWRHandController handController = interactor.GetComponent<QWRHandController>();
if (handController != null)
{
handType = handController.HandType;
}
// Trigger appropriate haptic feedback
if (inputManager != null)
{
switch (pattern)
{
case HapticPattern.Single:
inputManager.TriggerHapticFeedback(handType, amplitude, duration);
break;
case HapticPattern.Double:
StartCoroutine(DoubleHapticPulse(handType));
break;
case HapticPattern.Long:
inputManager.TriggerHapticFeedback(handType, amplitude, 0.5f);
break;
case HapticPattern.Ramp:
StartCoroutine(RampHapticPulse(handType));
break;
case HapticPattern.Custom:
QWRHapticPattern customPattern = new QWRHapticPattern(
new QWRHapticPulse(amplitude, 0.05f),
new QWRHapticPulse(0f, 0.05f),
new QWRHapticPulse(amplitude, 0.05f),
new QWRHapticPulse(0f, 0.05f),
new QWRHapticPulse(amplitude, 0.2f)
);
inputManager.TriggerHapticPattern(handType, customPattern);
break;
}
}
}
private void HandleReleased(QWRInteractor interactor)
{
// Restore original material
if (objectRenderer != null && normalMaterial != null)
{
objectRenderer.material = normalMaterial;
}
}
private System.Collections.IEnumerator DoubleHapticPulse(HandType handType)
{
inputManager.TriggerHapticFeedback(handType, amplitude, duration);
yield return new WaitForSeconds(0.1f);
inputManager.TriggerHapticFeedback(handType, amplitude, duration);
}
private System.Collections.IEnumerator RampHapticPulse(HandType handType)
{
float step = 0.1f;
for (float i = 0; i <= 1.0f; i += step)
{
inputManager.TriggerHapticFeedback(handType, i * amplitude, duration);
yield return new WaitForSeconds(duration);
}
}
}
- Create five cubes in your scene and position them in a row
- Add a Rigidbody component to each cube
- Add a QWRInteractable component to each cube with standard settings
- Create two materials: one for normal state and one for active state
- Add the HapticFeedbackObject script to each cube
- Configure each cube with a different haptic pattern
- Assign the normal and active materials to each cube
Step 6: Create Custom Input Actions
Let's create custom input actions for a specific game mechanic:
- Create a new C# script named "CustomInputHandler" with the following code:
using UnityEngine;
using UnityEngine.UI;
using QWR.Core.Input;
using QWR.Core.Tracking;
public class CustomInputHandler : MonoBehaviour
{
[SerializeField] private Text actionText;
[SerializeField] private float comboTimeWindow = 0.5f;
private QWRInputManager inputManager;
private bool leftTriggerPressed = false;
private bool rightTriggerPressed = false;
private float lastTriggerTime = 0f;
private void Start()
{
// Get input manager
inputManager = QWRInputManager.Instance;
if (inputManager == null)
{
Debug.LogError("QWRInputManager not found!");
return;
}
// Subscribe to input events
inputManager.OnTriggerPressed += HandleTriggerPressed;
inputManager.OnTriggerReleased += HandleTriggerReleased;
inputManager.OnGripPressed += HandleGripPressed;
// Clear action text
if (actionText != null)
{
actionText.text = "";
}
}
private void OnDestroy()
{
// Unsubscribe from input events
if (inputManager != null)
{
inputManager.OnTriggerPressed -= HandleTriggerPressed;
inputManager.OnTriggerReleased -= HandleTriggerReleased;
inputManager.OnGripPressed -= HandleGripPressed;
}
}
private void Update()
{
// Check for simultaneous trigger press (both triggers)
if (leftTriggerPressed && rightTriggerPressed)
{
DisplayAction("POWER MOVE: Dual Trigger!");
// Trigger haptic feedback on both controllers
if (inputManager != null)
{
inputManager.TriggerHapticFeedback(HandType.Left, 0.7f, 0.3f);
inputManager.TriggerHapticFeedback(HandType.Right, 0.7f, 0.3f);
}
// Reset state
leftTriggerPressed = false;
rightTriggerPressed = false;
}
}
private void HandleTriggerPressed(HandType handType)
{
// Record which trigger was pressed
if (handType == HandType.Left)
{
leftTriggerPressed = true;
lastTriggerTime = Time.time;
}
else if (handType == HandType.Right)
{
rightTriggerPressed = true;
lastTriggerTime = Time.time;
}
// Display basic action
DisplayAction($"{handType} Trigger Pressed");
}
private void HandleTriggerReleased(HandType handType)
{
// Reset trigger state
if (handType == HandType.Left)
{
leftTriggerPressed = false;
}
else if (handType == HandType.Right)
{
rightTriggerPressed = false;
}
}
private void HandleGripPressed(HandType handType)
{
// Check for combo (grip pressed shortly after trigger)
if (Time.time - lastTriggerTime < comboTimeWindow)
{
DisplayAction($"COMBO: {handType} Trigger + Grip!");
// Trigger haptic feedback
if (inputManager != null)
{
inputManager.TriggerHapticFeedback(handType, 0.8f, 0.2f);
}
}
else
{
// Display basic action
DisplayAction($"{handType} Grip Pressed");
}
}
private void DisplayAction(string action)
{
if (actionText != null)
{
actionText.text = action;
// Clear text after a delay
CancelInvoke("ClearActionText");
Invoke("ClearActionText", 2.0f);
}
}
private void ClearActionText()
{
if (actionText != null)
{
actionText.text = "";
}
}
}
- Create a Text element on your UI Canvas
- Create an empty GameObject named "CustomInputHandler"
- Add the CustomInputHandler script to it
- Assign the Text element to the Action Text field in the Inspector
Step 7: Create a Controller Model Switcher
Let's create a system to switch between different controller models:
- Create a new C# script named "ControllerModelSwitcher" with the following code:
using UnityEngine;
using UnityEngine.UI;
using QWR.Core.Tracking;
public class ControllerModelSwitcher : MonoBehaviour
{
[SerializeField] private GameObject[] leftControllerModels;
[SerializeField] private GameObject[] rightControllerModels;
[SerializeField] private Button nextModelButton;
[SerializeField] private Text currentModelText;
private int currentModelIndex = 0;
private void Start()
{
// Set up button
if (nextModelButton != null)
{
nextModelButton.onClick.AddListener(SwitchToNextModel);
}
// Initialize controller models
UpdateControllerModel();
}
public void SwitchToNextModel()
{
// Increment model index
currentModelIndex = (currentModelIndex + 1) % leftControllerModels.Length;
// Update controller model
UpdateControllerModel();
}
private void UpdateControllerModel()
{
// Disable all models
for (int i = 0; i < leftControllerModels.Length; i++)
{
if (leftControllerModels[i] != null)
leftControllerModels[i].SetActive(false);
if (rightControllerModels[i] != null)
rightControllerModels[i].SetActive(false);
}
// Enable current model
if (currentModelIndex < leftControllerModels.Length && leftControllerModels[currentModelIndex] != null)
leftControllerModels[currentModelIndex].SetActive(true);
if (currentModelIndex < rightControllerModels.Length && rightControllerModels[currentModelIndex] != null)
rightControllerModels[currentModelIndex].SetActive(true);
// Update text
if (currentModelText != null)
{
currentModelText.text = $"Controller Model: {currentModelIndex + 1}";
}
}
}
- In the Hierarchy, find the controller model GameObjects under Left Controller and Right Controller
- Create arrays of different controller model prefabs
- Add a Button and Text element to your UI Canvas
- Create an empty GameObject named "ControllerModelSwitcher"
- Add the ControllerModelSwitcher script to it
- Assign the controller models, button, and text to the corresponding fields in the Inspector
Step 8: Create an Input Mode Switcher
Let's create a system to switch between hand tracking and controller input:
- Create a new C# script named "InputModeSwitcher" with the following code:
using UnityEngine;
using UnityEngine.UI;
using QWR.Core.Input;
public class InputModeSwitcher : MonoBehaviour
{
[SerializeField] private Button handsButton;
[SerializeField] private Button controllersButton;
[SerializeField] private Button autoButton;
[SerializeField] private Text currentModeText;
private QWRInputManager inputManager;
private void Start()
{
// Get input manager
inputManager = QWRInputManager.Instance;
if (inputManager == null)
{
Debug.LogError("QWRInputManager not found!");
return;
}
// Set up buttons
if (handsButton != null)
handsButton.onClick.AddListener(() => SetInputMode(InputMode.Hands));
if (controllersButton != null)
controllersButton.onClick.AddListener(() => SetInputMode(InputMode.Controllers));
if (autoButton != null)
autoButton.onClick.AddListener(() => SetInputMode(InputMode.Auto));
// Subscribe to input mode changes
inputManager.OnInputModeChanged += HandleInputModeChanged;
// Update UI
UpdateInputModeText(inputManager.InputMode);
}
private void OnDestroy()
{
// Unsubscribe from events
if (inputManager != null)
{
inputManager.OnInputModeChanged -= HandleInputModeChanged;
}
}
private void SetInputMode(InputMode mode)
{
if (inputManager != null)
{
inputManager.SetInputMode(mode);
}
}
private void HandleInputModeChanged(InputMode newMode)
{
UpdateInputModeText(newMode);
}
private void UpdateInputModeText(InputMode mode)
{
if (currentModeText != null)
{
currentModeText.text = $"Input Mode: {mode}";
}
}
}
- Add three Buttons and a Text element to your UI Canvas
- Label the buttons "Hands", "Controllers", and "Auto"
- Create an empty GameObject named "InputModeSwitcher"
- Add the InputModeSwitcher script to it
- Assign the buttons and text to the corresponding fields in the Inspector
Step 9: Test Your Scene
- Save your scene
- Click the Play button to enter Play Mode
- Test the various controller features:
- Monitor input values on the UI
- Grab the haptic feedback objects to feel different vibration patterns
- Try the custom input combinations (dual triggers, trigger+grip combo)
- Switch between different controller models
- Toggle between hand tracking and controller input modes
Step 10: Build and Deploy
To test on a real device with controllers:
- Go to File > Build Settings
- Add your current scene to the build
- Select your target platform (e.g., Android for Quest devices)
- Click "Switch Platform"
- Configure platform-specific settings if needed
- Click "Build" or "Build And Run"
Next Steps
Now that you've learned about controller setup and customization, you can:
- Explore the Multiplayer Setup Tutorial to add networking capabilities
- Check out the Hand Interaction Tutorial if you haven't already
- Review the Controller Support Documentation for more detailed information
- Create your own custom controller interactions and input schemes
Troubleshooting
Controllers Not Detected
- Ensure your controllers are powered on and paired with your device
- Check that the correct interaction profiles are enabled in OpenXR settings
- Verify that controller support is enabled in the XR Plugin Management
Input Actions Not Working
- Check that the input action mappings match your controller type
- Verify that the QWRInputManager is properly initialized
- Ensure you're subscribed to the correct input events
Haptic Feedback Not Working
- Check that your controllers support haptic feedback
- Verify that the correct XR plugin is enabled
- Ensure the amplitude values are in the correct range (0-1)