Chapter 11-1. 포션 제조 : 연금테이블 제작(+ 코루틴 사용시 주의사항)

Date:     Updated:

카테고리:

태그:

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

포션 제조 : 연금 테이블 제작

🚀 연금 테이블 만들기

image

  • 연금 테이블 아이템을 포션 먹을 때처럼 손에 들고 있을 수 있다.
    • 연금 테이블 아이템을 손에 들고 있으면 자동으로 건축 시스템처럼 프리뷰가 생성되며 크로스헤어를 따라다닌다.
      • 이 상태에서 좌클 누르면 연금 테이블 건축 가능

✈ 실제 건축물

  • Box Collider 를 붙여준다.
  • Layer를 Building으로 한다.


✈ 프리팹

  • 📜PreviewObject.cs 를 붙여준다.
  • Rigidbody 를 붙여준다.
  • Box Collider 를 붙여준다.
    • Is Trigger 체크


🚀 연금 테이블 아이템화

연금 테이블 같은 Kit 종류의 아이템은 손에 들고 있을 때 기타 건축물처럼 건축이 가능하도록 프리뷰 오브젝트를 만들어주고 건축이 가능하게끔 하는 특성이 있어야 한다. 따라서 Kit 아이템을 추가해주고 이와 관련된 속성들도 추가해준다.

📜Item

[CreateAssetMenu(fileName = "New Item", menuName = "New Item/item")]
public class Item : ScriptableObject  // 게임 오브젝트에 붙일 필요 X 
{
    public enum ItemType  // 아이템 유형
    {
        Equipment,
        Used,
        Ingredient,
        Kit,  // ⭐추가!
        ETC,
    }

    public string itemName; // 아이템의 이름
    [TextArea]  // 여러 줄 가능해짐
    public string itemDesc; // 아이템의 설명
    public ItemType itemType; // 아이템 유형
    public Sprite itemImage; // 아이템의 이미지(인벤 토리 안에서 띄울)
    // Image는 Canvas 위 에서만 띄울 수 있고
    // Sprite는 Canvas와 상관없이 월드 어디에서든 이미지를 띄울 수 있다.
    public GameObject itemPrefab;  // 아이템의 프리팹 (아이템 생성시 프리팹으로 찍어냄)
    public GameObject kitPrefab;  // ⭐추가! 키트 프리팹
    public GameObject kitPreviewPrefab; // ⭐추가! 키트 프리뷰 프리팹
    public string weaponType;  // 무기 유형
}

image

연금 테이블의 Item ScriptableObject 에셋의 kitPrefabkitPreviewPrefab에 각각 위에서 만든 연금테이블 프리팹과 연금테이블 프리뷰 프리팹 할당.


🚀 맨손으로 연금테이블 들고 이 상태에서 건축 가능한 상태로 만들기

📜HandController

이 스크립트는 Player의 자식 Holder에 붙으며, 📜CloseWeaponContoller 를 상속 받고 있다.

