> ## 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.

# Implement custom VR interactions

> Create custom VR interaction components extending MolcaInteractionBase for specialized interaction behaviors.

**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

* **SDK modules**: Molca Core, Molca VR SDK installed
* **Unity setup**: RuntimeManager configured, XR Plugin Management installed, XR Interaction Toolkit package added
* **Prior knowledge**: [MolcaInteractionBase](/vr/interactions/molca-interaction-base), [Base components](/vr/base-components), [RuntimeManager](/core/runtime-manager)

## 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.

```csharp theme={null}
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.

```csharp theme={null}
// 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.

```csharp theme={null}
// 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.

```csharp theme={null}
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.

```csharp theme={null}
// 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.

```csharp theme={null}
// 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:

```csharp theme={null}
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.

## Related

* [MolcaInteractionBase](/vr/interactions/molca-interaction-base) — base class API and interaction lifecycle
* [Base components](/vr/base-components) — shared VR base scripts overview
* [Valve interaction](/vr/interactions/valve-interaction) — example analog interaction with rotation
* [Knob interaction](/vr/interactions/knob-interaction) — example analog interaction with twist
* [Interactable grab object](/vr/implementation/interactable-object) — complete grab interaction setup with visual feedback
* [ReferenceableComponent](/core/reference-system) — reference system for step-based lookups
* [RuntimeManager](/core/runtime-manager) — initialization and subsystem access
* [Recipe: Set up a VR scenario](/recipes/setup-vr-scenario) — foundational VR scenario setup
