새소식

🎮 Game Dev (게임개발)/PC (데스크탑, 노트북, 터치패널)

[3D 액션게임] 07. 원거리무기 공격구현

  • -

🔔 유튜브 크리에이터 골든메탈님의 유니티강의 3D 쿼터뷰 액션게임 [BE5] 를 보고 공부하여 작성한 게시글입니다! 🔔

 

 

 

전체코드보기

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    public float speed;
    public GameObject[] weapons;
    public bool[] hasWeapons;
    public GameObject[] grenades;
    public int hasGrenades;
    public Camera followCamera;

    public int ammo;
    public int health;
    public int coin;
    
    public int maxAmmo;
    public int maxHealth;
    public int maxCoin;
    public int maxHasGrenades;



    float hAxis;
    float vAxis;
    
    bool rDown;
    bool jDown;
    bool fDown;
    bool reDown;
    bool iDown;
    bool sDown1;
    bool sDown2;
    bool sDown3;

    bool isJump;
    bool isDodge;
    bool isSwap;
    bool isReload;
    bool isFireReady = true;
    

    Vector3 moveVec;
    Vector3 dodgeVec;
    Rigidbody rigid;

    Animator anim;

    GameObject nearObject;
    Weapon equipWeapon;
    int equipWeaponIndex = -1;
    float fireDelay;

    void Awake()
    {
        rigid = GetComponent<Rigidbody>();
        anim = GetComponentInChildren<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        GetInput();
        Move();
        Turn();
        Jump();
        Attack();
        Reload();
        Dodge();
        Swap();
        Interation();
        
        
    }


    void GetInput()
    {
        // 키보드입력에 따라 0 ~ 1로 변환 left right up down
        hAxis = Input.GetAxisRaw("Horizontal");
        vAxis = Input.GetAxisRaw("Vertical");
        rDown = Input.GetButton("Run");
        jDown = Input.GetButtonDown("Jump");
        reDown = Input.GetButtonDown("Reload");
        fDown = Input.GetButton("Fire1");
        iDown = Input.GetButtonDown("Interation");
        sDown1 = Input.GetButtonDown("Swap1");
        sDown2 = Input.GetButtonDown("Swap2");
        sDown3 = Input.GetButtonDown("Swap3");
    }

    void Move()
    {
        // x y z
        moveVec = new Vector3(hAxis, 0, vAxis).normalized;

        // 회피 시 업데이트 안되게
        if (isDodge)
            moveVec = dodgeVec;
        
        // 스왑 및 공격 시 못움직이게
        if (isSwap || !isFireReady || isReload)
            moveVec = Vector3.zero;

        // transform
        transform.position += moveVec * (rDown ? 1.3f : 1f) * speed * Time.deltaTime;

        // animator
        anim.SetBool("isWalk", moveVec != Vector3.zero);
        anim.SetBool("isRun", rDown);
    }

    void Turn()
    {
        // #1. 키보드에 의한 회전
        transform.LookAt(transform.position + moveVec);

        // #2. 마우스에 의한 회전

        // Ray란?, ScreenPointToRay() : 스크린에서 월드로 Ray를 쏘는 함수
        if(fDown)
        {
            Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit rayHit;
            // out 키워드는, 반환값을 주어진 변수에 저장하는 키워드
            if (Physics.Raycast(ray, out rayHit, 100))
            {
                Vector3 nextVec = rayHit.point;
                nextVec.y = 0;
                transform.LookAt(nextVec);
            }
        }
    }

    void Jump()
    {
        if(jDown && moveVec == Vector3.zero && !isJump && !isDodge) {
            // 물리엔진에 힘을 준다. 여기선 즉발적인 Impulse
            rigid.AddForce(Vector3.up * 15, ForceMode.Impulse);
            anim.SetBool("isJump", true);
            anim.SetTrigger("doJump");

            isJump = true;
        }
    }

    void Attack()
    {
        // 공격할 조건만 플레이어에 두고, 공격로직은 무기에 위임한다.
        if (equipWeapon == null)
            return;

        fireDelay += Time.deltaTime;
        isFireReady = equipWeapon.rate < fireDelay;

        if(fDown && isFireReady && !isDodge && !isSwap) {
            equipWeapon.Use();
            anim.SetTrigger(equipWeapon.type == Weapon.Type.Melee ? "doSwing" : "doShot");
            fireDelay = 0;
        }
    }

    void Reload()
    {
        if (equipWeapon == null)
            return;
        if (equipWeapon.type == Weapon.Type.Melee)
            return;
        if (ammo == 0)
            return;

        if(reDown && !isJump && !isDodge && !isSwap && isFireReady)
        {
            anim.SetTrigger("doReload");
            isReload = true;

            Invoke("ReloadOut", 2f);
        }
    }

    void ReloadOut()
    {
        int reAmmo = ammo < equipWeapon.maxAmmo ? ammo : equipWeapon.maxAmmo;
        equipWeapon.curAmmo = reAmmo;
        ammo -= reAmmo;
        isReload = false;
    }


    void Dodge()
    {
        // 이동하면서 점프할때, 
        if (jDown && moveVec != Vector3.zero && !isJump && !isDodge)
        {
            dodgeVec = moveVec;
            speed *= 2;
            anim.SetTrigger("doDodge");
            isDodge = true;

            Invoke("DodgeOut", 0.5f); // 시간지연 라이브러리 기능
        }
    }

    void DodgeOut()
    {
        speed *= 0.5f;
        isDodge = false;
    }
    
    void Swap()
    {
        if (sDown1 && (!hasWeapons[0] || equipWeaponIndex == 0))
            return;
        if (sDown2 && (!hasWeapons[1] || equipWeaponIndex == 1))
            return;
        if (sDown3 && (!hasWeapons[2] || equipWeaponIndex == 2))
            return;

        int weaponIndex = -1;
        if (sDown1) weaponIndex = 0;
        if (sDown2) weaponIndex = 1;
        if (sDown3) weaponIndex = 2;

        // 1, 2 ,3 버튼 누를때
        if((sDown1 || sDown2 || sDown3) && !isJump && !isDodge) {
            if(equipWeapon != null)
                equipWeapon.gameObject.SetActive(false);

            equipWeaponIndex = weaponIndex;
            equipWeapon = weapons[weaponIndex].GetComponent<Weapon>();
            equipWeapon.gameObject.SetActive(true);
            
            anim.SetTrigger("doSwap");
            

            // 스왑 중
            isSwap = true;
            Invoke("SwapOut", 0.4f);

        }
    }
    void SwapOut()
    {
        isSwap = false;
    }

    void Interation()
    {
        if(iDown && nearObject != null && !isJump && !isDodge) {
            if(nearObject.tag == "Weapon") {
                Item item = nearObject.GetComponent<Item>();
                int weaponIndex = item.value;
                hasWeapons[weaponIndex] = true;

                Destroy(nearObject);
            }
        }
    }

    // 충돌 시 이벤트 함수로 착지 구현
    void OnCollisionEnter(Collision collision)
    {
        if(collision.gameObject.tag == "Floor"){
            isJump = false;
            anim.SetBool("isJump", false);
        }
    }

    // 아이템 획득
    private void OnTriggerEnter(Collider other)
    {
        if(other.tag == "Item") {
            Item item = other.GetComponent<Item>();
            switch (item.type){
                case Item.Type.Ammo:
                    ammo += item.value;
                    if (ammo > maxAmmo)
                        ammo = maxAmmo;
                    break;
                case Item.Type.Coin:
                    coin += item.value;
                    if (coin > maxCoin)
                        coin = maxCoin;
                    break;
                case Item.Type.Heart:
                    health += item.value;
                    if (health > maxHealth)
                        health = maxHealth;
                    break;
                case Item.Type.Grenade:
                    if (hasGrenades == maxHasGrenades)
                        return;
                    grenades[hasGrenades].SetActive(true);
                    hasGrenades += item.value;
                    break;
            }
            Destroy(other.gameObject);
        }
    }

    private void OnTriggerStay(Collider other)
    {
        if (other.tag == "Weapon")
            nearObject = other.gameObject;
    }
    private void OnTriggerExit(Collider other)
    {
        if (other.tag == "Weapon")
            nearObject = null;
    }
}

