본문 바로가기

유니티/확장 프로젝트[3D]

3D 미니 프로젝트 2 - 3 ] 공격

지난 포스팅에서는 아이템 수집 및 장착을 구현하였다.

 

3D 미니 프로젝트 2 - 2 ] 아이템 수집 및 장착, 교체

지난 포스팅에서는 골드메탈님의 에셋과 강의를 통해 캐릭터의 움직임과 애니메이션을 구현하였다. 이제는 RPG에서는 빠질 수 없는 요소인 아이템에 대해 구현 해 보도록 하겠다. 이 영상을 참

mini-noriter.tistory.com

 

이제는 장착한 아이템을 가지고 공격을 구현 해 보도록 하겠다.

 


근접 공격

 

근접 공격은 가지고 있는 둔기 무기에 BoxCollider를 Trigger로 추가 하여 닿는 순간에 이벤트가 발생하며 데미지를 입히는 방식이다.

캐릭터와 무기의 Hierarchy

우선 캐릭터의 자식으로 내려가 팔 부분까지 내려가서 무기를 세팅 해 준다.

 

무기 위치 세팅

 

위치를 맞춰 준 다음, 비활성화를 해 준다.

 

 

Box Collider 설정

그리고 Hammer의 공격 판정을 판단하기 위하여 Box Collider를 만들어 세팅 해 준다.

 

근접 공격은 범위가 너무 적기 때문에 Collider 범위를 실제 크기보다 크게 설정 해 준다.

 

그리고 착용한 무기에 대한 코드를 하나 만들어 준다.

 

Weapon.cs

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

public class Weapon : MonoBehaviour
{
    public enum AtkType { Melee, Range }; // Melee - 근접 공격, Range - 원거리 공격
    public AtkType type; // 생성을 해 주어야 한다.
    public int Damage;
    public float AtkDelay;
    public BoxCollider meleeArea; // 근접 공격 범위
    public TrailRenderer trailEffect; // 효과?

}

우선 정보만을 넣어주는 것이 목적이기 때문에 무기 타입, 데미지, 공격 딜레이, 공격 범위(근접), 효과에 대한 변수들을 public으로 만들어 준다.

 

값은 나중에 세팅 해 주고, 때리는 효과를 넣기 위해 TrailRenderer를 추가하고 세팅 해 준다.


Trail Renderer 세팅

 

TrailRenderer 예시

 

Trail Renderer는 위 사진처럼 해당 객체가 지나 간 자리에 잔상(?)을 남기는 효과를 주는 것이다.

 

망치를 휘두르며 공격할 때만 효과를 주어 공격하는 맛이 느껴지도록 할 예정이다.

 

TrailRenderer 세부 세팅

색깔과 크기에 대한 세부 설정이다.

Materials를 먼저 설정하는 것을 잊지 말자.


공격 구현

 

Weapon.cs에서 이제 공격을 실행할 수 있게 코드를 추가 해 보자.

 

앞서 Collider를 추가 해 주었었는데, 이 것은 공격 시에만 활성화 되고, 공격이 아닐 때는 활성화가 되면 안된다.

 

따라서 Collider를 활성화 한 다음, 딜레이 이후에 Invoke를 통하여 공격을 다시 할 수 있게 해 주는 함수를 출력하게 해 주면 된다.

 

Collider를 활성화 하는 타이밍은 아래 사진과 같이 공격 애니메이션이 끝나는 시점에 잠깐 활성화를 시켜 준 다음, 공격이 끝나고 비활성화를 해 주어야 한다.

공격 과정

즉, 두 번의 딜레이를 거쳐야 하는 것이다.

 

따라서 Invoke를 두 번 사용하여 여러 함수들을 차례로 불러 내야 하는 불편함이 생기게 된다.


코루틴(CoRoutine)

 

이러한 불편함을 해소하기 위하여 유니티에서는 CoRoutine이라는 것을 제공한다.

 

한 개의 함수 속에 딜레이를 넣을 수 있는 특수한 함수라고 생각하면 된다.

 

즉, 함수 한 개로, 여러 개의 작업을 할 수 있게 한다고 생각해도 된다.

 

코루틴을 이용한 Weapon.cs 는 아래와 같다.

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

public class Weapon : MonoBehaviour
{
    public enum AtkType { Melee, Range }; // Melee - 근접 공격, Range - 원거리 공격
    public AtkType type; // 생성을 해 주어야 한다.
    public int Damage;
    public float AtkDelay;
    public BoxCollider meleeArea; // 근접 공격 범위
    public TrailRenderer trailEffect; // 효과?

