Chapter 11-1. 포션 제조 : 연금테이블 제작(+ 코루틴 사용시 주의사항)
Categories: Unity Lesson 3
Tags: Unity Game Engine
인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
포션 제조 : 연금 테이블 제작
🚀 연금 테이블 만들기
- 연금 테이블 아이템을 포션 먹을 때처럼 손에 들고 있을 수 있다.
- 연금 테이블 아이템을 손에 들고 있으면 자동으로 건축 시스템처럼 프리뷰가 생성되며 크로스헤어를 따라다닌다.
- 이 상태에서 좌클 누르면 연금 테이블 건축 가능
- 연금 테이블 아이템을 손에 들고 있으면 자동으로 건축 시스템처럼 프리뷰가 생성되며 크로스헤어를 따라다닌다.
✈ 실제 건축물
- 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; // 무기 유형
}
연금 테이블의 Item
ScriptableObject 에셋의 kitPrefab
과 kitPreviewPrefab
에 각각 위에서 만든 연금테이블 프리팹과 연금테이블 프리뷰 프리팹 할당.
🚀 맨손으로 연금테이블 들고 이 상태에서 건축 가능한 상태로 만들기
📜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
라는 레이어 마스크 멤버 변수를 만들어 주었다.
- 플레이어에서 Raycast를 쏴서
- 프리뷰의 위치를 매프레임마다 업데이트한다.
- Build
- 좌클 입력이 들어오면 연금 테이블 같은 키트 종류 아이템을 건축한다.
- 퀵슬롯 아이템 개수 차감
- 연금 테이블 생성
- 프리뷰는 삭제 후
currentKit
와isPreview
도 초기화
- 좌클 입력이 들어오면 연금 테이블 같은 키트 종류 아이템을 건축한다.
- Cancel
- 📜WeaponManager를 통해 무기를 바꿀 때 만약 맨손으로 연금테이블 들고 있는 상태에서 다른 무기로 교체했다면 프리뷰도 없애고
currentKit
와isPreview
도 초기화 해야 됨. 이때 필요하기 때문에 public 함수로 만들어 줌.
- 📜WeaponManager를 통해 무기를 바꿀 때 만약 맨손으로 연금테이블 들고 있는 상태에서 다른 무기로 교체했다면 프리뷰도 없애고
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 에서 프리뷰를 만들고 건축을 할 수 있게 하고 하는 것이다.
- 그리고 이
- 맨손 위에 아이템 생성하기 전에 키트 아이템이라면 📜HandController의
- HandItemCoroutine 코루틴 함수를 통해 맨 손 위에 키트 아이템을 장착하는 처리를 해준다.
📜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로 초기화해주어야 한다.
코루틴 함수 사용시 주의 사항
코루틴 사용시 병렬 처리된다는 것에 늘 주의하자. 코루틴 함수 사용시 뭔가 이상하게 돌아가면 병렬 처리 되는 바람에 다른게 바뀐게 아닐까 하고 생각해보자.
손에 들어야 하는 아이템이 키트 아이템인 경우 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문만 돌도록.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
Leave a comment