Feat(camera) : Add LostFootageRig - handheld camera shake, micro-jolts and inertia swing

This commit is contained in:
Ferlat Thibaud
2026-04-13 15:17:01 +02:00
parent 91f220f985
commit f982a74462
14 changed files with 769 additions and 451 deletions
@@ -367,7 +367,8 @@
"m_StageCapability": 3,
"m_Value": 0.0,
"m_DefaultValue": 0.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -486,6 +487,9 @@
"m_Guid": {
"m_GuidSerialized": "228c6fe3-75fc-4e2d-863e-95469d7dc071"
},
"promotedFromAssetID": "",
"promotedFromCategoryName": "",
"promotedOrdering": -1,
"m_Name": "Speed",
"m_DefaultRefNameVersion": 1,
"m_RefNameGeneratedByDisplayName": "Speed",
@@ -499,12 +503,25 @@
"overrideHLSLDeclaration": false,
"hlslDeclarationOverride": 0,
"m_Hidden": false,
"m_PerRendererData": false,
"m_customAttributes": [],
"m_Value": 0.20000000298023225,
"m_FloatType": 0,
"m_LiteralFloatMode": false,
"m_RangeValues": {
"x": 0.0,
"y": 1.0
}
},
"m_SliderType": 0,
"m_SliderPower": 3.0,
"m_EnumType": 0,
"m_CSharpEnumString": "",
"m_EnumNames": [
"Default"
],
"m_EnumValues": [
0
]
}
{
@@ -578,7 +595,10 @@
{
"m_SGVersion": 2,
"m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalUnlitSubTarget",
"m_ObjectId": "431b01bf9333463fa8ce6301922f7697"
"m_ObjectId": "431b01bf9333463fa8ce6301922f7697",
"m_KeepLightingVariants": false,
"m_DefaultDecalBlending": true,
"m_DefaultSSAO": true
}
{
@@ -649,7 +669,8 @@
"m_StageCapability": 3,
"m_Value": 0.0,
"m_DefaultValue": 0.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -710,8 +731,8 @@
"y": 1.0
},
"m_DefaultValue": {
"x": 0.0,
"y": 0.0
"x": 1.0,
"y": 1.0
},
"m_Labels": []
}
@@ -728,7 +749,8 @@
"m_StageCapability": 3,
"m_Value": 0.0,
"m_DefaultValue": 0.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -765,7 +787,8 @@
"m_StageCapability": 3,
"m_Value": 1.0,
"m_DefaultValue": 1.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -780,7 +803,8 @@
"m_StageCapability": 3,
"m_Value": 0.0,
"m_DefaultValue": 0.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -790,6 +814,9 @@
"m_Guid": {
"m_GuidSerialized": "977cef0d-2992-4263-896b-005b96fc6a9a"
},
"promotedFromAssetID": "",
"promotedFromCategoryName": "",
"promotedOrdering": -1,
"m_Name": "Scale",
"m_DefaultRefNameVersion": 1,
"m_RefNameGeneratedByDisplayName": "Scale",
@@ -803,12 +830,25 @@
"overrideHLSLDeclaration": false,
"hlslDeclarationOverride": 0,
"m_Hidden": false,
"m_PerRendererData": false,
"m_customAttributes": [],
"m_Value": 20.0,
"m_FloatType": 0,
"m_LiteralFloatMode": false,
"m_RangeValues": {
"x": 0.0,
"y": 1.0
}
},
"m_SliderType": 0,
"m_SliderPower": 3.0,
"m_EnumType": 0,
"m_CSharpEnumString": "",
"m_EnumNames": [
"Default"
],
"m_EnumValues": [
0
]
}
{
@@ -818,6 +858,9 @@
"m_Guid": {
"m_GuidSerialized": "585f60cb-18ac-412c-8368-13f719cfa634"
},
"promotedFromAssetID": "",
"promotedFromCategoryName": "",
"promotedOrdering": -1,
"m_Name": "Tiling",
"m_DefaultRefNameVersion": 1,
"m_RefNameGeneratedByDisplayName": "Tiling",
@@ -831,6 +874,8 @@
"overrideHLSLDeclaration": false,
"hlslDeclarationOverride": 0,
"m_Hidden": false,
"m_PerRendererData": false,
"m_customAttributes": [],
"m_Value": {
"x": 1.0,
"y": 1.0,
@@ -851,7 +896,8 @@
"m_StageCapability": 3,
"m_Value": 0.0,
"m_DefaultValue": 0.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -1017,7 +1063,8 @@
"m_StageCapability": 3,
"m_Value": 0.0,
"m_DefaultValue": 0.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -1032,7 +1079,8 @@
"m_StageCapability": 3,
"m_Value": 1.0,
"m_DefaultValue": 1.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -1101,6 +1149,10 @@
"m_AlphaClip": false,
"m_CastShadows": true,
"m_ReceiveShadows": true,
"m_DisableTint": false,
"m_Sort3DAs2DCompatible": false,
"m_AdditionalMotionVectorMode": 0,
"m_AlembicMotionVectors": false,
"m_SupportsLODCrossFade": false,
"m_CustomEditorGUI": "",
"m_SupportVFX": false
@@ -1341,7 +1393,8 @@
"m_StageCapability": 3,
"m_Value": 0.0,
"m_DefaultValue": 0.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -1392,7 +1445,8 @@
"m_StageCapability": 3,
"m_Value": 0.0,
"m_DefaultValue": 0.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -1504,7 +1558,8 @@
"m_StageCapability": 2,
"m_Value": 1.0,
"m_DefaultValue": 1.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -1519,7 +1574,8 @@
"m_StageCapability": 3,
"m_Value": 20.0,
"m_DefaultValue": 500.0,
"m_Labels": []
"m_Labels": [],
"m_LiteralMode": false
}
{
@@ -1574,6 +1630,9 @@
"m_Guid": {
"m_GuidSerialized": "0a8052f7-8e16-4680-ac9d-2bc58a2bf587"
},
"promotedFromAssetID": "",
"promotedFromCategoryName": "",
"promotedOrdering": -1,
"m_Name": "MainTex",
"m_DefaultRefNameVersion": 1,
"m_RefNameGeneratedByDisplayName": "MainTex",
@@ -1587,12 +1646,15 @@
"overrideHLSLDeclaration": false,
"hlslDeclarationOverride": 0,
"m_Hidden": false,
"m_PerRendererData": false,
"m_customAttributes": [],
"m_Value": {
"m_SerializedTexture": "",
"m_Guid": ""
},
"isMainTexture": false,
"useTilingAndOffset": false,
"useTexelSize": true,
"m_Modifiable": true,
"m_DefaultType": 0
}
@@ -64,7 +64,7 @@ Transform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4440040250278121308}
m_Father: {fileID: 5410551608337962707}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!20 &3666612565151794712
Camera:
@@ -186,6 +186,75 @@ MonoBehaviour:
serializedVersion: 2
m_Bits: 8
_raycastInterval: 0
--- !u!1 &2277482252851742510
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 5410551608337962707}
- component: {fileID: 1476756471834662335}
m_Layer: 0
m_Name: CameraPivotRig
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &5410551608337962707
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2277482252851742510}
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: 1836339402272228918}
- {fileID: 1175390396199967368}
m_Father: {fileID: 4440040250278121308}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1476756471834662335
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2277482252851742510}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 320cde26f18d40b40a31c85e85ee4e59, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Unheimlich.World.CameraSystem.LostFootageRig
_idleAmplitude: 0.25
_idleNoiseSpeed: 0.6
_walkAmplitude: 0.8
_walkNoiseSpeed: 1.4
_runAmplitude: 1.8
_runNoiseSpeed: 2.5
_stressAmplitudeMul: 2.5
_stressSpeedMul: 1.8
_idleJoltAmplitude: 0.5
_idleJoltIntervalRange: {x: 2, y: 4}
_walkJoltAmplitude: 1.2
_walkJoltIntervalRange: {x: 0.8, y: 2}
_runJoltAmplitude: 2.5
_runJoltIntervalRange: {x: 0.2, y: 0.8}
_joltSnapFraction: 0.25
_stressJoltFreqMul: 5
_stressJoltAmpMul: 3
_inertiaStrength: 4
_inertiaDamping: 6
_inertiaMaxAngle: 8
_fadeDuration: 0.6
_debugSustainStress: 0
_debugStressValue: 0
--- !u!1 &4625798697874999476
GameObject:
m_ObjectHideFlags: 0
@@ -215,7 +284,7 @@ Transform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 4440040250278121308}
m_Father: {fileID: 5410551608337962707}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &5575782855984327677
GameObject:
@@ -247,8 +316,7 @@ Transform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1836339402272228918}
- {fileID: 1175390396199967368}
- {fileID: 5410551608337962707}
m_Father: {fileID: 8725966219179855439}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &986184075687059205
@@ -277,6 +345,7 @@ GameObject:
- component: {fileID: 688176918831081180}
- component: {fileID: 3522630547589622128}
- component: {fileID: 4152409555847978467}
- component: {fileID: 4484393045093388587}
m_Layer: 0
m_Name: OctoPlayerFP
m_TagString: Player
@@ -347,7 +416,10 @@ MonoBehaviour:
RunSpeed: 20
JumpDuration: 0.4
JumpHeight: 0.4
StepHeight: 0.3
Gravity: 9.8
CanJump: 1
CanRun: 1
_mapping:
MoveAction: -560403583
LookAction: -916633937
@@ -397,3 +469,20 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Unheimlich.Interactions.Core.PlayerInspectionHandler
_inspectionHoldPoint: {fileID: 6579694192787271975}
--- !u!114 &4484393045093388587
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8162540767767967414}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b711f35d53cba9d43b8b6f55a40cedbc, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Unheimlich.Features.Player.StressController
_rig: {fileID: 1476756471834662335}
_decaySpeed: 0.3
_deactivateThreshold: 0.05
_debugSustainStress: 1
_debugStressValue: 0.2
File diff suppressed because one or more lines are too long
@@ -382,6 +382,10 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 1313019864}
m_Modifications:
- target: {fileID: 2208949961880032178, guid: 639055b404705a24cb022290319ad5ce, type: 3}
propertyPath: m_IsActive
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2264172744066454467, guid: 639055b404705a24cb022290319ad5ce, type: 3}
propertyPath: m_LocalPosition.y
value: 2.79
@@ -490,6 +494,114 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 184683168}
m_Modifications:
- target: {fileID: 330526706382485655, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _noiseSpeed
value: 0.6
objectReference: {fileID: 0}
- target: {fileID: 330526706382485655, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _baseAmplitude
value: 0.25
objectReference: {fileID: 0}
- target: {fileID: 330526706382485655, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _joltAmplitude
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 330526706382485655, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _stressSpeedMul
value: 1.8
objectReference: {fileID: 0}
- target: {fileID: 330526706382485655, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _joltSnapFraction
value: 0.25
objectReference: {fileID: 0}
- target: {fileID: 330526706382485655, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _stressJoltFreqMul
value: 5
objectReference: {fileID: 0}
- target: {fileID: 330526706382485655, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _stressAmplitudeMul
value: 2.5
objectReference: {fileID: 0}
- target: {fileID: 330526706382485655, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _joltIntervalRange.x
value: 3
objectReference: {fileID: 0}
- target: {fileID: 330526706382485655, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _joltIntervalRange.y
value: 6
objectReference: {fileID: 0}
- target: {fileID: 986184075687059205, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: m_Enabled
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _runAmplitude
value: 5
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _idleAmplitude
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _runNoiseSpeed
value: 0.6
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _walkAmplitude
value: 3
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _idleNoiseSpeed
value: 0.3
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _stressSpeedMul
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _walkNoiseSpeed
value: 0.4
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _joltSnapFraction
value: 0.1
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _runJoltAmplitude
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _stressJoltAmpMul
value: 1.5
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _idleJoltAmplitude
value: 0.1
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _stressJoltFreqMul
value: 3
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _walkJoltAmplitude
value: 0.3
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _runJoltIntervalRange.x
value: 2
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _runJoltIntervalRange.y
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _idleJoltIntervalRange.x
value: 8
objectReference: {fileID: 0}
- target: {fileID: 1476756471834662335, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _walkJoltIntervalRange.x
value: 4
objectReference: {fileID: 0}
- target: {fileID: 1735456339091181674, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: m_Height
value: 1.4
@@ -530,6 +642,10 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4484393045093388587, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _debugStressValue
value: 1
objectReference: {fileID: 0}
- target: {fileID: 5295567073180248356, guid: 6493d77978c5c1246a0410d1f9204524, type: 3}
propertyPath: _settings.CanRun
value: 0
@@ -1994,6 +2110,10 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 6008373919032892600, guid: 54812b5b482b1f949ac72c69aad0448d, type: 3}
propertyPath: m_IsActive
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6116972887718194121, guid: 54812b5b482b1f949ac72c69aad0448d, type: 3}
propertyPath: m_Name
value: Drape
@@ -2222,6 +2342,10 @@ PrefabInstance:
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2563863626105390945, guid: bb16a243f202765419faa008bf3ff78c, type: 3}
propertyPath: m_IsActive
value: 1
objectReference: {fileID: 0}
- target: {fileID: 3295540326788230531, guid: bb16a243f202765419faa008bf3ff78c, type: 3}
propertyPath: m_Mesh
value:
@@ -2270,6 +2394,10 @@ PrefabInstance:
propertyPath: m_Mesh
value:
objectReference: {fileID: 781800151}
- target: {fileID: 7729080784039828265, guid: bb16a243f202765419faa008bf3ff78c, type: 3}
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8634197476352450901, guid: bb16a243f202765419faa008bf3ff78c, type: 3}
propertyPath: m_LocalScale.x
value: 1139.1224
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0e774c6c7706766428a1bd334a02cba8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,337 @@
using System.Collections;
using UnityEngine;
namespace Unheimlich.World.CameraSystem
{
/// <summary>
/// Overlays a "lost footage / handheld camcorder" shake on a dedicated pivot
/// sitting between CameraPivot and Camera Player.
///
/// Layered in three passes:
/// 1. Continuous Perlin drift — organic idle tremor, scales with movement and stress.
/// 2. Micro-jolts — randomised snap/recoil spikes, more frequent under stress.
/// 3. Inertia swinging — the camera "lags" behind head rotation on all axes,
/// simulating the physical weight of a handheld camcorder.
///
/// Relies on its own dedicated GameObject (LostFootagePivot) so it never conflicts
/// with <see cref="CameraHeadBob"/> which owns the parent pivot's rotation.
/// </summary>
public class LostFootageRig : MonoBehaviour
{
// ── Inspector — Noise ─────────────────────────────────────────────────────
[Header("Noise — idle")]
[SerializeField] private float _idleAmplitude = 0.25f;
[SerializeField] private float _idleNoiseSpeed = 0.6f;
[Header("Noise — walk")]
[SerializeField] private float _walkAmplitude = 0.8f;
[SerializeField] private float _walkNoiseSpeed = 1.4f;
[Header("Noise — run")]
[SerializeField] private float _runAmplitude = 1.8f;
[SerializeField] private float _runNoiseSpeed = 2.5f;
[Header("Noise — stress scale")]
[SerializeField] private float _stressAmplitudeMul = 2.5f;
[SerializeField] private float _stressSpeedMul = 1.8f;
// ── Inspector — Micro-jolts ───────────────────────────────────────────────
[Header("Micro-jolts — idle")]
[SerializeField] private float _idleJoltAmplitude = 0.5f;
[SerializeField] private Vector2 _idleJoltIntervalRange = new Vector2(2f, 4f);
[Header("Micro-jolts — walk")]
[SerializeField] private float _walkJoltAmplitude = 1.2f;
[SerializeField] private Vector2 _walkJoltIntervalRange = new Vector2(0.8f, 2f);
[Header("Micro-jolts — run")]
[SerializeField] private float _runJoltAmplitude = 2.5f;
[SerializeField] private Vector2 _runJoltIntervalRange = new Vector2(0.2f, 0.8f);
[Header("Micro-jolts — shared")]
[SerializeField] private float _joltSnapFraction = 0.5f; // plus rapide que ton 0.25 original
[SerializeField] private float _stressJoltFreqMul = 5f;
[SerializeField] private float _stressJoltAmpMul = 3f;
// ── Inspector — Inertia swing ─────────────────────────────────────────────
[Header("Inertia swing")]
[SerializeField] private float _inertiaStrength = 4f;
[SerializeField] private float _inertiaDamping = 6f;
[SerializeField] private float _inertiaMaxAngle = 8f;
// ── Inspector — Fade ──────────────────────────────────────────────────────
[Header("Fade")]
[SerializeField] private float _fadeDuration = 0.6f;
// ── Inspector — Debug ─────────────────────────────────────────────────────
[Header("Debug")]
[SerializeField] private bool _debugSustainStress;
[SerializeField] [Range(0f, 1f)] private float _debugStressValue;
// ── Runtime state ─────────────────────────────────────────────────────────
/// <summary>Current stress level in [0, 1]. Driven by <see cref="StressController"/>.</summary>
public float StressLevel { get; set; }
/// <summary>Whether the rig is currently active (or fading in/out).</summary>
public bool IsActive { get; private set; }
private float _weight;
private Vector3 _noiseOffset;
private Vector3 _joltRotation;
// Inertia — tracks angular velocity of the parent to derive swing offset.
private Quaternion _lastParentRotation;
private Vector3 _inertiaOffset;
private Coroutine _fadeCoroutine;
private Coroutine _joltCoroutine;
// ── Unity lifecycle ───────────────────────────────────────────────────────
private void Awake()
{
_noiseOffset = new Vector3(
Random.Range(0f, 100f),
Random.Range(0f, 100f),
Random.Range(0f, 100f)
);
}
private void Start()
{
_lastParentRotation = transform.parent != null
? transform.parent.rotation
: transform.rotation;
}
private void Update()
{
if (_debugSustainStress)
StressLevel = _debugStressValue;
if (_weight <= 0f)
{
_UpdateInertia(); // keep tracking even when invisible
return;
}
float stress = Mathf.Clamp01(StressLevel);
// ── 1. Resolve movement state ─────────────────────────────────────────
float horizontalSpeed = 0f;
bool isRunning = false;
if (Unheimlich.Features.Player.PlayerController.Instance?.CharController != null)
{
var vel = Unheimlich.Features.Player.PlayerController.Instance.CharController.velocity;
horizontalSpeed = new Vector3(vel.x, 0f, vel.z).magnitude;
isRunning = OctoCore.Components.OctoPlayer.Instance != null
&& OctoCore.Components.OctoPlayer.Instance.IsRunning;
}
const float WalkSpeedMs = 10f / 3.6f;
const float RunSpeedMs = 20f / 3.6f;
float maxSpeed = isRunning ? RunSpeedMs : WalkSpeedMs;
float moveT = Mathf.Clamp01(horizontalSpeed / maxSpeed);
// ── 2. Interpolate noise parameters from movement state ───────────────
float targetAmp = isRunning
? Mathf.Lerp(_walkAmplitude, _runAmplitude, moveT)
: Mathf.Lerp(_idleAmplitude, _walkAmplitude, moveT);
float targetSpeed = isRunning
? Mathf.Lerp(_walkNoiseSpeed, _runNoiseSpeed, moveT)
: Mathf.Lerp(_idleNoiseSpeed, _walkNoiseSpeed, moveT);
float amp = (targetAmp + stress * _stressAmplitudeMul) * _weight;
float spd = (targetSpeed + stress * _stressSpeedMul);
float t = Time.time * spd;
// ── 3. Perlin drift ───────────────────────────────────────────────────
float rx = (Mathf.PerlinNoise(_noiseOffset.x + t, 0f) - 0.5f) * amp * 2.0f;
float ry = (Mathf.PerlinNoise(_noiseOffset.y + t, 0.5f) - 0.5f) * amp * 1.4f;
float rz = (Mathf.PerlinNoise(_noiseOffset.z + t, 1.0f) - 0.5f) * amp * 0.6f;
// ── 4. Inertia swing ──────────────────────────────────────────────────
_UpdateInertia();
Vector3 swing = _inertiaOffset * _weight;
// ── 5. Compose and apply ──────────────────────────────────────────────
transform.localRotation = Quaternion.Euler(
rx + _joltRotation.x + swing.x,
ry + _joltRotation.y + swing.y,
rz + _joltRotation.z + swing.z
);
}
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>
/// Enables or disables the lost-footage effect with a smooth fade.
/// Safe to call multiple times — an in-progress fade is cancelled and reversed.
/// </summary>
public void SetActive(bool active)
{
if (IsActive == active)
return;
IsActive = active;
if (_fadeCoroutine != null)
StopCoroutine(_fadeCoroutine);
_fadeCoroutine = StartCoroutine(FadeCoroutine(active));
if (active)
{
if (_joltCoroutine != null)
StopCoroutine(_joltCoroutine);
_joltCoroutine = StartCoroutine(JoltCoroutine());
}
}
// ── Private helpers ───────────────────────────────────────────────────────
/// <summary>
/// Computes the inertia swing offset from the delta between the parent's
/// current and previous rotation, then damps it back toward zero.
/// </summary>
private void _UpdateInertia()
{
Quaternion parentRot = transform.parent != null
? transform.parent.rotation
: Quaternion.identity;
Quaternion delta = parentRot * Quaternion.Inverse(_lastParentRotation);
delta.ToAngleAxis(out float angle, out Vector3 axis);
// Wrap angle to [-180, 180] to avoid 360° flips.
if (angle > 180f) angle -= 360f;
// Angular velocity this frame → drive inertia offset.
Vector3 angularVel = axis * (angle / Mathf.Max(Time.deltaTime, 0.0001f));
_inertiaOffset -= angularVel * (_inertiaStrength * Time.deltaTime);
_inertiaOffset = Vector3.ClampMagnitude(_inertiaOffset, _inertiaMaxAngle);
// Damp back toward zero.
_inertiaOffset = Vector3.Lerp(_inertiaOffset, Vector3.zero, _inertiaDamping * Time.deltaTime);
_lastParentRotation = parentRot;
}
private IEnumerator FadeCoroutine(bool fadeIn)
{
float start = _weight;
float target = fadeIn ? 1f : 0f;
float elapsed = 0f;
while (elapsed < _fadeDuration)
{
elapsed += Time.deltaTime;
_weight = Mathf.Lerp(start, target, elapsed / _fadeDuration);
yield return null;
}
_weight = target;
_fadeCoroutine = null;
if (!fadeIn && _joltCoroutine != null)
{
StopCoroutine(_joltCoroutine);
_joltCoroutine = null;
}
}
private IEnumerator JoltCoroutine()
{
while (true)
{
float stress = Mathf.Clamp01(StressLevel);
// Resolve jolt parameters from movement state.
float horizontalSpeed = 0f;
bool isRunning = false;
if (Unheimlich.Features.Player.PlayerController.Instance?.CharController != null)
{
var vel = Unheimlich.Features.Player.PlayerController.Instance.CharController.velocity;
horizontalSpeed = new Vector3(vel.x, 0f, vel.z).magnitude;
isRunning = OctoCore.Components.OctoPlayer.Instance != null
&& OctoCore.Components.OctoPlayer.Instance.IsRunning;
}
const float WalkSpeedMs = 10f / 3.6f;
float maxSpeed = isRunning ? 20f / 3.6f : WalkSpeedMs;
float moveT = Mathf.Clamp01(horizontalSpeed / maxSpeed);
float joltAmp = isRunning
? Mathf.Lerp(_walkJoltAmplitude, _runJoltAmplitude, moveT)
: Mathf.Lerp(_idleJoltAmplitude, _walkJoltAmplitude, moveT);
Vector2 intervalRange = isRunning
? Vector2.Lerp(_walkJoltIntervalRange, _runJoltIntervalRange, moveT)
: Vector2.Lerp(_idleJoltIntervalRange, _walkJoltIntervalRange, moveT);
float interval = Random.Range(intervalRange.x, intervalRange.y);
interval /= Mathf.Lerp(1f, _stressJoltFreqMul, stress);
yield return new WaitForSeconds(interval);
float ampScale = (1f + stress * _stressJoltAmpMul) * _weight;
Vector3 target = new Vector3(
Random.Range(-1f, 1f) * joltAmp * ampScale,
Random.Range(-1f, 1f) * joltAmp * ampScale,
Random.Range(-0.3f, 0.3f) * joltAmp * ampScale
);
yield return StartCoroutine(ApplyJolt(target, interval));
}
}
/// <summary>
/// Snaps to <paramref name="target"/> in the first fraction of the interval,
/// then eases back to zero for the remainder — simulating a hand recoil.
/// </summary>
private IEnumerator ApplyJolt(Vector3 target, float totalDuration)
{
float snapDur = totalDuration * _joltSnapFraction;
float easeDur = totalDuration * (1f - _joltSnapFraction);
float elapsed = 0f;
Vector3 start = _joltRotation;
while (elapsed < snapDur)
{
elapsed += Time.deltaTime;
float t = elapsed / snapDur;
_joltRotation = Vector3.Lerp(start, target, t * t);
yield return null;
}
elapsed = 0f;
start = _joltRotation;
while (elapsed < easeDur)
{
elapsed += Time.deltaTime;
float t = elapsed / easeDur;
_joltRotation = Vector3.Lerp(start, Vector3.zero, t);
yield return null;
}
_joltRotation = Vector3.zero;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 320cde26f18d40b40a31c85e85ee4e59
@@ -0,0 +1,95 @@
using UnityEngine;
namespace Unheimlich.Features.Player
{
/// <summary>
/// Manages the player's stress level and forwards it to <see cref="Unheimlich.World.CameraSystem.LostFootageRig"/>.
/// Stress rises instantly to the requested target and decays back to zero over time when not sustained.
///
/// Intended to sit on <c>OctoPlayerFP</c> alongside other player-level components.
/// Call <see cref="AddStress"/>, <see cref="SetStress"/>, or <see cref="CalmDown"/> from
/// gameplay systems (proximity triggers, jump-scares, scripted events, etc.).
/// </summary>
public class StressController : MonoBehaviour
{
// ── Inspector ─────────────────────────────────────────────────────────────
[Header("References")]
[Tooltip("The LostFootageRig component on CameraPivot.")]
[SerializeField] private Unheimlich.World.CameraSystem.LostFootageRig _rig;
[Header("Decay")]
[Tooltip("Rate at which stress falls back to zero per second when not sustained.")]
[SerializeField] private float _decaySpeed = 0.3f;
[Tooltip("Stress must stay below this threshold for the rig to auto-deactivate.")]
[SerializeField] private float _deactivateThreshold = 0.05f;
[Header("Debug")]
[SerializeField] private bool _debugSustainStress;
[SerializeField] [Range(0f, 1f)] private float _debugStressValue;
// ── Runtime state ─────────────────────────────────────────────────────────
private float _currentStress;
private float _targetStress;
private bool _sustained; // true while an external system holds a target this frame
// ── Unity lifecycle ───────────────────────────────────────────────────────
private void Update()
{
if (_debugSustainStress)
SetStress(_debugStressValue);
_sustained = false;
if (_currentStress > _targetStress)
_currentStress = Mathf.MoveTowards(_currentStress, _targetStress, _decaySpeed * Time.deltaTime);
else
_currentStress = _targetStress;
_targetStress = 0f; // reset each frame; callers must re-assert every Update if sustained
if (_rig == null)
return;
_rig.StressLevel = _currentStress;
// Auto-toggle the rig based on stress level.
if (!_rig.IsActive && _currentStress > _deactivateThreshold)
_rig.SetActive(true);
else if (_rig.IsActive && _currentStress <= _deactivateThreshold)
_rig.SetActive(false);
}
// ── Public API ────────────────────────────────────────────────────────────
/// <summary>
/// Sets a one-shot stress spike that will decay naturally next frame.
/// For sustained stress, call this every frame from a trigger zone or event.
/// </summary>
public void SetStress(float value)
{
_targetStress = Mathf.Max(_targetStress, Mathf.Clamp01(value));
_currentStress = Mathf.Max(_currentStress, _targetStress);
}
/// <summary>Adds <paramref name="delta"/> on top of the current target stress.</summary>
public void AddStress(float delta) => SetStress(_targetStress + delta);
/// <summary>Immediately zeroes stress and deactivates the rig.</summary>
public void CalmDown()
{
_currentStress = 0f;
_targetStress = 0f;
_rig?.SetActive(false);
}
/// <summary>
/// Directly activates or deactivates the rig regardless of stress level.
/// Useful for scripted sequences where you want the effect without the stress system.
/// </summary>
public void ForceRig(bool active) => _rig?.SetActive(active);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b711f35d53cba9d43b8b6f55a40cedbc
+15
View File
@@ -1,5 +1,13 @@
{
"dependencies": {
"com.bezi.sidekick": {
"version": "file:com.bezi.sidekick",
"depth": 0,
"source": "embedded",
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.2.1"
}
},
"com.coiriergraphics.octocore": {
"version": "file:OctoCore",
"depth": 0,
@@ -108,6 +116,13 @@
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.nuget.newtonsoft-json": {
"version": "3.2.2",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.probuilder": {
"version": "6.0.8",
"depth": 0,