Chapter 2-8. 무기 : 근접 무기

Date:     Updated:

카테고리:

태그:

인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click


Chapter 2. 무기

근접 무기 구현

🚖 근접 무기 클래스 계층 구조

상 속 ! ! !

  • 📜CloseWeapon.cs
    • 여러 종류의 근접 무기(맨손, 도끼, 곡괭이) 오브젝트들에게 이 스크립트를 붙여서 CloseWeapon 타입이라는 공통점을 만들어 준다.
      • 📜Hand.cs 에서 이름을 바꿔준 스크립트다.
  • 📜CloseWeaponController.cs 👉 추상클래스이며 각각의 근접 무기 컨트롤러들의 공통적인 기능 묶음
    • 모든 근접 무기들이 공통적으로 가지는
      • 속성
        • currentCloseWeapon 현재 장착된 근점 무기
        • isAttack 현재 공격 중인지
        • isSwing 팔을 휘두르는 중인지
        • hitInfo 현재 근접 무기에 닿은 것들의 정보
      • 기능 (차별화 없이 위 기능을 그대로 가짐)
        • TryAttack() 공격 시도 (마우스 좌클 && 공격 중이 아닐 때)
        • AttackCoroutine() 실제 공격
        • CheckObject() 충돌 된 오브젝트가 있었는지 검사 (공격이 들어갔는지)
    • 자식 클래스
      • 📜AxeController.cs 👉 도끼 컨트롤러
        • 도끼만이 차별화하여 가지는
          • 속성
            • isActivate 도끼가 현재 활성화 되어 있는지. static.
          • 기능 (도끼에 맞게 차별화. 오버라이딩)
            • HitCoroutine() 도끼의 공격 처리
              • 추상 함수이기 때문에 완전히 차별화 하여 구현
            • CloseWeaponChange 도끼로 무기 교체
              • 가상 함수이기 때문에 공통적으로 근접무기 교체를 동작하되 도끼만의 속성인 isActivate을 True 해주는 것으로 기능 추가 편집.
      • 📜HandController.cs 👉 맨 손 컨트롤러
        • 맨 손만이 차별화하여 가지는
        • 속성
          • isActivate 맨손이 현재 활성화 되어 있는지. static.
        • 기능 (맨손에 맞게 차별화. 오버라이딩)
          • HitCoroutine() 맨손의 공격 처리
            • 추상 함수이기 때문에 완전히 차별화 하여 구현
          • CloseWeaponChange 맨손으로 무기 교체
            • 가상 함수이기 때문에 공통적으로 근접무기 교체를 동작하되 맨손만의 속성인 isActivate을 True 해주는 것으로 기능 추가 편집.


📜 CloseWeapon.cs

근접 무기 오브젝들(맨손, 도끼, 곡괭이)이 공통적으로 가지고 있는 스크립트. Hand, Axe 오브젝트에 붙인다.

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

public class CloseWeapon : MonoBehaviour
{
    public string closeWeaponName;  // 근접 무기 이름

    // 근접 무기 유형
    public bool isHand;
    public bool isAxe;
    public bool isPickaxe;

    public float range; // 공격 범위. 팔을 뻗으면 어디까지 공격이 닿을지
    public int damage; // 공격력
    public float workSpeed; // 작업 속도
    public float attackDelay;  // 공격 딜레이. 마우스 클릭하는 순간 마다 공격할 순 없으므로.
    public float attackDelayA;  // 공격 활성화 시점. 공격 애니메이션 중에서 주먹이 다 뻗어졌을 때 부터 공격 데미지가 들어가야 한다.
    public float attackDelayB;  // 공격 비활성화 시점. 이제 다 때리고 주먹을 빼는 애니메이션이 시작되면 공격 데미지가 들어가면 안된다.

    public Animator anim;  // 애니메이터 컴포넌트
}
  • isHand, isAxe, isPickaxe 실제 근접 무기들을 구분할 수 있는 변수를 추가해주었다.
    • 이러면 따로 📜Hand.cs, 📜Axe.cs, 📜Pickaxe.cs 이렇게 따로 만들 필요가 없다.


📜 CloseWeaponController.cs

근접 무기 컨트롤러들의 공통적인 기능

