Patrick's Devlog

[Unity Project] 캐릭터 이동 보완 및 적 공격 본문

Project/Unity Engine

[Unity Project] 캐릭터 이동 보완 및 적 공격

Patrick_ 2022. 7. 7. 17:40

우선 3인칭을 기준으로 구현을 진행해야 하므로, 카메라를 뒤에 두어 진행하였다. 메인 캐릭터 안에 카메라를 넣고 진행했었는데, 그러다보니 카메라의 시야가 회전하면 캐릭터도 같이 회전되는 일이 발생되었다. 어떤식으로 해결할 지 고민하던 중, 유튜브에서 베르님의 강의를 참고하여 캐릭터 이동을 보완하였다. 자세한 영상은 https://www.youtube.com/watch?v=P4qyRyQdySw 여기서 참고하면 된다. 


앞서 언급했듯이, 카메라를 메인 캐릭터에 넣으면 캐릭터도 같이 회전하게 되는 불상사가 발생한다. 이를 막기 위해서 빈 프로젝트인 Character를 생성해주고, 그 안에 메인 캐릭터와 카메라 암을 넣어주었다. 

Hierarchy에서 수정된 부분

카메라 암에는 메인 카메라가 들어가 있다. 이렇게 수정한 후 코드를 수정해보았다. 우선 변수들을 먼저 살펴보자.

public class MainPlayer : MonoBehaviour
{
    private Animator animator;
    private Rigidbody rb;
    private Vector2 mouseInput; // 마우스 회전에 쓰일 변수

    [SerializeField]
    private float charSpeed = 5.0f; // 캐릭터 속도
    [SerializeField]
    private Transform characterBody; // 메인 캐릭터
    [SerializeField]
    private Transform cameraArm; // 메인 캐릭터의 카메라
    ...
}

유니티 툴에서도 변경할 수 있게 캐릭터 속도와 메인 캐릭터, 카메라를 SerializeField로 지정하였다. 메인 캐릭터에는 캐릭터의 자식에 저장된 MainPlayer를 넣어줄 것이며, 카메라부분에는 CameraArm을 넣어줄 것이다. 

 

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        animator = characterBody.GetComponentInChildren<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        Rotate();
    }

    private void FixedUpdate() // 이동 관련 함수는 FixedUpdate가 효율이 더 좋다고 함
    {
        Move();
    }

Rotate() 함수는 FixedUpdate에 넣어서 진행을 하면 마우스 회전률이 매우 느려졌었다. 시간의 제약을 받지 않는 Update로 따로 빼주었으며, Move()는 그대로 FixedUpdate에 넣어주었다. 

 

    void Move()
    {
        Vector2 moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
        // 이동 방향키 입력값 (수평 방향, 수직 방향)
        bool isWalk = moveInput.magnitude != 0; // 이동 방향키 입력 판정 (0보다 크면 입력 발생)
        animator.SetBool("isWalk",isWalk); // 애니메이션 세팅
        
        if (isWalk)
        {
            Vector3 lookForward = new Vector3(cameraArm.forward.x, 0f, cameraArm.forward.z).normalized;
            // 카메라가 바라보는 방향
            Vector3 lookRight = new Vector3(cameraArm.right.x, 0f, cameraArm.right.z).normalized;
            // 카메라 오른쪽 방향
            Vector3 moveDir = lookForward * moveInput.y + lookRight * moveInput.x;
            // 이동 방향

            characterBody.forward = moveDir; // 이동할 때 이동방향 바라보게 세팅
            transform.position += moveDir * Time.deltaTime * charSpeed; // 이동
        }
    }

전 코드와 다르게 카메라 방향에서 높이값을 제거해 캐릭터의 위치를 추출해낸다. 이런식으로 코딩을 하게 되면 문제점이 하나 생기게 된다. 다른 물체와 부딪히며 키보드 입력을 받으면 캐릭터는 멈춰있고, 카메라는 그대로 움직여 시점이 달라지게 된다. 이 부분에 대해서는 본 유튜브의 댓글에 해결 방법이 적혀있었다. 본 코드를 설명한 후 마저 설명하도록 하겠다. 

 

    void Rotate()
    {
        mouseInput.x = Input.GetAxis("Mouse X"); // 마우스 좌우 수치
        mouseInput.y = Input.GetAxis("Mouse Y"); // 마우스 위아래 수치
        Vector3 camAngle = cameraArm.rotation.eulerAngles;
        // 카메라의 원래 각도를 오일러 각으로 저장
        float x = camAngle.x - mouseInput.y;
        // 카메라의 피치 값 계산

        // 캐릭터 카메라 상하 움직임 제한 (위쪽으로 70도, 아래쪽으로 25도 이상 움직이지 못하게 제한)
        if (x < 180f) x = Mathf.Clamp(x, -1f, 70f);
        else x = Mathf.Clamp(x, 335f, 361f);

        cameraArm.rotation = Quaternion.Euler(x, camAngle.y + mouseInput.x, camAngle.z);
        // 카메라 암 회전
    }

