유니티에 대해 공부를 하다보면 에셋 번들에 대한 내용을 드문드문 접할 수 있다.

나 또한 에셋 번들에 관한 얘기를 지나가면서 몇 번 듣기는 했는데... 에셋 번들이 뭔지도 모르고 있었다.

지금으로부터 약 1달 전, 메모리 관리를 하며 에셋의 복제를 확인했고, 그 원인이 에셋 번들에 있음을 인지한 이후로 많은 공부를 하였다.

지금은 기존의 BuildPipeline을 Scriptable Build Pipeline으로 업그레이드를 했고, Shader Stripping에 문제가 있어서 해당 부분에 대한 원인을 찾아보고 있는 중이다.

아무튼, 이제는 에셋 번들에 대해 조금 알게된 것 같아 에셋 번들이란 무엇인지를 정리해보고자 한다.

 

모바일 게임을 설치하고 실행하면 이런 화면을 보게 될 것이다.

여기서 다운로드 받는 것이 에셋 번들이다.

 

원스토어에서 블루 아카이브의 용량은 143MB이지만, 실제 핸드폰에서 차지하는 게임 용량은 7.57GB로 굉장한 괴리감이 있다. 

스토어에서 받은 용량을 제외한 나머지 용량은 전부 에셋 번들을 다운로드 받은 것이라고 보면 된다.

 

그러면, 에셋 번들을 사용하는 이유는 무엇일까?

 

 

첫 번째로는 스토어에서 용량을 작게 유지하기 위해서이다.

 

구글 플레이 스토어의 경우, 앱의 크기를 150MB로 제한을 해놨기 때문에 울며 겨자먹기로 이 용량보다 작게 유지해야 한다.

 

앱스토어 버전은 용량 제한이 500MB라 그런지 464.4MB이고, 전체적으로 다른 앱들도 용량이 크다. (스토어를 통해 받으면 돈이 안 나가니까 비용 절감에 도움이 된다)

 

그리고 앱의 용량이 작을수록 다운로드 수가 늘어난다고 한다.

 

아마 나같아도 요렇게 되어있으면 별로 안 받고 싶어질 것 같다.

 

가끔 위 사진처럼 플레이 스토어에도 150MB를 넘는 경우가 있는데, PAD(Play Asset Delivery)라는 기능을 이용한 것이다. 앱은 따로 받고, 에셋을 따로 올려서 앱을 다운로드 받을 때 최대 1GB까지 같이 받게 만들 수 있다. 후술하겠지만 여기에 올릴 에셋은 LZ4로 압축하는 것이 좋다.

 

 

에셋 번들을 사용하는 두 번째 이유는, 게임 내 리소스들의 유동성을 확보할 수 있다는 점이다.

 

앱을 빌드하면 스토어에 바로 올릴 수 있는 것이 아니다. 스토어에 앱을 올릴 때는 스토어의 검수 과정을 거치는데, 거절을 당할 수도 있고, 거절당한 이유를 알 수 없는 경우도 있어서 예상한 것보다 시간이 많이 걸릴 수 있다. 매 번 유저에게 스토어에서 앱을 다운로드 받으라고 하는 것도 바람직하지 못하다. 에셋 번들을 사용하지 않는 경우 게임 내 이미지 1개를 바꾸는 데에도 이런 작업이 필요하다는 점에서 굉장히 비효율적이라고 할 수 있겠다. 데이터가 잘못 들어가서 버그가 났는데, 서버를 내리고, 앱을 다시 빌드해서 검수를 통과하여 며칠이 지난 뒤에 서버가 열린다? 생각만 해도 끔찍한 일이다.

 

반대로 에셋 번들을 사용한다면, 앱을 빌드하고 심사할 필요 없이 에셋 번들만 따로 빌드해서 업로드를 하고, 유저가 접속할 때 변경사항을 체크하여 바뀐 부분만 다운로드를 받으면 바로 게임에 적용할 수가 있다. 종종 게임에 접속할 때 점검도 없었는데 갑자기 작은 용량의 데이터를 받는 경우가 있을텐데, 그게 바로 이런 경우라고 할 수 있겠다. 이 경우, 버그가 터져도 에셋 번들을 빌드할 몇 시간만 서버를 내렸다가 다시 오픈하면 문제를 해결할 수 있다.

 

버그 뿐만 아니라 게임 내에서 테이블을 통해 데이터를 읽어오고 적용하도록 만들면, 신규 아이템이나 캐릭터 등을 추가할 때도 단순히 데이터 패치를 통해서 적용할 수 있다.

 

 

위 두 가지 이유로 인해서 모바일 게임을 만든다면 에셋 번들을 쓰는 것은 선택이 아니라 필수라고 할 수 있을 것이다.

 

 

하지만 에셋 번들의 단점이 있는데, 에셋 번들을 사용하는 시스템을 구축하는 것이 굉장히 복잡하다는 것이다.

 

에셋 번들을 사용하려면 위와 같은 시스템을 구축해야 한다.

 

먼저 에셋 번들의 빌드는 BuildPipeline이나 Scriptable Pipeline의 ContentPipeline 등을 사용하여 에셋 번들을 빌드할 수도 있고, 유니티에서 많은 부분을 개선하여 낸 Addressable을 사용할 수도 있다.

 

*BuildPipeline이나 ContentPipeline을 사용하면 굉장히 번거로운 처리들을 직접 해줘야 하기 때문에 신규로 에셋 번들을 사용할 준비를 하고 있다면 이런 부분들의 일부를 알아서 처리해주는 Addressable을 사용하는 것을 권장한다.

 

 

그 다음은 에셋 번들을 다운로드 받을 수 있도록 업로드를 해야한다. 보통은 aws를 사용할텐데, 1GB에 대략 30원 정도의 비용이 나간다. 용량이 작다면 위에 언급한 PAD나 ODR(On Demand Resources)을 활용할 수도 있겠다.

 

 

그 다음은 유저가 모바일 기기에서 에셋 번들을 다운로드하는 부분을 구축해야 한다. 에셋 번들마다 버전을 관리하고, Hash를 이용해서 변경된 에셋 번들만을 받을 수 있도록 해야한다. 유니티로 한다면 UnityWebRequest, DownloadHandlerAssetBundle을 사용하게 될 것이다.

DownloadHandler 생성 - Unity 매뉴얼 (unity3d.com)

 

DownloadHandler 생성 - Unity 매뉴얼

다음과 같은 몇 가지 DownloadHandler 타입이 있습니다.

docs.unity3d.com

 

마지막으로는 에셋 번들을 로드하는 과정을 거쳐야 한다. 에셋 번들의 Dependencies를 읽어와서 에셋을 로드하기 전에 필요한 다른 에셋들을 먼저 로드해야 리소스가 깨지지않고 정상적으로 보이게 된다. 이런 코드를 직접 만든다면 순환 참조를 하지 않도록 막아주는 것이 필요하다.

 

정말 마지막으로는 로드한 에셋을 관리하는 것인데, 더 이상 사용하지 않는 에셋들을 해제해준다거나 하는 부분이다. 다만 에셋의 복제나 리소스가 깨진다든지 등의 문제가 생길 가능성이 많다. Addressable은 사용하지 않는 에셋 번들을 자동으로 메모리에서 해제해준다고 하니 신경을 적게 써도 된다. Dependencies로 알아서 체크해준다는 것 같고. 이외에도 위에 언급한 내용 중 일부분을 알아서 처리해주기도 하니 새로 구축한다면 반드시! 꼭! Addressable을 사용하도록 하자!

 

 

에셋 번들을 빌드할 때는 압축을 하게 되는데 3가지 방식이 있다.

 

1. 압축을 아예 하지 않는 방법

압축을 하지 않아서 빌드가 빠르겠지만, 용량이 굉장히 커지기 때문에 권장하지 않는 방식이다.

 

Bug - Addressables - Extremely slow load time - Unity Forum

It seems that LZ4 makes that first load faster than Uncompressed

추가로, 위 글에서 Uncompressed보다 LZ4가 첫 로딩 속도가 빠른 것 같다는 내용이 있는데, Unity에서 저장하는 방식을 LZ4로 해놔서 아마 실행할 때 Uncompressed를 LZ4로 하여 디스크에 저장하는 과정이 있는 것이 아닐까 싶다. 결국 Uncompressed는 꼭 필요한 것이 아니라면 권장하는 방식이 아니라고 생각된다.

 

2. LZMA

