diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/VR - Player XRI.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/VR - Player XRI.prefab
index 5a5f53d9e..f913f740b 100644
--- a/Assets/Gothic-VR/Resources/VR/Prefabs/VR - Player XRI.prefab
+++ b/Assets/Gothic-VR/Resources/VR/Prefabs/VR - Player XRI.prefab
@@ -1232,12 +1232,16 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Backends.Xri.XriTeleportAimer
_settings:
- StartThreshold: 0.75
- ReleaseThreshold: 0.4
ArcSpeed: 9
ArcGravity: 9.81
ArcStep: 0.4
ArcSamples: 40
+ ArcRadius: 0.1
+ MaxJumpDistance: 1.5
+ MaxDropDistance: 3
+ LineOfSightHeight: 1
+ LineOfSightSamples: 8
+ GroundRayHeight: 0.3
ReticleRadius: 0.25
ValidColor: {r: 0.3, g: 0.9, b: 0.4, a: 0.9}
InvalidColor: {r: 0.9, g: 0.3, b: 0.3, a: 0.6}
@@ -1963,7 +1967,7 @@ MonoBehaviour:
m_RaycastUIDocumentTriggerInteraction: 1
m_TargetNumCurveSegments: 1
m_HitDetectionType: 2
- m_CastDistance: 30
+ m_CastDistance: 10
m_SphereCastRadius: 0.1
m_ConeCastAngle: 6
m_LiveConeCastDebugVisuals: 0
@@ -2726,7 +2730,7 @@ MonoBehaviour:
m_RaycastUIDocumentTriggerInteraction: 1
m_TargetNumCurveSegments: 1
m_HitDetectionType: 2
- m_CastDistance: 30
+ m_CastDistance: 10
m_SphereCastRadius: 0.1
m_ConeCastAngle: 6
m_LiveConeCastDebugVisuals: 0
diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCMobContainer.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCMobContainer.prefab
index fcabb65ab..ebc94dc48 100644
--- a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCMobContainer.prefab
+++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCMobContainer.prefab
@@ -2220,9 +2220,9 @@ GameObject:
- component: {fileID: 172510743831141755}
- component: {fileID: 8269181727875358990}
- component: {fileID: 1469359342129423611}
- - component: {fileID: 6745565813121138959}
- component: {fileID: 7611355444731811914}
- - component: {fileID: 8346136834463750903}
+ - component: {fileID: 1630585632304796214}
+ - component: {fileID: 7588009683243012385}
m_Layer: 11
m_Name: BIP01 CHEST_.*_1
m_TagString: Untagged
@@ -2535,7 +2535,19 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 9fca5660b7d87044b9f6309422813386, type: 3}
m_Name:
m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Backends.Hvr.HvrHingeBridge
---- !u!114 &6745565813121138959
+--- !u!114 &7611355444731811914
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5980433150175133251}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d53defac19fbb474d81910e60e275fd7, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Backends.Xri.XriInteractableBridge
+--- !u!114 &1630585632304796214
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -2544,9 +2556,9 @@ MonoBehaviour:
m_GameObject: {fileID: 5980433150175133251}
m_Enabled: 0
m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 8a35f6cfbfba9b548aaa00d52cfe8a50, type: 3}
+ m_Script: {fileID: 11500000, guid: 6087a45bbe7ac8f469bf5110b3d64b49, type: 3}
m_Name:
- m_EditorClassIdentifier: Unity.XR.Interaction.Toolkit::UnityEngine.XR.Interaction.Toolkit.Interactables.XRSimpleInteractable
+ m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Backends.Xri.GothicGrabInteractable
m_InteractionManager: {fileID: 0}
m_Colliders: []
m_InteractionLayers:
@@ -2609,19 +2621,65 @@ MonoBehaviour:
m_StartingInteractionStrengthFilters: []
m_AutoFindParentInteractableInHierarchy: 0
m_ParentInteractableObject: {fileID: 0}
---- !u!114 &7611355444731811914
-MonoBehaviour:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 5980433150175133251}
- m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: d53defac19fbb474d81910e60e275fd7, type: 3}
- m_Name:
- m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Backends.Xri.XriInteractableBridge
---- !u!114 &8346136834463750903
+ m_AttachTransform: {fileID: 0}
+ m_SecondaryAttachTransform: {fileID: 0}
+ m_UseDynamicAttach: 0
+ m_MatchAttachPosition: 1
+ m_MatchAttachRotation: 0
+ m_SnapToColliderVolume: 1
+ m_ReinitializeDynamicAttachEverySingleGrab: 1
+ m_AttachEaseInTime: 0.15
+ m_MovementType: 1
+ m_PredictedVisualsTransform: {fileID: 0}
+ m_VelocityDamping: 1
+ m_VelocityScale: 1
+ m_AngularVelocityDamping: 1
+ m_AngularVelocityScale: 1
+ m_TrackPosition: 0
+ m_SmoothPosition: 0
+ m_SmoothPositionAmount: 8
+ m_TightenPosition: 0.1
+ m_TrackRotation: 0
+ m_SmoothRotation: 0
+ m_SmoothRotationAmount: 8
+ m_TightenRotation: 0.1
+ m_TrackScale: 1
+ m_SmoothScale: 0
+ m_SmoothScaleAmount: 8
+ m_TightenScale: 0.1
+ m_ThrowOnDetach: 0
+ m_ThrowSmoothingDuration: 0.25
+ m_ThrowSmoothingCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 1
+ value: 1
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0
+ outWeight: 0
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+ m_ThrowVelocityScale: 1.5
+ m_ThrowAngularVelocityScale: 1
+ m_ForceGravityOnDetach: 0
+ m_UnparentTransformOnGrab: 0
+ m_RetainTransformParent: 1
+ m_StartingSingleGrabTransformers: []
+ m_StartingMultipleGrabTransformers: []
+ m_AddDefaultGrabTransformers: 1
+ m_FarAttachMode: 0
+ m_LimitLinearVelocity: 0
+ m_LimitAngularVelocity: 0
+ m_MaxLinearVelocityDelta: 10
+ m_MaxAngularVelocityDelta: 20
+ _heldInterpolation: 1
+ _isOneHandOnly: 0
+--- !u!114 &7588009683243012385
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -2630,11 +2688,22 @@ MonoBehaviour:
m_GameObject: {fileID: 5980433150175133251}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 0ba712f1856779441b7ab647bc58f884, type: 3}
+ m_Script: {fileID: 11500000, guid: 5bb5c68765cb1e54cbe08cc0a5a15f05, type: 3}
m_Name:
- m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Components.VrChestLidToggle
- _openAngle: 110
- _swingTime: 0.4
+ m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Components.VrHinge
+ _localAxis: {x: 1, y: 0, z: 0}
+ _isOpeningInverted: 0
+ _minAngle: 0
+ _maxAngle: 110
+ _closeAngle: 5
+ _openHysteresis: 1
+ _closeOverTime: 0.25
+ _soundTimeout: 0.5
+ _maxHandSpeed: 360
+ _swingDamping: 3
+ _maxSwingSpeed: 180
+ _unlockPushSpeed: 60
+ _breakDistance: 1
--- !u!1 &6389233531767616918
GameObject:
m_ObjectHideFlags: 0
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrHingeBridge.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrHingeBridge.cs
index 94af17dbf..ae6cd86c5 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrHingeBridge.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrHingeBridge.cs
@@ -1,4 +1,5 @@
#if GOTHIC_HVR_INSTALLED
+using System;
using Gothic.VR.Interaction.Ports;
using HurricaneVR.Framework.Components;
using Reflex.Attributes;
@@ -8,12 +9,15 @@ namespace Gothic.VR.Interaction.Backends.Hvr
{
///
/// Sits next to an HVRPhysicsDoor (doors, and chest lids via HvrPhysicsChest). Switches the door off if another
- /// VR backend runs.
+ /// VR backend runs. Raises IVrHinge.Opened/Closed from HVRPhysicsDoor.Opened/Closed (polled in Update).
///
public class HvrHingeBridge : MonoBehaviour, IVrHinge
{
[Inject] private readonly IVrBackend _vrBackend;
+ // Flags to ensure we raise Opened/Closed once, every time the door's open/close status changes.
+ private bool _isOpenedRaised;
+ private bool _isClosedRaised;
private HVRPhysicsDoor _door;
private HVRPhysicsDoor Door
@@ -26,6 +30,9 @@ private HVRPhysicsDoor Door
}
}
+ public event Action Opened;
+ public event Action Closed;
+
public bool IsActive => _vrBackend == null || _vrBackend is HvrBackend;
public Transform Transform => transform;
public Rigidbody Rigidbody => GetComponent();
@@ -37,6 +44,26 @@ private void Awake()
Door.enabled = false;
}
+ private void Update()
+ {
+ if (!IsActive || Door == null)
+ return;
+
+ if (Door.Opened && !_isOpenedRaised)
+ {
+ _isOpenedRaised = true;
+ _isClosedRaised = false;
+ Opened?.Invoke();
+ }
+
+ if (Door.Closed && !_isClosedRaised)
+ {
+ _isClosedRaised = true;
+ _isOpenedRaised = false;
+ Closed?.Invoke();
+ }
+ }
+
public void Lock()
{
Door.Lock();
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrPhysicsChest.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrPhysicsChest.cs
index ab6becfa1..91cef797a 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrPhysicsChest.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrPhysicsChest.cs
@@ -1,30 +1,19 @@
#if GOTHIC_HVR_INSTALLED
-using Gothic.Core.Logging;
-using Gothic.VR.Interaction.Components;
using Gothic.VR.Interaction.Ports;
using HurricaneVR.Framework.Components;
using Reflex.Attributes;
-using Logger = Gothic.Core.Logging.Logger;
namespace Gothic.VR.Interaction.Backends.Hvr
{
///
- /// Chest lid (HVR physics door). Opening it shows the chest's slots (VrChestSlots, which spawns the content on
- /// the first open), closing it hides them.
- /// Temporarily in the HVR backend as it subclasses HVRPhysicsDoor (VrHinge in Phase 4). On other backends it
- /// switches itself off before the door is set up (VrChestLidToggle is the lid there).
+ /// Chest lid (HVR physics door). Opening/closing is reported by HvrHingeBridge (IVrHinge.Opened/Closed), which
+ /// VrChestSlots listens to. Subclasses HVRPhysicsDoor only so that it can switch itself off before the door is
+ /// set up if another VR backend runs (VrHinge is the lid there).
///
public class HvrPhysicsChest : HVRPhysicsDoor
{
[Inject] private readonly IVrBackend _vrBackend;
- private VrChestSlots _chestSlots;
-
- // Flags to ensure we always call On*() once, everytime open/close status changes.
- private bool _onOpenedHandled;
- private bool _onClosedHandled;
-
-
public override void Start()
{
if (_vrBackend != null && _vrBackend is not HvrBackend)
@@ -34,43 +23,6 @@ public override void Start()
}
base.Start();
-
- _chestSlots = GetComponentInParent();
- if (_chestSlots == null)
- Logger.LogError($"[HVR] Chest lid {name} has no VrChestSlots in its parents.", LogCat.VR);
- }
-
- protected override void Update()
- {
- base.Update();
-
- if (Opened && !_onOpenedHandled)
- {
- OnOpened();
- }
-
- if (Closed && !_onClosedHandled)
- {
- OnClosed();
- }
- }
-
- private void OnOpened()
- {
- if (_chestSlots != null)
- _chestSlots.Open();
-
- _onOpenedHandled = true;
- _onClosedHandled = false;
- }
-
- private void OnClosed()
- {
- if (_chestSlots != null)
- _chestSlots.Close();
-
- _onOpenedHandled = false;
- _onClosedHandled = true;
}
}
}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriBackend.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriBackend.cs
index dde1666ea..7cc73bc11 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriBackend.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriBackend.cs
@@ -13,8 +13,8 @@ namespace Gothic.VR.Interaction.Backends.Xri
/// per hand, GothicGrabInteractable + XriInteractableBridge on oCItem), using mobs/NPCs and the socket core
/// (VrSocketSlot + XriSocketBridge, "VR Socket Slot.prefab"), the backpack ("BackPack XRI.prefab") on the
/// shoulder and the hip/shoulder holsters (VrBodyAnchor "Shoulders"/"Waist" on the rig), chest slots
- /// (VrChestSlots, lid opened by using it: VrChestLidToggle) and NPC loot slots.
- /// Missing: physical doors/chest lids/ladders (4) and hands (5).
+ /// (VrChestSlots, lid opened by hand: VrHinge), NPC loot slots and doors (VrHinge).
+ /// Missing: ladders (4c) and hands (5).
///
public class XriBackend : IVrBackend
{
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriRig.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriRig.cs
index eaa28799c..3cc7d522e 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriRig.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriRig.cs
@@ -75,6 +75,17 @@ public class XriRig : MonoBehaviour, IVrRig
[Tooltip("Terminal fall speed in m/s.")]
[SerializeField] private float _maxFallSpeed = 20f;
+ [Tooltip("Keeps the player on the ground when walking downhill or down stairs (like HVR's constant downward push). "
+ + "XRI's grounded check alone flickers between ground and air there.")]
+ [SerializeField] private bool _snapToGround = true;
+
+ [Tooltip("Max. drop in m per frame the player is pulled down to the ground (slopes, stairs). Larger drops "
+ + "(ledges) are real falls. Capped by the CharacterController's step offset.")]
+ [SerializeField] private float _groundSnapDistance = 0.3f;
+
+ [Tooltip("Time in s the player still counts as grounded after losing ground contact. Hides one-frame flickers.")]
+ [SerializeField] private float _groundedGraceTime = 0.12f;
+
[Header("Height")]
[Tooltip("Eye height of the virtual player in m. Used by the Sitting and Standing comfort settings.")]
[SerializeField] private float _eyeHeight = 1.66f;
@@ -99,8 +110,13 @@ public class XriRig : MonoBehaviour, IVrRig
private bool _isSprintLatched;
private bool _isCrouching;
private float _crouchOffset;
+ private float _lastGroundedTime = float.NegativeInfinity;
private bool _isTeleportQueued;
+ private int _lastTeleportFrame = -1;
private Vector3 _teleportPosition;
+ private XRBodyTransformer _bodyTransformer;
+ private int _collidingLayers;
+ private bool _isJumpAirborne;
public event Action Teleported;
@@ -112,7 +128,17 @@ public class XriRig : MonoBehaviour, IVrRig
public Camera Camera => _camera;
public CharacterController CharacterController => _characterController;
public float CameraHeight => _origin.CameraInOriginSpaceHeight;
- public bool IsGrounded => _gravityProvider.isGrounded || _characterController.isGrounded;
+
+ public bool IsGrounded
+ {
+ get
+ {
+ if (_gravityProvider.isGrounded || _characterController.isGrounded)
+ _lastGroundedTime = Time.time;
+
+ return Time.time - _lastGroundedTime <= _groundedGraceTime;
+ }
+ }
// Climbing comes with HVR phase 4 (ClimbProvider).
public bool IsClimbing => false;
@@ -233,7 +259,8 @@ public IReadOnlyList HandGrabbers
private void Awake()
{
// Grounded check against everything the player collides with (world, VOBs, NPCs), not only Default.
- _gravityProvider.sphereCastLayerMask = GetCollidingLayers(gameObject.layer);
+ _collidingLayers = GetCollidingLayers(gameObject.layer);
+ _gravityProvider.sphereCastLayerMask = _collidingLayers;
_teleportationProvider.locomotionEnded += OnTeleportLocomotionEnded;
@@ -249,12 +276,25 @@ private void Start()
LogCat.VR);
else
CreateTeleportAimer();
+
+ // Snapping has to run right after XRI moved the body this frame (see SnapToGround()).
+ _bodyTransformer = _moveProvider.mediator != null ? _moveProvider.mediator.bodyTransformer : null;
+ if (_bodyTransformer == null)
+ _bodyTransformer = GetComponentInChildren();
+
+ if (_bodyTransformer != null)
+ _bodyTransformer.afterApplyTransformations += OnAfterApplyTransformations;
+ else
+ Logger.LogError("[XRI] No XRBodyTransformer found. Walking downhill will bounce.", LogCat.VR);
}
private void OnDestroy()
{
if (_teleportationProvider != null)
_teleportationProvider.locomotionEnded -= OnTeleportLocomotionEnded;
+
+ if (_bodyTransformer != null)
+ _bodyTransformer.afterApplyTransformations -= OnAfterApplyTransformations;
}
private void Update()
@@ -263,6 +303,70 @@ private void Update()
UpdateCrouch();
}
+ private void OnAfterApplyTransformations(ApplyBodyTransformationsEventArgs args)
+ {
+ SnapToGround();
+ }
+
+ ///
+ /// XRI moves horizontally only and GravityProvider pauses gravity while its head-down sphere cast still finds
+ /// ground (up to ~4 cm below the feet). Walking downhill, the player therefore floats until that gap is
+ /// exceeded, then gravity restarts from 0 and drops them a bit: the bouncing. Like HVR (constant downward push),
+ /// we press the CharacterController onto the ground below it every frame after XRI's move (same frame,
+ /// before rendering and before the next grounded check). Limited to the snap distance, so ledges and jumps
+ /// stay real falls.
+ ///
+ private void SnapToGround()
+ {
+ if (!_snapToGround || _gravity <= 0f || _moveProvider.enableFly || !_characterController.enabled)
+ return;
+
+ // Teleports move the transform directly. A CharacterController.Move before the sync would undo them.
+ if (_isTeleportQueued || _lastTeleportFrame == Time.frameCount)
+ return;
+
+ // From the jump until landing, the player is in the air on purpose.
+ if (_jumpProvider.isJumping)
+ {
+ _isJumpAirborne = true;
+ return;
+ }
+
+ if (_isJumpAirborne)
+ {
+ if (!_gravityProvider.isGrounded)
+ return;
+
+ _isJumpAirborne = false;
+ }
+
+ // Only snap if we were on the ground a moment ago. Otherwise we're falling for real.
+ if (!IsGrounded)
+ return;
+
+ var up = Vector3.up;
+ var skinWidth = _characterController.skinWidth;
+ var radius = _characterController.radius * 0.5f;
+ var bottom = _characterController.transform.TransformPoint(_characterController.center)
+ - up * (_characterController.height * 0.5f);
+ var origin = bottom + up * (radius + skinWidth);
+ var maxDistance = Mathf.Min(_groundSnapDistance, _characterController.stepOffset) + 2f * skinWidth;
+
+ if (!Physics.SphereCast(origin, radius, -up, out var hit, maxDistance, _collidingLayers,
+ QueryTriggerInteraction.Ignore))
+ return;
+
+ // Too steep to stand on: let gravity slide the player down.
+ if (Vector3.Angle(hit.normal, up) > _characterController.slopeLimit)
+ return;
+
+ // CharacterController.Move stops at the collision, overshooting by the skin width is safe and makes
+ // CharacterController.isGrounded true.
+ var drop = hit.distance - skinWidth;
+ if (drop > 0f)
+ _characterController.Move(-up * drop);
+ }
+
///
/// Right thumbstick forward aims, releasing teleports, grip cancels.
///
@@ -441,6 +545,7 @@ private void OnTeleportLocomotionEnded(LocomotionProvider provider)
return;
_isTeleportQueued = false;
+ _lastTeleportFrame = Time.frameCount;
// XRBodyGroundPosition moves the transform directly. Without the sync, the CharacterController would
// continue from its old position (Physics.autoSyncTransforms is off).
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrChestLidToggle.cs b/Assets/Gothic-VR/Scripts/Interaction/Components/VrChestLidToggle.cs
deleted file mode 100644
index 5a0e0dfbf..000000000
--- a/Assets/Gothic-VR/Scripts/Interaction/Components/VrChestLidToggle.cs
+++ /dev/null
@@ -1,162 +0,0 @@
-using System.Collections;
-using Gothic.Core.Logging;
-using Gothic.Core.Models.Context;
-using Gothic.VR.Interaction.Models;
-using Gothic.VR.Interaction.Ports;
-using Reflex.Attributes;
-using UnityEngine;
-using Logger = Gothic.Core.Logging.Logger;
-
-namespace Gothic.VR.Interaction.Components
-{
- ///
- /// Interim chest lid for the XRI backend until Phase 4 (VrHinge): using the lid (select-only grab, like a mob)
- /// swings it open and shows the chest's slots (VrChestSlots), using it again closes it. The lid is kinematic,
- /// there is no physical pushing/pulling yet. Respects Lock()/Unlock() of the lock picking and plays the
- /// open/close sounds of VRVobContainer.
- /// Inactive on HVR, where HvrPhysicsChest (HVR physics door) is the lid.
- ///
- public class VrChestLidToggle : MonoBehaviour, IVrHinge
- {
- [Tooltip("Rotation around the lid's local X axis when open (degrees).")]
- [SerializeField] private float _openAngle = 110f;
-
- [Tooltip("Seconds the lid needs to swing open or closed.")]
- [SerializeField] private float _swingTime = 0.4f;
-
- [Inject] private readonly IVrBackend _vrBackend;
- [Inject] private readonly IVrSfxPlayer _sfxPlayer;
-
- private VrInteractable _interactable;
- private VrChestSlots _chestSlots;
- private Rigidbody _rigidbody;
- private Quaternion _closedRotation;
- private AudioClip _openedClip;
- private AudioClip _closedClip;
- private Coroutine _swingRoutine;
- private bool _isSubscribed;
- private bool _isLocked;
- private bool _isOpen;
-
- public bool IsActive => _vrBackend != null && _vrBackend.Kind == VrInteractionBackend.Xri;
- public bool IsOpen => _isOpen;
- public bool IsLocked => _isLocked;
- public Transform Transform => transform;
- public Rigidbody Rigidbody => GetComponent();
-
- private void Start()
- {
- if (!IsActive)
- {
- enabled = false;
- return;
- }
-
- // No joint holds the lid (HvrPhysicsChest doesn't set up its door on XRI).
- _rigidbody = GetComponent();
- if (_rigidbody != null)
- _rigidbody.isKinematic = true;
-
- _closedRotation = transform.localRotation;
- _chestSlots = GetComponentInParent();
- _interactable = GetComponent();
- if (_interactable == null || _chestSlots == null)
- {
- Logger.LogError($"[Chest] {name} needs a VrInteractable and a VrChestSlots in its parents.",
- LogCat.VR);
- return;
- }
-
- _interactable.Grabbed += OnGrabbed;
- _isSubscribed = true;
- }
-
- private void OnDestroy()
- {
- if (_isSubscribed && _interactable != null)
- _interactable.Grabbed -= OnGrabbed;
- }
-
- public void Lock()
- {
- _isLocked = true;
- }
-
- public void Unlock()
- {
- _isLocked = false;
- }
-
- public void SetSounds(AudioClip opened, AudioClip closed)
- {
- _openedClip = opened;
- _closedClip = closed;
- }
-
- public void Toggle()
- {
- if (_isOpen)
- Close();
- else
- Open();
- }
-
- public void Open()
- {
- if (_isOpen || !isActiveAndEnabled)
- return;
-
- if (_isLocked)
- {
- Logger.Log($"[Chest] {name} is locked.", LogCat.VR);
- return;
- }
-
- _isOpen = true;
- Swing(_closedRotation * Quaternion.Euler(_openAngle, 0, 0), _openedClip);
- _chestSlots.Open();
- }
-
- public void Close()
- {
- if (!_isOpen || !isActiveAndEnabled)
- return;
-
- _isOpen = false;
- _chestSlots.Close();
- Swing(_closedRotation, _closedClip);
- }
-
- private void OnGrabbed(VrGrabArgs args)
- {
- // Hands "use" the lid (XRSimpleInteractable). Sockets never hold it.
- if (!args.IsHandOrDistance)
- return;
-
- Toggle();
- }
-
- private void Swing(Quaternion target, AudioClip clipAtEnd)
- {
- if (_swingRoutine != null)
- StopCoroutine(_swingRoutine);
- _swingRoutine = StartCoroutine(SwingTo(target, clipAtEnd));
- }
-
- private IEnumerator SwingTo(Quaternion target, AudioClip clipAtEnd)
- {
- var start = transform.localRotation;
- for (var time = 0f; time < _swingTime; time += Time.deltaTime)
- {
- transform.localRotation = Quaternion.Slerp(start, target, time / _swingTime);
- yield return null;
- }
-
- transform.localRotation = target;
- _swingRoutine = null;
-
- if (clipAtEnd != null && _sfxPlayer != null)
- _sfxPlayer.Play(clipAtEnd, transform.position);
- }
- }
-}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrChestLidToggle.cs.meta b/Assets/Gothic-VR/Scripts/Interaction/Components/VrChestLidToggle.cs.meta
deleted file mode 100644
index dfa3b4965..000000000
--- a/Assets/Gothic-VR/Scripts/Interaction/Components/VrChestLidToggle.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 0ba712f1856779441b7ab647bc58f884
\ No newline at end of file
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrChestSlots.cs b/Assets/Gothic-VR/Scripts/Interaction/Components/VrChestSlots.cs
index 3a5bd6c55..5426e4c53 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Components/VrChestSlots.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrChestSlots.cs
@@ -17,8 +17,8 @@
namespace Gothic.VR.Interaction.Components
{
///
- /// Content of a chest (oCMobContainer), independent of the lid: the lid (HvrPhysicsChest on HVR,
- /// VrChestLidToggle on XRI until VrHinge in Phase 4) calls Open()/Close().
+ /// Content of a chest (oCMobContainer), independent of the lid: the lid (IVrHinge of the active
+ /// backend: HvrHingeBridge or VrHinge) opens and closes it by its Opened/Closed events.
/// - The prefab carries one slot inventory (IVrSocketContainer) per VR backend. The active one is used, the
/// others stay hidden.
/// - First open: the slots are laid out to the size of the chest mesh and the content (IContainer.Contents) is
@@ -48,6 +48,7 @@ public class VrChestSlots : MonoBehaviour
private bool _isResolved;
private bool _isContentSpawned;
private bool _isSpawning;
+ private IVrHinge _hinge;
private bool _isOpen;
public bool IsOpen => _isOpen;
@@ -69,6 +70,25 @@ private void Start()
Resolve();
if (!_isOpen && _inventoryGo != null)
_inventoryGo.SetActive(false);
+
+ _hinge = VrHinges.FindActive(this);
+ if (_hinge == null)
+ {
+ Logger.LogWarning($"[Chest] {name} has no active lid hinge. It can't be opened.", LogCat.VR);
+ return;
+ }
+
+ _hinge.Opened += Open;
+ _hinge.Closed += Close;
+ }
+
+ private void OnDestroy()
+ {
+ if (_hinge == null)
+ return;
+
+ _hinge.Opened -= Open;
+ _hinge.Closed -= Close;
}
public void Open()
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrHinge.cs b/Assets/Gothic-VR/Scripts/Interaction/Components/VrHinge.cs
index cecd5cadf..6bd12ff20 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Components/VrHinge.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrHinge.cs
@@ -11,7 +11,7 @@
namespace Gothic.VR.Interaction.Components
{
///
- /// Backend-neutral hinge for doors (and later chest lids). The leaf is kinematic and rotated by our own angle
+ /// Backend-neutral hinge for doors and chest lids. The leaf is kinematic and rotated by our own angle
/// solver (no joint: velocity tracking against a HingeJoint/ConfigurableJoint whipped and jammed the leaf):
/// - While a hand holds the leaf, the hand's angular movement around the hinge axis (projected onto the hinge
/// plane) turns the leaf by the same angle, clamped to Min/MaxAngle.
@@ -97,14 +97,7 @@ public class VrHinge : MonoBehaviour, IVrHinge
private bool _isOpen;
private bool _isClosed = true;
- ///
- /// Raised once when the leaf leaves the closed position.
- ///
public event Action Opened;
-
- ///
- /// Raised once when the leaf is back in the closed position.
- ///
public event Action Closed;
public bool IsActive => _vrBackend != null && _vrBackend.Kind == VrInteractionBackend.Xri;
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrHinge.cs b/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrHinge.cs
index 100e8500f..9416030bf 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrHinge.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrHinge.cs
@@ -1,3 +1,4 @@
+using System;
using UnityEngine;
namespace Gothic.VR.Interaction.Ports
@@ -13,6 +14,16 @@ public interface IVrHinge
///
bool IsActive { get; }
+ ///
+ /// Raised once when the hinge leaves the closed position.
+ ///
+ event Action Opened;
+
+ ///
+ /// Raised once when the hinge is back in the closed position.
+ ///
+ event Action Closed;
+
Transform Transform { get; }
Rigidbody Rigidbody { get; }
diff --git a/Docs/HVR-replacement/phase-4-doors-chests-ladders.md b/Docs/HVR-replacement/phase-4-doors-chests-ladders.md
index 283486563..6cb016b22 100644
--- a/Docs/HVR-replacement/phase-4-doors-chests-ladders.md
+++ b/Docs/HVR-replacement/phase-4-doors-chests-ladders.md
@@ -14,7 +14,7 @@ on `VrHinge` (replaces the interim `VrChestLidToggle`), **4c** ladders + `KeepPl
change with a 0.5 s timeout.)
- [x] Locked doors don't move; lock picking unlocks them (`VRContainerDoorPickingInteraction`). (4a: `Lock()` holds the
leaf at 0° and a hand can't pull it open; the picking script is unchanged. Unlock + push: see status below.)
-- [ ] Chest lids open 0-110°. The first open spawns the content (Phase 3); closing hides the slots; lock picking works.
+- [x] Chest lids open 0-110°. The first open spawns the content (Phase 3); closing hides the slots; lock picking works.
- [ ] Ladders can be climbed hand over hand. `IVrRig.IsClimbing` is true while climbing (used by `KeepPlayerWithin`).
## Behaviour spec (what we use from `HVRPhysicsDoor` today)
@@ -81,15 +81,17 @@ movement onto the hinge plane, clamp to the limits). (4a: this is what `VrHinge`
## Work items
- [x] `VrHinge` (+ `IVrHinge` implementation). (4a: `Interaction/Components/VrHinge.cs`, kinematic angle solver, no
- joint. `Opened`/`Closed` C# events are on `VrHinge`, not on `IVrHinge` yet (4b adds them with the chest).)
+ joint. 4b moved `Opened`/`Closed` onto `IVrHinge`; `HvrHingeBridge` raises them from `HVRPhysicsDoor.Opened/Closed`.)
- [ ] `VrLadderExit` (4c).
-- [ ] `VRVobContainerPhysicsChest` → `VrChestSlots` (logic) + `VrHinge` (lid); delete the subclassing on the Xri path.
+- [x] `VRVobContainerPhysicsChest` → `VrChestSlots` (logic) + `VrHinge` (lid); delete the subclassing on the Xri path.
(3c: `VrChestSlots` exists and has `Open()`/`Close()`; `HvrPhysicsChest` is only the HVR lid. On Xri the lid is
- the interim `VrChestLidToggle` (use = swing open/closed, kinematic, `IVrHinge`) - replace it with `VrHinge`.)
+ the interim `VrChestLidToggle` (use = swing open/closed, kinematic, `IVrHinge`) - replace it with `VrHinge`.
+ 4b done: `VrChestLidToggle` deleted, the lid has `VrHinge`; `VrChestSlots` subscribes to the active `IVrHinge`
+ (`VrHinges.FindActive`), `HvrPhysicsChest` is only the "switch off on other backends" shell.)
- [ ] **Prefabs (UnityMCP):** `oCMobDoor.prefab` (VrHinge + grab interactable + bridge) (4a done: the VR prefab's door
leaf got `VrHinge`, `GothicGrabInteractable` (disabled, Kinematic movement, tracking off, no unparenting) and
`XriInteractableBridge`; `HvrHingeBridge` now switches `HVRPhysicsDoor` off on other
- backends), `oCMobContainer.prefab` (lid, 4b), `oCMobLadder.prefab` (ClimbInteractable + bridge, 4c).
+ backends), `oCMobContainer.prefab` (lid, 4b: done, see below), `oCMobLadder.prefab` (ClimbInteractable + bridge, 4c).
- [ ] `VRContextInteractionService.KeepPlayerWithin` uses `IVrRig.IsClimbing`; verify with a dialog at a ladder.
## Test plan
@@ -132,6 +134,27 @@ feel):
- Known differences to HVR: the leaf has no physical collision with hands (Xri hands aren't colliders), so touching
a door without grabbing doesn't move it, and the player's body doesn't push it.
+## Status after 4b
+
+Xri + simulator, scripted hand (`TrackedPoseDriver` of the controller disabled, hand moved along the lid's arc,
+`SelectEnter`/`SelectExit`), G1 small crate near the start (51, 60, 287), editor at a few fps:
+- **Prefab:** the lid (`BIP01 CHEST_*_1`) lost `XRSimpleInteractable` and `VrChestLidToggle`; it got `GothicGrabInteractable`
+ (disabled, copy of the door's settings) and `VrHinge` (axis X, 0-110°). `XriInteractableBridge` was already there.
+- **Events on the port:** `IVrHinge.Opened/Closed`. `HvrHingeBridge.Update` raises them from `HVRPhysicsDoor.Opened/Closed`
+ (same flags as `HvrPhysicsChest` had), so `VrChestSlots` is backend-neutral (`Start` subscribes to the active hinge).
+ `HvrPhysicsChest` no longer calls the slots itself (the Hvr chest was not played again: only the event path moved).
+- Hand pulls the lid 0 → 115°: the lid follows 1:1 (0.2° behind), clamps at 110°; content (5 items) appears on the first
+ `Opened`; released at 110° it stays (no bounce).
+- Took one item by hand (4 left), closed by hand 110 → 0°: `Closed` once, slots hidden; opened again: the same 4 items,
+ nothing spawned twice.
+- `Lock()`: the lid snaps to 0° and the slots hide, a hand pulling to 60° does not move it; `Unlock()` pushes it open to
+ ~14° after a moment and opens the slots.
+- Not tested: a real pick-lock sequence by hand, the open/close sounds audibly (set by `VRVobContainer` via
+ `VrHinges.FindActive`, played by `VrHinge` from the state change), a headset, the Hvr chest (HVR physics) after the
+ event move, a lid hanging open when the chest is re-created by save/load.
+- Known differences to the interim toggle: the lid has to be pulled by hand (use = grab no longer opens it by a click);
+ the lid has no gravity (stays where it's let go, closes softly only below 5°).
+
## Risks
| Risk | Mitigation |