[SKKU DT] 43일차 -유니티 오브젝트 풀(Object Pool)

2024. 1. 2. 21:24SKKU DT

728x90
반응형

3D -공간 디자인, 라이팅, 3D 오브젝트

UI -Autolayout, TableView

서버 -JSON

배포 -Window-인스톨러/Web-최적화/Mobile-최적화,배포

데이터처리 C# -자료구조, 알고리즘

협업 -소스코드 관리, 작업 관리

 

 


 

 

오브젝트 풀(Object Pool)

박스가 컨베이어 벨트를 타고 가서 목적지에서 없어지는 프로젝트가 있다고 가정한다면, 만들어지는 박스는 시간이 갈수록 많아진다. 박스는 하나의 객체. 박스가 많아지면 메모리가 낭비되는 단점이 있다. 박스가 목적지에 도달하면 없어지는 것 처럼 보이게 했다가 다시 처음에 만들어지게 함으로써 재활용 하는 방법. Instantiate를 획기적으로 줄일 수 있다.

 

 


 

 

오브젝트 풀 예제

3D 프로젝트를 하나 만들고 Sphere 생성, Bullet 스크립트 생성, Sphere에 Bullet 스크립트 컴포넌트 추가

 

 

총알에 Rigid Body를 추가하고 AddForce로 힘을 줄 예정. Shoot() 함수를 사용해서 마우스 왼쪽 클릭을 할 때마다 동작하게 만든다. 오브젝트 풀을 사용하며, 별도의 다른 객체(GameManager)로 총알을 관리한다. 총알은 프리팹화 시킨다.

GameManager 스크립트도 생성.

Bullet은 프리팹화 시켰으므로 Hierarchy에서 삭제한다.

 

 

Bullet에 Rigidbody 추가

 

 

총알에 대한 정보를 넣는다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))] //Rigidbody가 필요하다. 사전에 rigidbody가 필요하다고 체크하는 것임.
public class Bullet : MonoBehaviour
{
    //총알이 나가야 하는 힘
    float force = 1000f;
    //Rigidbody를 담을 변수
    Rigidbody rb;

    private void Awake()
    {
        //Start에 할당하면 타이밍이 맞지 않기 때문에 Awake 함수에서 할당
        rb = GetComponent<Rigidbody>();
    }

    public void Shoot() //GameManager에서 호출할 수 있게 하려면 public이어야 한다.
    {
         rb.AddForce(transform.forward * force); //forward 방향인 z축 방향으로 힘을 준다.
    }
}

 

 

GameManager에는 총알을 생성하고 발사한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    [SerializeField] private Bullet bulletPrefab; //변수 자체는 private이기 때문에 다른 객체에서는 접근할 수 없다.
    //public GameObject bullet; //변수 자체가 public이라 다른 객체에서도 접근할 수 있다.
    void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            Bullet bullet = Instantiate(bulletPrefab, Vector3.zero, Quaternion.identity); //(0, 0, 0)자리 초기 회전값 대입
            bullet.Shoot();
        }
    }
}

 

 

이처럼 많이 생기는 오브젝트에 대한 관리를 Object Pool로 할 수 있다.

BulletPool 스크립트 생성. Bullet을 리스트 형태로 만들어야 한다. Pool 안에 쓸모없는 총알을 담는 함수(Add()), Pool로 부터 총알을 꺼내는 함수(GetBullet())가 필요하다. GetBullet 했을 때, Pool안에 쓸만한 총알이 있으면 반환하고 없다면 Instantiate한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletPool : MonoBehaviour
{
    //배열 선언이었다면,
    //Bullet[] bullets;
    //리스트 선언. 개수는 미리 정하지 않아도 된다.
    List<Bullet> bullets = new List<Bullet>(); //Bullet 타입의 리스트 생성

    //BulletPool에서 사용할 수 있는 총알을 반환하는 함수
    public Bullet GetBullet() //Bullet을 반환해야 하기 때문에 반환값이 Bullet
    {
        if(bullets.Count > 0) //리스트에 총알이 남아있다면
        {
            Bullet bullet = bullets[0];
            bullets.RemoveAt(0); //리스트에서 0번째 총알을 지우고

            bullet.gameObject.SetActive(true);

            return bullet; //총알을 반환한다.
        }
        return null;
    }
    //더 이상 사용되지 않는 총알을 Bullet Pool에 추가하는 함수
    public void AddBullet(Bullet bullet)
    {
        bullets.Add(bullet); //리스트에 Add되는 함수
    }
}

 

 

