오브젝트 풀링 기법은 유니티 게임 프로그래밍을 배울 때 가장 먼저 배우는 기법들 중 하나일 것이다.


일단 오브젝트 풀링이 왜 필요하고 정체가 무엇인지 예시를 들어 설명하겠다.


Strikers 1945 같은 전투기 슈팅 게임을 만들 때, 유저는 화면에 적이 있든 없든 총알 발사 버튼을 마구잡이로 누르게 된다.

이 때 총알 오브젝트를 Instantiate, Destroy 함수를 이용해서 생성과 소멸을 끊임없이 반복한다면 어떻게 될까?




1. 메모리 단편화(Memory fragmentation) 문제의 발생


바로 메모리 단편화(=메모리 파편화)라는 문제가 생기게 된다. 

메모리 단편화에는 외부 메모리 단편화내부 메모리 단편화가 있는데, 위의 상황에서는 외부 메모리 단편화가 발생한다..


외부 메모리 단편화란, 메모리에 빈 공간이 충분함에도 불구하고 빈 공간들이 따로 떨어져 있어서, 새로운 오브젝트를 생성하지 못하는 현상을 말한다.


아래 그림을 보면 더 쉽게 이해가 된다.


메모리 공간



위 그림의 상황 : 유니티에서 Instantiate 함수를 이용해서 오브젝트1 ~ 오브젝트4 에게 공간을 할당해줬다.



외부 메모리 단편화 상황



게임 진행을 하다보니 오브젝트2가 필요가 없어져서 Destroy함수를 이용해서 없애줬다.

그리고서 새로운 오브젝트5를 Instantiate함수로 생성하려고 했는데, 오브젝트5에게 할당해줄 알맞은 메모리 공간이 없다!

빈 공간 3개를 합치면 오브젝트5에게 할당해줄 연속된 공간이 충분할텐데, 빈 공간이 따로 떨어져있다보니 오브젝트5 생성에 문제가 생긴 것이다.


이렇게 Instantiate와 Destroy를 계속 사용하다보면 빈 공간이 우후죽순 생기게 되고, 메모리가 효율적으로 관리되지 못한다.

이런 상황을 외부 메모리 단편화라고 부른다.




2. 유니티는 메모리 단편화를 해결하지 못한다

보통의 C#에서는 가비지컬렉터(GC)가 메모리 단편화를 Compaction 해서 해결한다.
Compaction은 한글로 번역하면 압밀이라고 하는데, 압축, 밀착이라는 의미이다.

Compaction 진행 후. 이제 오브젝트 5가 들어갈 곳이 생겼다.


위의 그림과 같이, Compaction이 진행되면 오브젝트들의 메모리 공간을 좌로 밀착시키고, 빈 공간을 이어지게 해서 오브젝트5에게 메모리 공간을 할당해줄 수 있게 되었다.

