FPS서바이벌 디펜스

10.근접무기 구현, 무기 교체

ruripanda 2025. 5. 1. 16:51

본 강좌는 케이디님의 유튜브강좌 영상을 보고 유니티6로 구현한 것 입니다 이렇게 멋진 강좌를 만드신 케이디님을 존경합니다

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

public class WeaponManager : MonoBehaviour
{
    //무기 중복 교체 실행방지
    public static bool isChangeWeapon = false;//static 공유되는 자원

    //현재 무기의 애니메이션
    public static Transform currentWeapon;
    public static Animator currentWeaponAnim;

    //현재 무기 타입
    [SerializeField]
    private string currentWeaponType;

    [SerializeField]
    private float changeWeaponDelayTime;
    [SerializeField]
    private float changeWeaponEndDelayTime;

    //무기 종류들 전부 관리
    [SerializeField]
    private Gun[] guns;
    [SerializeField]
    private CloseWeapon[] hands;
    [SerializeField]
    private CloseWeapon[] axes;
    [SerializeField]
    private CloseWeapon[] pickaxes;

    //관리 차원에서 쉽게 무기 접근이 가능하게 만듦
    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> pickaxesDictionary = new Dictionary<string, CloseWeapon>();

    //필요한 컴포넌트
    [SerializeField]
    private GunController theGunController;
    [SerializeField]
    private HandController theHandController; 
    [SerializeField]
    private AxeController theAxeController;
    [SerializeField]
    private PickaxeController thePickaxeController;

    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++)
        {
            pickaxesDictionary.Add(pickaxes[i].closeWeaponName, pickaxes[i]);
        }
    }

    void Update()
    {
        if (!isChangeWeapon)
        {
            if (Input.GetKeyDown(KeyCode.Alpha1))
                StartCoroutine(ChangeWeaponCoroutine("HAND", "맨손"));
            else if (Input.GetKeyDown(KeyCode.Alpha2))
                StartCoroutine(ChangeWeaponCoroutine("GUN", "SubMachineGun1"));
            else if (Input.GetKeyDown(KeyCode.Alpha3))
                StartCoroutine(ChangeWeaponCoroutine("AXE", "Axe"));
            else if (Input.GetKeyDown(KeyCode.Alpha4))
                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.CloseWeaopnChange(handDictionary[_name]);
        else if (_type == "AXE")
            theAxeController.CloseWeaopnChange(axeDictionary[_name]);
        else if (_type == "PICKAXE")
            thePickaxeController.CloseWeaopnChange(pickaxesDictionary[_name]);
    }
}
using System.Collections;
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 CloseWeaopnChange(CloseWeapon _closeWeapon)
    {
        base.CloseWeaopnChange(_closeWeapon);
        isActivate = true;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PickaxeController : 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 CloseWeaopnChange(CloseWeapon _closeWeapon)
    {
        base.CloseWeaopnChange(_closeWeapon);
        isActivate = true;
    }
}
using System.Collections;
using UnityEngine;

public abstract class CloseWeaponController : MonoBehaviour
{
    //현재 장착된 Hand형 타입 무기
    [SerializeField]
    protected CloseWeapon currentCloseWeapon;

    //공격중??
    protected bool isAttack = false;
    protected bool isSwing = false;

    protected RaycastHit hitInfo;

    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 abstract IEnumerator HitCoroutine();//abstract 미완성 = 추상코루틴, 자식스크립트에서 완성예정

    protected bool CheckObject()
    {
        if (Physics.Raycast(transform.position, transform.forward /*transform.TransformDirection(Vector3.forward)은 같은 뜻*/, out hitInfo, currentCloseWeapon.range))
        {
            return true;
        }
        return false;
    }

    //virtual, 완성 함수이지만 추가 편집이 가능한 함수
    public virtual void CloseWeaopnChange(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);
    }
}

 

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

