승쨩개발공부

[Unity] ThirdPersonController 분석 본문

C#/Unity

[Unity] ThirdPersonController 분석

Unknowns 2024. 8. 8. 01:14

namespace StarterAssets
{
    [RequireComponent(typeof(CharacterController))]
#if ENABLE_INPUT_SYSTEM 
    [RequireComponent(typeof(PlayerInput))]
#endif
    public class ThirdPersonController : MonoBehaviour
    {
        [Header("Player")]
        [Tooltip("캐릭터의 이동속도 m/s")]
        public float MoveSpeed = 5.0f;

        [Tooltip("캐릭터의 달리기 속도 m/s")]
        public float SprintSpeed = 8f;

        [Tooltip("캐릭터가 이동방향을 향하여 회전하는 속도")]
        [Range(0.0f, 0.3f)]
        public float RotationSmoothTime = 0.12f;

        [Tooltip("가속 및 감속")]
        public float SpeedChangeRate = 10.0f;

        public AudioClip LandingAudioClip;
        public AudioClip[] FootstepAudioClips;
        [Range(0, 1)] public float FootstepAudioVolume = 0.5f;

        [Space(10)]
        [Tooltip("점프할수있는 높이")]
        public float JumpHeight = 1.2f;

        [Tooltip("캐릭터는 자체 중력값을 사용 엔진 기본값은 -9.81f")]
        public float Gravity = -15.0f;

        [Space(10)]
        [Tooltip("다시 점프할수 있을떄까지 필요하는 시간 즉시 다시 점프할려면 0")]
        public float JumpTimeout = 0.50f;

        [Tooltip("추락 상태로 진입하기까지 소요되는 시간")]
        public float FallTimeout = 0.15f;

        public bool m_isShootMove;
        public bool m_isSprint;

        [Header("Player Grounded")]
        [Tooltip("캐릭터가 땅인지 체크")]
        public bool Grounded = true;

        [Tooltip("땅인지 체크하는 OffSet")]
        public float GroundedOffset = -0.14f;

        [Tooltip("땅인지 체크하는 Radius")]
        public float GroundedRadius = 0.28f;

        [Tooltip("Ground 체크 레이어")]
        public LayerMask GroundLayers;

        [Header("Cinemachine")]
        [Tooltip("카메라가 따라갈 추적대상")]
        public GameObject CinemachineCameraTarget;

        [Tooltip("카메라 얼만큼 위로 향 할수있는지")]
        public float TopClamp = 70.0f;

        [Tooltip("카메라 얼만큼 아래로 향 할수있는지")]
        public float BottomClamp = -30.0f;

        [Tooltip("카메라를 재정의하는 추가단계 잠겨 있을 떄 카메라 위치를 미세조종하는데 유용함")]
        public float CameraAngleOverride = 0.0f;

        [Tooltip("모든 축에서 카메라 위치를 잠금")]
        public bool LockCameraPosition = false;

        // cinemachine
        private float _cinemachineTargetYaw;
        private float _cinemachineTargetPitch;

        // player
        private float _speed;
        private float _animationBlend;
        private float _targetRotation = 0.0f;
        private float _rotationVelocity;
        private float _verticalVelocity;
        private float _terminalVelocity = 53.0f;

        // timeout deltatime
        private float _jumpTimeoutDelta;
        private float _fallTimeoutDelta;

        // animation IDs
        private int _animIDSpeed;
        private int _animIDGrounded;
        private int _animIDJump;
        private int _animIDFreeFall;
        private int _animIDMotionSpeed;

#if ENABLE_INPUT_SYSTEM 
        private PlayerInput _playerInput;
#endif
        private Animator _animator;
        private CharacterController _controller;
        private StarterAssetsInputs _input;
        private GameObject _mainCamera;

        private const float _threshold = 0.01f;

        private bool _hasAnimator;

  

        private bool IsCurrentDeviceMouse
        {
            get
            {
#if ENABLE_INPUT_SYSTEM
                return _playerInput.currentControlScheme == "KeyboardMouse";
#else
return false;
#endif
            }
        }


        private void Awake()
        {
            // get a reference to our main camera
            if (_mainCamera == null)
            {
                _mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
            }
        }