BulletPool 스크립트가 생성됨에 따라 GameManager도 GetBullet을 받아와서 bullet이 비어있는지 확인하여 비어있지 않다면 바로 Shoot 함수를, 비어있다면 생성해서 Shoot 함수를 호출한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    [SerializeField] private Bullet bulletPrefab; //변수 자체는 private이기 때문에 다른 객체에서는 접근할 수 없다.
    [SerializeField] private BulletPool bulletPool;
    //public GameObject bullet; //변수 자체가 public이라 다른 객체에서도 접근할 수 있다.
    void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            Bullet bullet = bulletPool.GetBullet();
            if (bullet != null)
            {
                bullet.Shoot();
            }
            else
            {
                Bullet newBullet = Instantiate(bulletPrefab, Vector3.zero, Quaternion.identity); //(0, 0, 0)자리 초기 회전값 대입
                newBullet.Shoot();
            }
        }
    }
}

 

 

Bullet 스크립트에서 총알이 다른 트리거에 닿으면 비활성화 되거나 3초의 시간이 지나면 비활성화 되게끔 수정.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))] //Rigidbody가 필요하다. 사전에 rigidbody가 필요하다고 체크하는 것임.
public class Bullet : MonoBehaviour
{
    public BulletPool bulletPool;

    //총알이 나가야 하는 힘
    float force = 1000f;
    //Rigidbody를 담을 변수
    Rigidbody rb;
    //시간 부여
    float killTime = 3f;
    float time = 0;

    private void Awake()
    {
        //Start에 할당하면 타이밍이 맞지 않기 때문에 Awake 함수에서 할당
        rb = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        time += Time.deltaTime;
        if(time > killTime)
        {
            this.gameObject.SetActive(false);
        }
    }

    public void Shoot() //GameManager에서 호출할 수 있게 하려면 public이어야 한다.
    {
         rb.AddForce(transform.forward * force); //forward 방향인 z축 방향으로 힘을 준다.
    }

    private void OnTriggerEnter(Collider other) //사물에 총알이 닿으면,
    {
        this.gameObject.SetActive(false); //오브젝트 비활성화
    }

    private void OnEnable() //SetActive(true)이면 호출
    {
        //Enable 되는 순간 총알의 transform 초기화
        this.transform.position = Vector3.zero;
    }

    private void OnDisable() //SetActive(false)이면 호출
    {
        this.time = 0;
        rb.velocity = Vector3.zero; //가던 총알을 멈춘다
        bulletPool.AddBullet(this); //BullPool에 담는다
    }
}

 

 

다시 GameManager로 돌아와서 newBullet.bulletPool에 bulletPool을 할당한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    [SerializeField] private Bullet bulletPrefab; //변수 자체는 private이기 때문에 다른 객체에서는 접근할 수 없다.
    [SerializeField] private BulletPool bulletPool;
    //public GameObject bullet; //변수 자체가 public이라 다른 객체에서도 접근할 수 있다.
    void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            Bullet bullet = bulletPool.GetBullet();
            if (bullet != null)
            {
                bullet.Shoot();
            }
            else
            {
                Bullet newBullet = Instantiate(bulletPrefab, Vector3.zero, Quaternion.identity); //(0, 0, 0)자리 초기 회전값 대입
                newBullet.bulletPool = bulletPool; //만들어진 이후에 bullPool에게 할당
                newBullet.Shoot();
            }
        }
    }
}

 

 

빈 게임 오브젝트 생성 후 BulletPool 스크립트 추가

 

 

만든 BulletPool을 GameManager에 추가

 

 

일정 개수를 정하진 않았지만 일정 개수 이상의 총알이 더이상 생성되진 않는다.

 

 


 

 

Queue(큐), Stack(스택)

List: 뒤에 데이터가 달라붙는 형식.

Queue: 값이 추가되면 먼저 들어간 값을 반환한다. 원통형으로 비유하면 뒤에서 밀어서 앞이 나오는 형상. 서버에서 먼저 요청된 것을 먼저 처리하는 데에 쓸 수 있다.

Stack: 값이 쌓이는 형식. 나중에 들어간 값부터 나온다.

 

Queue(큐)를 사용하면 다음과 같이 선언할 수 있다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletPool : MonoBehaviour
{
    //배열 선언이었다면,
    //Bullet[] bullets;
    //리스트 선언. 개수는 미리 정하지 않아도 된다.
    //List<Bullet> bullets = new List<Bullet>(); //Bullet 타입의 리스트 생성
    Queue<Bullet> bulletsQueue = new Queue<Bullet>(); //큐 선언
    //Stack<Bullet> bulletsStack = new Stack<Bullet>(); //스택 선언

    //BulletPool에서 사용할 수 있는 총알을 반환하는 함수
    public Bullet GetBullet() //Bullet을 반환해야 하기 때문에 반환값이 Bullet
    {
        if(bulletsQueue.Count > 0) //리스트에 총알이 남아있다면
        {
            Bullet bullet = bulletsQueue.Dequeue();
            bullet.gameObject.SetActive(true);

            return bullet; //총알을 반환한다.
        }
        return null;
    }
    //더 이상 사용되지 않는 총알을 Bullet Pool에 추가하는 함수
    public void AddBullet(Bullet bullet)
    {
        bulletsQueue.Enqueue(bullet); //Queue에 값 추가. Add와 같은 역할
    }
}