용량을 가장 작게 줄일 수 있는 압축 방식으로, zip이나 7z 등도 해당 방식을 사용한다. 단, 압축률이 높은 만큼 데이터를 불러오는 속도가 느리다. 설상가상으로 에셋 중 1개만 필요하더라도 에셋 번들 전체를 압축 해제해야 하기 때문에, 이 방식으로는 사용하기 어렵다. 하지만, 유니티에서 앱을 실행할 때, 혹은 다운로드를 받는 도중에 바로 다음에 쓸 LZ4로 Recompress를 해서 디스크에 저장하기 때문에 이런 걱정은 할 필요가 없다. 단, 주의해야할 점은, aws 등을 사용해서 다운로드를 받게 한다면 다운로드와 Recompress가 동시에 일어나기 때문에 유저가 다운로드를 받으면서 자연스럽게 재압축을 할 수 있지만, PAD 등의 기능을 사용하면 LZMA를 받아서 앱을 처음 실행할 때 통째로 LZ4로 Recompress를 하기 때문에 앱을 처음에 실행할 때 굉장히 오래 걸리게 된다. 떄문에 이런 기능을 사용할 것이라면 LZ4로 압축하는 것이 좋다. 단, 후술하겠지만 BuildPipeline을 사용해서 LZ4와 LZMA로 따로 빌드를 한다면 Dependencies를 제대로 찾을 수 없기 때문에 에셋 중복이 일어날 수 있다는 점을 주의해야 한다.

 

3. LZ4

LZ4는 위에서 말한 것 처럼 압축률은 LZMA보다 낮지만, 데이터를 로딩하는 속도가 빠르고 청크 단위로 로딩이 가능하기 때문에, 필요한 에셋만 압축을 해제하여 사용할 수 있다. LZMA로 압축해도 유니티가 LZ4로 재압축을 해주기 때문에 대부분의 경우 LZMA로 압축하는 것이 유저의 다운로드 용량을 줄여주면서 비용도 절감할 수 있는 방법이다.

 

참고로, 파일에 따라 다르겠지만, 일반적으로 LZ4대신 LZMA를 사용하면 약 30%정도 용량을 더 줄일 수 있다는 것 같다. (기억에 의존한 내용)

 

에셋 번들 압축 - Unity 매뉴얼 (unity3d.com)

 

에셋 번들 압축 - Unity 매뉴얼

기본적으로 Unity는 LZMA 압축을 사용하여 에셋 번들을 생성하고, LZ4 압축을 사용하여 캐싱합니다. 이 섹션에서는 이 두 가지 압축 포맷에 대해 설명합니다.

docs.unity3d.com

자세한 내용은 위 링크를 참조하시라.

 

 

 

또, 에셋 번들을 사용하면 에셋이 중복되는 문제가 발생할 수 있다.

이는 메모리 프로파일러를 통해서 확인할 수 있다.

 

글이 길어져서 에셋의 중복에 대한 이야기는 끊고 다음 글에서 마저 다루도록 하겠다.

나는 노래를 듣는 것을 좋아하고, 종종 노래방에 다니며 부르는 것도 즐긴다.

그러면서 작곡에 대해서 관심은 있었지만, 내가 별로 할 수 있을 것 같지는 않아서 늘 미뤄왔는데, 보컬로이드 작곡 관련한 유튜브 영상이 가끔 쇼츠에 올라와서 접하다보니 한 번 사용해보면 재미있을 것 같다는 생각이 들어서 바로 시작해보았다.

 

보컬로이드하면 하츠네 미쿠가 굉장히 유명한데, 나도 옛날부터 하츠네 미쿠의 곡들을 많이 들어왔고 지금도 많이 듣고 있기 때문에 하츠네 미쿠의 목소리를 활용해볼 수 있으면 좋겠다고 생각했다.

하츠네 미쿠에는 여러 버전이 있는데, 최근 버전 중 가장 호환성도 좋고 발음이나 평가도 좋은 V4X를 사용해보기로 했다.

 

구글에 검색해보니 공식 사이트가 나왔다.

 

HATSUNE MIKU V4X BUNDLE | SONICWIRE

 

HATSUNE MIKU V4X BUNDLE(初音ミク V4X バンドル) | SONICWIRE

もっと使いやすく、綺麗な歌声で。音楽づくりの全てが揃った、初音ミクの日本語&英語ライブラリー同梱版。

sonicwire.com

 

일본어만 가능한 버전이 17,600엔, 영어까지 지원되는 버전은 22,000엔이다.

첫 39일간 무료로 데모판을 사용해볼 수 있어서, 데모판을 사용해보기로 했다.

생각보다 시작이 굉장히 간단한데, 가입만 하고 결제 정보를 등록할 필요도 없이 다운로드 링크로 다운로드를 하고, 하츠네 미쿠의 프로그램과 이를 사용해 곡을 만들 수 있는 Piapro Studio라는 프로그램을 설치한 후 로그인을 하기만 하면 된다.

 

 

단, 이 상태에서는 바로 사용할 수 없는 것 같고, Studio One 이라고 하는 프로그램을 깔아야 하는 것 같다.

 

piapro studio launch 라고 검색하면 맨 위에 이런 링크가 뜬다.

Let’s launch Piapro Studio (Presonus Studio One 5) | Piapro Studio Official Website

 

Let’s launch Piapro Studio (Presonus Studio One 5) | Piapro Studio Official Website

1.Launch the application of Studio One. 2.Click the “create a new song” button. 3.The setting screen of a new song appears then click the “OK” button. 4. 4.The project launches then open the “instruments” from the brows at the right of the scre

piaprostudio.com

 

지금은 Studio One 6 버전이 최신 버전이라 6으로 깔았는데, 5와 UI가 크게 다르지는 않은 것 같다.

이것도 마찬가지로 처음 30일간은 체험판으로 사용이 가능하고, 이후에는 무료 버전으로 사용하거나 돈을 내고 업그레이드를 할 수 있는 것 같다. 하츠네 미쿠 V4X를 사면 업그레이드 버전 키를 준다는 것 같은데, 아직 구매하진 않아서 확실하게는 모르겠다.

 

Studio One을 처음 켜면 이것저것 설치하라고 하는데, 그것들을 설치해주면 메인 화면이 나타난다.

 

메인 화면에서 좌측 상단의 새로 작성을 누르고, 레코딩과 믹싱 그대로 확인을 눌러주면 새로운 프로젝트가 생성된다.

 

처음에는 이런 화면인데, 위의 링크를 따라해주면 된다. 오른쪽의 악기를 눌러준다.

 

이후에 Crypton이라는 폴더 내의 Piapro Studio VSTi라는 악기를 드래그해서 옆으로 꺼내주면 된다.

 

그러면 일단 Studio One에는 이런 팝업이 뜨고, piapro studio가 실행된다.

이 팝업은 좌측의 악기 목록 오른쪽 위에 있는 주황색 건반을 눌러 껐다 켰다 할 수 있다.

팝업 가운데에 있는 piapro studio를 눌러주면 piapro studio가 열린다.

 

piapro studio는 처음 악기를 등록하면 열리기도 한다.

여기도 마찬가지로 처음에는 텅 비어있다.

 

왼쪽의 +를 눌러주면 하츠네 미쿠가 추가된다.

다른 보컬로이드도 설치되어 있으면 우클릭 -> Select Singer로 바꿔줄 수도 있는 것 같다.

 

빨간색으로 표시된, 트랙 영역을 더블클릭하면...

 

이렇게 쫘악 펼쳐지면서 작곡을 할 수 있게 된다.

다시 트랙을 더블클릭하면 위 사진처럼 작아진다.

 

상단의 메뉴 바에서 도구를 골라서 작곡을 시작할 수 있다.

사용해보면 알겠지만 대략 선택할 수 있는 마우스 커서 도구나, 범위 지정 가능한 Range 도구, 그 옆 옆 연필같이 생긴 것이 Pen Tool이다.

 

펜 툴을 눌러서 드래그하면 악보를 그릴 수 있는데, 처음에는 이런 식으로 청록색의 영역을 지정해주어야 한다. 아마 이 영역 안에 악보를 그릴 거야... 정도의 표시쯤 될 것 같다. 영역 확장도 가능하고, 추가로 만들 수도 있으니 적당히 만들어도 괜찮다.

 

이제 원하는 음계에 맞는 부분에 펜 툴을 갖다대고 클릭 후 드래그를 하면 이렇게 그려진다.

그려질 때 미쿠 목소리가 나온다. ㅋㅋ. 좌측에 건반을 누르면 어떤 소리가 나올지 들어볼 수 있다.

 

적당히 찍어보고 좌측 하단의 메뉴를 누르면 재생해볼 수 있다.

처음 재생해보면 신기하기 그지 없다. 듣기만 하던 미쿠 목소리를 직접 만들어볼 수 있다니...

 

다만 이후에는 작곡하는 법을 몰라서 떴다 떴다 비행기를 찍어보는 나 자신을 발견할 수 있었다.

 

지난 주 일요일 저녁에 처음 다운로드를 받아서 사용해봤기 때문에 아직 자세한 것은 모르지만, 약간의 팁은 있다.

 

1. 찍어놓은 노트를 길게 눌러 드래그해서 상하좌우로 움직일 수 있다.

2. 노트의 좌우에 마우스를 갖다대면 길이를 조절할 수 있는데, 항상 뜨는 것은 아니다. 안 뜰 때는 Alt키를 눌러주면 뜨는 것 같다.

3. 상단의 1/8 부분을 변경해서 촘촘하게 바꿀 수도 있고, 우측 하단의 -/+를 눌러서 악보를 축소/확대하여 조금 더 상세히 노트를 찍을 수 있다.

