Documentation Index
Fetch the complete documentation index at: https://docs-unity.molca.id/llms.txt
Use this file to discover all available pages before exploring further.
Applies to: Molca VR SDK
Overview
This recipe shows you how to create custom VR interaction components by extending MolcaInteractionBase. You’ll implement specialized interaction logic that responds to XR Interaction Toolkit events, integrate with the reference system for step-based validation, and add custom per-frame behavior while users interact with objects. This approach is ideal for creating unique interactions beyond standard grab mechanics—like levers, switches, dials, or custom analog controls.
Prerequisites
Step-by-step
Step 1: Create a custom interaction class
Create a new C# script that extends MolcaInteractionBase and implements the UpdateInteraction() method for per-frame logic.
using MolcaSDK.VR;
using UnityEngine;
/// <summary>
/// Custom lever interaction that rotates based on hand position.
/// </summary>
public class LeverInteraction : MolcaInteractionBase
{
[Header("Lever Settings")]
[SerializeField] private Transform leverHandle;
[SerializeField] private float minAngle = -45f;
[SerializeField] private float maxAngle = 45f;
[SerializeField] private Vector3 rotationAxis = Vector3.right;
private float _currentAngle;
protected override void UpdateInteraction()
{
// Called every frame while user is interacting
if (currentInteractor == null || leverHandle == null) return;
// Calculate angle based on hand position
Vector3 handPosition = currentInteractor.transform.position;
Vector3 toHand = handPosition - leverHandle.position;
float targetAngle = Vector3.SignedAngle(leverHandle.forward, toHand, rotationAxis);
// Clamp to limits
_currentAngle = Mathf.Clamp(targetAngle, minAngle, maxAngle);
// Apply rotation
leverHandle.localRotation = Quaternion.AngleAxis(_currentAngle, rotationAxis);
}
}
Why this works: MolcaInteractionBase automatically binds to the XRBaseInteractable on the same GameObject during Awake, tracks selectEntered/selectExited events, and calls UpdateInteraction() only while isInteracting is true. You focus on the interaction logic without managing XRI event wiring.
Step 2: Add XR Interactable component
Add an XRGrabInteractable (or XRSimpleInteractable) component to the same GameObject as your custom interaction script.
// In the Unity Editor:
// 1. Select the GameObject with your LeverInteraction script
// 2. Add Component → XR → XR Grab Interactable
// 3. Configure XRGrabInteractable:
// - Select Mode: Single (one hand only)
// - Movement Type: Instantaneous (no physics)
// - Throw on Detach: Disabled (lever stays in place)
Why this works: MolcaInteractionBase.Awake() automatically discovers the XRBaseInteractable on the same GameObject via GetComponent<XRBaseInteractable>(). If you need to reference an interactable on a different GameObject, assign it manually to the interactable field in the Inspector.
Step 3: Add ReferenceableComponent for step integration
Add a ReferenceableComponent to enable reference-based lookups from VR steps.
// In the Unity Editor:
// 1. Select the GameObject with your LeverInteraction
// 2. Add Component → Molca → Core → Referenceable Component
// 3. The component auto-generates a stable ID via OnValidate
// 4. Note the Reference ID for use in custom steps
Why this works: MolcaInteractionBase registers itself with ReferenceManager after RuntimeManager.WaitForInitialization() completes. This allows VR steps to find your interaction component by reference ID using ReferenceManager.GetReference<LeverInteraction>(refId).
Step 4: Implement interaction events
Add Unity events or C# events to expose interaction state changes to other systems.
using MolcaSDK.VR;
using UnityEngine;
using UnityEngine.Events;
public class LeverInteraction : MolcaInteractionBase
{
[Header("Lever Settings")]
[SerializeField] private Transform leverHandle;
[SerializeField] private float minAngle = -45f;
[SerializeField] private float maxAngle = 45f;
[SerializeField] private Vector3 rotationAxis = Vector3.right;
[Header("Events")]
public UnityEvent<float> onAngleChanged;
public UnityEvent onMinReached;
public UnityEvent onMaxReached;
private float _currentAngle;
private bool _wasAtMin;
private bool _wasAtMax;
protected override void UpdateInteraction()
{
if (currentInteractor == null || leverHandle == null) return;
Vector3 handPosition = currentInteractor.transform.position;
Vector3 toHand = handPosition - leverHandle.position;
float targetAngle = Vector3.SignedAngle(leverHandle.forward, toHand, rotationAxis);
_currentAngle = Mathf.Clamp(targetAngle, minAngle, maxAngle);
leverHandle.localRotation = Quaternion.AngleAxis(_currentAngle, rotationAxis);
// Fire angle changed event
onAngleChanged?.Invoke(_currentAngle);
// Fire limit events
if (_currentAngle <= minAngle && !_wasAtMin)
{
_wasAtMin = true;
onMinReached?.Invoke();
}
else if (_currentAngle >= maxAngle && !_wasAtMax)
{
_wasAtMax = true;
onMaxReached?.Invoke();
}
// Reset flags when leaving limits
if (_currentAngle > minAngle) _wasAtMin = false;
if (_currentAngle < maxAngle) _wasAtMax = false;
}
}
Why this works: Unity events appear in the Inspector and can be wired to other components without code. C# events (using System.Action or custom delegates) provide type-safe callbacks for code-based integration. Both patterns work well with the step system for completion validation.
Step 5: Add optional distance-based force release
Configure maxInteractionDistance to automatically release the interaction when the hand moves too far away.
// In the Unity Editor:
// 1. Select the GameObject with your LeverInteraction
// 2. In the Inspector, find "Max Interaction Distance"
// 3. Set to a positive value (e.g., 0.5 for 50cm max reach)
// 4. Set to 0 to disable distance checks
Why this works: MolcaInteractionBase checks distance every frame in Update() when maxInteractionDistance > 0. If the hand exceeds this distance, it calls ForceRelease() which triggers selectExited on the interactable, ending the interaction gracefully.
Step 6: Test in VR
Enter Play mode with VR active and test your custom interaction.
// Testing checklist:
// 1. Verify XRGrabInteractable highlights when hand hovers (XRI default behavior)
// 2. Grab the object and confirm UpdateInteraction() runs (add Debug.Log if needed)
// 3. Move hand and verify lever rotates within min/max limits
// 4. Release and confirm interaction stops (UpdateInteraction() no longer called)
// 5. Test distance-based release if enabled (move hand far away)
// 6. Check Unity events fire in Inspector (wire to Debug.Log for testing)
Why this works: MolcaInteractionBase handles all XRI event wiring automatically. Your UpdateInteraction() method only runs while the user is actively interacting, so you can safely assume currentInteractor is valid and isInteracting is true.
Complete example
Here’s a complete custom button interaction that requires the user to press and hold for a duration:
using MolcaSDK.VR;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.XR.Interaction.Toolkit;
/// <summary>
/// Button interaction that requires press-and-hold for a duration.
/// </summary>
public class HoldButtonInteraction : MolcaInteractionBase
{
[Header("Button Settings")]
[SerializeField] private Transform buttonVisual;
[SerializeField] private float pressDepth = 0.02f;
[SerializeField] private float holdDuration = 2f;
[SerializeField] private AnimationCurve pressCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
[Header("Events")]
public UnityEvent onPressed;
public UnityEvent<float> onHoldProgress; // 0-1 progress
public UnityEvent onHoldComplete;
public UnityEvent onReleased;
private Vector3 _initialPosition;
private float _holdTimer;
private bool _isPressed;
private bool _holdCompleted;
private void Start()
{
if (buttonVisual != null)
{
_initialPosition = buttonVisual.localPosition;
}
}
protected override void UpdateInteraction()
{
if (currentInteractor == null || buttonVisual == null) return;
// Calculate press depth based on hand proximity
Vector3 handPosition = currentInteractor.transform.position;
float distance = Vector3.Distance(handPosition, transform.position);
float pressAmount = Mathf.Clamp01(1f - (distance / pressDepth));
float curvedPress = pressCurve.Evaluate(pressAmount);
// Update visual position
Vector3 targetPosition = _initialPosition - (transform.forward * pressDepth * curvedPress);
buttonVisual.localPosition = targetPosition;
// Track press state
if (pressAmount > 0.8f && !_isPressed)
{
_isPressed = true;
onPressed?.Invoke();
}
// Track hold duration
if (_isPressed && !_holdCompleted)
{
_holdTimer += Time.deltaTime;
float progress = Mathf.Clamp01(_holdTimer / holdDuration);
onHoldProgress?.Invoke(progress);
if (_holdTimer >= holdDuration)
{
_holdCompleted = true;
onHoldComplete?.Invoke();
}
}
}
protected override void OnSelectExited(SelectExitEventArgs args)
{
base.OnSelectExited(args);
// Reset state on release
if (buttonVisual != null)
{
buttonVisual.localPosition = _initialPosition;
}
onReleased?.Invoke();
_isPressed = false;
_holdTimer = 0f;
_holdCompleted = false;
}
}
This button interaction provides visual feedback, tracks hold progress, and fires events at key moments. Wire onHoldComplete to a custom step’s Complete() method for step-based validation.
Troubleshooting
- UpdateInteraction() never called: Verify the GameObject has an
XRBaseInteractable component (XRGrabInteractable or XRSimpleInteractable). Check that the interactable is enabled and the XR rig is active. Ensure isInteracting is true by adding Debug.Log(isInteracting) in Update().
- Interactable field is null:
MolcaInteractionBase.Awake() only searches the same GameObject. If your interactable is on a child or parent, assign it manually in the Inspector to the interactable field.
- Reference not found by steps: Ensure
ReferenceableComponent is attached and has a stable ID (check Inspector). Verify RuntimeManager.WaitForInitialization() completes before steps try to resolve references. Check that ReferenceManager subsystem is active.
- Interaction releases unexpectedly: Check
maxInteractionDistance value. If set too low, the hand will exceed the distance and trigger ForceRelease(). Set to 0 to disable distance checks entirely.
- Events don’t fire: Verify Unity events are wired in the Inspector (expand the event section and add listeners). For C# events, ensure subscribers are registered in
OnEnable and unregistered in OnDisable to avoid memory leaks.
- Hand position jitter: Use smoothing or damping on hand position calculations. Consider using
Mathf.Lerp() or Vector3.Lerp() to smooth rotation/position updates. Check XR tracking quality in your headset settings.