Zum Hauptinhalt springen

Tutorial zum haptischen Feedback in dynamischen Szenen

Auf der Grundlage des Tutorials zu den Grundlagen der haptischen Rückmeldungzeigt dieser Leitfaden, wie Sie in Unity dynamische Interaktionen simulieren und es dem Benutzer ermöglichen können, haptische Rückmeldungen von sich bewegenden Objekten wahrzunehmen. In diesem Szenario wird deutlich, wie wichtig eine hohe Aktualisierungsrate bei der haptischen Rückmeldung ist. Diese liegt weit über der üblichen Aktualisierungsrate der visuellen Darstellung in Unity.

Einleitung​

Für ein überzeugendes haptisches Erlebnis, insbesondere in dynamischen Szenen, ist es äußerst wichtig, Berechnungen mit Frequenzen von mindestens 1 kHz durchzuführen. Dies unterscheidet sich erheblich von der Aktualisierungsschleife eines Spiels, die normalerweise mit etwa 60 Hz läuft. Die Herausforderung besteht darin, diese hochfrequenten Aktualisierungen parallel zur Hauptspielschleife zu verwalten und einen threadsicheren Datenaustausch zu gewährleisten, um ein konsistentes und präzises haptisches Feedback aufrechtzuerhalten.

Erweiterung der grundlegenden Einrichtung der Kraft-Feedback-Funktion​

Grundlegendes KraftfeedbackWir beginnen mit der Einrichtung der Szene aus dem Tutorial. Um dynamische Bewegungen einzubauen,SphereForceFeedback Passen Sie das Skript so an, dass es auf die Bewegung der Kugel reagiert und die Interaktion mit sich dynamisch bewegenden Objekten simulieren kann.

Wichtigste Änderungen​

  • Bewegung dynamischer Objekte: Integrieren Sie eine Logik, die die Position und Geschwindigkeit von „Sphere“ auf der Grundlage von Benutzereingaben oder vordefinierten Bewegungsmustern aktualisiert.
  • Thread-sicherer Datenaustausch: ReaderWriterLockSlim Damit wird der gleichzeitige Zugriff auf gemeinsam genutzte Daten zwischen dem Haupt- und dem Kraftsensor-Thread gesteuert.
  • Anpassung der Kraftberechnung: SphereForceFeedback.ForceCalculation Durch eine Anpassung der Methode und die Berücksichtigung der Geschwindigkeit der Kugel wird ein realistisches Feedback bereitgestellt, das sowohl auf der Position als auch auf der Bewegung basiert.

Dynamische Interaktion​

Um eine sich bewegende Kugel zu simulieren,Update Sie können die Position manuell über eine Methode aktualisieren oder eine andere Komponente verwenden, die die Bewegung anhand von Tastatureingaben oder anderen Interaktionen steuert. In diesem BeispielSphäre Den Namen des Spielobjekts Bewegter Ball in … ändern undAnleitungen In der Probe enthalten MovingObject Fügen Sie eine Komponente hinzu.

Für Mobilgeräte optimiert ForceCalculation Anpassung​

Bei der Berechnung des haptischen Feedbacks muss nun die Geschwindigkeit des „Moving Ball“ berücksichtigt werden, sodass die Kraft sowohl auf der Grundlage der Position als auch der Geschwindigkeit der Interaktion angepasst wird. Dadurch wird ein feinfühligeres und realistischeres haptisches Feedback erzielt, das die dynamischen Eigenschaften der Interaktion widerspiegelt.

  • Vector3 Methodenparameter des Typs otherVelocity hinzufügen
  • force -= cursorVelocity * damping を force -= (cursorVelocity - otherVelocity) * damping ersetzt durch
private Vector3 ForceCalculation(Vector3 cursorPosition, Vector3 cursorVelocity, float cursorRadius,
Vector3 otherPosition, Vector3 otherVelocity, float otherRadius)
{
var force = Vector3.zero;

var distanceVector = cursorPosition - otherPosition;
var distance = distanceVector.magnitude;
var penetration = otherRadius + cursorRadius - distance;

if (penetration > 0)
{
// Normalize the distance vector to get the direction of the force
var normal = distanceVector.normalized;

// Calculate the force based on penetration
force = normal * penetration * stiffness;

// Calculate the relative velocity
var relativeVelocity = cursorVelocity - otherVelocity;

// Apply damping based on the relative velocity
force -= relativeVelocity * damping;
}

return force;
}

Thread-sicherer Datenaustausch​

In dynamischen Szenen, in denen sich Objekte in Echtzeit bewegen und miteinander interagieren, ist es von entscheidender Bedeutung, sicherzustellen, dass die Berechnung des haptischen Feedbacks stets auf den aktuellsten Daten basiert, ohne dass es durch gleichzeitige Zugriffe zu Datenbeschädigungen kommt. Hierfür ist ein threadsicherer Datenaustausch unerlässlich.