4. 노트를 더블클릭하면 가사를 쓸 수 있는데, 하나의 노트에 1개의 발음만 넣을 수 있다. 한자 말고 히라가나를 써야하는 것 같고, わたしは라고 쓰면 watasiha 라고 발음을 하기 때문에 정직하게 わたしわ라고 적어줘야 한다. 여러 단어를 쓰면 뒤의 노트들까지 발음이 한꺼번에 들어간다. 요음은 한 번에 발음하는 것 같은데, 촉음은 설정이 있는 것 같다.

5. 하단의 꾸물거리는 건 바이브레이션인데, 좌우로 길이를 늘릴 수도 있고 더블클릭으로 바이브레이션의 스타일을 바꿀 수도 있다. 이외에도 Singing Style 등을 바꿀 수도 있는데 아직 잘 활용해보지는 못했다.

 

Studio One으로 돌아가보면 트랙 부분이 비어있는데, 재생하면 piapro studio과 같이 재생이 되어서 노래는 정상적으로 들린다. 악기 등은 Studio One에서 넣어줘야하는 것 같다.

이번에는 우측에서 Presence라는 악기를 드래그해주겠다.

 

그러면 이번에는 이런 모양의 팝업이 나타난다.

 

좌측 상단의 기본값을 눌러서 포함된 다양한 악기들을 변경하여 사용할 수 있다.

 

이후에 팝업에 있는 다양한 설정들을 이용해서 세팅을 해주면 된다. 하단의 건반을 눌러서 어떤 소리가 나는지 확인해볼 수도 있다.

 

악기를 골랐으면 창을 닫고 우측 하단의 편집을 눌러주면 이런 창이 다시 뜬다.

아까와 똑같이 펜 툴로 영역을 지정해주고, 안에서 악보를 찍으면 노래를 만들 수 있다.

 

이런 식으로 적당히 노트를 찍고...

악기를 더 넣고 싶으면 한 번 더 우측의 악기를 드래그해서 악기를 추가하고, 똑같이 노트를 찍어주면 된다.

 

음악 파일로 만들려면, 상단의 빨간 부분에 있는 저것을 드래그해줘서 영역을 지정해줘야 한다.

 

이런 식으로 전체 트랙의 영역을 지정해줘야 음악 파일로 만들어준다.

 

그 다음 Song의 믹스다운 익스포트를 눌러주면 창이 뜬다.

 

대충 경로 지정, 이름 정하고 확인 버튼 누르면 음악 파일이 나온다. 상세한 설정은 아직 잘 모르겠다.

 

다음은 처음에 만든 곡들의 버전과, 오늘까지 만든 노래를 올리겠다.

 

처음 만든 곡은 비행기이다. 떴다 떴다 비행기를 찍으며 연습을 해봤다. 가사도 적당히 넣고... 박자도 안 맞아서 어찌저찌 맞춰봤는데 여전히 조금 박자가 이상한 부분이 있다. 딱 미쿠 목소리만 있는 버전이다.

 

위의 비행기에서 굉장히 단순한 드럼(Basic Drum)을 추가한 버전이다.

 

위의 버전에서 드럼을 조금 더 디테일하게 만들어 본 버전이다.

 

유튜브에서 작곡하는 법을 검색해서 화성악이 어쩌구... 하는 영상을 보고 Guitar&Flute를 이용하여 대충 멜로디를 찍어본 버전이다.

 

멜로디가 조금 별로인 것 같아서 Organ으로 화음만 넣어본 버전이다.

 

뭔가 하나씩 추가하면서 노래다워지는 것이 상당히 신기하고, 또 재밌었다.

다만 정말 아쉬운 점은 음악에 대한 지식이 많이 부족해서 음을 하나 찍는 것도 굉장히 쉽지 않다는 점이었다. 미쿠 목소리를 찍는 데에만 1시간, 드럼은 30분? Flute도 1시간, 오르간은 30분... 이런 식으로 걸린 것 같다.

 

 

이후 월요일에 어떤 음이 좋은 음일까 감이 안와서 피아노가 있으면 좋겠다고 생각했는데, 집에 와서 인터넷에 검색해보니 웹에서 키보드로 피아노를 칠 수 있는 사이트가 있었다.

 

가상 피아노 | Musicca

 

가상 피아노 | Musicca

음악 선생님과 학생들을 위한 가상 피아노. 음이름, 음정, 화음, 음계를 시각화하고 컴퓨터 키보드로 피아노를 연주해보세요.

www.musicca.com

초등학교 저학년 때 피아노를 잠깐 쳤었는데, 이 사이트에서 오랜만에 피아노를 치면서 굉장히 단순하면서 괜찮은 멜로디를 치게 되어서 해당 멜로디로 노래를 만들어보았다.

 

처음엔 피아노(Grand Piano)만 넣고, 이후에 미쿠 목소리를 넣었다. 괜히 어줍잖은 멜로디로 노래를 만드니까 너무 별로라서 그냥 미 레 도... 단순하게 내려가게 만들었다. 현재 수준에서는 오히려 이게 더 듣기가 좋았다.

그리고 피아노 소리가 조금 큰 것 같아 Velocity를 조절해줬더니 굉장히 잔잔해져서 듣기가 좋았다. Velocity는 기본을 -50%, Punch를 30%를 주었고, 하이라이트 부분에 맞춰서 Velocity를 적절하게 수정해주면서 나름 노래다워졌다.

 

거기에 Bassoon을 추가했는데, 마찬가지로 소리가 커서 Velocity를 -50%, Punch를 20% 주었고, 간단한 화음만 넣어주었다.

 

반주는 도미솔... 도파... 도미... 도레... 이것의 반복이고 미쿠도 미... 레... 도... 반복이라... 굉장히 허접하긴 하지만.

50초짜리 곡이지만 하지만... 생각보다 들을만 하게 곡이 나온 것 같아서 꽤나 기쁘다.

 

최근에는 노래를 들을 때 이 노래들은 작곡을 어떻게 했을까... 염두에 두면서 듣는데, 덕분에 이 노래에는 이런 악기가 사용됐었구나... 하는 것을 느낄 수 있게 되었다. 그리고 출퇴근을 지하철로 하는데 소음이 심한데다 큰 소리로 듣는 것이 귀에 안 좋을 것 같아서 노이즈 캔슬링 헤드셋도 구매하였다.

 

어떤 노래들은 대부분의 파트가 반복되어서 이 노래는 굉장히 날로 먹은 노래군... 하는 생각도 들고, 어떤 노래는 반복되는 부분이 거의 없이 늘 새롭게 나와서 와... 이 노래를 만드는데 굉장한 노력이 들어갔겠군... 하는 생각도 든다.

좋은 악기를 들으면 이 악기는 무슨 악기일까 생각해보기도 하고. 나도 써보고 싶다는 생각을 하기도 한다.

 

평소에는 그냥 단순히 멜로디를 즐기거나 하는 느낌으로 들었는데, 음악을 조금 더 다채롭게 즐길 수 있게 된 것 같아서 굉장히 기분이 좋다. 화성학이나... 음악 관련해서 공부할 것은 많아서 조금은 힘들기도 하지만. 새로운 것들을 배워가는 것도 재밌는 것 같다.

 

========================================================================

 

최근에 에셋 번들 문제점을 해결하려고 Scriptable Build Pipeline으로 교체를 하고 있는데, 어려움이 많다. 다른 부분은 다 끝났는데 Shader Stripping이 조금 이상한 것 같아서... 많은 노력을 기울이고 있다. 그리고 다음 주 스터디에 준비해 갈 내용은 Addressable이다. 배울 내용도 많고... 많이 힘들긴 하지만. 이제 입사한지 딱 2개월이 되었는데, 덕분에 지식이 굉장히 많이 늘고 있어서 입사 이전처럼 정말 눈에 띄게 성장을 하고 있는 것 같다. 여태 에셋 번들을 다루는 과정 전반을 전체적으로 한 번씩은 훑어보고, 일부분을 고치기도 하면서 전반적인 내용들은 이해를 한 것 같다. 바로 만들라고 하면 만들지는 못 하겠지만... 시간만 있으면 여기저기 찾아보면서 직접 에셋 번들을 구축해볼 수도 있을 것 같다. 아마 이번 주 주말에 Addressable에 대한 공부를 하면서 에셋 번들에 대한 내용들을 블로그에 적어볼 것 같다. 현재 목표는... 일단 Shader 문제를 해결하고(가능하다면) Addressable로 우리가 사용하는 에셋 번들을 한 번 더 업그레이드 해볼 수 있으면 좋을 것 같다. 그리고 Jenkins를 활용하여 빌드 시스템을 관리하는 부분도 배우고 싶다.

 

