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 de permettre à l'utilisateur de ressentir un retour de force provenant d'objets en mouvement. Ce scénario montre que le retour haptique nécessite des mises à jour à haute fréquence, bien supérieures à la fréquence de mise à jour habituelle utilisée pour le rendu visuel dans Unity.
Présentation
Pour offrir une expérience haptique convaincante, en particulier dans les scènes dynamiques, il est essentiel d’effectuer les calculs à une fréquence supérieure à 1 kHz. Cela contraste fortement avec la boucle de mise à jour habituelle des jeux, qui fonctionne généralement à environ 60 Hz. Le défi consiste à gérer ces mises à jour à haute fréquence parallèlement à la boucle principale du jeu, tout en garantissant un échange de données sécurisé entre les threads afin de maintenir un retour de force cohérent et précis.
Étendre les paramètres par défaut du retour de force
Retour de force de base Commencez par configurer la scène du tutoriel. Pour mettre en œuvre des animations dynamiques, SphereForceFeedback Nous allons modifier le script afin qu'il réagisse aux mouvements de la sphère et simule une interaction avec un objet en mouvement dynamique.
Principales modifications
- Déplacement dynamique d'objets: comprend la logique permettant de mettre à jour la position et la vitesse de Sphere en fonction des entrées de l'utilisateur ou de schémas de mouvement prédéfinis.
- Échange de données sans risque pour les threads:
ReaderWriterLockSlimIl permet de gérer l'accès simultané aux données partagées entre le thread principal et le thread haptique. - Réglage de la puissance:
SphereForceFeedback.ForceCalculationModifiez la méthode afin qu'elle tienne compte de la vitesse de la sphère et fournisse un retour d'information réaliste basé à la fois sur la position et le mouvement.
Interactions dynamiques
Pour simuler une sphère en mouvement, Update Vous pouvez mettre à jour manuellement la position dans la méthode ou utiliser un composant distinct pour contrôler les mouvements en fonction des saisies au clavier ou d'autres interactions.
Dans cet exemple, Sphère Le nom de l'objet de jeu Balle en mouvementet le remplacer par, Tutoriels Contenu dans l'échantillon : MovingObject Je vais ajouter un composant.
adapté au mouvement ForceCalculation Ajustement
Il faut désormais prendre en compte la vitesse de la « Moving Ball » dans le calcul du retour de force et ajuster celle-ci en fonction à la fois de la position et de la vitesse de l'interaction. Cela permet d'offrir une sensation haptique plus nuancée et plus réaliste, reflétant les caractéristiques dynamiques de l'interaction.
Vector3de typeotherVelocityAjoutez des paramètres à la méthode.force -= cursorVelocity * damping을force -= (cursorVelocity - otherVelocity) * dampingVeuillez le remplacer par.
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 sans risque pour les threads
Dans les scènes dynamiques en temps réel où les objets se déplacent et interagissent, il est essentiel de garantir que le calcul du retour haptique s'appuie toujours sur les données les plus récentes, sans risque de corruption des données due à des accès simultanés. C'est précisément dans ce contexte que l'échange de données sécurisé au niveau des threads s'avère indispensable.
Concepts clés de l'échange de données sans risque pour les threads
- Mécanismes de sécurité inter-threads:
ReaderWriterLockSlimIl permet de gérer l'accès simultané aux données. Cela permet d'effectuer plusieurs opérations de lecture ou une seule opération d'écriture, tout en garantissant l'intégrité des données. - Lecture et écriture de données:
- Lecture: un thread haptique lit la position et la vitesse d'un objet alors que celui-ci est verrouillé en lecture, sans interférer avec la mise à jour des données.
- Écriture: la mise à jour des données d'un objet par le thread principal s'effectue sous verrou d'écriture, ce qui empêche toute perte de cohérence des données due à des opérations de lecture ou d'écriture simultanées.
Implémentation dans Unity
-
Structure de données de scène: afin de faciliter les opérations sans risque de conflit entre threads, nous définissons une structure contenant toutes les données nécessaires relatives à la scène. Cette structure comprend la position et la vitesse de la « Moving Ball » etdu curseur, ainsi que le rayon. Cette structure de données sert de base à l'échange de données sans risque de conflit entre threads.
private struct SceneData{public Vector3 ballPosition;public Vector3 ballVelocity;public float ballRadius;public float cursorRadius;}private SceneData _cachedSceneData; -
Réinitialisation du verrouillage: Afin de gérer l'accès aux données de scène
ReaderWriterLockSlimInitialise une instance. Ce verrou permet à plusieurs threads de lire les données simultanément, ou à un seul thread de verrouiller les données de manière exclusive pour les écrire ; ainsi, l'intégrité des données est garantie même en cas d'opérations simultanées.private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); -
Écriture dans le cache à l'aide d'un verrou d'écriture:
SaveSceneDataLa méthode met à jour les données de la scène sous verrou d'écriture. Cela permet d'éviter les conflits d'accès et de garantir la cohérence, car aucun autre thread ne peut lire ni écrire les données pendant qu'un thread les met à jour.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 de la mémoire cache à l'aide d'un verrou de lecture:
GetSceneDataLa méthode 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 et en toute sécurité, sans perturber les opérations d'écriture, et garantit que le calcul du retour haptique s'appuie 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 du thread principal:
FixedUpdateCette méthode sert à mettre à jour périodiquement les données de la scène dans le thread principal. Cela permet au calcul du retour haptique d'accéder aux données les plus récentes et de refléter ainsi les caractéristiques dynamiques de la scène.private void FixedUpdate(){SaveSceneData();} -
Application du calcul de force à l'aide de données sécurisées pour les threads:
OnDeviceStateChangedDans la fonction de rappel, le calcul des forces est effectué à l'aide des données de scène les plus récentes obtenues via des méthodes thread-safe. Cela garantit que le retour de force est précis et réagit avec sensibilité aux interactions dynamiques au sein 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
Grâce à ces améliorations apportées au script, vous pouvez interagir avec une sphère qui se déplace de manière active au sein de la scène. Le retour haptique est ajusté dynamiquement en fonction de la trajectoire de la sphère, offrant ainsi une expérience plus immersive et plus riche sur le plan tactile.

Fichier source
La scène complète de cet exemple ainsi que les fichiers associés sont disponibles dans la section « Tutorials » du gestionnaire de paquets Unity.
Tutoriel L'exemple contient plusieurs exemples illustrant comment utiliser les entrées clavier pour contrôler les mouvements d'objets de jeu,
MovableObjectIl contient un script.
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);
}
}
}