Zum Haupttext springen

Tutorial zum Force-Feedback in dynamischen Szenen

Aufbauend auf dem grundlegenden Tutorial zum Force Feedback wird in dieser Anleitung erläutert, wie Sie in Unity dynamische Interaktionen simulieren können, damit der Benutzer Force Feedback von sich bewegenden Objekten wahrnehmen kann. Dieses Szenario verdeutlicht, dass für haptisches Feedback hochfrequente Aktualisierungen erforderlich sind, die weit über der üblichen Aktualisierungsrate für die visuelle Darstellung in Unity liegen.

Einführung​

Insbesondere um in dynamischen Szenen ein überzeugendes haptisches Erlebnis zu bieten, ist es äußerst wichtig, Berechnungen mit Frequenzen von über 1 kHz durchzuführen. Dies steht in krassem Gegensatz zu den üblichen Spiel-Aktualisierungsschleifen, die normalerweise mit etwa 60 Hz arbeiten. 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 Force-Feedback aufrechtzuerhalten.

Erweiterung der Standard-Force-Feedback-Einstellungen​

Grundlegendes Force-Feedback Beginnen Sie mit den Szeneneinstellungen des Tutorials. Um dynamische Bewegungen zu realisieren, SphereForceFeedback Wir werden das Skript so anpassen, dass es auf die Bewegung der Kugel reagiert und die Interaktion mit sich dynamisch bewegenden Objekten simuliert.

Wichtigste Änderungen​

  • Dynamische Objektbewegung: Enthält die Logik zur Aktualisierung der Position und Geschwindigkeit von „Sphere“ auf der Grundlage von Benutzereingaben oder vordefinierten Bewegungsmustern.
  • Thread-sicherer Datenaustausch: ReaderWriterLockSlimwird verwendet, um den gleichzeitigen Zugriff auf Daten zu verwalten, die vom Haupt-Thread und dem Haptik-Thread gemeinsam genutzt werden.
  • Anpassung der Kraftberechnung: SphereForceFeedback.ForceCalculation Die Methode wird so angepasst, dass sie die Geschwindigkeit der Kugel berücksichtigt, und liefert auf der Grundlage sowohl der Position als auch der Bewegung ein realistisches Feedback.

Dynamische Interaktion​

Um eine sich bewegende Kugel zu simulieren, Update Sie können die Position manuell in der Methode aktualisieren oder eine separate Komponente verwenden, die die Bewegung anhand von Tastatureingaben oder anderen Interaktionen steuert. In diesem Beispiel Sphäre Den Namen des Spielobjekts Bewegter Ballin … ändern und Anleitungen In der Probe enthalten MovingObject Ich werde eine Komponente hinzufügen.

an die Bewegung angepasst ForceCalculation Anpassung​

Nun muss bei der Berechnung der Kraftrückmeldung die Geschwindigkeit des „Moving Ball“ berücksichtigt werden, und die Kraft muss sowohl auf der Grundlage der Position als auch der Geschwindigkeit der Interaktion angepasst werden. Auf diese Weise lässt sich ein fein abgestimmtes und realistischeres haptisches Erlebnis bieten, das die dynamischen Eigenschaften der Interaktion widerspiegelt.

  • Vector3 Typ otherVelocity Fügen Sie Methodenparameter hinzu.
  • force -= cursorVelocity * damping을 force -= (cursorVelocity - otherVelocity) * dampingÄndern Sie dies bitte in.
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 Echtzeit-Szenen, in denen sich Objekte bewegen und miteinander interagieren, ist es äußerst wichtig, sicherzustellen, dass die Berechnung des haptischen Feedbacks stets auf den aktuellsten Daten basiert, ohne dass es durch gleichzeitige Zugriffe zu Datenverlusten kommt. Genau in diesem Fall ist ein threadsicherer Datenaustausch unerlässlich.

Kernkonzepte des threadsicheren Datenaustauschs​

  • Mechanismen zur Thread-Sicherheit: ReaderWriterLockSlimwird zur Verwaltung des gleichzeitigen Datenzugriffs verwendet. Dadurch sind mehrere Lesevorgänge oder ein einzelner Schreibvorgang möglich, und die Datenintegrität wird gewährleistet.
  • Daten lesen und schreiben:
    • Lesen: Ein haptischer Thread liest die Position und Geschwindigkeit eines Objekts im Lesesperrzustand, ohne dabei die Datenaktualisierung zu beeinträchtigen.
    • Schreiben: Da die Aktualisierung von Objektdaten durch den Hauptthread unter einer Schreibsperre erfolgt, wird verhindert, dass der Datenstatus durch gleichzeitig stattfindende Lese- oder Schreibvorgänge inkonsistent wird.

Implementierung in Unity​

  • Struktur für Szenendaten: Um threadsichere Operationen zu erleichtern, wird eine Struktur definiert, die alle für die Szene erforderlichen Daten enthält. Diese Struktur umfasst die Position und Geschwindigkeit des „Moving Ball“sowie des Cursors und den Radius. 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;
  • Sperre zurücksetzen: Zur Verwaltung des Zugriffs auf Szenendaten ReaderWriterLockSlim Die Instanz wird initialisiert. Da diese Sperre es mehreren Threads ermöglicht, Daten gleichzeitig zu lesen, oder es einem einzelnen Thread erlaubt, Daten für Schreibvorgänge exklusiv zu sperren, wird die Datenintegrität auch bei gleichzeitigen Vorgängen gewährleistet.

    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
  • Schreiben in den Cache unter Verwendung einer Schreibsperre: SaveSceneData Die Methode aktualisiert die Szenendaten innerhalb einer Schreibsperre. Dadurch wird verhindert, dass andere Threads die Daten lesen oder schreiben können, während ein Thread sie aktualisiert, wodurch Datenkonflikte vermieden 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();
    }
    }
  • Cache-Lesezugriff mithilfe einer Lesesperre: GetSceneData Die Methode ruft die Szenendaten innerhalb einer Lesesperre ab. Dadurch können mehrere Threads die Daten gleichzeitig und sicher lesen, ohne Schreibvorgänge zu stören, und es wird sichergestellt, dass die Berechnung des haptischen Feedbacks auf der Grundlage der aktuellsten Szenendaten erfolgt.

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

    private void FixedUpdate()
    {
    SaveSceneData();
    }
  • Anwendung der Kraftberechnung unter Verwendung thread-sicherer Daten: OnDeviceStateChanged Im Callback werden die Kraftberechnungen anhand der aktuellsten Szenendaten durchgeführt, die über threadsichere Methoden abgerufen wurden. Dadurch wird sichergestellt, dass das Kraft-Feedback präzise ist und sensibel auf dynamische Interaktionen innerhalb der Szene reagiert.

    var sceneData = GetSceneData();

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

Spielerlebnis​

Dank dieser Verbesserungen am Skript können Sie mit einer Kugel interagieren, die sich aktiv innerhalb der Szene bewegt. Das haptische Feedback wird dynamisch an die Bewegungsbahn der Kugel angepasst und sorgt so für ein noch intensiveres und taktil reichhaltigeres Erlebnis.

Ein sich bewegender Ball

Quelldatei​

Die gesamte Szene dieses Beispiels sowie die zugehörigen Dateien können Sie aus den „Tutorials“-Beispielen im Unity-Paketmanager herunterladen.

Tutorial In den Beispielen wird in verschiedenen Fällen die Tastatureingabe verwendet, um die Bewegung von Spielobjekten zu steuern. MovableObject Es enthält ein Skript.

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);
}
}
}