    public void Use()
    {
        if (type == AtkType.Melee)
        {
            StopCoroutine("Swing"); // 동작하고 있는 중에도 다 정지시킬 수 있음
            StartCoroutine("Swing");
        }
        
    }

    IEnumerator Swing()
    {
        yield return new WaitForSeconds(0.1f);

        meleeArea.enabled = true; // box Collider 활성화
        trailEffect.enabled = true; // effect 활성화

        yield return new WaitForSeconds(0.3f);

        meleeArea.enabled = false; // box Collider 활성화

        yield return new WaitForSeconds(0.3f);

        trailEffect.enabled = false; // effect 활성화


    }

    // Use함수 : 메인 루틴 -> Swing() 서브 루틴이 실행 -> 다시 메인루틴으로 돌아와서 Use() 시행
    // 코루틴 사용 시 : 메인 + 서브 루틴이 동시 실행 (co-op 협동!)


}

 

코루틴은 IEnumerator 를 통하여 사용할 수 있으며, yield return 부분이 필수로 들어가야 한다.

 

yield return null; 을 주면 한 프레임의 딜레이 이후에 다음 작업을 수행하라는 의미이며

 

yield break; 는 남은 작업에 관계 없이 작업을 끝내라는 의미이다. (yield break; 아래에 있는 작업은 실행되지 않고 끝난다.)

 

그리고 위에서는 yield return new WaitForSeconds(time); 을 사용하였는데 이것은 time만큼의 딜레이를 준 다음 다음 작업으로 넘어 가라는 것이다.

 

위 코드를 통하여 Use()가 실행되면 만약 무기 타입이 근접 무기라면 BoxCollider와 TrailEffect를 활성화 시키고, 0.2초 뒤에 다시 비활성화 시키는 작업을 할 수 있게 된다.

 

무기에서 위와 같은 작업이 이루어지게 되니, 플레이어에서는 공격 키를 눌렀을 때, 위와 같은 작업(Use() -> Swing())으로 이어질 수 있게 연결 해 주어야 한다.

 

PlayerCode.cs에 아래 함수를 추가 해 준다.

void Attack()
{
    if (cntEquipWeapon == null)
        return;

    AtkDelay += Time.deltaTime;
    isAtkReady = cntEquipWeapon.AtkDelay < AtkDelay; // 공격 속도(무기 딜레이)보다 현재 딜레이 값이 클때만!

    if(AtkDown && isAtkReady && !isDodge && !isSwap)
    {
        cntEquipWeapon.Use(); // 공격할 준비가 되었으니 전달하고 나머지는 위임!
        anim.SetTrigger("DoSwing"); // 3항 연산자를 이용하여 한 줄로 두 가지 종류의 애니메이션을 실행한다.
        // 3항 연산자의 사용은 유연하게 가능하다는 것을 명심!
        AtkDelay = 0f;
    }

}

deltaTime을 통하여 시간을 계산 하고, Delay를 넘어서게 되어야 공격을 할 수 있게 하였다.

 

isAtkReady는 상태를 보여주는 bool 변수이며, 공격 딜레이가 누적된 시간보다 작을 때 true를 주는 조건문에 의해 딜레이마다 공격을 실행하게 되는 것이다.

 

공격 모션 애니메이션 세팅

애니메이터에 공격 모션을 추가 해 준다.

 

근접 공격

이렇게 애니메이션까지 추가 한 다음, 실행을 해 보면 위 움짤 처럼 근접 공격을 할 수 있게 됨을 볼 수 있다.

 


원거리 공격(총)

근거리 공격은 공격 범위가 존재하였다.

 

하지만, 원거리 공격은 총을 쏘면서 원거리에서 하는 공격이기에 스킬이 아닌 이상 공격 범위가 무기에 붙어있지는 않다.

 

대신 총알에 Collider를 넣어 주어 총알에 닿으면 공격 판정이 나게끔 할 것이다.

 

(총알에는 대상이 밀리면 안되기에 isTrigger를 체크 해 준다.)

 

총알을 제작 해 보자


총알 제작

 

총알로 사용 할 Collider

빈 오브젝트를 만들고 RigidBody와 Sphere Collider를 넣어 주어 중력의 영향을 받으면서 공격 판정이 나게끔 설정 하였다.

 

그리고 골드메탈님은 여기에 Trail Effect를 추가 해 주었다.

 