잠시 딴 이야기로 빠졌는데, 최근 머리를 많이 쓰다보니까 힘들기도 하지만 이렇게 새로운 취미가 생겨서 또 즐겁게 잘 지낼 수 있는 것 같다. 앞으로 꾸준히 공부하면서 음악을 만들면서 언젠가 내 마음에 쏙 드는, 다른 사람들에게도 들려주고 싶은 노래를 만들 수 있으면 좋을 것 같다.

함수를 호출하는 오버헤드에 따른 성능 차이를 알고 싶어서 Vector3의 Normalize 방법 별 성능 테스트를 해보았다.

 

이전에도 Vector3.Normalize()와 Vector3.normalized에 대한 차이는 해봤는데 그것과 더불어 Vector3.Normalize() 내부에 있는 함수를 꺼내와서 직접 사용하면 얼마나 차이가 있을지 비교해봤다.

 

using System;
using UnityEngine;

public partial class Tester : MonoBehaviour
{
    private void OverheadTest()
    {
        Vector3 vector3 = new Vector3(2, 1, 1);
        Vector3 vector3Temp = new Vector3();

        DoTest("Vector3.normalized Test", repeatTime => {
            for (int n = 0; n < repeatTime; ++n)
            {
                vector3Temp = vector3.normalized;
            }
        });

        DoTest("Vector3.Normalize() Test", repeatTime => {
            for (int n = 0; n < repeatTime; ++n)
            {
                vector3Temp = Vector3.Normalize(vector3);
            }
        });

        DoTest("Vector3.Normalize() disassemble Test", repeatTime => {
            for (int n = 0; n < repeatTime; ++n)
            {
                float num = (float)Math.Sqrt(vector3.x * vector3.x + vector3.y * vector3.y + vector3.z * vector3.z);
                if (num > 1E-05f)
                {
                    vector3Temp = vector3 / num;
                }
                else
                {
                    vector3Temp = Vector3.zero;
                }
            }
        });
    }
}

테스트 코드는 위와 같다.

 

Vector3.normalized는 내부적으로 Vector3.Normalized()를 호출하기 때문에 함수 호출이 1번 더 발생된다.

 

Vector3.Normalize()는 위와 같이 작동하는데, 이 부분을 빼와서 돌렸다.

Math.Sqrt는 "Math.Sqrt() translates to a single floating point machine code instruction." 이라고 하여 이대로 사용하는 게 빠를 것 같아서 이대로 두었다.

 

10억번씩 5회 돌려봤고, Vector3.normalized를 사용하는 경우 143612ms, Vector3.Normalized()를 사용하는 경우 120142ms, Vector3.Normalized() 내부를 꺼내와서 사용한 경우 78946ms가 소요되었다.

 

Vector3.Normalized()를 사용하면 Vector3.normalized를 사용하는 것보다 16.3%정도 빠르다.

Vector3.Normalized() 내부를 꺼내와서 사용하면 Vector3.normalized를 사용하는 것보다 45%나 빠르다.

 

차이가 생각보다 큰데, 테스트삼아 스크립트에 특별한 기능을 하지 않는 함수 참조를 0회~10회 하도록 만들어서 비교해봤지만, 20억 번씩을 돌려봐도 별 차이가 없어서 약간 의아했다. Vector3의 경우 외부에서 참조해오는 것이라 차이가 많이 생기는 것일 수도 있을 것 같다.

 

뭐, 아무튼... Vector3.Normalized()를 사용하는 것이 약간은 더 좋을 것 같다. 내부에서 꺼내와서 사용하는 것은 성능상으로는 좋을지 몰라도 일반적인 상황에서 딱히 좋은 방법은 아닐 것이라고 생각한다.

 

 

객체지향이란 무엇일까 최근에 생각하고 있는데, 성능을 일부 포기하고 가독성과 재사용성을 높여 생산성을 끌어올리는 방법이라고 생각하게 되었다. 조금 더 나아가자면 가독성을 일부 포기하면서까지 재사용성을 극단적으로 높이면서 생산성을 향상시키는 방법이라고 생각한다.

 

때문에 절차지향 프로그래밍에 비해서 속도가 얼마나 느릴까... 하는 생각으로 비교를 해봤는데, Vector3를 호출하는 경우는 생각보다 차이가 심하고, 같은 스크립트 내에서 참조하는 경우에는 전혀 차이가 없어서 조금 알쏭달쏭한 것 같다.

 

당연히 오버헤드에 대한 영향은 무조건 있겠지만, 언어 내부적으로 최적화를 통해서 비슷한 위치에서는 오버헤드에 따른 성능 저하가 거의 없고, Vector3와 같이 외부에서 호출하는 경우에는 그 영향이 크게 나타나는 것이 아닐까 싶다.

 

MethodImplOptions.AggressiveInlining에 대해서도 찾아봤는데 컴파일러가 해당 함수의 코드 전체를 호출한 코드 위치로 복사하여 대체하기 때문에 오버헤드를 줄여주는 키워드라고 한다. 아마 1번 실행할 때는 크게 차이가 없을 것 같고, 반복문 등에서 의미가 크게 나타날 것 같다.

 

.NET Framework: 2016. C# - JIT 컴파일러의 인라인 메서드 처리 유무 (sysnet.pe.kr)

 

.NET Framework: 2016. C# - JIT 컴파일러의 인라인 메서드 처리 유무

.NET Framework: 2016. C# - JIT 컴파일러의 인라인 메서드 처리 유무 [링크 복사], [링크+제목 복사] 조회: 3151 글쓴 사람 정성태 (techsharer at outlook.com) 홈페이지 첨부 파일 부모글 보이기/감추기 (연관된

www.sysnet.pe.kr

 

그리고 찾아보니 이런 글도 있었는데, JIT 컴파일러에서는 자체적으로 인라인을 하는 것에 따른 비용과 효과를 판단해서 인라인을 해주기도 하고, 안 해주기도 한다는 것 같다. 음... 내가 모르는, 성능 향상을 위한 노력들이 많구나 하는 것을 다시금 느끼게 되었다.

 

근데 그렇다면 인라인하는 처음 1번을 제외하고는 함수를 인라인한 테스트들과 같은 실행시간을 가져야할 것 같은데... 음... 결과를 보면 그렇지만도 않은 모양이다. 아마 개선을 해서 이 정도의 시간이 소요되는 것이 아닐까... 라고 생각해본다.

(Unity 2021.3.10f1 기준)

 

+2024.03.30.

List는 가비지를 생성하지 않는 것이 맞고, Dictionary는 처음에만 조금 가비지를 생성하는 것? 같다. 자료 구조에 따라 가비지가 생성될 수도 있으니 애매하면 프로파일러를 사용해서 확인해보면 좋을 것 같다.

JacksonDunstan.com | Do Foreach Loops Still Create Garbage?

 

JacksonDunstan.com | Do Foreach Loops Still Create Garbage?

Over a year ago I wrote an article title Do Foreach Loops Create Garbage using Unity 5.2 and tested foreach with a variety of collections: List, Dictionary, arrays, etc. Since then Unity has a new C# compiler and version 5.6 has been released. Is it safe t

www.jacksondunstan.com

 

[결론]

1. foreach는 더 이상 가비지를 생성하지 않는 것이 맞다. (+2024.03.30. List와 Dictionary에서)

2. 배열이나 List와 같이 index로 접근 가능한 경우, for문을 사용하는 게 속도도 50~60% 가량 빠르고 데이터를 제어하기도 편리하니 for문을 사용하는 것이 좋다.

3. Dictionary의 key로 된 List를 만들어서 for문을 돌리는 것보다도 foreach로 Dictionary를 돌리는 것이 2배 가량 빠르고 가비지가 안 나오니 Dictionary를 탐색할 때는 걱정없이 foreach를 사용하자.

 

for와 foreach의 성능 차이에 대한 이야기가 종종 화두로 떠오르곤 한다.

 

옛날에는 foreach가 가비지를 많이 생성해서 foreach 사용을 피하곤 했다고 하고, 예전 코드를 보다가 가비지 생성을 피하기 위해서인지 List에 Dictionary의 key를 저장하는 식으로 사용하는 것도 보았다.

 

그래서 이제는 foreach에서 가비지가 정말 안 나오는지와 for와 foreach의 성능 비교 확인해봤다.

 

기껏 테스트 환경을 만들어놨는데 로그를 무조건 띄우게 되어있어서 GC.Alloc()이 집계되는지라 Start()와 Update()에 따로 작성해주었다.

아무튼, int로 된 Dictionary를 만들어서 Update()에서 foreach로 순회했지만 GC Alloc은 하나도 발생하지 않았다. 이제는 가비지가 발생하지 않는 것이 맞다.

 

[Unity] Foreach의 GC의 원인 및 수정 (tistory.com)

 

[Unity] Foreach의 GC의 원인 및 수정

Unity 5.5 미만 버전에서 foreach를 사용할 경우 가비지가 발생하는 이슈가 있었다. 원인 - Mono C# Unity 버전에서 foreach loop 종료 시 값을 강제로 박싱 - 값 형식의 열거자를 생성하였는데, 해당 열거자

everyday-devup.tistory.com

