기능
여러가지 패턴에 대응한 탄막의 활성화와 비활성화.
주요 기술 우선 정리
1. Pool Manager
탄막 오브젝트 풀을 이용하여 Instantiate와 Destroy의 사용을 줄여 성능을 최적화
2. Coroutine
탄막 로직 중 Delay가 필요한 곳에 사용. 불필요한 연산을 줄여 성능 최적화에 좋을 것으로 판단하여 사용.
3. Scriptable Object x 2
몹들의 공격 패턴(페이즈)별 한 개의 파일로 저장.
Gen() 호출 시, 경과 시간 별 호출할 탄막 패턴과 그 상세 내용에 대해 모두 작성.
단순한 몹은 하나의 파일을, 보스몹은 페이즈 당 하나의 파일을 가지게 될 것.
3-add. Custom Editor
조건에 따른 SO 인스펙터의 표시 내용 수정
99. SO를 다른 형식으로 저장하거나 불러올 수 있는 장치 마련. 후순위.
개발 로그
03.07
DanmakuPoolManager에서 기본적인 풀링에 대응한 기능 구현
코드(확장성 추가 고려 필요)
03.08~03.10
지금까지 개발에서 거의 안 쓰던 코루틴 마음껏 남용 중(괜찮은지 모르겠음)
현 시점 PatternSO의 스크립트(테스트작성 단계)
1.
장점
•
2.
구조 작성
•
생성 - 움직임 - 조건부 하위탄막생성 - Pool 반환
◦
위 4단계를 주의하며 트리 구조로 발상을 하여 변수를 작성
3.
개선할 내용
•
성능 최적화
◦
대규모 탄막 데이터가 있는 경우, GetSpawnInfoByPatternName 메소드에서의 선형 검색은 성능 저하를 일으킬 수 있음. 혹시라도 검색이 필요하게 된다면 패턴 이름을 키로 하는 해시맵(Dictionary)을 사용하여 검색 시간을 상수 시간으로 줄일 수 있도록 한다.
4.
잘 모르겠는점
•
상속의 필요성
◦
조건에 따라 여러 갈래로 나뉘는 부분이 있어 상속이 필요할지 생각이 드는데, 상속 자체에 익숙치 않은 부분도 크고, 나뉘는 갈래가 매우 많은 것, SO에서 이를 적용하여 조건에 따라 Inspector에서 표시를 또 할 수 있을지에 대해 느껴지는 장벽이 매우 크기 때문에 당장 적용할 수는 없을 것으로 생각.
•
변수정보를 하위 탄막에 넘길지, 아니면 몹에서 전체 정보를 관리 할 지. 후자를 선택해도 성능 최적화에 도움이 될 것으로 보이지는 않음. 일단 전자로 구현.
03.11
계획
오늘자 목표 - SO 정보를 읽어 구 모양으로 발사하기. 필수요소만 준비하여 시연을 해 보자. Custom Editor 설정은… 후순위에 두고 건드려보기(인스펙터에서 여러 상황에 따라 변수를 보이지 않게 하기)
위 목표를 위해서는, 탄막정보(생성-이동-하위탄막생성(공란)-Pool반환)에 대한 기초적인 변수를 작성하고, 이 정보를 실제로 읽어 호출을 하여 탄막생성이 이루어지도록 해야한다. Linear 모양을 먼저 발사하는 것을 테스트 한 후, 또한 구 모양(Sphere Shape)에 필요한 정보도 입력받을 수 있도록 하여 테스트한다. PatternSO는 개발자가 Inspector를 통해 이름을 지어 케이스를 추가 해 나가며 탄막패턴의 기초 틀을 작성할 수 있는데, 여기에 작성된 내용은 Default값으로 그냥 사용 할 수도 있고, 사용 시(Ex.PhaseSO)에 일부 변수를 커스텀하여 사용할 수 있다.
위 내용을 1차적으로 완료하였다면, PhaseSO를 만드는 것을 다음 목표로 한다. 여기에는 PatternSO에 저장된 프리셋을 여럿 배치하여 몹이 사용하는 패턴 사이클을 작성 할 수 있다.
PhaseSO는 사이클을 돌릴 전체 시간을 지정하여 반복을 하도록 한다. ‘시간’과 Pattern(커스텀 가능)을 쌍으로 하여 여러 아이템들을 사이클시간 내에 배치할 수 있다. 보스몬스터의 경우, 페이즈마다 다른 방식의 공격방식을 사용할 가능성이 있는데, 이럴 경우, 보스에게 부착된 PhaseSO를 교체해주는 방식을 생각중이다.(예시- 남은 체력(60%, 30%…)에 따라 페이즈 전환)
커스텀 에디터를 사용하여 조건부로 변수를 SO 인스펙터에 보여주는 방법은, 이후 과제.
구현 세팅
추후 실사용에서는 tag:Player를 사용하지는 않겠지만, 테스트를 위해서 플레이어 오브젝트를 만들고 플레이어 태그를 통해 위치정보를 알아내도록 세팅하였다.
(12:45) 1차적으로 선형 발사를 위한 PatternSO 변수 준비를 마쳤다.
현재까지의 PatternSO.cs
해당 SO 정보를 읽어들여, Enemy가 SO세팅에 맞춘 공격을 할 수 있도록 해야겠다. (오늘 벌써 두 시간을 소모했다. 시간이 너무 빠름)
Pool에 등록된 탄막의 DanmakuController.cs에서 OnEnable사용에 인해 예견된 문제 발생
아직 관련된 파라미터도 갖고있지 않으면서, 게임시작 시 한 차례 실행을 시도함.
↑ 해당 이슈는 어느샌가 해결되어 보이지 않게 되었다.
03.12
못다한 탄막 생성
누가봐도 최적화 문제 + 왠지 씬에 남아있는 투명한 탄막 + 뭔가 시간이 지날수록 점점 무거워지는 게 무엇인가가 메모리에 누적되어가는 것 같은 문제
탄막의 렌더러를 없애도 비슷한 타이밍에 fps가 나락가는 걸 보면, 정말 단순하게 연산쪽이 무거워서 그런 것으로 보임.(그럴 만 함…)
버그 픽스를 위해
•
오브젝트 풀에서 초기화 또는 반환 도중 에러가 난 아이들
원점에 돌아갔지만, 움직이지 않고 Update문을 반복하며 오류를 뿜는다.
◦
초기화 오류인가, 반환 오류인가?
수동으로 반환 메서드를 실행하면 잘 되며, 여러가지 초기화 해 주어야 할 값이 모두 null인 것으로 보아, 초기화가 제대로 이루어지지 않은 것으로 보인다.
즉 DanmakuGenerator에서 미스가 있다.
var danmakuGo = DanmakuPoolManager.instance.GetGo(settings.danmakuPrefab.name);
if (danmakuGo != null)
{
// 또 다른 오류메시지의 위치
Vector3 initPosition = masterTransform.position;
...
C#
복사
아무래도 로직 도중 masterObject가 소실되어 Pool에서의 탄막 생성 후 이후 로직이 모두 무산되고 danmakuController.Initialize도 실행되지 않은 것으로 보인다.
해당 로직을 try문으로 감싸, 초기화가 제대로 이루어지지 않을 경우 바로 Pool에 반환을 할 수 있도록 해야겠다.
var danmakuGo = DanmakuPoolManager.instance.GetGo(settings.danmakuPrefab.name);
try
{
if (danmakuGo != null)
{
Vector3 initPosition = masterTransform.position; // 마스터의 위치를 기본값으로
// ... 기타 로직 및 탄막 파라미터들 초기화
}
}
catch
{
Pool.Release(danmakuGo);
}
C#
복사
해결되지 않았다.
아직 활성화 직전인 오브젝트를 반환 할 수 없는 게 이유인 것으로 보인다.
새로운 탄막 오브젝트를 Pool에서 꺼내오기 전에 masterObject의 상태를 체크하여 코루틴을 종료 할 수 있도록 해 보았다.
// masterObject의 상태 확인
if (masterObject == null || !masterObject.activeInHierarchy)
{
yield break; // masterObject가 비활성화되거나 파괴되면 코루틴 중단
}
var danmakuGo = DanmakuPoolManager.instance.GetGo(settings.danmakuPrefab.name);
C#
복사
해결 된 듯 보여 탄막 오브젝트를 1.6^4 = 6.6배정도 증량하여 더 부하를 줘 보았다.
FPS가 1.0 이하로 떨어지고 탄막의 반환이 살짝 늦을지언정 더 이상 Pool 버그는 일어나지 않았다.
참고로 위 영상에서 최대 탄막수는 4096개였다. Pool의 크기는 100였기 때문에, 사실상 Instantiate와 Destroy가 커버를 친 셈.
번외로 Pool의 크기를 4100으로 하여 FPS를 확인 해 보자. 버그를 찾으며 프로파일러를 확인하면서, Destory 등이 CPU성능이 90프로 이상인 시점이 있었던 것도 생각하면 왠지 FPS 저하가 플레이에 지장이 없을 정도가 될 것이라고도 생각된다.
워메… 아니었다. 프로파일러를 켜고 다시 확인 해 보자.
일단 랜더러쪽은 전혀 문제가 되지 않았고, 정말 그냥 Pool 반환을 위한 Deactive 량이 너무 많은 것이 원인이었다.(1.6초에 4000회) 그런데 이 반환 처리가 생성파괴와 성능차이가 크게 나지 않는다는 건 생각보다 쇼크.
생명주기 시간을 좀 늘려 반환이 이루어지지 않는 시점에도 FPS가 많이 떨어지는지 확인 해 보아야겠다. 실제 게임에서는 반환이 이렇게 짧은 시간에 대량으로 몰려있지는 않을 것으로 예상되기 때문에.
일단 랜더러쪽은 전혀 문제가 되지 않았고, 정말 그냥 Pool 반환을 위한 Deactive 량이 너무 많은 것이 원인이었다.(1.6초에 4000회) 그런데 이 반환 처리가 생성파괴와 성능차이가 크게 나지 않는다는 건 생각보다 쇼크.
생각 해 보니 하이에라키 등 게임 씬 이외의 요소가 성능저하의 문제가 될 가능성이 있어, Play Maximized 모드에서 추가로 확인을 해 보았다.
생명주기 시간을 좀 늘려 반환이 이루어지지 않는 시점에도 FPS가 많이 떨어지는지 확인 해 보아야겠다. 실제 게임에서는 반환이 이렇게 짧은 시간에 대량으로 몰려있지는 않을 것으로 예상되기 때문에.
생성순간이 13 FPS, 이후 18 FPS부터 시작하여 카메라에서 탄막이 사라지면서(낮아지는 Batches 수) 30 FPS 이상까지 회복한다. Play Maximized 모드에서 대략 1.5배정도의 성능을 보였다.
보스몬스터가 탄막을 무척 남발하도록 하고 싶은데, 이런 상태라면 좀 어려울 것으로 생각된다. 최적화가 필요한 부분이다.
다만, 현재 최우선순위는 패턴의 다양화이고, 이후 탄막의 네온 효과, 카메라에 포스트 프로세서등을 달아 어느 정도의 최적화가 더 필요한지 확인하여 견적을 잡아야겠다.
그리고, 탄막 생성삭제가 없는 시점(이동만을 할 때), CPU지분을 살펴보면
▪
렌더링 : 18%
▪
Script-Update(Move) : 33%
▪
Physics 20.5% 중 콜라이더(탄막당 구형 한 개) 연산 16.3%
나머지는 무시해도 괜찮을 수준이다.
연산 경량화 전략 구상
•
탄막의 Move와 Move변화에 관해서는 다른 오브젝트와 엮지 않고 탄막오브젝트에 맡기는 것이 베스트라고 생각. (Move변화는 아직 구현하지 않았지만, 특정시간에 크게 바뀌는 내용은 코루틴을 적극 활용하도록 하자. 위의 CPU 지분 결과를 보면 앞으로 Move 파트의 연산이 더 늘어날 것 같은데 걱정되는 부분이다.
•
VRChat 동방맵 레퍼런스처럼, 플레이어에게 2D의 스프라이트의 탄막이 보이도록 하는 방법도 존재한다. CPU의 랜더링 지분을 낮출 수 있을 듯 하다. 나중에 고려해봐도 좋을 사항.
•
탄막 정보를 전달하는 과정에서 참조를 더 활용하여 자원을 아낄 수 있을 것 같기도 하다. 아직 너무 추상적인 생각이기 때문에 필요성이 느껴진다면 구체화가 필요하다.
끝
번외 TIL(기능명세와 관련 없는 내용 포함)
이미 반환한 오브젝트를 또 반환하려고 하면 에러 표시
// 탄막위치가 y > 5가 될 경우 삭제
if (this.transform.position.y > 5)
{
// 오브젝트 풀에 반환
Pool.Release(this.gameObject);
Pool.Release(this.gameObject); // 이미 없는 오브젝트를 반환하려고 할 시, 에러 발생.
Pool.Release(this.gameObject);
Pool.Release(this.gameObject);
Pool.Release(this.gameObject);
}
C#
복사
Invoke와 코루틴 성능차이
•
어느쪽이든 사용해도 괜찮다면, 적게나마 코루틴이 성능적으로 앞섬.
int와 float의 차이(기술면접 참고)
코루틴 사용 시 new 반복사용 x
Awake, Start 대신 OnEnable 사용
setActive로 재사용할 경우에도, Active 될 때 마다 구문을 사용하도록 할 수 있음
탄막 여러가지 정보 초기화…
랜덤 Vector
// 반지름이 1인 원 안에서 위치를 Vector2로 가져옴
Vector2 randCirclePos = Random.insideUnitCircle;
// 반지름이 1인 구 안에서 위치를 Vector3로 가져옴
Vector3 randSpherePos = Random.insideUnitSphere;
// 반지름이 1인 구 위의 점 위치를 Vector3로 가져옴
Vector3 randOnSpherePos = Random.onUnitSphere;
C#
복사