Trail Renderer 추가 후 모습
Trail Renderer 세부 설정

총 색과 관련있게 색을 설정 해 주었으며, 크기도 적절하게 세팅 해 주면 된다.

 

그리고 총 Weapon.cs에서 총에는 데미지를 0으로 설정 해 준다.

 

왜냐하면, 총 자체에서 데미지 계산이 일어나는 것이 아니라 총알에서 일어나는 것이기 때문이다.

 

따라서, Bullet.cs 코드를 하나 만들어 여기서 데미지를 설정 해 보도록 하자.

Bullet.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    public int damage;

    private void OnCollisionEnter(Collision collision)
    {
        if(collision.gameObject.tag == "base")
        {
            Destroy(gameObject, 3);
        }
        else if(collision.gameObject.tag == "Wall")
        {
            Destroy(gameObject);
        }
    }
    
    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.tag == "base" || other.gameObject.tag == "Wall")
        {
            Destroy(gameObject);
        }

    }

}

데미지를 설정하고, 벽에 닿거나 땅에 닿게 되면 사라지게끔 해 주었다.

 

(Collision은 탄피에 적용시키려고 만들어 놓은 것이다.)

 

그리고 총알을 Prefab화 시켜 준다.

 

머신건 총알도 크기만 작게 해서 비슷하게 설정 해 준다.

 

머신건 총알 모습

이것 역시 Prafab화 시켜준다.

 

탄피도 만들어서 RigidBody, Collider를 적용시켜 주고, Bullet.cs를 넣은 다음 Prefab화 시켜 준다.


총알 발사

 

그리고 Weapon.cs에서 원거리 공격 전용 요소들을 만들어 준다.

 

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

public class Weapon : MonoBehaviour
{
    public enum AtkType { Melee, Range }; // Melee - 근접 공격, Range - 원거리 공격
    public AtkType type; // 생성을 해 주어야 한다.
    public int Damage;
    public float AtkDelay;
    public BoxCollider meleeArea; // 근접 공격 범위
    public TrailRenderer trailEffect; // 효과?

    // 원거리 전용
    public Transform bulletPos; // 총알 생성 위치
    public GameObject bullet; // 생성 될 총알
    public Transform bulletCasePos; // 탄피가 생성될 위치 설정
    public GameObject bulletCase; // 탄피
    public int maxCount; // 최대 총알 개수
    public int cntCount; // 현재 총알 개수


    public void Use()
    {
        if (type == AtkType.Melee)
        {
            StopCoroutine("Swing"); // 동작하고 있는 중에도 다 정지시킬 수 있음
            StartCoroutine("Swing");
        }
        else if(type == AtkType.Range && cntCount > 0)
        {
            cntCount--;
            StopCoroutine("Shot"); // 동작하고 있는 중에도 다 정지시킬 수 있음
            StartCoroutine("Shot");
        }
    }

    IEnumerator Swing()
    {
        yield return new WaitForSeconds(0.1f);

        meleeArea.enabled = true; // box Collider 활성화
        trailEffect.enabled = true; // effect 활성화

        yield return new WaitForSeconds(0.3f);

        meleeArea.enabled = false; // box Collider 활성화

        yield return new WaitForSeconds(0.3f);

        trailEffect.enabled = false; // effect 활성화


    }

    IEnumerator Shot()
    {

        // 총알 발사
        GameObject instantBullet = Instantiate(bullet, bulletPos.position, bulletPos.rotation);
        Rigidbody bulletRigid = instantBullet.GetComponent<RigidBody>();

        bulletRigid.velocity = bulletPos.forward * 50; // 총알의 속도를 설정

        yield return null; // 1프레임 쉬고

        // 탄피 배출

        GameObject instantCase = Instantiate(bulletCase, bulletCasePos.position, bulletCasePos.rotation);
        Rigidbody caseRigid = instantCase.GetComponent<RigidBody>();

        Vector3 caseVec = bulletCasePos.forward * Random.Range(-3, -1) + Vector3.up * Random.Range(2, 3); // 랜덤한 힘으로 배출!

        caseRigid.AddForce(caseVec, ForceMode.Impulse);
        caseRigid.AddTorque(Vector3.up * 10, ForceMode.Impulse); // 회전하는 힘을 주는 것!
    }

    // Use함수 : 메인 루틴 -> Swing() 서브 루틴이 실행 -> 다시 메인루틴으로 돌아와서 Use() 시행
    // 코루틴 사용 시 : 메인 + 서브 루틴이 동시 실행 (co-op 협동!)

}

 