🧷 1. 총알 및 탄피 만들기

-  총알 , 탄피 프리팹 만들기

  • Hierachy 공간에 creaty empty를 하여, 핸드건 총알, 서브머신건 총알을 명명 해줍시다.

 

  • 에셋에 들어있는 Bullet Case도 사용해 우리들만의 탄창 프리팹으로도 만들 예정이기에 총알과 같이 꾸며봅시다.

 

  • 이후에 Rigidbody, Collider을 총알들, 탄창에 맞게 넣어줍니다 또한 각 총알들에게만 Trail Renderer을 넣어 총알이 나갈 때 효과를 주었습니다.
  • Trail Renderer: 객체를 따라가는 이펙트효과로 사용할 것입니다.

 

Trail Renderer

  • 각자 원하는 효과대로 Width, Color, Time 등등 설정해줍시다.
  • 그러면 총알이 나갈 때, 잔상처럼 이펙트가 이쁘게 나갑니다.

 

  • 이후에 Bullet 스크립트까지 만들어 총알들, 탄피에 넣어줍시다.
/* bullet script */
public class Bullet : MonoBehaviour
{
    public int damage;

    void OnCollisionEnter(Collision collision)
    {
        if(collision.gameObject.tag == "Floor") {
            Destroy(gameObject, 3);
        }
        else if(collision.gameObject.tag == "Wall") {
            Destroy(gameObject);
        }
        
    }
}
  • bullet 스크립트는, 공격력과, 벽에 부딪히거나, 바닥에 닿으면 사라지도록 설정해둡니다.
  • Destroy(객체, 사라지기까지 시간)

 

  • 이후에 나만의 프리팹에 끌여당겨 저장해줍시다.

 

