[Skill] 간단한 콤보 어택 만들기



메인 카메라에 타겟 추적, 타겟과의 거리 조절, 회전을 제어하는 CameraController스크립트를 만든다.

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

public class CameraController : MonoBehaviour
{
    [SerializeField] private Transform target;
    [SerializeField] private float minDistance=3;
    [SerializeField] private float maxDistance = 30;
    [SerializeField] private float wheelSpeed = 500;
    [SerializeField] private float xMoveSpeed = 500;
    [SerializeField] private float yMoveSpeed = 250;
    private float yMinLimit = 5;
    private float yMaxLimit = 80;
    private float x, y;
    private float distance;

    private void Awake()
    {
        distance=Vector3.Distance(transform.position, target.position);
        Vector3 angles = transform.eulerAngles;
        x= angles.y;
        y= angles.x;
    }
    private void Update()
    {
        x+=Input.GetAxis("Mouse X")*xMoveSpeed*Time.deltaTime;
        y-= Input.GetAxis("Mouse Y") * yMoveSpeed * Time.deltaTime;

        y = ClampAngle(y, yMinLimit, yMaxLimit);

        transform.rotation=Quaternion.Euler(y , x,0);

        distance-=Input.GetAxis("Mouse ScrollWheel")*wheelSpeed*Time.deltaTime;
        distance = Mathf.Clamp(distance, minDistance, maxDistance);
    }
    private void LateUpdate()
    {
        if (target == null) return;

        transform.position=transform.rotation*new Vector3(0,0,-distance) + target.position; 
    }
    
    private float ClampAngle(float angle, float min, float max)
    {
        if (angle < -360) angle += 360;
        if (angle > 360) angle -= 360;

        return Mathf.Clamp(angle, min, max);
    }
}

위 스크립트를 메인 카메라의 컴포넌트로 추가해주고 Player오브젝트의 자식으로 빈 게임 오브젝트를 생성하고 이름을 CameraTarget으로 바꾸고 CameraController에 할당해준다.

image

다음으로 이동이 가능한 오브젝트의 이동 제어에 쓰일 Movement3D 스크립트를 만든다.

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

public class Movement3D : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5;
    private Vector3 moveDirection;

    private CharacterController characterController;

    public float MoveSpeed
    {
        set { moveSpeed = Mathf.Clamp(value,2.0f,5.0f); }
    }

    private void Awake()
    {
        characterController = GetComponent<CharacterController>();
    }

    private void Update()
    {
        characterController.Move(moveDirection*moveSpeed*Time.deltaTime);
    }

    public void MoveTo(Vector3 direction)
    {
        moveDirection=new Vector3(direction.x,moveDirection.y,direction.z);
    }
}

다음으로 플레이어 제어에 쓰일 PlayerController스크립트를 만든다.

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

public class PlayerController : MonoBehaviour
{
    [SerializeField]
    private Transform cameraTransform;
    private Movement3D movement3D;

    private void Awake()
    {
        //마우스 커서 안보이게 해준다
        Cursor.visible = false;
        //마우스 위치 고정
        Cursor.lockState = CursorLockMode.Locked;

        movement3D = GetComponent<Movement3D>();
    }

    private void Update()
    {
        float hAxis = Input.GetAxis("Horizontal");
        float vAxis = Input.GetAxis("Vertical");

        //이동속도 설정 (앞으로 이동할때만 5, 나머지 2)
        movement3D.MoveSpeed = vAxis > 0 ? 5.0f : 2.0f;
        //이동 함수 호출( 카메라가 보고 있는 방향을 기준으로 방향키에 따라 이동
        movement3D.MoveTo(cameraTransform.rotation * new Vector3(hAxis, 0, vAxis));
    }
}

플레이어 오브젝트에 PlayerController스크립트,Movement3D스크립트,CharacterController컴포넌트를 추가해준다. 그리고 PlayerController에 있는 CameraTransform 프로퍼티에 메인 카메라를 할당해준다.

image

플레이어 캐릭터가 카메라가 바라보는 전방방향을 계속해서 바라보도록 PlayerController스크립트를 수정해준다.

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

public class PlayerController : MonoBehaviour
{
    [SerializeField]
    private Transform cameraTransform;
    private Movement3D movement3D;

