✍ 오브젝트 풀링으로 최적화하기
- Instantiate와 Destroy는 생성, 삭제하면서 조각난 메모리가 계속해서 쌓이는데,
게임 도중 GC가 실행되면 렉이 심하게 걸린다.
- GC(Garbage Collector) : 쌓인 조각난 메모리를 비우는 기술
- 게임 도중 GC가 실행되면서 렉이 심하게 걸릴 수 있기때문에 최적화를 위해 오브젝트 풀링 사용
오브젝트 풀링
- 미리 생성해둔 풀에서 활성화/비활성화로 사용
- 게임상 보이는건 이전과 같지만, 내부 로직이 다르다.
풀 생성
- ObjectManager 스크립트와 오브젝트를 생성
- 많은 양을 담기 때문에 public이 아닌 private으로 생성
(public으로 생성 시 인스펙터 창에 굉장히 많은 배열이 등장)
- 프리펩을 생성하여 저장할 배열 변수를 생성함.
- 한번에 등장가능한 개수를 고려해서 배열 길이를 할당
- Instantiate를 사용하려면 프리펩이 필요하므로 프리펩 변수를 꼭 생성해준다.
- Instantiate를 사용해 생성한 인스턴스를 배열에 저장해 준다.
- 프리펩 이름만 써서 생성하고 저장, 위치와 회전은 입력하지 않음.
- 생성하면 좌표0,0,0에 모두 생성되므로 모두 비활성화
- 보통 첫 로딩시간에 장변 배치와 오브젝트 풀 생성을 진행함.
void Generate()
{
// #. Enemy
for(int i=0; i<enemyL.Length; i++)
{
enemyL[i] = Instantiate(enemyL_Prefab);
enemyL[i].SetActive(false);
}
for (int i = 0; i < enemyM.Length; i++)
{
enemyM[i] = Instantiate(enemyM_Prefab);
enemyM[i].SetActive(false);
}
for (int i = 0; i < enemyS.Length; i++)
{
enemyS[i] = Instantiate(enemyS_Prefab);
enemyS[i].SetActive(false);
}
// #. Item
for(int i=0; i<itemBoom.Length; i++)
{
itemBoom[i] = Instantiate(itemBoom_Prefab);
itemBoom[i].SetActive(false);
}
for (int i = 0; i < itemCoin.Length; i++)
{
itemCoin[i] = Instantiate(itemCoin_Prefab);
itemCoin[i].SetActive(false);
}
for (int i = 0; i < itemPower.Length; i++)
{
itemPower[i] = Instantiate(itemPower_Prefab);
itemPower[i].SetActive(false);
}
// #. Bullets
for(int i=0; i<playerBulletA.Length; i++)
{
playerBulletA[i] = Instantiate(playerBulletA_Prefab);
playerBulletA[i].SetActive(false);
}
for (int i = 0; i < playerBulletB.Length; i++)
{
playerBulletB[i] = Instantiate(playerBulletB_Prefab);
playerBulletB[i].SetActive(false);
}
for (int i = 0; i < enemyBulletA.Length; i++)
{
enemyBulletA[i] = Instantiate(enemyBulletA_Prefab);
enemyBulletA[i].SetActive(false);
}
for (int i = 0; i < enemyBulletB.Length; i++)
{
enemyBulletB[i] = Instantiate(enemyBulletB_Prefab);
enemyBulletB[i].SetActive(false);
}
}
풀 활용
- 오브젝트 풀에 접근할 수 있는 함수를 생성해서, 게임오브젝트를 반환시켜준다.
- activeSelf -> 활성화되었으면 true, 비활성화되었으면 false 반환
public GameObject MakeObj(string type)
{
switch(type)
{
case "EnemyL":
targetPool = enemyL;
break;
case "EnemyM":
targetPool = enemyM;
break;
case "EnemyS":
targetPool = enemyS;
break;
case "ItemBoom":
targetPool = itemBoom;
break;
case "ItemCoin":
targetPool = itemCoin;
break;
case "ItemPower":
targetPool = itemPower;
break;
case "PlayerBulletA":
targetPool = playerBulletA;
break;
case "PlayerBulletB":
targetPool = playerBulletB;
break;
case "EnemyBulletA":
targetPool = enemyBulletA;
break;
case "EnemyBulletB":
targetPool = enemyBulletB;
break;
}
for(int i=0; i<targetPool.Length; i++)
{
if(!targetPool[i].activeSelf)
{
targetPool[i].SetActive(true);
return targetPool[i];
}
}
return null;
}
- 반환값이 없으면 함수에서 에러가 나므로 모든 상황에서 반환을 꼭 해줘야함.
- 기존의 Instantiate들을 모두 오브젝트 풀링으로 교체
- 위치와 각도는 인스턴스 변수에서 적용하고 Destroy는 SetActive(false)로 바꾼다.![]
로직정리
Find함수 제거
- 오브젝트를 직접 찾는 Find 계열 함수는 성능 부하를 유발시킨다.
- 지정한 오브젝트 풀을 가져오는 함수를 추가해서 Find 계열 함수를 오브젝트 풀링으로 교체
public GameObject[] GetPool(string type)
{
switch (type)
{
case "EnemyL":
targetPool = enemyL;
break;
case "EnemyM":
targetPool = enemyM;
break;
case "EnemyS":
targetPool = enemyS;
break;
case "EnemyBulletA":
targetPool = enemyBulletA;
break;
case "EnemyBulletB":
targetPool = enemyBulletB;
break;
}
return targetPool;
}
바뀐 코드
1. Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public bool isTouchTop;
public bool isTouchBottom;
public bool isTouchLeft;
public bool isTouchRight;
public int score;
public int health;
public float speed;
public int boom;
public int maxBoom;
public int power;
public int maxPower;
public float curShotDelay;
public float maxShotDelay;
public bool isHit;
public bool isBoom;
public ObjectManager objectManager;
public GameObject boomEffect;
public GameManager gameManager;
/* public GameObject BulletA;
public GameObject BulletB;
*/
Animator anim;
private void Awake()
{
anim = GetComponent<Animator>();
}
// Update is called once per frame
void Update()
{
Move();
Fire();
Reload();
Boom();
}
void Boom()
{
if (!Input.GetButton("Fire2"))
return;
if (isBoom)
return;
if (boom == 0)
return;
boom--;
isBoom = true;
gameManager.UpdateBoom(boom);
//#1. Effect visible
boomEffect.SetActive(true);
Invoke("OffBoomEffect", 3f);
//#2. Remove Enemy
GameObject[] enemiesL = objectManager.GetPool("EnemyL");
GameObject[] enemiesM = objectManager.GetPool("EnemyM");
GameObject[] enemiesS = objectManager.GetPool("EnemyS");
for (int i = 0; i < enemiesL.Length; i++)
{
if (enemiesL[i].activeSelf)
{
Enemy enemyLogic = enemiesL[i].GetComponent<Enemy>();
enemyLogic.onDamaged(1000);
}
}
for (int i = 0; i < enemiesM.Length; i++)
{
if (enemiesM[i].activeSelf)
{
Enemy enemyLogic = enemiesM[i].GetComponent<Enemy>();
enemyLogic.onDamaged(1000);
}
}
for (int i = 0; i < enemiesS.Length; i++)
{
if (enemiesS[i].activeSelf)
{
Enemy enemyLogic = enemiesS[i].GetComponent<Enemy>();
enemyLogic.onDamaged(1000);
}
}
//#3. Remove Enemy Bullet
GameObject[] bulletsA = objectManager.GetPool("EnemyBulletA");
GameObject[] bulletsB = objectManager.GetPool("EnemyBulletB");
for (int i = 0; i < bulletsA.Length; i++)
if(bulletsA[i].activeSelf)
bulletsA[i].SetActive(false);
for (int i = 0; i < bulletsB.Length; i++)
if (bulletsB[i].activeSelf)
bulletsB[i].SetActive(false);
}
void Reload()
{
curShotDelay += Time.deltaTime;
}
void Fire()
{
if (!Input.GetButton("Fire1")) // Fire1은 마우스좌클릭
return;
if (curShotDelay < maxShotDelay)
return;
switch(power)
{
case 1:
// Power 1
GameObject Bullet = objectManager.MakeObj("PlayerBulletA");
Bullet.transform.position = transform.position;
Rigidbody2D rigid = Bullet.GetComponent<Rigidbody2D>();
rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
break;
case 2:
// Power 2
GameObject BulletR = objectManager.MakeObj("PlayerBulletA");
GameObject BulletL = objectManager.MakeObj("PlayerBulletA");
BulletR.transform.position = transform.position + Vector3.right * 0.1f;
BulletR.transform.position = transform.position + Vector3.left * 0.1f;
Rigidbody2D rigidR = BulletR.GetComponent<Rigidbody2D>();
Rigidbody2D rigidL = BulletL.GetComponent<Rigidbody2D>();
rigidR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
rigidL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
break;
case 3:
// Power 3
GameObject BulletRR = objectManager.MakeObj("PlayerBulletA");
GameObject BulletCC = objectManager.MakeObj("PlayerBulletB");
GameObject BulletLL = objectManager.MakeObj("PlayerBulletA");
BulletRR.transform.position = transform.position + Vector3.right * 0.35f;
BulletCC.transform.position = transform.position;
BulletLL.transform.position = transform.position + Vector3.left * 0.35f;
Rigidbody2D rigidRR = BulletRR.GetComponent<Rigidbody2D>();
Rigidbody2D rigidCC = BulletCC.GetComponent<Rigidbody2D>();
Rigidbody2D rigidLL = BulletLL.GetComponent<Rigidbody2D>();
rigidRR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
rigidCC.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
rigidLL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
break;
}
curShotDelay = 0;
}
void Move()
{
float h = Input.GetAxisRaw("Horizontal");
if ((isTouchRight && h == 1) || (isTouchLeft && h == -1))
h = 0;
float v = Input.GetAxisRaw("Vertical");
if ((isTouchTop && v == 1) || (isTouchBottom && v == -1))
v = 0;
Vector3 curPos = transform.position;
Vector3 nextPos = new Vector3(h, v, 0) * speed * Time.deltaTime;
transform.position = curPos + nextPos;
// Animation
if (Input.GetButtonUp("Horizontal") || Input.GetButtonDown("Horizontal"))
{
anim.SetInteger("Input", (int)h);
}
}
public void OnTriggerEnter2D(Collider2D collision)
{
if(collision.gameObject.tag == "Border")
{
switch(collision.gameObject.name)
{
case "Top":
isTouchTop = true;
break;
case "Bottom":
isTouchBottom = true;
break;
case "Left":
isTouchLeft = true;
break;
case "Right":
isTouchRight = true;
break;
}
}
else if(collision.gameObject.tag == "Enemy" || (collision.gameObject.tag == "EnemyBullet"))
{
if (isHit)
return;
isHit = true;
health--;
gameManager.UpdateLife(health);
if (health == 0)
gameManager.GameOver();
else
gameManager.RespawnPlayer();
gameObject.SetActive(false);
collision.gameObject.SetActive(false);
}
else if(collision.gameObject.tag == "Item")
{
Item item = collision.gameObject.GetComponent<Item>();
string itemtype = item.type;
switch(itemtype)
{
case "Coin":
score += 1000;
break;
case "Power":
if (power == maxPower)
score += 500;
else
power++;
break;
case "Boom":
if (boom == maxBoom)
score += 500;
else
{
boom++;
gameManager.UpdateBoom(boom);
}
break;
}
collision.gameObject.SetActive(false);
}
}
void OffBoomEffect()
{
boomEffect.SetActive(false);
isBoom = false;
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.gameObject.tag == "Border")
{
switch (collision.gameObject.name)
{
case "Top":
isTouchTop = false;
break;
case "Bottom":
isTouchBottom = false;
break;
case "Left":
isTouchLeft = false;
break;
case "Right":
isTouchRight = false;
break;
}
}
}
}
2. ObjectManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectManager : MonoBehaviour
{
GameObject[] enemyL;
GameObject[] enemyM;
GameObject[] enemyS;
GameObject[] itemBoom;
GameObject[] itemCoin;
GameObject[] itemPower;
GameObject[] enemyBulletA;
GameObject[] enemyBulletB;
GameObject[] playerBulletA;
GameObject[] playerBulletB;
GameObject[] targetPool;
// #. Prefabs
public GameObject enemyL_Prefab;
public GameObject enemyM_Prefab;
public GameObject enemyS_Prefab;
public GameObject itemBoom_Prefab;
public GameObject itemCoin_Prefab;
public GameObject itemPower_Prefab;
public GameObject enemyBulletA_Prefab;
public GameObject enemyBulletB_Prefab;
public GameObject playerBulletA_Prefab;
public GameObject playerBulletB_Prefab;
void Awake()
{
enemyL = new GameObject[10];
enemyM = new GameObject[10];
enemyS = new GameObject[10];
itemCoin = new GameObject[20];
itemBoom = new GameObject[20];
itemPower = new GameObject[20];
playerBulletA = new GameObject[100];
playerBulletB = new GameObject[100];
enemyBulletA = new GameObject[100];
enemyBulletB = new GameObject[100];
Generate();
}
void Generate()
{
// #. Enemy
for(int i=0; i<enemyL.Length; i++)
{
enemyL[i] = Instantiate(enemyL_Prefab);
enemyL[i].SetActive(false);
}
for (int i = 0; i < enemyM.Length; i++)
{
enemyM[i] = Instantiate(enemyM_Prefab);
enemyM[i].SetActive(false);
}
for (int i = 0; i < enemyS.Length; i++)
{
enemyS[i] = Instantiate(enemyS_Prefab);
enemyS[i].SetActive(false);
}
// #. Item
for(int i=0; i<itemBoom.Length; i++)
{
itemBoom[i] = Instantiate(itemBoom_Prefab);
itemBoom[i].SetActive(false);
}
for (int i = 0; i < itemCoin.Length; i++)
{
itemCoin[i] = Instantiate(itemCoin_Prefab);
itemCoin[i].SetActive(false);
}
for (int i = 0; i < itemPower.Length; i++)
{
itemPower[i] = Instantiate(itemPower_Prefab);
itemPower[i].SetActive(false);
}
// #. Bullets
for(int i=0; i<playerBulletA.Length; i++)
{
playerBulletA[i] = Instantiate(playerBulletA_Prefab);
playerBulletA[i].SetActive(false);
}
for (int i = 0; i < playerBulletB.Length; i++)
{
playerBulletB[i] = Instantiate(playerBulletB_Prefab);
playerBulletB[i].SetActive(false);
}
for (int i = 0; i < enemyBulletA.Length; i++)
{
enemyBulletA[i] = Instantiate(enemyBulletA_Prefab);
enemyBulletA[i].SetActive(false);
}
for (int i = 0; i < enemyBulletB.Length; i++)
{
enemyBulletB[i] = Instantiate(enemyBulletB_Prefab);
enemyBulletB[i].SetActive(false);
}
}
public GameObject MakeObj(string type)
{
switch(type)
{
case "EnemyL":
targetPool = enemyL;
break;
case "EnemyM":
targetPool = enemyM;
break;
case "EnemyS":
targetPool = enemyS;
break;
case "ItemBoom":
targetPool = itemBoom;
break;
case "ItemCoin":
targetPool = itemCoin;
break;
case "ItemPower":
targetPool = itemPower;
break;
case "PlayerBulletA":
targetPool = playerBulletA;
break;
case "PlayerBulletB":
targetPool = playerBulletB;
break;
case "EnemyBulletA":
targetPool = enemyBulletA;
break;
case "EnemyBulletB":
targetPool = enemyBulletB;
break;
}
for(int i=0; i<targetPool.Length; i++)
{
if(!targetPool[i].activeSelf)
{
targetPool[i].SetActive(true);
return targetPool[i];
}
}
return null;
}
public GameObject[] GetPool(string type)
{
switch (type)
{
case "EnemyL":
targetPool = enemyL;
break;
case "EnemyM":
targetPool = enemyM;
break;
case "EnemyS":
targetPool = enemyS;
break;
case "EnemyBulletA":
targetPool = enemyBulletA;
break;
case "EnemyBulletB":
targetPool = enemyBulletB;
break;
}
return targetPool;
}
}
3. item.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Item : MonoBehaviour
{
public string type;
Rigidbody2D rigid;
private void Awake()
{
rigid = GetComponent<Rigidbody2D>();
}
private void OnEnable()
{
rigid.velocity = Vector2.down * 1.5f;
}
}
4. GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using UnityEngine;
public class GameManager : MonoBehaviour
{
string[] enemyObjs;
public Transform[] spawnPoints;
public GameObject player;
public float maxSpawnDelay;
public float curSpawnDelay;
public Text scoreText;
public Image[] lifeImage;
public Image[] boomImage;
public GameObject gameOverSet;
public ObjectManager objectManager;
private void Awake()
{
enemyObjs = new string[] { "EnemyL", "EnemyM", "EnemyS" };
}
void Update()
{
curSpawnDelay += Time.deltaTime;
if(curSpawnDelay > maxSpawnDelay)
{
SpawnEnemy();
maxSpawnDelay = Random.Range(0.5f, 3f);
curSpawnDelay = 0;
}
// UI Score Update
Player playerLogic = player.GetComponent<Player>();
scoreText.text = string.Format("{0:n0}", playerLogic.score);
}
void SpawnEnemy()
{
int randomEnemy = Random.Range(0, 3);
int spawnPoint = Random.Range(0, 7);
GameObject enemy = objectManager.MakeObj(enemyObjs[randomEnemy]);
enemy.transform.position = spawnPoints[spawnPoint].position;
Rigidbody2D rigid = enemy.GetComponent<Rigidbody2D>();
Enemy enemyLogic = enemy.GetComponent<Enemy>();
enemyLogic.player = player;
enemyLogic.objectManager = objectManager;
if(spawnPoint == 5) // Left Spawn
{
enemy.transform.Rotate(Vector3.forward * 90);
rigid.velocity = new Vector2(1, -enemyLogic.speed);
}
else if(spawnPoint == 6) // Right Spawn
{
enemy.transform.Rotate(Vector3.back * 90);
rigid.velocity = new Vector2(-1, -enemyLogic.speed);
}
else // Front Spawn
{
rigid.velocity = new Vector2(0, -enemyLogic.speed);
}
}
public void RespawnPlayer()
{
Invoke("RespawnPlayerExe", 2f);
}
public void RespawnPlayerExe()
{
Player playerLogic = player.GetComponent<Player>();
playerLogic.isHit = false;
player.transform.position = Vector3.down * 4f;
player.SetActive(true);
}
public void UpdateLife(int Life)
{
// #. UI Init Disable
for (int i = 0; i < 3; i++)
lifeImage[i].color = new Color(1, 1, 1, 0);
// #. Life Active
for (int i = 0; i < Life; i++)
lifeImage[i].color = new Color(1, 1, 1, 1);
}
public void UpdateBoom(int boom)
{
// #. UI Init Disable
for (int i = 0; i < 3; i++)
boomImage[i].color = new Color(1, 1, 1, 0);
// #. Life Active
for (int i = 0; i < boom; i++)
boomImage[i].color = new Color(1, 1, 1, 1);
}
public void GameOver()
{
gameOverSet.SetActive(true);
}
public void Retry()
{
SceneManager.LoadScene(0);
}
}
5. Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
public string enemyName;
public float speed;
public int enemyScore;
public int health;
public float curShotDelay;
public float maxShotDelay;
public Sprite[] sprites;
public ObjectManager objectManager;
/*public GameObject BulletA;
public GameObject BulletB;
public GameObject itemCoin;
public GameObject itemPower;
public GameObject itemBoom;
*/
public GameObject player;
SpriteRenderer spriteRenderer;
void Awake()
{
spriteRenderer = GetComponent<SpriteRenderer>();
}
private void OnEnable()
{
switch (enemyName)
{
case "L":
health = 15;
break;
case "M":
health = 5;
break;
case "S":
health = 3;
break;
}
}
void Update()
{
Fire();
Reload();
}
void Reload()
{
curShotDelay += Time.deltaTime;
}
void Fire()
{
if (curShotDelay < maxShotDelay)
return;
if(enemyName == "S")
{
GameObject Bullet = objectManager.MakeObj("EnemyBulletA");
Bullet.transform.position = transform.position;
Rigidbody2D rigid = Bullet.GetComponent<Rigidbody2D>();
Vector3 dirVec = player.transform.position - transform.position;
rigid.AddForce(dirVec.normalized * 10, ForceMode2D.Impulse);
}
else if(enemyName == "L")
{
GameObject BulletR = objectManager.MakeObj("EnemyBulletB");
GameObject BulletL = objectManager.MakeObj("EnemyBulletB");
BulletR.transform.position = transform.position+ Vector3.right * 0.3f;
BulletL.transform.position = transform.position + Vector3.left * 0.3f;
Rigidbody2D rigidR = BulletR.GetComponent<Rigidbody2D>();
Rigidbody2D rigidL = BulletL.GetComponent<Rigidbody2D>();
Vector3 dirVecR = player.transform.position - (transform.position + Vector3.right * 0.3f);
Vector3 dirVecL = player.transform.position - (transform.position + Vector3.left * 0.3f);
rigidR.AddForce(dirVecR.normalized * 4, ForceMode2D.Impulse);
rigidL.AddForce(dirVecL.normalized * 4, ForceMode2D.Impulse);
}
curShotDelay = 0;
}
public void onDamaged(int dmg)
{
if (health <= 0)
return;
health -= dmg;
spriteRenderer.sprite = sprites[1];
Invoke("ReturnSprite", 0.1f);
if (health <= 0)
{
Player playerLogic = player.GetComponent<Player>();
playerLogic.score += enemyScore;
// # Item Drop
int ran = Random.Range(0, 10);
if (ran < 3) // 30%
Debug.Log("Not Item");
else if (ran < 6) // 30% coin
{
GameObject itemCoin = objectManager.MakeObj("ItemCoin");
itemCoin.transform.position = transform.position;
}
else if (ran < 8) // 20% power
{
GameObject itemPower = objectManager.MakeObj("ItemPower");
itemPower.transform.position = transform.position;
}
else if (ran < 10) // 20% boom
{
GameObject itemBoom = objectManager.MakeObj("ItemBoom");
itemBoom.transform.position = transform.position;
}
gameObject.SetActive(false);
transform.rotation = Quaternion.identity;
}
}
void ReturnSprite()
{
spriteRenderer.sprite = sprites[0];
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.tag == "BorderBullet")
{
gameObject.SetActive(false);
transform.rotation = Quaternion.identity;
}
else if (collision.gameObject.tag == "PlayerBullet")
{
Bullet bullet = collision.gameObject.GetComponent<Bullet>(); // collision.gameObject에서 GetComponent한다
onDamaged(bullet.dmg);
collision.gameObject.SetActive(false);
}
}
}
출처 - https://www.youtube.com/c/GoldMetal