-  총알 및 탄피 위치 잡기

  • 총알이 나가는 위치를 잡기위해 Player아래에 Empty를 만든 후 위치를 잡아줍니다.

 

  • 마찬가지로 탄피가 나가는 위치를 각 총에 맞게 설정해줍니다.

 

🧷 2. 총 쏘기 기능 구현

-  Weapon 스크립트에 Shot 기능 추가

/* Weapon script , 추가되는 부분만 넣었습니다. */
public class Weapon : MonoBehaviour
{
    
    public Transform bulletPos;
    public GameObject bullet;
    public Transform bulletCasePos;
    public GameObject bulletCase;

    public int maxAmmo;
    public int curAmmo;

    public void Use()
    {
        if(type == Type.Melee) {
            StopCoroutine("Swing");
            StartCoroutine("Swing");
        }
        
        else if(type == Type.Range && curAmmo > 0) {
            curAmmo--;
            StartCoroutine("Shot");
        }
    }

    IEnumerator Shot()
    {
        // 1. 총알 발사 , Instantiate() 함수로 총알 인스턴스화 하기
        GameObject instantBullet = Instantiate(bullet, bulletPos.position, bulletPos.rotation);
        Rigidbody bulletRigid = instantBullet.GetComponent<Rigidbody>();
        bulletRigid.velocity = bulletPos.forward * 50;         // velocity 속력주기

        yield return null;
        // 2. 탄피 배출
        GameObject instantCase = Instantiate(bulletCase, bulletCasePos.position, bulletCasePos.rotation);
        Rigidbody CaseRigid = instantCase.GetComponent<Rigidbody>();
        Vector3 caseVec = bulletCasePos.forward * Random.Range(-3, -2) + Vector3.up * Random.Range(2, 3);
        CaseRigid.AddForce(caseVec, ForceMode.Impulse);
        
        CaseRigid.AddTorque(Vector3.up * 10, ForceMode.Impulse); // 탄피 회전
    }

}
  • 총알과, 총알 포지션, 탄피, 탄피 포지션을 담을 오브젝트를 변수로 추가합니다.
  • 또한, 각 총의 장전된 총알, 최대 장전총알까지 변수로 추가합니다.
  • 이후 Use() 함수에서 근접무기때처럼, 원거리공격시 코루틴으로 Shot() 함수를 추가합니다. 이때, 총알이 한개 나가면 StopCoruoutine을 해줄 필요가 없기에 제외했습니다. 그리고 무조건 코루틴을 사용할 때 yield 를 넣어야합니다.
  • Shot() 함수에서는 인스턴트총알 = (총알, 총알포지션값, 총알회전값)을 주어 인스턴트화 시켰습니다.
  • instantBullet의 Rigidbody값도 변수에 저장합니다.

  • 유니티를 보고 총알이 나가는 위치는 z축 방향으로 앞을 바라보고 있으니 인스턴트 총알의 Rigidbody값에 velocity를 bulletPos.forward 벡터값을 준 뒤, 원하는 총알 속도를 넣어 총알의 속도 값을 넣어줍니다.
  • 이 후, 탄피 배출에도 인스턴트 값, 탄피가 배출되는 랜덤값, 회전값등을 주었습니다.

 

  • 스크립트가 완성 되었으니, 플레이어가 오른손에 들고있는 총들의 Weapon 스크립트 값에 Bullet, Bullet Pos, Bullet Case Pos, Bullet Case 오브젝트를 연결시켜 주었습니다.

 

탄피 배출 모습

 

-  Player 스크립트에 애니메이션 추가 

/* Player script, 추가된 부분만 넣었습니다. */
public class Player : MonoBehaviour
{
    void Attack()
    {
        // 공격할 조건만 플레이어에 두고, 공격로직은 무기에 위임한다.
        if (equipWeapon == null)
            return;

        fireDelay += Time.deltaTime;
        isFireReady = equipWeapon.rate < fireDelay;

        if(fDown && isFireReady && !isDodge && !isSwap) {
            equipWeapon.Use();
            anim.SetTrigger(equipWeapon.type == Weapon.Type.Melee ? "doSwing" : "doShot");
            fireDelay = 0;
        }
    }

    }
}
  • 플레이어 스크립트에서 현재 장착하고 있는 무기에 따른 애니메이션 트리거를 삼항 연산자로 설정해줍시다.

  • 애니메이션 컨트롤러에서도, doShot 트리거와 함께 에셋에서 Shot 모션도 넣어 설정해줍니다.

 