Grundlegende Konzepte des threadsicheren Datenaustauschs​

  • Thread-sichere Mechanismen: ReaderWriterLockSlim wird zur Steuerung des gleichzeitigen Datenzugriffs verwendet. Dadurch werden mehrere Lese- oder einzelne Schreibvorgänge ermöglicht, wobei die Datenkonsistenz gewährleistet bleibt.
  • Lesen und Schreiben von Daten:
    • Lesen: Der taktile Thread liest die Position und Geschwindigkeit des Objekts unter einer Lesesperre. Dadurch wird sichergestellt, dass die Aktualisierung der Daten nicht behindert wird.
    • Schreiben: Die Aktualisierung von Objektdaten durch den Hauptthread erfolgt unter einer Schreibsperre. Dadurch wird verhindert, dass durch gleichzeitige Lese- und Schreibvorgänge inkonsistente Datenzustände entstehen.

Implementierung in Unity​

  • Struktur für Szenendaten: Um threadsichere Operationen zu erleichtern, definieren wir eine Struktur, die alle erforderlichen Daten zur Szene enthält. Diese Struktur umfasst die Position und Geschwindigkeitdes „Moving Ball“ und des Cursors sowie deren jeweilige Radien. Diese Datenstruktur bildet die Grundlage für den threadsicheren Datenaustausch.

    private struct SceneData
    {
    public Vector3 ballPosition;
    public Vector3 ballVelocity;
    public float ballRadius;
    public float cursorRadius;
    }

    private SceneData _cachedSceneData;
  • Initialisierung der Sperre: Um den Zugriff auf Szenendaten zu verwalten,ReaderWriterLockSlim Die Instanz wird initialisiert. Durch diese Sperre wird die Datenkonsistenz bei gleichzeitigen Operationen gewährleistet, da mehrere Threads gleichzeitig Daten lesen können und ein einzelner Thread die Daten zum Schreiben exklusiv sperren kann.

    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
  • Schreiben in den Cache aufgrund einer Schreibsperre: SaveSceneData Die Methode aktualisiert die Szenendaten unter einer Schreibsperre. Dadurch können andere Threads weder lesen noch schreiben, solange ein Thread die Daten aktualisiert, wodurch Datenkonflikte verhindert und die Konsistenz gewährleistet werden.

    private void SaveSceneData()
    {
    _cacheLock.EnterWriteLock();
    try
    {
    var t = transform;
    _cachedSceneData.ballPosition = t.position;
    _cachedSceneData.ballRadius = t.lossyScale.x / 2f;

    _cachedSceneData.cursorRadius = inverse3.Cursor.Radius;

    _cachedSceneData.ballVelocity = _movableObject.Velocity;
    }
    finally
    {
    _cacheLock.ExitWriteLock();
    }
    }
  • Lesen aus dem Cache mit Lesesperre: GetSceneData Die Methode ruft die Szenendaten unter einer Lesesperre ab. Dadurch können mehrere Threads die Daten sicher und gleichzeitig lesen, ohne Schreibvorgänge zu behindern, und es wird gewährleistet, dass die Berechnung des haptischen Feedbacks auf der Grundlage der aktuellsten Szenendaten erfolgt.

    private SceneData GetSceneData()
    {
    _cacheLock.EnterReadLock();
    try
    {
    return _cachedSceneData;
    }
    finally
    {
    _cacheLock.ExitReadLock();
    }
    }
  • Datenaktualisierung im Hauptthread: FixedUpdate Die Methode wird verwendet, um die Szenendaten regelmäßig im Hauptthread zu aktualisieren. Dadurch kann die Berechnung des haptischen Feedbacks stets auf die aktuellsten Daten zurückgreifen, sodass die dynamischen Eigenschaften der Szene berücksichtigt werden.

    private void FixedUpdate()
    {
    SaveSceneData();
    }
  • Anwendung der Kraftberechnung unter Verwendung aktualisierter Daten: OnDeviceStateChanged Innerhalb des Callbacks werden die Kräfte anhand der aktuellsten Szenendaten berechnet, die auf threadsichere Weise abgerufen wurden. Dadurch wird eine genaue Kraftrückmeldung gewährleistet und die Reaktionsfähigkeit auf dynamische Interaktionen innerhalb der Szene bleibt erhalten.

    var sceneData = GetSceneData();

    var force = ForceCalculation(device.CursorLocalPosition, device.CursorLocalVelocity, sceneData.cursorRadius,
    sceneData.ballPosition, sceneData.ballVelocity, sceneData.ballRadius);

Spielerlebnis​

Dank der Erweiterungen dieser Skripte können Sie mit einer Kugel interagieren, die sich aktiv innerhalb der Szene bewegt. Das haptische Feedback ändert sich dynamisch entsprechend der Bewegungsbahn der Kugel und sorgt so für ein noch intensiveres und haptisch reichhaltigeres Erlebnis.

Der sich bewegende Ball

Quelldatei​

Die Szene dieses Beispiels sowie die zugehörigen Dateien können aus den Tutorial-Beispielen des Unity Package Managers importiert werden.

