Zum Hauptinhalt springen
Version: 2.1.1

Kraftrückkopplung in einer dynamischen Szene Tutorial

Aufbauend auf dem Basic Force-Feedback-Tutorial zeigt dieser Leitfaden, wie dynamische Interaktionen in Unity simuliert werden können, so dass Benutzer ein Kraft-Feedback von einem sich bewegenden Objekt spüren können. Dieses Szenario verdeutlicht die Notwendigkeit hochfrequenter Aktualisierungen für haptisches Feedback, die die typischen Aktualisierungsraten für visuelles Rendering in Unity deutlich übersteigen.

Einführung

Für ein überzeugendes haptisches Erlebnis, insbesondere in dynamischen Szenen, ist es entscheidend, Berechnungen mit Frequenzen über 1 kHz durchzuführen. Dies steht in krassem Gegensatz zur üblichen Spielaktualisierungsschleife, die mit etwa 60 Hz arbeitet. Die Herausforderung besteht darin, diese hochfrequenten Aktualisierungen neben der Hauptschleife des Spiels zu verwalten und einen thread-sicheren Datenaustausch zu gewährleisten, um ein konsistentes und genaues Force-Feedback zu erhalten.

Erweitern der grundlegenden Force-Feedback-Einrichtung

Beginnen Sie mit der Einrichtung der Szene aus dem Grundlegende Kraft-Rückkopplung Tutorium. Um dynamisches Verhalten einzubauen, werden wir die SphereForceFeedback Skript, so dass es auf die Bewegung der Kugel reagieren und die Interaktion mit einem sich dynamisch bewegenden Objekt simulieren kann.

Wichtige Änderungen

  • Dynamische Objektbewegung: Integrieren Sie eine Logik zur Aktualisierung der Position und Geschwindigkeit der Kugel auf der Grundlage von Benutzereingaben oder vordefinierten Bewegungsmustern.
  • Thread-sicherer Datenaustausch: Verwenden Sie eine ReaderWriterLockSlim um den gleichzeitigen Zugriff auf gemeinsame Daten zwischen dem Haupt- und dem haptischen Thread zu verwalten.
  • Anpassungen der Kraftberechnung: Ändern Sie die SphereForceFeedback.ForceCalculation Methode, um die Geschwindigkeit der Kugel zu berücksichtigen und so ein realistisches Feedback auf der Grundlage von Position und Bewegung zu erhalten.

Dynamische Interaktion

Um die sich bewegende Kugel zu simulieren, können Sie ihre Position entweder manuell in der Update Methode oder verwenden Sie eine separate Komponente, um ihre Bewegung auf der Grundlage von Tastatureingaben oder anderen Interaktionen zu steuern. In diesem Beispiel benennen wir die Sphäre Spielobjekt zu Bewegter Ball und fügen Sie die MovingObject Komponente, die in der Anleitungen Probe.

Einstellen von ForceCalculation für Bewegung

Die Berechnung der Kraftrückmeldung muss nun die Geschwindigkeit des Moving Balls berücksichtigen und die Kraft auf der Grundlage der Position und der Geschwindigkeit der Interaktion anpassen. Dies sorgt für ein nuancierteres und realistischeres haptisches Gefühl und spiegelt die dynamische Natur der Interaktion wider.

  • Hinzufügen einer Vector3 otherVelocity Methodenparameter
  • Ersetzen Sie force -= cursorVelocity * damping von force -= (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;
}

Thread-sicherer Datenaustausch

In dynamischen Szenen, in denen sich Objekte in Echtzeit bewegen und interagieren, muss sichergestellt werden, dass die Berechnungen des haptischen Feedbacks auf den aktuellsten Daten basieren, ohne dass es zu einer Datenbeschädigung durch gleichzeitigen Zugriff kommt. In diesem Fall ist ein thread-sicherer Datenaustausch unerlässlich.

Schlüsselkonzepte für den thread-sicheren Datenaustausch

  • Gewindesichere Mechanismen: Nutzen Sie ReaderWriterLockSlim um den gleichzeitigen Datenzugriff zu verwalten. Dies ermöglicht mehrere Lesevorgänge oder einen einzigen Schreibvorgang und gewährleistet die Datenintegrität.
  • Lesen und Schreiben von Daten:
    • Lesen: Der Haptik-Thread liest Objektpositionen und -geschwindigkeiten unter einer Lesesperre, um sicherzustellen, dass er nicht mit Datenaktualisierungen kollidiert.
    • Schreiben: Aktualisierungen von Objektdaten durch den Hauptthread erfolgen unter einer Schreibsperre, wodurch gleichzeitige Lese- oder Schreibvorgänge, die zu inkonsistenten Datenzuständen führen könnten, verhindert werden.