하지만, 유니티의 GC는 Compaction기능을 지원하지 않는다.(출처 : https://docs.unity3d.com/kr/current/Manual/BestPracticeUnderstandingPerformanceInUnity4-1.html)


그렇기 때문에 Instantiate와 Destroy를 자주 해서 빈공간이 우후죽순 생기게 되면, 나중에 오브젝트 5처럼 거대한 객체가 만들어졌을 때 들어갈 공간이 없으니 메모리 점유를 늘리게 되고, 결과적으로는 비효율적인 메모리 운용이 생길 수 밖에 없다. 최악의 경우에는 유니티가 종료되거나 컴퓨터가 다운될 수도 있다.


그래서 Instantiate(혹은 new)와 Destroy의 사용을 최대한 자제하는게 유니티 메모리 최적화의 핵심이다.




3. 기초적인 오브젝트 풀링(Object Pooling)

그럼 아까 상황으로 돌아와서, Strikers 1945같은 전투기 슈팅게임을 만든다고 해보자. 버튼을 누를 때마다 총알이 나가는데, 이때마다 Instantiate를 호출해서 총알을 생성하고, 총알이 적에게 닿거나 맵 밖으로 나가면 Destroy로 총알을 삭제한다면? 무수한 빈 공간이 생길 것이다.

물론 아까 생성한 총알과 똑같은 용량의 총알이 또 생성된다면, 예전에 생성되었던 총알이 Destroy된 후 생겨난 빈 공간을 새로운 총알에게 할당해줄 수도 있을 것이다. 하지만 파워업 아이템을 먹어서 총알에 이펙트나 효과음이 추가되어서 용량이 커지게 된다면? 위의 그림들에서 오브젝트2가 삭제된 후 남겨진 빈 공간에 오브젝트5가 못 들어가는 것과 같은 상황이 발생할 것이다.

그래서 적당히 수십발의 총알을 미리 생성해놓고, 적에게 총알이 닿거나 맵 밖으로 총알이 나가면 SetActive(false)를 해서 안보이게 한 후, 나중에 총알을 재사용하는 방법을 오브젝트 풀링이라고 한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
//전투기 클래스
public class Flight : MonoBehaviour
{
    //총알 프리팹
    public Bullet prefab_bullet;
 
    //총알 Pool
    private List<Bullet> bulletPool = new List<Bullet>();
 
    //내가 생성할 총알 갯수
    private readonly int bulletMaxCount = 10;
 
    //현재 장전된 총알의 인덱스
    private int curBulletIndex = 0;
 
    void Start()
    {
        //총알 10개 미리 생성
        for(int i = 0; i < bulletMaxCount; ++i)
        {
            Bullet b = Instantiate<Bullet>(prefab_bullet);
            
            //총알 발사하기 전까지는 비활성화 해준다.
            b.gameObject.SetActive(false);
 
            bulletPool.Add(b);
        }
    }
 
    void Update()
    {
        FireBulet();
    }
 
    //총알 발사
    void FireBulet()
    {
        //마우스 좌클릭 할 때마다 총알 발사
        if (Input.GetMouseButtonDown(0))
        {
            //발사되어야할 순번의 총알이 이전에 발사한 후로 아직 날아가고 있는 중이라면, 발사를 못하게 한다.
            if(bulletPool[curBulletIndex].gameObject.activeSelf)
            {
                return;
            }
 
            //총알 초기 위치는 전투기랑 같게
            bulletPool[curBulletIndex].transform.position = this.transform.position;
 
            //총알 활성화 해주기
            bulletPool[curBulletIndex].gameObject.SetActive(true);
 
            //방금 9번째 총알을 발사했다면 다시 0번째 총알을 발사할 준비를 한다.
            if (curBulletIndex >= bulletMaxCount - 1)
            {
                curBulletIndex = 0;
            }
            else
            {
                curBulletIndex++;
            }
        }
    }
}
cs


총알의 이동함수, 적에게 닿거나 맵 밖으로 나갔을 때 SetActive(false) 해주는 부분은 굳이 구현하지는 않겠다.

총알을 여러개 만들어서 리스트에 담아주고, 그것들을 필요할 때 빼서 쓰는 것이 기본 골자이다.


안쓰는 총알용 List와 Active된 총알용 List를 따로 만들어줘서, 총알을 발사하면 이 리스트에서 저 리스트에 넣어주고 이런식으로 관리를 해주는 방법도 있다.

또 위의 예제에서는 bulletPool[curBulletIndex] 이런식으로 List를 직접 가져다 썼지만, 총알 종류가 여러개라서 바꿔 쏠 수 있는 게임이라면, List를 여러개 만들어준 후 Get 함수를 만들어서 현재 유저가 설정한 총기 종류에 따라서 다른 List의 총알을 리턴해주는 방식도 있을 것이다.





오브젝트 풀링은 위에서 말했던 것처럼 메모리 단편화 문제 때문에도 쓰이고, 

애당초 Instantiate와 Destroy 함수 자체가 비용이 큰 함수이기 때문에 런타임때 자주 써주는건 좋지 못하다.


오브젝트 풀링의 안 좋은 점이라면, 내가 총알을 발사할 필요가 없을 때에도 총알이 메모리에 존재한다는 것인데, 총알의 max 갯수는 테스트를 통해 적당한 수치를 찾는게 좋을 것이다.

+ Recent posts