추상 함수를 가지고 있는 추상 클래스이므로 오브젝트에 붙지 못한다.

  • 추상 함수는 미완성되어 자식 클래스에서 완성해야하는 함수이기 때문에 추상클래스 자체 또한 미완성된 클래스로 본다. 객체 못만드는 것처럼 추상 클래스를 가진 스크립트는 유니티 게임 오브젝트에 붙일 수 없다.
  • 오브젝트에 붙어서 스크립트가 동작하지 못하므로 Update() 같은 이벤트 함수도 동작하지 못하므로 제거
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class CloseWeaponController : MonoBehaviour
{
    [SerializeField]
    protected CloseWeapon currentCloseWeapon; // 현재 장착된 근접 무기 

    protected bool isAttack = false;  // 현재 공격 중인지 
    protected bool isSwing = false;  // 팔을 휘두르는 중인지. isSwing = True 일 때만 데미지를 적용할 것이다.

    protected RaycastHit hitInfo;  // 현재 무기(Hand)에 닿은 것들의 정보.

    protected void TryAttack()
    {
        if (Input.GetButton("Fire1"))
        {
            if (!isAttack)
            {
                StartCoroutine(AttackCoroutine());
            }
        }
    }

    protected IEnumerator AttackCoroutine()
    {
        isAttack = true;
        currentCloseWeapon.anim.SetTrigger("Attack");

        yield return new WaitForSeconds(currentCloseWeapon.attackDelayA);
        isSwing = true;

        StartCoroutine(HitCoroutine());

        yield return new WaitForSeconds(currentCloseWeapon.attackDelayB);
        isSwing = false;

        yield return new WaitForSeconds(currentCloseWeapon.attackDelay - currentCloseWeapon.attackDelayA - currentCloseWeapon.attackDelayB);
        isAttack = false;
    }

    protected bool CheckObject()
    {
        if (Physics.Raycast(transform.position, transform.forward, out hitInfo, currentCloseWeapon.range))
        {
            return true;
        }

        return false;
    }

    protected abstract IEnumerator HitCoroutine();

    public virtual void CloseWeaponChange(CloseWeapon _closeWeapon)
    {
        if (WeaponManager.currentWeapon != null)
            WeaponManager.currentWeapon.gameObject.SetActive(false);

        currentCloseWeapon = _closeWeapon;
        WeaponManager.currentWeapon = currentCloseWeapon.GetComponent<Transform>();
        WeaponManager.currentWeaponAnim = currentCloseWeapon.anim;

        currentCloseWeapon.transform.localPosition = Vector3.zero;
        currentCloseWeapon.gameObject.SetActive(true);
    }
}

  • 모든 종류의 근접 무기들이 수정 없이 그대로 상속 받는 공통적인 속성 & 기능들. 즉, 모든 종류의 근접 무기들이 가지는 공통적인 속성과 기능.
    • 모든 종류의 근접 무기 컨트롤러 클래스들이 이 클래스를 상속 받을 것이므로 보호 수준을 protected로 올렸다.
    • 속성
      • currentCloseWeapon 현재 장착된 근점 무기
      • isAttack 현재 공격 중인지
      • isSwing 팔을 휘두르는 중인지
      • hitInfo 현재 근접 무기에 닿은 것들의 정보
    • 기능
      • TryAttack() 공격 시도 (마우스 좌클 && 공격 중이 아닐 때)
      • AttackCoroutine() 실제 공격
      • CheckObject() 충돌 된 오브젝트가 있었는지 검사 (공격이 들어갔는지)
  • 📜 CloseWeaponController.cs 를 상속 받는 자식 클래스 마다, 즉 각각의 근접 무기마다 차별화 되야할 기능
    • HitCoroutine() 👉 추상 함수
      • 추상 함수이므로 각각의 근접 무기마다 완전히 다르게 구현해야 한다.
        • 피격한 대상에 대한 데미지 처리는 근접 무기마다 다르기 때문이다.
    • CloseWeaponChange 👉 가상 함수
      • 가상 함수이므로 각각의 근접 무기마다 재정의 해도 되고 안해도 되는데, 재정의 한다면 기존의 완성된 부분에 추가 혹은 편집이 가능하다.
        • 근접 무기를 교체하는 과정은 다 동일하지만 밑에 후술할 각각의 근접무기 컨트롤러마다 본인만의 isActive 멤버를 가지는데, 무기마다 다르게 각각의 무기의 isActivate를 True로 해주어야 하는 차별화된 추가 작업이 붙기 때문에 가상 함수가 적합


📜 AxeController.cs

Holder에 붙는다. Axe 오브젝트의 위치를 잡고 교체해주는 등 도끼와 관련된 여려가지 일을 수행한다.

📜 CloseWeaponController.cs 을 상속 받는다.

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

