Added Snap Turning (#556)

* Added Snap Turning

- Disabled by default, can be enabled in settings
- Amount per input can be defined in settings (current range is 15 - 90 degrees)
- Default input action is the right thumb stick (same as smooth turn)

* Moved snap turn from slider to selector for settings

* Fixed snap turning with smooth HUD/Helmet

- Previously, the smooth HUD/Helmet would uncomfortably move a long distance after you snap turn
- Now, if smooth HUD and snap turning are both enabled, the helmet snap turns for a frame, then resumes it's previous behavior

* Disable snap turning while in zero-g

---------

Co-authored-by: Raicuparta <Raicuparta@users.noreply.github.com>
This commit is contained in:
Ezra Hill 2025-03-02 16:55:41 -05:00 committed by GitHub
parent 28a335a38d
commit aefee463b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 126 additions and 12 deletions

View File

@ -17,6 +17,8 @@ namespace NomaiVR.ModConfig
bool PreventClipping { get; }
bool FlashlightGesture { get; }
bool ControllerOrientedMovement { get; }
bool SnapTurning { get; }
string SnapTurnIncrement { get; }
bool AutoHideToolbelt { get; }
float ToolbeltHeight { get; }
float HudScale { get; }

View File

@ -19,6 +19,8 @@ namespace NomaiVR.ModConfig
public static bool PreventClipping => settingsProvider.PreventClipping;
public static bool FlashlightGesture => settingsProvider.FlashlightGesture;
public static bool ControllerOrientedMovement => settingsProvider.ControllerOrientedMovement;
public static bool SnapTurning => settingsProvider.SnapTurning;
public static string SnapTurnIncrement => settingsProvider.SnapTurnIncrement;
public static bool AutoHideToolbelt => settingsProvider.AutoHideToolbelt;
public static float ToolbeltHeight => settingsProvider.ToolbeltHeight;
public static float HudScale => settingsProvider.HudScale;

View File

@ -18,6 +18,8 @@ namespace NomaiVR.ModConfig
public bool PreventClipping { get; private set; }
public bool FlashlightGesture { get; private set; }
public bool ControllerOrientedMovement { get; private set; }
public bool SnapTurning { get; private set; }
public string SnapTurnIncrement { get; private set; }
public bool AutoHideToolbelt { get; private set; }
public float ToolbeltHeight { get; private set; }
public float HudScale { get; private set; }
@ -38,6 +40,8 @@ namespace NomaiVR.ModConfig
VibrationStrength = config.GetSettingsValue<float>("vibrationIntensity");
ShowHelmet = config.GetSettingsValue<bool>("helmetVisibility");
ControllerOrientedMovement = config.GetSettingsValue<bool>("movementControllerOriented");
SnapTurning = config.GetSettingsValue<bool>("snapTurning");
SnapTurnIncrement = config.GetSettingsValue<string>("snapTurnIncrement");
EnableGesturePrompts = config.GetSettingsValue<bool>("showGesturePrompts");
EnableHandLaser = config.GetSettingsValue<bool>("showHandLaser");
EnableFeetMarker = config.GetSettingsValue<bool>("showFeetMarker");

View File

@ -78,6 +78,25 @@
"yes": "Controller",
"no": "Head"
},
"snapTurning": {
"type": "toggle",
"value": false,
"title": "Snap turning",
"yes": "Enabled",
"no": "Disabled"
},
"snapTurnIncrement": {
"type": "selector",
"value": "45",
"options": [
"15",
"30",
"45",
"60",
"90"
],
"title": "Snap turn increment"
},
"showGesturePrompts": {
"type": "toggle",
"value": true,

View File

@ -14,6 +14,8 @@ namespace NomaiVR.Player
public class Behaviour : MonoBehaviour
{
public static Action OnSnapTurn;
private Transform cameraParent;
private static Transform playArea;
private static OWCamera playerCamera;
@ -22,6 +24,10 @@ namespace NomaiVR.Player
private static PlayerCharacterController playerController;
private static Autopilot autopilot;
private readonly SteamVR_Action_Boolean recenterAction = SteamVR_Actions._default.Recenter;
private static readonly SteamVR_Action_Vector2 turnAction = SteamVR_Actions._default.Look;
private static readonly float snapTurnInputThreshold = 0.15f;
private static bool isSnapTurnInCooldown = false;
private float lastTurnTime;
internal void Start()
{
@ -95,12 +101,23 @@ namespace NomaiVR.Player
}
UpdateRecenter();
if (ModSettings.SnapTurning)
{
// Check if time has passed and input stick was returned to neutral
if (isSnapTurnInCooldown && Time.time - lastTurnTime > 0.05f && Mathf.Abs(turnAction.axis.x) < snapTurnInputThreshold)
{
isSnapTurnInCooldown = false;
lastTurnTime = Time.time;
}
}
}
public class Patch : NomaiVRPatch
{
public override void ApplyPatches()
{
Postfix<OWInput>(nameof(OWInput.GetAxisValue), nameof(PostGetAxisValue));
Postfix<PlayerCharacterController>(nameof(PlayerCharacterController.UpdateTurning), nameof(PostCharacterTurning));
Postfix<JetpackThrusterController>(nameof(JetpackThrusterController.FixedUpdate), nameof(PostThrusterUpdate));
Prefix<OWCamera>("set_" + nameof(OWCamera.fieldOfView), nameof(PatchOwCameraFOV));
@ -136,11 +153,25 @@ namespace NomaiVR.Player
return;
}
if (ModSettings.SnapTurning && !PlayerState.InZeroG())
{
float turnInput = turnAction.axis.x;
// If snap turning, only do the snap turn, skip reorienting the play area
if (!isSnapTurnInCooldown && Mathf.Abs(turnInput) > snapTurnInputThreshold)
{
isSnapTurnInCooldown = true;
float sign = Mathf.Sign(turnInput);
Quaternion snapRotation = Quaternion.AngleAxis(GetSnapTurnIncrement() * sign, playerBody.transform.up);
var fromToSnap = Quaternion.FromToRotation(playerBody.transform.forward, snapRotation * playerBody.transform.forward);
rotationSetter(fromToSnap * playerBody.transform.rotation);
OnSnapTurn?.Invoke();
return;
}
}
var rotationSource = isControllerOriented ? LaserPointer.Behaviour.MovementLaser : playerCamera.transform;
var fromTo = Quaternion.FromToRotation(playerBody.transform.forward, Vector3.ProjectOnPlane(rotationSource.transform.forward, playerBody.transform.up));
var magnitude = 0f;
if (!isControllerOriented)
{
@ -154,6 +185,7 @@ namespace NomaiVR.Player
}
}
var fromTo = Quaternion.FromToRotation(playerBody.transform.forward, Vector3.ProjectOnPlane(rotationSource.transform.forward, playerBody.transform.up));
var targetRotation = fromTo * playerBody.transform.rotation;
var inverseRotation = Quaternion.Inverse(fromTo) * playArea.rotation;
@ -170,6 +202,20 @@ namespace NomaiVR.Player
}
}
// Override vanilla input handling for disabling turning while snap turning is enabled
private static void PostGetAxisValue(ref Vector2 __result, IInputCommands command, InputMode mask)
{
if (!ModSettings.SnapTurning || (OWInput.GetInputMode() != InputMode.Character))
{
return;
}
if (command.CommandType == InputConsts.InputCommandType.LOOK)
{
__result = Vector2.zero;
}
}
private static bool PatchOwCameraFOV(OWCamera __instance)
{
//Prevents changing the fov of VR cameras
@ -183,6 +229,25 @@ namespace NomaiVR.Player
if (__instance.mainCamera.stereoEnabled) __result = CameraHelper.GetScaledFieldOfView(__instance.mainCamera);
return !__instance.mainCamera.stereoEnabled;
}
private static float GetSnapTurnIncrement()
{
switch(ModSettings.SnapTurnIncrement)
{
case "15":
return 15f;
case "30":
return 30f;
case "45":
return 45f;
case "60":
return 60f;
case "90":
return 90f;
default:
return 45f;
}
}
}
}

