Unity 3D/유니티 짱

유니티 짱 05) 3D 액션 게임 캐릭터 조작 - 1

ybbro 2023. 4. 1. 22:43

이전 포스트인 시점을 구현하면서 함께 만들었습니다.

 

유니티짱 다이나믹은 옷자락 움직임 문제로 일반 유니티짱으로 바꾸었습니다.

Projects > Assets > UnityChan > Prefabs > unitychan을 하이어라키 창에 끌어오고 언팩해줍니다.

 

1. 하이어라키 창

유니티짱의 자식 오브젝트로 빈 오브젝트 2개 추가

카메라 위치의 기준이 되는 CamPos
레이캐스트 기능 사용 위치인 RayPosition

 

2. 인스펙터 창

1) 기존에 있던 컴포넌트 중에 Animator, Auto Blink 만 남기고 모두 지운 후 Animator 의 컨트롤러로 액션용으로 변경한 컨트롤러를 추가해줍니다.

    (1) Locomotion을 Run, Walk로 나눔

    (2) Jump 애니메이션을 Jump, Falling, Landing으로 나눔 -> 낙하가 길어질 때를 대비하기 위해

    (3) Slide 추가

    (4) 휴식 모션들(Rest01~03) 추가

2) 3D 액션 게임을 목표로 만들 것이기 때문에 물리적 충돌을 판정하기 위해 캡슐 콜라이더리지드바디를 추가하고 유니티짱의 크기에 맞게 콜라이더의 중심, 높이, 반지름을 수정. 리지드바디는 사용할 스크립트 조작에 맞게 수정

 

3) 제어를 위해 UnityChanControlScriptWithRgidBody을 복제하여 수정한 UnityChanControlScriptWithRgidBody_Custom 스크립트를 추가하였습니다.

using UnityEngine;

namespace UnityChan
{
    // 필요 컴포넌트들
    [RequireComponent(typeof(Animator))]
    [RequireComponent(typeof(CapsuleCollider))]
    [RequireComponent(typeof(Rigidbody))]

    public class UnityChanControlScriptWithRgidBody_Custom : MonoBehaviour
    {
        [HideInInspector] public bool isFarFromFloor;
        // 현재 회전의 기준이 되는 값 (ThirdPersonCamera_Custom 에서 변경)
        [HideInInspector] public float currentDirection = 0f;
        // 애니메이션의 현재 상태
        public AnimatorStateInfo currentBaseState;
        // 애니메이터 각 상태에 대한 참조
        public readonly int idleState = Animator.StringToHash("Base Layer.Idle");
        // 걷기, 달리기 구분
        public readonly int walkState = Animator.StringToHash("Base Layer.Walk");
        public readonly int runState = Animator.StringToHash("Base Layer.Run");
        // 점프 애니메이션을 상황에 맞게 쓰기 위해 
        // 점프 후 최고점까지 체공, 공중에 뜬 상태, 착지 3단계로 분할
        public readonly int jumpState = Animator.StringToHash("Base Layer.Jump");
        public readonly int fallState = Animator.StringToHash("Base Layer.Falling");
        public readonly int landState = Animator.StringToHash("Base Layer.Landing");
        // 휴식 모션 구분
        public readonly int restState01 = Animator.StringToHash("Base Layer.Rest01");
        public readonly int restState02 = Animator.StringToHash("Base Layer.Rest02");
        public readonly int restState03 = Animator.StringToHash("Base Layer.Rest03");
        // 슬라이딩 추가
        public readonly int slideState = Animator.StringToHash("Base Layer.Slide");


        // 스크립트로 조작할 유니티짱에 포함된 컴포넌트
        private CapsuleCollider col;
        private Rigidbody rb;
        private Animator anim;
        // 레이캐스트에 쓸 빛을 발사할 발 아주 살짝 위의 포지션
        Transform RayPosition;