Tutorial In den Beispielen wird in mehreren Fällen verwendet MovableObject Es enthält ein Skript, das die Bewegung des angehängten Spielobjekts über Tastatureingaben steuert.

SphereForceFeedback.cs​

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

using System.Threading;
using Haply.Inverse.DeviceControllers;
using Haply.Inverse.DeviceData;
using Haply.Samples.Tutorials.Utils;
using UnityEngine;

namespace Haply.Samples.Tutorials._4A_DynamicForceFeedback
{
public class SphereForceFeedback : MonoBehaviour
{
// must assign in inspector
public Inverse3Controller inverse3;

[Range(0, 800)]
// Stiffness of the force feedback.
public float stiffness = 500f;

[Range(0, 3)]
public float damping = 1f;

#region Thread-safe cached data

/// <summary>
/// Represents scene data that can be updated in the Update() call.
/// </summary>
private struct SceneData
{
public Vector3 ballPosition;
public Vector3 ballVelocity;
public float ballRadius;
public float cursorRadius;
}

/// <summary>
/// Cached version of the scene data.
/// </summary>
private SceneData _cachedSceneData;

private MovableObject _movableObject;

/// <summary>
/// Lock to ensure thread safety when reading or writing to the cache.
/// </summary>
private readonly ReaderWriterLockSlim _cacheLock = new();

/// <summary>
/// Safely reads the cached data.
/// </summary>
/// <returns>The cached scene data.</returns>
private SceneData GetSceneData()
{
_cacheLock.EnterReadLock();
try
{
return _cachedSceneData;
}
finally
{
_cacheLock.ExitReadLock();
}
}

/// <summary>
/// Safely updates the cached data.
/// </summary>
private void SaveSceneData()
{
_cacheLock.EnterWriteLock();
try
{
var t = transform;
_cachedSceneData.ballPosition = t.position;
_cachedSceneData.ballRadius = t.lossyScale.x / 2f;

_cachedSceneData.cursorRadius = inverse3.Cursor.Radius;

_cachedSceneData.ballVelocity = _movableObject.Velocity;
}
finally
{
_cacheLock.ExitWriteLock();
}
}

#endregion

/// <summary>
/// Saves the initial scene data cache.
/// </summary>
private void Start()
{
inverse3 ??= FindFirstObjectByType<Inverse3Controller>();
inverse3.Ready.AddListener((device, args) => SaveSceneData());
_movableObject = GetComponent<MovableObject>();
}

/// <summary>
/// Update scene data cache.
/// </summary>
private void FixedUpdate()
{
if (inverse3.IsReady)
{
SaveSceneData();
}
}

/// <summary>
/// Subscribes to the DeviceStateChanged event.
/// </summary>
private void OnEnable()
{
inverse3.DeviceStateChanged += OnDeviceStateChanged;
}

/// <summary>
/// Unsubscribes from the DeviceStateChanged event.
/// </summary>
private void OnDisable()
{
inverse3.DeviceStateChanged -= OnDeviceStateChanged;
inverse3.Release();
}

/// <summary>
/// Calculates the force based on the cursor's position and another sphere position.
/// </summary>
/// <param name="cursorPosition">The position of the cursor.</param>
/// <param name="cursorVelocity">The velocity of the cursor.</param>
/// <param name="cursorRadius">The radius of the cursor.</param>
/// <param name="otherPosition">The position of the other sphere (e.g., ball).</param>
/// <param name="otherVelocity">The velocity of the other sphere (e.g., ball).</param>
/// <param name="otherRadius">The radius of the other sphere.</param>
/// <returns>The calculated force vector.</returns>
private Vector3 ForceCalculation(Vector3 cursorPosition, Vector3 cursorVelocity, float cursorRadius,
Vector3 otherPosition, Vector3 otherVelocity, float otherRadius)
{
var force = Vector3.zero;

var distanceVector = cursorPosition - otherPosition;
var distance = distanceVector.magnitude;
var penetration = otherRadius + cursorRadius - distance;

if (penetration > 0)
{
// Normalize the distance vector to get the direction of the force
var normal = distanceVector.normalized;

// Calculate the force based on penetration
force = normal * penetration * stiffness;

// Calculate the relative velocity
var relativeVelocity = cursorVelocity - otherVelocity;

// Apply damping based on the relative velocity
force -= relativeVelocity * damping;
}

return force;
}

/// <summary>
/// Event handler that calculates and send the force to the device when the cursor's position changes.
/// </summary>
/// <param name="sender">The Inverse3 data object.</param>
/// <param name="args">The event arguments containing the device data.</param>
private void OnDeviceStateChanged(object sender, Inverse3EventArgs args)
{
var inverse3 = args.DeviceController;
var sceneData = GetSceneData();

// Calculate the moving ball force.
var force = ForceCalculation(inverse3.CursorLocalPosition, inverse3.CursorLocalVelocity, sceneData.cursorRadius,
sceneData.ballPosition, sceneData.ballVelocity, sceneData.ballRadius);

// Apply the force to the cursor.
inverse3.SetCursorLocalForce(force);
}
}
}