Skip to main content
Version : 2.2.0

Retour d'effort dans une scène dynamique Tutoriel

S'appuyant sur le tutoriel Basic Force-Feedback, ce guide présente comment simuler des interactions dynamiques dans Unity, en permettant aux utilisateurs de ressentir un retour de force à partir d'un objet en mouvement. Ce scénario met en évidence la nécessité de mises à jour à haute fréquence pour le retour haptique, qui dépasse de manière significative les taux de mise à jour typiques pour le rendu visuel dans Unity.

Introduction

Pour une expérience haptique convaincante, en particulier dans les scènes dynamiques, il est essentiel d'effectuer des calculs à des fréquences supérieures à 1 kHz. Cela contraste fortement avec la boucle de mise à jour habituelle du jeu, qui fonctionne à environ 60 Hz. Le défi consiste à gérer ces mises à jour à haute fréquence parallèlement à la boucle principale du jeu, en garantissant un échange de données sécurisé pour maintenir un retour de force cohérent et précis.

Extension de la configuration de base du retour de force

Commencez par la configuration de la scène à partir de la page Force de base - Retour d'information tutoriel. Pour intégrer un comportement dynamique, nous adapterons le SphereForceFeedback ce qui lui permet de réagir au mouvement de la sphère et de simuler des interactions avec un objet en mouvement dynamique.

Principales modifications

  • Mouvement dynamique des objets: Incorporer une logique pour mettre à jour la position et la vitesse de la sphère en fonction de l'entrée de l'utilisateur ou de modèles de mouvement prédéfinis.
  • Échange de données en toute sécurité: Utiliser un ReaderWriterLockSlim pour gérer l'accès concurrent aux données partagées entre les threads principaux et haptiques.
  • Ajustements du calcul de la force: Modifier le SphereForceFeedback.ForceCalculation pour prendre en compte la vitesse de la sphère, ce qui permet d'obtenir un retour d'information réaliste basé à la fois sur la position et le mouvement.

Interaction dynamique

Pour simuler la sphère en mouvement, vous pouvez soit mettre à jour manuellement sa position dans le fichier Update ou utiliser un composant séparé pour contrôler son mouvement en fonction de la saisie au clavier ou d'autres interactions. Dans cet exemple, nous renommerons le composant Sphère l'objet du jeu à Balle en mouvement et ajouter le MovingObject indiqué dans l'avis de la Commission européenne. Tutoriels échantillon.

Ajustement ForceCalculation pour le mouvement

Le calcul du retour de force doit désormais tenir compte de la vitesse de la balle en mouvement, en ajustant la force en fonction de la position et de la vitesse de l'interaction. Cela permet d'obtenir une sensation haptique plus nuancée et plus réaliste, reflétant la nature dynamique de l'interaction.

  • Ajouter un Vector3 otherVelocity paramètre de méthode
  • Remplacer force -= cursorVelocity * damping par 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;
}

Échange de données en toute sécurité

Dans les scènes dynamiques où les objets se déplacent et interagissent en temps réel, il est essentiel de s'assurer que les calculs du retour haptique sont basés sur les données les plus récentes sans provoquer de corruption des données en raison d'un accès simultané. C'est là que l'échange de données à sécurité intrinsèque devient essentiel.

Concepts clés pour l'échange de données en toute sécurité

  • Mécanismes de sécurité du filetage: Utiliser ReaderWriterLockSlim pour gérer l'accès simultané aux données. Cela permet des lectures multiples ou une seule opération d'écriture, ce qui garantit l'intégrité des données.
  • Lecture et écriture des données:
    • Lecture: Le thread haptique lit les positions et les vitesses des objets sous un verrou de lecture, ce qui garantit qu'il n'interfère pas avec les mises à jour des données.
    • Écriture: Les mises à jour des données de l'objet par le thread principal sont effectuées sous un verrou d'écriture, ce qui empêche les lectures ou écritures simultanées qui pourraient conduire à des états de données incohérents.

Mise en œuvre dans Unity

  • Structure pour les données de la scène: Pour faciliter les opérations sûres, nous définissons une structure qui contient toutes les données nécessaires sur la scène. Cette structure comprend la position et la vitesse de la Moving Ball et du curseur, ainsi que leurs rayons. Cette structure de données sert de base à notre échange de données à sécurité intrinsèque.

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

    private SceneData _cachedSceneData;
  • Initialisation de la serrure: A ReaderWriterLockSlim est initialisée pour gérer l'accès aux données de la scène. Ce verrou permet à plusieurs threads de lire les données simultanément ou de verrouiller exclusivement les données pour qu'un seul thread puisse les écrire, ce qui garantit l'intégrité des données lors d'opérations simultanées.

    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
  • Écriture dans la mémoire cache avec un verrou d'écriture: Le SaveSceneData met à jour les données de la scène à l'intérieur d'un verrou d'écriture. Cela garantit que pendant qu'un thread met à jour les données, aucun autre thread ne peut lire ou écrire, ce qui évite les courses aux données et garantit la cohérence.

    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();
    }
    }
  • Lecture de la mémoire cache avec un verrou de lecture: Le GetSceneData récupère les données de la scène sous un verrou de lecture. Cela permet à plusieurs threads de lire les données simultanément sans interférer avec les opérations d'écriture, ce qui garantit que les calculs du retour haptique sont basés sur les dernières données de la scène.

    private SceneData GetSceneData()
    {
    _cacheLock.EnterReadLock();
    try
    {
    return _cachedSceneData;
    }
    finally
    {
    _cacheLock.ExitReadLock();
    }
    }
  • Fil principal Mise à jour des données: Le FixedUpdate est utilisée pour mettre à jour périodiquement les données de la scène dans le fil d'exécution principal. Cela garantit que les calculs de retour haptique ont accès aux données les plus récentes, reflétant ainsi la nature dynamique de la scène.

    private void FixedUpdate()
    {
    SaveSceneData();
    }
  • Application du calcul de la force avec des données actualisées: Dans le OnDeviceStateChanged les calculs de force sont effectués à l'aide des dernières données de la scène obtenues par des méthodes sûres. Cela garantit que le retour de force est précis et qu'il répond aux interactions dynamiques de la scène.

    var sceneData = GetSceneData();

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

Expérience de jeu

Ces améliorations de script vous permettent d'interagir avec une sphère qui se déplace activement dans la scène. Le retour haptique s'adapte dynamiquement à la trajectoire de la sphère, offrant une expérience plus immersive et plus riche sur le plan tactile.

balle mobile

Fichiers sources

La scène complète et les fichiers associés de cet exemple peuvent être importés depuis l'échantillon Tutorials dans le Unity Package Manager.

Les Tutoriel L'échantillon comprend le MovableObject qui est utilisé dans de nombreux exemples pour contrôler le mouvement de l'objet de jeu attaché à l'aide d'une entrée clavier.

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