새소식

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

[3D 액션게임] 06. 근접무기 공격구현

  • -

🔔 유튜브 크리에이터 골든메탈님의 유니티강의 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 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 iDown;
    bool sDown1;
    bool sDown2;
    bool sDown3;

    bool isJump;
    bool isDodge;
    bool isSwap;
    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();
        Dodge();
        Interation();
        Swap();
        Attack();
    }


    void GetInput()
    {
        // 키보드입력에 따라 0 ~ 1로 변환 left right up down
        hAxis = Input.GetAxisRaw("Horizontal");
        vAxis = Input.GetAxisRaw("Vertical");
        rDown = Input.GetButton("Run");
        jDown = Input.GetButtonDown("Jump");
        fDown = Input.GetButtonDown("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)
            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()
    {
        // Rotation
        transform.LookAt(transform.position + moveVec);
    }

    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("doSwing");
            fireDelay = 0;
        }
    }

    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. 근접무기 변수 및 효과 추가

 

- 변수 추가

public class Weapon : MonoBehaviour
{

    public enum Type { Melee, Range };
    public Type type;
    public int damage;
    public float rate;
    public BoxCollider meleeArea;
    public TrailRenderer trailEffect;

}
  • 무기 타입, 데미지, 공격속도, 콜라이더, 렌더이펙트로 변수를 만들어줍시다.
  • public으로 만들어 default 값은 유니티에서 설정 할 예정입니다.

 

- 피격 범위 추가

  • Box Collider를 추가해 피격 범위를 설정합니다.

 

- 이펙트 추가

  • Trail Renderer을 이용해 움직일때 따라오는 이펙트를 만들어줍시다.
  • Width 곡선을 곡선형태로 만들어 시간이 가면갈수록 자연스레 사라지게 해줍니다.
  • 컬러 설정을 무기 색과 비슷하게 하였습니다
  • Min Vertex Distance 를 설정해 이펙트가 더 각지게 만들어 줬습니다.

 

이후 만든 이펙트, 박스 콜라이더를 플레이어 Weapon 스크립트에 끌여당겨 넣어줍니다.

 

🧷 2. 공격 로직

- 무기 스크립트 공격 추가 (코루틴)

public class Weapon : MonoBehaviour
{
    public void Use()
    {
        if(type == Type.Melee) {
            StopCoroutine("Swing");
            StartCoroutine("Swing");
        }
    }

    // IEnumerator : 열거형 함수 클래스
    IEnumerator Swing()
    {
        // 중요한 개념인 코루틴
        // 기존 : Use() 메인루틴 -> Swing() 서브루틴 -> Use() 메인루틴 (교차실행)
        // 코루틴 : Use() 메인루틴 + Swing() 코루틴 Co - (동시실행)

        // yield : 결과를 전달하는 키워드, 여러 개 사용해 시간차 로직 구현가능
        
        //1
        yield return new WaitForSeconds(0.1f); // 0.1 초 대기
        meleeArea.enabled = true;
        trailEffect.enabled = true;

        //2
        yield return new WaitForSeconds(0.3f); // 0.3 초 대기
        meleeArea.enabled = false;

        //3
        yield return new WaitForSeconds(0.3f); // 0.3 초 대기
        trailEffect.enabled = false;


    }

}
  • Swing 이란 기능에 yield를 넣어 임의의 대기시간을 만들어줍시다.
  • WaitForSeconds(x) :  실제시간 x초 대기 해줍니다.
  • 이후 콜라이더와, 이펙트의 효과 시간을 조절해줍시다.
  • Use() : 다시 시전시, 혹시 모르는 미리 시전된 코루틴을 종료시켜주면서 다시 시작해줍니다.

 

- 플레이어 스크립트 무기 사용 조건 추가

/* Player 스크립트, 근접무기 공격 추가, 추가된 내용만 올려놓겠습니다. */

public class Player : MonoBehaviour
{
	bool fDown; // 공격버튼
    bool isFireReady = true; // 공격가능 여부
    
    Weapon equipWeapon; // GameObject -> Weapon 스크립트로 변경
    float fireDelay; // 공격 딜레이 타임
    
    void Update(){
    	Attack();
    }
    
    void GetInput(){
    	fDown = Input.GetButtonDown("Fire1")
    }
    
    void Move(){  
        // 스왑 및 공격 시 못움직이게
        if (isSwap || !isFireReady)
            moveVec = Vector3.zero;
    }
    
    // 이번 스크립트 중요부분
    void Attack()
    {
        // 공격할 조건만 플레이어에 두고, 공격로직은 무기에 위임한다.
        if (equipWeapon == null)
            return;

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

        if(fDown && isFireReady && !isDodge && !isSwap) {
            equipWeapon.Use();
            anim.SetTrigger("doSwing");
            fireDelay = 0;
        }
    
    }

    
    void Swap()
    {
        // equipWeapon이 GameObject -> Weapon으로 바뀌기에 맞춰서 바꿔줍니다

        
        if((sDown1 || sDown2 || sDown3) && !isJump && !isDodge) {
            if(equipWeapon != null)
                equipWeapon.gameObject.SetActive(false);

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

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

        }
    }
    
}
  • 공격 딜레이 시간, 공격가능 여부
  • GameObject를 스크립트로 변경하여 수정점 변경
  • Attack() 함수를 통해 공격조건 구현 나머지는 장착한 무기의 Use()를 통해 구현

 

🧷 3. 스윙 애니메이션

  • 스윙 애니메이션을 넣어, doSwing이란 트리거도 만들어줍시다.

 

 

 

 

 

 

 

 

 

출처: 골든메탈님 유튜브

https://www.youtube.com/watch?v=Zfoyagdz1y0&list=PLO-mt5Iu5TeYkrBzWKuTCl6IUm_bA6BKy&index=6 

 

Contents

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

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