+기존의 foreach에서 가비지가 발생했던 원인은 값 타입을 interface(참조 형식)로 박싱했기 때문이고, 현재는 포인터를 직접 넘겨주는 식으로 수정되어 가비지가 발생하지 않는다고 한다.

 

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public partial class Tester : MonoBehaviour
{
    List<int> globalIntList = new List<int>();
    List<string> globalStringList = new List<string>();

    private void ForAndForeachTest()
    {
        List<int> preparedIntList = new List<int>();

        int temp = 0;

        for (int i = 0; i < _repeatTime; ++i)
        {
            preparedIntList.Add(i);
        }

        DoTest("List Traversal using for", repeatTime => {
            for (int i = 0; i < repeatTime; ++i)
            {
                temp = preparedIntList[i];
            }
        });

        DoTest("List Traversal using foreach", repeatTime => {
            foreach (var number in preparedIntList)
            {
                temp = number;
            }
        });

        Dictionary<int, int> intDictionary = new Dictionary<int, int>();

        for (int i = 0; i < _repeatTime; ++i)
        {
            intDictionary.Add(i, i);
            globalIntList.Add(i);
        }

        DoTest("int Dictionary Traversal using foreach", repeatTime => {
            foreach (var s in intDictionary)
            {
                temp = s.Value;
            }
        });

        DoTest("int Dictionary Traversal using Global List + for", repeatTime => {
            for (int i = 0; i < repeatTime; ++i)
            {
                temp = intDictionary[globalIntList[i]];
            }
        });

        DoTest("int Dictionary Traversal using new Key List + for", repeatTime => {
            List<int> newKeyList = intDictionary.Keys.ToList();

            for (int i = 0; i < repeatTime; ++i)
            {
                temp = intDictionary[newKeyList[i]];
            }
        });

        DoTest("int Dictionary Traversal using prepared List + for", repeatTime => {
            for (int i = 0; i < repeatTime; ++i)
            {
                temp = intDictionary[preparedIntList[i]];
            }
        });

        Dictionary<string, string> stringDictionary = new Dictionary<string, string>();
        List<string> stringList = new List<string>();

        string stringTemp = "";

        for (int i = 0; i < _repeatTime; ++i)
        {
            globalStringList.Add(i.ToString());
            stringDictionary.Add(i.ToString(), i.ToString());
            stringList.Add(i.ToString());
        }

        DoTest("string Dictionary Traversal using Global List + for", repeatTime => {
            for (int i = 0; i < repeatTime; ++i)
            {
                stringTemp = stringDictionary[globalStringList[i]];
            }
        });

        DoTest("string Dictionary Traversal using new Key List + for", repeatTime => {
            List<string> newKeyList = stringDictionary.Keys.ToList();

            for (int i = 0; i < repeatTime; ++i)
            {
                stringTemp = stringDictionary[newKeyList[i]];
            }
        });

        DoTest("string Dictionary Traversal using prepared List + for", repeatTime => {
            for (int i = 0; i < repeatTime; ++i)
            {
                stringTemp = stringDictionary[stringList[i]];
            }
        });
    }
}

 

 

결과를 보면 List를 사용할 때는 for문이 foreach보다 대략 50~60% 정도 빠르다.

Dictionary를 사용할 때는 for문으로 돌려면 List를 따로 만들어야 하는데, 미리 key로 이루어진 List를 만들어서 순회해도 foreach로 도는 것보다 2배 가량 느리다.

 

나머지는 key가 포함된 List를 전역 변수로 하는가, 지역 변수로 하는가, 람다식 내부에 만들어주는가에 따라서 속도 차이가 있는 것 같아서 차이를 보려고 했는데, 실행할 때마다, 또 껐다 킬 때마다 속도가 들쭉날쭉하다. 처음에는 Keys.ToList()가 제일 빨라서 가까운 곳에 선언되서 접근 속도가 빠른가보다 생각했는데, 별 관계 없는 것 같다.

 

근데 어차피 foreach가 빨라서 굳이 List로 만들어서 사용할 필요가 없다.

 

결론적으로 List나 배열 등 index로 접근 가능한 경우에는 for문을 사용하는 게 속도도 빠르고, 데이터를 제어하기도 편리하니 for문을 쓰고, Dictionary를 사용할 때는 걱정 없이 foreach를 사용하면 될 것 같다.

[결론]

요즘은 컴파일러들이 알아서 바꿔주기 떄문에 i++나 ++i나 별 차이 없다.

하지만 컴파일러가 바꿔준다는 것은 비효율적이라는 말이기도 하니 ++i로 작성하는 습관을 들이는 것이 좋겠다.

 

개발을 하다보면 성능에 대해서 고민이 들 때가 있다.

 

for문을 돌 때 ++i을 쓰는데, i++에 비해 속도가 얼마나 빠를까?

for문을 쓰는 것이 좋은가? 아니면 foreach문을 쓰는 것이 좋은가?

Vector3.normalized는 Vector3.Normalize()를 호출하기 때문에 Vector3.Normalize()를 호출하면 오버헤드가 적은데, 얼마나 빠를까?

Object.Instantiate()를 하고 SetParent, position, rotation 설정을 해주는 것과 Object.Instantiate()에서 한 번에 해주는 것에 얼마나 차이가 있을까?

 

등등... 다양한 것들에 대해서 문득 궁금해지는 순간들이 온다.

물론 이런 것들은 게임 개발에 있어서 성능 중 극히 일부분을 차지하는 것이긴 하지만, 알아두면 좋을 것 같다고 생각해서 궁금했던 것들을 한 번 테스트해보자는 생각을 했다.

 

이전에도 몇 번 테스트를 했던 경험이 있는데, 테스트를 조금 편하게 할 수 있으면 좋을 것 같아서 테스트 환경을 구축했다.

 

using System;
using System.Diagnostics;
using UnityEngine;
using Debug = UnityEngine.Debug;

public partial class Tester : MonoBehaviour
{
    [SerializeField] private string _testName = "";
    [SerializeField] private int _repeatTime = 0;
    [SerializeField] private byte _entireTestRepeatTime = 1;

    private string _currentTestName = "";
    private Stopwatch _stopwatch = new();

    public void StartTest()
    {
        for (int i = 0; i < _entireTestRepeatTime; ++i)
        {
            Invoke(_testName, 0);
        }
    }

    private void DoTest(string testName, Action<int> test, int repeatTime = 0)
    {
        int tempRepeatTime = _repeatTime;
        if (repeatTime > 0)
        {
            _repeatTime = repeatTime;
        }
        else if (repeatTime > _repeatTime)
        {
            Debug.LogError("repeatTime can't bigger than original repeatTime");
            return;
        }
        _currentTestName = testName;

        
        _stopwatch.Restart();

        test.Invoke(_repeatTime);

        _stopwatch.Stop();
        LogStopwatchElapsedMilliseconds();
    }

    private void LogStopwatchElapsedMilliseconds()
    {
        Debug.Log($"Test {_testName}/{_currentTestName} done, RepeatTime : {_repeatTime}, Takes {_stopwatch.ElapsedMilliseconds}ms");
    }
}

먼저 Tester.cs 파일에 Tester 클래스를 선언해주었다. 편한 사용을 위해서 인스펙터에서 테스트할 함수의 이름과 반복 횟수, 전체 테스트의 반복 횟수를 설정할 수 있도록 했다. Invoke로 함수 이름을 String으로 받아서 실행하는 것은 추적하기 힘들어지니 별로 좋은 방식은 아니지만, 여기서는 편리함을 중요시하여 사용하였다.

 

또, 실행 시간을 기록하거나 출력하는 것을 간편하게 처리해주도록 테스트를 실행할 함수를 Action으로 받아서 한꺼번에 처리해주도록 해주어서 테스트 코드를 간편하게 작성할 수 있도록 하였다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public partial class Tester : MonoBehaviour
{
    private void NppAndppNTest()
    {
        DoTest("n++ Test", repeatTime => {
            for (int n = 0; n < repeatTime; n++) ;
        });

        DoTest("++n Test", repeatTime => {
            for (int n = 0; n < repeatTime; ++n) ;
        });
    }
}

테스트할 함수가 많아지면 스크립트가 많이 길어질 것 같아서 Tester를 partial 클래스로 나누었다. 위의 경우 NppAndppNTest.cs로 스크립트를 나누어 작성하였다. 그리고 Tester에서 사용할 함수라는 것을 쉽게 분별할 수 있도록 TestList 폴더에 모아두었다.

 

이로써 위와 같이 테스트할 함수만 형식에 맞게 작성해주면 테스트를 간편하게 수행할 수 있게 되었다.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Tester))]
public class StartTestButtonEditor : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        Tester tester = (Tester)target;
        if (GUILayout.Button("Start Test"))
        {
            tester.StartTest();
        }
    }
}

또, 인스펙터에서 버튼을 눌러서 바로 테스트할 수 있도록 Editor도 추가해주었다.

 

결과적으로 이런 GameObject가 만들어졌다.

 

여기서 Start Test 버튼을 누르면...

 

이런 식으로 결과가 나온다.

