Smooth free-camera flight and look

FreeCamController: ease movement via dt-correct Vector3.SmoothDamp on a world-space velocity (accel/decel ramp) and lag the look with Mathf.SmoothDampAngle. Both are framerate-independent and collapse to instant at smoothTime 0; smoothed state is snapped on activation (SyncToCurrentView) so entering freecam never jumps to catch up.

Bundles accumulated deferred work: PlayerScaler uniform-scale helper, scale-aware wall/ceiling climb probes and the double-scale skinned-collider bake fix, plus user content authoring (Sit pose + animation, font SDF, scene/prefab edits).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-27 11:49:43 +02:00
1 parent f66ebbf10a
commit 8c89e95014
14 files changed
+12083 -360

No files matched your search

@@ -736,6 +736,36 @@ MonoBehaviour:
m_Scale: 1
m_AtlasIndex: 0
m_ClassDefinitionType: 0
- m_Index: 24
m_Metrics:
m_Width: 153.29688
m_Height: 219.59375
m_HorizontalBearingX: 9.296875
m_HorizontalBearingY: 216
m_HorizontalAdvance: 178.20312
m_GlyphRect:
m_X: 1202
m_Y: 891
m_Width: 154
m_Height: 220
m_Scale: 1
m_AtlasIndex: 0
m_ClassDefinitionType: 0
- m_Index: 17
m_Metrics:
m_Width: 150.29688
m_Height: 216.29688
m_HorizontalBearingX: 16.5
m_HorizontalBearingY: 216.29688
m_HorizontalAdvance: 178.20312
m_GlyphRect:
m_X: 729
m_Y: 839
m_Width: 151
m_Height: 217
m_Scale: 1
m_AtlasIndex: 0
m_ClassDefinitionType: 0
m_CharacterTable:
- m_ElementType: 1
m_Unicode: 8230
@@ -917,6 +947,14 @@ MonoBehaviour:
m_Unicode: 73
m_GlyphIndex: 40
m_Scale: 1
- m_ElementType: 1
m_Unicode: 57
m_GlyphIndex: 24
m_Scale: 1
- m_ElementType: 1
m_Unicode: 50
m_GlyphIndex: 17
m_Scale: 1
m_AtlasTextures:
- {fileID: 2664084111089754775}
m_AtlasTextureIndex: 0
@@ -1104,6 +1142,14 @@ MonoBehaviour:
m_Y: 632
m_Width: 74
m_Height: 252
- m_X: 1187
m_Y: 876
m_Width: 183
m_Height: 249
- m_X: 714
m_Y: 824
m_Width: 180
m_Height: 246
m_FreeGlyphRects:
- m_X: 415
m_Y: 45
@@ -1221,10 +1267,6 @@ MonoBehaviour:
m_Y: 804
m_Width: 144
m_Height: 11
- m_X: 714
m_Y: 804
m_Width: 44
m_Height: 1243
- m_X: 1221
m_Y: 492
m_Width: 28
@@ -1233,18 +1275,6 @@ MonoBehaviour:
m_Y: 576
m_Width: 85
m_Height: 165
- m_X: 1187
m_Y: 741
m_Width: 34
m_Height: 1306
- m_X: 714
m_Y: 993
m_Width: 1333
m_Height: 1054
- m_X: 714
m_Y: 824
m_Width: 238
m_Height: 1223
- m_X: 343
m_Y: 867
m_Width: 10
@@ -1257,10 +1287,6 @@ MonoBehaviour:
m_Y: 1119
m_Width: 175
m_Height: 928
- m_X: 532
m_Y: 1064
m_Width: 1515
m_Height: 983
- m_X: 532
m_Y: 936
m_Width: 2
@@ -1274,13 +1300,41 @@ MonoBehaviour:
m_Width: 556
m_Height: 1292
- m_X: 1187
m_Y: 741
m_Width: 34
m_Height: 135
- m_X: 532
m_Y: 1125
m_Width: 1515
m_Height: 922
- m_X: 1370
m_Y: 884
m_Width: 860
m_Width: 677
m_Height: 1163
- m_X: 1187
- m_X: 1370
m_Y: 876
m_Width: 230
m_Width: 47
m_Height: 1171
- m_X: 714
m_Y: 804
m_Width: 44
m_Height: 20
- m_X: 894
m_Y: 824
m_Width: 58
m_Height: 1223
- m_X: 894
m_Y: 993
m_Width: 293
m_Height: 1054
- m_X: 532
m_Y: 1070
m_Width: 655
m_Height: 977
- m_X: 532
m_Y: 1064
m_Width: 182
m_Height: 983
m_FontFeatureTable:
m_MultipleSubstitutionRecords: []
m_LigatureSubstitutionRecords:
@@ -13918,7 +13972,7 @@ Texture2D:
m_ColorSpace: 1
m_PlatformBlob:
image data: 4194304
_typelessdata: 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000204050506070809090807060505040301000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103040505060708090908070605050402000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020405050607080909080706050504030100000000000000000000000000000000000000000000000000000000000000000000000000000003050607080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808Line truncated
_typelessdata: 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000204050506070809090807060505040301000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103040505060708090908070605050402000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020405050607080909080706050504030100000000000000000000000000000000000000000000000000000000000000000000000000000003050607080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808Line truncated
m_StreamData:
serializedVersion: 2
offset: 0
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8080490af5235b043abfca704be7ff8d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large. Load diff
+24
View File
@@ -0,0 +1,24 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e3e6fee2893b9d848b8b1ccca743c14f, type: 3}
m_Name: Pose_Sit
m_EditorClassIdentifier: Assembly-CSharp::PlayerPose
clip: {fileID: 7400000, guid: 8080490af5235b043abfca704be7ff8d, type: 2}
clipSpeed: 1
walkClip: {fileID: 0}
walkClipSpeed: 1
runClip: {fileID: 0}
runClipSpeed: 1
controllerCenterY: 0.45
controllerRadius: 0.1
controllerHeight: 0.85
cameraTargetYOffset: -0.2
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 90df501d86681394b834f647f6f4e939
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+15
View File
@@ -13,6 +13,7 @@ GameObject:
- component: {fileID: 1035623159758896108}
- component: {fileID: 4587432968399885263}
- component: {fileID: 3810343734243857883}
- component: {fileID: 273411532115212066}
m_Layer: 0
m_Name: Player
m_TagString: Player
@@ -159,6 +160,20 @@ MonoBehaviour:
exposedColor: {r: 0.2, g: 1, b: 0.3, a: 0.25}
embeddedColor: {r: 1, g: 0.25, b: 0.2, a: 0.5}
visiblePercent: 0
--- !u!114 &273411532115212066
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5384024074013944205}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 26105206120a119419bee8bb5f810902, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::PlayerScaler
scale: 1
scaleJumpAndGravity: 1
--- !u!1 &6840099669803454162
GameObject:
m_ObjectHideFlags: 0
+51 -11
View File
@@ -579,9 +579,9 @@ MonoBehaviour:
invertY: 0
rotationSmoothing: 20
enableZoom: 1
zoomSpeed: 0.5
zoomSpeed: 0.25
zoomSmoothing: 10
minRadius: 1.5
minRadius: 0.5
maxRadius: 8
manageCursor: 1
--- !u!114 &172071432
@@ -767,7 +767,7 @@ Transform:
m_GameObject: {fileID: 172071430}
serializedVersion: 2
m_LocalRotation: {x: 0.1305262, y: 2.1996925e-11, z: -4.1198815e-12, w: 0.9914449}
m_LocalPosition: {x: 0, y: 1.9146858, z: -4.3466663}
m_LocalPosition: {x: 0, y: 1.5396858, z: -4.3466663}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -1480,7 +1480,7 @@ Transform:
m_GameObject: {fileID: 330585543}
serializedVersion: 2
m_LocalRotation: {x: 0.1305262, y: 2.1832952e-11, z: -2.8743629e-12, w: 0.9914449}
m_LocalPosition: {x: 0, y: 1.9146858, z: -4.3466663}
m_LocalPosition: {x: 0, y: 1.5396858, z: -4.3466663}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -4917,10 +4917,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 5ca7cff7542b53b498f0a9834786b9e0, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::FreeCamController
moveSpeed: 6
sprintMultiplier: 3
moveSpeed: 4
sprintMultiplier: 2
horizontalSensitivity: 0.1
verticalSensitivity: 0.07
verticalSensitivity: 0.075
invertY: 0
maxPitch: 89
activePriority: 100
@@ -4943,13 +4943,13 @@ CharacterController:
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 3
m_Height: 1.3
m_Height: 0.21
m_Radius: 0.1
m_SlopeLimit: 59
m_StepOffset: 0.15
m_StepOffset: 0.1
m_SkinWidth: 0.01
m_MinMoveDistance: 0.001
m_Center: {x: 0, y: -0.09, z: 0}
m_Center: {x: 0, y: 0, z: 0}
--- !u!114 &1282734306
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -7684,10 +7684,50 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 273411532115212066, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: scale
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 1514890176713383521, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_Enabled
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4587432968399885263, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: poses.Array.size
value: 5
objectReference: {fileID: 0}
- target: {fileID: 4587432968399885263, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: 'poses.Array.data[2]'
value:
objectReference: {fileID: 11400000, guid: 90df501d86681394b834f647f6f4e939, type: 2}
- target: {fileID: 4587432968399885263, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: 'poses.Array.data[3]'
value:
objectReference: {fileID: 11400000, guid: c5cf9d420554e5e4e96d3bee1903967a, type: 2}
- target: {fileID: 4587432968399885263, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: 'poses.Array.data[4]'
value:
objectReference: {fileID: 11400000, guid: 1545cfba9b5e06d41ae72c7ec766baaf, type: 2}
- target: {fileID: 5280718916654756701, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_IsActive
value: 1
objectReference: {fileID: 0}
- target: {fileID: 5384024074013944205, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_Name
value: Player
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalScale.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalScale.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalScale.z
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalPosition.x
value: 0
@@ -7741,6 +7781,7 @@ SceneRoots:
- {fileID: 2009475134}
- {fileID: 330585546}
- {fileID: 172071436}
- {fileID: 1282734307}
- {fileID: 832575519}
- {fileID: 410087041}
- {fileID: 2025317892}
@@ -7748,4 +7789,3 @@ SceneRoots:
- {fileID: 686187287}
- {fileID: 489277934}
- {fileID: 347639557}
- {fileID: 1282734307}
+46 -3
View File
@@ -39,6 +39,15 @@ public class FreeCamController : MonoBehaviour
[Range(1f, 89.9f)]
public float maxPitch = 89f;
[Header("Smoothing")]
[Tooltip("Acceleration/deceleration smoothing for flight, in seconds. 0 = instant (snappy starts/stops); higher = floatier ramp-up and coast-down.")]
[Range(0f, 1f)]
public float moveSmoothTime = 0.12f;
[Tooltip("Look smoothing, in seconds. 0 = raw mouse (instant); small values take the edge off jitter without feeling laggy.")]
[Range(0f, 0.3f)]
public float lookSmoothTime = 0.04f;
[Header("Camera")]
[Tooltip("Priority the dedicated CinemachineCamera takes while active (must beat the gameplay camera).")]
public int activePriority = 100;
@@ -48,8 +57,14 @@ public class FreeCamController : MonoBehaviour
CharacterController _controller;
CinemachineCamera _vcam;
float _yaw;
float _yaw; // look target (raw input accumulates here)
float _pitch;
float _displayYaw; // smoothed value actually applied to the transform
float _displayPitch;
float _yawSmoothRef;
float _pitchSmoothRef;
Vector3 _moveVelocity; // smoothed world-space flight velocity
Vector3 _moveVelocityRef;
bool _ignoredPlayer;
void Awake()
@@ -98,8 +113,21 @@ public class FreeCamController : MonoBehaviour
_yaw += delta.x * horizontalSensitivity;
float vs = verticalSensitivity * (invertY ? 1f : -1f);
_pitch = Mathf.Clamp(_pitch + delta.y * vs, -maxPitch, maxPitch);
if (lookSmoothTime > 0f)
{
// SmoothDampAngle is frame-rate-independent and wraps yaw correctly.
_displayYaw = Mathf.SmoothDampAngle(_displayYaw, _yaw, ref _yawSmoothRef, lookSmoothTime);
_displayPitch = Mathf.SmoothDampAngle(_displayPitch, _pitch, ref _pitchSmoothRef, lookSmoothTime);
}
else
{
_displayYaw = _yaw;
_displayPitch = _pitch;
}
// Rotation is unconstrained by the CharacterController; only position is.
transform.rotation = Quaternion.Euler(_pitch, _yaw, 0f);
transform.rotation = Quaternion.Euler(_displayPitch, _displayYaw, 0f);
}
void MoveTick()
@@ -109,7 +137,14 @@ public class FreeCamController : MonoBehaviour
dir.Normalize();
float speed = moveSpeed * (SprintHeld() ? sprintMultiplier : 1f);
_controller.Move(dir * (speed * Time.deltaTime));
Vector3 targetVelocity = dir * speed;
// SmoothDamp eases accel/decel and is already dt-correct; 0 collapses to instant.
_moveVelocity = moveSmoothTime > 0f
? Vector3.SmoothDamp(_moveVelocity, targetVelocity, ref _moveVelocityRef, moveSmoothTime)
: targetVelocity;
_controller.Move(_moveVelocity * Time.deltaTime);
}
/// <summary>Place the rig at the currently rendered view so going live produces no visible jump.</summary>
@@ -128,6 +163,14 @@ public class FreeCamController : MonoBehaviour
_yaw = e.y;
float p = e.x > 180f ? e.x - 360f : e.x;
_pitch = Mathf.Clamp(p, -maxPitch, maxPitch);
// Snap the smoothed state to the fresh pose so entering freecam never lags/jumps to catch up.
_displayYaw = _yaw;
_displayPitch = _pitch;
_yawSmoothRef = 0f;
_pitchSmoothRef = 0f;
_moveVelocity = Vector3.zero;
_moveVelocityRef = Vector3.zero;
}
/// <summary>Stop the freecam capsule from being blocked by the (frozen) player's capsule.</summary>
+29 -16
View File
@@ -636,7 +636,7 @@ public partial class PlayerCharacterController : MonoBehaviour
if (!found)
{
// Wall only: losing the center ray while climbing up the face = top edge → mantle.
if (!onCeiling && alongSurface.y > 0f && WallInFront(_controller.height * 0.5f))
if (!onCeiling && alongSurface.y > 0f && WallInFront(WorldHalfHeight))
{
_mantling = true;
_mantleClearedFeet = false;
@@ -695,8 +695,8 @@ public partial class PlayerCharacterController : MonoBehaviour
// longer moves the capsule (only the visual mesh), so it always rests against the wall.
// The controller's depenetration never fights back, so there's no buzz and no quantizing
// deadzone is needed. Exponential follow keeps it framerate-stable; clamp guards spikes.
const float maxPerpSpeed = 8f;
float gapError = _controller.skinWidth - gap;
float maxPerpSpeed = 8f * CapsuleScale;
float gapError = _controller.skinWidth * CapsuleScale - gap;
float follow = 1f - Mathf.Exp(-wallOffsetFollow * dt);
velocity += _wallNormal * Mathf.Clamp(gapError * follow / dt, -maxPerpSpeed, maxPerpSpeed);
}
@@ -877,6 +877,19 @@ public partial class PlayerCharacterController : MonoBehaviour
cameraTarget.localPosition = _cameraTargetBaseLocalPos + Vector3.up * y;
}
// The CharacterController's serialized center/radius/height are authored at the player's base
// (scale-1) size; transform.localScale resizes the ACTUAL collision capsule via lossyScale but
// leaves those serialized numbers untouched. Every climb probe below raycasts in WORLD space, so
// it must use the scaled (world) capsule dimensions and scale its world-distance paddings too --
// otherwise the wall grab range, head-to-ceiling gap and mantle distance are all measured at the
// authored size on a shrunk/grown player (PlayerScaler scales the tunables but not these probes).
// Uniform player scale is enforced by PlayerScaler, so a single lossyScale component suffices.
private float CapsuleScale => transform.lossyScale.y;
private Vector3 WorldCapsuleCenter => transform.TransformPoint(_controller.center);
private float WorldRadius => _controller.radius * CapsuleScale;
private float WorldHalfHeight => _controller.height * 0.5f * CapsuleScale;
private const float ProbeReachSlack = 0.5f; // extra ray length beyond the capsule surface (scale-1 units)
/// <summary>
/// Raycasts from the character center toward the surface (along -_wallNormal) to find the
/// current normal and the gap (surface clearance) to it. The capsule's support distance
@@ -888,10 +901,10 @@ public partial class PlayerCharacterController : MonoBehaviour
normal = _wallNormal;
gap = 0f;
Vector3 origin = transform.position + _controller.center;
Vector3 origin = WorldCapsuleCenter;
Vector3 dir = -_wallNormal;
float extent = CapsuleExtent(dir);
float maxDist = extent + wallMaxOffset + 0.5f;
float maxDist = extent + wallMaxOffset + ProbeReachSlack * CapsuleScale;
int count = Physics.RaycastNonAlloc(
origin, dir, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
@@ -921,8 +934,8 @@ public partial class PlayerCharacterController : MonoBehaviour
/// </summary>
private float CapsuleExtent(Vector3 dir)
{
float radius = _controller.radius;
return radius + Mathf.Abs(dir.y) * (_controller.height * 0.5f - radius);
float radius = WorldRadius;
return radius + Mathf.Abs(dir.y) * (WorldHalfHeight - radius);
}
/// <summary>
@@ -934,9 +947,9 @@ public partial class PlayerCharacterController : MonoBehaviour
normal = Vector3.down;
gap = 0f;
Vector3 origin = transform.position + _controller.center;
float half = _controller.height * 0.5f;
float maxDist = half + wallMaxOffset + 0.5f;
Vector3 origin = WorldCapsuleCenter;
float half = WorldHalfHeight;
float maxDist = half + wallMaxOffset + ProbeReachSlack * CapsuleScale;
int count = Physics.RaycastNonAlloc(
origin, Vector3.up, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
@@ -980,7 +993,7 @@ public partial class PlayerCharacterController : MonoBehaviour
{
// Phase 1: climb straight up until no wall is left in front of the feet.
_controller.Move(Vector3.up * (speed * dt));
if (!WallInFront(_controller.height * 0.5f))
if (!WallInFront(WorldHalfHeight))
_mantleClearedFeet = true;
}
else
@@ -988,7 +1001,7 @@ public partial class PlayerCharacterController : MonoBehaviour
// Phase 2: move forward onto the top surface, then finish.
_controller.Move(forward * (speed * dt));
_mantleOverDist += speed * dt;
if (_mantleOverDist >= _controller.radius * 2f + 0.15f || _controller.isGrounded)
if (_mantleOverDist >= WorldRadius * 2f + 0.15f * CapsuleScale || _controller.isGrounded)
{
_reentryCooldown = TopOutRegrabCooldown;
ExitWallClimb();
@@ -1010,8 +1023,8 @@ public partial class PlayerCharacterController : MonoBehaviour
/// </summary>
private bool WallInFront(float downOffset)
{
Vector3 origin = transform.position + _controller.center - Vector3.up * downOffset;
float maxDist = _controller.radius + wallMaxOffset + 0.5f;
Vector3 origin = WorldCapsuleCenter - Vector3.up * downOffset;
float maxDist = WorldRadius + wallMaxOffset + ProbeReachSlack * CapsuleScale;
int count = Physics.RaycastNonAlloc(
origin, -_wallNormal, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
@@ -1032,8 +1045,8 @@ public partial class PlayerCharacterController : MonoBehaviour
/// </summary>
private bool WalkableGroundBelow()
{
Vector3 origin = transform.position + _controller.center;
float maxDist = _controller.center.y + _controller.stepOffset;
Vector3 origin = WorldCapsuleCenter;
float maxDist = (_controller.center.y + _controller.stepOffset) * CapsuleScale;
int count = Physics.RaycastNonAlloc(
origin, Vector3.down, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
+116
View File
@@ -0,0 +1,116 @@
using UnityEngine;
/// <summary>
/// One knob that resizes the whole player. Setting <see cref="Scale"/> drives <c>transform.localScale</c>
/// — which Unity already propagates to the visible mesh, the CharacterController capsule, the per-pose
/// capsules (written as local values) and the camera target — and additionally rescales the movement,
/// jump, slide and wall tunables on <see cref="PlayerCharacterController"/> proportionally, so a smaller
/// or larger player still moves at the same body-relative pace instead of zipping or crawling. Without
/// this the speeds (units/second, not geometry) stay put and a half-size player feels twice as fast.
///
/// The authored values on the controller ARE the scale-1 design values: tune them normally there. They are
/// captured as the base in Awake and multiplied by Scale at runtime, so the serialized design values are
/// never overwritten on disk. In the editor only <c>localScale</c> is previewed live (geometry is the only
/// visual part); the speed rescale happens on play. The skinned mesh collider scales correctly because
/// <see cref="SkinnedColliderBaker"/> divides the player scale back out of its bake.
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(PlayerCharacterController))]
public class PlayerScaler : MonoBehaviour
{
[Tooltip("Uniform size multiplier for the whole player. 1 = authored size. Drives transform.localScale " +
"(mesh, CharacterController capsule, per-pose capsules and camera target follow automatically) " +
"and rescales the controller's movement/jump/slide/wall speeds proportionally.")]
[Min(0.01f)]
[SerializeField] private float scale = 1f;
[Tooltip("Also scale jump height, gravity and the wall top-out hop so the motion is geometrically " +
"self-similar: identical trajectory shape and air-time, just smaller/larger. Off keeps the " +
"authored gravity, giving a small player a floatier, relatively higher jump.")]
[SerializeField] private bool scaleJumpAndGravity = true;
/// <summary>Current uniform scale. Use <see cref="SetScale"/> to change it at runtime.</summary>
public float Scale => scale;
private PlayerCharacterController _pcc;
private bool _captured;
// Authored (scale-1) base values, snapshotted once from the controller in Awake. All are world-unit
// scalars (units, units/s, units/s^2) that do NOT follow transform.localScale; dimensionless ratios
// (sprint/climb multipliers), angular rates (rotation/turn speed) and times (exit grace) are left
// untouched — they are already scale-invariant.
private float _bMove, _bAccel, _bDecel, _bStick, _bSlideAcc, _bMaxSlide;
private float _bWallOffset, _bWallInset, _bWallReturn, _bWallSens, _bCeilGrab;
private float _bJump, _bGravity, _bTopOut;
void Awake()
{
_pcc = GetComponent<PlayerCharacterController>();
CaptureBase();
ApplyScale(scale);
}
/// <summary>Sets the scale at runtime (e.g. a grow/shrink mechanic) and re-applies from the base values.</summary>
public void SetScale(float newScale)
{
scale = Mathf.Max(0.01f, newScale);
if (!_pcc) _pcc = GetComponent<PlayerCharacterController>();
if (!_captured) CaptureBase();
ApplyScale(scale);
}
private void CaptureBase()
{
if (!_pcc) return;
_bMove = _pcc.moveSpeed;
_bAccel = _pcc.acceleration;
_bDecel = _pcc.deceleration;
_bStick = _pcc.groundedStickVelocity;
_bSlideAcc = _pcc.slideAcceleration;
_bMaxSlide = _pcc.maxSlideSpeed;
_bWallOffset = _pcc.wallMaxOffset;
_bWallInset = _pcc.wallMaxInset;
_bWallReturn = _pcc.wallOffsetReturnSpeed;
_bWallSens = _pcc.wallOffsetSensitivity;
_bCeilGrab = _pcc.ceilingGrabDistance;
_bJump = _pcc.jumpHeight;
_bGravity = _pcc.gravity;
_bTopOut = _pcc.wallTopOutBoost;
_captured = true;
}
private void ApplyScale(float s)
{
transform.localScale = Vector3.one * s;
if (!_pcc) return;
_pcc.moveSpeed = _bMove * s;
_pcc.acceleration = _bAccel * s;
_pcc.deceleration = _bDecel * s;
_pcc.groundedStickVelocity = _bStick * s;
_pcc.slideAcceleration = _bSlideAcc * s;
_pcc.maxSlideSpeed = _bMaxSlide * s;
_pcc.wallMaxOffset = _bWallOffset * s;
_pcc.wallMaxInset = _bWallInset * s;
_pcc.wallOffsetReturnSpeed = _bWallReturn * s;
_pcc.wallOffsetSensitivity = _bWallSens * s;
_pcc.ceilingGrabDistance = _bCeilGrab * s;
if (scaleJumpAndGravity)
{
_pcc.jumpHeight = _bJump * s;
_pcc.gravity = _bGravity * s;
_pcc.wallTopOutBoost = _bTopOut * s;
}
}
#if UNITY_EDITOR
void OnValidate()
{
// Live geometry preview in edit mode only; the speed rescale runs at play time from the authored
// base so the design values on the controller are never serialized in their scaled form.
if (!Application.isPlaying)
transform.localScale = Vector3.one * Mathf.Max(0.01f, scale);
}
#endif
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 26105206120a119419bee8bb5f810902
+31
View File
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
@@ -30,6 +31,7 @@ public class SkinnedColliderBaker : MonoBehaviour
private MeshCollider _collider;
private Mesh _baked;
private float _timer;
private readonly List<Vector3> _vbuf = new List<Vector3>(); // reused vertex scratch so the scale fixup allocates nothing per bake
private void Awake()
{
@@ -62,10 +64,39 @@ public class SkinnedColliderBaker : MonoBehaviour
{
if (!_skinned) return;
_skinned.BakeMesh(_baked);
// BakeMesh freezes the bone deformation — which already carries this transform's world scale —
// into the mesh. The MeshCollider below then applies the same lossyScale a SECOND time, so a
// scaled player (see PlayerScaler) would get a collider half/double the size of the visible
// mesh. Divide the scale back out so the baked mesh is true scale-1 local geometry (the contract
// this class already documents): the MeshCollider re-applies it exactly once (matching the
// renderer), and MeshPainter / ConcealmentMeter — which raise the baked mesh to world via
// localToWorldMatrix — stay correct at any player scale. No-op (and untouched) at scale 1; the
// uniform divide is rotation-independent, which the player's uniform scale guarantees.
Vector3 ls = transform.lossyScale;
if (ls.x != 1f || ls.y != 1f || ls.z != 1f)
RescaleToUnitScale(ls);
_collider.sharedMesh = null; // force a physics re-cook of the new geometry
_collider.sharedMesh = _baked;
}
private void RescaleToUnitScale(Vector3 lossyScale)
{
float ix = lossyScale.x != 0f ? 1f / lossyScale.x : 0f;
float iy = lossyScale.y != 0f ? 1f / lossyScale.y : 0f;
float iz = lossyScale.z != 0f ? 1f / lossyScale.z : 0f;
_baked.GetVertices(_vbuf);
for (int i = 0; i < _vbuf.Count; i++)
{
Vector3 v = _vbuf[i];
v.x *= ix; v.y *= iy; v.z *= iz;
_vbuf[i] = v;
}
_baked.SetVertices(_vbuf);
_baked.RecalculateBounds();
}
private void OnDestroy()
{
if (_baked) Destroy(_baked);