Queue<Bullet>으로 선언하고, bulletsQueue.Enqueue(bullet);으로 Add와 같은 역할을 할 수 있다.

bulletsQueue.Dequeue();로 큐에서 삭제할 수 있다.

맨 먼저 들어간 총알부터 큐를 빠져나오는 상황.(First In First Out)

 

 


 

 

Stack(스택)

새로운 씬을 만든다. 320x480 해상도 설정

 

 

캔버스에 패널 추가

 

 

Open 버튼을 눌러서 나올 패널을 추가한다. Open을 계속 누르면 무한히 패널이 생성된다.

 

 

Panel을 프리팹화 한다.

 

 

빈 오브젝트로 PanelManager 생성, 같은 이름으로 스크립트 생성

 

 

다음 세 가지 기능을 넣어본다.

  • Open 버튼 클릭시 새 Panel 프리팹 생성
  • 새 Panel의 Text 번호 표시 (+1)
  • 새 Panel의 배경 색상을 다르게 표시

 

새 Panel을 "Canvas"안에서 Instantiate 하려면 아래와 같은 코드를 사용한다.

public void NewPanel()
{
    Instantiate(panel, new Vector2(160, 240), Quaternion.identity, GameObject.Find("Canvas").transform); //캔버스 밑에서 Panel 프리팹 생성
}

 

 

Panel 프리팹에서 Onclick에 함수를 적용하는 것이 어렵다...

 

 


 

 

Panel 스크립트 생성, 프리팹에 할당

 

 

PanelManager가 Panel에게 몇 번 창인지 알려주는 역할을 한다.

 

PanelManager의 스크립트는 아래와 같다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PanelManager : MonoBehaviour
{
    [SerializeField] GameObject panelPrefab;
    [SerializeField] Transform parent;
    public void Open()
    {
        GameObject panelObject = Instantiate(panelPrefab, parent);
    }

    public void Close()
    {

    }
}

 

 

Panel 프리팹에 들어가는 스크립트는 아래와 같다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class Panel : MonoBehaviour
{
    //패널 가운데 텍스트 접근
    [SerializeField] private TMP_Text indexText;

    //PanelManager 선언
    public PanelManager manager; //프리팹이라 SerializeField를 사용할 수 없다.

    //프로퍼티를 사용하면 아래와 같다.
    /*
    int index; //저장을 위한 변수. 아예 지우고 아래의 index를 Index로 대문자로 쓰기도 한다.
    public int Index //값을 저장하기 위해서 접근할 수 있는 속성을 가진 프로퍼티
    {
        get
        {
            return Index;
        }
        set
        {
            indexText.text = value.ToString();
            Index = value;
        }
    }
    */
    
    public void SetPanelIndex(int index) //세터 메서드: 값을 외부에서 받아서 전달하기 위한 메서드
    {
        indexText.text = index.ToString(); //매개변수 index를 문자열로 변환
    }
    
    // Open 버튼 클릭시 호출되는 메서드
    public void Open()
    {
        manager.Open(); //manager 스크립트에게 위임
    }
    //Previous 버튼 클릭시 호출되는 메서드
    public void Close()
    {
        manager.Close(); //manager 스크립트에게 위임
    }
}

 

 

PanelManager에 컴포넌트들을 추가하면,

 

 

처음 Open 버튼을 누르면 새 창이 뜬다.

 

 

PanelManager에서, panel을 가져오고, SetPanelIndex를 추가하고 Text를 할당하면 숫자도 바뀐다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PanelManager : MonoBehaviour
{
    [SerializeField] GameObject panelPrefab;
    [SerializeField] Transform parent;
    public void Open()
    {
        GameObject panelObject = Instantiate(panelPrefab, parent);
        Panel panel = panelObject.GetComponent<Panel>();
        panel.SetPanelIndex(1);
    }

    public void Close()
    {

    }
}

 

 

프로퍼티를 이용해서 다시 스크립트를 수정하면, PanelManager는 다음과 같고

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PanelManager : MonoBehaviour
{
    [SerializeField] GameObject panelPrefab;
    [SerializeField] Transform parent;
    public void Open()
    {
        GameObject panelObject = Instantiate(panelPrefab, parent);
        Panel panel = panelObject.GetComponent<Panel>();

        panel.IndexText = 1;
    }

    public void Close()
    {

    }
}

 