🧷 3. 재장전 기능 구현

-  재장전 변수 추가

/* Player script, 추가된 부분만 넣었습니다. */
public class Player : MonoBehaviour
{
	bool reDown;
    bool isReload;
    
    void GetInput()
    {
        reDown = Input.GetButtonDown("Reload");
    }

}
  • Input Manager 에서 Reload 값을 r key로 설정하고 스크립트에 넣어주었습니다.
  • 또한 isReload를 통해 리로드 중인지 확인할 변수도 넣어줍시다.

 

- 재장전 기능 추가

/* Player script, 추가된 부분만 넣었습니다. */
public class Player : MonoBehaviour
{
    void Reload()
    {
        if (equipWeapon == null)
            return;
        if (equipWeapon.type == Weapon.Type.Melee)
            return;
        if (ammo == 0)
            return;

        if(reDown && !isJump && !isDodge && !isSwap && isFireReady)
        {
            anim.SetTrigger("doReload");
            isReload = true;

            Invoke("ReloadOut", 2f);
        }
    }

    void ReloadOut()
    {
        int reAmmo = ammo < equipWeapon.maxAmmo ? ammo : equipWeapon.maxAmmo;
        equipWeapon.curAmmo = reAmmo;
        ammo -= reAmmo;
        isReload = false;
    }

}
  • Reload() 함수를 만들어, 재장전시 조건(장착하고 있는 무기가 있고, 타입이 근접무기가 아니고, 총알이 가지고있는 재장전 총알이 0이 아닐때) 을 추가하였습니다.
  • 또한 조건을 통과 한 후, reload 키를 누를 시, 애니메이션 트리거가 실행되고, 리로드 중이라는 isReload = true 값이 됩니다.
  • 이 후 재장전 속도를 주기 위해 Invoke()와, ReloadOut()함수를 사용해 2초의 시간 후 ReloadOut() 함수가 실행 되도록 설정 하였습니다.
  • ReloadOut 에서는 재장전시 탄창의 수를 재조정하는 코드를 작성했습니다.

++ 재장전 연타시, 탄창이 자꾸 갈아끼워지는 버그를 고칠 예정이며, 남아있는 총알의 개수는 재장전시 탄창에서 빼줄 예정입니다.

 

- 재장전 애니메이션 추가

  • 애니메이션 컨트롤러에서 doReload트리거를 만들고 모션을 추가하여 연결시켜 주었습니다.

 

🧷 4. 마우스 방향으로 총 쏘기 

거의 다 왔습니다!!. 이제는 마우스 클릭에 따라서 총알이 나가게 설정 할 기능을 추가합니다.

 

/* Player script, 추가된 부분만 넣었습니다. */
public class Player : MonoBehaviour
{
	public Camera followCamera;
    
    void Turn()
    {
        // #1. 키보드에 의한 회전
        transform.LookAt(transform.position + moveVec);

        // #2. 마우스에 의한 회전

        // Ray란?, ScreenPointToRay() : 스크린에서 월드로 Ray를 쏘는 함수
        if(fDown)
        {
            Ray ray = followCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit rayHit;
            // out 키워드는, 반환값을 주어진 변수에 저장하는 키워드
            if (Physics.Raycast(ray, out rayHit, 100))
            {
                Vector3 nextVec = rayHit.point;
                nextVec.y = 0;
                transform.LookAt(nextVec);
            }
        }
    }

}
  • 카메라 변수를 가져와, 실제 우리 카메라 오브젝트를 끌여당겨 넣어줍시다.
  • Turn() 함수에 총 공격시, Ray 함수를 쓸껀데요, 이 ray를 만들어 RaycastHIt 클래스의 rayHit에 저장 합니다
  • rayHit.point 값을 통해 플레이어의 방향을 조절합니다.
  • nextVec.y = 0 을 두어 x,z축의 값에만 반응하도록 만들고
  • transform.LookAt(nextVec) 값으로 플레이어의 회전을 돕습니다.

++ Ray 함수는 따로 다뤄 보도록 하겠습니다.

++ 2022.04.03 - [Game Dev/개발지식] - [Unity] 레이(Ray), 레이 캐스트(Raycast)에 대해

 

 

출처: 골든메탈님 유튜브

https://www.youtube.com/watch?v=07q9RUTRq4M&list=PLO-mt5Iu5TeYkrBzWKuTCl6IUm_bA6BKy&index=7 

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.