TIL

[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임개발 3기 28일차

Cadi 2024. 12. 17. 23:14

 

랜덤 아이템 스폰 & 인벤토리 습득

랜덤 아이템 스폰(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으로 바꿔주어야 한다.