    private void Awake()
    {
        //마우스 커서 안보이게 해준다
        Cursor.visible = false;
        //마우스 위치 고정
        Cursor.lockState = CursorLockMode.Locked;

        movement3D = GetComponent<Movement3D>();
    }

    private void Update()
    {
        float hAxis = Input.GetAxis("Horizontal");
        float vAxis = Input.GetAxis("Vertical");

        //이동속도 설정 (앞으로 이동할때만 5, 나머지 2)
        movement3D.MoveSpeed = vAxis > 0 ? 5.0f : 2.0f;
        //이동 함수 호출( 카메라가 보고 있는 방향을 기준으로 방향키에 따라 이동
        movement3D.MoveTo(cameraTransform.rotation * new Vector3(hAxis, 0, vAxis));

        //회전 설정 (항상 앞만 보도록 캐릭터의 회전은 카메라와 같은 회전 값으로 설정)
        transform.rotation=Quaternion.Euler(0,cameraTransform.eulerAngles.y,0);
    }
}

이제 캐릭터의 뒷모습만 보게 된다.

다음으로 물리와 점프 설정을 위해 Movement3D 스크립트를 수정한다.

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

public class Movement3D : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5;
    [SerializeField] private float gravity = -9.81f;
    [SerializeField] private float jumpForce = 3.0f;
    private Vector3 moveDirection;

    private CharacterController characterController;

    public float MoveSpeed
    {
        set { moveSpeed = Mathf.Clamp(value,2.0f,5.0f); }
    }

    private void Awake()
    {
        characterController = GetComponent<CharacterController>();
    }

    private void Update()
    {
        if (characterController.isGrounded == false)
        {
            moveDirection.y += gravity * Time.deltaTime;
        }

        characterController.Move(moveDirection*moveSpeed*Time.deltaTime);
    }

    public void MoveTo(Vector3 direction)
    {
        moveDirection=new Vector3(direction.x,moveDirection.y,direction.z);
    }

    public void JumpTo()
    {
        if(characterController.isGrounded == true)
        {
            moveDirection.y = jumpForce;
        }
    }
}

점프 키 입력과 점프 함수 호출을 위해 PlayerController스크립트를 수정한다.

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

public class PlayerController : MonoBehaviour
{
    [SerializeField]
    private Transform cameraTransform;
    private Movement3D movement3D;

    private void Awake()
    {
        //마우스 커서 안보이게 해준다
        Cursor.visible = false;
        //마우스 위치 고정
        Cursor.lockState = CursorLockMode.Locked;

        movement3D = GetComponent<Movement3D>();
    }

    private void Update()
    {
        float hAxis = Input.GetAxis("Horizontal");
        float vAxis = Input.GetAxis("Vertical");

        //이동속도 설정 (앞으로 이동할때만 5, 나머지 2)
        movement3D.MoveSpeed = vAxis > 0 ? 5.0f : 2.0f;
        //이동 함수 호출( 카메라가 보고 있는 방향을 기준으로 방향키에 따라 이동
        movement3D.MoveTo(cameraTransform.rotation * new Vector3(hAxis, 0, vAxis));

        //회전 설정 (항상 앞만 보도록 캐릭터의 회전은 카메라와 같은 회전 값으로 설정)
        transform.rotation=Quaternion.Euler(0,cameraTransform.eulerAngles.y,0);

        if (Input.GetKeyDown(KeyCode.Space))
        {
            movement3D.JumpTo();
        }
    }
}

이제 스페이스바를 누르면 점프를 하게 된다.

다음으로 애니메이션 설정을 할 것이다. 그러기 위해 애니메이터 컨트롤러를 생성해준다. 그 다음 애니메이터 뷰로 가서 블랜드 트리를 하나 생성하고 이름을 Movement로 바꿔준다.

(1) 파라미터에 Float형으로 2개를 생성해주고 각각 이름을 Horizontal, Vertical로 바꿔준다.

(2) Blend Tree Type을 2D Simple Directional로 바꿔주고 파라미터에 Horizontal,Vertical을 등록한다.

(4) 모션 5개를 추가하고 각각 대기 앞으로 걷기, 뒤로 걷기 ,왼쪽으로 걷기, 오른쪽으로 걷기를 할당하고 알맞은 PosX,PosY를 입력한다.

image

그리고 다시 BaseLayer로 돌아가서 점프 애니메이션을 등록한다.