public class AxeController : CloseWeaponController
{
    // 활성화 여부
    public static bool isActivate = true;

    private void Start()
    {
        WeaponManager.currentWeapon = currentCloseWeapon.GetComponent<Transform>();
        WeaponManager.currentWeaponAnim = currentCloseWeapon.anim;
    }

    void Update()
    {
        if (isActivate)
            TryAttack();
    }

    protected override IEnumerator HitCoroutine()
    {
        while (isSwing)
        {
            if (CheckObject())
            {
                isSwing = false;
                Debug.Log(hitInfo.transform.name);
            }
            yield return null;
        }
    }

    public override void CloseWeaponChange(CloseWeapon _closeWeapon)
    {
        base.CloseWeaponChange(_closeWeapon);
        isActivate = true;
    }
}

  • isActivate
    • 상속 받은게 아닌, 도끼 오브젝트만의 활성화 여부를 알려주는 static 멤버
    • 게임이 시작되면 도끼를 기본으로 들고 있도록 할거라서 초기값 True
  • 부모 클래스인 📜 CloseWeaponController.cs 에선 Update() 를 실행시킬 수 없으므로 📜 AxeController.cs 에서 돌려 줌
  • Start()
    • 게임이 시작되면 도끼를 기본으로 들고 있도록 📜WeaponManager.cs를 통해 초기화.
  • 오버라이딩
    • HitCoroutine()
      • 도끼만의 데미지 처리
    • CloseWeaponChange(CloseWeapon _closeWeapon)
      • 도끼만의 무기 교체 처리
      • 📜 CloseWeaponController.cs 에서 정의한 내용 대로 수행하다가 (근접 무기들의 공통적인 교체 과정 수행)
      • 본인만의 isActivate 값을 True 로 설정


📜 HandController.cs

Holder에 붙는다. Hand 오브젝트의 위치를 잡고 교체해주는 등 맨손과 관련된 여려가지 일을 수행한다.

📜 CloseWeaponController.cs 을 상속 받는다.

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

public class HandController : CloseWeaponController
{
    // 활성화 여부
    public static bool isActivate = false;

    void Update()
    {
        if (isActivate)
            TryAttack();
    }

    protected override IEnumerator HitCoroutine()
    {
        while (isSwing)
        {
            if (CheckObject())
            {
                isSwing = false;
                Debug.Log(hitInfo.transform.name);
            }
            yield return null;
        }
    }

    public override void CloseWeaponChange(CloseWeapon _closeWeapon)
    {
        base.CloseWeaponChange(_closeWeapon);
        isActivate = true;
    }
}

  • isActivate
    • 상속 받은게 아닌, 맨손 오브젝트만의 활성화 여부를 알려주는 static 멤버
  • 부모 클래스인 📜 CloseWeaponController.cs 에선 Update() 를 실행시킬 수 없으므로 📜 AxeController.cs 에서 돌려 줌
  • 오버라이딩
    • HitCoroutine()
      • 맨손만의 데미지 처리
    • CloseWeaponChange(CloseWeapon _closeWeapon)
      • 맨손만의 무기 교체 처리
      • 📜 CloseWeaponController.cs 에서 정의한 내용 대로 수행하다가 (근접 무기들의 공통적인 교체 과정 수행)
      • 본인만의 isActivate 값을 True 로 설정


🚖 도끼 오브젝트

도끼 오브젝트 추가 및 컴포넌트 설정

image

Holder에 무기 컨트롤러 스크립트들 붙이기

image

  • Holder에 빈 오브젝트 Axe Holder 추가
    • 트랜스폼 값을 수정해 도끼 위치를 조정해주자.
    • Hand Holder, Gun Holder가 그랬던 것처럼 Y 축 중심으로 90도 회전
  • 도끼 3D 모델 파일을 Axe 오브젝트로 추가. Axe Holder의 자식으로.

image

  • HandAxe에 📜 CloseWeapon.cs 붙이고 설정
  • Axe의 Layer를 “Weapon”으로.

image


🚖 도끼 애니메이션

애니메이션 클립

image

도끼 3 D 모델 파일에 내장되어 있던 애니메이션을 구간별로 잘라서 8 개의 애니메이션 클립 완성. 걷기, 뛰기, 정지는 Loop 체크


애니메이션 컨트롤러

“AxeController” 이름의 애니메이션 컨트롤러를 만들고 Axe의 Animator 컴포넌트의 컨트롤러로 할당.

image

원래 해오던대로 노가다로 전이 조건 설정 완..


🚖 곡괭이도 추가!

