포스트

섹션10. Object Pooling

위 글은 인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 남긴 필기입니다.

Object Pooling이란

  • 오브젝트를 특정한 목표를 위해서 생성하고, 그 쓸모가 다하면 삭제하는 방식으로 수많은 오브젝트를 관리한다면, CPU는 각각의 생성과 삭제를 위해 그 각각의 시간동안 생성을 위한 연산과, 삭제를 위한 연산을 해야 할 것이다.
    • 당연한 말이지만 CPU는 오브젝트를 “생성”할 때 메모리의 특정 영역을 할당할 것이고, 반대로 오브젝트를 “삭제”할 때에는 메모리의 해당 영역을 다시 반환할 것이다.
  • 이러한 방식으로 관리하는 오브젝트가 많아질수록, CPU는 생성과 삭제에 더 많은 시간을 할당할 수 밖에 없으며, 이는 게임 자체의 성능에 지대한 영향을 줄 수 밖에 없다.
  • CPU는 오로지 사용자가 게임을 진행하는데 있어 필요한 연산만을 위주로 동작하는 것이 바람직하기 때문.
    • 그렇지 못하면, 게임 자체에 렉이나 정도가 심하다면 멈추는 상황이 발생할 수 있고, 이는 사용자 경험을 큰 폭으로 저해한다.

PoolManager & Poolable

  • Object Pooling을 위해 이를 관리할 PoolManager.cs 와 오브젝트의 Pooling 여부를 구별하기 위한 Poolable.cs을 만듦.
    • Poolable.cs 는 단순히 메모리 풀링을 적용할지 여부를 표시하기 위함.
1
2
3
4
5
public class Poolable : MonoBehaviour
{
    // 현재 풀링이 된 상태인지.
    public bool isUsing;
}
  • PoolManager.cs는 내부에서 GameObjectPool들을 이름Pool이 각각 Dictionary<string, Pool> 객체를 만들어 관리한다.
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
public class PoolManager
{
    #region Pool
    class Pool
    {
        public GameObject Original { get; private set; }
        public Transform Root { get; set; }

        private Stack<Poolable> _poolStack = new Stack<Poolable>();

        /// <summary>
        /// Pool 초기화
        /// </summary>
        public void Init(GameObject original, int count = 5)
        {
            Original = original;
            Root = new GameObject().transform;
            Root.name = $"{original.name}_Root";

            for (int i = 0; i < count; i++)
                Push(Create());
        }

        Poolable Create()
        {
            GameObject go = Object.Instantiate<GameObject>(Original);
            go.name = Original.name;

            return go.GetOrAddComponent<Poolable>();
        }

        public void Push(Poolable poolable)
        {
            if (poolable == null) return;

            poolable.transform.parent = Root;
            poolable.gameObject.SetActive(false);
            poolable.isUsing = false;
            
            _poolStack.Push(poolable);
        }

        public Poolable Pop(Transform parent)
        {
            Poolable poolable;

            if (_poolStack.Count > 0)
                poolable = _poolStack.Pop();
            else
                poolable = Create();

            poolable.gameObject.SetActive(true);

            // Dont destroy on load 해제 용도
            if (parent == null)
                poolable.transform.parent = Managers.Scene.CurrentScene.transform;
            
            poolable.transform.parent = parent;
            poolable.isUsing = true;
            
            return poolable;
        }
    }
    
    #endregion

    private Dictionary<string, Pool> _pool = new Dictionary<string, Pool>();
    private Transform _root;
    
    public void Init()
    {
        if (_root == null)
        {
            _root = new GameObject { name = "@Pool_root" }.transform;
            Object.DontDestroyOnLoad(_root);
        }
    }

    public void CreatePool(GameObject original, int count = 5)
    {
        Pool pool = new Pool();
        pool.Init(original, count);
        pool.Root.parent = _root;
        
        _pool.Add(original.name, pool);
    }
    
    /// <summary>
    /// 사용이 끝난 오브젝트를 Pool에 집어넣는 기능
    /// </summary>
    /// <param name="poolable"></param>
    public void Push(Poolable poolable)
    {
        string name = poolable.gameObject.name;
        if (_pool.ContainsKey(name) == false)
        {
            GameObject.Destroy(poolable.gameObject);
            return;
        }
        
        _pool[name].Push(poolable);
    }

    /// <summary>
    /// 사용할 오브젝트를 Pool에서 꺼내는 기능
    /// </summary>
    /// <param name="original"></param>
    /// <param name="parent"></param>
    /// <returns></returns>
    public Poolable Pop(GameObject original, Transform parent = null)
    {
        if (_pool.ContainsKey(original.name) == false)
            CreatePool(original);
        
        return _pool[original.name].Pop(parent);
    }

    public GameObject GetOriginal(string name)
    {
        if (_pool.ContainsKey(name) == false)
            return null;
        
        return _pool[name].Original;
    }

    public void Clear()
    {
        foreach (Transform child in _root)
            GameObject.Destroy(child.gameObject);
        
        _pool.Clear();
    }
}
  • PoolManager.cs가 추가됨에 따라 ResourceManager.cs도 함께 수정
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
public class ResourceManger 
{
    public T Load<T>(string path) where T : Object
    {
        // Load하려는 오브젝트가 GameObject고, PoolManager가 관리하는 _pool(Dict)에 있는 오리지널 중 하나라면,
        // 그 녀석을 가져오고
        // 그렇지 않다면 새로 Load한다.
        if (typeof(T) == typeof(GameObject))
        {
            string name = path;
            int index = name.LastIndexOf('/');
            if (index >= 0)
                name = name.Substring(index + 1);

            GameObject go = Managers.Pool.GetOriginal(name);
            if (go != null)
                return go as T;
        }
        
        return Resources.Load<T>(path);
    }

    public GameObject Instantiate(string path, Transform parent = null)
    {
        // 1. original 이미 들고있으면 바로 사용.
        var original = Load<GameObject>($"Prefabs/{path}");       // 원본
        if (original == null)
        {
            Debug.Log($"Failed to load prefab : {path}");
            return null;
        }

        // 2. 만약에 풀링 오브젝트라면, Poop에서 가져온다.
        if (original.GetComponent<Poolable>() != null)
            return Managers.Pool.Pop(original, parent).gameObject;
        
        var go = Object.Instantiate(original, parent);        // 복사본
        go.name = original.name;
        
        return go;
    }

    public void Destroy(GameObject go)
    {
        if (go == null)
            return;
        
        // 만약에 풀링 오브젝트라면, Destroy하지않고 Pool로 복귀시킨다.
        Poolable poolable = go.GetComponent<Poolable>();
        if (poolable != null)
        {
            Managers.Pool.Push(poolable);
            return;
        }
        
        Object.Destroy(go);
    }
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.