ThirdPersonController.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1.  using UnityEngine;
  2. #if ENABLE_INPUT_SYSTEM
  3. using UnityEngine.InputSystem;
  4. #endif
  5. /* Note: animations are called via the controller for both the character and capsule using animator null checks
  6. */
  7. namespace StarterAssets
  8. {
  9. [RequireComponent(typeof(CharacterController))]
  10. #if ENABLE_INPUT_SYSTEM
  11. [RequireComponent(typeof(PlayerInput))]
  12. #endif
  13. public class ThirdPersonController : MonoBehaviour
  14. {
  15. [Header("Player")]
  16. [Tooltip("Move speed of the character in m/s")]
  17. public float MoveSpeed = 2.0f;
  18. [Tooltip("Sprint speed of the character in m/s")]
  19. public float SprintSpeed = 5.335f;
  20. [Tooltip("How fast the character turns to face movement direction")]
  21. [Range(0.0f, 0.3f)]
  22. public float RotationSmoothTime = 0.12f;
  23. [Tooltip("Acceleration and deceleration")]
  24. public float SpeedChangeRate = 10.0f;
  25. public AudioClip LandingAudioClip;
  26. public AudioClip[] FootstepAudioClips;
  27. [Range(0, 1)] public float FootstepAudioVolume = 0.5f;
  28. [Space(10)]
  29. [Tooltip("The height the player can jump")]
  30. public float JumpHeight = 1.2f;
  31. [Tooltip("The character uses its own gravity value. The engine default is -9.81f")]
  32. public float Gravity = -15.0f;
  33. [Space(10)]
  34. [Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")]
  35. public float JumpTimeout = 0.50f;
  36. [Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")]
  37. public float FallTimeout = 0.15f;
  38. [Header("Player Grounded")]
  39. [Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")]
  40. public bool Grounded = true;
  41. [Tooltip("Useful for rough ground")]
  42. public float GroundedOffset = -0.14f;
  43. [Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")]
  44. public float GroundedRadius = 0.28f;
  45. [Tooltip("What layers the character uses as ground")]
  46. public LayerMask GroundLayers;
  47. [Header("Cinemachine")]
  48. [Tooltip("The follow target set in the Cinemachine Virtual Camera that the camera will follow")]
  49. public GameObject CinemachineCameraTarget;
  50. [Tooltip("How far in degrees can you move the camera up")]
  51. public float TopClamp = 70.0f;
  52. [Tooltip("How far in degrees can you move the camera down")]
  53. public float BottomClamp = -30.0f;
  54. [Tooltip("Additional degress to override the camera. Useful for fine tuning camera position when locked")]
  55. public float CameraAngleOverride = 0.0f;
  56. [Tooltip("For locking the camera position on all axis")]
  57. public bool LockCameraPosition = false;
  58. // cinemachine
  59. private float _cinemachineTargetYaw;
  60. private float _cinemachineTargetPitch;
  61. // player
  62. private float _speed;
  63. private float _animationBlend;
  64. private float _targetRotation = 0.0f;
  65. private float _rotationVelocity;
  66. private float _verticalVelocity;
  67. private float _terminalVelocity = 53.0f;
  68. // timeout deltatime
  69. private float _jumpTimeoutDelta;
  70. private float _fallTimeoutDelta;
  71. // animation IDs
  72. private int _animIDSpeed;
  73. private int _animIDGrounded;
  74. private int _animIDJump;
  75. private int _animIDFreeFall;
  76. private int _animIDMotionSpeed;
  77. #if ENABLE_INPUT_SYSTEM
  78. private PlayerInput _playerInput;
  79. #endif
  80. private Animator _animator;
  81. private CharacterController _controller;
  82. private StarterAssetsInputs _input;
  83. private GameObject _mainCamera;
  84. private const float _threshold = 0.01f;
  85. private bool _hasAnimator;
  86. private bool IsCurrentDeviceMouse
  87. {
  88. get
  89. {
  90. #if ENABLE_INPUT_SYSTEM
  91. return _playerInput.currentControlScheme == "KeyboardMouse";
  92. #else
  93. return false;
  94. #endif
  95. }
  96. }
  97. private void Awake()
  98. {
  99. // get a reference to our main camera
  100. if (_mainCamera == null)
  101. {
  102. _mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
  103. }
  104. }
  105. private void Start()
  106. {
  107. _cinemachineTargetYaw = CinemachineCameraTarget.transform.rotation.eulerAngles.y;
  108. _hasAnimator = TryGetComponent(out _animator);
  109. _controller = GetComponent<CharacterController>();
  110. _input = GetComponent<StarterAssetsInputs>();
  111. #if ENABLE_INPUT_SYSTEM
  112. _playerInput = GetComponent<PlayerInput>();
  113. #else
  114. Debug.LogError( "Starter Assets package is missing dependencies. Please use Tools/Starter Assets/Reinstall Dependencies to fix it");
  115. #endif
  116. AssignAnimationIDs();
  117. // reset our timeouts on start
  118. _jumpTimeoutDelta = JumpTimeout;
  119. _fallTimeoutDelta = FallTimeout;
  120. }
  121. private void Update()
  122. {
  123. _hasAnimator = TryGetComponent(out _animator);
  124. JumpAndGravity();
  125. GroundedCheck();
  126. Move();
  127. }
  128. private void LateUpdate()
  129. {
  130. CameraRotation();
  131. }
  132. private void AssignAnimationIDs()
  133. {
  134. _animIDSpeed = Animator.StringToHash("Speed");
  135. _animIDGrounded = Animator.StringToHash("Grounded");
  136. _animIDJump = Animator.StringToHash("Jump");
  137. _animIDFreeFall = Animator.StringToHash("FreeFall");
  138. _animIDMotionSpeed = Animator.StringToHash("MotionSpeed");
  139. }
  140. private void GroundedCheck()
  141. {
  142. // set sphere position, with offset
  143. Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset,
  144. transform.position.z);
  145. Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers,
  146. QueryTriggerInteraction.Ignore);
  147. // update animator if using character
  148. if (_hasAnimator)
  149. {
  150. _animator.SetBool(_animIDGrounded, Grounded);
  151. }
  152. }
  153. private void CameraRotation()
  154. {
  155. // if there is an input and camera position is not fixed
  156. if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
  157. {
  158. //Don't multiply mouse input by Time.deltaTime;
  159. float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;
  160. _cinemachineTargetYaw += _input.look.x * deltaTimeMultiplier;
  161. _cinemachineTargetPitch += _input.look.y * deltaTimeMultiplier;
  162. }
  163. // clamp our rotations so our values are limited 360 degrees
  164. _cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);
  165. _cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);
  166. // Cinemachine will follow this target
  167. CinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride,
  168. _cinemachineTargetYaw, 0.0f);
  169. }
  170. private void Move()
  171. {
  172. // set target speed based on move speed, sprint speed and if sprint is pressed
  173. float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;
  174. // a simplistic acceleration and deceleration designed to be easy to remove, replace, or iterate upon
  175. // note: Vector2's == operator uses approximation so is not floating point error prone, and is cheaper than magnitude
  176. // if there is no input, set the target speed to 0
  177. if (_input.move == Vector2.zero) targetSpeed = 0.0f;
  178. // a reference to the players current horizontal velocity
  179. float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;
  180. float speedOffset = 0.1f;
  181. float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;
  182. // accelerate or decelerate to target speed
  183. if (currentHorizontalSpeed < targetSpeed - speedOffset ||
  184. currentHorizontalSpeed > targetSpeed + speedOffset)
  185. {
  186. // creates curved result rather than a linear one giving a more organic speed change
  187. // note T in Lerp is clamped, so we don't need to clamp our speed
  188. _speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude,
  189. Time.deltaTime * SpeedChangeRate);
  190. // round speed to 3 decimal places
  191. _speed = Mathf.Round(_speed * 1000f) / 1000f;
  192. }
  193. else
  194. {
  195. _speed = targetSpeed;
  196. }
  197. _animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);
  198. if (_animationBlend < 0.01f) _animationBlend = 0f;
  199. // normalise input direction
  200. Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;
  201. // note: Vector2's != operator uses approximation so is not floating point error prone, and is cheaper than magnitude
  202. // if there is a move input rotate player when the player is moving
  203. if (_input.move != Vector2.zero)
  204. {
  205. _targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg +
  206. _mainCamera.transform.eulerAngles.y;
  207. float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
  208. RotationSmoothTime);
  209. // rotate to face input direction relative to camera position
  210. transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
  211. }
  212. Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;
  213. // move the player
  214. _controller.Move(targetDirection.normalized * (_speed * Time.deltaTime) +
  215. new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
  216. // update animator if using character
  217. if (_hasAnimator)
  218. {
  219. _animator.SetFloat(_animIDSpeed, _animationBlend);
  220. _animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
  221. }
  222. }
  223. private void JumpAndGravity()
  224. {
  225. if (Grounded)
  226. {
  227. // reset the fall timeout timer
  228. _fallTimeoutDelta = FallTimeout;
  229. // update animator if using character
  230. if (_hasAnimator)
  231. {
  232. _animator.SetBool(_animIDJump, false);
  233. _animator.SetBool(_animIDFreeFall, false);
  234. }
  235. // stop our velocity dropping infinitely when grounded
  236. if (_verticalVelocity < 0.0f)
  237. {
  238. _verticalVelocity = -2f;
  239. }
  240. // Jump
  241. if (_input.jump && _jumpTimeoutDelta <= 0.0f)
  242. {
  243. // the square root of H * -2 * G = how much velocity needed to reach desired height
  244. _verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);
  245. // update animator if using character
  246. if (_hasAnimator)
  247. {
  248. _animator.SetBool(_animIDJump, true);
  249. }
  250. }
  251. // jump timeout
  252. if (_jumpTimeoutDelta >= 0.0f)
  253. {
  254. _jumpTimeoutDelta -= Time.deltaTime;
  255. }
  256. }
  257. else
  258. {
  259. // reset the jump timeout timer
  260. _jumpTimeoutDelta = JumpTimeout;
  261. // fall timeout
  262. if (_fallTimeoutDelta >= 0.0f)
  263. {
  264. _fallTimeoutDelta -= Time.deltaTime;
  265. }
  266. else
  267. {
  268. // update animator if using character
  269. if (_hasAnimator)
  270. {
  271. _animator.SetBool(_animIDFreeFall, true);
  272. }
  273. }
  274. // if we are not grounded, do not jump
  275. _input.jump = false;
  276. }
  277. // apply gravity over time if under terminal (multiply by delta time twice to linearly speed up over time)
  278. if (_verticalVelocity < _terminalVelocity)
  279. {
  280. _verticalVelocity += Gravity * Time.deltaTime;
  281. }
  282. }
  283. private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
  284. {
  285. if (lfAngle < -360f) lfAngle += 360f;
  286. if (lfAngle > 360f) lfAngle -= 360f;
  287. return Mathf.Clamp(lfAngle, lfMin, lfMax);
  288. }
  289. private void OnDrawGizmosSelected()
  290. {
  291. Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);
  292. Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);
  293. if (Grounded) Gizmos.color = transparentGreen;
  294. else Gizmos.color = transparentRed;
  295. // when selected, draw a gizmo in the position of, and matching radius of, the grounded collider
  296. Gizmos.DrawSphere(
  297. new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z),
  298. GroundedRadius);
  299. }
  300. private void OnFootstep(AnimationEvent animationEvent)
  301. {
  302. if (animationEvent.animatorClipInfo.weight > 0.5f)
  303. {
  304. if (FootstepAudioClips.Length > 0)
  305. {
  306. var index = Random.Range(0, FootstepAudioClips.Length);
  307. AudioSource.PlayClipAtPoint(FootstepAudioClips[index], transform.TransformPoint(_controller.center), FootstepAudioVolume);
  308. }
  309. }
  310. }
  311. private void OnLand(AnimationEvent animationEvent)
  312. {
  313. if (animationEvent.animatorClipInfo.weight > 0.5f)
  314. {
  315. AudioSource.PlayClipAtPoint(LandingAudioClip, transform.TransformPoint(_controller.center), FootstepAudioVolume);
  316. }
  317. }
  318. }
  319. }