11
17

 

 

유니티로 게임 만드시면서 오브젝트 폴링을 모르시진 않으실겁니다.

총알, 몬스터 등 여러번 비활성화되고 다시 활성화되어야하는 오브젝트들은 재활용해야합니다.

Pool 안에 담아놨다가 필요할 때 하나씩 비활성화되어있는 오브젝트를 꺼내서 활성화 시켜 사용하고

비활성화되어있는 오브젝트가 없다면 새로 생성하는 방식으로 최적화하는 기법입니다.

 

다만 코드들이 항상 복잡해지고 버그가 자주 발생합니다.

상속해서 쓴다면 이 단점들을 해결할 수 있습니다. 

 

일단 Better Object Pooling의 특징을 말씀드리겠습니다.

- 오브젝트 폴링 매니저가 필요없음

- 폴링될 오브젝트는 폴(매니저같은거)을 찾을 필요없이 스스로 비활성화시 들어가집니다.

- gameObject뿐 아니라 다양한 컴포넌트도 참조해둘 수 있습니다.

- 인터페이스를 활용해서 상속하기 편한 깔끔한 코드입니다.

 

유니티에서 기본적으로 제공하는 오브젝트 폴링 시스템도 있습니다. 관심있으시면 확인 해봐도 됩니다.

https://unity.com/kr/how-to/use-object-pooling-boost-performance-c-scripts-unity

 

오브젝트 풀링을 사용하여 Unity에서 C# 스크립트 성능 향상하기

이 페이지에서는 오브젝트 풀링과 이를 통해 게임 성능을 개선하는 방법에 대해 설명합니다. 여기에는 프로젝트에 Unity의 빌트인 오브젝트 풀링 시스템을 구현하는 방법에 대한 예제가 포함되

unity.com

 

구현해보기

이제 직접 만들면서 어떤 시스템인지 한번 봅시다.

Better Object Pooling에서는 인터페이스를 활용한다고 했습니다.

 

이 두가지 인터페이스가 핵심입니다.

폴링될 오브젝트는 해당 두 인터페이스를 통해 폴에 들어가고(Push) 꺼내집니다(Pull)

오른쪽 인터페이스는 대체로 폴이 될 오브젝트에 쓰입니다. ReturnToPool을 통해 개체를 폴에 들어가게 합니다.

 


폴 개체가 들어갈 만들기

이제 폴 개체들을 담는 폴을 만들어봅시다.

ObjectPool.cs 를 생성해주시고 아래 코드를 입력해주세요.

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

namespace ObjectPooling
{
    public class ObjectPool<T> : IPool<T> where T : MonoBehaviour, IPoolable<T>
    {
        public ObjectPool(GameObject pooledObject, int numToSpawn = 0)
        {
            this.prefab = pooledObject;
            Spawn(numToSpawn);
        }
        public ObjectPool(GameObject pooledObject, Transform parent, int numToSpawn = 0)
        {
            this.prefab = pooledObject;
            Spawn(numToSpawn, parent);
        }

        public ObjectPool(GameObject pooledObject, Action<T> pullObject, Action<T> pushObject, int numToSpawn = 0)
        {
            this.prefab = pooledObject;
            this.pullObject = pullObject;
            this.pushObject = pushObject;
            Spawn(numToSpawn);
        }

        private System.Action<T> pullObject;
        private System.Action<T> pushObject;
        private Stack<T> pooledObjects = new Stack<T>();
        private GameObject prefab;
        public int pooledCount
        {
            get
            {
                return pooledObjects.Count;
            }
        }

        public T Pull()
        {
            T t;
            if (pooledCount > 0)
                t = pooledObjects.Pop();
            else
                t = GameObject.Instantiate(prefab).GetComponent<T>();

            t.gameObject.SetActive(true); //ensure the object is on
            t.Initialize(Push);

            //allow default behavior and turning object back on
            pullObject?.Invoke(t);

            return t;
        }

        public T Pull(Vector3 position)
        {
            T t = Pull();
            t.transform.position = position;
            return t;
        }

        public T Pull(Vector3 position, Quaternion rotation)
        {
            T t = Pull();
            t.transform.position = position;
            t.transform.rotation = rotation;
            return t;
        }

        public GameObject PullGameObject()
        {
            return Pull().gameObject;
        }

        public GameObject PullGameObject(Vector3 position)
        {
            GameObject go = Pull().gameObject;
            go.transform.position = position;
            return go;
        }

        public GameObject PullGameObject(Vector3 position, Quaternion rotation)
        {
            GameObject go = Pull().gameObject;
            go.transform.position = position;
            go.transform.rotation = rotation;
            return go;
        }
        public GameObject PullGameObject(Vector3 position, Quaternion rotation, Transform parent)
        {
            GameObject go = Pull().gameObject;
            go.transform.SetParent(parent);
            go.transform.position = position;
            go.transform.rotation = rotation;
            return go;
        }

        public void Push(T t)
        {
            pooledObjects.Push(t);

            //create default behavior to turn off objects
            pushObject?.Invoke(t);

            t.gameObject.SetActive(false);
        }

        private void Spawn(int number)
        {
            T t;

            for (int i = 0; i < number; i++)
            {
                t = GameObject.Instantiate(prefab).GetComponent<T>();
                pooledObjects.Push(t);
                t.gameObject.SetActive(false);
            }
        }
        private void Spawn(int number, Transform parent)
        {
            T t;

            for (int i = 0; i < number; i++)
            {
                t = GameObject.Instantiate(prefab).GetComponent<T>();
                pooledObjects.Push(t);
                t.transform.SetParent(parent);
                t.gameObject.SetActive(false);
            }
        }
    }