Axe와 마찬가지로 위와 같은 과정을 그대로 반복한다.. 오브젝트 추가하고 애니메이션 클립 만드는 등등..

📜PickaxeController.cs

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

public class PickaxeController : CloseWeaponController
{
    // 활성화 여부
    public static bool isActivate = false;

    void Update()
    {
        if (isActivate)
            TryAttack();
    }

    protected override IEnumerator HitCoroutine()
    {
        while (isSwing)
        {
            if (CheckObject())
            {
                isSwing = false;
                Debug.Log(hitInfo.transform.name);
            }
            yield return null;
        }
    }

    public override void CloseWeaponChange(CloseWeapon _closeWeapon)
    {
        base.CloseWeaponChange(_closeWeapon);
        isActivate = true;
    }
}


🚖 곡괭이 애니메이션 : 애니메이션 컨트롤러 오버라이딩

애니메이션 컨트롤러도 상속 개념이 있다.

곡괭이 애니메이션이 도끼 애니메이션과 상태도와 모든 전이 조건이 똑같고, 단지 애니메이션 클립만 다르다면 일일이 노가다로 다시 만들 필요 없이 도끼 애니메이션 컨트롤러를 상속받아 곡괭이 애니메이션 컨트롤러를 만들 수 있다.

파라미터와 전이 조건, 전이 관계들은 그대로 유지하되 애니메이션 클립만 바꿀 수 있다. 상태도와 파라미터를 그대로 유지하되 클립만 다른 것으로 사용하길 원할 땐 오버라이드 하자.

애니메이션 컨트롤러 상속 받기

image

우클 - Create - Animator Override Controller 로 다른 컨트롤러를 상속 받아 만들 곡괭이의 애니메이션 컨트롤러를 추가해준다. 이름은 “PickaxeController”

image

도끼 애니메이션 컨트롤러를 상속 받기 위해 “AxeController”를 할당해준다.

image

도끼 애니메이션 컨트롤러를 그대로 따르되 위와 같이 애니메이션 클립만 바꿔치기 해서 곡괭이 애니메이션 컨트롤러를 완성한다. Tree 클립 상태는 곡괭이에겐 없기 때문에 None으로 둔다.


📜WeaponManager.cs 수정 (무기 교체에 도끼, 곡괭이도 추가)

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

[RequireComponent(typeof(GunController))]       // 자동으로 이 스크립트가 활성화되면 📜GunController.cs 가 컴포넌트로 붙게 된다. 실수 방지(📜GunController.cs가 필요하다고 강제함)
public class WeaponManager : MonoBehaviour
{
    public static bool isChangeWeapon = false;  // 무기 교체 중복 실행 방지. (True면 못하게)

    [SerializeField]
    private float changeweaponDelayTime;  // 무기 교체 딜레이 시간. 총을 꺼내기 위해 손 집어 넣는 그 시간. 대략 Weapon_Out 애니메이션 시간.
    [SerializeField]
    private float changeweaponEndDelayTime;  // 무기 교체가 완전히 끝난 시점. 대략 Weapon_In 애니메이션 시간.

    [SerializeField]
    private Gun[] guns;  // 모든 종류의 총을 원소로 가지는 배열
    [SerializeField]
    private CloseWeapon[] hands;  //  Hand 형 무기를 가지는 배열
    [SerializeField]
    private CloseWeapon[] axes;  // 도끼 형 무기를 가지는 배열
    [SerializeField]
    private CloseWeapon[] pickaxes;  // 곡괭이 형 무기를 가지는 배열

    // 관리 차원에서 이름으로 쉽게 무기 접근이 가능하도록 Dictionary 자료 구조 사용.
    private Dictionary<string, Gun> gunDictionary = new Dictionary<string, Gun>();
    private Dictionary<string, CloseWeapon> handDictionary = new Dictionary<string, CloseWeapon>();
    private Dictionary<string, CloseWeapon> axeDictionary = new Dictionary<string, CloseWeapon>();
    private Dictionary<string, CloseWeapon> pickaxeDictionary = new Dictionary<string, CloseWeapon>();

    [SerializeField]
    private string currentWeaponType;  // 현재 무기의 타입 (총, 도끼 등등)
    public static Transform currentWeapon;  // 현재 무기. static으로 선언하여 여러 스크립트에서 클래스 이름으로 바로 접근할 수 있게 함.
    public static Animator currentWeaponAnim; // 현재 무기의 애니메이션. static으로 선언하여 여러 스크립트에서 클래스 이름으로 바로 접근할 수 있게 함.

