Most Common Methods

Methods are blocks of code defined within a class to perform specific actions. Think of them as individual instructions or steps in a manual: when you need a particular action, you call the method, and it executes the set of instructions written inside it. This helps organize code by allowing you to reuse and group related actions, making your code more structured and easier to manage.

OnDestroy()

The OnDestroy() method is automatically called when an object is about to be destroyed. It is used to clean up resources, such as unsubscribing from events or stopping coroutines, to ensure that no unwanted behavior or memory leaks occur.

Declaration Note:
Because the base class MVRScriptdoes not define OnDestroy() as a virtual method, you should declare your cleanup method without the override keyword:

private void OnDestroy() {
    // Cleanup code here.
}

Raycasting in VAM

Raycasting is a fundamental technique that lets you detect what’s under the mouse pointer or along a specific direction in your scene. It’s especially useful for interactive plugins—like snapping an object to the floor when you click on it.

Ray: An invisible line cast from a point in a specified direction. Raycast: The process of projecting a ray and checking for intersections with colliders in the scene. Hit Information: If the ray intersects a collider, the returned RaycastHit object contains details like the exact point of collision. Steps to Use Raycasting in Your Plugin:

Generate a Ray: Use the main camera to create a ray from the current mouse position.

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

Perform the Raycast: Call Physics.Raycast to check if the ray hits any colliders within a maximum distance.

RaycastHit hit;
if (Physics.Raycast(ray, out hit, 1000f))
{
    // hit.point now holds the world-space coordinates of the collision.
}

Use the Hit Data: For example, if you want to move an atom to the point where the ray hits (such as snapping a Cube atom to the floor), you can do the following:

Atom cubeAtom = SuperController.singleton.GetAtomByUid("Cube");
if (cubeAtom != null)
{
    cubeAtom.transform.position = hit.point;
    SuperController.LogMessage("Cube atom snapped to: " + hit.point);
}
else
{
    SuperController.LogError("Cube atom not found.");
}

Complete Example: This plugin (SnapCubePrecisePlugin) demonstrates how to precisely reposition an atom using raycasting in a VaM scene. When the plugin initializes, it retrieves an atom with the UID “Cube” and sets up a JSONStorable toggle (“EnableSnap”) for activating snap mode. When snap mode is enabled and the user clicks the left mouse button, the plugin casts a ray from the main camera through the mouse pointer into the scene. If the ray hits a collider, it clears the Cube’s parent (so any container offsets are removed) and then retrieves the Cube’s main control node via its “control” storable. The plugin then moves that control node to the raycast hit point, effectively snapping the Cube to the clicked position. This method ensures that the Cube is positioned accurately by working in world space and accounting for potential offsets.

using System.Collections;
using UnityEngine;

public class SnapCubePrecisePlugin : MVRScript
{
    private JSONStorableBool enableSnap;
    private Atom cubeAtom;

    public override void Init()
    {
        // Create a toggle to enable/disable snap mode.
        enableSnap = new JSONStorableBool("EnableSnap", false, OnEnableSnapChanged);
        RegisterBool(enableSnap);
        CreateToggle(enableSnap, false);

        // Attempt to retrieve the Cube atom by its UID.
        cubeAtom = SuperController.singleton.GetAtomByUid("Cube");
        if (cubeAtom == null)
        {
            SuperController.LogError("Cube atom not found. Ensure an atom with UID 'Cube' exists.");
        }
    }

    private void OnEnableSnapChanged(bool value)
    {
        if (value)
            SuperController.LogMessage("Snap mode enabled. Click on the floor to snap the Cube.");
        else
            SuperController.LogMessage("Snap mode disabled.");
    }

    // Update is not marked as override since MVRScript doesn't have a virtual Update.
    public void Update()
    {
        if (enableSnap.val && Input.GetMouseButtonDown(0))
        {
            // Cast a ray from the main camera based on mouse position.
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 1000f))
            {
                // For accurate snapping, clear the atom's parent (if any) so that world position is applied directly.
                cubeAtom.ClearParentAtom();

                // Retrieve the main controller from the Cube's storables.
                FreeControllerV3 mainController = cubeAtom.GetStorableByID("control") as FreeControllerV3;
                if (mainController != null)
                {
                    // Move the main controller's transform to the hit point.
                    mainController.transform.position = hit.point;
                    SuperController.LogMessage("Cube snapped to: " + hit.point);
                }
                else
                {
                    SuperController.LogError("Failed to retrieve the main controller for the Cube atom.");
                }
            }
        }
    }
}