    public interface IPool<T>
    {
        T Pull(); //* 옵젝 폴에서 꺼내기
        void Push(T t); //* 넣기
    }

    public interface IPoolable<T>
    {
        void Initialize(System.Action<T> returnAction);
        void ReturnToPool();
    }
}

 

일단 폴이 완성되었으니 폴 객체도 만들어봅시다. 코드 설명은 더 아래에서 하겠습니다.

 


폴 객체

PoolObject.cs

using System;
using UnityEngine;

namespace ObjectPooling
{
    public class PoolObject : MonoBehaviour, IPoolable<PoolObject>
    {
        private Action<PoolObject> returnToPool; //* 다시 Push할 때 일어날 액션

        protected virtual void OnDisable()
        {
            ReturnToPool();
        }

        public void Initialize(Action<PoolObject> returnAction)
        {
            //cache reference to return action
            this.returnToPool = returnAction;
        }

        public void ReturnToPool()
        {
            //invoke and return this object to pool
            returnToPool?.Invoke(this);
        }
    }
}

 


사용법

폴도 있고 폴 객체도 있고 이제 폴과 폴 객체를 일반적인 클래스에서 동시에 관리해봅시다.

Spawner.cs를 만들어 아래 코드를 작성해주세요.

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

/// <summary>
/// 오브젝트 폴링으로 소환하는 예제 스포너입니다.
/// </summary>
namespace ObjectPooling
{
    public class Spawner : MonoBehaviour
    {
        public GameObject cubePrefab;
        public GameObject spherePrefab;
        public GameObject capsulePrefab;
        [Range(1f, 15f)]
        public float range = 5f;
        private static ObjectPool<PoolObject> cubePool;
        private static ObjectPool<PoolObject> spherePool;
        private static ObjectPool<PoolObject> capsulePool;
        public bool canSpawn = true;

        private void OnEnable()
        {
            cubePool = new ObjectPool<PoolObject>(cubePrefab, ResetOnPull, ResetOnPush);
            spherePool = new ObjectPool<PoolObject>(spherePrefab);
            capsulePool = new ObjectPool<PoolObject>(capsulePrefab);

            StartCoroutine(SpawnOverTime());
        }

        IEnumerator SpawnOverTime()
        {
            while (canSpawn)
            {
                Spawn();
                yield return null;
            }
        }

        public void Spawn()
        {
            int random = Random.Range(0, 3);
            Vector3 position = Random.insideUnitSphere * range + this.transform.position;
            GameObject prefab;

            switch (random)
            {
                case 0:
                    prefab = cubePool.PullGameObject(position, Random.rotation);
                    break;
                case 1:
                    prefab = spherePool.PullGameObject(position, Random.rotation);
                    break;
                case 2:
                    prefab = capsulePool.PullGameObject(position, Random.rotation);
                    break;
                default:
                    prefab = cubePool.PullGameObject(position, Random.rotation);
                    break;
            }

            //* 이렇게 스폰시 호출될 구문은 여기말고 아래 호출 함수로 만드세요
            // prefab.GetComponent<Rigidbody>().velocity = Vector3.zero;
        }
        //* 이렇게 스폰시 호출될 함수는 Pull시 호출될 액션에 포함할 함수로 만드는게 맞습니다. 
        private void ResetOnPull(PoolObject poolObject)
        {
            poolObject.GetComponent<Rigidbody>().velocity = Vector3.zero;
        }
        private void ResetOnPush(PoolObject pushObject)
        {
            pushObject.GetComponent<Rigidbody>().velocity = Vector3.zero;
        }
    }
}

 

 

코드를 보다시피 이런식으로 원하는 객체를 원하는 폴에서 관리할 수 있습니다.

폴과 폴 객체를 한 눈에 볼 수 있으니 굉장히 편하고 버그가 발생할 우려가 적습니다.

 

번외

저는 이러한 스포너가 자동으로 스폰되도록 구현하고 싶어서 자동스폰하는 추상 매니저 클래스를 하나 더 만들었습니다.

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

public abstract class ObjectPoolManager : MonoBehaviour
{
    public int activeObjCount = 0; // 현재 활성화된 오브젝트 수
    protected abstract void ResetOnPull(PoolObject poolObject);
    protected abstract void ResetOnPush(PoolObject poolObject);
    protected abstract void ResetAllPools();

    [Header("자동스폰 원할시 아래 설정")]
    public bool autoSpawn = false;
    public float spawnTimer = 4f;
    public float spawnTimerMax = 4f;
}

이 클래스를 상속해서 자동 스폰되게 구현하시면 됩니다.

Unitask나 코루틴이나 Update문 셋 중 원하시는 방식으로 타이머 자동 스폰을 구현하시면 되겠습니다.

 


1년간 이 시스템을 통해 다양한 오브젝트를 폴링해봤습니다.

객체지향을 잘 다루신다면 정말 이만한 시스템이 없다고 생각합니다.

그만큼 장점이 많은 방식이니 오브젝트 폴링을 많이 사용하신다면 시간 절약겸 최적화 해보시기 바랍니다.


참조

https://onewheelstudio.com/blog/2022/1/17/object-pooling-20

 

(Better) Object Pooling — One Wheel Studio

Why Reinvent?

onewheelstudio.com

 

https://github.com/onewheelstudio/Programming-Patterns/tree/master/Programming%20Patterns/Assets/Object%20Pooling/Scripts

 

Programming-Patterns/Programming Patterns/Assets/Object Pooling/Scripts at master · onewheelstudio/Programming-Patterns

Contribute to onewheelstudio/Programming-Patterns development by creating an account on GitHub.

github.com

 

COMMENT