Tutoriel sur le retour de force dans les scènes dynamiques
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 le retour de force provenant d'un objet en mouvement. Ce scénario met en évidence la nécessité d'un taux de rafraîchissement élevé pour le retour de force, qui dépasse largement la fréquence de rafraîchissement habituelle du rendu visuel dans Unity.
Introduction
Pour offrir une expérience haptique convaincante, en particulier dans les scènes dynamiques, il est essentiel d’effectuer les calculs à des fréquences supérieures à 1 kHz. Cela diffère considérablement de la boucle de mise à jour du jeu, qui fonctionne généralement à environ 60 Hz. Le défi consiste à gérer ces mises à jour à haute fréquence en parallèle de la boucle principale du jeu et à garantir un échange de données « thread-safe » afin de maintenir un retour haptique cohérent et précis.
Extension de la configuration de base du retour de force
Retour d'information tactile de baseNous allons commencer par la configuration de la scène du tutoriel. Afin d'intégrer des mouvements dynamiques,SphereForceFeedback Adaptez le script afin qu'il réagisse aux mouvements de la sphère et permette de simuler une interaction avec des objets en mouvement.
Principales modifications
- Déplacement des objets dynamiques: intégrez une logique permettant de mettre à jour la position et la vitesse de la sphère en fonction des actions de l'utilisateur ou de schémas de mouvement prédéfinis.
- Échange de données sans risque de conflit entre threads:
ReaderWriterLockSlimpermet de gérer l'accès simultané aux données partagées entre le thread principal et le thread de force. - Ajustement du calcul de la force:
SphereForceFeedback.ForceCalculationEn modifiant la méthode et en tenant compte de la vitesse de la sphère, nous fournissons un retour d'information réaliste basé à la fois sur la position et le mouvement.
Interactions dynamiques
Pour simuler un corps sphérique en mouvement,Update Vous pouvez soit mettre à jour manuellement la position à l'aide d'une méthode, soit utiliser un autre composant permettant de contrôler le déplacement en fonction des saisies au clavier ou d'autres interactions.
Dans cet exemple,Sphère Le nom de l'objet de jeu Balle en mouvement et l'a remplacé par,Tutoriels Contenu de l'échantillon MovingObject Ajouter un composant.
Adapté à la mobilité ForceCalculation Ajustement de
Le calcul du retour haptique doit désormais tenir compte de la vitesse de la « Moving Ball » ; la force est ainsi ajustée en fonction à la fois de la position et de la vitesse de l'interaction. Cela permet d'obtenir un retour haptique plus subtil et plus réaliste, qui reflète la nature dynamique de l'interaction.
Vector3Paramètres des méthodes de typeotherVelocityAjouterforce -= cursorVelocity * dampingをforce -= (cursorVelocity - otherVelocity) * dampingpar
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 de conflit entre threads
Dans une scène dynamique où les objets se déplacent et interagissent en temps réel, il est extrêmement important de garantir que le calcul du retour tactile repose toujours sur les données les plus récentes, sans risquer de corrompre les données en raison d'accès simultanés. C'est là qu'un échange de données « thread-safe » s'avère indispensable.
Principes fondamentaux de l'échange de données sans risque de conflit entre threads
- Mécanisme « thread-safe »:
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: le thread tactile lit la position et la vitesse de l'objet sous le verrou de lecture. Cela permet d'éviter d'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 un verrou d'écriture. Cela permet d'éviter toute incohérence dans l'état des données due à des opérations de lecture ou d'écriture simultanées.
Implémentation dans Unity
-
Structure pour les 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 dela « Moving Ball » et du curseur, ainsi que leur rayon respectif. Cette structure de données constitue la base d'un é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; -
Initialisation du verrou: Afin de gérer l'accès aux données de scène,
ReaderWriterLockSlimInitialise l'instance. Ce verrou permet à plusieurs threads de lire simultanément les données et à un seul thread de verrouiller les données de manière exclusive pour les écrire, garantissant ainsi la cohérence des données lors d'opérations simultanées.private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); -
Écriture dans le cache en raison d'un verrou d'écriture:
SaveSceneDataLa méthode met à jour les données de la scène sous un verrou d'écriture. Ainsi, tant qu'un thread met à jour les données, les autres threads ne peuvent ni lire ni écrire ces données, ce qui permet d'éviter les conflits d'accès et de garantir 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 à partir du cache avec verrouillage en lecture:
GetSceneDataLa méthode récupère les données de scène sous un verrou de lecture. Cela permet à plusieurs threads de lire les données simultanément et en toute sécurité, sans interférer avec les opérations d'écriture, et garantit ainsi que le calcul du retour tactile s'effectue sur la base des 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:
FixedUpdateCette méthode sert à mettre à jour régulièrement les données de la scène sur le thread principal. Cela permet au calcul du retour tactile d'accéder en permanence aux données les plus récentes, reflétant ainsi la nature dynamique de la scène.private void FixedUpdate(){SaveSceneData();} -
Application du calcul des forces à l'aide de données mises à jour:
OnDeviceStateChangedAu sein de la fonction de rappel, le calcul des forces s'effectue à l'aide des données de scène les plus récentes, obtenues de manière « thread-safe ». Cela permet d'assurer la précision du retour d'information sur les forces et de maintenir la réactivité face 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 aux fonctionnalités avancées de ces scripts, vous pourrez interagir avec une sphère qui se déplace de manière active au sein de la scène. Le retour tactile évolue de manière dynamique 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 de cet exemple ainsi que les fichiers associés peuvent être importés à partir de l'exemple « Tutorials » du Gestionnaire de packages Unity.
Tutoriel L'exemple contient plusieurs cas d'utilisation
MovableObjectIl contient un script qui permet de contrôler le déplacement d'un objet de jeu associé via les commandes 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);
}
}
}