Skip to main content
Version: 2.2.0

Simple Position Control Tutorial

This tutorial demonstrates how to control the cursor position of an Inverse3 device with a dynamic game object within a Unity scene, building upon concepts introduced in the Quick Start Guide.

Introduction

The Inverse3 device supports two control modes: Force Control and Position Control. While the former adjusts the force based on the cursor's position, the latter directly manipulates the cursor's position. This tutorial, leveraging the scene setup from Force Feedback in a Dynamic Scene Tutorial, focuses on employing CursorSetPosition within the FixedUpdate method to enable the device's cursor to follow a moving game object.

Scene Setup

Begin with the scene configuration described in the Force Feedback in a Dynamic Scene Tutorial, which includes a Haptic Rig and a Moving Ball game object controlled by the MovableObject component.

Implementing SpherePositionControl Component

Replace the SphereForceFeedback component on the Moving Ball GameObject with a new C# script named SpherePositionControl.cs. Define the following properties within the SpherePositionControl class:

public Inverse3 inverse3;
public float minSyncDistance = 0.05f;
private bool _isCursorSynchronized;
  • inverse3: Reference to the Inverse3 device component, set via the inspector.
  • minSyncDistance: Minimum distance threshold to initiate cursor synchronization.
  • _isCursorSynchronized: Flag indicating whether the cursor's movement is synchronized with the Moving Ball.

Synchronization Logic

Implement methods to start and stop cursor synchronization based on proximity to the Moving Ball:

private void StartSynchronizeCursor()
{
var cursorPosition = inverse3.Cursor.transform.position;
GetComponent<MovableObject>().SetTargetPosition(cursorPosition, teleport: true);
_isCursorSynchronized = true;
}

private void StopSynchronizeCursor()
{
_isCursorSynchronized = !inverse3.Release();
}

Here we use the MovableObject.SetTargetPosition(position, teleport:true) which teleport the MovingBall to the cursor position.

Update and FixedUpdate Methods

In the Update method, toggle cursor synchronization based on the distance between the cursor and the Moving Ball:

private void Update()
{
var distance = Vector3.Distance(inverse3.CursorPosition, transform.position);
if (!_isCursorSynchronized && distance <= minSyncDistance)
{
StartSynchronizeCursor();
}
else if (_isCursorSynchronized && distance > minSyncDistance)
{
StopSynchronizeCursor();
}
}

In FixedUpdate, update the cursor's position to match the Moving Ball if synchronization is active:

private void FixedUpdate()
{
if (_isCursorSynchronized)
{
inverse3.CursorSetPosition(transform.position);
}
}

Use the FixedUpdate method to ensure position control operates at a stable framerate, independent of graphical rendering performance.

Initialization

In Awake(), ensure the Moving Ball starts in a position accessible by the Inverse3 device, regardless of its handedness:

private void Awake()
{
inverse3.Ready.AddListener(device =>
{
GetComponent<MovableObject>().SetTargetPosition(((Inverse3)device).WorkspaceCenter, teleport: true);
});
}

Gameplay Experience

Secure the Inverse3 device and ensure it has ample space. Activating Play Mode and approaching the Moving Ball with the cursor will cause the Inverse3 to follow the Moving Ball's movements. Keyboard inputs can be used to move the Moving Ball, demonstrating the cursor's ability to track its position.

simple-position-control

Source Files

The complete scene and associated files for this tutorial can be found in the Tutorials sample within the Unity Package Manager. This includes the MovableObject script, which is utilized in several examples to control game object movement via keyboard inputs.

SpherePositionControl.cs

/*
* Copyright 2024 Haply Robotics Inc. All rights reserved.
*/

using Haply.Inverse.Unity;
using Haply.Samples.Tutorials.Utils;
using UnityEngine;

namespace Haply.Samples.Tutorials._5_SimplePositionControl
{
/// <summary>
/// Controls the Inverse3 cursor position based on the current position of this GameObject.
/// When the GameObject is within a specified distance from the cursor, it initiates synchronized control,
/// allowing the cursor to follow the GameObject's movements.
/// </summary>
[RequireComponent(typeof(MovableObject))]
public class SpherePositionControl : MonoBehaviour
{
public Inverse3 inverse3;

[Tooltip("Minimum distance required to initiate synchronized control between this GameObject and the Inverse3 cursor.")]
[Range(0, 1)]
public float minSyncDistance = 0.05f;

private bool _isCursorSynchronized;

private void Awake()
{
// Ensure inverse3 is set, finding it in the scene if necessary.
if (inverse3 == null)
{
inverse3 = FindObjectOfType<Inverse3>();
}

// When inverse3 is ready, so the handedness is defined
inverse3.Ready.AddListener(device =>
{
// Teleport the sphere to its workspace center to ensure it can be reached,
// regardless of whether the device is left or right-handed. This ensures the GameObject starts in a
// position that is accessible by the Inverse3 device.
GetComponent<MovableObject>().SetTargetPosition(device.WorkspaceCenterPosition, teleport:true);
});
}

private void OnDisable()
{
// Ensure movement synchronization is disabled when the component is disabled.
StopSynchronizeCursor();
}

private void Update()
{
// Calculate the distance between the Inverse3 position and this object's position.
var distance = Vector3.Distance(inverse3.CursorPosition, transform.position);

// Enable synchronized movement if within the minimum sync distance and not already synced.
if (!_isCursorSynchronized && distance <= minSyncDistance)
{
StartSynchronizeCursor();
}
// Disable synchronized movement if outside the minimum sync distance and currently synced.
else if (_isCursorSynchronized && distance > minSyncDistance)
{
StopSynchronizeCursor();
}
}

private void FixedUpdate()
{
if (_isCursorSynchronized)
{
// If in sync, set the Inverse3 cursor position to this object's position.
inverse3.CursorSetPosition(transform.position);
}
}

private void StartSynchronizeCursor()
{
// Get the current cursor position.
var cursorPosition = inverse3.Cursor.transform.position;

// Teleport this object to the cursor position to avoid a sudden jump when position control starts.
GetComponent<MovableObject>().SetTargetPosition(cursorPosition, teleport:true);

// Start synchronizing the movement of this object with the cursor.
_isCursorSynchronized = true;
}

private void StopSynchronizeCursor()
{
// Stop synchronizing the movement.
_isCursorSynchronized = !inverse3.Release();
}
}
}