본문 바로가기
C#

[C#][Unity] 캐릭터 공격 모션 구현하기

by teamnova 2022. 1. 27.
728x90

https://stickode.tistory.com/327 

위 링크(캐릭터 회전 구현하기)에 이어서 이번에는 캐릭터 공격 모션을 구현해보도록 하겠습니다.

 

실행 환경

개발 툴: Unity 2019.4.21f1

IDE : vscode

 

공격을 구현하기 위해서 일단 공격을 받을 오브젝트를 만들도록 하겠습니다.

Hierarchy-SampleScene에 큐브를 하나 추가하고 위치를 ( 5, 0.5 .0 ) 으로 설정합니다.

공격받을 오브젝트 생성
큐브 위치는 (5, 0.5, 0) 으로 설정

 

생성한 큐브와 바닥이 클릭됐을 때를 구분하기 위해서 태그를 설정해줍니다.

 

이제 MyUnit 스크립트를 켜고 아래 코드를 붙여넣어 줍니다.

MyUnit.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyUnit : MonoBehaviour
{
    public float speed;      // 캐릭터 움직임 스피드
    public CharacterController characterController; // 캐릭터 컨트롤러
    public Vector3 movePoint; // 이동 위치 저장
    public Camera mainCamera; // 메인 카메라
    public Vector3 cameraOffset; // 카메라 
    public Animator animator; // 애니메이터
    private Vector3 unitPlanePosition; // 유닛의 바닥 위치 ( 거리 계산할 때 사용 )
    public GameObject target; // 공격 타겟
    public StateType stateType; // 유닛의 상태
    public enum StateType { none, move, attack } // 유닛 상태 Enum 타입
    public float attckRange = 2f; // 사거리

    public enum AttackStateType { ready, swing, cooltime } // 유닛의 공격 상태 Enum 타입
    public AttackStateType attackStateType; // 공격 상태를 담는 변수
    WaitForSeconds attackCooltimeWaitForSeconds; // 공격 쿨타임 코루틴용 WaitforSeconds;
    public Coroutine attackCoroutine; // 공격 코루틴을 담는 변수. 코루틴을 취소시키기 위해서 사용

    void Start()
    {
        speed = 4.0f;
        mainCamera = Camera.main;
        characterController = GetComponent<CharacterController>();
        animator = GetComponentInChildren<Animator>();
        // 공격 쿨타임 코루틴용 WaitForSecondes 셋팅
        setAttackCooltimeWaitForSecondes(1.5f);
        attackStateType = AttackStateType.ready;
    }

    public void setAttackCooltimeWaitForSecondes(float attackCooltime)
    {
        attackCooltimeWaitForSeconds = new WaitForSeconds(attackCooltime);
    }

    void Update()
    {

        // 좌클릭 이벤트가 들어왔다면
        if (Input.GetMouseButtonUp(0))
        {
            // 카메라에서 레이저를 쏜다.
            Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
            // Scence 에서 카메라에서 나오는 레이저 눈으로 확인하기
            Debug.DrawRay(ray.origin, ray.direction * 10f, Color.red, 1f);

            // 레이저가 뭔가에 맞았다면
            if (Physics.Raycast(ray, out RaycastHit raycastHit))
            {
                // 맞은 물체가 Enemy 라면
                if (raycastHit.collider.CompareTag("Enemy"))
                {
                    target = raycastHit.transform.gameObject;
                    stateType = StateType.attack;
                }
                else
                {
                    // 맞은 위치를 목적지로 저장
                    movePoint = raycastHit.point;
                    movePoint.y = 0;
                    stateType = StateType.move;
                }

                CancelSwing();
            }

        }

        switch (stateType)
        {
            case StateType.move:
                MoveCommand();
                break;
            case StateType.attack:
                AttackCommand();
                break;
        }

        // 매 업데이트 메소드가 실행될 때마다 카메라의 위치를 오브젝트의 위치 + 카메라 오프셋의 위치로 바꾼다.
        mainCamera.transform.position = transform.position + cameraOffset;
    }

    // MoveCommand() 는 이동 명령 메소드다.
    // 목적지까지 거리를 확인해서 이동하거나 멈추게 만든다.
    void MoveCommand()
    {
        unitPlanePosition.x = transform.position.x;
        unitPlanePosition.z = transform.position.z;
        // 목적지까지 거리가 0.5f 보다 멀다면
        if (Vector3.Distance(unitPlanePosition, movePoint) > 0.5f)
        {
            // 이동
            Move();
        }
        else
        {
            // 멈춤
            Stop();
        }
    }

    // Move() 는 유닛을 이동시키는 메소드다.
    // 달리기 애니메이션을 실행하고, 이동방향으로 회전 후 위치를 이동시킨다.
    void Move()
    {
        // 달리기 애니메이션 시작
        animator.SetBool("isRun", true);
        // 회전
        Rotation(movePoint);
        // thisUpdatePoint 는 이번 업데이트(프레임) 에서 이동할 포인트를 담는 변수다.
        // 이동할 방향(이동할 곳-현재 위치) 곱하기 속도를 해서 이동할 위치값을 계산한다.
        Vector3 thisUpdatePoint = (movePoint - transform.position).normalized * speed;
        // characterController 는 캐릭터 이동에 사용하는 컴포넌트다.
        // simpleMove 는 자동으로 중력을 계산해서 이동시켜주는 메소드다.
        // 값으로 이동할 포인트를 전달해주면 된다.
        characterController.SimpleMove(thisUpdatePoint);

    }

    // Stop() 은 유닛을 멈추는 메소드다.
    void Stop()
    {
        // 달리기 애니메이션 종료
        animator.SetBool("isRun", false);
    }


    // Rotation() 은 유닛을 targetPoint 쪽으로 회전시키는 메소드다.
    void Rotation(Vector3 targetPoint)
    {
        // 현재 위치에서 목적지쪽 벡터 계산
        Vector3 relativePosition = targetPoint - transform.position;
        relativePosition.y = 0;
        // 회전 방향 계산
        Quaternion rotation = Quaternion.LookRotation(relativePosition, Vector3.up);
        // 회전방향 셋팅
        transform.rotation = rotation;
    }

    // AttackCommand() 는 공격 명령 메소드다.
    // 타겟이 있으면 목적지를 타겟이 있는 위치로 바꾸고 목적지까지 거리에 따라 이동하거나 공격한다.
    void AttackCommand()
    {
        if (target == null) return;
        movePoint = target.transform.position;
        float distance = Vector3.Distance(transform.position, movePoint);

        if (distance < attckRange)
            Swing();
        else
            Move();

    }

    // Swing() 은 무기 휘두르기 메소드다.
    // 목적지(타겟 위치) 방향으로 회전한 후 멈추고 무기 휘두르기를 시작한다.
    void Swing()
    {
        Rotation(movePoint);
        Stop();
        if (attackStateType == AttackStateType.ready)
        {
            attackCoroutine = StartCoroutine(SwingIEnumerator());
        }
    }

    // AttackCancel() 는 공격 취소 메소드다.
    // 공격 상태가 공격 모션중이면 공격을 취소한다.
    void CancelSwing()
    {
        // 공격 모션이 끝나지 않았다면
        if (attackStateType == AttackStateType.swing)
        {
            // 유닛 상태가 이동이면 
            if (stateType == StateType.move)
                // 트리거로 공격 트리거를 덮어버린다.
                animator.SetTrigger("cancel");

            // 상태가 이동이 아니면(공격이면)
            else
                // 공격 트리거를 초기화한다.
                animator.ResetTrigger("swing");

            // 공격 코루틴을 중지한다.
            StopCoroutine(attackCoroutine);
            // 공격 가능한 상태로 바꾼다.
            attackStateType = AttackStateType.ready;
        }

    }

    // AttackIEnumerator() 는 코루틴에 넣을 공격 메소드다.
    // 공격 상태를 모션으로 바꾸고 공격 애니메이션을 실행한다.
    // 지정한 공격 쿨타임 시간이 지나고 나면 다시 공격 상태를 준비 완료로 변경한다.
    public IEnumerator SwingIEnumerator()
    {
        attackStateType = AttackStateType.swing;
        animator.SetTrigger("swing");
        yield return attackCooltimeWaitForSeconds;
        attackStateType = AttackStateType.ready;
    }
}

 

지난 시간과 차이나는 부분은 유닛 상태 변수( 이동 or 공격 ), 공격 사거리 변수, 공격 상태 변수( 공격 준비완료, 공격 모션중, 쿨타임 ) 외 쿨타임 시간을 담는 변수가 추가됐습니다.

그리고 Update() 에서 클릭 이벤트가 들어왔을 때 맞은 오브젝트가 적인지 확인하고 적이면 유닛 상태를 공격 상태로 바꿔줍니다.

 

유닛 상태가 이동이면 이동 명령을 내리고, 유닛 상태가 공격이면 공격 명령을 내립니다.

 

이동 명령은 지난번에 만든 부분과 같고, 공격 명령은 타겟까지 거리가 공격 사거리보다 짧으면 무기를 휘두르고, 길면 타겟 위치로 이동하게 만듭니다.

 

 

이제 스크립트를 저장하시고 다시 유니티로 돌아옵니다.

 

스크립트에서 사용했던 애니메이션 트리거인 swing, cancel 을 설정하도록 하겠습니다.

 

Hierarchy 에 Capsule 안에 넣어둔 캐릭터 프리펩(DogPolyart)을 선택해주세요.

그리고 Animator 컴포넌트에 들어있는 DogControl 을 더블 클릭해주세요.

 

다음으로 트리거들을 만들고 트랜잭션들을 설정해주세요.

 

방법은 아래 영상을 보고 따라해주세요.

 

내용 추가 )

