[Unity][2D-Game] Undead Survivor (6)

suhan0304·2023년 11월 4일
0
post-thumbnail

preview Link : https://velog.io/@suhan0304/Unity2D-Game-Undead-Survivor-5
gitHub Link : https://github.com/suhan0304/Undead-Survivor


Review

  • 몬스터 오브젝트를 만들어줘서 적절한 컴포넌트를 추가했다.
  • 플레이어를 향해 움직이도록 Enemy 스크립트를 작성해 플레이어 추적 로직을 작성해주었다.
  • 몬스터가 플레이어와 멀리 떨어지면 가까이 재배치되도록 기존의 Reposition을 수정해주었다.

강의영상(6) - 오브젝트 풀링으로 소환하기


개발

오브젝트 프리팹 만들기

기존의 몬스터 오브젝트의 이름을 Enemy A, Enemy B로 바꾼 후에 Assets > Undead Survivor 에 Prefabs 폴더를 만들어서 해당 폴더로 드래그드랍을 해서 프리팹을 만든다.

  • 프리팹과 연결된 오브젝트는 아이콘과 색상이 파란색으로 변한다.

프리팹이란? 미리 만들어진 Object를 재활용 가능한 형태로 만들어 두는 것을 의미한다. 같은 오브젝트를 여러개 만들어 사용할 때 프리팹으로 만들어놓고 해당 프리팹을 복사해서 사용하는 방식으로 주로 사용한다.

  • 프리팹을 수정하면 해당 프리팹을 복사해서 만든 모든 오브젝트에 수정된 내용이 적용된다.

오브젝트 풀 만들기

프리팹을 instantiate(복사) + destroy(파괴) 해서 사용할 수도 있으나 해당 함수를 자주 사용하면 메모리 문제가 발생하게 된다. 이는 게임 프레임에 영향을 주기 때문에 오브젝트 풀링 기법을 사용해 해당 문제를 해결할 수 있다.