(1) Movement블랜드트리와 Jump스테이트를 이어주는 트렌지션을 만들어주고 MoveMent에서 Jump로가는 트랜지션에 조건으로 트리거형 파라미터 OnJump를 등록해준다.

image

이제 이 애니메이션들을 제어해주는 PlayerAnimator스크립트를 만들어보자.

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

public class PlayerAnimator : MonoBehaviour
{
    private Animator anim;

    private void Awake()
    {
        anim= GetComponent<Animator>(); 
    }

    public void OnMovment(float horizontal,float vertical)
    {
        anim.SetFloat("Horizontal", horizontal);
        anim.SetFloat("Vertical", vertical);
    }

    public void OnJump()
    {
        anim.SetTrigger("OnJump");
    }
}

이제 이 스크립트를 활용하기 위해 PlayerController스크립트를 수정해보자.

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

public class PlayerController : MonoBehaviour
{
    [SerializeField]
    private Transform cameraTransform;
    private Movement3D movement3D;
    private PlayerAnimator playerAnimator;

    private void Awake()
    {
        //마우스 커서 안보이게 해준다
        Cursor.visible = false;
        //마우스 위치 고정
        Cursor.lockState = CursorLockMode.Locked;

        movement3D = GetComponent<Movement3D>();
        playerAnimator = GetComponentInChildren<PlayerAnimator>();
    }

    private void Update()
    {
        float hAxis = Input.GetAxis("Horizontal");
        float vAxis = Input.GetAxis("Vertical");

        playerAnimator.OnMovment(hAxis, vAxis);

        //이동속도 설정 (앞으로 이동할때만 5, 나머지 2)
        movement3D.MoveSpeed = vAxis > 0 ? 5.0f : 2.0f;
        //이동 함수 호출( 카메라가 보고 있는 방향을 기준으로 방향키에 따라 이동
        movement3D.MoveTo(cameraTransform.rotation * new Vector3(hAxis, 0, vAxis));

        //회전 설정 (항상 앞만 보도록 캐릭터의 회전은 카메라와 같은 회전 값으로 설정)
        transform.rotation=Quaternion.Euler(0,cameraTransform.eulerAngles.y,0);

        if (Input.GetKeyDown(KeyCode.Space))
        {
            movement3D.JumpTo();
            playerAnimator.OnJump();
        }
    }
}

PlayerAnimator를 Animator컴포넌트를 가지고 있는 Toko_sum오브젝트에 추가하고 실행하면 작성한대로 움직이고 애니메이션일 재생되는 걸 볼 수 있다.

콤보 어택

콤보 어택 구현을 해보자.

(1) 애니메이터 뷰로 가서 마우스 우클릭 Create Sub State Machine을 눌러서 서브 스테이트 머신 하나를 만들고 이름을 ComboAttack으로 바꿔주자.

image

(2) 서브 스테이트 머신 내부로 이동하여 공격 애니메이션들을 등록해주고 콤보어택이 가능하도록 어택끼리 트렌지션을 연결해준다.

image

위와 같이 있으면 공격 순서는 3 - 4 - 2 -1순으로 공격이 이루어진다.

(3) 콤보 어택은 도중에 중단이 될 수 있기 때문에 모든 공격 스테이트에서 Base Layer로 가는 트랜지션을 만들어 준다.(이때 MoveMent로 연결해주면 된다)

image

(4) 공격에서 공격 스테이트로 전이하기 위해서 트리거형 파라미터를 만들고 이름을 OnAttack으로 해준다.

(공격에서 공격으로의 상태 전이는 공격이 완전히 끝나고 다음 공격으로 넘어가도록 Has Exit Time을 true로 해준다.)

(5) 공격에서 (Up) Base Layer로의 상태 전이는 Exit Time을 0.9로 해주고, Transition Duration을 0.1로 해준다.

(TIP) 공격에서 공격으로 이어지는 상태전이의 Exit Time이 0.79, 공격에서 Base Layer로 이어지는 상태전이의 Exit Time이 0.9이기 때문에 공격에서 공격으로 이어지는 상태전이를 먼저 검사하고, 그 후에 Base Layer로 이어지는 상태전이를 검사한다. 즉, 공격에서 공격으로의 상태전이의 조건을 먼저 검사해서 충족하지 못하면 상태전이가 Base Layer로 간다.