public class AxeColtroller : 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 CloseWeaopnChange(CloseWeapon _closeWeapon)
    {
        base.CloseWeaopnChange(_closeWeapon);
        isActivate = true;
    }
}
using UnityEngine;

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

    //웨폰 유형
    public bool isHand;
    public bool isAxe;
    public bool isPickaxe;          //bool값으로 무기를 고를 수 있게 한다

    public float range;             //공격범위
    public int damage;              //공격력
    public float workSpeed;         //작업속도
    public float attackDelay;       //공격 딜레이
    public float attackDelayA;      //공격 활성화 시점
    public float attackDelayB;      //공격 비활성화 시점

    public Animator anim;           //애니메이션
}

 

오늘은 근접무기를 구현할 것이다

복잡한 부분이 많으니 차근차근 따라하자

 

일단 Hand 스크립트를 CloseWeapon으로 이름을 바꾸고 실행해주자

일단 표시한 부분을 수정해주자

3번줄에 v자로 체크한 부분은 스크립트 이름인데 실제 파일이름과 일치하지 않으면 오류가 나므로 파일명과 똑같이 바꿔준다

그리고 5번줄과 8~10번줄까지 수정하고 추가해주자

bool로 선언된 8~10번은 true가 되면 해당 무기를 사용하게 변경된다

 

컴파일이 끝나면 콘솔에 오류가 마구 날탠데...

Hand 라고 스크립트 명이 들어간 부분이 오류가 날것이다

더블클릭해서 해당스크립트 부분으로 들어간 다음 Hand를 CloseWeapon으로 바꿔주자

스크린샷을 찍는게 좋은데 이번 강좌를 너무 수정되는 부분이 많아서 못 찍었다

케이디님도 강좌로 보여주니까 보면서 수정하자

 

이제 C# 스크립트 AxeController를 만들어서 작성해주자

그리고 HandController을 열어주자

 

AxeController 와 HandController 는 메커니즘이 똑같다

우리는 상속을 통해서 스크립트가 중복되는 부분을 작성해주고 편하게 관리할 것이다

 

CloseWeaponController 라는 C# 스크립트를 만들어주자

그리고 AxeController 와 HandController에 중복되는 부분을 잘라서 붙여놓어주자

 

그리고 AxeController 스크립트에서 v자로 체크된 부분에 CloseWeaonController 이라고 작성하여 스크립트를 상속해주자

HandController도 마찬가지로 해주자

 

그리고 기존 private나 public 로 작성된 부분을 protected로 바꿔주자

이건 자식클레스 즉 상속받은 클레스에서 같이 사용할 수 있게 보호수준을 내려주는 기능이다

 

맨 위에 업로드한 스크립트를 참고해서 수정해주자

 

 

그리고 지난시간에 작성한 함수인 히트코루틴이 이렇게 수정된 것을 볼 수 있다

abstract라고 선언을 해주었는데 이 의미는 해당 함수는 자식 클레스에서 완성시킨다는 뜻이다

그리고 abstract라고 선언을 해주면

해당 스크립트의 이름 옆에도 이렇게 abstract를 작성해주어야 한다

AxeController 스크립트에서 완성된 부분이다 override라고 선언을해주어서 이어받고 완성해주었다

 

케이디님은 영상에에서 스크립트 이름이 빨간색으로 밑줄이 쳐지면 빨간줄에서 오른쪽 마우스버튼을 클릭하고 빠른 작업 및 리펙터링을 하셨다 그 뒤 히트 코루틴을 불러오셨는데 필자는 이미 작성을 해버려서 스크린샷을 못 찍었다

앞으로는 스크립트 부분은 복사해서 따로 오리지널을 만들은 다음 스크린샷을 올릴까 고민 중이다

그리고 abstract로 선언된 클레스가 적용된 스크립트는 단독으로는 사용이 불가능하다

미완성인 채로 남겨놨기 때문이다

 

이제 AxeController 스크립트를 마저 작성해주자

HandController도 마찬가지 이다

 

그리고 CloseWeaponController에 빨간줄이 또 생겼을 탠데 CloseWeapon으로 싹다 바꿔주자

편의를 위해서 위에 스크립트를 올려놓았다

 

WeaponManager 스크립트에도 빨간줄이 나온게 생겼을 것이다 수정해주자

 

그리고 CloseWeaponController 스크립트로 돌아와서

CloseWeaponChange 함수를 보자

virtual 이라고 선언해주었다 위에 주석 말 그대로 완성 함수이지만 추가 편입이 가능한 함수로 바꿔주는 기능이다