다음 카메라 회전 부분이다. 마우스의 수치들을 각각 저장한 후의 카매라의 원래 각도를 오일러 각으로 저장하여 이를 이용해 카메라를 회전시킨다. 상하 움직임을 제한하지 않으면, 카메라가 다방면에서 캐릭터를 비출 수 있으므로 이를 막기 위해 움직임을 제한하는 코드까지 추가해주었다. 

 

이렇게 메인 캐릭터의 코드를 완성시켰다. Jump 부분은 비정상적인 행동이 많아 추후 시간이 남을 때 제대로 구현하고자 하여 우선 지금은 Jump를 제거하였다. 그리고 이제 캐릭터가 오브젝트에 부딪힐 때 카메라만 움직이는 것을 해결하고자, 카메라 암에 스크립트를 따로 만들어 저장해주었다. 코드는 아래와 같다. 

 

public class CameraArm : MonoBehaviour
{
    public Transform character;
    public Vector3 coordinate;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        transform.position = character.position + coordinate;
    }
}

character에는 메인 캐릭터 오브젝트를 넣어주었고, coordinate은 카메라 암을 고정할 좌표를 설정하였다. 캐릭터의 위치를 받아 고정 좌표를 더해 현 위치로 바꿔주면 카메라도 움직이지 않고 그대로 메인 캐릭터를 바라보게 될 것이다. 단순한 방법이지만 떠오르지 않아 조금 헤맸었다. 

 

이렇게 코드를 구현하면 자연스럽게 움직임이 완성될 것이다. 다음은 공격하는 모션과 적을 추가하였다. 

적 추가

애셋 스토어에서 적을 하나 다운받아 가져왔다. 그리고 애니메이터 부분에는 공격을 추가하여 연결해주었다. 왼쪽 마우스를 클릭하게 되면, 어택이라는 애니메이션이 실행되게끔 추가적으로 바꾸어주었다. 

수정된 애니메이션

그리고 여기서 Attack이라는 트리거 변수도 추가해서 화살표에 넣어주었다. 앞서 글에 언급한 Jump 처럼 Any State를 사용하여 왼쪽 마우스를 클릭할 때 애니메이션이 실행되는 코드를 작성하였다. 본 코드는 캐릭터의 자식인 메인 캐릭터에 스크립트를 넣어주었다.

public class PlayerAttack : MonoBehaviour
{
    private Animator animator;
    // Start is called before the first frame update
    void Start()
    {
        animator = GetComponent<Animator>(); 
    }

    // Update is called once per frame
    void Update()
    {
        Attack();
    }

    void Attack()
    {
        if (Input.GetMouseButtonDown(0)) // 마우스 왼쪽 버튼을 선택했을 때
        {
            animator.SetTrigger("Attack"); // 어택 애니메이션 실행
        }
    }
    ...
}

이렇게 코드까지 실행하면 아래의 결과처럼 나오게 된다.

메인 캐릭터 이동 및 공격

이제 적에게 닿였을 때와 적을 검으로 공격했을 때 로그를 띄우도록 한다. 메인 캐릭터 안에 존재하는 스크립트에서 코드를 짜주었다.

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Enemy"))
        {
            Debug.Log("아파!");
        }
    }

메인 캐릭터가 적에게 닿였을 때 "아파!"라는 로그를 띄우게끔 하였다. 그리고 메인 캐릭터의 검에는 "Sward"라는 태그를 지정해주고, 적에게 스크립트를 만들어 아래와 같이 코드를 짜주었다. 

public class SlimeEnemy : MonoBehaviour
{
    private Animator playerAnimator;
    private GameObject playerObject;
    // Start is called before the first frame update
    void Start()
    {
        playerObject = GameObject.Find("MainPlayer");
        playerAnimator = playerObject.GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.CompareTag("Sward") && playerAnimator.GetCurrentAnimatorStateInfo(0).IsName("Attack"))
        {
            Debug.Log("몬스터가 맞았어!");
        }
    }
}

우선 메인 캐릭터를 찾아 게임 오브젝트로 저장하고, 메인 캐릭터가 진행하고 있는 애니메이션을 찾는다. 그리고 몬스터와 검이 닿았을 때, 검의 태그가 Sward이고 메인 캐릭터의 행동이 Attack 중이라면 "몬스터가 맞았어!"라는 로그를 띄우게 된다. 

 

위의 코드를 모두 종합하여 아래의 결과를 확인할 수 있다

최종 결과

카메라의 회전으로 인해 조금 시간이 많이 걸렸다. 그리고 공격 부분에서도 검과 적을 어떤식으로 판단할지 감이 안잡혀 조금 헤맸었다. Collision을 사용하지 말고 Trigger를 통해 서로 만나면 디버그가 뜨게끔 해보자고 생각하여 위의 코드처럼 구현해보았다. 아무래도 조금 보완 작업을 더 거쳐야 할 것이라 생각한다.