Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions Packages/com.styly.styly-xr-rig/Runtime/STYLY XR Rig.prefab
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ GameObject:
- component: {fileID: 1901460064738478333}
- component: {fileID: 4321886860393714057}
- component: {fileID: 129172355910230854}
- component: {fileID: 6993317928114314129}
m_Layer: 0
m_Name: STYLY XR RigManager
m_TagString: Untagged
Expand Down Expand Up @@ -249,6 +250,51 @@ MonoBehaviour:
target: {fileID: 932545197216251794}
moveSpeed: 5
lookSensitivity: 8
--- !u!114 &6993317928114314129
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1591456111131864104}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5b7de3e7974a54b6eaa9f0b6ee1c46c6, type: 3}
m_Name:
m_EditorClassIdentifier:
moveAction:
m_UseReference: 1
m_Action:
m_Name: Move
m_Type: 0
m_ExpectedControlType:
m_Id: 58d00aa3-b799-4f2c-8ac7-3e09b2d2094a
m_Processors:
m_Interactions:
m_SingletonActionBindings: []
m_Flags: 0
m_Reference: {fileID: 6972639530819350904, guid: c348712bda248c246b8c49b3db54643f,
type: 3}
snapTurnAction:
m_UseReference: 1
m_Action:
m_Name: Snap Turn
m_Type: 0
m_ExpectedControlType:
m_Id: 4fa0c090-6501-4bd0-86bc-70767d9b47a3
m_Processors:
m_Interactions:
m_SingletonActionBindings: []
m_Flags: 0
m_Reference: {fileID: -8525429354371678379, guid: c348712bda248c246b8c49b3db54643f,
type: 3}
headTransform: {fileID: 0}
moveTarget: {fileID: 932545197216251794}
moveSpeed: 2
flattenToGroundPlane: 1
snapTurnDegrees: 45
snapTurnDeadzone: 0.5
snapTurnDebounce: 0.25
--- !u!1001 &1313010322738206687
PrefabInstance:
m_ObjectHideFlags: 0
Expand Down
203 changes: 203 additions & 0 deletions Packages/com.styly.styly-xr-rig/Runtime/Scripts/VrStickController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
using System;
using UnityEngine;
using UnityEngine.InputSystem;