Below is a demonstration of what you can achieve by combining raycasting, hotkeys, and smooth movement using Vector3.Lerp. This plugin lays the foundation for a simple path-finding navigation system in VaM. When you hold down the Ctrl key and left-click on the floor, a ray is cast from the camera to determine the exact point where you clicked. If a collider is hit, any ongoing navigation is interrupted, and the Person atom’s control node is rotated to face the new target using smooth spherical interpolation (Quaternion.Slerp). Then, using a constant speed calculated from the distance to the target, the control node moves toward that point via Vector3.Lerp. This approach not only makes the motion smooth and uniform regardless of the distance but also shows how combining these core techniques can create interactive and responsive navigation systems in VaM.

using System.Collections;
using UnityEngine;

public class InterruptiblePersonPathMovePlugin : MVRScript
{
    private JSONStorableBool enableSnap;
    private JSONStorableBool useCtrlHotkey;
    private Atom personAtom;
    private FreeControllerV3 personController;
    private Coroutine currentNavigation = null;
    
    // Constant speed (units per second) and fixed rotation duration.
    private const float moveSpeed = 2f;
    private const float rotateDuration = 0.5f;

    public override void Init()
    {
        // Create and register a toggle for snap mode.
        enableSnap = new JSONStorableBool("EnableSnap", false, OnEnableSnapChanged);
        RegisterBool(enableSnap);
        CreateToggle(enableSnap, false);

        // Create and register a toggle to require Ctrl+Click.
        useCtrlHotkey = new JSONStorableBool("UseCtrlHotkey", true, OnUseCtrlHotkeyChanged);
        RegisterBool(useCtrlHotkey);
        CreateToggle(useCtrlHotkey, false);

        // Retrieve the Person atom by its UID.
        personAtom = SuperController.singleton.GetAtomByUid("Person");
        if (personAtom == null)
        {
            SuperController.LogError("Person atom not found. Ensure an atom with UID 'Person' exists.");
            return;
        }

        // Retrieve the main control node of the Person (stored as "control").
        personController = personAtom.GetStorableByID("control") as FreeControllerV3;
        if (personController == null)
        {
            SuperController.LogError("Failed to retrieve the control node of the Person atom.");
        }
    }

    private void OnEnableSnapChanged(bool value)
    {
        if (value)
            SuperController.LogMessage("Snap mode enabled. Click on the floor (with Ctrl if enabled) to move the Person.");
        else
            SuperController.LogMessage("Snap mode disabled.");
    }

    private void OnUseCtrlHotkeyChanged(bool value)
    {
        SuperController.LogMessage("UseCtrlHotkey set to: " + value);
    }

    // Update is used to listen for mouse clicks.
    public void Update()
    {
        if (enableSnap.val && Input.GetMouseButtonDown(0))
        {
            // If the Ctrl hotkey toggle is enabled, require that Ctrl is pressed.
            if (useCtrlHotkey.val && !(Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)))
                return;

            // Cast a ray from the main camera using the mouse position.
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 1000f))
            {
                // If already navigating, interrupt it.
                if (currentNavigation != null)
                {
                    StopCoroutine(currentNavigation);
                    currentNavigation = null;
                    SuperController.LogMessage("Navigation interrupted.");
                }
                // Start a new navigation coroutine.
                if (personController != null)
                    currentNavigation = StartCoroutine(MovePersonToTarget(personController, hit.point));
            }
        }
    }

    private IEnumerator MovePersonToTarget(FreeControllerV3 controller, Vector3 targetPoint)
    {
        // Clear any parent so movement uses world space.
        personAtom.ClearParentAtom();

        // Use current position as starting point.
        Vector3 startPosition = controller.transform.position;
        Vector3 direction = (targetPoint - startPosition).normalized;

        // Calculate target rotation to face the new destination.
        Quaternion startRotation = controller.transform.rotation;
        Quaternion targetRotation = Quaternion.LookRotation(direction, Vector3.up);

        // Smoothly rotate over a fixed duration.
        float elapsed = 0f;
        while (elapsed < rotateDuration)
        {
            controller.transform.rotation = Quaternion.Slerp(startRotation, targetRotation, elapsed / rotateDuration);
            elapsed += Time.deltaTime;
            yield return null;
        }
        controller.transform.rotation = targetRotation;

        // Calculate move duration to maintain constant speed.
        float distance = Vector3.Distance(startPosition, targetPoint);
        float moveDuration = distance / moveSpeed;

        elapsed = 0f;
        while (elapsed < moveDuration)
        {
            controller.transform.position = Vector3.Lerp(startPosition, targetPoint, elapsed / moveDuration);
            elapsed += Time.deltaTime;
            yield return null;
        }
        controller.transform.position = targetPoint;

        SuperController.LogMessage("Person moved to: " + targetPoint);
        currentNavigation = null;
    }
}

Prev