    [SerializeField]
    private GunController theGunController;  // 총 일땐 📜GunController.cs 활성화, 다른 무기일 땐 📜GunController.cs 비활성화 
    [SerializeField]
    private HandController theHandController; // 손 일땐 📜HandController.cs 활성화, 다른 무기일 땐 📜HandController.cs 비활성화
    [SerializeField]
    private AxeController theAxeController; // 도끼 일땐 📜AxeController.cs 활성화, 다른 무기일 땐 📜AxeController.cs 비활성화
    [SerializeField]
    private PickaxeController thePickaxeController; // 곡괭이 일땐 📜PickaxeController.cs 활성화, 다른 무기일 땐 📜PickaxeController.cs 비활성화


    void Start()
    {
        for (int i = 0; i < guns.Length; i++)
        {
            gunDictionary.Add(guns[i].gunName, guns[i]);
        }
        for (int i = 0; i < hands.Length; i++)
        {
            handDictionary.Add(hands[i].closeWeaponName, hands[i]);
        }
        for (int i = 0; i < axes.Length; i++)
        {
            axeDictionary.Add(axes[i].closeWeaponName, axes[i]);
        }
        for (int i = 0; i < pickaxes.Length; i++)
        {
            pickaxeDictionary.Add(pickaxes[i].closeWeaponName, pickaxes[i]);
        }
    }

    void Update()
    {
        if (!isChangeWeapon)
        {
            if (Input.GetKeyDown(KeyCode.Alpha1)) // 1 누르면 '맨손'으로 무기 교체 실행
                StartCoroutine(ChangeWeaponCoroutine("HAND", "맨손"));
            else if (Input.GetKeyDown(KeyCode.Alpha2)) // 2 누르면 '서브 머신건'으로 무기 교체 실행
                StartCoroutine(ChangeWeaponCoroutine("GUN", "SubMachineGun1"));
            else if (Input.GetKeyDown(KeyCode.Alpha3)) // 3 누르면 '도끼'로 무기 교체 실행
                StartCoroutine(ChangeWeaponCoroutine("AXE", "Axe"));
            else if (Input.GetKeyDown(KeyCode.Alpha4)) // 4 누르면 '곡괭이'로 무기 교체 실행
                StartCoroutine(ChangeWeaponCoroutine("PICKAXE", "Pickaxe"));
        }
    }

    public IEnumerator ChangeWeaponCoroutine(string _type, string _name)
    {
        isChangeWeapon = true;
        currentWeaponAnim.SetTrigger("Weapon_Out");

        yield return new WaitForSeconds(changeweaponDelayTime);

        CancelPreWeaponAction();
        WeaponChange(_type, _name);

        yield return new WaitForSeconds(changeweaponEndDelayTime);

        currentWeaponType = _type;
        isChangeWeapon = false;
    }

    private void CancelPreWeaponAction()
    {
        switch(currentWeaponType)
        {
            case "GUN":
                theGunController.CancelFineSight();
                theGunController.CancelReload();
                GunController.isActivate = false;
                break;
            case "HAND":
                HandController.isActivate = false;
                break;
            case "AXE":
                AxeController.isActivate = false;
                break;
            case "PICKAXE":
                PickaxeController.isActivate = false;
                break;
        }
    }

    private void WeaponChange(string _type, string _name)
    {
        if(_type == "GUN")
        {
            theGunController.GunChange(gunDictionary[_name]);
        }
        else if(_type == "HAND")
        {
            theHandController.CloseWeaponChange(handDictionary[_name]);
        }
        else if (_type == "AXE")
        {
            theAxeController.CloseWeaponChange(axeDictionary[_name]);
        }
        else if (_type == "PICKAXE")
        {
            thePickaxeController.CloseWeaponChange(pickaxeDictionary[_name]);
        }
    }
}

image

총 무기가 1,2,3 종류가 있고 곡괭이형 무기가 여러개 있고 이러면 이제 배열의 원소로 추가하여 넣어주면 된다.


🚔 오늘 알게 된 꿀팁

Visual Studio

  1. for 까지 치고 Tab을 두번 누르면 for문이 자동으로 완성 된다.
  2. 클래스 이름 - 우클 - 빠른 작업 및 리팩터링
    • 오버라이딩 해야할 함수 정의와 바디 형식을 바로 만들어주고
    • 생성자도 빠르게 만들 수 있다.


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Unity Lesson 3 카테고리 내 다른 글 보러가기

댓글 남기기