Tutoriel sur le retour de force dans une scène dynamique
En s'appuyant sur le tutoriel de base consacré au retour de force, 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 habituels du rendu visuel dans Unity.
Introduction
Pour offrir une expérience haptique convaincante, notamment dans les scènes dynamiques, il est essentiel d'effectuer les calculs à des fréquences supérieures à 1 kHz. Cela contraste fortement avec la boucle de mise à jour habituelle d'un jeu, qui fonctionne à environ 60 Hz. 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écurisé entre les threads afin de maintenir un retour de force cohérent et précis.
Extension de la configuration de base du retour de force
Commencez par la scène du Tutoriel sur la force de retour 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 permettant de mettre à 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écurisé entre threads : utilisez un
ReaderWriterLockSlimpour gérer l'accès simultané aux données partagées entre le thread principal et le thread haptique. - Ajustements du calcul de la force : modifiez la méthode
SphereForceFeedback.ForceCalculationpour tenir compte de la vitesse de la sphère, afin de fournir un retour d'information réaliste basé à la fois sur la position et sur le mouvement.
Interaction dynamique
Pour simuler le mouvement de la sphère, 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 Sphère fr Balle en mouvement et nous ajouterons le composant MovingObject fourni dans l'exemple Tutoriels.
Adaptation de ForceCalculation pour le mouvement
Le calcul de la force de retour doit désormais 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écurisé entre 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 s'appuient sur les données les plus récentes, sans que celles-ci ne soient altérées par des accès concurrents. C'est là que l'échange de données sécurisé entre les threads devient indispensable.
Concepts clés de l'échange de données sécurisé entre threads
- Mécanismes sécurisés pour les threads : utilisez
ReaderWriterLockSlimpour gérer les accès simultanés aux données. Cela permet d'effectuer plusieurs lectures ou une seule opération d'écriture, ce qui garantit l'intégrité des données. - Lecture et écriture des données :
- Cours: 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 s'effectuent sous un verrou d'écriture, ce qui empêche toute lecture ou écriture simultanée susceptible d'entraîner des incohérences dans l'état des données.
Implémentation dans Unity
-
Structure pour les données de scène: afin de faciliter les opérations inter-threads, nous définissons une structure qui contient toutes les données nécessaires concernant 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 inter-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 les données en écriture de manière exclusive pour un seul thread, afin de garantir l'intégrité des données lors d'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 ni lire ni écrire, ce qui évite 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 s'appuient 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
FixedUpdatepermet de 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 récentes, ce qui reflète la nature dynamique de la scène.private void FixedUpdate(){SaveSceneData();} -
Application du calcul de force à partir des 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 à l'aide 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 apportées au 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 ainsi une expérience plus immersive et plus 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 la section « Tutorials » du Package Manager de Unity.
L'exemple Tutoriel comprend le script
MovableObject, utilisé dans plusieurs exemples pour contrôler le déplacement de l'objet de jeu associé à l'aide 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);
}
}
}