From dbf4843410e1e8312139f5b4e135d993459c428c Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Fri, 23 Oct 2026 12:55:03 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20chest=20lids=20open=20by=20hand=20o?= =?UTF-8?q?n=20the=20Xri=20backend=20-=20grabbing=20the=20lid=20pulls=20it?= =?UTF-8?q?=20open=20around=20its=20hinge=20up=20to=20110=C2=B0,=20the=20c?= =?UTF-8?q?ontent=20spawns=20into=20the=20slots=20on=20the=20first=20open,?= =?UTF-8?q?=20closing=20it=20by=20hand=20hides=20the=20slots=20and=20reope?= =?UTF-8?q?ning=20shows=20the=20same=20items;=20locked=20chests=20stay=20s?= =?UTF-8?q?hut=20and=20unlocking=20by=20lock=20picking=20pushes=20the=20li?= =?UTF-8?q?d=20open=20a=20bit.=20The=20interim=20VrChestLidToggle=20(use?= =?UTF-8?q?=20=3D=20open/close)=20is=20replaced=20by=20VrHinge=20on=20oCMo?= =?UTF-8?q?bContainer.prefab's=20lid=20(+=20GothicGrabInteractable);=20IVr?= =?UTF-8?q?Hinge=20gets=20Opened/Closed=20events=20(VrHinge,=20and=20HvrHi?= =?UTF-8?q?ngeBridge=20from=20HVRPhysicsDoor's=20state),=20so=20VrChestSlo?= =?UTF-8?q?ts=20subscribes=20to=20the=20active=20hinge=20on=20both=20backe?= =?UTF-8?q?nds=20and=20HvrPhysicsChest=20no=20longer=20drives=20the=20slot?= =?UTF-8?q?s=20(HVR=20phase=204b).=20No=20player-visible=20change=20on=20t?= =?UTF-8?q?he=20default=20Hvr=20backend.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../VR/Prefabs/Vobs/oCMobContainer.prefab | 113 +++++++++--- .../Backends/Hvr/HvrHingeBridge.cs | 29 +++- .../Backends/Hvr/HvrPhysicsChest.cs | 54 +----- .../Interaction/Backends/Xri/XriBackend.cs | 4 +- .../Components/VrChestLidToggle.cs | 162 ------------------ .../Components/VrChestLidToggle.cs.meta | 2 - .../Interaction/Components/VrChestSlots.cs | 24 ++- .../Scripts/Interaction/Components/VrHinge.cs | 9 +- .../Scripts/Interaction/Ports/IVrHinge.cs | 11 ++ .../phase-4-doors-chests-ladders.md | 33 +++- 10 files changed, 186 insertions(+), 255 deletions(-) delete mode 100644 Assets/Gothic-VR/Scripts/Interaction/Components/VrChestLidToggle.cs delete mode 100644 Assets/Gothic-VR/Scripts/Interaction/Components/VrChestLidToggle.cs.meta 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/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 | From 6ed86a77fe9e109a16bb6eacd0dfafdabb40ce26 Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sun, 25 Oct 2026 08:50:33 +0100 Subject: [PATCH 2/2] fix: walking down won't kind of "hiccup" + Grab distance is reduced. Co-Authored-By: Claude Opus 5.5 --- .../VR/Prefabs/VR - Player XRI.prefab | 12 +- .../Interaction/Backends/Xri/XriRig.cs | 109 +++++++++++++++++- 2 files changed, 115 insertions(+), 6 deletions(-) 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/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).