생성-> class에 맞는 데이터 크기만큼 메모리 할당-> 재생성시 재 대입해줘야 하는 데이터 값
그리고 파괴할 때 가비지 메모리 제거 등 많은 작업이 필요하다.
이러한 작업이 적을 경우에는 크게 상관이 없을 수 있지만 많은 데이터를 생성과 제거를 반복한다 생각하면 끔찍하다.
이러한 작업을 적게 할 방법이 ObjectPool이라는 방식이다.
ObjectPool은 오브젝트를 (생성-> 사용-> 제거)반복 에서 생성-> 사용-> (보관->재사용)반복 ->제거 식으로 사용하여
메모리를 아끼는 방식이다.
2.Unity ObjectPool
Pool 관련 클래스는 몇 가지가 있지만. 대부분 비슷한 사용법을 가진다.
그중에서 ObjectPool <T0>을 사용할 것이다.
namespace UnityEngine.Pool
{
//
// 요약:
// A stack based Pool.IObjectPool_1.
public class ObjectPool<T> : IDisposable, IObjectPool<T> where T : class
{
public ObjectPool(Func<T> createFunc, Action<T> actionOnGet = null, Action<T> actionOnRelease = null, Action<T> actionOnDestroy = null, bool collectionCheck = true, int defaultCapacity = 10, int maxSize = 10000);
public int CountAll { get; }
public int CountActive { get; }
public int CountInactive { get; }
public void Clear();
public void Dispose();
public T Get();
public PooledObject<T> Get(out T v);
public void Release(T element);
}
}
이게 내부 구성과 ObjectPool의 매개변수다.
ObjectPool(생성할 때 함수, 사용할 때 함수, 사용 끝났을 때 함수, 제거 함수, 충돌 체크, 기본적으로 생성해둘 양, 최대 양)
으로 생각하면 쉽다
총과 총알로 설명하겠다.
먼저 총알 prefab에 IobjectPool <GameObject>pool {get;set;}을 생 성해준 뒤 Update에 날아가는 코드를 작성해준다.
using UnityEngine.Pool;
public class Bullet : MonoBehaviour
{
public IObjectPool<GameObject> Pool { get; set; }
public virtual void ReleaseObject() => Pool.Release(this.gameObject);
public virtual void OnTakeFromPool(GameObject obj) { }
public virtual void OnReturnedToPool(GameObject obj) => obj.SetActive(false);
public virtual void OnDestroyPoolObject(GameObject obj) => Destroy(obj);
}
public class Bullet_1 : BulletBase
{
public float speed = 7;
private void Update()
{
if (transform.position.y >= 10)
{
ReleaseObject();
}
transform.Translate(speed * Time.deltaTime * Vector3.up);
}
public override void OnTakeFromPool(GameObject obj)
{
obj.SetActive(true);
}
}
그 후 BulletSpawner.cs를 만들어주어 ObjectPool <Gameobject> Pool를 만들어준 뒤
ObjectPool내부에 생성할 Bullet의 함수들을 넣어둔다.
그리고 bullet 생성 함수에서 bullet 내부의 IObjcetPoolpool에 spawn의 Pool을 넣어준다.
( bullet 내부에서 spawn의 Pool을 사용해야 하기 때문에)
그리고 특정 상황마다 BulletSpawner 에서 Pool.Get();을 해준다.
Pool.Get()은 Pool 내부 함수로 자동으로 현재 재사용 가능한 오브젝트가 있는지 없는지를 판단하여
생성해준다.
public class BulletSpawner : MonoBehaviour
{
public BulletBase poolablePrefab;
public IObjectPool<GameObject> Pool { get; private set; }
public void Init()
{
Pool = new ObjectPool<GameObject>(
delegate {
// 오브젝트 생성함수
// 오브젝트 생성
GameObject obj = Instantiate(poolablePrefab.gameObject);
//생성한 오브젝트의 IObjectPool Pool 에 Spawner의 ObjectPool을 넣어준다.
obj.GetComponent<BulletBase>().Pool = Pool;
return obj;
},
// Bullet에 구현된 사용할 함수들 입력
poolablePrefab.OnTakeFromPool, poolablePrefab.OnReturnedToPool,
poolablePrefab.OnDestroyPoolObject,
true, poolablePrefab.defaultCapacity, 50));
}
public void Shoot()
{
var a= poolablePrefab.Pool.Get();
a.transform.position = this.gameObject.transform.position;
}
private void Start()
{
Init();
InvokeRepeating("Shoot", 0f,5f);
}
}
using UnityEngine.Pool;
// This component returns the particle system to the pool when the OnParticleSystemStopped event is received.
[RequireComponent(typeof(ParticleSystem))]
public class ReturnToPool : MonoBehaviour
{
public ParticleSystem system;
public IObjectPool<ParticleSystem> pool;
void Start()
{
system = GetComponent<ParticleSystem>();
var main = system.main;
main.stopAction = ParticleSystemStopAction.Callback;
}
void OnParticleSystemStopped()
{
// Return to the pool
pool.Release(system);
}
}
// This example spans a random number of ParticleSystems using a pool so that old systems can be reused.
public class PoolExample : MonoBehaviour
{
public enum PoolType
{
Stack,
LinkedList
}
public PoolType poolType;
// Collection checks will throw errors if we try to release an item that is already in the pool.
public bool collectionChecks = true;
public int maxPoolSize = 10;
IObjectPool<ParticleSystem> m_Pool;
public IObjectPool<ParticleSystem> Pool
{
get
{
if (m_Pool == null)
{
if (poolType == PoolType.Stack)
m_Pool = new ObjectPool<ParticleSystem>(CreatePooledItem, OnTakeFromPool, OnReturnedToPool, OnDestroyPoolObject, collectionChecks, 10, maxPoolSize);
else
m_Pool = new LinkedPool<ParticleSystem>(CreatePooledItem, OnTakeFromPool, OnReturnedToPool, OnDestroyPoolObject, collectionChecks, maxPoolSize);
}
return m_Pool;
}
}
ParticleSystem CreatePooledItem()
{
var go = new GameObject("Pooled Particle System");
var ps = go.AddComponent<ParticleSystem>();
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
var main = ps.main;
main.duration = 1;
main.startLifetime = 1;
main.loop = false;
// This is used to return ParticleSystems to the pool when they have stopped.
var returnToPool = go.AddComponent<ReturnToPool>();
returnToPool.pool = Pool;
return ps;
}
// Called when an item is returned to the pool using Release
void OnReturnedToPool(ParticleSystem system)
{
system.gameObject.SetActive(false);
}
// Called when an item is taken from the pool using Get
void OnTakeFromPool(ParticleSystem system)
{
system.gameObject.SetActive(true);
}
// If the pool capacity is reached then any items returned will be destroyed.
// We can control what the destroy behavior does, here we destroy the GameObject.
void OnDestroyPoolObject(ParticleSystem system)
{
Destroy(system.gameObject);
}
void OnGUI()
{
GUILayout.Label("Pool size: " + Pool.CountInactive);
if (GUILayout.Button("Create Particles"))
{
var amount = Random.Range(1, 10);
for (int i = 0; i < amount; ++i)
{
var ps = Pool.Get();
ps.transform.position = Random.insideUnitSphere * 10;
ps.Play();
}
}
}
3.
내가 구현한 방식이다.
글쓴이는 싱글톤으로 PoolManager을 만들어서 같은 총알은 서로 공유하여 사용하게 만들어보았다.
이런식으로 만들경우 Spawn컴퍼넌트를 오브젝트에 붙여주고 원하는 BulletPrefab을 넣어주면 완성이다.
하지만 null 이 필요할수 있기때문에 자료형 뒤에 ? 를 붙이면 null 표현이 가능해진다.
사용 방식
값 타입 자료형 ? 변수명
int? a = 10;
int? b = null;
// int c = null; null을 허용하지 않는 값 형식
class A{}
// A a?; null 을 허용하지 않는 값 형식 이여야 한다.
int?[] arr = new int?[10];
//배열 선언방법
Nullable 값 형식에서 기본 형식으로 변환
int? a = 10;
int b = 0;
b = a ?? -1;
// a가 null 이면 -1이 들어가고 null이 아닐경우 a값이 그대로 들어가 10이 들어간다.
?? 연산자는 왼쪽 값이 null 이면 오른쪽 값을 사용하고 null이 아닐경우 왼쪽 값을 사용하는
일종의 null 을 검사하는 연산자다.
is
is 연산자로는 Nullable 형식인지 아닌지 구분 불가하다
마소 예제코드
int? a = 1;
int b = 2;
if ( a is int )
{
Console.WriteLine(int? instance is compatible with int);
}
if (b is int?)
{
Console.WriteLine("int instance is compatible with int?");
}
// Output:
// int? instance is compatible with int
// int instance is compatible with int?
GetType
Object.GetType은 Null 허용값 형식의 boxing으로 기본형식 값의 boxing과 동일하게 나온다.
GetType은 기본형식 Type 인스턴스를 반환한다.
int? a = 17;
Type typeOfA = a.GetType();
Console.WriteLine(typeOfA.FullName);
// Output:
// System.Int32
그럼으로 아래 코드 처럼 구분할수 있다
마소코드
Console.WriteLine($"int? is {(IsNullable(typeof(int?)) ? "nullable" : "non nullable")} value type");
Console.WriteLine($"int is {(IsNullable(typeof(int)) ? "nullable" : "non-nullable")} value type");
bool IsNullable(Type type) => Nullable.GetUnderlyingType(type) != null;
// Output:
// int? is nullable value type
// int is non-nullable value type
//마소 c# 튜플 사용 사례
var ys = new[] { -9, 0, 67, 100 };
var (minimum, maximum) = FindMinMax(ys);
// Output:
// Limits of [-9 0 67 100] are -9 and 100
(int min, int max) FindMinMax(int[] input)
{
if (input is null || input.Length == 0)
{
}
var min = int.MaxValue;
var max = int.MinValue;
foreach (var i in input)
{
if (i < min)
{
min = i;
}
if (i > max)
{
max = i;
}
}
return (min, max);
}
위 사용 사례에서 보이는것 처럼 minimum 과 maximum 을 따로 구하지 않고 한번의 묶음으로 처리하여
하나의 함수로 처리하는것을 볼수있다.
또다른 사용 예시로는 비교문에서 알수있다.
//튜플 비교문
(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right); // output: True
Console.WriteLine(left != right); // output: False
var t1 = (A: 5, B: 10);
var t2 = (B: 5, A: 10);
Console.WriteLine(t1 == t2); // output: True
Console.WriteLine(t1 != t2); // output: False
보이는것 처럼 두개의 묶음으로 비교하여 비교문을 줄일수 있다.
튜플은 Dictionary 에서도 사용이 가능하다.
var limitsLookup = new Dictionary<int, (int Min, int Max)>()
{
[2] = (4, 10),
[4] = (10, 20),
[6] = (0, 23)
};
if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits))
{
Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}");
}
// Output:
// Found limits: min is 10, max is 20
이처럼 쉽게 저장하고 사용하는것을 볼수있다.
참고
1. 지금 사용한 튜플 형식사용은 기존 Tuple class 와는 다르다.
ㄴ 지금 사용한 형식은 System.ValueTuple 이고 Tuple class는 System.Tuple 이다.
Ref 키워드와 Out 키워드를 더 잘 이해하기 위해서는 call by refurence , call by value를 알고 보는 게 좋다.
Ref , Out키워드
정의부터 설명하자면
RefPass by Reference
ㄴ 얕은 복사 매개변수 지정자
Out Output Parameters
ㄴ 출력용 매개변수 - 내부에서 값을 할당해주어야 한다.
사용 방법
ref 와 out 사용법
함수 선언 시
리턴 값함수명 (ref & out키워드매개변수 타입변수 이름 ) <-함수를 선언 또는 구현 시에도 매개변수 앞에 써줘야 한다.
함수 사용 시
함수명( ref & out 키워드변수 ) <- 사용할 때에도 매개변수 앞에다 ref 키워드를 써줘야 한다.
public void FuncRefB(ref int A)
{
A++;
}
public void Func( int A)
{
A++;
}
public void FuncOutB(out int A)
{
A = 10; // 내부에서 값을 대입 하지 않으면 오류가 나온다.
}
...
{
int A;
FuncOutB(out A);
Console.WriteLine(A); // 내부에서 생성해서 받는다 Call by Refurence + @ 10
Func(A);
Console.WriteLine(A); // 늘어나지 않는다 Call by Value 10
FuncRefB(ref A);
Console.WriteLine(A); // 값이 늘어난다. Call by Refurence 11
}
이처럼 c나 c++처럼 포인터 참조형처럼 Call by Reference 형식으로 넘겨줄 수 있다.
(참고할 점)
1. Ref 키워드는 할당이 되어있어야 한다.
2. Out 은 Ref처럼 할당이 되어있지 않아도 사용 가능하지만. 내부에서 새로 할당한다.(이전 값은 사라짐)
3. Ref int -> int 오버로드는 가능하나 Out -> Ref , in 오버로드는 불가능하다.
구조체 같은 경우 기본적으로 call by value 형식이기 때문에 구조체를 ref 키워드로 넘겨주면 얕은 복사(주소 값)처럼 사용할 수 있다.
in 키워드
in : 읽기 전용 매개변수 지정자
함수 선언 시
리턴 값함수명( in 키워드매개변수 타입 변수이름) <-함수를 선언 또는 구현 시에도 매개변수 앞에 써줘야 한다.
함수 사용 시
함수명( 키워드변수 ) <- in은 사용할 때 앞에 붙여 주지 않는다.
void FuncIn(in int num)
{
//num++; 내부 수정불가능
}
내부에서 수정이 불가능하기 때문에 혹여나 다른 값을 입력하거나 수정하는 실수를 미연에 방지할 수 있다.
Ref 와 in 키워드는 복사 비용을 참조로 변경하여 성능 향상을 생각할수 있다.
--추가--
out 키워드 의경우 C#7.0 버전부터는 튜블 반환을 지원했다.
튜플 반환이 있기전에 여러값을 반환 할수 없었기 때문에 out을 주로 사용하였다.
(int sum, int product) Calculate(int a, int b) => (a + b, a * b);
void Calculate(int a, int b, out int sum, out int product)
{
sum = a + b;
product = a * b;
}
Calculate(2, 3, out int sum, out int product);
out 키워드는 성능이 중요한 코드나 오래된 api, 호환성 때문에 쓰인다.
반복적으로 많이 쓰일때 생성되는것보다 out으로 불필요한 메모리할당과 GC부하를 줄일수있다.