        // CapsuleCollider에 설정된 콜라이더 Height, Center의 초기 값
        float orgColHight;
        Vector3 orgVectColCenter;
        // 슬라이딩 중 콜라이더 Height, Center 값
        const float slideColHeight = 0.25f;
        readonly Vector3 slideColCenter = Vector3.up * 0.5f;
        // 0f보다 아주 약간 큰 값이 필요한 경우
        const float slideOffsetY = 0.00001f;
        // 착지 모션을 시작하는 바닥으로부터의 높이
        float landHeight = 1f;
        // 낙하 카메라 무빙을 시작할 바닥으로부터의 높이
        float fallFarStandard = 5f;
        // 전체 애니메이션 재생 속도 (게임의 템포에 맞게 변화)
        float animSpeed = 1.5f;
        // 걸을 때의 속도
        float walkSpeed = 5f;
        // 달릴 때의 속도
        float runSpeed = 10f;
        // 점프, 슬라이드 초기 속력, 감속의 정도를 나타내는 변수
        float initSpeed, speedRate;
        // 착지, 슬라이딩 중 감속 비율
        float[] decreaseRate = new float[2] { 5f, 1.5f };
        // 점프, 슬라이드의 초기 방향
        Vector3 initDirection;
        // 매 Update()에서 최종 계산된 속도
        Vector3 velocity;
        // 공중에 떠 있을 때 중력의 영향을 받아 부여할 Y축 방향 속력
        float vel_Y_inAir;
        // 자유 낙하할 때 최대 속력(m/s)
        // 선형 공기저항의 영향을 받을 때 53m/s에 근사(자세에 따라 약간의 차이는 있음)
        const float vel_Y_Limit = -53f;
        // idle - rest 애니메이션 간 카운터
        float restCount = 0f;
        const float restDelay = 6f;
        // 키보드 입력값
        float h;
        float v;
        bool jump;
        bool slide;
        // 달리기 키 (이동 키와 함께 누르면 달리기)
        KeyCode run_key = KeyCode.LeftShift;
        // 슬라이딩 키 (이동 중에만 사용 가능)
        KeyCode Slide_key = KeyCode.Mouse1;

        void Start()
        {
            // 필요한 컴포넌트 가져오기
            anim = GetComponent<Animator>();
            col = GetComponent<CapsuleCollider>();
            rb = GetComponent<Rigidbody>();
            RayPosition = transform.Find("RayPosition");

            // 콜라이더 height, center 기본값을 받아오기
            orgColHight = col.height;
            orgVectColCenter = col.center;

            // 애니메이션 재생 속도를 처음 1번만 변경
            anim.speed = animSpeed;

            // 프레임 당 중력으로 인한 속력 증가분
            vel_Y_inAir = 9.81f * Time.deltaTime;
        }

        private void Update()
        {
            // 유니티짱 발 밑 오브젝트 탐색
            FloorDetect();
            // 현재 애니메이션 상태
            currentBaseState = anim.GetCurrentAnimatorStateInfo(0);

            // 점프 중, 슬라이드 중이 아닐 때
            // 이동, 점프, 슬라이딩 등 애니메이션으로 전환하는 조작이 가능하게끔
            if (currentBaseState.fullPathHash != jumpState &&
                currentBaseState.fullPathHash != fallState &&
                currentBaseState.fullPathHash != landState && 
                currentBaseState.fullPathHash != slideState)
            {
                // 1. 이동 및 회전 -> 이동 중에만 슬라이딩이 가능
                MoveEntered();
                // 2. 점프
                JumpEntered();
            }
            // 현재 재생 중인 애니메이션에 따라 처리할 조건
            ByAnimState();

            // 속도에 따라 위치 이동 구문 수정
            // 포지션을 직접 움직이면 다른 오브젝트를 뚫고 지나가기에 속도를 주는 방식으로 이를 개선
            // 속도에 영향을 미치는 요건이 여럿이기에 이를 적용한 최종 속도를 부여
            rb.velocity = velocity;
        }

        // 캐릭터와 발 아래 땅과의 거리가 얼마인가에 따라 처리할 내용
        void FloorDetect()
        {
            // 캐릭터 발 약간 위에서 아래 방향으로 빛 발사
            Ray ray = new Ray(RayPosition.position, Vector3.down);
            RaycastHit hitInfo = new RaycastHit();
            if (Physics.Raycast(ray, out hitInfo))
            {
                float distance = hitInfo.distance;
                // 발 밑의 오브젝트와의 거리가 일정 이하라면, 착지 모션 재생 시작
                if (distance < landHeight)
                {
                    anim.SetBool("Land", true);
                    anim.SetBool("Fall", false);
                }
                else
                {
                    anim.SetBool("Land", false);
                    anim.SetBool("Fall", true);
                    if (distance > fallFarStandard)
                        isFarFromFloor = true;
                    else
                        isFarFromFloor = false;
                }
            }
        }

        // 콜라이더 사이즈 초기화
        void resetCollider()
        {
            col.height = orgColHight;
            col.center = orgVectColCenter;
        }

