Aller au contenu principal

Tutoriel sur le retour de force dans les scènes dynamiques

En complément du tutoriel sur le retour de force de base, ce guide explique comment simuler des interactions dynamiques dans Unity, afin de permettre à l'utilisateur de ressentir le retour de force provenant d'objets en mouvement. Ce scénario met en évidence le besoin d'un retour tactile à haute fréquence, dont la fréquence de mise à jour est bien supérieure à celle habituellement utilisée pour le rendu visuel dans Unity.

Présentation​

Pour offrir une expérience tactile captivante, 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 le cycle de rafraîchissement habituel des jeux, qui fonctionne généralement à environ 60 Hz. Le défi consiste à gérer ces mises à jour à haute fréquence en parallèle du cycle principal du jeu, tout en garantissant un échange de données sécurisé au niveau des threads, afin de préserver la cohérence et la précision du retour de force.

Paramètres avancés de retour de force​

Depuis Retour de force de base Commençons par la configuration de la scène dans ce tutoriel. Afin d'y ajouter des effets dynamiques, nous allons modifier SphereForceFeedback un script permettant de réagir aux mouvements de la sphère et de simuler les interactions avec des objets en mouvement.

Principales modifications​

  • Mouvement d'un objet dynamique: intégrez une logique permettant de mettre à jour la position et la vitesse de la sphère en fonction des entrées de l'utilisateur ou de schémas de mouvement prédéfinis.
  • Échange de données sécurisé au niveau des threads: Utilisation ReaderWriterLockSlim Gérer l'accès concurrent aux données partagées entre le thread principal et le thread tactile.
  • Ajustement du calcul de la force: Modification SphereForceFeedback.ForceCalculation Une méthode permettant de prendre en compte la vitesse de la sphère, afin de fournir un retour d'information réaliste en fonction de la position et du mouvement.

Interaction dynamique​

Pour simuler une sphère en mouvement, vous pouvez utiliser Update Vous pouvez mettre à jour manuellement sa position dans le code, ou utiliser un composant distinct pour contrôler son déplacement en fonction des entrées clavier ou d'autres interactions. Dans cet exemple, nous allons Sphère Renommer l'objet du jeu en Balle en mouvement, puis ajouter Tutoriels fournis dans l'exemple MovingObject Composants.

Adaptation à l'effort physique ForceCalculation​

Le calcul de la force de retour doit désormais tenir compte de la vitesse de la « Moving Ball » et ajuster la force en fonction de la position et de la vitesse de l'interaction. Cela permet d'offrir des sensations tactiles plus fines et plus réalistes, reflétant ainsi les caractéristiques dynamiques de l'interaction.

  • Ajouter un Vector3 de type otherVelocity Paramètres de la méthode
  • Va force -= cursorVelocity * damping Remplacer 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 sécurisé au niveau des threads​

Dans les scènes dynamiques où les objets se déplacent et interagissent en temps réel, il est essentiel de s'assurer que le calcul du retour tactile repose sur les données les plus récentes, tout en évitant que des accès simultanés n'entraînent une corruption des données. C'est précisément pour cette raison que l'échange de données sécurisé au niveau des threads devient indispensable.

Les concepts fondamentaux de l'échange de données sécurisé au niveau des threads​

  • Mécanismes de sécurité des threads: Utilisation ReaderWriterLockSlim Gérer l'accès concurrent aux données. Ce mécanisme permet d'effectuer simultanément plusieurs opérations de lecture ou une seule opération d'écriture, garantissant ainsi 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 la protection d'un verrou de lecture, afin de garantir qu'il n'interfère pas avec la mise à jour des données.
    • Écriture: les mises à jour des données d'un objet effectuées par le thread principal sont protégées par un verrou d'écriture, ce qui empêche toute incohérence des données due à des opérations de lecture et d'écriture simultanées.

Implémentation dans Unity​

  • Structure de données de scène: afin de faciliter l'exécution d'opérations sûres sur les threads, nous avons défini une structure destinée à stocker toutes les données nécessaires relatives à la scène. Cette structure contient les positions et les vitesses de la « Moving Ball » et du curseur, ainsi que leurs rayons respectifs. Cette structure de données constitue la base de notre implémentation d'un échange de données sûr sur les threads.

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

    private SceneData _cachedSceneData;
  • Initialisation du verrou: Initialiser un ReaderWriterLockSlim Exemple : permet de gérer l'accès aux données d'une scène. Ce verrou autorise plusieurs threads à lire les données simultanément, ou permet à un seul thread d'obtenir un verrou exclusif pour effectuer des écritures, garantissant ainsi l'intégrité des données lors d'opérations concurrentes.

    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
  • Utilisation d'un verrou d'écriture pour l'écriture dans le cache:SaveSceneData La méthode permet de mettre à jour les données de la scène à l'intérieur d'un verrou. Cela garantit que, lorsqu'un thread met à jour les données, les autres threads ne peuvent ni les lire ni les modifier, ce qui évite les conflits d'accès 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();
    }
    }
  • Utilisation d'un verrou de lecture pour lire le cache:GetSceneData Méthode permettant d'obtenir les données de scène sous la protection d'un verrou de lecture. Cela permet à plusieurs threads de lire les données en parallèle en toute sécurité, sans perturber les opérations d'écriture, garantissant ainsi que le calcul du retour tactile repose 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:FixedUpdate Cette méthode permet de mettre à jour régulièrement les données de la scène dans le thread principal. Cela garantit que le calcul du retour tactile s'appuie sur les données les plus récentes, reflétant ainsi les changements dynamiques de la scène.

    private void FixedUpdate()
    {
    SaveSceneData();
    }
  • Effectuer les calculs de force à l'aide des données mises à jour: à OnDeviceStateChanged Lors du rappel, le calcul des forces s'effectue à partir des données de scène les plus récentes, obtenues via une méthode sécurisée au niveau des threads. Cela garantit la précision du retour de force et permet de réagir rapidement 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 fonctionnalités avancées de script vous permettent d'interagir avec des sphères qui se déplacent de manière autonome dans la scène. Le retour tactile s'adapte dynamiquement en fonction de la trajectoire des sphères, offrant ainsi une expérience plus immersive et plus riche sur le plan tactile.

La petite balle qui bouge

Fichier source​

Le scénario complet de cet exemple ainsi que les fichiers associés peuvent être importés à partir de l'exemple « Tutorials » dans le Gestionnaire de paquets Unity.

Tutoriel L'exemple comprend MovableObject Script utilisé dans plusieurs exemples pour contrôler le déplacement d'un objet de jeu associé à l'aide des 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);
}
}
}