        private void Start()
        {
            // 플레이어를 추격하는 카메라의 Y로테이션 오일러값
            _cinemachineTargetYaw = CinemachineCameraTarget.transform.rotation.eulerAngles.y;
            
            _hasAnimator = TryGetComponent(out _animator);
            _controller = GetComponent<CharacterController>();
            _input = GetComponent<StarterAssetsInputs>();
#if ENABLE_INPUT_SYSTEM 
            _playerInput = GetComponent<PlayerInput>();
#else
Debug.LogError( "Starter Assets package is missing dependencies. Please use Tools/Starter Assets/Reinstall Dependencies to fix it");
#endif
            // 애니메이션 파라미터값을 맴버변수로 받음
            AssignAnimationIDs();

            // 시작시 점프,추락 시간초과를 재설정함
            _jumpTimeoutDelta = JumpTimeout;
            _fallTimeoutDelta = FallTimeout;
        }

        private void Update()
        {
            // 애니메이터를 찾았는지 못찾았는지 Bool변수로 계속 체크함
            _hasAnimator = TryGetComponent(out _animator);

            JumpAndGravity();
            GroundedCheck();
            Move();
        }

        private void LateUpdate()
        {
            CameraRotation();
        }

        private void AssignAnimationIDs()
        {
            // 애니메이션 파라미터 맴버변수로 저장
            _animIDSpeed = Animator.StringToHash("Speed");
            _animIDGrounded = Animator.StringToHash("Grounded");
            _animIDJump = Animator.StringToHash("Jump");
            _animIDFreeFall = Animator.StringToHash("FreeFall");
            _animIDMotionSpeed = Animator.StringToHash("MotionSpeed");
        }

        private void GroundedCheck()
        {
            // 오프셋을 사용하여 도구 위치 설정
            Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset,
                transform.position.z);
            Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers,
                QueryTriggerInteraction.Ignore);