        // 이동이 가능한 상태에서 이동 키가 입력되었을 때
        void MoveEntered()
        {
            h = Input.GetAxis("Horizontal");
            v = Input.GetAxis("Vertical");
            // 키 입력에 따라 걷기/달리기 애니메이션(locoState를 Walk와 Run으로 나눔)
            anim.SetBool("Running", Input.GetKey(run_key));
            // 키 입력에 대응하여 회전
            PlayerRotate();
            // 키 입력에 따라 이동 bool 변경
            if (Mathf.Abs(h) > 0.1f || Mathf.Abs(v) > 0.1f)
                anim.SetBool("Move", true);
            else
            {
                anim.SetBool("Move", false);
                // 이동 애니메이션 재생 중이 아닐때는 움직이지 않도록
                velocity = Vector3.zero;
            }
        }

        // 카메라가 보는 각도와 입력값에 맞게 유니티짱 회전
        void PlayerRotate()
        {
            if (v > 0.1f)
            {
                if (h > 0.1f)
                    transform.eulerAngles = Vector3.up *
                    (currentDirection + Mathf.Lerp(0f, 45f, h));
                else if (h < -0.1f)
                    transform.eulerAngles = Vector3.up *
                    (currentDirection - Mathf.Lerp(0f, 45f, -h));
                else
                    transform.eulerAngles = Vector3.up * currentDirection;
            }
            else if (v < -0.1f)
            {
                if (h > 0.1f)
                    transform.eulerAngles = Vector3.up *
                    (currentDirection + 180f - Mathf.Lerp(0f, 45f, h));
                else if (h < -0.1f)
                    transform.eulerAngles = Vector3.up *
                    (currentDirection + 180f + Mathf.Lerp(0f, 45f, -h));
                else
                    transform.eulerAngles = Vector3.up * (currentDirection + 180f);
            }
            else if (h > 0.1f)
                transform.eulerAngles = Vector3.up * (currentDirection + 90f);
            else if (h < -0.1f)
                transform.eulerAngles = Vector3.up * (currentDirection - 90f);
        }

        // 점프할 수 있는 상태에서 점프 키가 입력되었을 때, 점프 시작과 동시에 1번만 처리할 구문
        void JumpEntered()
        {
            // 점프가 잘 안먹히던데 고쳐보자
            // 원인 : 키 입력 인식이 Fixed Update 내에 있어 키 입력이 되었다가 안되었다 함
            // 해결 : 키 입력 인식 구문을 Update()로 빼서 해결
            // 애니메이션 상태가 locoState일 때만 점프 가능 이라는 구문 삭제
            // y축 양의 방향으로 힘을 가하는 구문 삭제
            // -> 애니메이션 재생만으로도 위로 점프하는 것을 시각적으로 보여주고
            // -> 힘을 가해도 아주 살짝 위로 뜨는 정도에 불과하기에 불필요한 연산이라 판단
            // 점프 애니메이션 재생 시작
            jump = Input.GetButton("Jump");
            if (jump)
            {
                anim.SetBool("Jump", true);
                if (anim.GetBool("Running"))
                    initSpeed = runSpeed;
                else
                    initSpeed = walkSpeed;
                // 현재 속도 방향을 정규화 (크기는 1, 방향에 따른 비율만)
                initDirection = rb.velocity.normalized;
            }
        }

        // 현재 애니메이션 상태에 따라 수행할 내용
        void ByAnimState()
        {
            // 가만히 있는 상태 (idle)
            if (currentBaseState.fullPathHash == idleState)
                Idle();
            // 휴식 모션 재생 중
            else if (currentBaseState.fullPathHash == restState01 ||
                     currentBaseState.fullPathHash == restState02 ||
                     currentBaseState.fullPathHash == restState03)
                Rest();
            // 걷기 / 달리기 애니메이션 재생 중
            else if (currentBaseState.fullPathHash == walkState || 
                     currentBaseState.fullPathHash == runState)
                Moving();
            // 점프 애니메이션 재생 중
            else if (currentBaseState.fullPathHash == jumpState)
                JumpingUp();
            // 낙하 애니메이션 재생 중
            else if (currentBaseState.fullPathHash == fallState)
                Falling();
            // 착지 애니메이션 재생 중
            else if (currentBaseState.fullPathHash == landState)
                Landing();
            // 슬라이딩 애니메이션 재생 중
            else if (currentBaseState.fullPathHash == slideState)
                Sliding();
        }