연금 테이블 같은 Kit 종류의 아이템을 맨 손으로 들고 있을 때 프리뷰를 생성하고 건축 할 수 있도록 해야 한다. 이를 📜HandController에서 구현한다. 참고로 맨 손에 아이템을 드는 처리는 📜QuickSlotController 에서 이루어진다.

    public static Item currentKit; // 설치하려는 킷 (연금 테이블)

    private bool isPreview = false; // 현재 프리뷰 활성화 중인지 (프리뷰 중복 생성 방지용)

    private GameObject go_preview;  // 설치할 키트 프리뷰
    private Vector3 previewPos; // 설치할 키트 위치 (크로스헤어 따라 움직이게 계속 갱신할 것)
    [SerializeField] private float rangeAdd;  // 건축시 추가 사정거리(코 앞에 말고 보통의 근접무기의 사정 거리 range보다는 더 먼 곳에 건축할 수 있도록.)
    [SerializeField] private LayerMask preview_Kit_LayerMask; // 키트 건축할 수 있는 곳 레이어 (Building, Terrain)

    [SerializeField]
    private QuickSlotController theQuickSlotController;
  • currentKit는 📜QuickSlotController 에서 퀵슬롯에 있는 연금테이블같은 Kit 아이템이 활성화 되었다면 이 currentKit에 해당 Kit 아이템이 할당 되도록 할 것이다.
    • 여러 곳에서 공유될 수 있고 📜HandController 객체 생성 할 필요 없이 static 변수로 선언
  • ranage는 📜CloseWeaponContoller 로부터 상속 받은, 맨 손 같은 근접 무기들이 충돌을 감지 할 수 있는 사정 거리다.
    • 건축 할 수 있는 위치와 동시에 프리뷰가 매프레임 갱신 될 위치는 여타 근접 무기들의 range보다 좀 더 먼 곳에서도 생성될 수 있도록 해야 한다.
    • 코 앞에서만 건축이 가능하다면 부자연스러우니까!
    • 따라서 밑에 Raycast 충돌 처리에서 range + rangeAdd 사정 거리까지 여유를 두었다.
    void Update()
    {
        if (isActivate && !Inventory.invectoryActivated)
        {
            if (currentKit == null)
            {
                if (QuickSlotController.go_HandItem == null) // 손에 들린 아이템이 없으면 맨손 공격 가능 상태
                    TryAttack();
                else  // 손에 들린 아이템이 있다면 포션 같은 소비아이템이므로 먹을 수 있는 상태
                    TryEating();
            }
            else 
            {
                if (!isPreview)
                    InstallPreviewKit();
                PreviewPositionUpdate();
                Build();
            }
        }         
    }

    private void InstallPreviewKit()
    {
        isPreview = true;
        go_preview = Instantiate(currentKit.kitPreviewPrefab, transform.position, Quaternion.identity);
    }

    private void PreviewPositionUpdate()
    {
        if (Physics.Raycast(transform.position, transform.forward, out hitInfo, currentCloseWeapon.range + rangeAdd, preview_Kit_LayerMask))
        {
            previewPos = hitInfo.point;
            go_preview.transform.position = previewPos;
        }
    }

    private void Build()
    {
        if(Input.GetButtonDown("Fire1"))
        {
            if (go_preview.GetComponent<PreviewObject>().isBuildable())
            {
                theQuickSlotController.DecreaseSelectedItem(); // 슬롯 아이템 개수 1 차감
                GameObject temp = Instantiate(currentKit.kitPrefab, previewPos, Quaternion.identity);
                temp.name = currentKit.itemName;
                Destroy(go_preview);
                currentKit = null;
                isPreview = false;
            }
        }
    }

    public void Cancel()
    {
        Destroy(go_preview);
        currentKit = null;
        isPreview = false;
    }
  • Update
    • currentKit가 null 이 아니라면 키트 아이템을 들게 된 것이므로 이에 대한 처리를 해야 한다.
      • 매 프레임마다
        • 현재 프리뷰 없는 상태라면 프리뷰 생성 👉 InstallPreviewKit
        • 프리뷰 생성 후 프리뷰의 위치를 플레이어에서 Raycast 쏴서 충돌한 위치로 업데이트 👉 PreviewPositionUpdate
        • 좌클 입력 받아 건축 작업 👉 Build
  • InstallPreviewKit
    • 프리뷰 중복 생성 방지하도록 isPreview 값을 true로
    • 프리뷰 생성
      • 일반 플레이어의 위치 transform.position에서 생성한다.
  • PreviewPositionUpdate
    • 프리뷰의 위치를 매프레임마다 업데이트한다.
      • 플레이어에서 Raycast를 쏴서 currentCloseWeapon.range + rangeAdd 사정 거리 내에서 preview_Kit_LayerMask에 해당하는 레이어를 가진 충돌 오브젝트의 정보를 hitInfo에 담는다. 그리고 그 충돌 위치로 프리뷰의 위치를 설정한다.
        • preview_Kit_LayerMask에는 Terrain, Building 레이어를 설정할 것이다. 즉, 일반 지형 위 혹은 건축물 위에서만 이 키트를 지을 수 있게 할 것이다.
        • 강의에서는 📜CloseWeaponContoller 로부터 상속 받은 layerMask를 그대로 쓰셨는데 이 레이어 마스크는 맨손으로 때릴 때나 아이템 주울 때나 이럴 때 하는 충돌처리에 사용했던 레이어 마스크기 때문에 Animal, Item, Default, Water 등등 이 연금테이블 건축과 상관없는 여러 레이어들이 선택되어 있는 상태였다. 그래서 프리뷰의 레이어가 이 layerMask에 해당되는 레이어라면 hitInfo.point가 방금 프리뷰가 생성됐던 플레이어 위치가 되버려서 플레이어의 Collider와 프리뷰의 Collider 가 마구 충돌하는 버그가 발생하기 때문에 프리뷰의 레이어를 layerMask에 속하지 않는 Ignore Raycast 이런 걸로 바꿔 줬어야 했다. 또 Animal, Item, Water 등등 이런 오브젝트 위에도 연금테이블을 지을 수 있다는 문제가 생기기 때문에 강의와 달리 preview_Kit_LayerMask라는 레이어 마스크 멤버 변수를 만들어 주었다.
  • Build
    • 좌클 입력이 들어오면 연금 테이블 같은 키트 종류 아이템을 건축한다.
      • 퀵슬롯 아이템 개수 차감
      • 연금 테이블 생성
      • 프리뷰는 삭제 후 currentKitisPreview도 초기화
  • Cancel
    • 📜WeaponManager를 통해 무기를 바꿀 때 만약 맨손으로 연금테이블 들고 있는 상태에서 다른 무기로 교체했다면 프리뷰도 없애고 currentKitisPreview도 초기화 해야 됨. 이때 필요하기 때문에 public 함수로 만들어 줌.

