유니티 ThirdPresonController 파헤치기

정선호·2023년 11월 4일
0

Unity Features

목록 보기
22/27

CharacterController를 이용한 TPS 플레이어를 구현하는 도중 내 플레이어와 유니티가 샘플로 만든 플레이어의 퀄리티 차이를 체감하고, 유니티의 ThirdPersonController는 어떻게 작성되어 있나 확인하기 위한 문서


ThirdPersonController.cs

해당 스크립트는 다음 주소의 에셋에서 확인할 수 있다.

변수 부분

        [Header("Player")]
        [Tooltip("Move speed of the character in m/s")]
        public float MoveSpeed = 2.0f;

        [Tooltip("Sprint speed of the character in m/s")]
        public float SprintSpeed = 5.335f;

        [Tooltip("How fast the character turns to face movement direction")]
        [Range(0.0f, 0.3f)]
        public float RotationSmoothTime = 0.12f;

        [Tooltip("Acceleration and deceleration")]
        public float SpeedChangeRate = 10.0f;

        // 플레이어 발소리 관련 변수들
        public AudioClip LandingAudioClip;
        public AudioClip[] FootstepAudioClips;
        [Range(0, 1)] public float FootstepAudioVolume = 0.5f;

        [Space(10)]
        [Tooltip("The height the player can jump")]
        public float JumpHeight = 1.2f;

        [Tooltip("The character uses its own gravity value. The engine default is -9.81f")]
        public float Gravity = -15.0f;

        [Space(10)]
        [Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")]
        public float JumpTimeout = 0.50f;

        [Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")]
        public float FallTimeout = 0.15f;

        [Header("Player Grounded")]
        [Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")]
        public bool Grounded = true;

        [Tooltip("Useful for rough ground")]
        public float GroundedOffset = -0.14f;

        [Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")]
        public float GroundedRadius = 0.28f;

        [Tooltip("What layers the character uses as ground")]
        public LayerMask GroundLayers;

        [Header("Cinemachine")]
        [Tooltip("The follow target set in the Cinemachine Virtual Camera that the camera will follow")]
        public GameObject CinemachineCameraTarget;

        [Tooltip("How far in degrees can you move the camera up")]
        public float TopClamp = 70.0f;

        [Tooltip("How far in degrees can you move the camera down")]
        public float BottomClamp = -30.0f;

        [Tooltip("Additional degress to override the camera. Useful for fine tuning camera position when locked")]
        public float CameraAngleOverride = 0.0f;

        [Tooltip("For locking the camera position on all axis")]
        public bool LockCameraPosition = false;

        // 시네머신 관련 변수
        private float _cinemachineTargetYaw;
        private float _cinemachineTargetPitch;

        // 플레이어 변수
        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;

        // 애니메이터의 애니메이션 값 ID들
        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()
        {
            // 메인 카메라의 레퍼런스를 가져온다
            if (_mainCamera == null)
            {
                _mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
            }
        }

        private void Start()
        {
            // 시네머신 카메라의 요 회전값을 가져온다
            _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

            // 애니메이터의 애니메이션 트리거 ID 등록
            AssignAnimationIDs();

            // 타임아웃 값들 초기화
            _jumpTimeoutDelta = JumpTimeout;
            _fallTimeoutDelta = FallTimeout;
        }

        private void Update()
        {
            _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");
        }

그라운드 체크

  • Physics.CheckSphere()를 이용해 그라운드를 체크한다.
  • 애니메이터가 존재하면 애니메이터에 해당 값을 갱신한다.
        private void GroundedCheck()
        {
            // 오프셋을 적용해 스피어 콜라이더의 위치를 잡는다
            Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset,
                transform.position.z);
            // CheckSphere()로 캐릭터의 바닥쪽에 바닥으로 정한 물체 콜라이더가 닿았는지 체크한다.
            Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers,
                QueryTriggerInteraction.Ignore);

            // 애니메이션을 가진 아바타가 있으면 애니메이션 프로퍼티를 갱신한다
            if (_hasAnimator)
            {
                _animator.SetBool(_animIDGrounded, Grounded);
            }
        }

카메라 회전

  • 카메라의 트랜스폼 로테이션을 회전시키는 함수
        private void CameraRotation()
        {
            // 카메라 포지션이 고정되지 않았고 카메라 회전 입력이 들어왔으면
            if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
            {
                // 마우스 입력은 DeltaTime을 곱하지 않는다
                float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;

                // 각각 x,z축 입력을 할당한다
                _cinemachineTargetYaw += _input.look.x * deltaTimeMultiplier;
                _cinemachineTargetPitch += _input.look.y * deltaTimeMultiplier;
            }

            // 회전각이 0에서 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;

            // 제거, 교체 또는 반복하기 쉽도록 설계된 단순한 가속 및 감속 기능 구현

            // 추신 : 벡터2의 ==는 근사치 계산을 하므로 부동소수점 오류가 발생하지 않으며 magnitude보다 가볍다
            // 입력이 없으면 속도를 0으로 설정한다
            if (_input.move == Vector2.zero) targetSpeed = 0.0f;

            // 현재 플레이어의 수평 속도 계산
            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;

            // 타겟 스패드로 가속 혹은 감속
            if (currentHorizontalSpeed < targetSpeed - speedOffset ||
                currentHorizontalSpeed > targetSpeed + speedOffset)
            {
                // 자연스런 속도변화를 위해 선형이 아닌 곡선형 변화 제공
                // 추신 : Lerp내 T는 클램프되므로 따로 클램핑할 필요는 없다
                _speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude,
                    Time.deltaTime * SpeedChangeRate);

                // 소수점 세 자리에서 자르기
                _speed = Mathf.Round(_speed * 1000f) / 1000f;
            }
            else
            {
                _speed = targetSpeed;
            }

            // 애니메이터에 제공할 이동속도 값 계산
            _animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);
            if (_animationBlend < 0.01f) _animationBlend = 0f;

            // 입력값 노멀라이징
            Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;

            // 추신 : 벡터2의 !=는 근사치 계산을 하므로 부동소수점 오류가 발생하지 않으며 magnitude보다 가볍다
            // 입력이 들어오면 플레이어를 입력이 들어온 쪽으로 회전시킨다
            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);

                // 카메라의 포지션을 참고해 가는 방향으로 회전킨다
                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);

            // 아바타가 있으면 아바타 애니메이터 값을 설정한다
            if (_hasAnimator)
            {
                _animator.SetFloat(_animIDSpeed, _animationBlend);
                _animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
            }
        }