추가한 트리거는 cancel, swing 입니다.

왼쪽(idle 과 연결된) 트랜잭션에 cancel 을 설정해주시고

오른쪽(attack과 연결된) 트랜잰션에 swing 을 설정해주세요.

 

다음으로 공격 모션이 끝났을 때 공격 상태를 쿨타임으로 변경해 보도록 하겠습니다.

 

Project 에 Scripts 폴더에 UnitEventListener 라는 이름으로 스크립트 파일을 생성해주세요.

 

그리고 아래 코드를 복사 붙여넣기 해주세요.

 

UnitEventListener.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UnitEventListener : MonoBehaviour
{
    MyUnit myUnit;
    void Start()
    {
        // 부모 오브젝트에 있는 MyUnit 컴포넌트를 가져오기
        myUnit = GetComponentInParent<MyUnit>();

    }

    public void FinishAttack()
    {
        // 공격 상태가 무기 휘두르기 상태면
        if (myUnit.attackStateType == MyUnit.AttackStateType.swing)
        {
            // 공격 상태를 쿨타임으로 변경
            myUnit.attackStateType = MyUnit.AttackStateType.cooltime;
        }
    }

}

 

저장하시고 유니티로 돌아와주세요.

 

다음으로 애니메이션에 Event 를 설정해주세요.

 

방법은 아래 영상을 보고 따라해주세요.

 

내용 추가 )

Event 명은 FinishAttack 으로 해주세요. (UnitEventListener 스크립트 안에 정의한 메소드명과 동일한 이름으로 해주세요.)

그리고 마지막으로 Unit Event Listener 를 애니메이터가 들어있는 오브젝트(DogPolyart)에 추가해주세요.

 

 

마지막으로 실행 영상입니다.