3번 테스트를 수행해서 2개씩 3번 나눠서 봐야하는데, ++n이 약간 빠른가? 싶지만 마지막 결과는 n++이 빠르다.

왜 이런 결과가 나왔는지 의문이 들어서 찾아보니 요즘에는 컴파일을 하면서 n++과 같은 것들을 ++n으로 바꿔도 되는 경우(for문에서 사용하는 것처럼 단순히 증가만 시키는 경우) ++n으로 바꿔서 컴파일해준다고 한다.

 

그래서 결론적으로 n++을 쓰나 ++n으로 쓰나 상관없다는 것이다.

쩝... 뭔가 허무한 결과긴 하지만 컴파일러가 바꿔준다는 것은 비효율적이라는 말이기도 하니 n++보다는 ++n이라고 작성하는 습관을 들이는 것이 좋을 것 같다.

저번 사볼에 이어서 오랜만에 쓰는 것 같다. 프로세카를 얼마 전에 시작해서 출퇴근시간에 하고 있는데, 덕분에 출퇴근시간이 상당히 즐거워져서 내가 리듬 게임을 좋아하는구나, 라는 것을 몸소 느낄 수 있었다. 최근에 가장 많이 하는 게임이 리듬 게임인 것 같아서 리듬게임을 먼저 쓰기로 했다.

 

내가 처음 접한 리듬 게임은 아마 EZ2DJ일 것으로 생각된다. 동네 문방구 앞에 오락기들이 깔려있었는데, 데몬 프론트와 스노우 브라더스2, 그리고 EZ2DJ였다. 대략 초등학교 2학년 때였던 것으로 기억하는데, 당시에는 오락기 뒤에서 다른 사람들이 하는 것을 보고만 있어도 재미가 있었다. 나름 당시의 인터넷 방송 비스무리한 것 아니었을까 싶다. 아무튼, 중학생 형들이 어려운 곡들을 곧잘 깨는 것을 지켜보면서 멋있다는 생각을 했던 것 같다.

 

아무튼, 처음에 어떻게 시작을 했는지는 모르겠지만 우선 옆의 턴테이블과 페달을 끄고 했던 것으로 기억한다. 가끔 친구들이 있을 때는 해당 옵션을 켜서 같이 플레이하거나 하기도 했던 것 같다.

 

https://youtu.be/N_3ooCIXG7s

당시 깼던 가장 레벨 높은 곡은 일명 '곰돌이'라고 불렀던 Night Madness다. 탱크라고 불렀던 Shout나 오토바이 등도 깼던 것 같고, 아파트나 200억 같은 것들은 깨지는 못했던 것 같다. 지금 영상을 다시 보니 턴테이블과 페달이 거의 메인인 것 같은데 왜 저걸 끄고 했는지 ㅋㅋ. 기회가 되면 오락실에서 켠 상태로 다시 해보면 재밌을 것 같다.

 

그리고 피쳐폰 시절에 리듬스타도 했던 것 같은데, 당시 핸드폰이 없어서 형이나 친구 핸드폰으로 몇 번 해본 것이 전부라 기억이 많지는 않다.

 

스마트폰 시절로 들어와서는 터치팝을 많이 했던 것 같다. 자랑은 아니지만, 당시 mp3로 음원들을 다운받아서 듣곤 했었는데 터치팝은 유저들이 직접 채보를 만들 수 있었던 점이 상당히 매력적이었다. 내가 좋아하는 곡들에 대한 채보를 검색해서 다운받거나, 직접 만들어서 배포할 수도 있었다. 처음엔 연타 등 정직하게 만들다가, 채보를 더 어렵게 만들고 싶은 생각에 손을 꼬아야 하는 패턴들을 만들기도 했다. 가령 오른쪽에서 시작한 롱노트를 왼쪽으로 보낸다음에 오른쪽에 노트를 떨구는 식이었는데, 덕분에 사람들이 채보를 거지같이 만든다고 댓글을 달곤 했던 것 같다. ㅋㅋ. 아무튼 직접 채보를 만들 수 있다는 특징 덕분에 꽤나 즐겼던 게임이었다. 지금은 서비스 종료를 한 것 같지만, 유저가 직접 채보를 만들 수 있도록 한 기능은 지금 생각해봐도 상당히 훌륭한 것 같다.

 

 

탭소닉, 사이터스, 디모 등을 플레이하기도 했었는데 부분 유료 정책으로 인해서 많이 하지는 않았던 것 같다. 탭소닉은 스태미너 같은 것이 있었던 것 같고, 사이터스와 디모는 곡 패키지 같은 것을 사야 했던 것 같다. 당시에는 중학생이었는데 용돈도 얼마 안 되고 그래서 무료 곡들만 좀 하다가 접었던 것으로 기억한다.

 

https://youtu.be/9V2oyvvULAc

고등학교 즈음 해서는 러브라이브 스쿨 아이돌 페스티벌이라는 리듬게임을 했었다. 위 영상 썸네일이 메인 화면이었던 시기에 플레이 했었는데, 음... 러브라이브를 좋아하던 시기에 1~2달 정도 했던 것 같다. 문제는 뭐.. 뮤즈 파이널 라이브니 뭐니 하면서 여러모로 논란이 있었는데, 그 시기에 나도 그냥 빠져나왔다.

 

그리고 이후에 시작한 것이 속칭 "데레스테" 라고 불리는 아이돌마스터 신데렐라 걸즈 스타라이트 스테이지이다. 처음에는 신데마스 애니를 접하면서 관심을 가지고 시작했던 것 같은데, 이 게임은 그래도 1년 정도는 하지 않았나 싶다. 아이돌 마스터 신데렐라 걸즈 소셜 게임도 했었는데, 한국 서비스는 잠깐 하다가 섭종을 했던 것 같고, 일본 서비스 쪽에서도 잠깐 하다가 접었던 것 같다. 이 데레스테를 통해서 고등학교 때 닿은 인연이 있는데, 그 인연이 아직까지 이어지고 있다. 2학년 때 3학년 졸업식에 참여하러 평화의 전당에 갔었는데, 내 옆자리에 앉은 같은 반 친구가 데레스테를 켜서 "어! 너도 그거 하는구나!" 하면서 친해졌던 기억이 난다. 이전에도 잠깐 언급했던 것 같은데, 당시 나름 학교에서 유명했던 오타쿠인 나의 관심을 끌려고 일부러 옆자리에 앉아서 데레스테를 켜다고 하는 후문이 있다. 그 친구가 나에게 블루 아카이브를 추천해준 친구이다. 나름 취향이 잘 맞는 것 같아서 이 친구를 통해서 새로운 것들을 많이 접하고, 즐기고 있기도 하다.

 

그리고 가장 열심히 했던 리듬 게임이 바로 "밀리시타"라고 불리는 아이돌마스터 밀리언 라이브 시어터 데이즈이다. 아마 사전 예약까지 했던 오픈 유저였던 것으로 기억한다. 부지런히 하지는 않았던 것 같은데, 짬짬이 하면서 대략 3주년까지는 플레이를 했었다. 타 게임을 할 때는 신경쓰지 않던 이벤트 상위권에 들기 위한 노력도 많이 했던 것으로 기억한다. 스태미너 뺀답시고 야자 시간에 화장실에 가서 플레이하던 기억도 난다. 주년 이벤트에서 in100을 했으면 자랑이라도 할 수 있을텐데 아쉽게도 in1000정도만 했던 것 같다. 친구는 아마 in100을 2번 했던 것으로 기억한다.

스샷을 찍어놨던 3주년 이벤트 당시 기록이다.

 

 

이외에도 오락실에서 유비트, 이전에 글을 썼던 사운드 볼텍스, 댄스 러시, 리플렉 비트를 하기도 했었다. 펌프같은 것도 가끔씩 했던 기억이 난다.

 

글 처음에 언급했듯 최근에는 "프로세카"라고 불리는 프로젝트 세카이 컬러풀 스테이지 feat. 하츠네 미쿠를 시작했다. セカイ라는 곡이 아주 좋은데, 원래 알고 있었지만 시작은 ラストスコア라는 곡을 통해서 시작하게 되었다. 시작한지는 약 3주 정도 되었는데 지금까지는 꽤나 재밌게 하고 있는 중이다. 모바일 리듬 게임 치고는 난이도가 상당히 어려운 편인데, MASTER 난이도의 일부 곡들은 엄지손가락만으로 플레이하기에는 상당히 무리가 있는 것 같다. 그래서 패드를 하나 살까 고민을 하고 있기는 한데... 아직까지는 그냥 핸드폰으로 하고 있다. 初音ミクの激唱 EXPERT 난이도 풀콤을 하고 싶어서 출퇴근 중에 주구장창 하는데, 맨날 똑같은 곳에서 실수를 해서 1 GOOD으로 풀콤을 못하고 있다. 연타 구간이 끝날 때 FAST가 뜨곤 하는데 이틀 전인 금요일에 퇴근할 때는 5판 연속 그곳에서 GOOD이 떠서 좌절한 기억이 있다.

 

