Tutoriel sur le retour de force dans une scène dynamique
En s'appuyant sur le tutoriel retour de force de base, ce guide explique comment simuler des interactions dynamiques dans Unity, afin que les utilisateurs puissent ressentir le retour de force provenant d'un objet en mouvement. Ce scénario met en évidence la nécessité de mises à jour à haute fréquence pour le retour haptique, une fréquence qui dépasse nettement les taux de mise à jour typiques du rendu visuel dans Unity.
Introduction
Pour une expérience haptique convaincante, surtout dans les scènes dynamiques, il est essentiel d'effectuer les calculs à des fréquences supérieures à 1kHz. Cela contraste fortement avec la boucle de mise à jour habituelle d'un jeu, qui fonctionne autour de 60Hz. Le défi consiste à gérer ces mises à jour à haute fréquence en parallèle avec la boucle principale du jeu, tout en assurant un échange de données sûr pour les threads afin de maintenir un retour de force cohérent et précis.
Extension de la configuration du retour de force de base
Partez de la scène du tutoriel retour de force de base. Pour intégrer un comportement dynamique, nous adapterons le script SphereForceFeedback afin qu'il réagisse au mouvement de la sphère et simule des interactions avec un objet en mouvement.
Principales modifications
- Déplacement dynamique de l'objet : intégrez une logique qui met à jour la position et la vitesse de l'objet Sphere en fonction des entrées de l'utilisateur ou de schémas de déplacement prédéfinis.
- Échange de données sûr pour les threads : utilisez un
ReaderWriterLockSlimpour gérer l'accès concurrent aux données partagées entre le thread principal et le thread haptique. - Ajustements du calcul de force : modifiez la méthode
SphereForceFeedback.ForceCalculationpour tenir compte de la vitesse de la sphère, afin de fournir un retour réaliste fondé à la fois sur la position et sur le mouvement.
Interaction dynamique
Pour simuler la sphère en mouvement, vous pouvez soit mettre à jour manuellement sa position dans la méthode Update, soit utiliser un composant distinct qui contrôle son déplacement en fonction des entrées clavier ou d'autres interactions.
Dans cet exemple, nous renommerons l'objet Sphere en Moving Ball et ajouterons le composant MovingObject fourni dans l'exemple Tutorials.
Adaptation de ForceCalculation pour le mouvement
Le calcul du retour de force doit maintenant tenir compte de la vitesse de l'objet Moving Ball, en ajustant la force en fonction de la position et de la vitesse de l'interaction. Cela procure une sensation haptique plus nuancée et réaliste, qui reflète la nature dynamique de l'interaction.
- Ajoutez le paramètre de méthode
Vector3otherVelocity - Remplacez
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;
}
Échange de données sûr pour les threads
Dans les scènes dynamiques, où des objets se déplacent et interagissent en temps réel, il est essentiel de s'assurer que les calculs de retour haptique reposent sur les données les plus récentes, sans corruption des données causée par les accès concurrents. C'est là que l'échange de données sûr pour les threads devient indispensable.
Concepts clés de l'échange de données sûr pour les threads
- Mécanismes sûrs pour les threads : utilisez
ReaderWriterLockSlimpour gérer les accès concurrents aux données. Cela permet plusieurs lectures 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, afin de ne pas interférer avec les mises à jour des données.
- Écriture : les mises à jour des données des objets par le thread principal sont effectuées sous un verrou d'écriture, ce qui empêche les lectures ou les écritures simultanées pouvant mener à des états de données incohérents.
Implémentation dans Unity
-
Structure pour les données de scène : pour faciliter les opérations sûres pour les threads, 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 Moving Ball et du curseur, ainsi que leurs rayons. Cette structure de données constitue la base de notre échange de données sûr pour les threads.
private struct SceneData{public Vector3 ballPosition;public Vector3 ballVelocity;public float ballRadius;public float cursorRadius;}private SceneData _cachedSceneData; -
Initialisation du verrou : une instance de
ReaderWriterLockSlimest 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 un seul thread en écriture, afin de garantir l'intégrité des données lors des opérations concurrentes.private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); -
Écriture dans le cache avec un verrou d'écriture : la méthode
SaveSceneDatamet à jour les données de la scène sous un verrou d'écriture. Cela garantit que, pendant qu'un thread met à jour les données, aucun autre thread ne peut lire ni écrire, ce qui prévient les conditions de concurrence et assure 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.Radius;_cachedSceneData.ballVelocity = _movableObject.Velocity;}finally{_cacheLock.ExitWriteLock();}} -
Lecture dans le cache avec un verrou de lecture : la méthode
GetSceneDataré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, afin que les calculs de retour haptique reposent sur les données de scène les plus récentes.private SceneData GetSceneData(){_cacheLock.EnterReadLock();try{return _cachedSceneData;}finally{_cacheLock.ExitReadLock();}} -
Mise à jour des données dans le thread principal : la méthode
FixedUpdatesert à mettre à jour périodiquement les données de la scène dans le thread principal. Cela garantit que les calculs de retour haptique ont accès aux données les plus à jour, ce qui reflète la nature dynamique de la scène.private void FixedUpdate(){SaveSceneData();} -
Application du calcul de force avec les données mises à jour : dans la fonction de rappel
OnDeviceStateChanged, les calculs de force sont effectués à partir des données de scène les plus récentes, obtenues au moyen de méthodes sûres pour les threads. Cela garantit que le retour de force est précis et réactif 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 du 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 riche sur le plan tactile.

Fichiers sources
La scène complète et les fichiers associés à cet exemple peuvent être importés à partir de l'exemple Tutorials du Package Manager de Unity.
L'exemple Tutorial comprend le script
MovableObject, utilisé dans plusieurs exemples pour contrôler le déplacement de l'objet de jeu attaché au moyen des touches du clavier.
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);
}
}
}