우선 원거리 공격은 총알이 나갈 위치와 탄피가 배출 될 위치, 총알 prefab, 탄피 prefab, 현재 총알 개수, 최대 총알 개수가 들어 갈 변수를 설정 해 준다.

 

총알이 나갈 위치 세팅

 

총알이 나갈 위치는 Empty GameObject를 만들어 위 사진과 같이 위치시킨다.

 

탄피 배출 위치

그리고 탄피 배출위치는 무기 자식 오브젝트에 빈 오브젝트를 추가 해 주어 위 사진과 같이 위치시켜 준다.

 

무기 스크립트 세팅

그리고 위와 같이 다 넣어 준다. (총알 프리팹, 총알 발사 위치, 탄피 프리팹, 탄피 발사 위치)

 

// 총알 발사
GameObject instantBullet = Instantiate(bullet, bulletPos.position, bulletPos.rotation);
Rigidbody bulletRigid = instantBullet.GetComponent();

bulletRigid.velocity = bulletPos.forward * 50; // 총알의 속도를 설정

yield return null; // 1프레임 쉬고

그 다음에 코루틴을 이용한 Shot()에서 Instantiate()를 통하여 prefab을 생성 해 준다.

(아까 설정 했던 위치를 사용한다. bulletPos)

 

그런데 생성만 해 줘서는 안된다. 총알에 힘을 주어야 한다.

 

따라서 Rigidbody bulletRigid = instantBullet.GetComponent(); 을 통하여 힘을 줄 수 있게 해 준다.

 

AddForce를 해 주어도 되지만 총알에는 속도를 설정 해 준다.

 

속도는 bulletPos의 forward 벡터를 기반으로 설정하였다. (이래서 prefab을 만들 때, 방향이 중요하다.)

 

그 다음에는 코루틴의 특징을 따라서 yield return null; 을 통해 1프레임 쉬고 탄피 배출 로직으로 이동한다.

 

GameObject instantCase = Instantiate(bulletCase, bulletCasePos.position, bulletCasePos.rotation);
Rigidbody caseRigid = instantCase.GetComponent();

Vector3 caseVec = bulletCasePos.forward * Random.Range(-3, -1) + Vector3.up * Random.Range(2, 3); // 랜덤한 힘으로 배출!

caseRigid.AddForce(caseVec, ForceMode.Impulse);
caseRigid.AddTorque(Vector3.up * 10, ForceMode.Impulse); // 회전하는 힘을 주는 것!

탄피는 비슷하지만 AddForce를 해 준다. 그리고 방향 벡터를 랜덤으로 설정 해 두어 탄피가 튀는 맛이 있게 하였다.

 

public void Use()
{
    if (type == AtkType.Melee)
    {
        StopCoroutine("Swing"); // 동작하고 있는 중에도 다 정지시킬 수 있음
        StartCoroutine("Swing");
    }
    else if(type == AtkType.Range && cntCount > 0)
    {
        cntCount--;
        StopCoroutine("Shot"); // 동작하고 있는 중에도 다 정지시킬 수 있음
        StartCoroutine("Shot");
    }
}

그리고 위 코드에서 보면 현재 총알 개수가 1개 이상이어야 Shot()이 발동되게 하였다.

 

Shot 애니메이션 세팅

애니메이션도 설정 해 두고, DoShot 트리거를 통하여 발동되게 하였다.

 

그런데 PlayerCode.cs에서 Attack() 함수는 애니메이션 트리거 세팅을 하는 것이 DoSwing만 존재하였다.

 

총을 추가 했기 때문에 착용한 무기에 따라서 애니메이션이 바뀌어야 하는데, 이러한 역할을 해 주기 위해 if문을 활용해도 되지만 삼항 연산자를 활용할 수도 있다.

void Attack()
{
    if (cntEquipWeapon == null)
        return;

    AtkDelay += Time.deltaTime;
    isAtkReady = cntEquipWeapon.AtkDelay < AtkDelay; // 공격 속도(무기 딜레이)보다 현재 딜레이 값이 클때만!

    if(AtkDown && isAtkReady && !isDodge && !isSwap)
    {
        cntEquipWeapon.Use(); // 공격할 준비가 되었으니 전달하고 나머지는 위임!
        anim.SetTrigger(cntEquipWeapon.type == Weapon.AtkType.Melee ? "DoSwing" : "DoShot"); // 3항 연산자를 이용하여 한 줄로 두 가지 종류의 애니메이션을 실행한다.
        // 3항 연산자의 사용은 유연하게 가능하다는 것을 명심!
        AtkDelay = 0f;
    }

}