https://youtu.be/8DtsTm1iMGQ

위 영상의 대략 2분 7~8초 쯤 구간이다.

 

아무튼, 적당히 난이도도 어렵고 노래들도 내가 아는 노래들이 많고, 이동하면서 하기도 좋고, 캐릭터들도 마음에 들고 해서 음... 약간의 현질과 함께 즐겁게 플레이를 하고 있다. 당분간 격창 풀콤을 많이 시도할 것 같다. 그리고 프로세카로 리듬 게임에 대한 갈증이 조금 채워졌는지 최근에는 사볼을 하러 가지 않아도 별로 아쉬운 느낌이 들지 않는다.

 

다만 단점으로는 열심히 하다보면 엄지 손가락과 손목이 조금 아프기도 하다는 것인데, 이것 때문에라도 패드를 사야하나, 싶기도 하다. 아무튼, 게임이야 바뀌더라도 피지컬이 받쳐주는 한 리듬 게임은 앞으로도 계속 하게될 것 같다.

'잡담' 카테고리의 다른 글

2023년 한 해를 되돌아보며  (1) 2024.01.01
보충역(공익/산업기능요원 등) 훈련소 3주 후기  (0) 2023.12.16
3주 출근 후기  (0) 2023.06.27
취미 3 - 사운드 볼텍스  (4) 2023.06.06
취미 2 - WOW  (3) 2023.06.01

오늘은 유니티에서 Package Manager를 통해서 제공하는 Memory Profiler에 대해서 소개할 예정이다. 유니티 2021에서 제공하는 0.7.1 버전을 기준으로 소개를 할 것인데, 현재는 1.0도 나와있다. UI나 세부적인 부분은 바뀌었겠지만 아마 핵심적인 부분들은 크게 바뀌지 않았을 것이라고 생각된다.

 

 

1.0에 대한 내용은 Unity Korea 채널에서 오지현님의 Memory Profiler 1.0 소개 영상을 보면 도움이 될 것 같다.

 

https://youtu.be/rdspAfOFRJI

 

우선, 기본적으로 제공하는 프로파일러에서도 메모리에 대한 대략적인 정보와 어느정도의 디테일한 정보를 볼 수 있었지만, 지금은 Detail한 정보를 보려면 메모리 프로파일러를 통해서 보라고 한다. 메모리 프로파일러는 Heap Memory에 올라와있는 GameObject, Transform은 물론이고 Texture2D, Shader 등 Native Memory에 있는 리소스들까지 전부 볼 수 있다. 프로파일러처럼 실시간으로 보지는 못하지만, 원하는 시점에 스냅샷을 찍어서 해당 시점의 메모리 상황을 저장해놓고, 다른 스냅샷과 비교하면서 어떤 차이가 있는지 살펴볼 수 있는 훌륭한 도구이다.

 

메모리 프로파일러를 열면 다음과 같은 창을 볼 수 있다. 처음에는 아무것도 없는 상태이다. 기본적인 정보를 설명하자면 이렇다.

 

1. Editor라고 되어있는 부분을 누르면 Editor, 혹은 모바일 기기 등을 선택하고 스냅샷을 찍을 수 있다. 모바일 기기를 연결하기 위해서는 안드로이드는 ADB 관련 세팅이, ios는 XCode 관련 세팅이 필요할 것이다. Editor에서는 유니티 Editor에서 사용되는 메모리들도 같이 잡히기 때문에 실제 타겟 기기에서 사용하는 메모리와는 상당히 차이가 크게 나타나기 때문에, 의미있는 메모리 분석을 하기 위해서는 타겟 기기에서 스냅샷을 찍는 것이 좋다.

 

2. 스냅샷을 찍는 버튼이다. 오른쪽 버튼을 누르면 스냅샷을 찍을 때 기록할 메모리들과 일부 설정들을 조절할 수 있다. 스냅샷을 찍을 때 경고창이 나타나는데, 모든 메모리들이 기록되기 때문에 중요한 정보들이 유출되지 않도록 조심하라는 내용이다.

 

3. Single Snapshot은 1개의 스냅샷에 대한 자세한 정보를 볼 수 있고, Compare Snapshots를 누르면 2개의 스냅샷을 선택해서 각 상황에서의 메모리 변화를 비교할 수 있다.

 

4. 기록한 스냅샷이 저장된 모습이다. 기록할 당시의 스크린샷과 이름, 기록한 시간 등이 저장된다. 스크린샷의 오른쪽 아래에는 어느 플랫폼에서 찍었는지 나타내준다. 위 사진의 경우 윈도우, 유니티 에디터에서 찍었기 때문에 두 아이콘이 나타났는데, 안드로이드 기기에서 찍으면 안드로이드 모양 아이콘이 나타나는 식이다. 이름을 누르면 이름 변경이 가능하고, 그 외의 부분을 누르면 스냅샷을 살펴볼 수 있다. 우클릭으로 추가 메뉴를 열 수도 있다. 정보를 세세하게 기록하기 때문에 용량이 상당히 큰데, 대략 200~300MB정도 된다.

 

스냅샷을 누르면 기록할 당시의 메모리의 세부적인 부분을 볼 수 있다. 테스트용 안드로이드 기기를 회사에 놓고 와서 에디터에서 기록한 스냅샷을 기준으로 설명하겠지만, 정확한 분석을 위해서는 타겟 기기에서 스냅샷을 기록할 필요가 있다는 점을 다시 한 번 강조하겠다.

 

Managed Heap은 가비지 컬렉터가 관리하는 메모리 공간으로, 우리가 객체를 생성하는 등의 활동을 할 때 이 영역을 차지한다. 96.3MB를 할당받은 상태이기 때문에 사용량에 관계없이 96.3MB의 메모리 공간을 차지한다. 현재 44MB밖에 사용하고 있지 않은데, 이 상태가 지속되면 자동으로 할당받은 메모리 공간이 줄어든다.

 

Vitrual Machine (Mono)는 에디터에서 실행하거나, Mono로 빌드한 경우에 사용되는 메모리이다. 유니티 엔진은 C++로 되어있기 때문에 프로그래머가 작성한 C# 코드를 그때그때 C++ 코드로 변환하면서 사용되는 메모리이다. IL2CPP 방식으로 빌드하면 해당 메모리 사용량이 0이 되니 크게 신경쓸 필요는 없다.

 

이외에는 Native Memory나 프로파일러, 유니티에서 사용하는 메모리 등을 보여주는데, 에디터라 전체적인 사용량이 큰 모습이다.

 

현재 화면에서는 대략적인 사용량만 볼 수 있는데, 실제로 자세히 분석을 위해서는 상단에 있는 메뉴들을 클릭해주어야 한다.

 

https://docs.unity3d.com/Packages/com.unity.memoryprofiler@0.7/manual/memory-breakdowns.html

 

Memory Breakdowns | Memory Profiler | 0.7.1-preview.1

Memory Breakdowns The Memory Breakdowns tab displays a broader view of how the memory in your snapshot is distributed. Use this information to identify areas where you can eliminate duplicate memory entries or to find which objects use the most memory. The

docs.unity3d.com

 

Memory Breakdowns는 2022.1 이상의 버전에서 지원하는 기능이다. 여기서는 자동으로 Type별로 묶고, 크기를 내림차순으로 정렬하여 보여주는 것 같다. 유니티에서 분석하여 Potential Duplicates view라는 것도 보여주어 메모리를 조금 더 간편하게 분석할 수 있도록 지원하는 것 같다. 나는 아직 2021 버전에서밖에 사용해보지 않았기 때문에 더 자세한 내용은 위 문서를 참조하길 바란다.

 

Tree Map을 누르면 현재 전체 메모리 사용량을 Tree Map 형식으로 볼 수 있다. 현재 왼쪽 위 사각형인 Texture2D를 눌러서 Texture2D에 대한 자세한 정보가 표시된 모습이다. 현재 어떤 요소가 메모리를 많이 잡아먹고 있는지, 그 안에는 어떤 것들의 사용량이 큰지 눈으로 쉽게 파악할 수 있다.

 

그 다음인 Objects And Allocations는 내가 가장 자주 사용하는 탭으로, 전체 메모리를 한 눈에 살펴볼 수 있다.

 

위에 Data Type, Type, Name, Size 등의 부분을 누르면 해당 내용을 기준으로 Group을 짓거나, 원하는 내용만 표시할 수도 있으며, 크기순으로 정렬할 수도 있다.

 

예를 들어 Type으로 Group짓고, Size를 내림차순으로 정렬하면 이런 식으로 메모리를 많이 차지하고 있는 항목들을 순서대로 볼 수 있다.

 

원하는 내용을 펼쳐보면 내부에서도 내림차순으로 정렬되어 있는데, 기능 구현에 중점을 둔 포트폴리오라 메모리 최적화는 신경을 쓰지 않아서 오디오 클립 하나가 20.6MB를 차지하고, Texture 사이즈도 제멋대로라 압축이 제대로 되지 않은 등의 모습을 확인할 수 있었다. 만약 메모리가 부족하다면 이곳에서 큰 용량을 차지하는 메모리를 줄이는 등의 목표를 세울 수 있다.

 