View File

@ -1,4 +1,5 @@
using NomaiVR.ModConfig;
using NomaiVR.Player;
using UnityEngine;
namespace NomaiVR.UI
@ -8,19 +9,22 @@ namespace NomaiVR.UI
private Quaternion lastFrameRotation;
private const float speed = 0.5f;
private bool smoothEnabled = true;
private bool snapEnabled = false;
private bool snapTurnedLastFrame = false;
private void Start()
{
lastFrameRotation = transform.rotation;
SetSmoothEnabled();
RefreshEnabledSettings();
ModSettings.OnConfigChange += SetSmoothEnabled;
ModSettings.OnConfigChange += RefreshEnabledSettings;
}
private void OnDestroy()
{
ModSettings.OnConfigChange -= SetSmoothEnabled;
ModSettings.OnConfigChange -= RefreshEnabledSettings;
PlayerBodyPosition.Behaviour.OnSnapTurn -= OnSnapTurn;
}
private void LateUpdate()
@ -32,23 +36,41 @@ namespace NomaiVR.UI
var targetRotation = Camera.main.transform.rotation;
if (smoothEnabled)
if (!smoothEnabled)
{
transform.rotation = targetRotation;
}
else if (snapEnabled && snapTurnedLastFrame)
{
snapTurnedLastFrame = false;
transform.rotation = targetRotation;
}
else
{
var difference = Mathf.Abs(Quaternion.Angle(lastFrameRotation, targetRotation));
var step = speed * Time.unscaledDeltaTime * difference * difference;
transform.rotation = Quaternion.RotateTowards(lastFrameRotation, targetRotation, step);
}
else
{
transform.rotation = targetRotation;
}
lastFrameRotation = transform.rotation;
}
private void SetSmoothEnabled()
private void RefreshEnabledSettings()
{
smoothEnabled = ModSettings.HudSmoothFollow;
snapEnabled = ModSettings.SnapTurning;
PlayerBodyPosition.Behaviour.OnSnapTurn -= OnSnapTurn;
if (snapEnabled)
{
PlayerBodyPosition.Behaviour.OnSnapTurn += OnSnapTurn;
}
}
private void OnSnapTurn()
{
snapTurnedLastFrame = true;
}
}
}