image

  • rangeAdd
    • range 값이 3 이고 rangeAdd가 3 이라면 3 + 3 = 6 의 사정 거리내에 연금 테이블 건축 가능
  • preview_Kit_LayerMask
    • 연금테이블 프리뷰 및 연금테이블이 Terrain, Building 레이어를 가진 오브젝트에서만 지어질 수 있도록


📜QuickSlotController

    private void Execute()
    {
        CoolTimeReset();
        AppearReset();

        if (quickSlots[selectedSlot].item != null)
        {
            if (quickSlots[selectedSlot].item.itemType == Item.ItemType.Equipment)
                StartCoroutine(theWeaponManager.ChangeWeaponCoroutine(quickSlots[selectedSlot].item.weaponType, quickSlots[selectedSlot].item.itemName));
            else if (quickSlots[selectedSlot].item.itemType == Item.ItemType.Used || quickSlots[selectedSlot].item.itemType == Item.ItemType.Kit)
                ChangeHand(quickSlots[selectedSlot].item);
            else
                ChangeHand();
        }
        else
        {
            ChangeHand();
        }
    }

두 번째 else if 문에서 Item.ItemType.Used 소비 아이템 뿐만 아니라 Item.ItemType.Kit 연금테이블 같은 키트 아이템도 맨손에 아이템을 들게 하도록 ChangeHand(quickSlots[selectedSlot].item); 호출

    private void ChangeHand(Item _item = null)
    {
        StartCoroutine(theWeaponManager.ChangeWeaponCoroutine("HAND", "맨손"));

        if (_item != null)
        {
            StartCoroutine(HandItemCoroutine(_item));
        }
    }

    IEnumerator HandItemCoroutine(Item _item)
    {
        HandController.isActivate = false;
        yield return new WaitUntil(() => HandController.isActivate);  // 맨손 교체의 마지막 과정

        if (_item.itemType == Item.ItemType.Kit)
            HandController.currentKit = _item;

        go_HandItem = Instantiate(quickSlots[selectedSlot].item.itemPrefab, tf_ItemPos.position, tf_ItemPos.rotation);
        go_HandItem.GetComponent<Rigidbody>().isKinematic = true;  // 중력 영향 X 
        go_HandItem.GetComponent<Collider>().enabled = false;  // 콜라이더 끔 (플레이어와 충돌하지 않게)
        go_HandItem.tag = "Untagged";   // 획득 안되도록 레이어 태그 바꿈
        go_HandItem.layer = 8;  // "Weapon" 레이어는 int
        go_HandItem.transform.SetParent(tf_ItemPos);
    }
  • 퀵슬롯을 통해 키트 아이템을 활성화하게 된 경우 처리
    • HandItemCoroutine 코루틴 함수를 통해 맨 손 위에 키트 아이템을 장착하는 처리를 해준다.
      • 맨손 위에 아이템 생성하기 전에 키트 아이템이라면 📜HandController의 currentKit 값에 이제 퀵슬롯을 통해 막 낀 키트 아이템을 할당해주어야 한다.
        • 그리고 이 currentKit을 가지고 📜HandController 에서 프리뷰를 만들고 건축을 할 수 있게 하고 하는 것이다.


📜WeaponManager

    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;
                if (HandController.currentKit != null)
                    theHandController.Cancel();
                if (QuickSlotController.go_HandItem != null)
                    Destroy(QuickSlotController.go_HandItem);
                break;
            case "AXE":
                AxeController.isActivate = false;
                break;
            case "PICKAXE":
                PickaxeController.isActivate = false;
                break;
        }
    }

