diff --git a/Assets/Gothic-VR/Resources/VR/Input/GothicVR.inputactions b/Assets/Gothic-VR/Resources/VR/Input/GothicVR.inputactions index c486c0e29..61b12c8fb 100644 --- a/Assets/Gothic-VR/Resources/VR/Input/GothicVR.inputactions +++ b/Assets/Gothic-VR/Resources/VR/Input/GothicVR.inputactions @@ -3143,7 +3143,7 @@ "name": "", "id": "62690862-4688-4010-975b-b3d9c6062157", "path": "{RightHand}/{Primary2DAxis}", - "interactions": "Sector(directions=1)", + "interactions": "Sector(directions=2)", "processors": "", "groups": "", "action": "Teleport Mode", 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 58ac8c5b0..5a5f53d9e 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/VR - Player XRI.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/VR - Player XRI.prefab @@ -416,6 +416,146 @@ MeshRenderer: m_SortingOrder: 0 m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} +--- !u!1 &2970714513379137289 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2879737534919961666} + - component: {fileID: 3947543953460428864} + m_Layer: 0 + m_Name: Arc + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2879737534919961666 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2970714513379137289} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8750008290288450499} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!120 &3947543953460428864 +LineRenderer: + serializedVersion: 3 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2970714513379137289} + m_Enabled: 0 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 0 + m_LightProbeUsage: 0 + m_ReflectionProbeUsage: 0 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 10306, guid: 0000000000000000f000000000000000, type: 0} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Positions: [] + m_Parameters: + serializedVersion: 3 + widthMultiplier: 0.015 + widthCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + colorGradient: + serializedVersion: 2 + key0: {r: 1, g: 1, b: 1, a: 1} + key1: {r: 1, g: 1, b: 1, a: 1} + key2: {r: 0, g: 0, b: 0, a: 0} + key3: {r: 0, g: 0, b: 0, a: 0} + key4: {r: 0, g: 0, b: 0, a: 0} + key5: {r: 0, g: 0, b: 0, a: 0} + key6: {r: 0, g: 0, b: 0, a: 0} + key7: {r: 0, g: 0, b: 0, a: 0} + ctime0: 0 + ctime1: 65535 + ctime2: 0 + ctime3: 0 + ctime4: 0 + ctime5: 0 + ctime6: 0 + ctime7: 0 + atime0: 0 + atime1: 65535 + atime2: 0 + atime3: 0 + atime4: 0 + atime5: 0 + atime6: 0 + atime7: 0 + m_Mode: 0 + m_ColorSpace: -1 + m_NumColorKeys: 2 + m_NumAlphaKeys: 2 + numCornerVertices: 0 + numCapVertices: 2 + alignment: 0 + textureMode: 0 + textureScale: {x: 1, y: 1} + shadowBias: 0.5 + generateLightingData: 0 + m_UseWorldSpace: 1 + m_Loop: 0 + m_ApplyActiveColorSpace: 1 --- !u!1 &3580888346999583535 GameObject: m_ObjectHideFlags: 0 @@ -1045,6 +1185,64 @@ MonoBehaviour: _fadeSpeed: 6 _teleportDistance: 1 _distanceIterations: 5 +--- !u!1 &4786995511277724993 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8750008290288450499} + - component: {fileID: 8981795712984714} + m_Layer: 0 + m_Name: Teleport + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8750008290288450499 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4786995511277724993} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2879737534919961666} + - {fileID: 2958122511468957170} + m_Father: {fileID: 1361426706454156200} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &8981795712984714 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4786995511277724993} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7f622519f5c2fe64a93dfdb569a97b2d, type: 3} + 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 + 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} + _arc: {fileID: 3947543953460428864} + _reticle: {fileID: 7864087409581978865} --- !u!1 &5458298503362937332 GameObject: m_ObjectHideFlags: 0 @@ -1994,6 +2192,146 @@ MonoBehaviour: _pitchThreshold: 30 _catchUpAngle: 70 _catchUpSpeed: 90 +--- !u!1 &5943813740192046332 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2958122511468957170} + - component: {fileID: 7864087409581978865} + m_Layer: 0 + m_Name: Reticle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2958122511468957170 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5943813740192046332} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8750008290288450499} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!120 &7864087409581978865 +LineRenderer: + serializedVersion: 3 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5943813740192046332} + m_Enabled: 0 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 0 + m_LightProbeUsage: 0 + m_ReflectionProbeUsage: 0 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 10306, guid: 0000000000000000f000000000000000, type: 0} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Positions: [] + m_Parameters: + serializedVersion: 3 + widthMultiplier: 0.015 + widthCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + colorGradient: + serializedVersion: 2 + key0: {r: 1, g: 1, b: 1, a: 1} + key1: {r: 1, g: 1, b: 1, a: 1} + key2: {r: 0, g: 0, b: 0, a: 0} + key3: {r: 0, g: 0, b: 0, a: 0} + key4: {r: 0, g: 0, b: 0, a: 0} + key5: {r: 0, g: 0, b: 0, a: 0} + key6: {r: 0, g: 0, b: 0, a: 0} + key7: {r: 0, g: 0, b: 0, a: 0} + ctime0: 0 + ctime1: 65535 + ctime2: 0 + ctime3: 0 + ctime4: 0 + ctime5: 0 + ctime6: 0 + ctime7: 0 + atime0: 0 + atime1: 65535 + atime2: 0 + atime3: 0 + atime4: 0 + atime5: 0 + atime6: 0 + atime7: 0 + m_Mode: 0 + m_ColorSpace: -1 + m_NumColorKeys: 2 + m_NumAlphaKeys: 2 + numCornerVertices: 0 + numCapVertices: 2 + alignment: 0 + textureMode: 0 + textureScale: {x: 1, y: 1} + shadowBias: 0.5 + generateLightingData: 0 + m_UseWorldSpace: 1 + m_Loop: 1 + m_ApplyActiveColorSpace: 1 --- !u!1 &6648638844185410883 GameObject: m_ObjectHideFlags: 0 @@ -2963,6 +3301,7 @@ Transform: - {fileID: 5689876404678866823} - {fileID: 1769492897988253260} - {fileID: 229440269703855637} + - {fileID: 8750008290288450499} m_Father: {fileID: 3866233120056885002} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!143 &3989580456932340501 @@ -3277,6 +3616,7 @@ MonoBehaviour: _teleportationProvider: {fileID: 5987075695519224435} _gravityProvider: {fileID: 1435405315088257603} _jumpProvider: {fileID: 5504711397442723850} + _teleportAimer: {fileID: 8981795712984714} _moveSpeed: 1.5 _runSpeed: 3.5 _sprintStopThreshold: 0.1 @@ -4777,11 +5117,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 5582560689641485545, guid: 8348fe5825a421c42be0dad392e03669, type: 3} propertyPath: m_AnchoredPosition.x - value: -366.49997 + value: 0.00015258789 objectReference: {fileID: 0} - target: {fileID: 5582560689641485545, guid: 8348fe5825a421c42be0dad392e03669, type: 3} propertyPath: m_AnchoredPosition.y - value: 166.49922 + value: -0.015060425 objectReference: {fileID: 0} - target: {fileID: 5603077536188825030, guid: 8348fe5825a421c42be0dad392e03669, type: 3} propertyPath: m_AnchorMax.x @@ -4797,11 +5137,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: 6272730690859954775, guid: 8348fe5825a421c42be0dad392e03669, type: 3} propertyPath: m_Size - value: 0.9514283 + value: 0.9514286 objectReference: {fileID: 0} - target: {fileID: 6272730690859954775, guid: 8348fe5825a421c42be0dad392e03669, type: 3} propertyPath: m_Value - value: 1.000043 + value: 10.795005 objectReference: {fileID: 0} - target: {fileID: 8278234239060405203, guid: 8348fe5825a421c42be0dad392e03669, type: 3} propertyPath: m_AnchorMax.x diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCMobDoor.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCMobDoor.prefab index 8d82277d7..6d1dcdb93 100644 --- a/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCMobDoor.prefab +++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Vobs/oCMobDoor.prefab @@ -18,6 +18,9 @@ GameObject: - component: {fileID: 1447025355381534221} - component: {fileID: 7842253579892008169} - component: {fileID: 87455771706928418} + - component: {fileID: 221434347929742982} + - component: {fileID: 786056569404290700} + - component: {fileID: 7188846223050807764} m_Layer: 6 m_Name: BIP01 DOORANGLE m_TagString: Untagged @@ -330,6 +333,175 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 9fca5660b7d87044b9f6309422813386, type: 3} m_Name: m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Backends.Hvr.HvrHingeBridge +--- !u!114 &221434347929742982 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 980716176992391296} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6087a45bbe7ac8f469bf5110b3d64b49, type: 3} + m_Name: + m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Backends.Xri.GothicGrabInteractable + m_InteractionManager: {fileID: 0} + m_Colliders: [] + m_InteractionLayers: + m_Bits: 1 + m_DistanceCalculationMode: 1 + m_SelectMode: 0 + m_FocusMode: 1 + m_CustomReticle: {fileID: 0} + m_AllowGazeInteraction: 0 + m_AllowGazeSelect: 0 + m_OverrideGazeTimeToSelect: 0 + m_GazeTimeToSelect: 0.5 + m_OverrideTimeToAutoDeselectGaze: 0 + m_TimeToAutoDeselectGaze: 3 + m_AllowGazeAssistance: 0 + m_FirstHoverEntered: + m_PersistentCalls: + m_Calls: [] + m_LastHoverExited: + m_PersistentCalls: + m_Calls: [] + m_HoverEntered: + m_PersistentCalls: + m_Calls: [] + m_HoverExited: + m_PersistentCalls: + m_Calls: [] + m_FirstSelectEntered: + m_PersistentCalls: + m_Calls: [] + m_LastSelectExited: + m_PersistentCalls: + m_Calls: [] + m_SelectEntered: + m_PersistentCalls: + m_Calls: [] + m_SelectExited: + m_PersistentCalls: + m_Calls: [] + m_FirstFocusEntered: + m_PersistentCalls: + m_Calls: [] + m_LastFocusExited: + m_PersistentCalls: + m_Calls: [] + m_FocusEntered: + m_PersistentCalls: + m_Calls: [] + m_FocusExited: + m_PersistentCalls: + m_Calls: [] + m_Activated: + m_PersistentCalls: + m_Calls: [] + m_Deactivated: + m_PersistentCalls: + m_Calls: [] + m_StartingHoverFilters: [] + m_StartingSelectFilters: [] + m_StartingInteractionStrengthFilters: [] + m_AutoFindParentInteractableInHierarchy: 0 + m_ParentInteractableObject: {fileID: 0} + 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 &786056569404290700 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 980716176992391296} + 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 &7188846223050807764 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 980716176992391296} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5bb5c68765cb1e54cbe08cc0a5a15f05, type: 3} + m_Name: + m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Components.VrHinge + _localAxis: {x: 0, y: 1, z: 0} + _isOpeningInverted: 0 + _minAngle: 0 + _maxAngle: 90 + _closeAngle: 5 + _openHysteresis: 1 + _closeOverTime: 0.25 + _soundTimeout: 0.5 + _maxHandSpeed: 360 + _swingDamping: 3 + _maxSwingSpeed: 180 + _unlockPushSpeed: 60 + _breakDistance: 1 --- !u!1 &1222579370199367537 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerItemAdapter.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerItemAdapter.cs index 9e50444b2..ae8a869ee 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerItemAdapter.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRPlayerItemAdapter.cs @@ -1,4 +1,5 @@ using Gothic.Core.Manager; +using Gothic.VR.Interaction.Components; using Gothic.VR.Interaction.Models; using Gothic.VR.Interaction.Ports; using Reflex.Attributes; @@ -46,12 +47,16 @@ private void OnBeforeGrabbed(VrGrabArgs args) if (args.Kind == VrGrabberKind.Socket || args.Interactable == null) return; + // The hand grabbers grab mobs (doors, chest lids, seats), NPCs and the backpack as well. Their physics + // stay as they are (e.g. VrHinge keeps a door leaf kinematic, XRI would restore physics on it on drop). + if (!VrSocketSlot.IsItem(args.Interactable)) + return; + // In Gothic, Items have no physics when lying around. We need to activate physics for HVR to properly move items into our hands. if (args.Interactable.TryGetComponent(out var rb)) { rb.isKinematic = false; } } - } } diff --git a/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs b/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs index 857ffd190..e55e35462 100644 --- a/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs +++ b/Assets/Gothic-VR/Scripts/Adapters/Vob/LockPicking/VRContainerDoorPickingInteraction.cs @@ -20,7 +20,6 @@ namespace Gothic.VR.Adapters.Vob.LockPicking { public class VRContainerDoorPickingInteraction : MonoBehaviour { - [SerializeField] private GameObject _rootGO; [SerializeField] private AudioSource _audioSource; @@ -40,7 +39,7 @@ public class VRContainerDoorPickingInteraction : MonoBehaviour private const string _lockInteractionColliderName = "LockPickInteraction"; private int _combinationPos = 0; - + public enum DoorLockStatus { StepSuccess, @@ -68,7 +67,8 @@ private void Start() _keyInstance = container.Key; break; default: - Logger.LogError($"VRDoorLockInteraction: No door or container found for >{_lockable.Vob.Name}<.", LogCat.VR); + Logger.LogError($"VRDoorLockInteraction: No door or container found for >{_lockable.Vob.Name}<.", + LogCat.VR); break; } @@ -85,7 +85,7 @@ private void Start() private IEnumerator StartDelayed() { yield return null; - + // Deactivate rotation _hinge?.Lock(); } @@ -136,7 +136,7 @@ private static bool IsHeldItem(GameObject heldItem, GameObject lockInteraction) var interaction = heldItem.GetComponentInChildren(); return interaction != null && interaction.gameObject == lockInteraction; } - + private void OnTriggerExit(Collider other) { if (!other.gameObject.name.Equals(_lockInteractionColliderName)) @@ -244,17 +244,22 @@ private void UnlockAndPushOpen() _hinge.Unlock(); - // Apply force in the door's forward direction (world space) - var pushDirection = _hinge.Transform.forward; - var pushForce = 10f; - _hinge.Rigidbody.AddForce(pushDirection * pushForce, ForceMode.Impulse); + // Physics hinges (HVR) get pushed open here, kinematic ones (VrHinge) push themselves in Unlock(). + var body = _hinge.Rigidbody; + if (body != null && !body.isKinematic) + { + var pushDirection = _hinge.Transform.forward; + var pushForce = 10f; + body.AddForce(pushDirection * pushForce, ForceMode.Impulse); + } + gameObject.SetActive(false); } private void PlaySound(string soundName, string fallback = null) { var clip = _audioService.GetRandomSoundClip(soundName); - + if (clip == null && fallback != null) { PlaySound(fallback); @@ -263,7 +268,7 @@ private void PlaySound(string soundName, string fallback = null) if (clip == null) return; - + _audioSource.PlayOneShot(clip); } } diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrHingeBridge.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrHingeBridge.cs index a144ed501..94af17dbf 100644 --- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrHingeBridge.cs +++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrHingeBridge.cs @@ -7,7 +7,8 @@ namespace Gothic.VR.Interaction.Backends.Hvr { /// - /// Sits next to an HVRPhysicsDoor (doors, and chest lids via HvrPhysicsChest). + /// Sits next to an HVRPhysicsDoor (doors, and chest lids via HvrPhysicsChest). Switches the door off if another + /// VR backend runs. /// public class HvrHingeBridge : MonoBehaviour, IVrHinge { @@ -29,6 +30,13 @@ private HVRPhysicsDoor Door public Transform Transform => transform; public Rigidbody Rigidbody => GetComponent(); + private void Awake() + { + // The door would set up its own joint in Start and fight the hinge of another backend (VrHinge). + if (!IsActive && Door != null) + Door.enabled = false; + } + public void Lock() { Door.Lock(); diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/GothicGrabInteractable.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/GothicGrabInteractable.cs index a7a03fbaa..d0b3f1103 100644 --- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/GothicGrabInteractable.cs +++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/GothicGrabInteractable.cs @@ -21,12 +21,13 @@ namespace Gothic.VR.Interaction.Backends.Xri /// public class GothicGrabInteractable : XRGrabInteractable { - [Tooltip("Rigidbody interpolation while held. Restored on drop.")] - [SerializeField] private RigidbodyInterpolation _heldInterpolation = RigidbodyInterpolation.Interpolate; + [Tooltip("Rigidbody interpolation while held. Restored on drop.")] [SerializeField] + private RigidbodyInterpolation _heldInterpolation = RigidbodyInterpolation.Interpolate; [Tooltip("While a hand holds it, other hands can neither hover nor take it (the backpack: the other hand " + "takes items out of its slots).")] - [SerializeField] private bool _isOneHandOnly; + [SerializeField] + private bool _isOneHandOnly; private XriInteractableBridge _bridge; private RigidbodyInterpolation _interpolationBeforeGrab; @@ -130,6 +131,10 @@ protected override void Detach() return; } + // Kinematic leaves (doors, chest lids: VrHinge swings them on itself) aren't thrown either. + if (TryGetComponent(out var body) && body.isKinematic) + return; + base.Detach(); } diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriRig.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriRig.cs index ea06a69bc..eaa28799c 100644 --- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriRig.cs +++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriRig.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using Gothic.Core.Logging; +using Gothic.Core.Models.Vm; +using Gothic.Core.Services; using Gothic.VR.Adapters.Player; using Gothic.VR.Interaction.Models; using Gothic.VR.Interaction.Ports; @@ -26,6 +28,7 @@ namespace Gothic.VR.Interaction.Backends.Xri public class XriRig : MonoBehaviour, IVrRig { [Inject] private readonly IVrInput _vrInput; + [Inject] private readonly GameStateService _gameStateService; [Header("Rig")] [SerializeField] private XROrigin _origin; @@ -52,6 +55,10 @@ public class XriRig : MonoBehaviour, IVrRig [SerializeField] private GravityProvider _gravityProvider; [SerializeField] private JumpProvider _jumpProvider; + [Header("Teleport")] + [Tooltip("Thumbstick teleport (arc + ring). Child of this GameObject.")] + [SerializeField] private XriTeleportAimer _teleportAimer; + [Header("Movement")] [Tooltip("Walking speed in m/s.")] [SerializeField] private float _moveSpeed = 1.5f; @@ -128,7 +135,7 @@ public bool TurnEnabled /// /// Player-initiated teleporting. Teleports from code (Teleport()) always work, therefore the - /// TeleportationProvider itself stays enabled. There are no teleport interactors yet (HVR phase 2). + /// TeleportationProvider itself stays enabled. Disabling only stops the thumbstick teleport (XriTeleportAimer). /// public bool TeleportEnabled { @@ -240,6 +247,8 @@ private void Start() if (_xriInput == null) Logger.LogError("XriRig needs the XRI backend (XriInput). Sprinting and crouching won't work.", LogCat.VR); + else + CreateTeleportAimer(); } private void OnDestroy() @@ -254,6 +263,40 @@ private void Update() UpdateCrouch(); } + /// + /// Right thumbstick forward aims, releasing teleports, grip cancels. + /// + private void CreateTeleportAimer() + { + var map = _xriInput.Actions?.FindActionMap("XRI Right Locomotion"); + var stickAction = map?.FindAction("Teleport Mode"); + if (stickAction == null) + { + Logger.LogError("[XRI] Action 'XRI Right Locomotion/Teleport Mode' not found. " + + "Teleporting with the thumbstick won't work.", LogCat.VR); + return; + } + + var cancelAction = map.FindAction("Teleport Mode Cancel"); + if (cancelAction == null) + Logger.LogWarning("[XRI] Action 'XRI Right Locomotion/Teleport Mode Cancel' not found. " + + "Aiming a teleport can't be canceled.", LogCat.VR); + + // While crouching, the CharacterController is lower than the player once standing up at the target. + _teleportAimer.Init(stickAction, cancelAction, _rightController, _camera.transform, _characterController, + GetCollidingLayers(gameObject.layer), () => _teleportEnabled, () => IsGrounded, + () => CameraHeight + _crouchOffset, GetMaxSlopeAngle, Teleport); + } + + /// + /// Slide angle (degrees) of the human guild from Gothic's GIL_Values. Steeper ground makes Gothic's player + /// slide. 0 if the script isn't loaded (the aimer then falls back to the CharacterController's slope limit). + /// + private float GetMaxSlopeAngle() + { + return _gameStateService?.GuildValues?.GetSlideAngle((int)VmGothicEnums.Guild.GIL_HUMAN) ?? 0; + } + private void UpdateSprint() { var sprintAction = _xriInput?.GetGothicAction(XriGothicAction.Sprint) ?? default; diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportAimer.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportAimer.cs new file mode 100644 index 000000000..96e64a82b --- /dev/null +++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportAimer.cs @@ -0,0 +1,257 @@ +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace Gothic.VR.Interaction.Backends.Xri +{ + /// + /// Player-initiated teleport: pulling the thumbstick back shows an arc from the hand, releasing it teleports to + /// the arc's end if the player fits there. The world is created at runtime, so there are no TeleportationArea + /// interactables. The arc is checked against the physics world instead. The teleport itself is done by XRI's + /// TeleportationProvider (via the onTeleport callback). + /// Releasing at an invalid spot (red) does nothing. The cancel button (grip) cancels until the stick is released. + /// + public class XriTeleportAimer : MonoBehaviour + { + private const int _reticleSegments = 24; + + [SerializeField] private XriTeleportSettings _settings = new(); + + [Tooltip("Line of the aiming arc. Material, width and shadows are set on the component.")] + [SerializeField] private LineRenderer _arc; + + [Tooltip("Ring at the arc's end. Its points are written at runtime, so it needs Loop enabled.")] + [SerializeField] private LineRenderer _reticle; + + private Vector3[] _points; + + private InputAction _stickAction; + private InputAction _cancelAction; + private Transform _hand; + private Transform _head; + private CharacterController _character; + private LayerMask _blockingLayers; + private Func _isAllowed; + private Func _isGrounded; + private Func _getStandingHeight; + private Func _getMaxSlopeAngle; + private Action _onTeleport; + private bool _isInitialized; + private bool _isAiming; + private bool _isCanceled; + private bool _hasTarget; + private Vector3 _target; + + /// + /// Called by XriRig, which owns the rig references. Aiming starts only after this. + /// + public void Init(InputAction stickAction, InputAction cancelAction, Transform hand, Transform head, + CharacterController character, LayerMask blockingLayers, Func isAllowed, Func isGrounded, + Func getStandingHeight, Func getMaxSlopeAngle, Action onTeleport) + { + _stickAction = stickAction; + _cancelAction = cancelAction; + _hand = hand; + _head = head; + _character = character; + _blockingLayers = blockingLayers; + _isAllowed = isAllowed; + _isGrounded = isGrounded; + _getStandingHeight = getStandingHeight; + _getMaxSlopeAngle = getMaxSlopeAngle; + _onTeleport = onTeleport; + + _stickAction.Enable(); + _cancelAction?.Enable(); + + _points = new Vector3[_settings.ArcSamples + 1]; + _reticle.positionCount = _reticleSegments; + SetVisible(false); + _isInitialized = true; + } + + private void Update() + { + if (!_isInitialized) + return; + + // The Sector interaction (South, Locked) performs when the stick is pulled back first, stays performed + // when it's turned sideways and cancels once the stick returns to the center. A stick pushed sideways + // first (snap turn) never performs. + if (_stickAction.phase != InputActionPhase.Performed) + { + if (_isAiming && _hasTarget && _isAllowed() && _isGrounded()) + _onTeleport(_target, Quaternion.Euler(0f, _head.eulerAngles.y, 0f)); + Stop(); + _isCanceled = false; + return; + } + + if (_isCanceled) + return; + + if (_isAiming && _cancelAction != null && _cancelAction.WasPressedThisFrame()) + { + Stop(); + _isCanceled = true; + return; + } + + if (!_isAllowed()) + { + Stop(); + return; + } + + _isAiming = true; + UpdateArc(); + } + + private void Stop() + { + _isAiming = false; + _hasTarget = false; + SetVisible(false); + } + + private void UpdateArc() + { + _hasTarget = false; + + var origin = _hand.position; + var velocity = _hand.forward * _settings.ArcSpeed; + var gravity = Vector3.down * _settings.ArcGravity; + var step = _settings.ArcStep / _settings.ArcSpeed; + var previous = origin; + var normal = Vector3.up; + var isHit = false; + var count = 0; + _points[count++] = origin; + + for (var i = 1; i <= _settings.ArcSamples; i++) + { + var t = i * step; + var point = origin + velocity * t + 0.5f * t * t * gravity; + var segment = point - previous; + + if (Physics.SphereCast(previous, _settings.ArcRadius, segment.normalized, out var hit, + segment.magnitude, _blockingLayers, QueryTriggerInteraction.Ignore)) + { + _points[count++] = hit.point; + _target = hit.point; + normal = hit.normal; + isHit = true; + break; + } + + _points[count++] = point; + previous = point; + } + + // Mid-air (jumping, falling) the arc stays red, so that teleports can't extend jumps or cancel falls. + if (isHit) + SnapToGround(ref _target, ref normal); + + _hasTarget = isHit && _isGrounded() && IsValidTarget(_target, normal); + + var color = _hasTarget ? _settings.ValidColor : _settings.InvalidColor; + _arc.startColor = _arc.endColor = color; + _arc.positionCount = count; + _arc.SetPositions(_points); + _arc.enabled = true; + + if (isHit) + DrawReticle(_target, normal, color); + else + _reticle.enabled = false; + } + + /// + /// The sphere cast's normal is the contact normal of the sphere. On edges and steps it is tilted and says + /// nothing about the ground. A ray from above the hit (like HVR's downward ray) gives the surface the player + /// would stand on. + /// + private void SnapToGround(ref Vector3 point, ref Vector3 normal) + { + var origin = point + Vector3.up * _settings.GroundRayHeight; + if (Physics.Raycast(origin, Vector3.down, out var hit, _settings.GroundRayHeight * 2f, _blockingLayers, + QueryTriggerInteraction.Ignore)) + { + point = hit.point; + normal = hit.normal; + } + } + + /// + /// Walkable slope (OpenGothic MoveAlgo::testSlide: the NPC slides once normal.y < cos(slide_angle)), height + /// difference to the capsule's bottom (HVR CheckVerticalDistance / CheckFallDistance), enough room for the + /// player's standing capsule (also while crouching, as the player stands up afterwards) and a free line of + /// sight from the head (HVR CheckDestinationLineOfSight). + /// + private bool IsValidTarget(Vector3 point, Vector3 normal) + { + var maxSlopeAngle = _getMaxSlopeAngle(); + if (maxSlopeAngle <= 0f) + maxSlopeAngle = _character.slopeLimit; + if (normal.y < Mathf.Cos(maxSlopeAngle * Mathf.Deg2Rad)) + return false; + + // Same as HVR's GetBottom() for a CharacterController. + var capsuleBottom = _character.transform.TransformPoint(_character.center).y - _character.height * 0.5f; + if (point.y > capsuleBottom + _settings.MaxJumpDistance || + point.y < capsuleBottom - _settings.MaxDropDistance) + return false; + + var radius = _character.radius; + var height = Mathf.Max(_character.height, _getStandingHeight()); + var bottom = point + Vector3.up * (radius + _character.stepOffset); + var top = point + Vector3.up * Mathf.Max(height - radius, radius + _character.stepOffset); + + if (Physics.CheckCapsule(bottom, top, radius * 0.95f, _blockingLayers, QueryTriggerInteraction.Ignore)) + return false; + + return HasLineOfSight(point); + } + + /// + /// One free line from the head to any of the points above the target is enough, so that stairs and ledges + /// near the target don't block it, but walls between player and target do. + /// + private bool HasLineOfSight(Vector3 point) + { + var head = _head.position; + var samples = Mathf.Max(1, _settings.LineOfSightSamples); + + for (var i = 0; i < samples; i++) + { + var target = point + Vector3.up * (i / (float)samples * _settings.LineOfSightHeight); + var direction = target - head; + if (!Physics.Raycast(head, direction, direction.magnitude, _blockingLayers, + QueryTriggerInteraction.Ignore)) + return true; + } + + return false; + } + + private void DrawReticle(Vector3 center, Vector3 normal, Color color) + { + var rotation = Quaternion.FromToRotation(Vector3.up, normal); + for (var i = 0; i < _reticleSegments; i++) + { + var angle = i * Mathf.PI * 2f / _reticleSegments; + var local = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * _settings.ReticleRadius; + _reticle.SetPosition(i, center + normal * 0.02f + rotation * local); + } + + _reticle.startColor = _reticle.endColor = color; + _reticle.enabled = true; + } + + private void SetVisible(bool isVisible) + { + _arc.enabled = isVisible; + _reticle.enabled = isVisible; + } + } +} diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportAimer.cs.meta b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportAimer.cs.meta new file mode 100644 index 000000000..a6ebd5b91 --- /dev/null +++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportAimer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7f622519f5c2fe64a93dfdb569a97b2d \ No newline at end of file diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportSettings.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportSettings.cs new file mode 100644 index 000000000..0c4af021d --- /dev/null +++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportSettings.cs @@ -0,0 +1,54 @@ +using System; +using UnityEngine; + +namespace Gothic.VR.Interaction.Backends.Xri +{ + /// + /// Tuning of the player-initiated teleport (right thumbstick back, release to teleport). + /// When aiming starts and ends is defined by the Sector interaction of 'XRI Right Locomotion/Teleport Mode'. + /// + [Serializable] + public class XriTeleportSettings + { + [Tooltip("Launch speed of the aiming arc in m/s. Higher values reach farther.")] + public float ArcSpeed = 9f; + + [Tooltip("Gravity of the aiming arc in m/s^2.")] + public float ArcGravity = 9.81f; + + [Tooltip("Distance in m between two arc samples.")] + public float ArcStep = 0.4f; + + [Tooltip("Number of arc samples. Together with ArcStep it limits the teleport distance.")] + public int ArcSamples = 40; + + [Tooltip("Radius in m of the sphere cast along the arc, so that it doesn't slip through bars and small gaps.")] + public float ArcRadius = 0.1f; + + [Tooltip("Maximum height in m the target can be above the bottom of the player's capsule " + + "(HVR: MaxJumpDistance).")] + public float MaxJumpDistance = 1.5f; + + [Tooltip("Maximum height in m the target can be below the bottom of the player's capsule " + + "(HVR: MaxDropDistance).")] + public float MaxDropDistance = 3f; + + [Tooltip("Height in m above the target up to which the head needs a free line of sight " + + "(HVR: DestinationHeight).")] + public float LineOfSightHeight = 1f; + + [Tooltip("Number of lines of sight from the head to the target, spread over LineOfSightHeight. " + + "One free line is enough (HVR: DestinationIntervals).")] + public int LineOfSightSamples = 8; + + [Tooltip("Height in m above the arc's hit from where the ground is found with a downward ray. " + + "The ray reaches twice as far down.")] + public float GroundRayHeight = 0.3f; + + [Tooltip("Radius of the target ring in m.")] + public float ReticleRadius = 0.25f; + + public Color ValidColor = new(0.3f, 0.9f, 0.4f, 0.9f); + public Color InvalidColor = new(0.9f, 0.3f, 0.3f, 0.6f); + } +} diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportSettings.cs.meta b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportSettings.cs.meta new file mode 100644 index 000000000..63edf7751 --- /dev/null +++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriTeleportSettings.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a8f89e72ca2cc544e945c8106ebef437 \ No newline at end of file diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrHinge.cs b/Assets/Gothic-VR/Scripts/Interaction/Components/VrHinge.cs new file mode 100644 index 000000000..cecd5cadf --- /dev/null +++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrHinge.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections.Generic; +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 +{ + /// + /// Backend-neutral hinge for doors (and later 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. + /// - After letting go, the leaf swings on with the hand's last angular speed and slows down (SwingDamping). It + /// stops at the limits without bouncing. + /// - Angle is the opened angle in degrees (0 = closed). + /// - Closed below CloseAngle, open above CloseAngle + OpenHysteresis. Opened/Closed are raised once per change. + /// The Gothic open/close sound is played once per change, but not more often than SoundTimeout. + /// - Soft close: released below CloseAngle, the leaf closes completely within CloseOverTime. + /// - Lock() holds the leaf at 0 degrees, Unlock() frees it with a small push open (lock picking). + /// - Being kinematic, the player's CharacterController can't push the leaf through its limits. + /// - Releases the leaf when the hand gets farther than BreakDistance from it (e.g. teleporting while holding). + /// + /// The grab itself is done by the backend (GothicGrabInteractable + XriInteractableBridge, with tracking off). + /// Inactive on HVR, where HVRPhysicsDoor next to HvrHingeBridge is the hinge. + /// + [RequireComponent(typeof(Rigidbody))] + public class VrHinge : MonoBehaviour, IVrHinge + { + [Tooltip("Hinge axis in the leaf's local space.")] [SerializeField] + private Vector3 _localAxis = Vector3.up; + + [Tooltip("The leaf opens against the right-hand rotation around the axis.")] [SerializeField] + private bool _isOpeningInverted; + + [Tooltip("Smallest opened angle (degrees).")] [SerializeField] + private float _minAngle; + + [Tooltip("Largest opened angle (degrees).")] [SerializeField] + private float _maxAngle = 90f; + + [Tooltip("Below this opened angle the hinge counts as closed (degrees).")] [SerializeField] + private float _closeAngle = 5f; + + [Tooltip("Degrees above CloseAngle until the hinge counts as open again. Prevents repeated events and " + + "sounds while the leaf rattles around the threshold.")] + [SerializeField] + private float _openHysteresis = 1f; + + [Tooltip("Seconds the leaf needs to close completely after letting go below CloseAngle. 0 = off.")] + [SerializeField] + private float _closeOverTime = 0.25f; + + [Tooltip("Minimum seconds between two open/close sounds.")] [SerializeField] + private float _soundTimeout = 0.5f; + + [Tooltip("Fastest rotation while a hand drags the leaf (degrees/s). Hands can't whip it through the limits.")] + [SerializeField] + private float _maxHandSpeed = 360f; + + [Tooltip("How fast a released leaf slows down (1/s). Higher = stops sooner.")] [SerializeField] + private float _swingDamping = 3f; + + [Tooltip("Fastest rotation of a released leaf (degrees/s).")] [SerializeField] + private float _maxSwingSpeed = 180f; + + [Tooltip("Rotation speed (degrees/s) the leaf gets when it is unlocked (the push of lock picking).")] + [SerializeField] + private float _unlockPushSpeed = 60f; + + [Tooltip("A hand farther than this from the leaf (meters) lets go of it.")] [SerializeField] + private float _breakDistance = 1f; + + [Inject] private readonly IVrBackend _vrBackend; + [Inject] private readonly IVrSfxPlayer _sfxPlayer; + + private readonly List _grabbers = new(); + + private VrInteractable _interactable; + private Quaternion _closedRotation; + private Vector3 _axis; + private Vector3 _planeReference; + private AudioClip _openedClip; + private AudioClip _closedClip; + private float _angle; + private float _speed; + private float _grabHandAngle; + private float _grabLeafAngle; + private float _lastSoundTime = float.NegativeInfinity; + private bool _isStarted; + private bool _isSubscribed; + private bool _isLocked; + 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; + public bool IsOpen => _isOpen; + public bool IsClosed => _isClosed; + public bool IsLocked => _isLocked; + public bool IsHeld => _grabbers.Count > 0; + public Transform Transform => transform; + public Rigidbody Rigidbody => GetComponent(); + + /// + /// Opened angle in degrees. 0 = closed, MaxAngle = fully open. + /// + public float Angle => _angle; + + private void Start() + { + if (!IsActive) + { + enabled = false; + return; + } + + // Moved by transform only. The Rigidbody is kept for the colliders and the lock picking. + var body = GetComponent(); + body.isKinematic = true; + body.useGravity = false; + + _axis = _localAxis.normalized; + _closedRotation = transform.localRotation; + var worldAxis = GetWorldAxis(); + var reference = Mathf.Abs(Vector3.Dot(worldAxis, Vector3.up)) > 0.9f ? Vector3.right : Vector3.up; + _planeReference = Vector3.ProjectOnPlane(reference, worldAxis).normalized; + _isStarted = true; + + _interactable = GetComponent(); + if (_interactable == null) + { + Logger.LogWarning($"[Hinge] {name} has no VrInteractable, it can't be opened by hand.", LogCat.VR); + return; + } + + _interactable.Grabbed += OnGrabbed; + _interactable.Released += OnReleased; + _isSubscribed = true; + } + + private void OnDestroy() + { + if (!_isSubscribed || _interactable == null) + return; + + _interactable.Grabbed -= OnGrabbed; + _interactable.Released -= OnReleased; + } + + private void LateUpdate() + { + // After the XR rig has moved the hands this frame. + BreakIfHandIsFar(); + + var deltaTime = Time.deltaTime; + if (deltaTime <= 0f) + return; + + if (_isLocked) + MoveTo(0f, deltaTime); + else if (IsHeld) + FollowHand(deltaTime); + else + Swing(deltaTime); + + UpdateState(_angle); + } + + public void Lock() + { + _isLocked = true; + _speed = 0f; + if (!_isStarted) + return; + + _angle = 0f; + ApplyAngle(); + UpdateState(_angle); + } + + public void Unlock() + { + _isLocked = false; + if (!_isStarted) + return; + + // The push open of the lock picking. + _speed = _unlockPushSpeed; + RebaselineGrab(); + } + + public void SetSounds(AudioClip opened, AudioClip closed) + { + _openedClip = opened; + _closedClip = closed; + } + + private Vector3 GetWorldAxis() + { + return transform.TransformDirection(_localAxis.normalized); + } + + /// + /// Angle of the hand around the hinge axis in the hinge plane, in the opening direction (degrees). + /// + private float GetHandAngle(Vector3 handPosition) + { + var worldAxis = GetWorldAxis(); + var fromHinge = Vector3.ProjectOnPlane(handPosition - transform.position, worldAxis); + var angle = Vector3.SignedAngle(_planeReference, fromHinge, worldAxis); + return _isOpeningInverted ? -angle : angle; + } + + private void FollowHand(float deltaTime) + { + var hand = _grabbers[0]; + if (hand == null) + return; + + var handPosition = hand.transform.position; + var radius = Vector3.ProjectOnPlane(handPosition - transform.position, GetWorldAxis()).magnitude; + // A hand right at the hinge has no meaningful angle. + if (radius < 0.05f) + return; + + var handDelta = Mathf.DeltaAngle(_grabHandAngle, GetHandAngle(handPosition)); + var target = Mathf.Clamp(_grabLeafAngle + handDelta, _minAngle, _maxAngle); + var previous = _angle; + MoveTo(target, deltaTime, _maxHandSpeed); + + // Keeps its speed for the swing after letting go. + _speed = Mathf.Lerp(_speed, (_angle - previous) / deltaTime, 0.5f); + } + + private void Swing(float deltaTime) + { + if (_closeOverTime > 0f && _angle < _closeAngle && _angle > _minAngle && Mathf.Abs(_speed) < 5f) + { + _speed = 0f; + MoveTo(_minAngle, deltaTime, _closeAngle / _closeOverTime); + return; + } + + if (Mathf.Approximately(_speed, 0f)) + return; + + _speed = Mathf.Clamp(_speed * Mathf.Exp(-_swingDamping * deltaTime), -_maxSwingSpeed, _maxSwingSpeed); + if (Mathf.Abs(_speed) < 0.5f) + _speed = 0f; + + var next = Mathf.Clamp(_angle + _speed * deltaTime, _minAngle, _maxAngle); + if (Mathf.Approximately(next, _minAngle) || Mathf.Approximately(next, _maxAngle)) + _speed = 0f; + + _angle = next; + ApplyAngle(); + } + + private void MoveTo(float target, float deltaTime, float maxSpeed = float.PositiveInfinity) + { + var next = Mathf.MoveTowards(_angle, target, maxSpeed * deltaTime); + if (Mathf.Approximately(next, _angle)) + return; + + _angle = next; + ApplyAngle(); + } + + private void ApplyAngle() + { + var sign = _isOpeningInverted ? -1f : 1f; + transform.localRotation = _closedRotation * Quaternion.AngleAxis(sign * _angle, _axis); + } + + private void UpdateState(float angle) + { + if (_isClosed && angle > _closeAngle + _openHysteresis) + { + _isClosed = false; + _isOpen = true; + PlaySound(_openedClip); + Opened?.Invoke(); + } + else if (_isOpen && angle < _closeAngle) + { + _isClosed = true; + _isOpen = false; + PlaySound(_closedClip); + Closed?.Invoke(); + } + } + + private void PlaySound(AudioClip clip) + { + if (clip == null || _sfxPlayer == null || Time.time - _lastSoundTime < _soundTimeout) + return; + + _lastSoundTime = Time.time; + _sfxPlayer.Play(clip, transform.position); + } + + private void BreakIfHandIsFar() + { + if (!IsHeld || _interactable == null) + return; + + foreach (var grabber in _grabbers) + { + if (grabber == null || GetDistanceToLeaf(grabber.transform.position) <= _breakDistance) + continue; + + Logger.Log($"[Hinge] {name} released: hand is farther than {_breakDistance}m away.", LogCat.VR); + _interactable.ForceRelease(); + return; + } + } + + private float GetDistanceToLeaf(Vector3 position) + { + var distance = float.MaxValue; + foreach (var leafCollider in _interactable.Colliders) + { + if (leafCollider == null || !leafCollider.enabled) + continue; + + distance = Mathf.Min(distance, Vector3.Distance(leafCollider.ClosestPointOnBounds(position), position)); + } + + return distance; + } + + private void OnGrabbed(VrGrabArgs args) + { + if (!args.IsHandOrDistance || args.GrabberGo == null || _grabbers.Contains(args.GrabberGo)) + return; + + _grabbers.Add(args.GrabberGo); + _speed = 0f; + RebaselineGrab(); + } + + private void OnReleased(VrGrabArgs args) + { + _grabbers.Remove(args.GrabberGo); + RebaselineGrab(); + } + + /// + /// The leaf follows the first hand from where the leaf and the hand are now. + /// + private void RebaselineGrab() + { + if (!IsHeld || _grabbers[0] == null) + return; + + _grabLeafAngle = _angle; + _grabHandAngle = GetHandAngle(_grabbers[0].transform.position); + } + } +} diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrHinge.cs.meta b/Assets/Gothic-VR/Scripts/Interaction/Components/VrHinge.cs.meta new file mode 100644 index 000000000..f67033dd5 --- /dev/null +++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrHinge.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5bb5c68765cb1e54cbe08cc0a5a15f05 \ No newline at end of file diff --git a/Docs/HVR-replacement/found-bugs.md b/Docs/HVR-replacement/found-bugs.md index 888e6e717..8d9b8532f 100644 --- a/Docs/HVR-replacement/found-bugs.md +++ b/Docs/HVR-replacement/found-bugs.md @@ -44,7 +44,6 @@ Move them to GitHub issues when the branch is merged, if they are still open. - Taking items from a chest doesn't change the chest's saved contents. - Chest slot spheres overlap on small chests (same as with HVR). - ## Misc - Head never sticks into the wall as it is blocked from going into (poking back) - Marvin menu is rotated wrong (fixed later, when we have a hand and actually see, where it should be placed). @@ -55,3 +54,4 @@ Move them to GitHub issues when the branch is merged, if they are still open. - Simulator: Very complex to traverse. Why not having it like the one from https://github.com/AdrielHercules/hvr-simulator (aka Controller handling etc.) - Rotation + Scaling of objects inside Slots is broken - Lockpicking - finalizing the minigame should open the door directly. Currently: Nothing. +- Raycaster feels "wobbly". Should be a sharp and straight line diff --git a/Docs/HVR-replacement/phase-1-rig-input-locomotion-ui.md b/Docs/HVR-replacement/phase-1-rig-input-locomotion-ui.md index cfabd2258..8c26f82fd 100644 --- a/Docs/HVR-replacement/phase-1-rig-input-locomotion-ui.md +++ b/Docs/HVR-replacement/phase-1-rig-input-locomotion-ui.md @@ -104,7 +104,11 @@ VR - Player XRI - `Gravity` maps to `GravityProvider.gravityAccelerationModifier` (relative to `Physics.gravity`), 0 disables gravity; the grounded sphere cast uses every layer the Player layer collides with. `IsGrounded` = `GravityProvider.isGrounded || CharacterController.isGrounded`. - - `TeleportEnabled` is only a flag for player-initiated teleports (no teleport interactor yet). The + - `TeleportEnabled` gates player-initiated teleports: `XriTeleportAimer` (right thumbstick forward = arc, release = + teleport, grip = cancel; driven by the `Sector` interaction of `Teleport Mode`). No `TeleportationArea`s, as the + world is runtime-built. Instead the arc is a sphere cast against the physics world, and a target is valid only when + grounded, within `MaxRise`/`MaxDrop` of the feet, flat enough, with room for the standing capsule and a free path + for the head (no teleports through bars or over high walls). Settings in `XriTeleportSettings`. The `TeleportationProvider` stays enabled, so `Teleport()` also works while the player is locked (dialogs, cinema). - `Teleport` queues `MatchOrientation.TargetUpAndForward` (the camera, not the origin, ends above the position and looks along the rotation; `default(Quaternion)` = identity). The HVR workaround of setting the rotation afterwards diff --git a/Docs/HVR-replacement/phase-4-doors-chests-ladders.md b/Docs/HVR-replacement/phase-4-doors-chests-ladders.md index d00abf11a..283486563 100644 --- a/Docs/HVR-replacement/phase-4-doors-chests-ladders.md +++ b/Docs/HVR-replacement/phase-4-doors-chests-ladders.md @@ -4,11 +4,16 @@ **Size:** M (1-2 weeks) · **Depends on:** Phases 0-2 · **Parallel with:** Phase 3 (chest slots) +Split into three PRs: **4a** `VrHinge` + doors on Xri (lock picking, soft close, hand break distance), **4b** chest lid +on `VrHinge` (replaces the interim `VrChestLidToggle`), **4c** ladders + `KeepPlayerWithin`/`IsClimbing`. + ## Exit criteria -- [ ] Doors open by grabbing and pulling/pushing, are limited to 0-90°, and play the Gothic open/close sounds from the - door's MDS once per open/close. -- [ ] Locked doors don't move; lock picking unlocks them (`VRContainerDoorPickingInteraction`). +- [x] Doors open by grabbing and pulling/pushing, are limited to 0-90°, and play the Gothic open/close sounds from the + door's MDS once per open/close. (4a; the Gothic sounds are handed to the hinge by `VRVobDoor`, played once per + 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. - [ ] Ladders can be climbed hand over hand. `IVrRig.IsClimbing` is true while climbing (used by `KeepPlayerWithin`). @@ -51,8 +56,8 @@ hand drags the leaf physically. No position tracking of the hand → hinge, whic problem. Set `trackRotation = false` so only the position drives the leaf (feels like pushing/pulling a handle). Alternative if VelocityTracking + HingeJoint feels jittery: kinematic leaf with our own angle solve (project the hand -movement onto the hinge plane, clamp to the limits). (Decision 2026-10-22: nothing from UltimateXR is used, not even -as a code reference.) +movement onto the hinge plane, clamp to the limits). (4a: this is what `VrHinge` does, see the status below.) +(Decision 2026-10-22: nothing from UltimateXR is used, not even as a code reference.) ### Chest @@ -75,12 +80,16 @@ as a code reference.) ## Work items -- [ ] `VrHinge` (+ `IVrHinge` implementation), `VrLadderExit`. +- [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).) +- [ ] `VrLadderExit` (4c). - [ ] `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`.) -- [ ] **Prefabs (UnityMCP):** `oCMobDoor.prefab` (VrHinge + grab interactable + bridge), `oCMobContainer.prefab` - (lid), `oCMobLadder.prefab` (ClimbInteractable + bridge). +- [ ] **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). - [ ] `VRContextInteractionService.KeepPlayerWithin` uses `IVrRig.IsClimbing`; verify with a dialog at a ladder. ## Test plan @@ -94,6 +103,35 @@ as a code reference.) gravity). 6. Teleport while holding a door → the door releases cleanly. +## Status after 4a + +Xri + simulator, programmatic hand (hand controller moved by script, `SelectEnter`/`SelectExit` on the interactor), +G1 old camp doors at (302, 4.5, 140) and (183, -2, 291, locked), editor at a few fps (so only logic was judged, not +feel): +- **Decision: kinematic angle solving instead of velocity tracking.** The first version (leaf on a `HingeJoint` / + `ConfigurableJoint` to an anchor body, hand drags it by velocity tracking) was played first: `HingeJoint.angle` has + an arbitrary zero (limits outside -180..180 break), and with the joint the leaf overshot 8° past the limit, jammed + and recoiled when released. `VrHinge` now rotates the kinematic leaf itself: the hand's angle around the hinge axis + (in the hinge plane) turns the leaf, clamped to 0-90°; after letting go it swings on with the hand's speed and + stops at the limits. `GothicGrabInteractable` on the leaf only selects (Kinematic movement, position and rotation + tracking off). Side effect: the CharacterController can't push the leaf (risk "CharacterController pushes the + door" is solved by design), no `Rigidbody` dynamics, no recoil. +- Pull open with a hand moving 25°/s: leaf follows 1:1 up to 75°, back to closed; `Opened` once, release at 3° → + soft close to 0° in 0.25 s and `Closed` once. Sweep of the hand to 125°: leaf clamps at 90°. Released while moving + at ~50°/s: swings to 90° and stays without overshoot. +- Open/close clips are set by `VRVobDoor` (`Door_open01`/`Door_close01b`), played from the state change (not heard). +- `Lock()`: leaf snaps to 0° and a hand pulling 40°/s doesn't move it; `Unlock()` gives a push open (~60°/s, comes + to rest at ~20°). The real locked door (hut at 183, -2, 291) gets `Lock()` from `VRContainerDoorPickingInteraction` + (`IsLocked` true). The picking script pushes only non-kinematic hinges (HVR), `VrHinge` + pushes itself in `Unlock()`. +- Hand farther than 1 m from the leaf (teleport) lets go of it. +- Hvr backend: `HVRPhysicsDoor` is disabled on Xri (`HvrHingeBridge.Awake`), `VrHinge` disables itself on Hvr + (checked by the components' state at runtime on Xri; the Hvr door was not played). +- Not tested: a real pick-lock sequence by hand (haptics), closing a door by hand with sound audible, a headset + (hand tracking noise: the hand angle is used raw), Hvr door play test, two hands on one door (first hand wins). +- 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. + ## Risks | Risk | Mitigation |