Anleitung zur Force-Feedback-Implementierung in einer dynamischen Szene
Aufbauend auf dem Tutorial zu den Grundlagen der Haptik erklärt dieser Leitfaden, wie dynamische Interaktionen in Unity simuliert werden können, sodass Nutzer die haptische Rückmeldung eines sich bewegenden Objekts spüren können. Dieses Szenario verdeutlicht die Notwendigkeit von Aktualisierungen mit hoher Frequenz für die haptische Rückmeldung – eine Frequenz, die deutlich über den typischen Aktualisierungsraten der visuellen Darstellung in Unity liegt.
Einführung
Für ein überzeugendes haptisches Erlebnis, insbesondere in dynamischen Szenen, ist es unerlässlich, die Berechnungen mit Frequenzen von über 1 kHz durchzuführen. Dies steht in starkem Kontrast zur üblichen Aktualisierungsschleife eines Spiels, die mit etwa 60 Hz arbeitet. Die Herausforderung besteht darin, diese hochfrequenten Aktualisierungen parallel zur Hauptschleife des Spiels zu verarbeiten und gleichzeitig einen sicheren Datenaustausch zwischen den Threads zu gewährleisten, um ein konsistentes und präzises Force-Feedback aufrechtzuerhalten.
Erweiterung der Konfiguration der grundlegenden Force-Feedback-Funktion
Beginnen Sie an der Szene des Grundlegendes Tutorial zur Force-Feedback-Funktion. Um ein dynamisches Verhalten zu integrieren, werden wir das Skript anpassen SphereForceFeedback damit es auf die Bewegung der Kugel reagiert und Interaktionen mit einem sich bewegenden Objekt simuliert.
Wichtigste Änderungen
- Dynamische Bewegung des Objekts: Implementieren Sie eine Logik, die die Position und Geschwindigkeit des Objekts „Sphere“ entsprechend den Benutzereingaben oder vordefinierten Bewegungsmustern aktualisiert.
- Sicherer Datenaustausch zwischen Threads : Verwenden Sie ein
ReaderWriterLockSlimum den gleichzeitigen Zugriff auf gemeinsam genutzte Daten zwischen dem Haupt-Thread und dem haptischen Thread zu verwalten. - Anpassungen bei der Kraftberechnung : Ändern Sie die Methode
SphereForceFeedback.ForceCalculationum die Geschwindigkeit der Kugel zu berücksichtigen und so ein realistisches Feedback zu liefern, das sowohl auf der Position als auch auf der Bewegung basiert.
Dynamische Interaktion
Um die Bewegung der Kugel zu simulieren, können Sie entweder ihre Position in der Methode manuell aktualisieren Update, oder eine separate Komponente verwenden, die dessen Bewegung anhand von Tastatureingaben oder anderen Interaktionen steuert.
In diesem Beispiel werden wir das Objekt umbenennen Sphäre de Bewegter Ball und fügen die Komponente hinzu MovingObject im Beispiel angegeben Anleitungen.
Adaption von ForceCalculation für die Bewegung
Bei der Berechnung der Rückkopplungskraft muss nun die Geschwindigkeit des Objekts „Moving Ball“ berücksichtigt werden, indem die Kraft entsprechend der Position und Geschwindigkeit der Interaktion angepasst wird. Dies sorgt für ein differenzierteres und realistischeres haptisches Erlebnis, das die dynamische Natur der Interaktion widerspiegelt.
- Fügen Sie den Methodenparameter hinzu
Vector3otherVelocity - Ersetzen Sie
force -= cursorVelocity * dampingparforce -= (cursorVelocity - otherVelocity) * damping
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;
}
Sicherer Datenaustausch zwischen Threads
In dynamischen Szenen, in denen sich Objekte bewegen und in Echtzeit miteinander interagieren, muss unbedingt sichergestellt werden, dass die Berechnungen für das haptische Feedback auf den aktuellsten Daten basieren, ohne dass es durch gleichzeitige Zugriffe zu Datenverfälschungen kommt. Hier ist ein thread-sicherer Datenaustausch unverzichtbar.
Wichtige Konzepte für den thread-sicheren Datenaustausch
- Sichere Mechanismen für Threads : verwenden Sie
ReaderWriterLockSlimum den gleichzeitigen Zugriff auf die Daten zu verwalten. Dadurch sind mehrere Lesevorgänge oder ein einzelner Schreibvorgang möglich, was die Datenintegrität gewährleistet. - Einlesen und Schreiben von Daten :
- Vorlesung: Der haptische Thread liest die Positionen und Geschwindigkeiten der Objekte unter einer Lesesperre aus, um die Datenaktualisierungen nicht zu beeinträchtigen.
- Schreiben: Die Aktualisierung von Objektdaten durch den Hauptthread erfolgt unter einer Schreibsperre, wodurch gleichzeitige Lese- oder Schreibvorgänge verhindert werden, die zu inkonsistenten Datenzuständen führen könnten.
Implementierung in Unity
-
Struktur für die Szenendaten: Um thread-sichere Operationen zu erleichtern, definieren wir eine Struktur, die alle erforderlichen Daten zur Szene enthält. Diese Struktur umfasst die Position und Geschwindigkeit des „Moving Ball“ und des Cursors sowie deren Radien. Diese Datenstruktur bildet die Grundlage für unseren thread-sicheren Datenaustausch.
private struct SceneData{public Vector3 ballPosition;public Vector3 ballVelocity;public float ballRadius;public float cursorRadius;}private SceneData _cachedSceneData; -
Initialisierung der Verriegelung : eine Instanz von
ReaderWriterLockSlimwird initialisiert, um den Zugriff auf die Daten der Szene zu verwalten. Diese Sperre ermöglicht es mehreren Threads, die Daten gleichzeitig zu lesen, oder die Daten exklusiv für einen einzelnen Thread zum Schreiben zu sperren, um die Datenintegrität bei parallel laufenden Vorgängen zu gewährleisten.private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); -
Schreiben in den Cache mit einer Schreibsperre : die Methode
SaveSceneDataaktualisiert die Daten der Szene unter einer Schreibsperre. Dadurch wird sichergestellt, dass während ein Thread die Daten aktualisiert, kein anderer Thread lesen oder schreiben kann, was Race-Conditions verhindert und die Konsistenz gewährleistet.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 einer Lesesperre : die Methode
GetSceneDataruft die Szenendaten unter einer Lesesperre ab. Dadurch können mehrere Threads die Daten gleichzeitig lesen, ohne Schreibvorgänge zu beeinträchtigen, sodass die Berechnungen für das haptische Feedback auf den aktuellsten Szenendaten basieren.private SceneData GetSceneData(){_cacheLock.EnterReadLock();try{return _cachedSceneData;}finally{_cacheLock.ExitReadLock();}} -
Aktualisierung der Daten im Hauptthread : die Methode
FixedUpdatedient dazu, die Daten der Szene im Hauptthread regelmäßig zu aktualisieren. Dadurch wird sichergestellt, dass die Berechnungen für das haptische Feedback auf die aktuellsten Daten zugreifen können, was der Dynamik der Szene Rechnung trägt.private void FixedUpdate(){SaveSceneData();} -
Anwendung der Kraftberechnung mit den aktualisierten Daten : in der Rückruffunktion
OnDeviceStateChanged, Die Kraftberechnungen basieren auf den aktuellsten Szenendaten, die mithilfe thread-sicherer Methoden ermittelt werden. Dadurch wird sichergestellt, dass die Kraftrückmeldung präzise ist und schnell auf dynamische Interaktionen in 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 des Skripts können Sie mit einer Kugel interagieren, die sich aktiv durch die Szene bewegt. Das haptische Feedback passt sich dynamisch an die Bewegungsbahn der Kugel an und sorgt so für ein noch intensiveres und taktil reichhaltigeres Erlebnis.

Quelldateien
Die komplette Szene und die zu diesem Beispiel gehörenden Dateien können aus dem Beispiel „Tutorials“ im Unity Package Manager importiert werden.
Das Beispiel Tutorial enthält das Skript
MovableObject, das in mehreren Beispielen verwendet wird, um die Bewegung des daran befestigten Spielobjekts mithilfe der Tastaturtasten zu steuern.
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);
}
}
}