오늘은 게임을 효율적으로 구현할 수 있는 방법에 대해 공부하겠습니다.
Object Pooling
1. Object Pooling이란?
게임에서는 엄청나게 다양하고 많은 오브젝트를이 쓰입니다. 만약 이런 오브젝트들을 생성파괴생성파괴생성파괴... 한다면 CPU성능이 나빠질 것입니다. 오브젝트를 생성할 때마다 메모리에 데이터를 할당하고, 파괴될 때마다 메모리에 데이터를 할당 해제합니다. 할당과 해제는 CPU가 담당하기 때문에 CPU 성능이 나빠질 수 밖에 없습니다.
그리고 C#에는 가비지 컬렉터가 있어 메모리에서 사용되지 않는 데이터를 할당 해제하는데, 가비지 컬렉터는 CPU가 작동시킵니다. 그럼 가비지 컬렉터가 자주 발생하면? CPU에 부담이 가게 됩니다.
이러한 점을 보완하기 위해 등장한 것이 "Object를 필요한 만큼 미리 만들어 놓고, 삭제하지 말고, 필요할 때마다 꺼내 쓰는" 것이 Object Pooling 기법입니다.
물론 Object Pooling기법을 사용하면, 미리 갯수를 정해서 만들면 낭비되는 오브젝트가 있습니다. 메모리가 낭비되긴 하지만 성능향상을 할 수 있습니다. 고로 적절한 갯수만 정한다면 뛰어난 성능향상을 기대할 수 있습니다.
2. Object Pooling 사용
우선, 저는 단순한 형태의 Object Pooling과 범용 목적의 Object Pooling기법을 다뤄보겠습니다.
1. 단순: 특정 오브젝트의 스크립트 상에서 바로 Object Pooling을 구현
2. 범용: Object Pool 클래스를 생성해서, 적재적소에 특정한 오브젝트를 위해 사용할 수 있게 구현
첫번째 구현방법은 오브젝트를 추가할 때마다 Pool을 추가로 생성해주는 낭비가 있습니다. 예로 총알이 한 종류 늘어날 때마다 오브젝트 풀링을 구현해줘야 합니다. 정말 오브젝트 풀이 뭔지만 보여주는 것입니다.
두번째 구현방법은 한 번 구현해 놓으면 다양한 오브젝트에 대해서 지원가능하니 이 구현법이 더 범용성 있다고 말할 수 있습니다.
저는 배워가는 단계니 두개의 구현방법 모두 해보도록 하겠습니다.
탄 10발을 3초마다 쏘는 코드를 첫번째 Object Pooling기법으로 구현하겠습니다.
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
|
public class CharacterBehaviour : MonoBehaviour{
public GameObject bullet;
public Transform firePos;
public int poolCount = 10; // 총알 갯수
List<GameObject> bullets; // Object Pool
void Start(){
bullets = new List<GameObject>();
while(poolCount > 0){
GameObject obj = Instantiate(bullet, firePos.position, Quaternion.identity);
obj.SetActive(false); // 생성되자마자 비활성화
bullets.Add(obj); // Pool 리스트에 추가
poolCount -= 1;
}
StartCoroutine(Fire(0.1f)); // 0.1의 시간간격을 두고 실행
}
private IEnumerator Fire(float coolTime){
while (true){
foreach(GameObject bullet in bullets){ // Pool을 뒤져서
if (!bullet.activeInHierarchy){ // 비활성화된 오브젝트가 있다면
bullet.transform.position = firePos.position; // firePos 위치에
bullet.GetComponent<BulletBehaviour>().Spawn(); // 탄 생성!
break;
}
}
yield return new WaitForSeconds(coolTime);
}
}
}
public class BulletBehaviour : MonoBehaviour{
public float speed = 5f;
private void OnEnable(){ // 활성화 된다면
StartCoroutine(ActiveTime(3f));
}
public void Spawn(){
gameObject.SetActive(true); // 생성 = 활성화
}
private IEnumerator ActiveTime(float coolTime){
yield return new WaitForSeconds(coolTime); // coolTime만큼 활성화
gameObject.SetActive(false); // coolTime 다 됐으니 비활성화
}
void Update(){
transform.Translate(Vector2.right * speed * Time.deltaTime);
}
}
|
cs |
*코드+*
▶ private void OnEnable() { }
- 스크립트가 포함된 오브젝트가 활성화되는 순간 작동합니다. MonoBehaviour에 상속된 함수입니다.
탄에 해당하는 Pool을 만들어 작성해보았습니다.
그렇다면 탄의 종류를 늘려볼까요?
List<GameObject> bullets1; // Object Pool (1)
List<GameObject> bullets2; // Object Pool (2)
List<GameObject> bullets3; // Object Pool (3)
...음
여러 종류의 탄을 구현할 것을 생각해서 풀을 여러 개 만들면서부터 막막해졌습니다.
그렇다면 이제 Object 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
|
public class CharacterBehaviour : MonoBehaviour
{
public Transform firePos;
public GameObject bulletObjectPool;
private ObjectPoolClass bulletObjectPooler;
void Start(){
bulletObjectPooler = bulletObjectPool.GetComponent<ObjectPoolClass>();
StartCoroutine(Fire(0.1f));
}
private IEnumerator Fire(float coolTime){
while (true){
GameObject bullet = bulletObjectPooler.GetObject();
// 오브젝트를 받아와서
if (bullet == null) yield return null;
// 없다면 종료
bullet.transform.position = firePos.position;
bullet.GetComponent<BulletBehaviour>().Spawn();
// 있으면 활성화
yield return new WaitForSeconds(coolTime);
}
}
}
public class BulletBehaviour : MonoBehaviour
{
public float speed = 5f;
private void OnEnable(){
StartCoroutine(ActiveTime(3f));
}
public void Spawn(){
gameObject.SetActive(true);
}
private IEnumerator ActiveTime(float coolTime){
yield return new WaitForSeconds(coolTime);
gameObject.SetActive(false);
}
void Update(){
transform.Translate(Vector2.right * speed * Time.deltaTime);
}
}
public class ObjectPoolClass : MonoBehaviour{
public GameObject pooledObject; // 처리하고자 하는 오브젝트
public int poolCount = 10; // 탄 갯수
public bool more = true;
// poolCount보다 많은 오브젝트 갯수가 화면에 등장해야 할 경우
// 추가적으로 오브젝트를 생성해주기 위한 변수
// 즉, 화면에 보여주는 필요한 만큼만 생성할 수 있게됨
private List<GameObject> pooledObjects;
void Start(){
pooledObjects = new List<GameObject>();
while(poolCount > 0){
GameObject obj = Instantiate(pooledObject);
obj.SetActive(false);
pooledObjects.Add(obj);
poolCount -= 1;
} // 오브젝트 10개 생성
}
public GameObject GetObject(){
// Object Pool에서 하나의 오브젝트를 꺼내는 함수
foreach(GameObject obj in pooledObjects){ // Pool을 뒤져서
if (!obj.activeInHierarchy){ //비활성 오브젝트가 있다면
return obj; // 반환해서 사용할 수 있도록 함
}
}
if (more){
// 게임 내에 오브젝트가 전부 활성화 되었을 때
GameObject obj = Instantiate(pooledObject);
// 추가 오브젝트 생성
pooledObjects.Add(obj);
return obj;
}
return null; // 오류났을 때 null 반환 (메모리 공간 없을 때)
}
// 그래서 GetObject를 받아가는 쪽에서 활성화시켜서 사용하면 됨
}
|
cs |
이제 탄 종류를 따로 코드수정 없이 추가할 수 있게 되었습니다.
3. 결론
Object Pooling 요약
- 오브젝트 생성/파괴로 CPU부담에 의한 렉이 발생합니다. 그래서 필요한 오브젝트를 미리 만들어놓고 활성/비활성화를 통해 CPU부담을 줄여주는 기법입니다.
Object Pooling 기법은 어느 오브젝트에 적용해줘야 할까??
- 메모리 할당/할당해제 (생성, 파괴)가 자주 일어나는 오브젝트에게 사용합니다. ( 종류가 있다면 더욱 )
'게임 > Unity' 카테고리의 다른 글
[Unity 이론] Firebase Database 연동 (0) | 2019.11.28 |
---|---|
[Unity 이론] Firebase Database (0) | 2019.11.28 |
[Unity 이론] Coroutine (1) | 2019.11.26 |
[Unity 3D] 충돌 체크 (0) | 2019.11.25 |
[Unity 이론] Time.deltaTime (0) | 2019.11.24 |
댓글