(6) Base Layer에서 돌아가서 Movement에서 ComboAttack으로 이동할 수 있게 트랜지션을 만들어준다.여기서는 State Machine의 ComboAttack으로 연결해주면 된다.

(7) Movement에서 ComboAttack으로의 상태 전이는 아무때나 바로 이동 가능하게 Has Exit Time을 꺼주고, 컨디션에 OnAttack을 추가해준다.

이제 PlayerAnimator스크립트에 가서 콤보 어택을 하도록 수정한다.

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

public class PlayerAnimator : MonoBehaviour
{
    private Animator anim;

    private void Awake()
    {
        anim= GetComponent<Animator>(); 
    }

    public void OnMovment(float horizontal,float vertical)
    {
        anim.SetFloat("Horizontal", horizontal);
        anim.SetFloat("Vertical", vertical);
    }

    public void OnJump()
    {
        anim.SetTrigger("OnJump");
    }

    public void OnAttack()
    {
        anim.SetTrigger("OnAttack");
    }
}

그리고 PlayerController스크립에 가서 공격 함수를 호출해주는 코드를 넣어준다.

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

public class PlayerController : MonoBehaviour
{
    [SerializeField]
    private Transform cameraTransform;
    private Movement3D movement3D;
    private PlayerAnimator playerAnimator;

    private void Awake()
    {
        //마우스 커서 안보이게 해준다
        Cursor.visible = false;
        //마우스 위치 고정
        Cursor.lockState = CursorLockMode.Locked;

        movement3D = GetComponent<Movement3D>();
        playerAnimator = GetComponentInChildren<PlayerAnimator>();
    }

    private void Update()
    {
        float hAxis = Input.GetAxis("Horizontal");
        float vAxis = Input.GetAxis("Vertical");

        playerAnimator.OnMovment(hAxis, vAxis);

        //이동속도 설정 (앞으로 이동할때만 5, 나머지 2)
        movement3D.MoveSpeed = vAxis > 0 ? 5.0f : 2.0f;
        //이동 함수 호출( 카메라가 보고 있는 방향을 기준으로 방향키에 따라 이동
        movement3D.MoveTo(cameraTransform.rotation * new Vector3(hAxis, 0, vAxis));

        //회전 설정 (항상 앞만 보도록 캐릭터의 회전은 카메라와 같은 회전 값으로 설정)
        transform.rotation=Quaternion.Euler(0,cameraTransform.eulerAngles.y,0);

        if (Input.GetKeyDown(KeyCode.Space))
        {
            movement3D.JumpTo();
            playerAnimator.OnJump();
        }

        if (Input.GetMouseButtonDown(0))
        {
            playerAnimator.OnAttack();
        }
    }
}

이제 게임을 실행해서 마우스 좌클릭을 하면 콤보 어택을 하는 모습을 볼 수 있다.

적 피격 이벤트

원리는 다음과 같다

(1) 마우스 좌클릭 -> 공격 애니메이션 재생

(2) 공격 애니메이션의 특정 플레임에 이벤트 함수 호출 -> 공격 충돌 박스 AttackCollision 오브젝트 활성화

(3) 공격 충돌 박스에 오브젝트가 부딪히면? -> (피격 애니메이션, 오브젝트 색상 변경)

이제 위 원리를 이용해서 적을 피격해보자.

그 전에 적 오브젝트를 생성하자.

(1) 우선 플레이어의 공격을 받을 적 오브젝트를 만들어 주고 이름을 Enemy로 바꿔주자.

(2) 적 오브젝트를 위한 애니메이터 컨트롤러를 만들어서 이름을 Enemy로 바꿔주자.

(3) 충돌 확인을 위해서 capsule collider컴포넌트를 추가해주자.

(4) Material 설정을 위해 RPG-Character Mesh로 가서 RPG-Character를 적용해준다.

(5) Enemy 오브젝트의 애니메이션은 대기 상태와 플레이어에게 공격을 당했을 때 피격 애니메이션을 등록해준다.

(6) 피격 애니메이션은 어떤 애니메이션이 실행되고 있더라고 실행되어야 하기 때문에 Any State에서 GetHit로 트랜지션을 연결해주고 GetHit는 Idle로 연결해준다.

image

(7) Any State에서 GetHit로 이동하는 조건을 설정하기 위해서 파라미터에 트리거형으로 OnHit를 만들어준다.

