주요 콘텐츠로 건너뛰기
버전: 2.1.1

다이내믹 씬 튜토리얼의 강제 피드백

이 가이드에서는 기본 포스 피드백 튜토리얼을 기반으로 Unity 내에서 동적 상호작용을 시뮬레이션하여 사용자가 움직이는 오브젝트에서 포스 피드백을 느낄 수 있도록 하는 방법을 소개합니다. 이 시나리오는 햅틱 피드백을 위한 고빈도 업데이트의 필요성을 강조하며, 이는 Unity의 일반적인 시각 렌더링 업데이트 속도를 훨씬 뛰어넘는 것입니다.

소개

특히 역동적인 장면에서 매력적인 햅틱 경험을 제공하려면 1kHz 이상의 주파수에서 계산을 수행하는 것이 중요합니다. 이는 약 60Hz로 작동하는 일반적인 게임 업데이트 루프와는 완전히 대조적입니다. 문제는 이러한 고주파 업데이트를 메인 게임 루프와 함께 관리하여 일관되고 정확한 포스 피드백을 유지하기 위한 스레드 안전 데이터 교환을 보장하는 것입니다.

기본 강제 피드백 설정 확장하기

에서 씬 설정부터 시작하세요. 기본 강제 피드백 튜토리얼을 참고하세요. 동적 동작을 통합하기 위해 동적 동작을 통합하기 위해 SphereForceFeedback 스크립트를 사용하여 구의 움직임에 반응하고 동적으로 움직이는 물체와의 상호작용을 시뮬레이션할 수 있습니다.

주요 수정 사항

  • 동적 오브젝트 이동: 사용자 입력 또는 미리 정의된 이동 패턴에 따라 구의 위치와 속도를 업데이트하는 로직을 통합합니다.
  • 스레드 안전 데이터 교환: 사용 ReaderWriterLockSlim 를 사용하여 메인 스레드와 햅틱 스레드 간의 공유 데이터에 대한 동시 액세스를 관리할 수 있습니다.
  • 힘 계산 조정: 수정 SphereForceFeedback.ForceCalculation 메서드를 사용하여 구의 속도를 고려하여 위치와 모션 모두에 기반한 사실적인 피드백을 제공합니다.

동적 상호 작용

움직이는 구를 시뮬레이션하려면, 움직이는 구의 위치를 수동으로 업데이트하거나 Update 메서드를 사용하거나 별도의 컴포넌트를 사용하여 키보드 입력이나 기타 상호 작용에 따라 움직임을 제어할 수 있습니다. 이 예제에서는 구체 게임 개체를 움직이는 공 를 클릭하고 MovingObject 컴포넌트에서 주어진 튜토리얼 샘플.

조정 ForceCalculation 움직임

이제 힘 피드백 계산은 움직이는 공의 속도를 고려하여 상호작용의 위치와 속도에 따라 힘을 조정해야 합니다. 이를 통해 상호작용의 역동적인 특성을 반영하여 보다 미묘하고 사실적인 햅틱 감각을 제공합니다.

  • 추가 Vector3 otherVelocity 메서드 매개변수
  • 교체 force -= cursorVelocity * damping by 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;
}

스레드 안전 데이터 교환

오브젝트가 실시간으로 움직이고 상호작용하는 역동적인 장면에서는 동시 액세스로 인한 데이터 손상 없이 최신 데이터를 기반으로 햅틱 피드백을 계산하는 것이 매우 중요합니다. 바로 이 부분에서 스레드 안전 데이터 교환이 필수적입니다.

스레드 안전 데이터 교환을 위한 주요 개념

  • 스레드 안전 메커니즘: 활용 ReaderWriterLockSlim 를 사용해 동시 데이터 액세스를 관리할 수 있습니다. 이를 통해 여러 번의 읽기 또는 한 번의 쓰기 작업을 허용하여 데이터 무결성을 보장합니다.
  • 데이터 읽기 및 쓰기:
    • 읽기: 햅틱 스레드는 읽기 잠금 상태에서 객체의 위치와 속도를 읽어 데이터 업데이트를 방해하지 않도록 합니다.
    • 쓰기: 쓰기: 메인 스레드에 의한 객체 데이터 업데이트는 쓰기 잠금 상태에서 수행되므로 데이터 상태가 일관되지 않을 수 있는 동시 읽기 또는 쓰기를 방지할 수 있습니다.