            // 애니메이터가 True면 애니메이터 Grounded에 Grounded체크 bool변수를 넣어서 애니메이션 업데이트
            if (_hasAnimator)
            {
                _animator.SetBool(_animIDGrounded, Grounded);
            }
        }

        private void CameraRotation()
        {
            // 입력이 있고 카메라 위치가 고정되지 않은경우
            if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
            {
                //마우스 입력에 Time.deltaTime을 곱하지마시오
                float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;

                _cinemachineTargetYaw += _input.look.x * deltaTimeMultiplier;
                _cinemachineTargetPitch += _input.look.y * deltaTimeMultiplier;
            }

            // 회전을 고정하여 값이 360도로 제한되도록 하기
            _cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);
            _cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);

            // 시네머신은 이 타겟을 따름
            CinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride,
                _cinemachineTargetYaw, 0.0f);
        }

        private void Move()
        {
            // 이동속도, 질주속도 및 질주를 누른경우에 따라 목표 속도를 설정
            float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;

            
            
            // 제거, 교체 또는 반복이 쉽도록 설계된 단순한 가속 및 감속

            // 참고 : Vector2의 == 연산자는 근사치를 사용하므로 부동 소수점 오류가 발생하지 않으며 크기보다 저렴함
            // 입력이 없으면 목표 속도를 0으로 설정함
            if (_input.move == Vector2.zero) targetSpeed = 0.0f;

            // 플레이어의 현재 Horizontal 속도에 대한 참조 (백터의 크기)
            float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;

            float speedOffset = 0.1f;
            float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;

            // 내가 추가한것
            float SprintCheck = _animator.GetFloat("Speed");
            float CurrentSpeed = (SprintSpeed + MoveSpeed) / 2;
   
            if (SprintCheck > CurrentSpeed)
            {
                m_isSprint = true;
            }
            else 
            {
                m_isSprint = false;
            }
            //

            // 목표 속도까지 가속 또는 감속
            if (currentHorizontalSpeed < targetSpeed - speedOffset ||
                currentHorizontalSpeed > targetSpeed + speedOffset)
            { // 보다 유기적인 속도변화를 제공하는 선형결과가 아닌 곡선 결과를 생성함
                _speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude,
                    Time.deltaTime * SpeedChangeRate);

                // 속도를 소주점 이하 3자리로 반올림
                _speed = Mathf.Round(_speed * 1000f) / 1000f;
            }
            else
            {
                _speed = targetSpeed;
            }

            _animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);
            if (_animationBlend < 0.01f) _animationBlend = 0f;

            // 입력 방향을 Normalize 크기를 0으로만듬 방향만 가지게
            Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;

            // 이동 입력이 있는경우 플레이어가 움직일떄 플레이어 회전
            if (_input.move != Vector2.zero)
            {
                

                _targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg +
                                  _mainCamera.transform.eulerAngles.y;
                float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
                    RotationSmoothTime);

                if (!m_isShootMove)
                {
                    transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
                }
                // 카메라 위치를 기준으로 입력 방향을 향하도록 회전
                

            }


            Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;

            // 플레이어 이동
            _controller.Move(targetDirection.normalized * (_speed * Time.deltaTime) +
                             new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);

            // 애니메이터 스피드 파라미터값을 _animationBlend값으로
            // 모션스피드값을 InputMagnitude값으로 증가 및 감소
            if (_hasAnimator)
            {
                _animator.SetFloat(_animIDSpeed, _animationBlend);
                _animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
            }
        }

        private void JumpAndGravity()
        {
            // 땅일경우
            if (Grounded)
            {
                // 떨어지는 타이머를 재설정함
                _fallTimeoutDelta = FallTimeout;

                // Jump와 Fall 애니메이터 파라미터를 false함
                if (_hasAnimator)
                {
                    _animator.SetBool(_animIDJump, false);
                    _animator.SetBool(_animIDFreeFall, false);
                }

                // 땅일경우 속도가 무한이 떨어지는걸 중지함
                if (_verticalVelocity < 0.0f)
                {
                    _verticalVelocity = -2f;
                }

                // 점프를누르고 점프타임아웃델타가 0과 같거나 작을경우
                if (_input.jump && _jumpTimeoutDelta <= 0.0f)
                {
                   // 원하는 높이에 도달 하는데 필요한 속도를 제곱근으로 저장
                    _verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);

                    // 점프 애니메이션 True
                    if (_hasAnimator)
                    {
                        _animator.SetBool(_animIDJump, true);
                    }
                }

                // 점프시간 초과가 0보다 작거나 클떄
                if (_jumpTimeoutDelta >= 0.0f)
                {
                    _jumpTimeoutDelta -= Time.deltaTime;
                }
            }
            // 땅이 아닐경우
            else
            {
                // 점프 시간 초과 타이머를 재설정
                _jumpTimeoutDelta = JumpTimeout;

                // Fall 타임아웃일떄
                if (_fallTimeoutDelta >= 0.0f)
                {
                    _fallTimeoutDelta -= Time.deltaTime;
                }
                // Fall 타임아웃이 아닐떄
                else
                {
                    // Fall 애니메이션 True
                    if (_hasAnimator)
                    {
                        _animator.SetBool(_animIDFreeFall, true);
                    }
                }

                // 땅이 아닐경우 점프 못하게
                _input.jump = false;
            }

            // 터미널 아래에 있는 경우시간에따라 중력을 적용함 (시간에 따라 떨어지는 속도를 높힐려면 델타타임을 두번곱함)
            if (_verticalVelocity < _terminalVelocity)
            {
                _verticalVelocity += Gravity * Time.deltaTime;
            }
        }

        private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
        {
            if (lfAngle < -360f) lfAngle += 360f;
            if (lfAngle > 360f) lfAngle -= 360f;
            return Mathf.Clamp(lfAngle, lfMin, lfMax);
        }

        private void OnDrawGizmosSelected()
        {
            Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);
            Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);

            // 땅일경우 초록색 땅이아니면 빨간색 기즈모 색상 설정
            if (Grounded) Gizmos.color = transparentGreen;
            else Gizmos.color = transparentRed;

            // 충돌체의 위치와 일치하는 반경에 기즈모를 그림
            Gizmos.DrawSphere(
                new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z),
                GroundedRadius);
        }

        private void OnFootstep(AnimationEvent animationEvent)
        {
            if (animationEvent.animatorClipInfo.weight > 0.5f)
            {
                if (FootstepAudioClips.Length > 0)
                {
                    var index = Random.Range(0, FootstepAudioClips.Length);
                    AudioSource.PlayClipAtPoint(FootstepAudioClips[index], transform.TransformPoint(_controller.center), FootstepAudioVolume);
                }
            }
        }

        private void OnLand(AnimationEvent animationEvent)
        {
            if (animationEvent.animatorClipInfo.weight > 0.5f)
            {
                AudioSource.PlayClipAtPoint(LandingAudioClip, transform.TransformPoint(_controller.center), FootstepAudioVolume);
            }
        }
    }
}

'C# > Unity' 카테고리의 다른 글

[Blender] 애니메이션 원점/이동값 제거  (0) 2024.09.27
[Unity] Particle System  (0) 2024.08.15
[Unity] 블라스퍼머스 모작1  (0) 2024.07.09
[Unity] Math 두점 사이의 각도구하기  (2) 2024.05.09
[Unity] 충돌여백  (0) 2024.04.25