Implementieren in Unity

  • Struktur für Szenendaten: Um thread-sichere Operationen zu ermöglichen, definieren wir eine Struktur, die alle notwendigen Daten über die Szene enthält. Diese Struktur enthält die Position und Geschwindigkeit des Moving Balls und des Cursors sowie deren Radien. Diese Datenstruktur dient als 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 sperren: A ReaderWriterLockSlim Instanz wird initialisiert, um den Zugriff auf die Szenendaten zu verwalten. Diese Sperre ermöglicht es mehreren Threads, Daten gleichzeitig zu lesen oder die Daten exklusiv für einen einzelnen Thread zum Schreiben zu sperren, um die Datenintegrität bei gleichzeitigen Vorgängen zu gewährleisten.

    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
  • Schreiben in den Cache mit einer Schreibsperre: Die SaveSceneData Methode aktualisiert die Szenendaten innerhalb einer Schreibsperre. Dadurch wird sichergestellt, dass, während ein Thread die Daten aktualisiert, kein anderer Thread lesen oder schreiben kann, was Datenwettläufe 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.Model.transform.lossyScale.x / 2f;
    _cachedSceneData.ballVelocity = _movableObject.CursorVelocity;
    }
    finally
    {
    _cacheLock.ExitWriteLock();
    }
    }
  • Lesen aus dem Cache mit einer Lesesperre: Die GetSceneData Methode ruft die Szenendaten mit einer Lesesperre ab. Dadurch können mehrere Threads die Daten sicher und gleichzeitig lesen, ohne die Schreibvorgänge zu beeinträchtigen, und es wird sichergestellt, dass die Berechnungen des haptischen Feedbacks auf den neuesten Szenendaten basieren.

    private SceneData GetSceneData()
    {
    _cacheLock.EnterReadLock();
    try
    {
    return _cachedSceneData;
    }
    finally
    {
    _cacheLock.ExitReadLock();
    }
    }
  • Hauptthema Datenaktualisierung: Die FixedUpdate Methode wird verwendet, um die Szenendaten im Hauptthread regelmäßig zu aktualisieren. Dadurch wird sichergestellt, dass die Berechnungen des haptischen Feedbacks auf die aktuellsten Daten zugreifen können, was die Dynamik der Szene widerspiegelt.

    private void FixedUpdate()
    {
    SaveSceneData();
    }
  • Anwendung der Kraftberechnung mit aktualisierten Daten: In der OnDeviceStateChanged Callback werden Kraftberechnungen unter Verwendung der neuesten Szenendaten durchgeführt, die durch thread-sichere Methoden erhalten werden. Dadurch wird sichergestellt, dass die Kraftrückmeldung genau ist und auf die dynamischen Interaktionen innerhalb der Szene reagiert.

    var sceneData = GetSceneData();

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

Gameplay-Erfahrung

Diese Skriptverbesserungen ermöglichen es Ihnen, mit einer Kugel zu interagieren, die sich aktiv durch die Szene bewegt. Das haptische Feedback passt sich dynamisch an die Flugbahn der Kugel an und bietet so ein intensiveres und taktileres Erlebnis.

beweglicher Ball

Quelldateien

Die komplette Szene und die zugehörigen Dateien für dieses Beispiel können aus dem Tutorials-Beispiel im Unity-Paketmanager importiert werden.

Die Tutorial Probe umfasst die MovableObject Skript, das in mehreren Beispielen verwendet wird, um die Bewegung des angehängten Spielobjekts mit Tastatureingaben zu steuern.

SphereForceFeedback.cs

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

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

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

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

[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.Model.transform.lossyScale.x / 2f;

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

#endregion

/// <summary>
/// Saves the initial scene data cache.
/// </summary>
private void Start()
{
_movableObject = GetComponent<MovableObject>();
SaveSceneData();
}

/// <summary>
/// Update scene data cache.
/// </summary>
private void FixedUpdate()
{
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;
}

/// <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="device">The Inverse3 device instance.</param>
private void OnDeviceStateChanged(Inverse3 device)
{
var sceneData = GetSceneData();

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

// Apply the force to the cursor.
device.CursorSetLocalForce(force);
}
}
}