오브젝트 풀링 기법이란? 오브젝트의 Pool(웅덩이0를 만들어두고 그 웅덩이 안에서 필요할 때 마다 객체를 꺼내서 사용하는 것을 오브젝트 풀링이라고 한다.

  • 계속해서 생성하고 파괴를 반복하는 것은 메모리 해제 과정에서 가비지 컬렉터가 발생하며 성능(특히 CPU)에 큰 부담을 준다.
  • 미리 오브젝트를 생성해놓고 필요할 때만 꺼내서 사용하고 다 사용하고 나면 반납하는 오브젝트 풀링 기법은 이러한 생성, 파괴로 인한 성능 저하를 방지할 수 있다.

프리팹을 저장할 Pool 오브젝트와 PoolManager 스크립트파일을 생성한다.

public class PoolManager : MonoBehaviour
{
    // .. 프리팹을 보관할 변수
    public GameObject[] prefabs; //프리팹이 저장될 배열 변수

    // .. 풀 담당을 하는 리스트
    List<GameObject>[] pools; //오브젝트 풀들을 저장할 배열 변수

    void Awake()
    {
        // 각각의 프리팹 길이 만큼의 리스트를 생성
        pools = new List<GameObject>[prefabs.Length]; //풀을 담는 리스트 초기화
        // 새로 생성한 리스트도 초기화가 필요함
        for (int i=0;i<pools.Length;i++)
        {
            //모든 오브젝트 풀 리스트를 초기화
            pools[i] = new List<GameObject>(); //풀을 초기화
        }
    }
}

위와 같은 코드로 프리팹들이 저장될 prefabs 리스트, 각 프리팹 별로 만들어줄 pool들이 저장될 pools 리스트를 선언해주고 prefabs은 직접 prefab을 인스펙터를 통해 넣어주고 pools는 Awake 함수 안에 초기화하는 과정을 추가한다.

Prfabs를 PoolManager에서 생성해놓은 Prefabs 배열에 드래그 해서 이름에 드랍하면 배열에 Prefabs를 추가할 수 있다.


풀링 함수 작성

어디서나 오브젝트를 요청해서 사용할 수 있게 PoolManager안에 Get이라는 함수를 작성했다.

//어디서나 사용할 수 있도록 public 선언
public GameObject Get(int index) //요청한 게임 오브젝트를 반환해주기위해 GameObject형으로 선언
{
	GameObject select = null;//지역변수는 초기화 필요

    // ... 선택한 풀의 놀고 (비활성화 된) 있는 게임 오브젝트 접근
    foreach (GameObject item in pools[index]) //pools[index]에 있는 pool 리스트에서 오브젝트를 꺼내면서 검사
    {
    	// ... 발견하면 select 변수에 할당
        if (!item.activeSelf) //오브젝트가 비활성화 되어있으면 select에 할당 
        {
        	select = item; //할당을 진행
            select.SetActive(true); //이제 사용할 것이므로 활성화
            break;
        }
	}

	// ... 만약 오브젝트 풀의 모든 오브젝트가 사용중이면?
	if (!select) //select가 null이면? 비활성화 오브젝트를 찾지 못했다.
	{
		// ... 추가적인 오브젝트를 생성 후 select 변수에 할당
		select = Instantiate(prefabs[index], transform); 
		//instantiate 함수를 이용해 프리팹을 복사해서 생성
		//transform으로 부모를 맞춰주지 않으면 오브젝트 생성이 자식 오브젝트 위치가 아니라 최외곽에서 생성된다.
		pools[index].Add(select); //새로 생성한 오브젝트도 풀에 추가해줘서 앞으로 재활용이 가능하도록 한다.
	}

	return select;
}

만약 비활성화 된 오브젝트가 없으면 Instantiate 함수를 이용햐 프리팹을 따로 생성해준다. 이 때 부모는 PoolManager 자신이므로 transform으로 설정해준다. 이 때 새로 복사해서 생성한 오브젝트를 pool에 추가를 해서 다 쓰고 반납 받으면 또 다시 재활용 할 수 있도록 설정해준다.

이 때 Get 함수를 모든 스크립트에서 자유롭게 호출할 수 있도록 게임 매니저에 다음과 같이 코드를 작성해 풀 매니저를 추가해주고 인스펙터 창으로 만들어놓은 PoolManager 오브젝트를 드래그드랍해서 변수 초기화를 해준다.


풀링 사용해보기

위 Get을 해서 몬스터를 계속 생성할 Spawner 라는 오브젝트를 Player의 자식 오브젝트로 추가해준다. 해당 오브젝트에 추가해줄 Spawner 스크립트를 다음과 같이 작성했다.

  • 임의로 스페이스바를 입력해서 몹을 Pool에서 Get으로 생성해본다.
public class Spawner : MonoBehaviour
{
    void Update()
    {
        if (Input.GetButtonDown("Jump"))
        {
            GameManager.Instance.pool.Get(1);
        }   
    }
}

이 때 에셋과 씬 무대는 별도로 분리된 공간이기 때문에 에셋에 있는 프리팹을 씬으로 Instantiate로 복사를 해와도 Target 오브젝트 설정이 따로 되지는 않는다. 따라서 오브젝트가 에셋에 있는 프리팹의 인스펙터로 들어갈 수가 없다.

따라서 몬스터가 플레이어를 스스로 찾도록 설정한다. 몬스터가 Enemy 스크립트에 OnEnable 함수를 사용해서 타겟을 스스로 찾도록 로직을 구현했다.

void OnEnable()
{
	target = GameManager.Instance.player.GetComponent<Rigidbody2D>();
}

OnEnable : 스크립트가 활성화 될 때, 호출되는 이벤트 함수


주변에 생성하기

Spawner의 자식 오브젝트로 포인트를 카메라의 바깥 쪽에 많이 생성해준다. 해당 포인트에서 몬스터가 스폰되도록 로직을 구현할 것이다.

Spawer 스크립트에서 자식 오브젝트(포인트)의 트랜스폼을 담을 변수 배열을

인스펙터의 아이콘을 클릭하여 다른 아이콘으로 변결할 수 있다.

public class Spawner : MonoBehaviour
{
    public Transform[] spawnPoint;

    float timer;

    void Awake()
    {
        // 배열로 여러개를 가져올 것이기 때문에 Component's'를 써야함
        spawnPoint = GetComponentsInChildren<Transform>();
    }

    void Update()
    {
        //DeltaTime : 한 프레임에 걸린 시간
        timer += Time.deltaTime;

        //1초마다 Spawn 실행
        if (timer > 1f)
        {
            timer = 0;
            Spawn();
        }
    }

    void Spawn()
    {
        //0번 또는 1번 몬스터를 랜덤으로 지정
        GameObject enemy = GameManager.Instance.pool.Get(Random.Range(0,2));

        //랜덤 포인트에서 생성되도록 설정
        enemy.transform.position = spawnPoint[Random.Range(1, spawnPoint.Length)].position;
        //1번부터 하는 이유 -> GetComponentsInChildren에는 자기 자신도 포함이라 0번은 자기 자신 transform이 들어가있음
    }
}

이 때 GetComponentsInChildren에서 중요한점은 Components와 Component의 차이를 기억해야한다.

  • 현재 스폰 포인트가 여러개이기 때문에 꼭 Components를 써서 모든 자식 스폰 포인트 오브젝트를 가져오도록 해야한다.

  • GetComponentsInChildren에는 자기 자신도 포함되기 때문에 반복문을 시작할 때 0번을 건너뛰고 1번부터 랜덤되는 스폰 포인트의 위치에 스폰되도록 해야한다.


결과물

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글