virtual는 정확히는 가상함수인데 이렇게 기억을 하면 기억하지 못할 확율이 높다

 

AxeController 스크립트를 열어보자

이렇게 override라고 선언이 되었고 함수 안에 base.CloseWeaponChange라고 작성이 되어 있다

이 부분은 virtual이라고 선언된 함수 부분을 불러오는 것이다

필자는 자주 쓰던 기능이 아니라서 머리 아프다...

 

HandController에도 추가해주자

 

이렇게해주면 새 근접 무기가 나올때 마다 이렇게 작성하여 넣어줄 수 있다

 

이제 유니티엔진으로 돌아와서 도끼 애니메이션을 나눠주자

 

프레임은 위 스샷을 참고해서 나눠주면 되고 Idle,Walk,Run은 LoopTime와 LoopPose가 체크된다

 

그 다음 애니메이션 폴더에 애니메이션 컨트롤러를 만들어주고 오른쪽 스샷처럼 화살표를 이어주고 화살표 설정도 잡아주자

지금까지 해온 기능을 적절히 활용하면 충분히 다 작성할 수 있다

필자도 공부한거 다 총동원해서 넣었더니 잘 됬다

 

그 다음 Holder 객체의 내부에 Axe Holder 객체를 만들어주고 위 스크린샷 처럼 방향을 수정해준다

그 뒤 우리가 수정해준 모델을 하위에 넣어주고  Axe라고 바꿔주자

그리고 모델에 있는 카메라를 제거해주자

 

그럼 이렇게 모습이 바뀌어 있을 것이다

 

이제 Axe라고 이름을 바꾼 모델에서 CloseWeapon 스크립트를 넣어주고 Animator도 넣어주자

 

그뒤 Animator에 우리가 만들어준 Axe_Controller을 슬롯에 넣어주고 Animator 컴포넌트를 CloseWeapon 스크립트에 Anim슬롯에 넣어준다

 

그리고 지난 시간에 만들어놓은 Hand도 이렇게 수정해준다

 

그리고 스크립트를 수정했으니 Hand Controller도 다시 슬롯에 넣어준다

 

그리고 테스트를 위해 8번 줄을 true로 잠시만 바꿔주자

그리고 다른 무기들은 전부 false로 바꿔주자

 

그뒤 

 

맨 AxeController에 void Start()함수를 이렇게 작성해준다

이것도 잠시만 넣어주자

 

여기까지 했다면 도끼가 잘 움직일 것이다 단 무기를 바꾸는 것은 안될 것이다

 

이제 무기를 바꾸는 것을 구현해보자

WeaponManager 스크립트로 들어가서 v자로 체크된 부분을 수정하자

여기까지 체크한 부분을 작성해넣자

 

이제 인스펙터도 넣어주자

 

여기까지하면 무기를 바꿔주는 것까지 구현이 될 것이다

도끼가 짤리는 현상이 나오면 Layer를 Weapon으로 바꿔주자

 

이제 곡굉이를 만들어보자

 

곡굉이도 모델에 있는 에니메이션을 나눠줘야 한다

Idle,Walk,Run은 전부 LoopTime와 LoopPose 체크되어야한다

 

여기서 기능을 소개하겠다 Override Animator 이라고 불리는 기능이다

일단 원본으로 사용될 Axe_Controller을 슬롯에 넣어주고 그에 대응하는 부분에 에니메이션을 넣어주면 깔끔하게 복제가 된다

 

나머지는 도끼를 만들때와 똑같이 반복해주면 된다

스크립트는 위에 전부 올려놓았다

케이디님도 영상에 반복하는 것을 올려놓았으니 보면서 똑같이 따라하자

그러면 도끼와 곡갱이가 구현이 끝날 것이다

 

이렇게 전부 구현이 되었다

'FPS서바이벌 디펜스' 카테고리의 다른 글

12.SoundManager  (0) 2025.05.06
11.광석 채굴  (0) 2025.05.02
9.Weapon Sway(마우스 민감도)  (0) 2025.04.28
8.Weapon Manager  (0) 2025.04.26
7.크로스헤어(조준점)  (0) 2025.04.23