        // Idle 애니메이션 재생 중
        void Idle()
        {
            resetCollider();
            // 휴식은 기존 : idle 상태에서 스페이스(점프) 키
            // -> 변경 : idle 상태에서 조작 없이 일정 시간이 지난다면 랜덤 휴식 모션 재생
            restCount += Time.deltaTime;
            if (restCount > restDelay)
            {
                anim.SetBool("Rest", true);
                anim.SetInteger("RestRandom", Random.Range(1, 4));
                restCount = 0f;
            }
            // 키 입력이 있었다면 휴식 상태로 전환하는 카운터 초기화
            if (Input.anyKey)
                restCount = 0f;
        }

        // 휴식 애니메이션 재생 중
        void Rest()
        {
            restCount += Time.deltaTime;
            // 카운트 시간이 지났거나 아무 키나 입력되었다면, 휴식 상태 종료
            if (restCount > restDelay || Input.anyKey)
            {
                anim.SetBool("Rest", false);
                restCount = 0f;
            }
        }

        // 걷기/달리기 애니메이션 재생 중
        void Moving()
        {
            // 1. 이동
            // 방향 벡터 (이동은 캐릭터 좌표계의 z축 양의 방향으로만 이동하게끔 수정)
            velocity = transform.forward;
            // 달리기 키 입력 여부에 따라 달리기/걷기 속도
            if (anim.GetBool("Running"))
                velocity *= runSpeed;
            else
                velocity *= walkSpeed;

            // 2. 슬라이딩 (이동 중에만 가능)
            // 슬라이딩 키 입력 인식
            slide = Input.GetKey(Slide_key);
            // 슬라이딩 키를 눌렀다면, 해당 방향으로 슬라이딩
            if (slide)
            {
                anim.SetBool("Slide", true);
                // 달리기/걷기 도중에 사용 시 슬라이딩 속력이 달라진다
                if (anim.GetBool("Running"))
                    initSpeed = runSpeed * 1.5f;
                else
                    initSpeed = walkSpeed * 1.5f;

                // 현재 속도 방향을 정규화 (크기는 1, 방향에 따른 비율만)
                initDirection = rb.velocity.normalized;
                // 슬라이딩 속도 비율 초기화
                speedRate = 0f;
            }
        }
        //점프, 낙하 중 콜라이더 변경
        void ColliderChange_Jump()
        {
            // animator에서 점프 높이에 따른 값(0 ~ 1 사이 값)을 받아와서 콜라이더 높이, 중심을 갱신
            float jumpHeight = anim.GetFloat("JumpHeight");
            col.height = orgColHight - jumpHeight;
            float adjCenterY = orgVectColCenter.y + jumpHeight;
            col.center = Vector3.up * adjCenterY;
        }
        // 중력가속도에 따른 y축 음방향 속도 증가 
        void GravityEffect()
        {
            // 자유 낙하 최대 속도가 되지 않았다면, 중력가속도에 따라 속도 증가
            if (velocity.y > vel_Y_Limit)
            {
                // V = V0 + a * t
                velocity.y -= vel_Y_inAir;
            }
        }
        // 점프 시작 -> 최고점 도착까지
        void JumpingUp()
        {
            ColliderChange_Jump();
            anim.SetBool("Jump", false);
        }
        // 떨어지는 모든 상황에 대해
        void Falling()
        {
            ColliderChange_Jump();
            // 착지 감속에 쓰기 위해 미리 초기화
            speedRate = 0f;
            // 중력 적용
            GravityEffect();
        }
        // 착지
        void Landing()
        {
            resetCollider();
            GravityEffect();
            speedRate += decreaseRate[0] * Time.deltaTime;
            velocity = initDirection * Mathf.Lerp(initSpeed, 0f, speedRate);
        }


        // 슬라이딩 중
        void Sliding()
        {
            // 슬라이딩 애니메이션 재생이 끝나면 자동으로 idle로 가게
            anim.SetBool("Slide", false);
            // 콜라이더 높이, 중심을 슬라이딩 중의 캐릭터와 맞게끔 조정
            col.height = slideColHeight;
            col.center = slideColCenter;
            // 이동 속도는 슬라이딩 초기엔 빠르나 느려지도록
            speedRate += decreaseRate[1] * Time.deltaTime;
            velocity = initDirection * Mathf.Lerp(initSpeed, 0f, speedRate);
        }

        // 슬라이딩 애니메이션이 끝날 때 idle 애니메이션으로 돌리기 위한 이벤트
        public void SlideEndEvent()
        {
            anim.SetTrigger("SlideEnd");
        }
    }
}

걷기 -> 달리기 -> 달리기 중 슬라이딩 -> 걷기 중 슬라이딩 -> 달리기 중 점프 -> 걷기 중 점프