08강) 미사일 발사2 |
지난 시간에는 일반적인 방법으로 "Instantiate 함수"를 이용하여 미사일을 생성하고 "Destroy 함수"를 사용하여 미사일을 제거하였습니다.
오늘은 조금 다른 방법인 "메모리 풀링"을 사용한 미사일 발사를 사용해봅시다.
메모리 풀링(Memory Pooling) |
우리는 무슨 엔진을 사용하여 게임을 만들고 있을까요?
바로 "유니티" 입니다.
그리고 우리는 어떤 언어를 사용하고 있나요?
바로 "C#" 입니다.
C++(다른 컴퓨터 언어)로 게임을 만들게되면 메모리 관리를 프로그래머가 직접적으로 관여를 하여 사용했던 객체가 필요없어지면 메모리 공간에서 반환시켜줘야 합니다.
이렇게 되었을 때 장점은 메모리를 원할 때 확보해줄 수 있다는 것이죠. 하지만 그 만큼 메모리 관리를 철저히 해줘야 한다는 어려움이 있습니다.
C#은 자바(다른 컴퓨터 언어)와 마찬가지로 GC(가비지 컬렉터)라는 것이 메모리를 자동으로 관리해줍니다.
이것의 장점은 메모리 해제를 자동으로 해주기 때문에 프로그래머의 수고를 덜어줄 수 있다는 것이지요. 하지만 엄청난 단점이 있습니다. 바로 예기치 않은 "GC 호출"입니다.
GC가 호출이 되면 자동으로 필요없는 객체를 찾아서 메모리 공간에서 없애버립니다. 이렇게 하여 자동으로 메모리를 확보하게 됩니다. 하지만 이 과정에서 부하가 생기게 되어 예기치 않은 랙이 발생하게 됩니다.
그런데 GC는 객체가 사라질때마다 호출이 되어 많은 부하를 일으키죠.
(즉, Destroy 함수가 호출된 다음에 바로 GC가 호출이 됩니다.)
그래서 유니티에서는 "메모리 풀링"을 사용하여 객체를 처리합니다.
메모리 풀링(Memory Pooling) |
미리 메모리 공간을 사용할 만큼 할당하여 그 안에 사용할 객체들을 미리 넣어둔다. 그리고 사용할 때 마다 메모리 풀에서 가져와서 활성화 시켜 사용하고 필요없을 때는 비활성화만 시킨다. 이 과정에서 "Destroy 함수"가 호출되지 않기에 최대한으로 GC의 호출을 막을 수 있다. |
그림으로 볼까요?
(실제로 메모리 공간에 미사일이 저렇게 들어가지는 않습니다. 보기 좋게 표현하기 위해서 저렇게 했습니다.)
위의 그림과 같다고 생각하시면 되겠습니다.
메모리 풀링소스 |
좋은 소스가 있으니 가져다 씁시다.
using UnityEngine; using System.Collections; // 메모리 풀 클래스 출처 : http://hyunity3d.tistory.com/195 //----------------------------------------------------------------------------------------- // 메모리 풀 클래스 // 용도 : 특정 게임오브젝트를 실시간으로 생성과 삭제하지 않고, // : 미리 생성해 둔 게임오브젝트를 재활용하는 클래스입니다. //----------------------------------------------------------------------------------------- //MonoBehaviour 상속 안받음. IEnumerable 상속시 foreach 사용 가능 //System.IDisposable 관리되지 않는 메모리(리소스)를 해제 함 public class MemoryPool : IEnumerable, System.IDisposable { //------------------------------------------------------------------------------------- // 아이템 클래스 //------------------------------------------------------------------------------------- class Item { public bool active; // 오브젝트가 사용하고 있는 중인지 판단하는 변수 public GameObject gameObject; // 저장할 오브젝트 } // 위의 아이템 클래스를 배열로 선언(즉, 여러개의 아이템을 저장 가능) Item[] table; //------------------------------------------------------------------------------------ // 열거자 기본 재정의(foreach에서 사용하는 것인데 우리는 사용하지 않음, 이유는 나중에..) //------------------------------------------------------------------------------------ public IEnumerator GetEnumerator() { if (table == null) // 만약 table이 객체화 되지 않았다면? yield break; // 함수를 그냥 탈출 // table이 존재하면 여기서부터 실행 // count는 table의 길이(즉, 배열의 크기) int count = table.Length; for (int i = 0; i < count; i++) // 총 count만큼 반복 { Item item = table[i]; // item에 table의 i위치에 해당되는 객체를 대입 if (item.active) // item이 사용중이면 yield return item.gameObject; // 현 item의 오브젝트를 반환 } } //------------------------------------------------------------------------------------- // 메모리 풀 생성 // original : 미리 생성해 둘 원본소스 // count : 풀 최고 갯수 //------------------------------------------------------------------------------------- public void Create(Object original, int count) { Dispose(); // 메모리풀 초기화 table = new Item[count]; // count 만큼 배열을 생성 for (int i = 0; i < count; i++) // count 만큼 반복 { Item item = new Item(); item.active = false; item.gameObject = GameObject.Instantiate(original) as GameObject; // original을 GameObject 형식으로 item.gameObject에 저장 item.gameObject.SetActive(false); // SetActive는 활성화 함수인데 메모리에만 올릴 것이므로 비활성화 상태로 저장 table[i] = item; } } //------------------------------------------------------------------------------------- // 새 아이템 요청 - 쉬고 있는 객체를 반납한다. //------------------------------------------------------------------------------------- public GameObject NewItem() // GetEnumerator()와 비슷 { if (table == null) return null; int count = table.Length; for (int i = 0; i < count; i++) { Item item = table[i]; if (item.active == false) { item.active = true; item.gameObject.SetActive(true); return item.gameObject; } } return null; } //-------------------------------------------------------------------------------------- // 아이템 사용종료 - 사용하던 객체를 쉬게한다. // gameOBject : NewItem으로 얻었던 객체 //-------------------------------------------------------------------------------------- public void RemoveItem(GameObject gameObject) { // table이 객체화되지 않았거나, 매개변수로 오는 gameObject가 없다면 if (table == null || gameObject == null) return; // 함수 탈출 // table이 존재하거나, 매개변수로 오는 gameObject가 존재하면 여기서부터 실행 // count는 table의 길이(즉, 배열의 크기) int count = table.Length; for (int i = 0; i < count; i++) { Item item = table[i]; // 매개변수 gameObject와 item의 gameObject가 같다면 if (item.gameObject == gameObject) { // active 변수를 false로 item.active = false; // 그리고 게임오브젝트를 비활성화 시킨다. item.gameObject.SetActive(false); break; } } } //-------------------------------------------------------------------------------------- // 모든 아이템 사용종료 - 모든 객체를 쉬게한다. //-------------------------------------------------------------------------------------- public void ClearItem() { // table이 객체화되지 않았다면.. if (table == null) return; // table이 존재하면... // count는 table의 길이(즉, 배열의 크기) int count = table.Length; for (int i = 0; i < count; i++) { Item item = table[i]; // item이 비어있지 않고, 활성화되어 있다면 if (item != null && item.active) { // 비활성화 처리를 시작합니다. item.active = false; item.gameObject.SetActive(false); } } } //-------------------------------------------------------------------------------------- // 메모리 풀 삭제 //-------------------------------------------------------------------------------------- public void Dispose() { // table이 객체화되지 않았다면.. if (table == null) return; // table이 존재하면... // count는 table의 길이(즉, 배열의 크기) int count = table.Length; for (int i = 0; i < count; i++) { Item item = table[i]; GameObject.Destroy(item.gameObject); // 메모리 풀을 삭제하는 것이기 때문에 모든 오브젝트를 Destroy 한다. } table = null; } }
만약에 컴퓨터 언어에 대한 지식이 없으시고 여기서 C# 기본을 배우셨다면 무슨말인지 모르실 수 있습니다만 잘 읽어보시면 아실수도...
즉, 미사일을 위의 소스를 이용하여 발사를 하게 되면 다음과 같은 과정을 거치게 됩니다.
이런 식이죠.
그리고 위의 그림에는 안나왔지만 플레이어가 사망하거나 게임이 끝나면 Dispose를 해주면 됩니다.
적용하기) 스크립트 추가 |
위에 있던 스크립트를 추가합시다.
자신이 스크립트를 넣는 폴더에 생성하여 그대로 옮겨붙이면 됩니다.
이때! 스크립트 파일 이름과 class의 이름은 같아야 합니다.
적용하기) 미사일 발사 스크립트 수정 |
지난 시간에 만들었던 미사일 발사 스크립트로 갑시다.
그리고 아래와 같이 추가해줍시다.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player_Fire : MonoBehaviour { public GameObject PlayerMissile; // 복제할 미사일 오브젝트 public Transform MissileLocation; // 미사일이 발사될 위치 public float FireDelay; // 미사일 발사 속도(미사일이 날라가는 속도x) private bool FireState; // 미사일 발사 속도를 제어할 변수 public int MissileMaxPool; // 메모리 풀에 저장할 미사일 개수 private MemoryPool MPool; // 메모리 풀 private GameObject[] MissileArray; // 메모리 풀과 연동하여 사용할 미사일 배열 // 게임이 종료되면 자동으로 호출되는 함수 private void OnApplicationQuit() { // 메모리 풀을 비웁니다. MPool.Dispose(); } void Start () { // 처음에 미사일을 발사할 수 있도록 제어변수를 true로 설정 FireState = true; // 메모리 풀을 초기화합니다. MPool = new MemoryPool(); // PlayerMissile을 MissileMaxPool만큼 생성합니다. MPool.Create(PlayerMissile, MissileMaxPool); // 배열도 초기화 합니다.(이때 모든 값은 null이 됩니다.) MissileArray = new GameObject[MissileMaxPool]; } void Update () { // 매 프레임마다 미사일발사 함수를 체크한다. playerFire(); } // 미사일을 발사하는 함수 private void playerFire() { // 제어변수가 true일때만 발동 if (FireState) { // 키보드의 "A"를 누르면 if (Input.GetKey(KeyCode.A)) { // 코루틴 "FireCycleControl"이 실행되며 StartCoroutine(FireCycleControl()); // 미사일 풀에서 발사되지 않은 미사일을 찾아서 발사합니다. for(int i = 0; i < MissileMaxPool; i++) { // 만약 미사일배열[i]가 비어있다면 if (MissileArray[i] == null) { // 메모리풀에서 미사일을 가져온다. MissileArray[i] = MPool.NewItem(); // 해당 미사일의 위치를 미사일 발사지점으로 맞춘다. MissileArray[i].transform.position = MissileLocation.transform.position; // 발사 후에 for문을 바로 빠져나간다. break; } } } } // 미사일이 발사될때마다 미사일을 메모리풀로 돌려보내는 것을 체크한다. for(int i = 0; i < MissileMaxPool; i++) { // 만약 미사일[i]가 활성화 되어있다면 if(MissileArray[i]) { // 미사일[i]의 Collider2D가 비활성 되었다면 if(MissileArray[i].GetComponent().enabled == false) { // 다시 Collider2D를 활성화 시키고 MissileArray[i].GetComponent ().enabled = true; // 미사일을 메모리로 돌려보내고 MPool.RemoveItem(MissileArray[i]); // 가리키는 배열의 해당 항목도 null(값 없음)로 만든다. MissileArray[i] = null; } } } } // 코루틴 함수 IEnumerator FireCycleControl() { // 처음에 FireState를 false로 만들고 FireState = false; // FireDelay초 후에 yield return new WaitForSeconds(FireDelay); // FireState를 true로 만든다. FireState = true; } }
여기서 변경된 사항들을 뽑아서 보도록 합시다.
public int MissileMaxPool; // 메모리 풀에 저장할 미사일 개수 private MemoryPool MPool; // 메모리 풀 private GameObject[] MissileArray; // 메모리 풀과 연동하여 사용할 미사일 배열
변수에 3가지가 추가되었습니다.
미사일을 몇개 생성할 것인가를 정하는 "MissileMaxPool"
메모리풀용 객체 "MPool"
이 메모리풀과 연동하여 사용할 오브젝트배열인 "MissileArray"가 추가되었습니다.
// 게임이 종료되면 자동으로 호출되는 함수 private void OnApplicationQuit() { // 메모리 풀을 비웁니다. MPool.Dispose(); }
"OnApplicationQuit"라는 함수입니다.
이 함수는 유니티 엔진에서 미리 정의해놓은 함수로 "게임 종료시에 자동 실행"되는 함수입니다.
게임이 종료되면 메모리를 자동으로 비우도록 합니다.
void Start () { // 처음에 미사일을 발사할 수 있도록 제어변수를 true로 설정 FireState = true; // 메모리 풀을 초기화합니다. MPool = new MemoryPool(); // PlayerMissile을 MissileMaxPool만큼 생성합니다. MPool.Create(PlayerMissile, MissileMaxPool); // 배열도 초기화 합니다.(이때 모든 값은 null이 됩니다.) MissileArray = new GameObject[MissileMaxPool]; }
Start 함수에 3가지가 추가되었습니다.
MPool을 객체화시키는 구절과
MPool을 이용하여 메모리풀에 미사일을 정한 개수만큼 생성하는 구절과
MPool과 연결하여 사용할 미사일배열을 할당하는 구절입니다.
// 미사일 풀에서 발사되지 않은 미사일을 찾아서 발사합니다. for(int i = 0; i < MissileMaxPool; i++) { // 만약 미사일배열[i]가 비어있다면 if (MissileArray[i] == null) { // 메모리풀에서 미사일을 가져온다. MissileArray[i] = MPool.NewItem(); // 해당 미사일의 위치를 미사일 발사지점으로 맞춘다. MissileArray[i].transform.position = MissileLocation.transform.position; // 발사 후에 for문을 바로 빠져나간다. break; } }
미사일을 발사하는 부분에 추가된 for문 입니다.
여기서 미사일 개수만큼 for문이 반복하는데 개수가 10이라고 해서 무작정 10번 반복하는 것이 아닙니다.
0부터 MissileArray가 null인지 판단을 합니다.
그리고 null이라고 한다면 메모리풀에서 미사일 하나를 가져와서 발사지점에 위치하도록 한 다음 활성화를 시켜줍니다.
그리고 바로 for문을 탈출하게 됩니다.
왜 탈출하냐면 한번에 미사일이 1개만 나가기 때문이죠.
(궁금하시면 break을 없애고 해보시지요.)
즉, 위의 for문의 원리는
미사일을 한번 발사한 상태에서는 "MissileArray[0]"이 null값이 아닌 "MPool.NewItem()"으로 활성화된 미사일을 가지게 됩니다.
이 때 미사일을 한번 더 발사하게 되면 i가 0일때 MissileArray[0]값이 null이 아니기에 그대로 한번 지나게 되고, i가 1이 되어 MissileArray[1]이 null값인지 확인하게 됩니다. 이때 MissileArray[1]값은 null이기에 두번째 미사일이 활성화되어 날라가게 됩니다.
null이란? |
현재 변수에 아무것도 들어있지 않은 상태(객체화 혹은 초기화되지 않은 상태)일때 null이라고 합니다. (가리킬 것이 없는 상태) |
// 미사일이 발사될때마다 미사일을 메모리풀로 돌려보내는 것을 체크한다. for(int i = 0; i < MissileMaxPool; i++) { // 만약 미사일[i]가 활성화 되어있다면 if(MissileArray[i]) { // 미사일[i]의 Collider2D가 비활성 되었다면 if(MissileArray[i].GetComponent().enabled == false) { // 다시 Collider2D를 활성화 시키고 MissileArray[i].GetComponent ().enabled = true; // 미사일을 메모리로 돌려보내고 MPool.RemoveItem(MissileArray[i]); // 가리키는 배열의 해당 항목도 null(값 없음)로 만든다. MissileArray[i] = null; } } }
미사일 발사하는 함수에 새롭게 추가된 for문 입니다.
이것은 미사일이 발사될때마다 실행이 되는데요.
현재 미사일이 활성화 되어있을 때 미사일을 메모리풀로 다시 되돌려보내도록 확인하는 함수입니다.
GetComponent<Collider2D>().enabled == ???? |
위의 코드에서 새롭게 추가된 for문의 if문장을 보시면 이상한 문장을 검사하는 것을 볼 수 있습니다. if(MissileArray[i].GetComponent 해석해보자면 "MissileArray[i]의 Collider2D에 해당되는 컴포넌트가 활성화되어 있지 않다면.." 이라는 뜻입니다. GetComponent는 해당 오브젝트에 들어있는 컴포넌트를 불러올 수 있습니다. 위와 같은 Transform, Script, Collider 등이 컴포넌트입니다. |
그런데 왜 Collider2D가 비활성화되면 메모리풀로 돌려보내게 했을까요?
바로 결과의 모호성때문입니다.
왜 Collider2D를 사용하여 메모리풀로 되돌리는 방식을 사용했나요? |
그 이유는 바로 결과의 모호때문입니다. 미사일이 날라갑니다. 그런데 미사일이 무언가와 충돌했습니다. 이때 프로그래머는 "충돌할 때 어떤 미사일이 충돌했는지 확인하여 그 미사일을 메모리풀로 돌려보내야지" 라고 생각을 하고 "OnCollisionEnter 함수"를 이용하여 구현했습니다. (이 함수에 대해서는 나중에 충돌을 구현할 때 설명하겠습니다.) (간단히 말해서 충돌을 판단하는 함수입니다.) 그런데 미사일이 충돌하는 순간 확인해보니 "오브젝트"만 판단이 되어 난감해했습니다. 유니티에서 충돌을 판단하는 함수는 크게 2가지가 있습니다. "OnTriggerEnter", "OnCollisionEnter"가 있습니다. (이 두 함수는 충돌이 처음 시작되는 순간 호출 되는 함수입니다.) 이 때 충돌한 오브젝트의 원본이 무엇인지는 판단이 가능하나, 배열에 담겨있을 때 몇번째 배열에 담겨있는 오브젝트인지는 판단이 불가능 합니다. 그래서 저는 충돌한 오브젝트의 특정 속성을 변경하여 판단하는 방법을 사용한 것입니다. 어떤 오브젝트의 특정 속성이 변경되었다면 그 오브젝트는 충돌했다는 것을 증명해주기 때문이죠. 이때 사용하는 특정 속성은 스크립트적인 작용으로만 변경되는 속성을 선택하는 것이 좋습니다. (즉, Collider2D 이외의 속성을 이용해도 좋습니다.) |
그래서 미사일이 사라져야 할 조건에서 저는 Collider가 비활성화를 시키고, 그걸 캐치해서 미사일을 돌려보내는 방법을 택했습니다.
이제 이 스크립트가 적용된 플레이어 오브젝트로 가서
미사일 개수를 알맞게 정해줍시다.
적용하기) 미사일 수정 |
지난 시간에 만들었던 미사일 오브젝트를 수정해야합니다.
미사일을 끌어옵니다.
그리고 끌어온 미사일에 "Capsule Collider 2D"를 추가합니다.
그럼 위와 같이 미사일 주위에 캡슐이 "초록색"으로 생기게 되는 것을 볼 수 있습니다.
이제 이 크기를 알맞게 맞춰줍니다.
Offset과 Size를 이용하여 알맞게 맞춰준 후, Apply를 눌러 적용합니다.
Collider2D? |
Collider란 골격을 의미하는데요. Collider끼리 부딛히면 이 때를 "충돌"이라고 표현합니다. 즉, Collider가 있으면 인간이 되는 것이고, 없으면 죽은 영혼이라고 보시면 됩니다. 2D는 2D용 프로젝트에서 사용하는 것으로 Z좌표가 없습니다. (0으로 고정) |
그리고 밖으로 꺼낸 미사일을 제거합니다.
확인하기 |
이제 게임을 시작해봅시다.
시작함과 동시에 미사일이 설정한 개수만큼 미리 생성되어 있음을 알 수 있습니다.
이 상태에서 미사일을 발사해봅시다.
미사일이 사라지는 조건인 Y좌표를 일정부분 넘어서면 "Collider"가 비활성화되게 하여 다시 메모리풀로 되돌아가는 것을 확인할 수 있습니다.
어때요, 정말 쉽죠?
다음 시간에는 |
미사일의 충돌, 플레이어의 충돌에 대해서 알아보도록 하겠습니다.
'Study > Unity 5(유니티5)' 카테고리의 다른 글
유니티 5(Unity 5) 왕초보를 위한 간단한 비행기 슈팅게임 만들기(C#) - 10 [적 기체 제거] (4) | 2017.07.10 |
---|---|
유니티 5(Unity 5) 왕초보를 위한 간단한 비행기 슈팅게임 만들기(C#) - 09 [충돌 처리] (0) | 2017.06.03 |
유니티 5(Unity 5) 왕초보를 위한 간단한 비행기 슈팅게임 만들기(C#) - 07 [미사일 발사1] (7) | 2017.03.12 |
유니티 5(Unity 5) 왕초보를 위한 간단한 비행기 슈팅게임 만들기(C#) - 06 [비행기 생성 및 움직이기] (1) | 2017.02.24 |
유니티 5(Unity 5) 왕초보를 위한 간단한 비행기 슈팅게임 만들기(C#) - 05 [게임의 요소 결정 및 구상] (0) | 2017.02.10 |