Compare Snapshots로 2개의 스냅샷을 비교해보면 이런 식으로 나타난다. 게임을 켜고 아무것도 하지 않았음에도 불구하고 전체적인 메모리 사용량이 크게 늘어난 모습이다. 에디터이기 때문에 그렇다. 스냅샷을 찍을 때마다 메모리가 아주 큰 폭으로 늘어나기 때문에 시간이 지날수록 렉이 걸리는 것을 경험할 수 있을 것이다. 모바일 기기에서 테스트를 하는 경우, 특수한 경우가 아니라면 한참이 지난 후에 찍더라도 전체 용량이 1MB도 채 변하지 않는다. 그래서 정확한 분석을 위해서는 타겟 기기에서 스냅샷을 찍으라고 누차 이야기를 하고 있는 것이다.

 

Compare Snapshots에서는 Tree Map은 제공하지 않고, Objects and Allocations를 이용해야 한다.

 

다시 Objects and Allocations로 들어와보면 Diff라는 탭이 하나 더 생긴 것을 확인할 수 있다.

또, 상단의 메모리 사용량을 보면 Total Sizes: 뒤에서 두 스냅샷에서의 메모리 사용량의 차이를 볼 수 있다.

 

Diff를 기준으로 Group지으면 이렇게 Same, Deleted in B, New in B를 나눠서 확인할 수 있다. Deleted in B와 New in B를 중점적으로 살펴보면서 삭제되서는 안 될 것들이 삭제되었다든지, 생성되서는 안 될 것들이 생성되었다든지를 확인하면 된다. 위의 경우, 가만히 있었는데도 105MB가 삭제되고 284.5MB가 생성되었다는 것은 무언가 큰 문제가 있다는 뜻이다. 물론 에디터에서 찍은 스냅샷이라 그럴 것이다.

 

 

아까와 같이 Type별로 추가로 Group짓고, Size를 내림차순으로 정렬해보면 더 쉽게 파악할 수 있다. 위 사진에서 볼 수 있듯이 RenderTexture같은 경우는 똑같은 것들이 삭제되었다가 새로 생긴 것을 볼 수 있는데, 모종의 이유로 파괴되었다가 다시 생성되어 교체된 것이다. 음... 이 경우는 이유는 잘 모르겠다. 아마 화면이 완전히 가려져서 그런 것이 아닐까 생각해본다.

 

실질적으로 어떻게 유용하게 사용할 수 있는지 예시를 들어보자면, 나의 경우에는 비교 작업을 통해서 몬스터를 잡을 때 몬스터의 Material, SkinnedMeshRenderer 등이 매 번 삭제되고 생성되고 있는 것을 확인할 수 있었다. 무언가 이상하다고 느껴 확인해보니 몬스터에 대한 오브젝트 풀링이 제대로 작동하지 않고 있었던 것을 발견하고 수정하였다. 이를 통해서 사냥 중 가비지 컬렉터가 작동하는 주기를 50% ~ 100% 혹은 그 이상 늘릴 수 있었다. (평소에는 2분 주기로 GC가 호출되었는데, 3~4분 정도로 바뀌었고, 몬스터가 위치하는 필드가 작은 등의 일부 경우에는 30분 가량 호출이 되지 않을 때도 있었다)

또, AssetBundle 사용 관련 문제로 인해 Shader가 복제된 것을 확인하였는데, 에셋 번들 빌드 중 Dependencies가 제대로 등록되지 않아 해당 원인이 무엇인지 파악 중에 있다.

추가로, Shader 용량이 예상보다 큰 것을 확인하여 사용하지 않는 Varient를 없애 전체 Shader 용량을 1/4 가량으로 줄이는 작업에 조금이나마 기여를 한 바가 있다.

 

그리고 풀링한 오브젝트들 중에 사용한 지 오래된 리소스들을 정리하는 작업도 하였는데, 이 과정에서 불필요하게 메모리를 차지하고 있는 리소스(AudioClip 등)를 발견하여 정리해주는 작업을 추가로 하기도 하였다.

 

이외에도 따로 한참 방치해두며 스냅샷을 찍어 메모리 누수가 없는 것을 확인하기도 하였다.

 

여태까지 약 2~3주간 사용법을 익히고 위에 언급한 것처럼 내가 생각해 낸 방식대로 사용을 하였는데, 더 유용한 사용법들이 많이 있을 것 같다. 위에 언급한 내용들만으로도 충분히 사용할 가치가 있을 것이라고도 생각한다. 메모리 관리에 어려움이 있다면, 메모리 프로파일러를 사용해보는 것을 적극 권장하는 바이다.

 

마지막으로, ADB의 dumpsys meminfo를 활용하여 기기 내의 전체 용량과 어플리케이션이 사용하는 비중을 확인하는 것도 도움이 될 수 있을 것 같다.

 

C#에서는 가비지 컬렉터가 더이상 참조되지 않는, 즉 사용되지 않는 이른바 쓰레기 메모리, 즉 가비지들을 수거해간다.

 

가비지 컬렉터가 메모리를 자동으로 관리해주긴 하지만, 가비지 컬렉터는 참조가 하나라도 있으면 사용된다고 간주하기 때문에 더이상 사용하지 않는 것들의 참조를 해제함으로써 가비지 컬렉터가 수집해갈 수 있도록 해줄 필요가 있다.

즉, 가비지로 만들어주는 작업이 필요한 셈이다.

 

 

 

스크립트 상에서 사용하고 있는 변수에 null을 넣어서 참조를 없애주는 방법이 있지만, 다른 곳에서 참조하고 있을 수도 있기 때문에 제대로 사라질 것이라는 보장은 없다. 특히 오브젝트의 경우 null을 넣어도 제대로 해제가 되지 않으니 Destroy()를 사용하라고 하는데, 확인해보니 GameObject는 Transform과 상호 참조를 하기 때문에 쓰는 곳도 없는데 메모리에 남아있는 문제가 있었다.

 

 

그림으로 그려보면 대충 이런 느낌일 것 같다.

 

가비지 컬렉터가 쓰지 않는 메모리들을 수거해주긴 하지만, 우리가 참조를 끊음으로써 가비지 컬렉터가 수거해갈 수 있도록 만들어줘야 한다.

 

만약 이런식으로 사용하지 않는데 참조 해제를 제대로 해주지 않는 일이 반복되면 메모리 상에 쓰레기가 계속 쌓이고, 결국 메모리가 부족해지는 때가 올 것이다. 부족해지면 메모리를 늘리다가 한계에 다다르면 어플리케이션이 종료되는 마지막을 맞이하게 된다.

 

이렇게 되지 않기 위해서 우리는 가비지 컬렉터를 믿고 맡길 것이 아니라 메모리의 사용이 끝나면 제대로 해제해줘야 한다.

 

 

추가로, Garbage Collector는 Managed Heap만을 관리해준다. 여기는 프로그래머가 사용하는 각종 Class들이 주로 저장된다.

 

Texture, Mesh, Material 등 Resource들은 Native Memory에서 따로 관리를 해준다. Native Memory는 유니티 내부에서 C++로 관리를 하는 것으로 알고 있는데, GC가 관리하는 영역이 아니기 때문에 Load한 Texture, Mesh 등의 참조를 전부 끊고 GC를 돌려도 수집해가지 않는다. Native Memory에서 Resource들을 해제해주려면 Resources.UnloadUnusedAssets()를 호출해줘야 수거해간다. (씬이 생성될 때 가져온 리소스들은 삭제되지 않는다든가 하는 내용을 봤던 것 같은데, 어디서 봤는지 잘 모르겠다)

 

 

아무튼, Native Memory를 관리해주는 가비지 컬렉터가 따로 있는 셈이다. 그런데, Native Memory는 대체로 용량이 큰데다가 가비지 컬렉터와 같은 점진적 수집 기능이 없어서 그런지 Resources.UnloadUnusedAssets()을 호출하면 게임이 잠깐 뚝하고 끊긴다. 로딩을 할 때라든지, 메모리가 부족할 때(onLowMemory가 호출되었을 때 등) 호출하는 것이 바람직하다.

 

===========================================================================

 

뭔가 그림이 있으면 재밌을 것 같아서 그려봤는데, 나름대로 괜찮게 그려진 것 같다.

최근 회사에서 메모리 관리에 대해서 맡게 되어 메모리 프로파일러를 보는데, 처음에 이런 내용을 모르고 보니까 머리에 내용이 들어오지 않았다. 여러 방면으로 삽질하고 공부하다가 최근 이해가 되어서 나름대로 정리해본 내용이다. 다음엔 아마 메모리 프로파일러 보는 법에 대해서 적지 않을까 싶다. 익숙해지는데 시간이 좀 걸리고, 많이 복잡하지만 메모리 관리에 상당한 도움을 주는 좋은 도구이다.

+ Recent posts