점프와 중력

  • 점프와 중력을 담당하는 스크립트이다.
        private void JumpAndGravity()
        {
            // 땅에 있으면
            if (Grounded)
            {
                // 낙하 타임아웃 타이머 초기화
                _fallTimeoutDelta = FallTimeout;

                // 애니메이터 값 갱신
                if (_hasAnimator)
                {
                    _animator.SetBool(_animIDJump, false);
                    _animator.SetBool(_animIDFreeFall, false);
                }

                // 땅에 있을 때 낙하가속을 고정시킴
                if (_verticalVelocity < 0.0f)
                {
                    _verticalVelocity = -2f;
                }

                // 점프
                if (_input.jump && _jumpTimeoutDelta <= 0.0f)
                {
                    // 높이 * 중력가속도 * -2의 제곱근 = 원하는 높이에 닿기까지 필요한 가속도
                    _verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);

                    // 애니메이션 값 갱신
                    if (_hasAnimator)
                    {
                        _animator.SetBool(_animIDJump, true);
                    }
                }

                // 점프 타임아웃 타이머
                if (_jumpTimeoutDelta >= 0.0f)
                {
                    _jumpTimeoutDelta -= Time.deltaTime;
                }
            }
            else
            {
                // 점프 타임아웃 타이머 초기화
                _jumpTimeoutDelta = JumpTimeout;

                // 낙하 타임아웃 타이머
                if (_fallTimeoutDelta >= 0.0f)
                {
                    _fallTimeoutDelta -= Time.deltaTime;
                }
                else
                {
                    // 애니메이션 값 갱신
                    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);
            }
        }
profile
학습한 내용을 빠르게 다시 찾기 위한 저장소

0개의 댓글