Panel 스크립트는 다음과 같다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

public class Panel : MonoBehaviour
{
    //패널 가운데 텍스트 접근
    [SerializeField] private TMP_Text indexText;

    //PanelManager 선언
    public PanelManager manager; //프리팹이라 SerializeField를 사용할 수 없다.

    /*
    public int IndexText { get; set; } //프로퍼티의 축약형. 변수처럼 쓸수있고 함수도 사용할 수 있다.
    public int IndexText2; //변수. 유효한 값을 판단할 수 없다. 프로퍼티에서는 set 중괄호 안에 if, else문을 넣을 수 있다.
    */

    //프로퍼티를 사용하면 아래와 같다.
    public int index; //저장을 위한 변수. 아예 지우고 아래의 index를 Index로 대문자로 쓰기도 한다.
    public int IndexText //값을 저장하기 위해서 접근할 수 있는 속성을 가진 프로퍼티
    {
        get
        {
            return index;
        }
        set
        {
            index = value;
            indexText.text = index.ToString();
        }
    }
    /*
    프로퍼티를 쓰기 때문에 SetPanelIndex는 쓰지 않는다.
    public void SetPanelIndex(int index) //세터 메서드: 값을 외부에서 받아서 전달하기 위한 메서드
    {
        indexText.text = index.ToString(); //매개변수 index를 문자열로 변환
    }
    */
    
    // Open 버튼 클릭시 호출되는 메서드
    public void Open()
    {
        manager.Open(); //manager 스크립트에게 위임
    }
    //Previous 버튼 클릭시 호출되는 메서드
    public void Close()
    {
        manager.Close(); //manager 스크립트에게 위임
    }
}

 

 

Panel 프리팹에서, 각 버튼에는 Panel 자기 자신을 넣고 함수를 매핑한다.

 

Hierarchy 창에 Panel이 계속 생성되는 것을 볼 수 있다.

 

 

패널 색상 변경하기 스크립트 수정

Panel 스크립

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;

public class Panel : MonoBehaviour
{
    //패널 가운데 텍스트 접근
    [SerializeField] private TMP_Text indexText;
    //패널 색상 변경 위해 색상 접근
    [SerializeField] private Image image;

    //PanelManager 선언
    public PanelManager manager; //프리팹이라 SerializeField를 사용할 수 없다.

    /*
    public int IndexText { get; set; } //프로퍼티의 축약형. 변수처럼 쓸수있고 함수도 사용할 수 있다.
    public int IndexText2; //변수. 유효한 값을 판단할 수 없다. 프로퍼티에서는 set 중괄호 안에 if, else문을 넣을 수 있다.
    */

    //프로퍼티를 사용하면 아래와 같다.
    public int index; //저장을 위한 변수. 아예 지우고 아래의 index를 Index로 대문자로 쓰기도 한다.
    public int IndexText //값을 저장하기 위해서 접근할 수 있는 속성을 가진 프로퍼티
    {
        get
        {
            return index;
        }
        set
        {
            index = value;
            indexText.text = index.ToString();
        }
    }
    /*
    프로퍼티를 쓰기 때문에 SetPanelIndex는 쓰지 않는다.
    public void SetPanelIndex(int index) //세터 메서드: 값을 외부에서 받아서 전달하기 위한 메서드
    {
        indexText.text = index.ToString(); //매개변수 index를 문자열로 변환
    }
    */
    
    // Open 버튼 클릭시 호출되는 메서드
    public void Open()
    {
        manager.Open(); //manager 스크립트에게 위임
    }
    //Previous 버튼 클릭시 호출되는 메서드
    public void Close()
    {
        manager.Close(); //manager 스크립트에게 위임
    }

    public void ChangeColor(Color color) //컬러 변경 메서드 생성
    {
        image.color = color;

    }
}

 

PanelManager 스크립트

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PanelManager : MonoBehaviour
{
    [SerializeField] GameObject panelPrefab;
    [SerializeField] Transform parent;

    //흰색 뺀 색상을 배열에 정의
    Color[] colors = new Color[4] { Color.black, Color.blue, Color.yellow, Color.green };
    public void Open()
    {
        GameObject panelObject = Instantiate(panelPrefab, parent);
        Panel panel = panelObject.GetComponent<Panel>();
        panel.IndexText = 1;
        panel.manager = this;
        panel.ChangeColor(colors[Random.Range(0,3)]);
    }

    public void Close()
    {

    }
}
728x90
반응형