(8) 적 오브젝트가 공격 당했을 때 피격 애니메이션을 재생시켜주는 EnemyAnimator스크립트를 만들어준다.

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

public class EnemyAnimator : MonoBehaviour
{
    private Animator anim;
    private SkinnedMeshRenderer meshRenderer;
    private Color originColor;

    private void Awake()
    {
        anim = GetComponent<Animator>();
        meshRenderer = GetComponent<SkinnedMeshRenderer>();
        originColor=meshRenderer.material.color;
    }

    public void TakeDamage(int damage)
    {
        Debug.Log(damage + "의 체력이 감소합니다.");

        anim.SetTrigger("OnHit");

        StartCoroutine("OnHitColor");
    }

    private IEnumerator OnHitColor()
    {
        meshRenderer.material.color = Color.red;

        yield return new WaitForSeconds(0.1f);

        meshRenderer.material.color = originColor;
    }
}

이 스크립트를 Enemy오브젝트에 컴포넌트로 추가해준다.

플레이어 공격 범위 설정

(TIP) 공격이 적중되는 충돌 범위를 손에 직접 연결할 수도 있고, 공격에 따라 공격범위의 크기를 다르게 설정할 수도 있다. 그리고 Box가 아닌 다른 콜라이더를 사용할 수도 있다.

(1) Cube오브젝트를 생성해서 Player오브젝트의 자식으로 넣어주고 이름을 AttackCollision으로 바꾼다.

(2) 위치와 크기를 수정하고 Mesh Renderer 체크를 해제한다.

(3) Box Collider의 is Trigger를 체크해주고 충돌처리를 위해 RigidBody컴포넌트를 추가하고 Use Gravity는 꺼준다.

(4) 공격 충돌 박스를 제어하는 PlayerAttackCollision스크립트를 만들어준다.

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

public class PlayerAttackCollision : MonoBehaviour
{
    private void OnEnable()
    {
        StartCoroutine("AutoDisable");
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Enemy"))
        {
            other.GetComponent<EnemyAnimator>().TakeDamage(10);
        }  
    }

    private IEnumerator AutoDisable()
    {
        yield return new WaitForSeconds(0.1f);

        gameObject.SetActive(false);
    }
}

위 스크립트를 AttackCollision오브젝트의 컴포넌트로 추가해준다.

(5) Enemy 오브젝트의 태그를 Enemy로 해준다.

(6) PlayerAnimator스크립트로 가서 공격 충돌 박스를 활성화해주는 코드를 추가해준다.

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

public class PlayerAnimator : MonoBehaviour
{
    [SerializeField] GameObject attackCollision;
    private Animator anim;

    private void Awake()
    {
        anim= GetComponent<Animator>(); 
    }

    public void OnMovment(float horizontal,float vertical)
    {
        anim.SetFloat("Horizontal", horizontal);
        anim.SetFloat("Vertical", vertical);
    }

    public void OnJump()
    {
        anim.SetTrigger("OnJump");
    }

    public void OnAttack()
    {
        anim.SetTrigger("OnAttack");
    }

    public void OnAttackCollision()
    {
        attackCollision.SetActive(true);
    }
}

여기서 OnAttackCollision()는 애니메이션이 실행되면서 특정 프레임에 호출 되도록 할 것이다.

(7) 공격 애니메이션으로가서 하단에 Events탭을 열어 특정 프레임에 호출 할 함수를 추가해준다.

image

(참고) 여기서 이벤트 함수로 추가할 수 있는 함수는 해당 애니메이션을 재생하는 애니메이터 컴포넌트와 같은 오브젝트에 포함된 스크립트 컴포넌트의 public 함수만 가능하다. 예를들어 현재 공격은 Player가 사용하고 있고, 플레이어의 공격 애니메이션을 재생하는것은 Toko_sum 오브젝트의 Animator컴포넌트가 하고있다. 그럼 공격 애니메이션에 이벤트 함수를 등록하려면 Toko_sum 오브젝트에 있는 컴포넌트여야한다.

이제 게임을 실행하고 적을 공격해보면 특정 프레임에 활성화되는 공격 범위 콜라이더에 적이 맞으면 적 오브젝트의 색상이 변하고 데미지를 입는 모습을 확인할 수 있다.

image




© 2022. by KSC

Powered by sora