현재 들고 있던 무기 currentWeaponType가 맨손일 때 case "HAND": 키트 아이템을 들고 있었다면 8if (HandController.currentKit != null)* theHandController.Cancel()을 통해 손에 들고 있던 키트 아이템을 없애고 currentKit도 null로 초기화해주어야 한다.

코루틴 함수 사용시 주의 사항

image

코루틴 사용시 병렬 처리된다는 것에 늘 주의하자. 코루틴 함수 사용시 뭔가 이상하게 돌아가면 병렬 처리 되는 바람에 다른게 바뀐게 아닐까 하고 생각해보자.

손에 들어야 하는 아이템이 키트 아이템인 경우 currentKit에 그 키트 아이템을 넣어주는 처리를 📜QuickController의 ChangeHand 함수 안에서 해줄 수도 있었다. 오른쪽 2 번 그림처럼! 그러나 이렇게 해주면 생기는 문제점은..

  • StartCoroutine(theWeaponManager.ChangeWeaponCoroutine(“HAND”, “맨손”));
    • 1️⃣ 맨손으로 무기 교체
  • StartCoroutine(HandItemCoroutine(_item));
    • 2️⃣ 맨손에 아이템 드는 처리

두 코루틴이 병렬적으로 동시에 실행되기 때문에 즉, 📜theWeaponManager의 ChangeWeaponCoroutine 함수 실행이 전부 끝나야지만 📜QuickController의 HandItemCoroutine 함수가 실행되는 것이 아니다. 두 함수는 코루틴이기 때문에 병렬적으로 각자 동시에 실행이 된다. 따라서 현재 currentKit이 null이 아닌 상태이기 때문에 📜theWeaponManager의 ChangeWeaponCoroutine 함수 안에서 또 호출된 CancelPreWeaponAction 함수를 통해 맨손인 상태에서 키트 아이템을 들게 되면 case "Hand"if (HandController.currentKit != null)에 걸리게 되기 때문에 theHandController.Cancel() 이렇게 currentKit이 다시 null이 되버린다. 이 상태에서 첫 번째 코루틴이 실행되는 동안 if 참이 되어 두 번째 코루틴이 실행됐고 그 사이에 첫 번째 코루틴에서 currentKit이 다시 null이 되버렸다면 📜HandController의 Update를 통해 마실 수 있게 되는 등등.. 갖가지 버그가 생길 수 있다. 따라서 이렇게 코루틴을 사용할 땐 병렬적으로 실행됨에 유의해야 한다.

  • 그래서 왼쪽 1번 그림처럼 currentKit 할당 과정을 HandItemCoroutine 에서 맨손 교체 과정이 끝나고 난 후에 진행할 수 있도록 하여 해결한다.
    • 맨손 교체가 완료되어 맨손의 isActivated가 ture가 될때까지 대기한 후 currentKit 할당 진행
          yield return new WaitUntil(() => HandController.isActivate);  // 맨손 교체의 마지막 과정
      
          if (_item.itemType == Item.ItemType.Kit)
              HandController.currentKit = _item;
      


📜Inventory

    private void PutSlot(Slot[] _slots, Item _item, int _count)
    {
        if (Item.ItemType.Equipment != _item.itemType && Item.ItemType.Kit != _item.itemType)
        {
            for (int i = 0; i < _slots.Length; i++)
            {
                if (_slots[i].item != null)
                {
                    if (_slots[i].item.itemName == _item.itemName)
                    {
                        slotNumber = i;
                        _slots[i].SetSlotCount(_count);
                        isNotPut = false;
                        return;
                    }
                }
            }
        }

        for (int i = 0; i < _slots.Length; i++)
        {
            if (_slots[i].item == null)
            {
                _slots[i].AddItem(_item, _count);
                isNotPut = false;
                return;
            }
        }

        isNotPut = true;
    }
        if (Item.ItemType.Equipment != _item.itemType && Item.ItemType.Kit != _item.itemType)

무기와 마찬가지로 키트 아이템 또한 인벤토리 혹은 퀵슬롯 안에서 한 슬롯 안에서 2 개 이상의 아이템을 두진 않을 것이다. 따라서 조건 추가. 이미 있는 슬롯에 증가시키는 첫 번째 for문은 돌지 않고 두 번째 for문만 돌도록.



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

맨 위로 이동하기

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

댓글 남기기