랜덤 아이템 스폰 & 인벤토리 습득
랜덤 아이템 스폰(1) : 임의의 시간이 지난 후 아이템 생성
간단한 방식으로, 코루틴의 WaitForSeconds를써서 일정 시간이 지나고 아이템이 생성되게 할 수 있다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSpawner : MonoBehaviour
{
public GameObject itemPrefab;
public float minSpawnTime;
public float maxSpawnTime;
// Start is called before the first frame update
void Start()
{
StartCoroutine(SpawnItem());
}
// Update is called once per frame
void Update()
{
}
IEnumerator SpawnItem()
{
GameObject item = Instantiate(itemPrefab, transform.position, Quaternion.identity);
float nextRandomtime = Random.Range(minSpawnTime, maxSpawnTime);
yield return new WaitForSeconds(nextRandomtime);
StartCoroutine(SpawnItem());
}
}
다만, 이렇게만 만들면, 계속 생성되어서 누적된다.
아이템이 생성되고, yield return new WaitForSeconds 함수가 돌아가서 시간초를 기다리고, 다음 아이템 생성(SapawnItem())을 실행하기 때문이다. 이 문제는델리게이트를 사용해서 해결할 수있다.
델리게이트 관련은 따로 포스팅을 할 예정.
콜백 함수 : 함수에 파라미터로 들어가는 함수, 순차적으로 사용하고 싶을 때 사용
우리가 만들고 싶은 것은, Spawn된 아이템이 파괴된 이후에 실행되는 함수를 만들고 싶은 것이다.
그렇기에 콜백 함수의 개념을 사용해서 , OnDestroy() 함수 안에 넣어준다면 원하는 결과를 얻을 수 있을 것이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSpawner : MonoBehaviour
{
public GameObject itemPrefab;
public GameObject Invetory;
public float minSpawnTime;
public float maxSpawnTime;
// Start is called before the first frame update
void Start()
{
SpawnItemCallback();
}
IEnumerator SpawnItem()
{
float nextRandomtime = Random.Range(minSpawnTime, maxSpawnTime);
yield return new WaitForSeconds(nextRandomtime);
SpawnItemCallback();
}
private void SpawnItemCallback()
{
GameObject item = Instantiate(itemPrefab, transform.position, Quaternion.identity);
//익명 함수, 델리게이트 하나
item.GetComponent<SpawnedItem>().OnDestroiedAction += () =>
{
Debug.Log("Item spawned");
StartCoroutine(SpawnItem());
};
}
}
SpawnItem은 랜덤한 시간이 지나면 SpawnItemCallback을 호출하는 함수이고
SpawnItemCallback은 아이템을 생성하고, 생성한 아이템이 파괴되면 로그와 함께 다시 SpawnItem을 호출하는 함수다.
구현한 방법은 다음과 같다. 아이템 프리팹에 SpawnedItem 스크립트를 넣어놨다.
이 스크립트에는 void 함수를 담을 Action인 OnDestroiedAction이 변수로 선언되어 있고
파괴되면 자동적으로 호출되는 OnDestroy 함수가 있다.
이 함수 안에 OnDestroyiedAction을 넣어주고, null이 아닐 때만 실행되도록 해준다.
Invoke는 ()와 같은 것인데 물음표 뒤에 괄호를 붙일 수 없으므로 써준 것이라고 생각하면 된다.
즉 파괴될 때, OnDestroiedAction이 null이 아니라면 실행한다는 뜻이다.
위와 연관지어 생각하면 item.GetComponent<SpawnedItem>().OnDestroiedAction += () =>은
생성된 아이템의 SpawnedItem 컴포넌트에 접근해서, OnDestroiedAction에 다음과 같은 익명 함수를 더해주겠다는 뜻이다. 이익명함수는 아이템이 스폰되었다를 로그로 생성하고, Spawn아이템 코루틴을 호출한다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnedItem : MonoBehaviour
{
public Action OnDestroiedAction;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private void OnDestroy()
{
OnDestroiedAction?.Invoke();
}
}
이와 조금 다른 방식으로 표현할 수도 있다. 지금 Spawn Item에 아이템을 스폰하는 기능이 들어가 있지 않으므로 다 위로 올려주고, 콜백함수에는 코루틴 호출만 할 수도 있다.
IEnumerator SpawnItem()
{
yield return new WaitForSeconds(Random.Range(1f, 3f));
var randomIndex = Random.Range(0, items.Count);
var randomItemData = items[randomIndex];
GameObject item = Instantiate(itemPrefab, transform.position, Quaternion.identity);
var spawnedItemComp = item.GetComponent<SpawnedItem>();
spawnedItemComp.SetItemData(randomItemData);
spawnedItemComp.spawnCallback = SpawnItemCallback;
}
void SpawnItemCallback()
{
StartCoroutine(SpawnItem());
}
}
public class SpawnedItem : MonoBehaviour
{
public Action spawnCallback;
public ItemData itemData;
public void SetItemData(ItemData _itemData)
{
itemData = _itemData;
GetComponent<SpriteRenderer>().sprite = _itemData.icon;
}
void OnDestroy()
{
Debug.Log("Destroy");
if (spawnCallback != null)
spawnCallback();
}
}
랜덤 아이템 스폰(2) : 여러가지 아이템 생성
위에서 이미 완성된 코드를 사용했지만 간단히 설명하자면 ,
public List<ItemData> items = new(); (뒤에를 new만 붙이고 생략 가능)으로 새로운 아이템 데이터 배열을 만든다.
그리고 생성 전에 , randomindex를 결정하고, 그 랜덤인덱스번째의 아이템을 randomItemData에 할당한다.
아이템 프리팹을 생성하는 것은 바꿀 필요가 없다, 생성한 아이템에 데이터를 덮어씌우면 되기 때문이다.
*개인적으로 할 때는, 아이템 프리팹을 여러가지 인스펙터 창에 할당하고, 아이템 프리팹을 생성하는 방식으로 했었는데 이는 좀 비 효율적인 방식인 것 같다.
이처럼 아이템을 생성하고 SetItemData(ItemData _itemData)로 데이터를 설정해준다. (데이터와 이미지)
아이템 습득
아이템 습득에 앞서, 코드를 좀 정리해 줄 것이다.
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Image = UnityEngine.UI.Image;
public class ItemGetter : MonoBehaviour
{
public Inventory inventory;
public RectTransform itemRectTransform;
public Canvas canvas;
public GameObject itemPrefab;
public GameObject getEfecctPrefab;
public Camera camera;
public GameObject Button;
public float shakeDuration = 3.0f;
public float shakeAmount = 100f;
public AnimationCurve curve = AnimationCurve.Linear(0,0,1,1);
IEnumerator GoingTBox(RectTransform itemTransform, RectTransform boxTransform)
{
float duration = 1.0f;
float t = 0.0f;
Vector3 itemBeginPOS = itemTransform.position;
while (1.0f >= t / duration)
{
Vector3 newPosition = Vector3.Lerp(itemBeginPOS,
boxTransform.position, curve.Evaluate(t / duration));
itemTransform.position = newPosition;
t += Time.deltaTime;
yield return null;
}
itemTransform.position = boxTransform.position;
inventory.AddItem(itemTransform.GetComponent<GettedObject>());
var particle = Instantiate(getEfecctPrefab, boxTransform.position, getEfecctPrefab.transform.rotation);
particle.transform.localScale = boxTransform.localScale;
StartCoroutine(ShakeAndBake());
Destroy(itemTransform.gameObject);
Destroy(particle, duration);
}
IEnumerator ShakeAndBake()
{
RectTransform buttonRect = Button.GetComponent<RectTransform>();
Vector2 originalPosition = buttonRect.anchoredPosition;
float elapsed = 0;
while (elapsed < shakeDuration)
{
elapsed += Time.deltaTime;
Vector2 randomOffset = Random.insideUnitCircle * shakeAmount;
buttonRect.anchoredPosition = originalPosition + randomOffset;
yield return null;
}
buttonRect.anchoredPosition = originalPosition;
}
private void OnTriggerEnter2D(Collider2D other)
{
Debug.Log(other);
var newObject = Instantiate(itemPrefab, other.transform.position, Quaternion.identity, canvas.transform);
newObject.GetComponent<GettedObject>().SetItemData(other.GetComponent<SpawnedItem>().itemData);
newObject.transform.position = other.transform.position;
var newScreenPosition = Camera.main.WorldToScreenPoint(newObject.transform.position);
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.GetComponent<RectTransform>(), newScreenPosition, camera, out localPoint);
newObject.transform.localPosition = localPoint;
StartCoroutine(GoingTBox(newObject.GetComponent<RectTransform>(), itemRectTransform));
Destroy(other.gameObject);
}
}
직관적이게 해석되지 않으므로, 코드를 읽고 해석하고 고치는데 어려움이 있다.
최선은 아니지만 정리해 주도록 하자.
private void OnTriggerEnter2D(Collider2D other)
{
Debug.Log(other);
var newObject = CreateGettedItem(other);
newObject.transform.position = GetLocalPosition(newObject);
StartCoroutine(GoingTBox(newObject.GetComponent<RectTransform>(), itemRectTransform));
Destroy(other.gameObject);
}
private GameObject CreateGettedItem(Collider2D other)
{
var newObject = Instantiate(itemPrefab, other.transform.position, Quaternion.identity, canvas.transform);
newObject.GetComponent<GettedObject>().SetItemData(other.GetComponent<SpawnedItem>().itemData);
newObject.transform.position = other.transform.position;
return newObject;
}
private Vector2 GetLocalPosition(GameObject newObject)
{
var newScreenPosition = Camera.main.WorldToScreenPoint(newObject.transform.position);
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.GetComponent<RectTransform>(), newScreenPosition, camera, out localPoint);
return localPoint;
}
}
이제, 인벤토리로 들어간 후에, 즉 GoingToBox 코루틴이 끝나고 난 후에 인벤토리에 추가해주는 작업을 할 것이다.
GoingToBox안의 코루틴에 AddItem이라고 있다. 시간이 지난 후 실행되는 함수로 등록한 인벤토리의 AddItem함수를 실행하는 것이고 Inventory의 Add함수는 다음과 같다.
//전역변수
[SerializeField]GridLayoutGroup gridLayoutGroup;
private ItemButton[] buttons;
public void AddItem(GettedObject item)
{
for (var i = 0; i < buttons.Length; i++)
{
if (buttons[i].ItemInfo == null)
{
buttons[i].ItemInfo = new ItemInfo{itemData = item.ItemData, amount =1 };
buttons[i].GetComponentInChildren<TMP_Text>().text = buttons[i].ItemInfo.amount.ToString();
break;
}
if (buttons[i].ItemInfo.itemData == item.ItemData)
{
buttons[i].ItemInfo.amount ++;
Debug.Log(buttons[i].ItemInfo.amount + " " + buttons[i].ItemInfo.itemData);
buttons[i].GetComponentInChildren<TMP_Text>().text = buttons[i].ItemInfo.amount.ToString();
break;
}
}
}
즉 item을 받으면, 버튼들(인벤토리 창)이 비어 있으면 새 아이템을 넣고 (이미지를 추가하고) amount를 1로 설정한다.
만일 이미 데이터가 있는 것이라면 amount를 1 추가해준다는 것이다.
밑의 TMP_Text를 통해 수량을 나타내주기도 한다.
몬스터 생성 & 이동 & 충돌
코드가 매우 간단하므로 코드 먼저 보자
우선, 캐릭터가 밀려날 것이므로, 캐릭터 안에 게임오브젝트를 만들고 HitCollsiion과 Collider를 넣어준다.
(콜라이더 범위 조정)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HitCollision : MonoBehaviour
{
[NonSerialized] public Rigidbody2D parentRigidbody;
// Start is called before the first frame update
void Start()
{
parentRigidbody = GetComponentInParent<Rigidbody2D>();
}
}
이제는 몬스터콜리젼이다. 몬스터는 만약에 맞춘다면 맞은 상대의컴포넌트에서 부모리지드바디를 나타내는 것을
rb에 할당하고, rb에 방향값을 노말라이즈한 것에 비례하는 충격을 가해 준다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HitCollisionMonster : MonoBehaviour
{
// Start is called before the first frame update
void OnTriggerEnter2D (Collider2D other)
{
Debug.Log("Hit");
Rigidbody2D rb = other.GetComponent<HitCollision>().parentRigidbody;
Vector3 backPosition = rb.transform.position - transform.position;
backPosition.Normalize();
backPosition.x *= 3;
rb.AddForce(backPosition * 800, ForceMode2D.Force);
}
}
주의해야 할 점은, 레이어 설정이랑 둘 중 하나는 isTrigger가 있어야 하고, Simulated도 있어야 한다.
https://docs.unity3d.com/kr/2022.3/Manual/rigidbody2D-simulated-property.html
Rigidbody 2D 프로퍼티:Simulated - Unity 매뉴얼
Simulated 프로퍼티는 사용 가능한 모든 바디 타입에 공통으로 적용됩니다.이 프로퍼티를 사용하여 리지드바디 2D와 연결된 콜라이더 2D 및 조인트 2D가 2D 물리 시뮬레이션과 상호작용하는 것을 시
docs.unity3d.com
몬스터를 움직이게 만들어 보자, 몬스터는 자동으로 일정 시간이 지나면 왔다 갔다를 반복할 것이다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Monster : MonoBehaviour
{
private SpriteRenderer spriteRenderer;
private Animator animator;
private Rigidbody2D rb2d;
public float speed = 5.0f;
public int switchCount = 0;
private int moveCount = 0;
public Vector2 direction;
private LayerMask playerlayerMask;
void Start()
{
spriteRenderer = GetComponent<SpriteRenderer>();
animator = GetComponent<Animator>();
rb2d = GetComponent<Rigidbody2D>();
playerlayerMask = LayerMask.NameToLayer("Player");
}
void FixedUpdate()
{
transform.position += new Vector3(direction.x * speed *Time.deltaTime, 0,0);
moveCount++;
if (moveCount >= switchCount)
{
direction *= -1;
spriteRenderer.flipX = direction.x < 0;
moveCount = 0;
}
}
}
switchCount 는 움직일 시간(범위)라고 생각하면 된다. moveCount가 한 단위씩 증가하다 Swithcount와 같게 되면, 방향이 바뀌고 (애니메이션 방향도 바꿔준다) moveCount가 초기화 된다.
* 정리했더니, GettedItem이 이상한 곳에 생성되는 문제가 발생해 고치느라 시간을 많이 썼다.
그래서 일단 티스토리 정리를 짧게 하고 고치고 나머지 개념 공부를 하려 한다.
수정:
newObject.transform.position = GetLocalPosition(newObject);
의 position을 localposition으로 바꿔주어야 한다.
'TIL' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임 개발 3기 30일차 (0) | 2024.12.20 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임 개발 3기 29일차 (2) | 2024.12.18 |
[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임 개발 3기 27일차 (1) | 2024.12.17 |
[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임개발 3기 25-26일차 (0) | 2024.12.15 |
[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임 개발 24일차 (0) | 2024.12.14 |