namespace Styly.XRRig
{
public class VrStickController : MonoBehaviour
{
// Error message constant
private const string MoveTargetNotSetError = "VrStickController: moveTarget is not set. Please assign the Transform to move in the Inspector.";

[Header("Input Actions (assign in Inspector)")]
[Tooltip("Assign \"XRI Left Locomotion/Move\" from Starter Assets/XRI Default Input Actions")]
public InputActionProperty moveAction;

[Tooltip("Assign \"XRI Right Locomotion/Snap Turn\" from Starter Assets/XRI Default Input Actions")]
public InputActionProperty snapTurnAction;

// Head (Main Camera) will be assigned
private Transform headTransform;

// STYLY XR Rig root will be assigned
private Transform moveTarget;

[Tooltip("Move speed (m/s)")]
public float moveSpeed = 2.0f;

[Tooltip("Ignore Y component and move along the horizontal plane")]
public bool flattenToGroundPlane = true;

[Header("Snap Turn")]
[Tooltip("Snap turn angle per step (degrees)")]
public float snapTurnDegrees = 45f;

[Tooltip("Snap turn input threshold (right: +threshold / left: -threshold)")]
[Range(0.1f, 0.95f)]
public float snapTurnDeadzone = 0.5f;

[Tooltip("Minimum interval between consecutive snap turns in the same direction (seconds)")]
public float snapTurnDebounce = 0.25f;

// Internal state
float _prevSnapValue = 0f;
float _lastSnapTime = -999f;

void Awake()
{
// Use Main Camera if not explicitly specified
if (headTransform == null && Camera.main != null)
headTransform = Camera.main.transform;
}

void Start()
{
// Remove redundant assignment as it's already done in Awake
// Only find and assign moveTarget with proper null checking
var stylyXrRig = FindFirstObjectByType<Styly.XRRig.StylyXrRig>();
if (stylyXrRig != null)
{
moveTarget = stylyXrRig.transform;
}
else
{
Debug.LogError("VrStickController: No StylyXrRig found in the scene. Please ensure STYLY XR Rig is present.");
}
}

void OnEnable()
{
// Enable InputActions (generally safe even with Input Action Manager)
if (moveAction.action != null) moveAction.action.Enable();
if (snapTurnAction.action != null) snapTurnAction.action.Enable();
}

void OnDisable()
{
if (moveAction.action != null) moveAction.action.Disable();
if (snapTurnAction.action != null) snapTurnAction.action.Disable();
}

void Update()
{
HandleMove();
HandleSnapTurn();
}

void HandleMove()
{
if (moveAction.action == null) return;

if (moveTarget == null)
{
Debug.LogError(MoveTargetNotSetError);
return;
}

Vector2 move = Vector2.zero;
try
{
move = moveAction.action.ReadValue<Vector2>();
}
catch (InvalidOperationException ex)
{
Debug.LogWarning($"VrStickController: Failed to read move input as Vector2: {ex.Message}");
}
catch (Exception ex)
{
Debug.LogError($"VrStickController: Unexpected error reading move input: {ex.Message}");
}

if (move.sqrMagnitude < 0.0001f) return;

// Forward and right vectors based on head (camera)
Vector3 forward = headTransform ? headTransform.forward : transform.forward;
Vector3 right = headTransform ? headTransform.right : transform.right;

if (flattenToGroundPlane)
{
forward = Vector3.ProjectOnPlane(forward, Vector3.up).normalized;
right = Vector3.ProjectOnPlane(right, Vector3.up).normalized;
}
else
{
forward.Normalize();
right.Normalize();
}

Vector3 worldMove = forward * move.y + right * move.x;
moveTarget.position += worldMove * moveSpeed * Time.deltaTime;
}

void HandleSnapTurn()
{
if (snapTurnAction.action == null) return;

if (moveTarget == null)
{
Debug.LogError(MoveTargetNotSetError);
return;
}

float raw = 0f;
try
{
// XRI Snap Turn is often mapped to float or Vector2.x
// InputAction itself has no valueType, so check from bound control
var action = snapTurnAction.action;
InputControl control = action.activeControl ?? (action.controls.Count > 0 ? action.controls[0] : null);
Type ctrlType = control != null ? control.valueType : null;

if (ctrlType == typeof(Vector2))
{
raw = action.ReadValue<Vector2>().x;
}
else
{
// Default is float (Axis)
raw = action.ReadValue<float>();
}
}
catch (InvalidOperationException ex)
{
Debug.LogWarning($"VrStickController: Failed to read snap turn input: {ex.Message}");
}
catch (Exception ex)
{
Debug.LogError($"VrStickController: Unexpected error reading snap turn input: {ex.Message}");
}

// Fire only on edge crossing the deadzone threshold + debounce
float now = Time.time;
bool risingRight = (_prevSnapValue <= snapTurnDeadzone) && (raw > snapTurnDeadzone);
bool fallingLeft = (_prevSnapValue >= -snapTurnDeadzone) && (raw < -snapTurnDeadzone);

if ((risingRight || fallingLeft) && (now - _lastSnapTime >= snapTurnDebounce))
{
float dir = risingRight ? +1f : -1f;
// Rotate horizontally (Y axis in world space)
moveTarget.Rotate(0f, snapTurnDegrees * dir, 0f, Space.World);
_lastSnapTime = now;
}

_prevSnapValue = raw;
}

#if UNITY_EDITOR
void OnValidate()
{
if (headTransform == null && Camera.main != null)
headTransform = Camera.main.transform;

if (moveTarget == null)
{
Debug.LogWarning(MoveTargetNotSetError);
}

moveSpeed = Mathf.Max(0f, moveSpeed);
snapTurnDegrees = Mathf.Clamp(snapTurnDegrees, 1f, 180f);
snapTurnDebounce = Mathf.Clamp(snapTurnDebounce, 0f, 2f);
}
#endif
}
}