From 792c8c964fec2e5a11ff310a80a3955e2d7c6f15 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Mon, 22 Jun 2026 13:01:50 +0200 Subject: [PATCH] Add WASD character controller with jump and slope sliding - Add PlayerCharacterController script: CharacterController-based movement (camera-relative WASD), jump, gravity, rotate-toward-movement and slope sliding so the player slides off surfaces/edges steeper than the slope limit instead of balancing on them. - Update SampleScene: add Player (with CharacterController), ground plane and obstacle cubes; parent the paintable paintman_UV under the Player. Co-Authored-By: Claude Opus 4.8 --- Assets/Scenes/SampleScene.unity | 445 +++++++++++++++++- Assets/Scripts/PlayerCharacterController.cs | 212 +++++++++ .../Scripts/PlayerCharacterController.cs.meta | 2 + 3 files changed, 649 insertions(+), 10 deletions(-) create mode 100644 Assets/Scripts/PlayerCharacterController.cs create mode 100644 Assets/Scripts/PlayerCharacterController.cs.meta diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 8d5ec2e..47b19e4 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -119,6 +119,118 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} +--- !u!1 &200502118 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 200502122} + - component: {fileID: 200502121} + - component: {fileID: 200502120} + - component: {fileID: 200502119} + m_Layer: 0 + m_Name: Cube (1) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!65 &200502119 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200502118} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &200502120 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200502118} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + 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: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + 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_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &200502121 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200502118} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &200502122 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200502118} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1.29, y: 0.5, z: -2.5} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &330585543 GameObject: m_ObjectHideFlags: 0 @@ -205,13 +317,13 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 330585543} serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0.67, z: -1.3} + m_LocalRotation: {x: 0.3420201, y: 0, z: 0, w: 0.9396927} + m_LocalPosition: {x: 0, y: 4.43, z: -7.8} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_LocalEulerAnglesHint: {x: 40, y: 0, z: 0} --- !u!114 &330585547 MonoBehaviour: m_ObjectHideFlags: 0 @@ -410,13 +522,126 @@ MonoBehaviour: updateColliderEveryFrame: 0 brushShader: {fileID: 0} dilateShader: {fileID: 0} +--- !u!1 &721670969 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 721670973} + - component: {fileID: 721670972} + - component: {fileID: 721670971} + - component: {fileID: 721670970} + m_Layer: 0 + m_Name: Plane + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!64 &721670970 +MeshCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 721670969} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 5 + m_Convex: 0 + m_CookingOptions: 30 + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &721670971 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 721670969} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + 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: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + 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_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &721670972 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 721670969} + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &721670973 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 721670969} + 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: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1001 &821516643 PrefabInstance: m_ObjectHideFlags: 0 serializedVersion: 2 m_Modification: serializedVersion: 3 - m_TransformParent: {fileID: 0} + m_TransformParent: {fileID: 1141039639} m_Modifications: - target: {fileID: -8679921383154817045, guid: 554c0bdf86263754d862d7effd04045b, type: 3} propertyPath: m_LocalPosition.x @@ -440,11 +665,11 @@ PrefabInstance: objectReference: {fileID: 0} - target: {fileID: -8679921383154817045, guid: 554c0bdf86263754d862d7effd04045b, type: 3} propertyPath: m_LocalRotation.y - value: 0 + value: -0 objectReference: {fileID: 0} - target: {fileID: -8679921383154817045, guid: 554c0bdf86263754d862d7effd04045b, type: 3} propertyPath: m_LocalRotation.z - value: 0 + value: -0 objectReference: {fileID: 0} - target: {fileID: -8679921383154817045, guid: 554c0bdf86263754d862d7effd04045b, type: 3} propertyPath: m_LocalEulerAnglesHint.x @@ -519,6 +744,206 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &963088878 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 963088882} + - component: {fileID: 963088881} + - component: {fileID: 963088880} + - component: {fileID: 963088879} + m_Layer: 0 + m_Name: Cube + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!65 &963088879 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 963088878} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &963088880 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 963088878} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + 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: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + 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_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &963088881 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 963088878} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &963088882 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 963088878} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 4, y: 0.5, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1141039638 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1141039639} + - component: {fileID: 1141039641} + - component: {fileID: 1141039642} + m_Layer: 0 + m_Name: Player + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1141039639 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1141039638} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1944972203} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!143 &1141039641 +CharacterController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1141039638} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Height: 1.35 + m_Radius: 0.2 + m_SlopeLimit: 45 + m_StepOffset: 0.3 + m_SkinWidth: 0.02 + m_MinMoveDistance: 0.001 + m_Center: {x: 0, y: 0.675, z: 0} +--- !u!114 &1141039642 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1141039638} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 463b6be548abf464287096e1e92fcd01, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::PlayerCharacterController + moveSpeed: 4 + cameraRelative: 1 + cameraTransform: {fileID: 0} + rotateTowardMovement: 1 + rotationSpeed: 720 + jumpHeight: 1.2 + gravity: -19.62 + groundedStickVelocity: -2 + slideDownSteepSlopes: 1 + slideAcceleration: 20 + maxSlideSpeed: 8 + slideControl: 0.3 +--- !u!4 &1944972203 stripped +Transform: + m_CorrespondingSourceObject: {fileID: -8679921383154817045, guid: 554c0bdf86263754d862d7effd04045b, type: 3} + m_PrefabInstance: {fileID: 821516643} + m_PrefabAsset: {fileID: 0} --- !u!1 &2009475132 GameObject: m_ObjectHideFlags: 0 @@ -552,9 +977,6 @@ MonoBehaviour: targetFps: 60 disableVSyncWhenLimiting: 1 vSyncCountWhenUnlimited: 0 - toggleLimitKey: 94 - cyclePresetKey: 95 - presets: 1e0000003c0000007800000090000000 --- !u!4 &2009475134 Transform: m_ObjectHideFlags: 0 @@ -578,4 +1000,7 @@ SceneRoots: - {fileID: 330585546} - {fileID: 410087041} - {fileID: 832575519} - - {fileID: 821516643} + - {fileID: 1141039639} + - {fileID: 721670973} + - {fileID: 963088882} + - {fileID: 200502122} diff --git a/Assets/Scripts/PlayerCharacterController.cs b/Assets/Scripts/PlayerCharacterController.cs new file mode 100644 index 0000000..7e1c07b --- /dev/null +++ b/Assets/Scripts/PlayerCharacterController.cs @@ -0,0 +1,212 @@ +using UnityEngine; +#if ENABLE_INPUT_SYSTEM +using UnityEngine.InputSystem; +#endif + +/// +/// Simple first/third-person style character controller driven by a +/// : WASD for horizontal movement and Space to jump. +/// +/// Movement can be camera-relative (W moves away from the camera) so it feels +/// natural with the fixed scene camera. Gravity and jumping are integrated +/// manually because CharacterController does not apply physics on its own. +/// +/// The script reads input through the new Input System (Keyboard.current) when it +/// is enabled, and falls back to the legacy Input Manager otherwise. The project +/// currently runs the new Input System only. +/// +/// Attach this to the Player GameObject (the one that owns the CharacterController). +/// +/// Note: the class is intentionally named PlayerCharacterController to avoid a +/// name clash with Unity.VisualScripting's example "PlayerController" type. +/// +[RequireComponent(typeof(CharacterController))] +[DisallowMultipleComponent] +public class PlayerCharacterController : MonoBehaviour +{ + [Header("Movement")] + [Tooltip("Horizontal movement speed in units per second.")] + public float moveSpeed = 4f; + + [Tooltip("If ON, WASD is interpreted relative to the camera (W = away from " + + "the camera). If OFF, WASD moves along the world X/Z axes.")] + public bool cameraRelative = true; + + [Tooltip("Camera used for camera-relative movement. Empty = Camera.main.")] + public Transform cameraTransform; + + [Header("Rotation")] + [Tooltip("If ON, the character smoothly turns to face its movement direction.")] + public bool rotateTowardMovement = true; + + [Tooltip("Turn speed in degrees per second (used when 'Rotate Toward Movement' is on).")] + public float rotationSpeed = 720f; + + [Header("Jump & Gravity")] + [Tooltip("Peak jump height in units.")] + public float jumpHeight = 1.2f; + + [Tooltip("Gravity in units per second squared (negative = downward).")] + public float gravity = -19.62f; + + [Tooltip("Small downward velocity kept while grounded so the controller " + + "stays firmly on the ground (and isGrounded stays reliable).")] + public float groundedStickVelocity = -2f; + + [Header("Slope Sliding")] + [Tooltip("If ON, the character slides down surfaces steeper than the " + + "CharacterController's Slope Limit (and off sharp edges/corners) " + + "instead of balancing on them.")] + public bool slideDownSteepSlopes = true; + + [Tooltip("How quickly the slide accelerates while on a too-steep surface " + + "(units per second squared).")] + public float slideAcceleration = 20f; + + [Tooltip("Maximum downhill slide speed (units per second).")] + public float maxSlideSpeed = 8f; + + [Tooltip("How much steering control the player keeps while sliding " + + "(0 = none, 1 = full normal control).")] + [Range(0f, 1f)] + public float slideControl = 0.3f; + + private CharacterController _controller; + private float _verticalVelocity; + + // Ground contact normal collected from the latest Move (used for slope sliding). + private Vector3 _groundNormal = Vector3.up; + private bool _groundNormalUpdated; + private float _slideSpeed; + + void Awake() + { + _controller = GetComponent(); + if (cameraTransform == null && Camera.main != null) + cameraTransform = Camera.main.transform; + } + + void Update() + { + // --- Read input ----------------------------------------------------- + float h = 0f, v = 0f; + bool jumpPressed = false; + +#if ENABLE_INPUT_SYSTEM + var keyboard = Keyboard.current; + if (keyboard != null) + { + if (keyboard.aKey.isPressed) h -= 1f; + if (keyboard.dKey.isPressed) h += 1f; + if (keyboard.sKey.isPressed) v -= 1f; + if (keyboard.wKey.isPressed) v += 1f; + jumpPressed = keyboard.spaceKey.wasPressedThisFrame; + } +#else + h = Input.GetAxisRaw("Horizontal"); + v = Input.GetAxisRaw("Vertical"); + jumpPressed = Input.GetButtonDown("Jump"); +#endif + + // --- Build the horizontal move direction ---------------------------- + Vector3 inputDir = new Vector3(h, 0f, v); + if (inputDir.sqrMagnitude > 1f) + inputDir.Normalize(); + + Vector3 moveDir; + if (cameraRelative && cameraTransform != null) + { + // Project the camera axes onto the ground plane so movement stays horizontal. + Vector3 forward = cameraTransform.forward; + forward.y = 0f; + forward.Normalize(); + Vector3 right = cameraTransform.right; + right.y = 0f; + right.Normalize(); + moveDir = forward * inputDir.z + right * inputDir.x; + } + else + { + moveDir = inputDir; + } + + // --- Evaluate the ground slope (for sliding) ------------------------ + // The ground normal is gathered in OnControllerColliderHit during the + // previous Move. If it is steeper than the controller's Slope Limit, the + // surface is too steep to stand on (this also covers balancing on a sharp + // box edge/corner, whose contact normal is steep). + float slopeAngle = Vector3.Angle(_groundNormal, Vector3.up); + bool onSteepSlope = slideDownSteepSlopes + && _controller.isGrounded + && slopeAngle > _controller.slopeLimit + 0.01f; + + // --- Gravity & jumping --------------------------------------------- + if (_controller.isGrounded) + { + // Reset accumulated fall speed and keep a small downward bias on the ground. + if (_verticalVelocity < 0f) + _verticalVelocity = groundedStickVelocity; + + // No jumping off a slope that is too steep to stand on. + if (jumpPressed && !onSteepSlope) + _verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity); + } + + _verticalVelocity += gravity * Time.deltaTime; + + // --- Slope sliding -------------------------------------------------- + Vector3 slideVelocity = Vector3.zero; + if (onSteepSlope) + { + // Steepest-descent direction along the slope surface. + Vector3 slideDir = Vector3.ProjectOnPlane(Vector3.down, _groundNormal).normalized; + // Accelerate the slide up to the maximum slide speed. + _slideSpeed = Mathf.Min(_slideSpeed + slideAcceleration * Time.deltaTime, maxSlideSpeed); + slideVelocity = slideDir * _slideSpeed; + // While sliding the player keeps only reduced steering control. + moveDir *= slideControl; + } + else + { + _slideSpeed = 0f; + } + + // --- Apply movement ------------------------------------------------- + Vector3 velocity = moveDir * moveSpeed + slideVelocity + Vector3.up * _verticalVelocity; + + // Reset the per-frame ground-normal collection, then Move (which fires + // OnControllerColliderHit), then fall back to "up" if nothing was hit. + _groundNormalUpdated = false; + _controller.Move(velocity * Time.deltaTime); + if (!_groundNormalUpdated) + _groundNormal = Vector3.up; + + // --- Rotate toward the movement direction --------------------------- + if (rotateTowardMovement && moveDir.sqrMagnitude > 0.0001f) + { + Quaternion target = Quaternion.LookRotation(moveDir, Vector3.up); + transform.rotation = Quaternion.RotateTowards( + transform.rotation, target, rotationSpeed * Time.deltaTime); + } + } + + /// + /// Collects the ground contact normal during . + /// Walls and ceilings (normal pointing sideways or down) are ignored; among the + /// upward-facing contacts the flattest one is kept, which represents the surface + /// the character is actually standing on. + /// + private void OnControllerColliderHit(ControllerColliderHit hit) + { + // Ignore near-vertical walls and ceilings. + if (hit.normal.y <= 0.05f) + return; + + // Keep the flattest (most supportive) contact seen this frame. + if (!_groundNormalUpdated || hit.normal.y > _groundNormal.y) + { + _groundNormal = hit.normal; + _groundNormalUpdated = true; + } + } +} diff --git a/Assets/Scripts/PlayerCharacterController.cs.meta b/Assets/Scripts/PlayerCharacterController.cs.meta new file mode 100644 index 0000000..1cd8e4f --- /dev/null +++ b/Assets/Scripts/PlayerCharacterController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 463b6be548abf464287096e1e92fcd01 \ No newline at end of file