Unity에서 구현하기

  • 씬 데이터용 구조체: 스레드 안전 작업을 용이하게 하기 위해 장면에 대한 모든 필요한 데이터를 저장하는 구조를 정의합니다. 이 구조에는 움직이는 공과 커서의 위치와 속도, 반경이 모두 포함됩니다. 이 데이터 구조는 스레드 안전 데이터 교환의 기반이 됩니다.

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

    private SceneData _cachedSceneData;
  • 잠금 초기화: A ReaderWriterLockSlim 인스턴스가 초기화되어 씬 데이터에 대한 액세스를 관리합니다. 이 잠금을 통해 여러 스레드가 동시에 데이터를 읽거나 단일 스레드가 쓸 수 있도록 데이터를 독점적으로 잠글 수 있으므로 동시 작업 중 데이터 무결성을 보장할 수 있습니다.

    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
  • 쓰기 잠금으로 캐시에 쓰기: : The SaveSceneData 메서드는 쓰기 잠금 내에서 씬 데이터를 업데이트합니다. 이렇게 하면 한 스레드가 데이터를 업데이트하는 동안 다른 스레드는 데이터를 읽거나 쓸 수 없으므로 데이터 경합을 방지하고 일관성을 보장할 수 있습니다.

    private void SaveSceneData()
    {
    _cacheLock.EnterWriteLock();
    try
    {
    var t = transform;
    _cachedSceneData.ballPosition = t.position;
    _cachedSceneData.ballRadius = t.lossyScale.x / 2f;
    _cachedSceneData.cursorRadius = inverse3.Cursor.Model.transform.lossyScale.x / 2f;
    _cachedSceneData.ballVelocity = _movableObject.CursorVelocity;
    }
    finally
    {
    _cacheLock.ExitWriteLock();
    }
    }
  • 읽기 잠금으로 캐시에서 읽기: : The GetSceneData 메서드는 읽기 잠금 상태에서 씬 데이터를 검색합니다. 이렇게 하면 여러 스레드가 쓰기 작업을 방해하지 않고 동시에 데이터를 안전하게 읽을 수 있으므로 햅틱 피드백 계산이 최신 씬 데이터를 기반으로 이루어집니다.

    private SceneData GetSceneData()
    {
    _cacheLock.EnterReadLock();
    try
    {
    return _cachedSceneData;
    }
    finally
    {
    _cacheLock.ExitReadLock();
    }
    }
  • 메인 스레드 데이터 업데이트: : The FixedUpdate 메서드는 메인 스레드에서 씬 데이터를 주기적으로 업데이트하는 데 사용됩니다. 이렇게 하면 햅틱 피드백 계산이 장면의 동적 특성을 반영하여 가장 최신 데이터에 액세스할 수 있습니다.

    private void FixedUpdate()
    {
    SaveSceneData();
    }
  • 업데이트된 데이터로 힘 계산 적용하기: 에서 OnDeviceStateChanged 콜백을 호출하면 스레드 안전 메서드를 통해 얻은 최신 씬 데이터를 사용하여 힘 계산이 수행됩니다. 따라서 포스 피드백이 정확하고 씬 내의 동적 상호작용에 반응할 수 있습니다.

    var sceneData = GetSceneData();

    var force = ForceCalculation(device.CursorLocalPosition, device.CursorLocalVelocity, sceneData.cursorRadius,
    sceneData.ballPosition, sceneData.ballVelocity, sceneData.ballRadius);

게임 플레이 경험

이러한 스크립트 개선 사항을 통해 씬에서 활발하게 움직이는 구체와 상호작용할 수 있습니다. 햅틱 피드백은 구의 궤적에 따라 동적으로 조정되어 더욱 몰입감 있고 촉각적으로 풍부한 경험을 제공합니다.

움직이는 공

소스 파일

이 예제의 전체 씬과 관련 파일은 Unity 패키지 관리자의 튜토리얼 샘플에서 임포트할 수 있습니다.

그리고 튜토리얼 샘플에는 MovableObject 스크립트를 사용하여 키보드 입력으로 첨부된 게임 오브젝트의 움직임을 제어할 수 있습니다.

SphereForceFeedback.cs

/*
* Copyright 2024 Haply Robotics Inc. All rights reserved.
*/

using System.Threading;
using Haply.Inverse.Unity;
using Haply.Samples.Tutorials.Utils;
using UnityEngine;

namespace Haply.Samples.Tutorials._4A_DynamicForceFeedback
{
public class SphereForceFeedback : MonoBehaviour
{
// must assign in inspector
public Inverse3 inverse3;

[Range(0, 800)]
// Stiffness of the force feedback.
public float stiffness = 300f;

[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.Model.transform.lossyScale.x / 2f;

_cachedSceneData.ballVelocity = _movableObject.CursorVelocity;
}
finally
{
_cacheLock.ExitWriteLock();
}
}

#endregion

/// <summary>
/// Saves the initial scene data cache.
/// </summary>
private void Start()
{
_movableObject = GetComponent<MovableObject>();
SaveSceneData();
}

/// <summary>
/// Update scene data cache.
/// </summary>
private void FixedUpdate()
{
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;
}

/// <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="device">The Inverse3 device instance.</param>
private void OnDeviceStateChanged(Inverse3 device)
{
var sceneData = GetSceneData();

// Calculate the moving ball force.
var force = ForceCalculation(device.CursorLocalPosition, device.CursorLocalVelocity, sceneData.cursorRadius,
sceneData.ballPosition, sceneData.ballVelocity, sceneData.ballRadius);

// Apply the force to the cursor.
device.CursorSetLocalForce(force);
}
}
}