위 부분과 같이 코드를 고쳐 준다.

 

SetTtrigger 속에도 삼항 연산자를 써 주어 해당하는 애니메이션이 발동되게 하였다.

(현재 장착하고 있는 무기의 타입이 근접무기면 DoSwing Trigger 활성화!)

 

이렇게 삼항 연산자의 응용은 폭 넓게 이루어진다.

 

원거리 공격 모습

이제, 총을 장착하고 쏘게 되면 위 움짤과 같이 공격하게 된다.


총알 재장전

 

총에는 최대 총알 개수가 있고, 이것을 재 장전을 통해서 채워 주어야 한다.

 

PlayerCode.cs에 아래 함수들을 추가 해 준다.

 

void ReLoad()
{
    if (cntEquipWeapon == null)
        return;

    if (cntEquipWeapon.type == Weapon.AtkType.Melee)
        return;

    if (bullet == 0) // 남은 총알이 0개이면 안된다.
        return;

    if(rDown && !isJump && !isDodge && !isSwap && isAtkReady && !isReloading)
    {
        // 재장전 키가 눌리고, 점프중,회피중,무기교체중이 아닐때이면서 공격 준비가 되었을 때 실행되게 한다.
        isReloading = true;
        anim.SetTrigger("DoReload");

        Invoke("ReLoadOut", 0.7f);

    }

}

 void ReLoadOut()
{
    int reCount = bullet < cntEquipWeapon.maxCount ? bullet : cntEquipWeapon.maxCount - cntEquipWeapon.cntCount;
    cntEquipWeapon.cntCount += reCount;

    if (cntEquipWeapon.cntCount > cntEquipWeapon.maxCount)
    {
        reCount = bullet - (cntEquipWeapon.cntCount - cntEquipWeapon.maxCount); // 남은 총알 개수에서 넘치는 부분을 뺀 만큼을 충전해야 한다.
        cntEquipWeapon.cntCount = cntEquipWeapon.maxCount;
    }

    bullet -= reCount; // 플레이어의 총알 개수 갱신

    isReloading = false;
}

r 버튼을 누르면 재장전 함수가 실행되게 하였으며, 애니메이션이 발동 된 다음(0.7초 딜레이 이후) ReLoadOut() 함수에서 실제로 총알이 리필되게 하였다.

 

여기서 플레이어가 가진 총알의 개수와, 몇 개의 총알을 더 채워야 하는지 등을 고려하여 총알을 채워 주어야 한다.

 

우선 reCount는 채울 총알의 개수이다.

 

여기서 삼항 연산자를 이용하여 플레이어의 남은 총알 개수가 탄창 용량보다 작으면(탄창에 다 못들어갈 때) 남은 총알을 다 때려 넣고, 탄창 용량보다 많으면 최대치와 현재 가진 총알 개수의 차이만큼 넣어주는 것으로 설정하였다.

 

그런데, 애매한 순간이 있다. 예를 들어 탄창 용량이 10개이고 남은 총알이 6개인데, 탄창에 8발을 남긴 상태로 재장전을 하면, 분명히 남은 총알 개수가 탄창 용량보다는 작기 때문에 6발을 다 때려 넣게 되면 14발이 되어 탄창 용량을 초과하게 된다.

 

따라서 초과를 하게 되면, 남은 총알에서 초과 한 부분을 빼 준 값을 reCount에 다시 넣어주게 된다.

ex - 6(남은 총알 개수) - (14(남은것을 다 넣었을 때 총알 개수) - 10(탄창 용량))(초과분) = 2 (남은 총알에서 채울 수 있는 개수)

 

그리고 현재 총알 개수를 탄창 용량으로 맞추고 reCount에 다시 넣어 준 채울 수 있는 개수를 남은 총알 개수에서 빼 주면 된다.

 

이렇게 해 주면 아래 움짤과 같이 장전이 되게 된다.

 

재장전 모습 (숫자 주목!)

위 설명이 좀 길긴 했지만.. 나름 골드메탈님이 하신 것에서 조금 더 보완하기 위한 고뇌(?)였다고 생각한다..

 

다음 포스팅에서는 얻은 아이템에 대한 강화 시스템을 만들어 보도록 하겠다.

(강화 부분은 골드메탈님 강의와는 별개의 내용이다.)