<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>eugene-doobu.log</title>
        <link>https://velog.io/</link>
        <description>간식주세요</description>
        <lastBuildDate>Fri, 23 Jan 2026 12:28:09 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>eugene-doobu.log</title>
            <url>https://velog.velcdn.com/images/eugene-doobu/profile/324cdef5-7c5e-4afa-a782-2449f2c87580/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. eugene-doobu.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/eugene-doobu" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[AI랑 뱀서류 게임 개발 - 3D 모델링편]]></title>
            <link>https://velog.io/@eugene-doobu/AI%EB%A1%9C-%EB%B1%80%EC%84%9C%EB%A5%98-%EA%B2%8C%EC%9E%84-%EA%B0%9C%EB%B0%9C-%EB%AA%A8%EB%8D%B8%EB%A7%81%ED%8E%B8</link>
            <guid>https://velog.io/@eugene-doobu/AI%EB%A1%9C-%EB%B1%80%EC%84%9C%EB%A5%98-%EA%B2%8C%EC%9E%84-%EA%B0%9C%EB%B0%9C-%EB%AA%A8%EB%8D%B8%EB%A7%81%ED%8E%B8</guid>
            <pubDate>Fri, 23 Jan 2026 12:28:09 GMT</pubDate>
            <description><![CDATA[<p><em>도와줘~ 제미니몽~</em></p>
<p>요즘 AI를 최대한 활용해서 1인 게임을 개발하고 있다. 옛날부터 내 게임을 만들어서 출시 하고 싶다는 생각은 많이 했는데, 나약한 의지력과 함께 <code>아트 리소스</code>의 벽에 가로 막혀 매번 제대로 시도도 하지 않고 포기하기 일쑤였다. 하지만 이제는 상황이 달라졌다! AI가 나를 도와 게임을 완성시킬 수 있게 도와주고 있다. 이제 진짜 게임을 완성 못한다면, 기술이나 시간의 문제가 아니라 나의 의지의 문제라고 할 수 있을 것이다.</p>
<p>이번에는 나의 기술을 극대화한 뱀서류 게임을 만들어보며, AI에게 도움을 받는 과정들을 글로 작성해보려고 한다. 지금 시점에서 나름 최신의 모델들을 사용해 작업을 진행해나가고 있지만, AI의 발전속도가 워낙 빠르기 때문에 글 작성이 완료된 시점에 이미 내가 사용한 기술들이 구시대의 기술이 될 지도 모르겠다.</p>
<h2 id="원하는-것">원하는 것</h2>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/b0a1c818-a76c-4396-8c0e-babac1605f1c/image.png" alt="캐릭터 일러"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/3a0fd213-d006-4358-8a5b-7bc6344ea9c7/image.png" alt="캐릭터"></th>
</tr>
</thead>
</table>
<p>현재 개발중인 게임에는 이런 잘생긴 닭이 있다. 원본 모델에서는 해당 닭이 &#39;수류탄&#39;을 들고 있었는데, 나는 조금 더 코믹한 느낌을 살리기 위해 해당 수류탄을 &#39;계란&#39;으로 변경하고자 했다. 의도에 맞게 일러스트를 생성하고, 인게임에서 쓸 <code>3D 모델링</code>이 필요하다. 이번에는 AI님의 도움을 받아 이 계란 모양의 수류탄 에셋을 만들어보고자 한다.</p>
<h2 id="훈위안">훈위안</h2>
<p>3D 생성 모델로는 텐센트의 <code>Hunyuan3D</code>을 선택했다. 이것을 사용하는 이유는 일단 나랑 훈위안이 제일 잘 맞는다. 일단 나는 <code>카툰 렌더링</code> 느낌의 셀 셰이딩을 주로 사용하는 편인데, 이러한 모델에 <code>Hunyuan3D</code>이 가장 잘 어울린다고 생각했다. </p>
<p>대중적으로는 <code>Meshy</code>가 가장 유명하지만, 특유의 수채화 같은 텍스쳐 질감이 내 취향과는 맞지 않았다. 그리고 나는 고해상도의 리얼한 모델보다는 아기자기하고 가벼운 모델을 원했는데, 이런 스타일이야말로 소규모 인디 게임에 최적화된생각한다. 여기서 <strong>훈위안</strong>이 타 AI 모델 대비 압도적인 퀄리티를 보여주었다. </p>
<h2 id="이미지-생성">이미지 생성</h2>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/c15f3e67-36da-4a2a-ab26-77fcc7d3890d/image.png" alt="계란1트"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/e8f563c6-9955-40d7-b922-dfda9c3ad896/image.png" alt="계란3트"></th>
</tr>
</thead>
</table>
<p>3D 모델을 생성할 때에는 image-to-3D가 원하는 메시를 만들어내기에 적합하기에, 우선 제미나이로 이미지 생성을 진행했다. 보통 GPT나 Gemini로 이미지 생성을 주로 요청하는데, GPT는 품질이 아쉽지만 말은 알잘딱 알아듣는 느낌이고, Gemini는 정말 확실하게 말해줘야 말을 알아듣는데 그림 품질은 최고인 느낌이다. 왜 계란을 디자인 해달라고 했는데, 계란에 저런 얼굴을 그려주는거니.</p>
<h2 id="모델링-생성">모델링 생성</h2>
<h3 id="1-메시-생성">1. 메시 생성</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/48e51ae1-f5f0-4a48-becc-10380e782df2/image.png" alt="메시생성"></p>
<p>이제 훈위안으로 가서 방금 만든 계란 이미지를 input으로 3D 모델을 생성하였다. 사용한 모델은 3.1v로 현재 시점에서 최신 버전이다. 모델의 디테일을 설정해줄 수 있는 &#39;face수&#39;는 가장 작은 수치인 50k(5만개)를 선택했다. 디테일이 중요한 모델도 아니고, 가볍게 만드는게 중요하기에 이런 모델 만들때에는 항상 최소 face수인 50k를 선택한다.</p>
<h3 id="2-모델-분할">2. 모델 분할</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/99556223-8ac4-4b68-8bf4-581ca8cd7964/image.png" alt="모델 분할"></p>
<p>요즘 훈위안에서 밀고있는 기술, <code>모델 분할</code>은 생성된 3D모델을 &#39;적당하게 쪼개어&#39; 한개의 메시를 여러 개의 서브 메시로 만들어 준다. 예를 들어 계란 수류탄의 경우, 계란의 꽁지 부분과 몸통 부분을 별도의 메시로 쪼개서 각각 별도로 메시로 만들고 어떤 메시(파츠)를 렌더링 할지 안 할지를 선택할 수 있게 되는 것이다. 그리고 그렇게 될 것을 기대하고 모델 분할을 진행했는데, 너무 단순한 모델이라 그런지 그냥 하나의 덩어리가 아웃풋으로 나왔다..</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/9b199572-4bfd-4129-bedd-33ba05321c11/image.png" alt="실망"></p>
<p>뭐 계란수류탄에서 꽁지부분이 별도 파츠로 분리되는건 크게 중요한건 아니니 그냥 이대로 진행하기로 하였다. 아쉬운 김에 전에 만들어 놓은 다른 모델을 기준으로 서브 메시를 활성화/비활성화하는걸 보여주자면 이렇다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/db689d58-2b2b-4d4e-b73b-d4c5e2754baa/image.png" alt="새집1"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/e0769340-0c20-4b00-99e2-ae2d8b4d8271/image.png" alt="새집2"></th>
</tr>
</thead>
</table>
<p>새 집의 몸통(Part16)의 활성화/비활성화 모습</p>
<h3 id="3-메시-리토폴로지">3. 메시 리토폴로지</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/6b472b32-da26-47d6-898d-54f959e1a658/image.png" alt="리토폴로지"></p>
<p>개인적으로 <code>훈위안의 꽃</code>이라고 생각하는 기능이다. 훈위안이 게임 개발에 가장 어울리는 모델이라고 생각하는 이유가 바로 이 기능 때문이다. 위에서 <code>Hunyuan3.1</code> 모델 기준, 메시 생성 단계에서 만들 수 있는 가장 작은 face수 옵션이 50k 였는데, 이정도 값은 간단한 프랍에 쓰기에는 상당히 무거운 편이다. 하지만 훈위안의 로우폴리 리토폴로지 기능을 사용하면은 정말 실제 게임에 당장 써도 될만한 수준으로 모델을 가볍게 바꾸어준다. 위 스크린샷에서 face수가 2584개로 줄어들은게 보일 것이다.</p>
<p>단순히 3D모델이 가벼워진걸 떠나서, 생성되는 토폴로지가 정말 스마트하게 생성된다. 이러한 3D 로우폴리 기능은 다른 3D생성 모델에도 있는것처럼 보이지만, 결제까지 하면서 이런저런 모델 다 써본 결과 그냥 훈위안의 품질이 압도적이다. 역시 게임기술은 중국이다.</p>
<h3 id="4-uv-생성">4. UV 생성</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/7ba3ead3-9922-4a45-8b21-8e9bb3d47531/image.png" alt="UV생성"></p>
<p>3D 모델에 색상을 입힐 때 텍스쳐(이미지)를 주로 이용하는데, 대충 이미지의 어느 위치에 있는 픽셀이 모델의 어떤 부분에 입혀질지를 정해주는 부분이다. 대강 계란의 밑둥, 몸통, 머리부분과 꼭지의 마감부분과 몸통 이렇게 5개로 나뉘어진 것으로 보인다.</p>
<h3 id="5-텍스쳐-생성">5. 텍스쳐 생성</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/f6dbd273-731d-4320-8f16-740ce72e0c89/image.png" alt="텍스쳐 생성 후"></p>
<p>방금 생성된 UV에 맞게 계란에 색칠을 해줄 텍스쳐를 생성하였다. 계란 몸통 부분과 꼭지 부분에 적절히 색이 잘 들어간 것을 볼 수 있다. 텍스쳐를 생성할 때에도 처음 모델을 생성했던 이미지를 그대로 사용하였는데, 같은 이미지를 넣더라도 생성할 때 마다 색감이 약간씩 다르게 나오므로 뭔가 색감이 맘에 안들면 계속해서 재생성해보면 된다.</p>
<h2 id="인게임-적용">인게임 적용</h2>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/2ab10446-75ef-4acb-a697-ad0864a29cc6/image.png" alt="유니티"></p>
<p>생성한 모델을 유니티로 가져왔다. 이렇게 보니 꼭지쪽에 약간 계란 살색이 섞인게 보였다. 이정도는 텍스쳐 이미지 파일 직접 열어서 수정할까 했는데 귀찮아서 그러진 않았다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/db4df9f9-7de7-4b84-bb6c-d8aeaea9d0ab/image.png" alt="계란 툰"></p>
<p>계란 수류탄도 기존 닭과 똑같이 카툰 렌더링을 적용하였다. 아웃라인 디테일이 약간 아쉽긴 하지만, 역시 카툰렌더링은 아웃라인이 있어야 제맛이다.</p>
<p>!youtube[6qCYaGne8H8?si=y1OX5K0sos3P9bc-]</p>
<p>혹시 카툰렌더링의 원리가 궁금하다면 해당 영상을 보시는것을 추천합니다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/20a7e5e6-7268-446a-8d90-710f07b38da5/image.png" alt="불꽃 이펙트"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/b82c43f0-6f16-45ec-8a05-a0811c1148db/image.png" alt="불꽃 텍스쳐"></th>
</tr>
</thead>
</table>
<p>수류탄의 심지부분에 불꽃 이펙트를 달아주었다. 우측의 이미지처럼 스프라이트 애니메이션을 이용하여 불꽃을 표현하고, 해당 이미지는 빌보드로 항상 카메라를 정면으로 바라보도록 설정하였다. 여기까지 작업하면 &#39;프로젝타일&#39;로써의 계란 수류탄은 완성이고, 마지막으로 캐릭터가 평상시에 들고 있는 계란 수류탄에는 <code>Point Light</code>를 추가해줘서 광원 효과를 주었다. 여기에 적 피격시 적용할 적당한 이펙트를 추가하고 인게임에서 테스트 해보았다.</p>
<p>!youtube[OJ1Mi0fEtgk?si=hMRs0wIbWh9tOGpk]</p>
<p>이렇게 AI를 통해 인게임에 쓸만한 3D모델링을 만들어 보았다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/7e9d4465-c00e-4bd6-b9c0-e730ae0562e2/image.png" alt="따봉"></p>
<p>모두 즐거운 게임개발 해보세요</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[라이브 게임 에셋 관리 개선기 - 3.DLC를 통한 에셋 패치 시스템]]></title>
            <link>https://velog.io/@eugene-doobu/%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EA%B2%8C%EC%9E%84-%EC%97%90%EC%85%8B-%EA%B4%80%EB%A6%AC-%EA%B0%9C%EC%84%A0%EA%B8%B0-3.DLC%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%97%90%EC%85%8B-%ED%8C%A8%EC%B9%98-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@eugene-doobu/%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EA%B2%8C%EC%9E%84-%EC%97%90%EC%85%8B-%EA%B4%80%EB%A6%AC-%EA%B0%9C%EC%84%A0%EA%B8%B0-3.DLC%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%97%90%EC%85%8B-%ED%8C%A8%EC%B9%98-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Thu, 20 Mar 2025 08:01:10 GMT</pubDate>
            <description><![CDATA[<h1 id="인트로">인트로</h1>
<hr>
<p>지금까지 클라이언트 단에서 사용하는 에셋들을 최적화하고 관리하는 방법들에 대해 알아보았다. 이제는 클라이언트만의 영역을 넘어 외부 서버에서 게임 에셋을 다운로드 하여 사용하는 패치 시스템에 대한 내용을 정리해 볼 것이다. 이번 편에서는 어드레서블 에셋과 AWS를 이용한 게임의 에셋 패치 시스템에 대한 내용을 다룬다. 관련 내용은 이전 글에서도 설명하였으니 이 글을 보기 전에 같이 보고오면 좋을 것이다.</p>
<p><a href="https://velog.io/@eugene-doobu/%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EA%B2%8C%EC%9E%84-%EC%97%90%EC%85%8B-%EA%B4%80%EB%A6%AC-%EA%B0%9C%EC%84%A0%EA%B8%B0-1.%EC%96%B4%EB%93%9C%EB%A0%88%EC%84%9C%EB%B8%94-%EC%97%90%EC%85%8B-%EB%8F%84%EC%9E%85">라이브-게임-에셋-관리-개선기-1.어드레서블-에셋-도입</a></p>
<p>1부. 어드레서블 에셋 도입<br>2부. 메모리 사용 구조 개선 / 리소스 최적화
번외1. 리소스 최적화 기법
<strong>3부. DLC를 통한 패치 시스템</strong> &lt;- 현재 글  </p>
<h1 id="에셋-패치-시스템이-필요한-이유">에셋 패치 시스템이 필요한 이유</h1>
<hr>
<p>DLC를 통한 패치 시스템이 모바일 프로젝트의 에셋 관리에서 가장 핵심적인 부분이 될 가능성이 높다. 이미 패치 시스템이 도입된 다른 게임들을 예시로 그 이유를 알아보자.</p>
<h3 id="1-에셋-메타데이터">1. 에셋 메타데이터</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/a7a9a353-b1e9-4fc7-993d-dc2d09a84320/image.png" alt="번들 구조"></p>
<p><em>&quot;출처: UniteSeoul2018 - 에셋번들 실전 가이드 (AssetBundle Best Practices)&quot;</em></p>
<p>기존의 방식대로 프로젝트 빌드에 모든 에셋을 포함시키면 일어나는 문제 중 하나가 <code>Lookup Table</code>과 프로젝트 로드에 필요한 <code>에셋 메타데이터</code>의 크기가 비대해진다는 것이다. 관련 데이터 파일들은 생각보다 무겁고, Lookup Table은 트리 구조(플랫폼에 따라 RB트리, BST)로 되어있는데 모든 에셋을 로드하기 위해 <code>nlogn</code>의 복잡도를 가지기 때문에 이를 로드하기 위한 시간이 생각보다 오래 걸릴 수 있다. 이 작업은 특히 디바이스의 사양이 낮은 모바일에 치명적으로 작용할 수 있다.</p>
<p>이를 에셋번들로 분리함으로써 번들마다 별도의 테이블을 갖게 만들고, 필요한 순간 비동기적으로 로드해서 사용하게 한다면 앱 실행시 에셋을 로드하는 부담을 크게 줄일 수 있게 된다.</p>
<h3 id="2-패치시스템--앱-용량-줄이기">2. 패치시스템 + 앱 용량 줄이기</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/16e88043-d629-48ed-b874-806831075476/image.png" alt="프커업뎃"></p>
<p><em>&quot;게임 프린세스 커넥트! Re:Dive 추가 다운로드 팝업&quot;</em></p>
<p>위 게임은 내가 몰래 숨어서 플레이하고 있는 미소녀 수집형 게임인 &#39;프리코네&#39;의 업데이트 팝업이다. 2024년 4월 18일 기준, iOS 앱스토어에 업로드 된 이 게임의 기본 크기는 <strong>290.1MB</strong> 이정도는 &#39;일반적인 서비스 어플&#39;들보다 약간 큰 정도이다. 하지만 게임을 실제로 플레이하기 위해서는 추가적으로 10.73GB의 데이터를 다운로드 받아야 한다.</p>
<p>이처럼 대부분의 모바일 게임들은 실제 앱스토어에는 앱을 구동시키기 위한 코드와 최소한의 에셋들만 포함되어있는 빌드파일을 올려놓고, 대부분의 핵심 에셋들은 별도로 다운받는 구조로 제작되어 있다. 위에 예시로 든 프리코네의 경우, 신규 캐릭터 획득시마다 그 캐릭터와 관련된 리소스나 게임 스토리를 보기 위한 리소스도 필요시마다 추가로 다운로드 하고 있어 실제로 게임 플레이를 하면서 다운받는 리소스의 양은 훨씬 많아질 수 있다.</p>
<p>이렇게 외부에서 리소스를 다운 받는 이유는 다음과 같다.</p>
<ol>
<li>앱 스토어에서 보이는 앱 용량이 진입장벽으로 느껴질 수 있다.</li>
<li>안드로이드는 apk로 앱을 업로드 시 150MB 이하로 유지해야 하는 전통이 있었다.</li>
<li>그리고 패치 시스템을 이용하면 <strong>번거로운 앱 심사 과정을 회피하고 업데이트</strong>를 할 수 있다</li>
</ol>
<p>사실 개인적인 입장에서는 3번이 핵심이라고 생각한다. 게임개발을 하다 보면 추가로 빌드 과정을 진행하는 것도 엄청난 일이며 이를 앱스토어에 심사받고 정해진 기간까지 심사가 통과할지 기도하는 일도 회사 팀원들의 마음을 힘들게 한다. 이 과정은 많은 시간과 정성이 필요하며, 때로는 예상치 못한 심사 지연이나 거절 사유를 해결하기 위해 추가적인 노력(과 야근)이 필요할 수 있다.</p>
<h3 id="앱-패치-심사">앱 패치 심사</h3>
<pre><code>애플의 앱스토어 심사는 구글 마켓에 비해
까다롭고 오래 걸리는 것으로 유명합니다.
몇 시간 정도 걸리는 안드로이드 심사와 달리

애플 앱 심사는 약 일주일 정도 걸리는데요.
일주일을 기다린 앱 심사 결과가
거부(reject)면 재심사까지 더해 약 2주일,
한번 더 reject되면 3주까지 늘어나기도 하죠.

그렇기에 앱스토어 심사를 가장 빨리 통과하는
방법은 거부(reject)당하지 않는 거죠.
- 디스이즈게임, [카드뉴스] 거부를 거부한다! 앱스토어 심사에서 리젝당하지 않는 법</code></pre><p><a href="https://www.thisisgame.com/webzine/nboard/257/?n=61254?n">https://www.thisisgame.com/webzine/nboard/257/?n=61254?n</a></p>
<p>모바일게임을 만들고 앱 마켓에 등록하기 위해서는 필수적으로 심사 과정을 거쳐야 한다. 앱 심사는 대부분 첫 심사가 매우 까다롭고 이후 심사는 상대적으로 너그럽게 진행되는 편인 것 같다. 하지만 첫 심사가 아니라고 방심하면은 안 된다. 앱 심사 거절은 언제 어디서 일어날지 모른다.</p>
<p>만약 코드 수정이 아닌 단순 리소스 수정이라면, 위와 같은 패치 시스템을 이용해서 앱 심사를 우회하고 바로 유저들에게 콘텐츠 업데이트를 할 수 있다. 한번 DLC 업데이트에 익숙해지고 나면,  단순 리소스 수정 같은 일로 하루 이상의 딜레이를 가질 수 있는 앱 심사 과정을 거쳐야 하는 것이 매우 번거로운 일로 느껴질 수 있을 것이다.</p>
<h1 id="적용사례">적용사례</h1>
<hr>
<p>에셋을 패치 하려면 유저들이 앱 실행 중 항상 접근할 수 있는 웹 서버에 사용할 에셋들을 업로드해야 한다. 나는 회사에서 사용 중인 AWS S3 버킷에 에셋을 업로드하고 게임에서 이를 로드해서 사용해보는 테스트를 하였다.</p>
<p>!youtube[6qXSP9vJ06s?si=RusxbkGKXnIVDUKv]</p>
<p>(위 영상에서 &#39;바로 로드 안됨&#39;이라고 나오는 부분의 문제는 해결했는데, 어이없게도 에셋 로드를 하고 로드가 완료되는 걸 기다리지 않고 오브젝트를 생성하고 있었다.)</p>
<p>위 어드레서블의 장점에서 &#39;Remote&#39;로 에셋을 저장해주는 세팅을 하면 에셋이 원격 저장소에 저장된다고 말했다. 위 영상이 바로 <strong>ActorPrefab</strong> 그룹을 Remote에 저장되도록 세팅하고 그 에셋들을 s 3원격 저장소에 올려서 사용하는 영상이다. 영상을 보면 알 수 있듯이, 어드레서블에서 제공해주는 기능만을 이용하면 프로젝트의 외딴곳에 에셋이 빌드되고 이를 수동으로 손으로 옮겨서 S3 버킷에 업로드해주는걸 볼 수 있을 것이다.</p>
<p>또한 현재 클라이언트에 있는 에셋이 최신 에셋인지 파악하기 위해 해시 값들을 관리해야 하고, 이러한 해시 값을 관리해주는 카탈로그 개념에 대해 인지하고 있어야 한다. 이에 대한 경험과 자세한 설명들은 추후 DLC를 통한 패치 시스템에 대한 글을 쓸 때 다뤄보려고 했으나..</p>
<h2 id="적용-중단">적용 중단</h2>
<p>다양한 이유로 해당 작업이 중단되었다. 내부 판단으로 굳이 패치시스템까지 적용할 필요가 없다는 결론이 났다. 해당 작업을 위해 리서치한 내용들이 있었는데 이 내용들은 다음 기회에 따로 다뤄보도록 하겠다.</p>
<ul>
<li>S3에서 에셋 관리 전략</li>
<li>프로젝트에서의 에셋 관리 전략</li>
<li>에셋 그룹화 및 번들 패킹 전략</li>
<li>CloudFront와 캐시무효화</li>
</ul>
<h1 id="끝">끝</h1>
<hr>
<p>드디어 첫 글을 작성한 2024년 7월부터 2025년 3월까지, 긴 시간동안 나태함에 빠져 빠르게 진행하지 못했던 라이브 게임 에셋 관리 개선기에 대한 글 시리즈 작성을 마무리 하였다. 결국 첫 글에서 약속했던 패치시스템을 적용하는 모습은 보여주진 못했지만 지금도 다양한 일을 진행하고 있다. 다음에 재미있는 주제로 새로운 시리즈를 연재할 수 있게 다양한 시도를 하면서 개발자 생활을 이어나가야겠다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/90050fa4-f365-4df1-892d-c069ea731f08/image.png" alt="끝"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[IK의 이해와 FABRIK 구현(Unity 기반)]]></title>
            <link>https://velog.io/@eugene-doobu/IK%EC%9D%98-%EC%9D%B4%ED%95%B4%EC%99%80-FABRIK-%EA%B5%AC%ED%98%84Unity-%EA%B8%B0%EB%B0%98</link>
            <guid>https://velog.io/@eugene-doobu/IK%EC%9D%98-%EC%9D%B4%ED%95%B4%EC%99%80-FABRIK-%EA%B5%AC%ED%98%84Unity-%EA%B8%B0%EB%B0%98</guid>
            <pubDate>Wed, 26 Feb 2025 17:15:08 GMT</pubDate>
            <description><![CDATA[<p>이번 글은 <a href="https://velog.io/@eugene-doobu/Feet-ik-%EB%95%85-%EC%9C%84%EB%A5%BC-%EA%B1%B8%EC%96%B4%EB%8B%A4%EB%8B%88%EA%B3%A0-%EC%9E%88%EC%96%B4%EC%9A%94">내 캐릭터의 발이 돌을 뚫고 들어갔어요</a>의 심화버전입니다. 해당 글을 읽지 않아도 글의 내용을 이해하는데 문제는 없지만, 유니티에서 Foot IK를 내장함수로 간단하게 구현하고 싶으신 분은 위 글을 보시는걸 추천드립니다.</p>
<h1 id="ikinverse-kinematics란">IK(Inverse Kinematics)란</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/4d088c8c-be0a-40d5-bfcb-021e7cc52885/image.png" alt="Hand Ik"></p>
<p>게임 속 모든 동작을 미리 애니메이션 데이터로 제작하는 것은 현실적으로 불가능하다. 예를 들어, 다양한 높이의 계단을 오르거나 불규칙한 지형을 걷는 경우, 애니메이션을 정해진 데이터로만 처리하면 발이 공중에 뜨거나 지형을 뚫는 등의 문제가 발생할 수 있다.  </p>
<p>이를 해결하기 위해 <strong>실시간으로 본(Bone)의 움직임을 조정하는 절차적 애니메이션(Procedural Animation)</strong> 기법이 사용된다. 그중 가장 대표적인 방법이 <strong>IK(Inverse Kinematics, 역운동학)</strong> 이다. IK는 <strong>엔드 이펙터(End Effector, 손이나 발과 같은 본의 끝 부분)의 목표 위치를 설정하면, 이를 만족하는 본의 회전 값을 자동으로 계산하는 기법</strong>이다.  </p>
<p>이와 반대되는 개념으로는 FK(Forward Kinematics, 순방향 운동학)가 있는데 두 방식을 비교해보자.</p>
<ul>
<li><strong>FK</strong>: 부모 본의 회전을 직접 제어하여 자식 본이 따라가도록 설정하는 방식.  </li>
<li><strong>IK</strong>: 목표 위치(예: 손, 발)를 기준으로 본들의 회전 값을 자동으로 계산하는 방식.  </li>
</ul>
<p>실제 게임에서는 <strong>IK를 활용하여 애니메이션을 보정</strong>함으로써 더 자연스러운 동작을 만들 수 있다. 예를 들어 캐릭터가 걷거나 뛰는 애니메이션에서 발이 지형을 따라가도록 자동 조정하거나, 컵이나 총과 같은 물체 자연스럽게 들기, 주먹의 타격 지점을 정확한 곳으로 옮기는 방식 등으로 응용하여 사용할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/db154146-1951-4727-9802-1cdd2728bb2c/image.png" alt="gun"></p>
<p>(윗 짤처럼 캐릭터에게 총을 잡게 하려면 Hand IK의 적절한 이용이 필요하다)</p>
<h3 id="ik-모델링">IK 모델링</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/b0649694-5260-4c1a-83b6-7a49e824acf5/image.png" alt="iKJoint"></p>
<p>IK시스템에서는 주로 joint를 기준으로 회전과 포지션을 정의한다.</p>
<ul>
<li>child가 없는 joint들을 end-effector라고 한다.</li>
<li>모든 joint들은 <strong>parent-child</strong>를 사용하며, 유니티의 Transform처럼 parent joint가 바뀌면 child joint도 영향을 받는다.</li>
<li>IK에서는 체인 위주의 변화가 일어나며 체인 외의 joint들은 크게 변하지 않는다.(예를 들어 왼손의 IK를 적용한다고 오른손의 본 상태가 크게 바뀌지 않는다)</li>
</ul>
<p>이에 따라 아래에서는 각 joint들을 <code>Effector</code>, IK 계산시 서로 영향을 받는 joint들을 묶어 <code>IKChain</code>으로 이름을 붙여 구현할 것이다.</p>
<h1 id="2-유니티-에서의-휴머노이드">2. 유니티 에서의 휴머노이드</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/9b325512-9a15-40a6-85b3-27819a0ed562/image.png" alt="휴머노이드 본"></p>
<p>(사진 출처: <a href="https://discussions.unity.com/t/ongoing-humanoid-issues-in-animation-rigging/833975/6">Ongoing humanoid issues in Animation Rigging</a>)</p>
<p>게임 속 캐릭터의 본 구조는 <strong>휴머노이드(Humanoid) 아바타</strong>를 기반으로 제작되는 경우가 많다. Unity의 <strong>Humanoid Rig</strong>는 사람이 가지는 기본적인 본 구조를 정의한 표준 구조이며 일반적으로 게임 제작에서 가장 많이 사용되는 아바타의 유형이라고 볼 수 있다. 아는 사람은 다 아는 <a href="https://www.mixamo.com/#/">Mixamo</a>에서는 실제 사람의 움직임을 모션캡쳐한 휴머노이드 아바타의 애니메이션을들 다운 받을 수 있다. 캐릭터의 애니메이션을 만들 때에는 주로 이러한 모션 캡쳐 데이터에 IK등을 통한 후처리로 자연스러운 애니메이션을 만들어 게임에 적용한다고 보면 된다.</p>
<h2 id="휴머노이드-아바타의-특징"><strong>휴머노이드 아바타의 특징</strong></h2>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/9df7db7e-e813-43ba-b054-ccd15c05a9c4/image.png" alt="T-pose"></p>
<p>(사진 출처: <a href="https://docs.unity3d.com/Manual/UsingHumanoidChars.html">Unity:UsingHumanoidChars</a>)</p>
<p>Unity의 <strong>Humanoid Rig</strong>는 인간 골격을 기반으로 설계된 표준 본 구조를 제공하며, <strong>모션 캡처 데이터 및 IK 시스템과 결합하여 자연스러운 애니메이션을 생성할 수 있도록 설계</strong>되었다. 일반적으로 <strong>모델링, 리깅(Rigging), 스키닝(Skinning)</strong>의 과정을 거쳐 Unity의 애니메이션 시스템에서 사용된다.</p>
<h3 id="humanoid-모델의-기본-구조">Humanoid 모델의 기본 구조</h3>
<ul>
<li>휴머노이드 모델은 <strong>실제 인간 골격과 유사한 최소 15개 이상의 본(Bone)으로 구성</strong>된다.  </li>
<li>주요 본 구조:<ul>
<li><strong>Hips (골반)</strong> → Spine (척추) → Chest (가슴) → Neck (목) → Head (머리)</li>
<li><strong>Hips (골반)</strong> → UpperLeg (허벅지) → Leg (정강이) → Foot (발) → Toe (발가락)</li>
<li><strong>Chest (가슴)</strong> → Shoulder (어깨) → Arm (팔) → Forearm (팔뚝) → Hand (손)</li>
</ul>
</li>
<li>이러한 구조는 Unity의 <strong>Avatar 시스템과 매칭</strong>되며, <strong>자동으로 본을 매핑하여 적용할 수 있음</strong>.</li>
<li>팔과 다리는 2개씩 있으므로 일관성이 있는 명명 규칙을 사용해야 함 (예: 왼팔은 “arm_L”, 오른팔은 “arm_R”)</li>
<li><strong>T-포즈(T-Pose) 또는 A-포즈(A-Pose)로 모델링</strong>하는 것이 일반적.</li>
</ul>
<ul>
<li><strong>머리, 상체, 팔, 다리 등 필수적인 본(Bone) 계층이 포함</strong>되어 있다.  </li>
<li>IK를 적용할 때, <strong>제약 조건(Constraints)을 고려해야 현실적인 움직임을 만들 수 있음</strong>.  </li>
</ul>
<p>그래픽스에서 사용하는 human body는 수많은 <strong>joint</strong>들로 이루어져 있고, 이들은 서로 다른 <strong>DoF(Degree of Freedom)</strong>와 Restriction(해당 본은 어느 범위 안에서만 회전 가능 등등..)을 가지고 있다.</p>
<h3 id="휴머노이드-ik-적용-시-제약-조건">휴머노이드 IK 적용 시 제약 조건</h3>
<p>IK는 자유롭게 본을 움직일 수 있지만, <strong>현실적인 움직임을 위해서는 물리적 제약을 추가해야 한다</strong>.  </p>
<p><strong>관절의 자연스러운 각도 제한</strong>  </p>
<ul>
<li>인간의 관절은 일정한 범위를 넘어서는 회전이 불가능하다.  </li>
<li>예: 팔꿈치는 뒤쪽으로 꺾일 수 없으며, 무릎도 특정 각도 이상 접히지 않는다.  </li>
</ul>
<p><strong>본들의 충돌 방지</strong>  </p>
<ul>
<li>관절이 서로 겹치는 비현실적인 동작을 방지해야 한다.  </li>
<li>예: 손이 허리를 통과하는 모션이 나오지 않도록 함.</li>
</ul>
<h1 id="3-ik-기법들-비교">3. IK 기법들 비교</h1>
<hr>
<p>IK는 다양한 수학적 기법들을 통해 구현할 수 있다. 대표적인 기법인 <strong>CCD</strong>, <strong>FABRIK</strong>, <strong>자코비안 행렬기반</strong> 세 가지를 비교해보겠다.</p>
<h3 id="자코비안-행렬">자코비안 행렬</h3>
<p>가장 기본적인 접근법으로 자코비안 행렬을 계산하여 선형 근사하는 방법이다. 로봇공학 등에서 많이 사용하는 <strong>해석적/수치적 방법</strong>으로, <strong>자코비안 행렬</strong>을 이용하여 관절 각도의 변화를 계산한다. 엔드 이펙터의 위치 변화와 관절 각도 변화 사이의 관계를 나타내는 <strong>Jacobian 행렬 (J)</strong>을 구성한 뒤, 목표와 현재의 위치 차이를 줄이도록 (J)의 <strong>역행렬(또는 의사역행렬)</strong>을 사용해 각도 변화 ($\Delta$ $\Theta$)를 계산한다.</p>
<ul>
<li>가장 일반성이 높은 방법으로 관절 개수, 관절 구조, 다중 end-effector등 <strong>복잡한 시스템도 통일된 수학적 틀로</strong> 풀 수 있다. Joint에 한계나 추가 목표를 반영하려면 목적함수에 제약을 추가하거나 DLS방식으로 가중치를 주는 등 확장이 용이하다.</li>
<li><strong>정밀한 제어</strong>가 가능하며 오차를 매우 작게 줄일 수 있다.</li>
<li><strong>계산 비용이 크고 구현 난이도</strong>가 높다. 관절 수가 많으면 <strong>(J)</strong>가 커지고 매 프레임 <strong>행렬 연산 (역행렬)</strong>을 해야 하므로, 실시간 게임에선 부담이 될 수 있다. 특히 3D 캐릭터에 수십 개 관절이 있을 경우 자코비안 계산은 비효율적일 수 있다.</li>
</ul>
<p>일반적으로 실시간 게임에서는 잘 활용되지 않는 방법이지만 로봇 시뮬레이션 등에는 활용하고 있으며, 유니티 엔진에서는 <a href="https://docs.unity3d.com/6000.0/Documentation/ScriptReference/ArticulationJacobian.html#:~:text=Scripting%20API%3A%20ArticulationJacobian%20,the%20reduced%20coordinate%20space">ArticulationJacobian</a>와 같은 PhysicsModule API가 제공되기도 한다.</p>
<h3 id="ccd">CCD</h3>
<p>FABRIK 등장 이전에 많이 쓰이던 방식이다. CCD는 <strong>순환 좌표 하강법</strong>이라는 이름 그대로, <strong>관절을 하나씩 순차적으로 조정</strong>하여 end effector(말단 연골 또는 말단 부위)이 목표에 다가가도록 하는 휴리스틱 방법이다. 체인의 <strong>끝 관절부터 시작하여 거꾸로(base 방향)</strong> 각 관절을 차례로 회전시켜 end effector가 목표 위치에 최대한 가까워지도록 반복한다.</p>
<ul>
<li>각 반복(iteration)에서 <strong>가장 끝 관절</strong>(end effector에 가장 가까운 관절)을 목표 방향으로 회전시켜 end effector를 목표에 근접시킨 후, 그 다음 관절로 이동해 같은 작업을 수행한다. 이러한 <strong>역순으로의 반복적인 각도 조절</strong>을 통해 오차를 점차 줄여나간다.  </li>
<li>수학적으로는 특별한 행렬 계산 없이, <strong>기하학적 각도 계산</strong>만으로 구현된다. 예를 들어 각 관절에서, end effector와 목표를 잇는 벡터 사이의 각도를 계산하고, 그 각도만큼 관절을 회전시키는 식이다.</li>
<li>이러한 작업을 만족스러운 값이 나올 때 까지 반복하며, <strong>구현이 간단하고 비용이 적기 때문에 실시간 IK에서 활용</strong>할 수 있다.</li>
<li>단점으로는 많은 반복이 필요할 수 있고, 관절이 많은 체인이거나 초기 오차가 큰 경우 목표에 도달하기 위한 반복 횟수가 많아질 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/1990a516-62d9-44cc-8533-c0c3e2510906/image.png" alt="CCD"></p>
<h3 id="fabrik">FABRIK</h3>
<p>FABRIK는 <strong>Forward And Backward Reaching Inverse Kinematics</strong>의 약자로, <strong>전진-후진 반복</strong>을 통해 관절 위치를 계산하는 기법이다. CCD처럼 반복적이지만, 각 단계에서 <strong>회전 각도가 아닌 관절의 위치</strong>를 직접 계산한다는 차이가 있다. 알고리듬은 다음 두 단계로 이루어진 <strong>한 번의 iteration</strong>을 반복한다. FABRIK은 <strong>언리얼에서도 유니티에서도 주로 사용</strong>하고 있는 기법이다.</p>
<ul>
<li><strong>수학적 처리:</strong> FABRIK는 주로 <strong>벡터 산술과 거리 계산</strong>으로 이루어진다. 각 관절 간의 <strong>거리(d_i)</strong>를 초기 설정 때 저장해두고, 알고리즘 진행 중에는 <strong>정규화된 방향 벡터</strong>를 따라 해당 거리만큼 점을 이동시키는 계산을 반복합니다. 복잡한 행렬 연산 없이 기하학적으로 위치를 결정하는 방식이다.  </li>
<li><strong>목표 도달 가능 여부:</strong> 알고리즘 첫 단계에서 <strong>목표가 reachable한지</strong> 검사한다. 루트에서 목표까지의 거리와 모든 뼈 길이의 합을 비교하여, 목표가 사슬 최대 길이보다 멀면 <strong>도달 불가능</strong>하다고 판단한다. 이 경우 FABRIK은 체인을 <strong>목표 방향으로 최대한 뻗은 직선 형태</strong>로 배치하고 종료한다(end effector는 목표에는 못 미치지만 최대한 가까워짐)</li>
</ul>
<p><strong>Foward</strong></p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/33a8939c-0b50-46ad-a3c3-c62915339bd0/image.png" alt="Fabrik-Foward"></p>
<p>체인의 <strong>end effector를 목표 위치로 바로 이동</strong>시킨 후, 끝역방향으로 차례로 관절들을 재배치한다. 이 때 <strong>뼈의 길이</strong>를 보존해야 하므로, 자식 관절의 새 위치로부터 <strong>본래 길이만큼 떨어진 지점</strong>에 상위 관절을 배치한다. 이렇게 하면 end effector부터 루트까지 관절들이 목표 쪽으로 끌려온다.</p>
<p><strong>Backward</strong></p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/9ef3a116-192a-41aa-b6d6-d93105788081/image.png" alt="Fabrik-Backward"></p>
<p>Backward 단계가 끝나면 체인의 <strong>루트 관절을 원래 위치</strong>로 돌려놓는다. 그리고 이제 <strong>순방향(루트→말단)</strong>으로 진행하면서, 방금 정한 루트 위치로부터 차례로 자식 관절들을 <strong>뼈 길이를 유지</strong>하며 배치합니다. 즉, 루트에서 2번째 관절을 루트로부터 제자리 뼈 길이만큼 떨어진 곳에 놓고, 그 다음 관절도 이전 관절로부터 일정 거리 떨어뜨리는 식으로 end effector까지 진행합니다.</p>
<p><strong>반복</strong></p>
<p>위와 같은 과정을 원하는 지정된 횟수만큼 반복한다. 중간에 오차범위가 원하는 범위까지 좁혀지면 반복을 그만둘 수도 있다.</p>
<p>🔗 <a href="https://www.youtube.com/watch?v=tN6RQ4yrNPU">유튜브 영상: FABRIK IK 알고리즘 개요</a><br>🔗 <a href="https://velog.io/@tjswodud/GE2022-6.-Heuristic-Inverse-Kinematics-Algorithms">IK 알고리즘 비교 및 FABRIK 설명</a></p>
<h4 id="fabrik의-장단점">FABRIK의 장단점</h4>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/bc024020-8b13-476d-848e-fa1256800b4b/image.png" alt="속도비교"></p>
<p>자료출처: <a href="https://www.researchgate.net/publication/220632147_FABRIK_A_fast_iterative_solver_for_the_Inverse_Kinematics_problem">FABRIK: A fast, iterative solver for the Inverse Kinematics problem
</a></p>
<p><strong>장점:</strong>  </p>
<ul>
<li><strong>수렴 속도가 빠르고 안정적</strong>이다. 논문 결과 FABRIK은 <strong>CCD보다 약 10배 빠르고, 자코비안 기반 방법보다 1000배 빠르다</strong>고 보고되었다.</li>
<li><strong>구현이 비교적 쉽고 범용적</strong>이다. 각 단계가 단순한 거리 계산과 보간으로 이뤄져 있어 이해하기 쉬우며, <strong>임의 길이와 구조의 체인</strong>에도 적용 가능하다.</li>
<li><strong>다중 end-effector 확장</strong>이 가능하다. 휴머노이드 캐릭터처럼 <strong>여러 개의 말단(손, 발 등)</strong>을 가진 모델에도 FABRIK은 확장된 알고리즘으로 대응할 수 있다.</li>
</ul>
<p><strong>단점:</strong>  </p>
<ul>
<li><strong>관절 회전 제한을 고려하지 않는다</strong>는 한계가 있다. FABRIK은 거리만 유지하며 위치를 옮기기 때문에, 관절이 가질 수 없는 각도로 배치될 수도 있다.</li>
<li><strong>목표에 매우 근접하게 수렴하기 위해서는 반복</strong>이 필요하다. 1~2번의 iteration으로 대강의 자세는 나오지만 오차를 수 픽셀 이하로 줄이려면 여러 번 더 반복해야 한다. 반복 계산 자체는 가볍지만 리얼타임 렌더러에서 <strong>프레임 단위로 많은 IK를 풀 때</strong>는 이 역시 고려해야 한다.</li>
</ul>
<h1 id="4-유니티에서-fabrik-적용">4. 유니티에서 FABRIK 적용</h1>
<hr>
<p>유니티의 Humanoid 시스템과 Animation시스템과 연동될 수 있도록 클래스들을 구현하였다. 기존 오브젝트에 추가적인 컴포넌트를 추가하지 않고 필드 하나(<code>HumanoidFabrk</code>)만 추가하여 동작하도록 해보았다. 구현한 클래스에대한 설명이다.</p>
<h3 id="1-humanoidfabrik">1. HumanoidFabrik</h3>
<p><code>HumanoidFabrik</code> 클래스는 <strong>Humanoid</strong> 모델에 FABRIK IK를 적용하기 위한 인터페이스이자 IK 솔버 관리 클래스이다. IK 체인들과 effector들을 통합 관리하며, 각 프레임에 IK 계산을 수행하는 <strong>중앙 제어</strong> 역할을 한다.</p>
<h3 id="2-fabrikchain">2. FabrikChain</h3>
<p><code>FabrikChain</code> 클래스는 <strong>하나의 관절 체인</strong>에 대해 FABRIK 알고리즘을 적용하는 <strong>IK 솔버 클래스</strong>이다. 이 클래스는 한 개의 end effector와 그것에 이르는 일련의 관절들을 나타내며, FABRIK 수식을 실제로 계산하여 <strong>관절들의 새로운 위치를 결정</strong>한다.</p>
<ul>
<li><p><strong>IK 과정</strong>  </p>
<ol>
<li><p><strong>초기 상태 갱신:</strong></p>
<ul>
<li><strong>$$\mathbf{p}_i$$</strong> : 각 본의 초기(루트 공간) 위치<ul>
<li><strong>$$L_i$$</strong> : 인접 본 사이의 링크 길이</li>
<li><strong>$$\mathbf{d}_i$$</strong> : 각 본의 초기 자식 방향 (즉, $$\mathbf{p}_{i+1} - \mathbf{p}_i$$)</li>
<li><strong>$$\mathbf{R}_i$$</strong> : 각 본의 초기 회전 (루트 공간 기준)</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>목표와의 거리 확인:</strong>  </p>
<ul>
<li>만약 목표 $$\mathbf{t}$$ 와 루트 본 사이의 거리가 전체 체인 길이보다 크면, 본들을 단순하게 늘린다.</li>
</ul>
</li>
<li><p><strong>반복적 Forward-Backward 과정:</strong>  </p>
<ul>
<li><p><strong>Backward 단계:</strong>  </p>
<ul>
<li><p>마지막 본(엔드 이펙터)를 $$\mathbf{p}_n \gets \mathbf{t}$$ 로 고정한 후,<br>$$i = n-1$$ 부터 $$i = 1$$ 까지 아래의 수식으로 업데이트</p>
<p>$$
\mathbf{p}<em>i \gets \mathbf{p}</em>{i+1} + L_i \cdot \frac{\mathbf{p}<em>i - \mathbf{p}</em>{i+1}}{|\mathbf{p}<em>i - \mathbf{p}</em>{i+1}|}
$$</p>
</li>
</ul>
</li>
<li><p><strong>Forward 단계:</strong>  </p>
<ul>
<li><p>루트 본($$i = 0$$)의 위치는 고정한 채,  $$i = 1$$ 부터 $$i = n$$ 까지 업데이트</p>
<p>$$
\mathbf{p}<em>i \gets \mathbf{p}</em>{i-1} + L_{i-1} \cdot \frac{\mathbf{p}<em>i - \mathbf{p}</em>{i-1}}{|\mathbf{p}<em>i - \mathbf{p}</em>{i-1}|}
$$</p>
</li>
</ul>
</li>
<li><p>오차 $$|\mathbf{p}_n - \mathbf{t}|^2$$ 가 미리 정한 $$\Delta^2$$ 이하가 될 때까지 반복</p>
</li>
</ul>
</li>
</ol>
</li>
</ul>
<h3 id="3-humanoidfabrikeffector">3. HumanoidFabrikEffector</h3>
<p>FABRIK 체인을 구성할 때, 트랜스폼 값의 저장과 부모-자식 관계를 나타내는 역할을 수행한다.</p>
<h2 id="41-코드분석">4.1. 코드분석</h2>
<h3 id="0-humanoid-util">0. Humanoid Util</h3>
<pre><code class="language-c#">public static class HumanoidUtils
{
    private static readonly Dictionary&lt;HumanBodyBones, HumanBodyBones&gt; HumanBodyBonesParentMap = new()
    {
        // Hips → Spine → Chest → Neck → Head
        { HumanBodyBones.Spine, HumanBodyBones.Hips },
        { HumanBodyBones.Chest, HumanBodyBones.Spine },
        { HumanBodyBones.Neck, HumanBodyBones.Chest },
        { HumanBodyBones.Head, HumanBodyBones.Neck },

        // Hips → UpperLeg → LowerLeg → Foot → Toes
        { HumanBodyBones.LeftUpperLeg, HumanBodyBones.Hips },
        { HumanBodyBones.LeftLowerLeg, HumanBodyBones.LeftUpperLeg },
        { HumanBodyBones.LeftFoot, HumanBodyBones.LeftLowerLeg },

        { HumanBodyBones.RightUpperLeg, HumanBodyBones.Hips },
        { HumanBodyBones.RightLowerLeg, HumanBodyBones.RightUpperLeg },
        { HumanBodyBones.RightFoot, HumanBodyBones.RightLowerLeg },

        // Spine - Chest - Shoulders - Arm - Forearm - Hand
        // UpperChest는 인덱스 문제로 사용하지 않음
        // { HumanBodyBones.UpperChest, HumanBodyBones.Chest },

        { HumanBodyBones.LeftShoulder, HumanBodyBones.Chest /* UpperChest */ },
        { HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftShoulder },
        { HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftUpperArm },
        { HumanBodyBones.LeftHand, HumanBodyBones.LeftLowerArm },

        { HumanBodyBones.RightShoulder, HumanBodyBones.Chest /* UpperChest */ },
        { HumanBodyBones.RightUpperArm, HumanBodyBones.RightShoulder },
        { HumanBodyBones.RightLowerArm, HumanBodyBones.RightUpperArm },
        { HumanBodyBones.RightHand, HumanBodyBones.RightLowerArm },
    };


    public static readonly Dictionary&lt;HumanBodyBones, List&lt;HumanBodyBones&gt;?&gt; HumanBodyBonesChildrenMap = new()
    {
        { HumanBodyBones.Hips, new List&lt;HumanBodyBones&gt; { HumanBodyBones.Spine, HumanBodyBones.LeftUpperLeg, HumanBodyBones.RightUpperLeg } },
        { HumanBodyBones.Spine, new List&lt;HumanBodyBones&gt; { HumanBodyBones.Chest } },
        { HumanBodyBones.Chest, new List&lt;HumanBodyBones&gt; { HumanBodyBones.Neck, HumanBodyBones.LeftShoulder, HumanBodyBones.RightShoulder } },
        { HumanBodyBones.Neck, new List&lt;HumanBodyBones&gt; { HumanBodyBones.Head } },
        // HumanBodyBonesParentMap 관계를 역으로 저장
        ...
    };

    public static readonly SortedSet&lt;HumanBodyBones&gt; IkEffectorBones = new()
    {
        HumanBodyBones.Hips,
        HumanBodyBones.Spine,
        // 위 dict에서 사용하는 bone들을 저장
        ...
    };
    ...
}</code></pre>
<p>휴머노이드 본을 어떻게 해석하고 체인을 구성할지에 대한 값들이 저장된 데이터를 <code>HumanoidUtils</code>클래스에 저장해두었다. 본들의 연결 구조는 위 <code>#Humanoid 모델의 기본 구조</code> 챕터의 내용을 참고하여 구성하였다.</p>
<p>유니티의 enum class인 <code>HumanBodyBones</code>를 분석해보니 휴머노이드 아바타에서의 본은 부모본보다 value가 크게 설정되어 있는 것으로 보여 <strong>enum value의 값을 비교하여 부모본인지 아닌지 체크할 수 있게 구성</strong>해보았다.</p>
<p>(이상하게도 <code>UpperChest</code>는 예외적으로 큰 값이 설정되어 있어 해당 본은 제외하였다)</p>
<h3 id="1-humanoidfabrik-1">1. HumanoidFabrik</h3>
<pre><code class="language-c#">
public class HumanoidFabrik
{
    private Animator _animator;

    private readonly Dictionary&lt;HumanBodyBones, HumanoidFabrikEffector&gt; _effectors = new();
    private readonly Dictionary&lt;HumanBodyBones, FabrikChain&gt; _endChains = new();
    private readonly List&lt;FabrikChain&gt; _chains = new();

    public void Initialize(Animator animator)
    {
        _animator = animator;

        InitializeEffectors(animator);
        var rootEffector = _effectors[HumanBodyBones.Hips];

        _rootChain = LoadSystem(rootEffector);
        _chains.Sort((x, y) =&gt; y.Layer.CompareTo(x.Layer));
    }
    ....
}</code></pre>
<p>Humanoid Fabrik은 루트인 <code>Hips</code>본을 시작으로 각 본에 해당하는 <strong>Effector</strong>와 IK를 기반으로 본들의 트랜스폼을 계산할 <strong>Chain</strong>을 초기화한다. 각 이펙터들은 <code>_effectors</code> 딕셔너리에 Bone을 키로 접근할 수 있도록 초기화된다. 이후 <code>LoadSystem</code> 메서드를 호출하여 각 체인들을 초기화하고 Layer순으로 오름차순 정렬을 시도한다.</p>
<pre><code class="language-c#">private FabrikChain LoadSystem(HumanoidFabrikEffector effector, FabrikChain parent = null, int layer = 0)
{
    var effectors = new List&lt;HumanoidFabrikEffector&gt;();
    if (parent != null)
        effectors.Add(parent.EndEffector);
    List&lt;HumanBodyBones&gt; childrenBones = null;
    while (effector != null)
    {
        childrenBones = HumanoidUtils.GetChildrenBones(effector.Bone);
        effectors.Add(effector);
        if (childrenBones == null)
            break;
        // childCount &gt; 1 is a new sub-base
        if (childrenBones.Count != 1)
            break;
            effector = _effectors[childrenBones[0]];
    }
    var chain = new FabrikChain(effectors, layer, _animator, _effectors);
    _chains.Add(chain);

    if (chain.IsEndChain)
        _endChains.Add(chain.EndEffector.Bone, chain);
    else if (childrenBones != null)
        foreach (var child in childrenBones)
            LoadSystem(_effectors[child], chain, layer + 1);
        return chain;
}</code></pre>
<p>체인을 초기화하는 LoadSystem함수이다. <code>_endChains</code>에는 EndEffector의 Bone을 기준으로 각 체인에 접근할 수 있게 초기화 되며, 초기화 과정은 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/cba685ea-e3de-4a69-b353-6a2fe7133e1d/image.png" alt="chain"></p>
<p>붉은 원은 사용하는 본들 중에서 여러 개의 Sub-System으로 분리되는 부분이고, 파란 원은 사용하는 본들중에서 EndBone에 해당하는 부분이다. 초기화는 Hips부터 시작되므로, Hips-Chain이 Layer 0값을 가지고 초기화되며, 이후 다른 Sub-System으로 분리되는 체인들은 전부 Layer1의 값을 가진다. 이후 Chest에서 다시 한번 Sub-System으로 분리되어 생성되는 체인은 Layer2값을 갖는다. 이를 정리하면 아래와 같다.</p>
<ul>
<li>hips chaine - Layer 0</li>
<li>Right Foot - Layer 1 <code>end</code></li>
<li>Left Foot - Layer 1 <code>end</code></li>
<li>Chest - Layer 1</li>
<li>RightHand - Layer 2 <code>end</code></li>
<li>LeftHand -Layer 2 <code>end</code> </li>
<li>Head - Layer 2 <code>end</code></li>
</ul>
<p>만약 Eye본이나 손가락과 관련된 본도 제어하는 경우 더 많은 Layer와 end chain들이 생성될 것이며 현재 구현을 사용하기 위해 end-chain과 관련된 로직들을 변경해서 사용해야 할 것이다.</p>
<p>이러한 시스템이 성공적으로 초기화 되면, IK Target을 설정하고 Solve IK 함수를 호출하여 원하는 포지션에 End Bone을 위치시키고, End Bone의 체인에 포함된 본들이 자연스럽게 조정될 수 있을 것이다.</p>
<h3 id="2-fabrikchain-1">2. FabrikChain</h3>
<pre><code class="language-c#">public FabrikChain(List&lt;HumanoidFabrikEffector&gt; effectors, int layer, Animator animator, IReadOnlyDictionary&lt;HumanBodyBones, HumanoidFabrikEffector&gt; dictionary)
{
    ...
    //initial array
    Bones              = new Transform[ChainLength + 1];
    Positions          = new Vector3[ChainLength + 1];
    BonesLength        = new float[ChainLength];
    StartDirectionSucc = new Vector3[ChainLength + 1];
    StartRotationBone  = new Quaternion[ChainLength + 1];
    ...

    // Init Pole
    ...

    //init data
    var current = EndEffector;
    CompleteLength = 0;
    for (var i = Bones.Length - 1; i &gt;= 0; i--)
    {
        Bones[i]             = current.Transform;
        StartRotationBone[i] = GetRotationRootSpace(current.Transform);

        if (i == Bones.Length - 1)
        {
            //leaf
            StartDirectionSucc[i] = GetPositionRootSpace(Target) - GetPositionRootSpace(current.Transform);
        }
        else
        {
            //mid bone
            StartDirectionSucc[i] =  GetPositionRootSpace(Bones[i + 1]) - GetPositionRootSpace(current.Transform);
            BonesLength[i]        =  StartDirectionSucc[i].magnitude;
            CompleteLength        += BonesLength[i];
        }

        var parent = HumanoidUtils.GetParentBone(current.Bone);
        if (parent == HumanBodyBones.LastBone)
            break;
        current = dictionary[parent];
    }
}</code></pre>
<p>생성자에서는 체인에게 사용하는 어레이들을 초기화하는 부분을 집중해서 보면 될 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/0320b611-386f-4ed0-9a61-8d0346f9ab57/image.png" alt="UnityEventLogic"></p>
<p>유니티 이벤트 호출 순서를 보면 <code>Update</code> -&gt; <code>ProcessAnimation</code> -&gt; <code>OnAnimationIK</code> -&gt; <code>LateUpdate</code>순서로 호출되는 것을 확인할 수 있다. 따라서 애니메이션이 적용된 후 다시 본의 위치를 재조정하려면 <code>LateUpdate</code> 타이밍에 <code>SolveIK()</code>를 호출하는 것이 적절하다.</p>
<pre><code class="language-c#">public void SolveIK()
{
    ...
    var targetPosition = GetPositionRootSpace(_target);
    var targetRotation = GetRotationRootSpace(_target);

    var isReachable = (targetPosition - GetPositionRootSpace(_bones[0].Transform)).sqrMagnitude &lt;
                      _completeLength * _completeLength;
    if (!isReachable)
    {
        // Just stretch it
        var direction = (targetPosition - _positions[0]).normalized;
        for (int i = 1; i &lt; _positions.Length; i++)
            _positions[i] = _positions[i - 1] + direction * _bonesLength[i - 1];
    }
    else
    {
        for (var i = 0; i &lt; _positions.Length - 1; i++)
            _positions[i + 1] = Vector3.Lerp(_positions[i + 1], _positions[i] + _startDirectionSucc[i], SNAP_BACK_STRENGTH);

        for (var iteration = 0; iteration &lt; ITERATIONS; iteration++)
        {
            // Back
            for (var i = _positions.Length - 1; i &gt; 0; i--)
            {
                if (i == _positions.Length - 1)
                    _positions[i] = targetPosition;
                else
                    _positions[i] = _positions[i + 1] + (_positions[i] - _positions[i + 1]).normalized * _bonesLength[i];
            }

            // Forward
            for (var i = 1; i &lt; _positions.Length; i++)
                _positions[i] = _positions[i - 1] + (_positions[i] - _positions[i - 1]).normalized * _bonesLength[i - 1];

            // Close enough
            if ((_positions[^1] - targetPosition).sqrMagnitude &lt; DELTA * DELTA)
                break;
        }
    }

    // Move towards pole
    ...

    // Set position &amp; rotation
    ...
}</code></pre>
<p>이제 실질적으로 IK를 계산하는 부분의 코드를 살펴보자. 닿지 못하는 경우 stretch되는 부분과 Back/Forward가 계산되고 반복되는 과정이 위에 적힌 수식 그대로 구현되었음을 확인할 수 있다. Backword과정에서 End-Effector를 target으로 이동시키고, 루트를 제외한 모든 본들을 조정시킨 후 기존의 루트본에서 다시 Forward를 계산해주고 있다.</p>
<pre><code class="language-c#">// move towards pole
var polePosition = GetPositionRootSpace(Pole);
for (int i = 1; i &lt; Positions.Length - 1; i++)
{
    var plane = new Plane(Positions[i + 1] - Positions[i - 1], Positions[i - 1]);
    var projectedPole = plane.ClosestPointOnPlane(polePosition);
    var projectedBone = plane.ClosestPointOnPlane(Positions[i]);
    var angle = Vector3.SignedAngle(projectedBone - Positions[i - 1], projectedPole - Positions[i - 1], plane.normal);
    Positions[i] = Quaternion.AngleAxis(angle, plane.normal) * (Positions[i] - Positions[i - 1]) + Positions[i - 1];
}</code></pre>
<p>Pole은 IK문제를 해결할 때 본의 방향을 제어하는데 도움을 준다. 신체적으로 부자연스러운 회전을 방지하거나 비일관적 동작을 막기 위해 사용된다. 아래 움짤은 <code>Unreal Engine</code>에서 비슷한 역할을 하는 <code>Joint Target</code>을 설정하고 있는 것이며, 지정된 위치(녹색 점)방향으로 팔꿈치 방향이 이동하는 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/b2e61f37-42aa-4216-b752-87298c0f0760/image.gif" alt="jopint-target"></p>
<p>(사진 출처: <a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-blueprint-two-bone-ik-in-unreal-engine">Unreal Engine - Two Bone IK</a>)</p>
<h2 id="42-footik">4.2. FootIK</h2>
<p>이전 글에서 구현했던 <a href="https://velog.io/@eugene-doobu/Feet-ik-%EB%95%85-%EC%9C%84%EB%A5%BC-%EA%B1%B8%EC%96%B4%EB%8B%A4%EB%8B%88%EA%B3%A0-%EC%9E%88%EC%96%B4%EC%9A%94">FootIK</a>를 이번에 구현한 FABRIK기반으로 다시 구현해보도록 하겠다. 기본적인 구현방식은 이전과 동일하게 발의 포지션에서 일정 높이 위의 포지션에서 바닥 방향으로 레이를 발사하여 발이 위치할 포지션과 회전값을 지정해 IK를 적용할 것이다.</p>
<pre><code class="language-c#">private FootIkSolverData FeetPositionSolver(Vector3 fromSkyPosition)
{
    if (!Physics.Raycast(fromSkyPosition, Vector3.down, out var feetOutHit,
                         raycastDownDistance + heightFromGroundRaycast, environmentLayer))
    {
        return new FootIkSolverData
        {
            IsDetectGround = false,
            FootPosition   = Vector3.zero,
            FootRotation   = Quaternion.identity
        };
    }

    var feetIkPositions = fromSkyPosition;
    feetIkPositions.y = feetOutHit.point.y + pelvisOffset;
    var feetIkRotations = Quaternion.FromToRotation(Vector3.up, feetOutHit.normal) * transform.rotation;

    return new FootIkSolverData
    {
        IsDetectGround = true,
        FootPosition   = feetIkPositions,
        FootRotation   = feetIkRotations
    };
}</code></pre>
<p>타겟으로 삼을 FootPosition과 FootRotation을 적용하는 부분은 기존 코드와 같다. 이제 여기서 얻은 <code>SolverData</code>를 적용하는 부분도 사실상 이전과 같다. 그저 Unity Animator를 통해 IK포지션을 지정해주던 부분을 위에서 작성한 Fabrik 객체에 넘겨주면 된다.</p>
<pre><code class="language-c#">private void MoveFeetToIkPoint(AvatarIKGoal foot, FootIkSolverData solverData, ref float lastFootPositionY)
{
    if (!solverData.IsDetectGround) return;
    var positionIkHolder = solverData.FootPosition;
    var rotationIkHolder = solverData.FootRotation;
    var targetBone       = foot == AvatarIKGoal.RightFoot ? HumanBodyBones.RightFoot : HumanBodyBones.LeftFoot;
    var targetIkPosition = ObjAnimator.GetBoneTransform(targetBone).position;

    targetIkPosition = transform.InverseTransformPoint(targetIkPosition);
    positionIkHolder = transform.InverseTransformPoint(positionIkHolder);

    var yVariable = Mathf.Lerp(lastFootPositionY, positionIkHolder.y, feetToIkPositionSpeed);
    targetIkPosition.y += yVariable;
    lastFootPositionY  =  yVariable;

    targetIkPosition = transform.TransformPoint(targetIkPosition);

    _fabrik.SetTarget(foot, targetIkPosition, rotationIkHolder);
}</code></pre>
<p>이렇게만 해서 IK를 적용하면 아래 <code>다리 보정 전</code>의 영상처럼 IK가 적용되긴 하는거 같은데 바들바들 떨리면서 이동하는 것을 볼 수 있다. 이건 위에서 언급한 <code>Pole</code>이 셋팅되지 않았기 때문이다. 이후 <code>Pole</code>을 재설정한 이후 정상적으로 걷는 것을 확인할 수 있다. 현재 모델에서 무릎의 Pole은 모델의 정면, <code>Foward</code>방향 벡터에 위치하도록 설정하였다.</p>
<p>!youtube[fBFrar4zRqQ?si=ZIFsNM-mIR2XFsqC]</p>
<p>영상 마지막쯤에 보면 지형물 위에 올라가 있을 때 살짝 떠있는거처럼 보이는 것을 볼 수 있다. 이는 현재 구현된 IK에는 본이 늘어나지 않도록 구현되었기 때문에 원래 본들을 쭉 핀 거리보다 위치해야할 발의 포지션이 멀리 떨어져 있는 경우 이러한 상황이 나올 수 있다. 이런 경우 발이 땅에 닿을 수 있게 골반을 추가적으로 보정해주면 아래 스크린샷과 같이 발이 땅에 닿는 것을 확인할 수 있다. </p>
<p>(그림자때문에 이것도 살짝 떠있는거처럼 보이긴 하는데.. 위 영상보다 무릎을 더 많이 굽히는건 확인할 수 있다)</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/a56b466c-daa8-49eb-8ad9-d27d7569aac2/image.png" alt="골반보정핑"></p>
<h2 id="43-handik">4.3. HandIK</h2>
<p>위 로직이 HandIK에서도 잘 적용되는지 테스트하기 위해 간단한 HandIK예제를 만들어보자. 현재 테스트중인 게임의 캐릭터에는 펀치로 적을 공격하는 Muscle Cat캐릭터가 있는데, 해당 캐릭터의 주먹이 특정 타겟을 공격하도록 애니메이션을 보정해보려고 한다.</p>
<p>다양한 펀치 애니메이션에 대해서 후보정을 수정하기는 까다롭기 때문에, 애니메이터에서 사용하는 모든 펀치 애니메이션을 <code>RightHook</code> anim 데이터를 이용하도록 수정하고, 해당 애니메이션에서 IK를 이용한 후보정을 진행해보도록 하겠다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/6bbccb82-6b25-40da-9a37-4eff6fb9a0c1/image.png" alt="펀치 10프레임"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/534a5aad-34b2-4688-8137-3ed141ea3384/image.png" alt="펀치 15프레임"></th>
</tr>
</thead>
</table>
<p>위 스크린샷은 <code>RightHook</code> 애니메이션에서 주먹을 뻗는 부분(10 프레임)과 주먹을 거두는 부분(15프레임)을 캡쳐한 것이다. 해당 프레임을 기준으로 주먹을 뻗는 부분에서 정해진 타겟을 가격하도록 애니메이션을 수정한 후 주먹을 거두는 애니메이션 쯤 원래 애니메이션으로 돌아오도록 애니메이션을 보정해보겠다.</p>
<p> <img src="https://velog.velcdn.com/images/eugene-doobu/post/538ecf15-2b03-42bf-9faf-a75b127cc7db/image.png" alt="Right Hook anim data"></p>
<p>사용할 캐릭터 제어 스크립트에 <code>HandIkWeight</code>라는 필드를 추가 후, <code>RightHook</code> anim 데이터에서 10프레임에 해당 값을 1, 0과 15프레임에 해당 값을 0으로 셋팅하도록 하였다. 지정된 프레임 사이 값은 애니메이터에서 기본적으로 보간해주는 값으로 설정되어있다.</p>
<pre><code class="language-cpp">        [SerializeField] private Transform rightHandTarget;

        protected override void LateUpdate()
        {
            base.LateUpdate();

            if (rightHandTarget &amp;&amp; HandIkWeight &gt; 0.01f)
            {
                var rightHand = ObjAnimator.GetBoneTransform(HumanBodyBones.RightHand);
                var targetPosition = Vector3.Lerp(rightHand.position, rightHandTarget.position, HandIkWeight);
                var targetRotation = Quaternion.Lerp(rightHand.rotation, rightHandTarget.rotation, HandIkWeight);
                Fabrik.SetTarget(AvatarIKGoal.RightHand, targetPosition, targetRotation);
            }
        }</code></pre>
<p>샘플 코드는 매우 간단하게 작성되었다. 애니메이션으로 지정되는 값인 <code>HandIkWeight</code>가 0.01이상이며 <code>rightHandTarget</code>이 있는 경우 주먹의 Transform값과 Target의 Transform값을 보간하여 애니메이션을 보정하도록 셋팅하였다.</p>
<p>!youtube[NUXyswKa_ts?si=B8aF96Cid5jRgq2m]</p>
<p>HandIK를 적용 후, 주먹을 뻗을 때 지정된 타겟 포지션으로 주먹이 나가는 것을 확인할 수 있다.</p>
<p>하지만 어색한 점을 확인할 수 있는데 본의 회전 값이 매우 어색한 포인트들을 발견할 수 있다. FABRIK의 기본 구현에서 신체의 각도 제한을 고려하지 않기 때문에 물리적으로 불가능한 애니메이션이 수행되는 경우가 있다. 이를 방지하기 위해서는 다음과 같은 방법들이 있다.</p>
<ul>
<li>컨텐츠 로직: IK 후보정을 수행하는 조건에 시야에 따라 일정 각도 안에 있는 오브젝트에 대해서만 IK Target으로 판정을 해주는 로직을 추가하는 방법</li>
<li>IK 로직: 회전값이 없는 IK Target 설정 메서드 추가 또는 본의 자유도를 제한하거나 일정 범위에서만 회전이 가능하도록 변화값의 제한 추가</li>
</ul>
<p>IK 로직의 개선에 대한 내용은 아래 개선사항에서 조금 더 자세히 다뤄보도록 하겠다.</p>
<h1 id="5-개선사항">5. 개선사항</h1>
<hr>
<p>위에서 언급했듯이, FABRIK는 각도 제한을 고려하지 않는다. 따라서 자연스러운 신체 움직임을 위해서는 추가적인 보정이 필요한데, 위 걷기 애니메이션에서는 단순히 무릎 앞쪽으로 Pole을 지정해주는 것 만으로도 자연스럽게 보정이 되었다. 하지만 모든 상황에서 Pole을 지정해준 것 만으로 자연스러운 애니메이션이 가능하지는 않을 것이다. <strong>조인트간 자유도 지정, 회전 각도 제한</strong> 등 <strong>물리적으로 가능한 애니메이션만 수행하도록 보정이 더 필요</strong>하다.</p>
<p>Humanoid Model에서 쓰는 Joint들은 아래와 같은 자유도를 가진다.</p>
<ul>
<li>suture joint model (1 DoF) : 절대 열리면 안되는 fixed joint. 움직이더라도 매우 제한적으로 움직일 것. 두개골과 같은 곳에 쓰임.</li>
<li>hinge joint model (1 DoF) : 가장 단순한 형태의 joint. 팔꿈치, 무릎, 손가락/발가락 등에 사용됨. 한 방향으로만 rotation이 이루어짐.</li>
<li>gliding joint model (2 DoF) : 손목, 발목과 같이 보다 넓게 회전이 가능한 joint</li>
<li>saddle joint model (2 DoF) : hinge나 gliding보다 자유롭게 움직일 수 있는 joint. 두 방향으로 움직일 수 있음.</li>
<li>pivot joint model (2 DoF) : 목과 같은 곳에 쓰이는 joint. 좌우로 돌릴 수 있음.</li>
<li>ball and socket joint model (3 DoF) : 인체에서 가장 유동적인 형태의 joint. 타원형 joint임.</li>
</ul>
<p>(내용 출처: <a href="https://velog.io/@tjswodud/GE2022-1.-Introduction-to-Inverse-Kinematics">GE2022-1.-Introduction-to-Inverse-Kinematics</a>)</p>
<p>또한 언리얼 엔진의 <a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-blueprint-two-bone-ik-in-unreal-engine">Two Bone IK</a>에서 설정 가능한 파라메터들을 보면 어떠한 보정을 해줄 수 있을지 힌트를 얻을 수 있다. Stretch 관련된 셋팅 값들이나 Twist, Joint Target에 대한 설명을 보면 해당 프로퍼티들이 IK를 어떻게 제어하는지 알 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/1bb76ca5-719f-4bad-af6d-b1b894eb90ff/image.gif" alt="Walk"></p>
<p>자연스럽게 걸어보아요</p>
<h2 id="참고">참고</h2>
<hr>
<p><a href="https://medium.com/unity3danimation/overview-of-jacobian-ik-a33939639ab2#:~:text=And%20the%20Jacobian%20is%20merely,the%20rotation%20of%20each%20joint">Overview of Jacobian IK</a>
<a href="https://velog.io/@tjswodud/GE2022-1.-Introduction-to-Inverse-Kinematics">[GE] 1. Introduction to Inverse Kinematics 시리즈</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[라이브 게임 에셋 관리 개선기 - 번외1.리소스 최적화 기법]]></title>
            <link>https://velog.io/@eugene-doobu/%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EA%B2%8C%EC%9E%84-%EC%97%90%EC%85%8B-%EA%B4%80%EB%A6%AC-%EA%B0%9C%EC%84%A0%EA%B8%B0-%EB%B2%88%EC%99%B81.%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B8%B0%EB%B2%95</link>
            <guid>https://velog.io/@eugene-doobu/%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EA%B2%8C%EC%9E%84-%EC%97%90%EC%85%8B-%EA%B4%80%EB%A6%AC-%EA%B0%9C%EC%84%A0%EA%B8%B0-%EB%B2%88%EC%99%B81.%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B8%B0%EB%B2%95</guid>
            <pubDate>Mon, 10 Feb 2025 14:49:57 GMT</pubDate>
            <description><![CDATA[<p>이번 글은 이전 글에서 이야기한 최적화 기법의 세부 내용을 다루는 글입니다. 이 글을 읽기 전에 이전 글을 읽어보는 걸 추천합니다.</p>
<p><a href="https://velog.io/@eugene-doobu/%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EA%B2%8C%EC%9E%84-%EC%97%90%EC%85%8B-%EA%B4%80%EB%A6%AC-%EA%B0%9C%EC%84%A0%EA%B8%B0-2.%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%82%AC%EC%9A%A9-%EC%A0%88%EA%B0%90%EB%82%98%EC%9D%B8%ED%81%AC%EB%A1%9C%EB%8B%88%ED%81%B4">라이브 게임 에셋 관리 개선기 - 2.메모리 사용 절감
</a></p>
<p>1부. 어드레서블 에셋 도입<br>2부. 메모리 사용 구조 개선 / 리소스 최적화
<strong>번외1. 리소스 최적화 기법</strong> &lt;- 현재 글<br>3부. DLC를 통한 패치 시스템</p>
<h1 id="인트로">인트로</h1>
<hr>
<p>이전 글에서 게임의 용량과 메모리에 대한 내용에 대해 이야기 하고 게임 리소스를 최적화하여 앱스토어에 올라가는 앱의 용량과 게임의 메모리 사용량을 줄이는 작업을 해 보았다. 이번 글에서는 이전 글에서 간단하게 설명만 하고 지나간 리소스 최적화에 사용한 설정에 관한 내용을 정리해 보려고 한다.</p>
<p>이번 번외편에서는 이전에 다룬 텍스처 압축, 텍스처 패킹과 스프라이트 아틀라스, 그리고 오디오 압축 등 주요 리소스 최적화 기법에 대해 간단히 정리해보고자 한다. 단순 나열식에 가까운 글이 될 것 같아 <code>어떤 방식들이 있구나</code> 하는 정도로 가볍게 훑어 보는걸 추천한다.</p>
<h1 id="0-다시-한번-나인크로니클">0. 다시 한번 나인크로니클</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/bcf8edab-ac8b-4dc9-b6e3-f837c17a49a4/image.png" alt="플레이화면"></p>
<p>게임 최적화를 하기 이전에 게임이 어떤 특징을 가지고 있는지 알아야 한다. 특히 어떤 디바이스를 사용하느냐에 따라 사용되는 옵션과 전략이 크게 달라질 수 있다. 먼저 <a href="https://github.com/planetarium/NineChronicles">나인크로니클</a>(9C)은 풀 블록체인 기반의 2D RPG게임이며 <strong>모바일-PC 크로스 플랫폼</strong>을 지원한다. 블록체인 게임이라는 굉장히 특별한 특징이지만, 리소스 최적화와는 크게 연관은 없는 내용이라 <code>2D</code>와 <code>모바일-PC 크로스 플랫폼</code>이라는 점이 핵심이 된다.</p>
<h3 id="2d-게임에서의-최적화">2D 게임에서의 최적화</h3>
<p>2D 게임의 경우 확실히 3D게임 보다는 최적화 부담이 덜하긴 하지만, 텍스처가 많아지고 이에 대한 관리를 소홀히 하면 큰 문제가 생길 수도 있다. 특히 여러 스프라이트를 겹쳐서 사용하는 경우가 많은데, 이러한 경우 렌더러가 한 픽셀을 여러번 렌더하는 <strong>OverDraw현상이 발생</strong>하여 불필요한 연산이 많아질 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/7e3792c4-00c6-4416-8b9a-0dc1d20286a3/image.png" alt="OverDraw"></p>
<p>위 스크린샷은 9c 메인 화면에서 <code>OverDraw</code>옵션을 활성화 시킨 모습이다. 화면의 대부분이 흰색으로 보이는데, 이는 씬에서 사용하는 파티클이 중첩되어 렌더링되어 나타난 현상으로 보인다. 좌상단에 있는 퀘스트 UI를 아래 OverDraw를 비활성화 시킨 UI랑 비교해보면, 던전 아이콘(마름모 모양)이 사각형 형태로 오버드로우 되고 있음을 알 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/13402bbc-b3a2-4bb7-81fe-83e9d50bfd42/image.png" alt="퀘스트 UI"></p>
<blockquote>
<p><strong>OverDraw:</strong> 게임 오브젝트를 투명한 “실루엣”으로 렌더링합니다. 투명한 컬러가 중첩되면 한 오브젝트 위에 다른 오브젝트가 그려진 곳을 쉽게 파악할 수 있습니다.</p>
</blockquote>
<h3 id="pc-모바일-게임에서의-최적화">PC-모바일 게임에서의 최적화</h3>
<p>크로스 플랫폼의 경우 일반적으로 PC의 성능이 모바일보다 압도적으로 좋기 때문에, 모바일을 대상으로 최적화한다면 PC 도 자연스럽게 최적화가 될 것으로 생각한다. 따라서 저번 글에서도 오로지 모바일 스토어 상에서의 빌드 용량과 메모리 사용량을 비교를 하였다.</p>
<p>모바일 게임 최적화는 옛날부터 핫한 주제였기에 다양한 글들이 있다. 이에 대한 관심이 있다면 유니티 블로그에 작성된 아래 글을 읽어 보는 것을 추천한다.</p>
<p><a href="https://unity.com/kr/blog/games/optimize-your-mobile-game-performance-expert-tips-on-graphics-and-assets">모바일 게임 성능 최적화: 그래픽과 에셋에 관한 전문가 팁</a></p>
<h1 id="1-스프라이트-아틀라스">1. 스프라이트 아틀라스</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/f74ac31a-012f-412d-ba47-0d6bd81eb2c5/image.png" alt="Atlas"></p>
<p>스프라이트 아틀라스는 게임에서 사용하는 스프라이트 텍스처들을 위 스크린샷과 같이 하나로 묶어 하나의 텍스처처럼 사용하는 것이다. 게임에서 사용하는 자잘한 텍스처들이 많을수록 최적화를 위해 스프라이트 아틀라스를 적용하는게 좋다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/ea15b26a-0ffd-49c5-98a3-0a19a923d42b/image.png" alt="Batches"></p>
<p><strong>1. 드로우콜 줄이기</strong></p>
<p>CPU가 GPU에게 일을 시키기 위해서는 어떤걸 그려야 한다고 명령을 호출해야 하는데, 이를 <code>드로우콜(DrawCall)</code> 이라고 한다. GPU는 연산이 빠르지만, CPU와 데이터를 주고 받는 시간은 느리기 때문에 GPU를 최대한으로 활용하기 위해서는 이 드로우콜 횟수를 줄여야 한다. 유니티에서는 <strong>동일한 메터리얼을 사용하는 오브젝트들을 묶어 1개의 드로우콜로 묶어서 보내주는</strong> <code>배칭(Batching)기능</code>들이 있다. 2D Sprite도 배칭이 가능하며 텍스처 아틀라스와 같은 기법을 활용하여 스프라이트들을 하나의 이미지에 모아 GPU로 보내줄 수 있는 것이다. 배치의 수는 유니티의 Statisics에서 쉽게 확인할 수 있다.</p>
<p><strong>2.텍스처 압축 포멧의 POT(Power of two)</strong></p>
<p>텍스처 압축 포맷중 압축을 하기 위한 조건이 POT(Power of two)인 경우가 있다. 해당 조건이 있는 포맷의 경우 텍스처 이미지의 사이드가 2의 승수가 아닌 경우 텍스처 압축이 불가능 하다. UI에 사용하는 아이콘들은 이러한 조건을 갖추기 어렵기 때문에, 이러한 텍스처들을 모아 2의 승수의 크기로 아틀라스를 뽑아주면 텍스처 압축이 가능해진다. 텍스처 압축에 대한 내용은 뒤에서 자세하게 다루도록 하겠다.</p>
<h3 id="아틀라스-이중압축">아틀라스 이중압축</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/3570d562-9a88-4368-b54e-0e928d66e428/image.png" alt="아틀라스 이중 압축"></p>
<p>(사진 출처: <a href="https://discussions.unity.com/t/do-sprite-atlases-double-compress-sprites/228209">unity discussions</a>)</p>
<p>텍스처 압축은 텍스처의 품질을 변경시키는데, 아틀라스에서 가져오는 텍스처들은 압축이 되어있는지 안되어있는지 체크되지 않는다. 위 스크린샷을 보면, 압축되지 않은(RGBA 32 bit)포맷 으로 설정하고 압축된 텍스처와 압축되지 않은 텍스처를 가져오는 경우 <strong>압축된 텍스처가 품질이 저하된 그대로 아틀라스에 들어오는 것을 확인</strong>할 수 있다. 따라서 스프라이트 아틀라스를 사용하는 경우, 아틀라스에 포함될 텍스처들은 압축이 되지 않도록 잘 분리를 해주어야 좋다.</p>
<p>(텍스처 압축 포맷은 결국 랜덤 액세스가 가능하게 포맷당 정해진 비트를 사용하므로 이중 압축이 되어도 성능적으로 이득을 보는 건 없다)</p>
<h1 id="2-텍스처-압축">2. 텍스처 압축</h1>
<hr>
<p>iOS: <code>625MB</code> -&gt; <code>460MB</code> (앱 스토어 커넥트에서 <code>압축 파일 크기</code>)
Android: <code>648MB</code> -&gt; <code>478MB</code> (구글 플레이 콘솔에서 <code>원본 파일</code>)</p>
<p><a href="https://github.com/planetarium/NineChronicles/pull/4419">텍스처 패킹에 대한 PR</a></p>
<p>이전 글에서 텍스처 압축을 통해 앱스토어의 용량을 약 26%씩 줄인 결과를 보여주었다. 실제로는 이 작업이 머지되면서 대형 컨텐츠들이 업데이트 되었고, 텍스처 압축 전 빌드 용량은 이 컨텐츠 리소스들이 포함되지 않은 상태였기에 실제로는 26% 이상의 용량이 절약되었을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/86efd201-4480-4df8-b2bb-d83940a3b1b0/image.png" alt="용량의 차이"></p>
<p>이처럼 2D 프로젝트에서 텍스처 압축은 엄청난 힘을 보여준다. 이번 챕터에서는 유니티에서 사용할 수 있는 텍스처 압축 옵션들은 어떤 것들이 있고 프로젝트에 어떻게 적용하였는지에 대해 설명해보고자 한다.</p>
<h3 id="텍스쳐-파일-포맷">텍스쳐 파일 포맷</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/247b5455-9714-402f-8ad8-a4dc92764bad/image.png" alt="파일 포멧"></p>
<p><em>(사진 출처: 유니티 코리아 유튜브)</em></p>
<p>흔히 텍스처 포맷이라고 하면 우리가 흔하게 볼 수 있는 <code>파일 포맷</code>을 생각해볼 수 있다. 이는 일반적으로 디스크에 파일을 저장할 때 저장 용량을 아끼기 위한 압축 포맷으로 생각하면 된다.</p>
<p>유니티에서는 다양한 파일 포맷을 가져와 별도의 처리 없이 사용할 수 있다. 물론 PSD를 사용하면 프로젝트 자체의 용량이 어마어마 해진다던지, PNG를 사용하면 알파 채널을 컨트롤 하기 힘들어 추가 옵션을 설정해야 한다던지 차이점이 조금씩 존재하지만, 결과적으로 게임 빌드파일에 들어갈 텍스처 어떤 포맷을 사용하든 상관없다.</p>
<blockquote>
<p><strong>사용 가능 포맷:</strong> BMP, EXR, GIF ,HDR, IFF, JPG, PICT, PNG, PSD, TGA, TIFF...</p>
</blockquote>
<p>이러한 텍스처 포맷들은 디스크에 GPU를 위한 형태로 저장되어 있지 않다. 따라서 게임에 텍스처를 효율적으로 사용하기 위해서는 디스크에 저장된 이미지 파일들을 GPU를 위한 텍스처 포맷으로 따로 지정해주어야 한다. 실제 게임에서는 GPU를 위해 변경된 포맷으로 텍스처를 변환 시켜 사용하기 때문에 디스크에 저장되어 있는 포맷이 무엇인지는 상관 없는 것이다.</p>
<p>텍스처 포맷을 지정할 때 현재 사용하는 디바이스에서 지원해주고, 시각적인 효과 대비 이미지 품질이 적게 변하는 포맷으로 잘 조율하여 변경하여야 한다. 텍스처 압축에서 가장 중요한 것은 적절한 압축 포맷을 사용하는 것이다.</p>
<h4 id="왜-gpu에서-못써요">왜 GPU에서 못써요?</h4>
<p>디스크에 저장되는 파일 포맷들은 보통 저장 용량을 아끼기 위한 압축 포맷인 경우가 많은데, 예를 들어 PNG 파일은 <code>가변 비율 압축</code>을 사용하여 자신의 용량을 압축하고 있다. 예를 들어 aaaabbbcccccd라는 원본 데이터가 있을 때, a4b3c5d1과 같은 식으로 디스크에 저장 되게 되는 것이다. GPU에서 텍스처를 사용할 때에는 보통 UV좌표를 이용해서 텍스처의 특정 부분을 <code>랜덤 엑세스</code>하여 색상을 가져오게 되는데, <code>시작 주소+오프셋</code>방식으로 접근하는 랜덤 액세스 방식은 위처럼 압축되어 있는 데이터에 사용하기가 힘들다. 따라서 GPU에서 랜덤 액세스를 통해 샘플링을 할 수 있도록 포맷을 변경해줘야 한다.</p>
<h2 id="텍스처-압축-포맷">텍스처 압축 포맷</h2>
<p>대표적인 압축 포맷인 ETC2, ASTC, PVRTC에 대한 정보를 정리해보았다.</p>
<h3 id="etc2-범용성을-앞세운-안드로이드-표준">ETC2: 범용성을 앞세운 안드로이드 표준</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/ab79022c-26e9-41f3-a169-40ae6d8d1cf4/image.png" alt="ETC2"></p>
<p>(사진 출처: Ericsson AB 2009, ETC2-PACKAGE)</p>
<p><strong>ETC2</strong>(Ericsson Texture Compression 2)는 OpenGL ES 3.0 이상 버전을 지원하는 대부분의 안드로이드 기기에서 표준처럼 사용되는 압축 포맷이다. <code>ETC1</code>의 후속 버전으로, 알파채널을 지원하고 전반적인 압축 후 이미지 퀄리티가 ETC1보다 개선되었다.</p>
<h3 id="특징">특징</h3>
<ol>
<li><strong>범용 지원</strong>: 안드로이드 환경 전반에 광범위하게 적용되어 있어 호환성 면에서 유리하다.</li>
<li><strong>알파 지원</strong>: ETC1은 알파 채널을 지원하지 않았지만, ETC2는 RGBA 형태로 알파까지 담을 수 있다.  </li>
<li><strong>고정 비트레이트</strong>: ASTC처럼 가변 비트레이트를 지원하지는 않아, 화질과 크기를 섬세하게 조절하기는 어렵다.</li>
</ol>
<p>ETC2는 현재 <strong>안드로이드에서 가장 호환성이 좋은 텍스처 포맷</strong>으로 생각하면 된다. 저사양 안드로이드 디바이스들을 타겟으로 삼고 있는 경우 ETC2 포맷을 선택하는 것이 좋다. 뒤에서 소개할 <code>ASTC</code> 포맷이 압축 품질, 유연성 면에서 우수하기 때문에 주 타겟으로 삼고있는 디바이스가 저사양 기기가 아니라면 ASTC포맷을 사용하는게 유리하다.</p>
<p>(ASTC 6X6의 경우 ETC2와 비슷한 품질을 보여주지만 용량은 절반 수준이다)</p>
<h3 id="pvrtc-ios-위주의-압축">PVRTC: iOS 위주의 압축</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/7a01acc4-5eca-49c4-8a18-8bb234169335/image.png" alt="PVRTC"></p>
<p>(사진 출처: imaginationtech)</p>
<h3 id="개요">개요</h3>
<p><strong>PVRTC</strong>(PowerVR Texture Compression)는 iOS 디바이스를 중심으로 많이 사용되는 텍스처 압축 포멧이다. 모든 세대의 iPhone, iPod Touch, iPad에서 사용가능하며, PowerVR GPU를 사용하는 특정 Android 기기에서도 지원된다. <strong>텍스처의 해상도가 2의 지수승 정사각형이어야 한다</strong>는 제약이 있다.</p>
<h3 id="특징-1">특징</h3>
<ol>
<li><strong>블록 크기</strong>: PVRTC는 텍스처가 2의 거듭제곱 해상도를 권장하며, 블록 단위 압축 특성이 있어 이미지가 특정 크기여야 최적 결과를 얻기 쉬움  </li>
<li>블록 경계를 뭉개며 <strong>블러링</strong>을 시켜주며, <strong>블록 경계에서 색이 번지는 현상</strong>이 일어날 수 있다. 도트나 아이콘에는 좋지 않다.</li>
</ol>
<p>조사하면 할 수록 현재 시점에서 이 포맷을 왜 쓰는지 이해가 가지 않았다. 텍스처의 해상도가 2의 지수승 정사각형이어야 한다는 제약이 너무 크게 느껴졌다. 소형 아이콘은 아틀라스로 묶어서 쓴다고 하더라도 모든 텍스처를 그렇게 쓰기는 힘들다고 생각된다. iOS도 역시 가능하면 <strong>ASTC</strong>를 사용하는게 가장 좋은 방향인 것 같다. 근데 ETC2랑 상황이 다르게 ASTC는 iOS유저라면 거의 확정적으로 사용할 수 있다.</p>
<p>(PVRTC를 사용해야하는 특별한 이유가 있다면 댓글로 알려주세요...)</p>
<h3 id="astc-다양한-비트레이트로-품질-조절">ASTC: 다양한 비트레이트로 품질 조절</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/fd357a41-c2a4-4622-bc64-58a5ee2fbec0/image.png" alt="ASTC"></p>
<p>(사진 출처: developer.arm.com)</p>
<p><strong>ASTC</strong>(Adaptive Scalable Texture Compression)는 ARM과 AMD가 공동 개발한 압축 포맷이다. 가변 비트레이트를 지원하며 개발자가 해상도·화질·압축률 간의 트레이드오프를 자유롭게 조정할 수 있다. 또한 위에서 설명한 이전 시대의 포맷들 보다 압축률 대비 텍스처 품질도 뛰어나다.</p>
<h3 id="특징-2">특징</h3>
<ol>
<li><strong>가변 블록 크기</strong>: 4×4부터 12×12 픽셀 블록까지, 비트레이트를 자유롭게 설정 → 원하는 균형점을 찾기 쉬움  </li>
<li><strong>멀티플랫폼 지원 확대</strong>: 최신 안드로이드 기기나 iOS Metal 등에서 점차 지원이 확산.</li>
<li><strong>고품질 유지</strong>: 높은 비트레이트(예: 4×4 블록)로 설정하면 일반 DXT나 ETC2보다 화질이 우수한 압축 결과를 낼 수 있음  </li>
<li>비교적 <strong>최신 기기에서만 지원</strong>한다.</li>
</ol>
<p>유연한 포맷으로 모델링이라던지 정밀도가 높아야 하는 텍스처는 압축 블럭을 작게 지정(4x4)하고, 이펙트와 같은 휘발성이 높거나 디테일이 높지 않아도 되는 경우 압축 블럭을 크기 지정(12x12)하는 식으로 사용할 수 있다. 어느정도 압축(6x6 정도)을 하더라도 ETC2나 PVRTC와 비슷한 품질이 나오기 때문에 주 타겟 디바이스가 <strong>ASTC를 지원한다면 ASTC를 선택하는걸 추천</strong>한다</p>
<h2 id="나인크로니클에서의-텍스처-압축">나인크로니클에서의 텍스처 압축</h2>
<h3 id="기존상태">기존상태</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/5dca7eb2-279e-4bb1-8847-ce76a4e315f1/image.png" alt="build report"></p>
<p>텍스처 압축 설정 전에 빌드 리포트를 뽑아 리소스의 사이즈를 체크해 본 결과이다. 빌드 리포트의 상위권에 많은 양의 Uncompressed Texture2D 친구들이 있었다. 이것만 압축해도 빌드 용량을 크게 줄일 수 있다. 살펴보니 압축 옵션이 되있는 것들과 안 되 있는 것들이 섞여있던 상태. 우선 되있는 것들의 포맷에 맞춰야 한다는 생각이 들었다.</p>
<p>또한 위의 <strong>이중압축 문제</strong>가 발생하지 않게 아틀라스에 포함될 텍스쳐는 압축을 하지 않아야 한다. 아틀라스에 포함되는 텍스처들과 포함되지 않는 텍스처들이 프로젝트에서 분리가 되어있지 않아 이러한 구분이 까다로웠다. 아틀라스로 관리될 텍스쳐들은 확실히 분리하는게 좋다.</p>
<h3 id="시장조사">시장조사</h3>
<p>2024-02-16기준으로 디바이스별 텍스처 압축 포맷 지원에 대한 통계를 찾아보았다.</p>
<h4 id="안드로이드">안드로이드</h4>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/53fb8ec7-fcc9-4ec5-8a45-331eeb06356c/image.png" alt="Android Texture"></p>
<p>(+2022.8.8기준 PVRTC 11%)</p>
<h4 id="ios">iOS</h4>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/2fa6cd66-a63c-4516-8586-5d5fe96f5084/image.png" alt="iOS Usage"></p>
<p>현재 89% 유저가 ios16 이상 기기 사용, <strong>ios13이상 사용시 확정적으로 astc 지원</strong></p>
<p>(2021년 4월 기준, <a href="https://forum.unity.com/threads/ios-changed-default-texture-compression-format-from-pvrtc-to-astc.1088845/">약 2%의 유저들만 A7(astc 지원 안하는 기기)칩을 이용</a>한다고 함)</p>
<p>ios의 경우 아래 포함 상위 기기에서 모두 지원(A8 processor)</p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/IPhone_6">iPhone 6 &amp; 6 Plus</a></li>
<li><a href="https://en.wikipedia.org/wiki/IPod_Touch_(6th_generation)">iPod Touch (6th generation)</a></li>
<li><a href="https://en.wikipedia.org/wiki/IPad_Mini_4">iPad Mini 4</a></li>
<li><a href="https://en.wikipedia.org/wiki/Apple_TV">Apple TV HD</a> (formerly 4th generation)</li>
<li><a href="https://en.wikipedia.org/wiki/HomePod">HomePod (1st generation)</a></li>
</ul>
<h4 id="나인크로니클-플레이-유저">나인크로니클 플레이 유저</h4>
<p>회사 내부에서 사용하고 있는 솔루션들의 통계를 통해 게임을 플레이하는 유저들의 국가 통계를 알 수 있었다. 확인 결과 역시 안드로이드는 ETC2로 가는게 안전하다는 판단이 들었다. iOS의 경우 굳이 PVRTC를 사용할 이유를 찾지 못하였다.</p>
<h2 id="결론">결론</h2>
<h3 id="안드로이드-→-etc2">안드로이드 → ETC2</h3>
<ul>
<li><strong>일부 색상 손실</strong></li>
<li><strong>95%의 디바이스 지원</strong></li>
<li>ETC2가 지원되지 않는 5% 기기는 무압축 32/16bit텍스처 지원</li>
<li>기존에도 해당 압축 옵션을 사용한 텍스처들이 꽤 있음</li>
</ul>
<h3 id="ios-→-astc">IOS → ASTC</h3>
<ul>
<li><strong>iPhone 5s 타겟으로 최적 지원 안됨</strong> (ios13부터 100%기기 모두 지원)
→ 확인상 거의 모든 텍스처가 ASTC 아니면 압축안함으로 설정되어 있었음. 따라서 디바이스가 ASTC를 지원하지 않아도 사실상 변경사항 거의 없음</li>
</ul>
<p>따라서, 앞으로</p>
<ul>
<li>PVRTC를 사용하지 않음으로 텍스처 이미지를 정사각형으로 뽑을 필요가 없다.</li>
<li>텍스처 추출은 안드로이드(ETC2)를 위해 일단 POT으로 뽑아야함</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/c428b723-c5bc-4141-b992-8f0b832bdbfc/image.png" alt="Spine Pot"></p>
<p>이 기준에 맞게 프로젝트에서 사용하는 <a href="https://github.com/planetarium/NineChronicles/pull/4373">스파인 리소스들을 정리</a>하였다.</p>
<h1 id="3-오디오-압축-기법">3. 오디오 압축 기법</h1>
<hr>
<p>오디오도 설정에 따라 프로젝트의 메모리 사용량이 크게 달라진다.</p>
<h3 id="모노와-스테레오">모노와 스테레오</h3>
<p>사운드는 모노와 스테레오 사운드로 나뉜다. 스테레오는 2개의 채널을 사용하므로 모노의 2배의 용량을 사용한다고 보면 된다. 사운드의 용량도 가벼운 편이 아니므로 디바이스와 게임 콘텐츠 특성을 고려해 올바른 옵션을 선택하여야 한다.</p>
<ul>
<li><strong>모노</strong>: 하나의 채널을 통해 믹싱되고 재생되는 오디오를 말하며, 이는 하나의 스피커 또는 여러 개의 스피커가 동일한 소리를 동시에 내는 형태입니다.</li>
<li><strong>스테레오(Stereo)</strong>: 왼쪽과 오른쪽 두 채널을 사용하여 서로 다른 오디오 신호를 전달하며, 깊이와 차원의 감각을 더해줍니다.
(출처: <a href="https://kr.kef.com/blogs/news/mono-vs-stereo-speakers-sound-differences">KEF: 모노(Mono) vs. 스테레오(Stereo) 사운드: 차이점은 무엇일까요?</a>)</li>
</ul>
<p>일반적으로 모바일 디바이스의 경우 게임에서 스테레오 사운드를 사용할 일이 거의 없다. 모바일의 경우 대부분 MONO로 설정해주면 된다.</p>
<h3 id="로드타입">로드타입</h3>
<ul>
<li><strong>Decompress on load</strong>: 압축을 풀어서 올리는 굉장히 위험한 옵션. 재생속도가 굉장히 빠른 특성이 있다. 사운드의 길이가 짧고 반응속도가 매우 중요한 경우 사용하는게 좋다.</li>
<li><strong>Compressed into memory</strong>: 압축된 사운드 사용, 일반적인 효과음을 이 옵션으로 지정하는게 좋다.</li>
<li><strong>Streaming</strong>: 보통 사운드 길이가 긴 배경음악, 반응속도가 중요하지 않은 사운드에 적용하는게 좋다.</li>
</ul>
<p>위 설명대로 대부분의 경우 효과음은 <code>Compressed into memory</code>, 배경음악은 <code>Streaming</code>을 사용하면 될 것 같다.</p>
<h3 id="compression-format">Compression Format</h3>
<ul>
<li>PCM: 비압축 포맷으로 퀄리티가 중요하며 재생시간이 짧은 오디오 파일에 적합하다.</li>
<li>Vorbis: 중간길이 정도의 효과음 또는 배경음악에 적합하며 압축률(Quality)를 조절할 수 있다. 대부분의 경우 이 옵션을 선택한다.</li>
<li>ADPCM: 압축률이 PCM대비 3.5배이기에 메모리는 덜 쓸 수 있지만, CPU자원은 조금 더 사용한다. 노이즈가 발생하기에 노이즈가 크게 상관없는 효과음에 적합하다.</li>
</ul>
<p>또한 사운드가 Mute되어도 프로세스에 메모리는 존재하는 상태로 남아있는다. 이러한 점을 주의하고 프로파일링을 통해 낭비되고 있는 메모리가 없는지 체크하는게 중요하다.</p>
<h3 id="9c에서의-사운드-압축">9C에서의 사운드 압축</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/0f339d05-40aa-480a-a248-9d8093f2e7e4/image.png" alt="오디오"></p>
<p>메모리 프로파일링 도중.. 뭔가 엄청나 보이는 메모리를 차지하는 AudioClip을 발견하였고, 이를 계기로 프로젝트 오디오 파일들의 압축 옵션을 확인하게 되었었다. 적용 내용은 간단하게 모든 bgm에 대해 동일한 옵션을 적용하였다.</p>
<ul>
<li>LoadType: Streaming</li>
<li>Compression Format: ADPCM</li>
<li>Sample Rate Setting: Override Sample Rate</li>
<li>Sample Rate: 22,050</li>
</ul>
<p>또한 사운드의 디테일을 크게 살릴 필요성이 없다고 판단되어 모든 사운드를 <strong>MONO</strong>형태로 저장하기로 하였다.</p>
<p>이런 간단한 변경을 통해 iOS기준 <strong>백메가 단위의 상당히 많은 메모리를 절약</strong>시킬 수 있었다.</p>
<p><a href="https://github.com/planetarium/NineChronicles/pull/5283">사운드 옵션 pr</a></p>
<blockquote>
<p><strong>추천 옵션</strong>
<strong>배경음악</strong>: Streaming + Vorbis
<strong>효과음</strong>: Compressed into memory + ADPCM</p>
</blockquote>
<h2 id="4-스파인-데이터-개선">4. 스파인 데이터 개선</h2>
<p>이전 글에서 GC에 대한 설명과, 유니티 환경에서 GC가 왜 중요한지에 대해 이야기를 나눈 바 있다. 유니티에서 제공하는 <a href="https://unity.com/blog/games/optimize-your-mobile-game-performance-tips-on-profiling-memory-and-code-architecture-from">모바일 디바이스 최적화 책</a>서는 메모리와 GC 파트에서 다음과 같은 내용을 강조한다.</p>
<p><strong>불필요한 힙 할당으로 인해 GC 스파이크가 발생할 수 있다는 점에 유의하세요:</strong></p>
<ul>
<li>문자열: C#에서 문자열은 값 유형이 아닌 참조 유형입니다. 불필요한 문자열 생성이나 조작을 줄입니다. <strong>JSON 및 XML(혹은 csv)과 같은 문자열 기반 데이터 파일을 구문 분석하지 말고</strong>, 대신 <strong>ScriptableObjects나 MessagePack 또는 Protobuf와 같은 형식으로 데이터를 저장</strong>합니다. 런타임에 문자열을 빌드해야 하는 경우 StringBuilder 클래스 사용
...</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/24a170b7-b77e-4137-a59b-eacd149d5396/image.png" alt="스파인 프로파일링"></p>
<p>기존 9C 프로젝트는 JSON 기반 데이터 파일을 런타임에 파싱하여 스파인 애니메이션을 수행하고 있었다. 에디터 프로파일링 결과, JSON 데이터 파싱 과정에서 상당한 GC Alloc이 발생하고, 이에 따른 처리 시간이 소요되는 것을 확인할 수 있었다.</p>
<h3 id="스파인-데이터-포맷-변경">스파인 데이터 포맷 변경</h3>
<p>스파인 데이터 포맷을 JSON에서 바이너리 형태로 변경했다. 기존 스파인 데이터(JSON 포맷)는 아래와 같이 설정값을 직관적으로 확인하고 수정하기 쉽다는 장점이 있었다.</p>
<pre><code class="language-js">{
    &quot;skeleton&quot;: {
        &quot;hash&quot;: &quot;pk8bvriq1EUyoeKgE79GQivbfyg&quot;,
        &quot;spine&quot;: &quot;3.8.99&quot;,
        &quot;x&quot;: -384.15,
        &quot;y&quot;: -105.72,
        &quot;width&quot;: 674,
        &quot;height&quot;: 450,
        &quot;images&quot;: &quot;./images/&quot;,
        &quot;audio&quot;: &quot;C:/Users/user/Desktop/animation/Cut_Scene&quot;
    },
    &quot;bones&quot;: [{
        &quot;name&quot;: &quot;root&quot;,
        &quot;x&quot;: 1.47
    }, {
        &quot;name&quot;: &quot;cutscene_01&quot;,
        &quot;parent&quot;: &quot;root&quot;,
        &quot;rotation&quot;: 50.7,
        &quot;x&quot;: 291.32,
        &quot;y&quot;: -106.04
    }],
    &quot;slots&quot;: [{
      ...
    }],
    &quot;skins&quot;: [{
        &quot;name&quot;: &quot;default&quot;,
        &quot;attachments&quot;: {
            &quot;cutscene&quot;: {
                &quot;cutscene_01&quot;: {
                    &quot;x&quot;: -40.95,
                    &quot;y&quot;: 405.78,
                    &quot;rotation&quot;: -50.7,
                    &quot;width&quot;: 674,
                    &quot;height&quot;: 450
                }
            }
        }
    }],
    &quot;animations&quot;: {
      ...
    }
}</code></pre>
<p>이러한 특성 때문에 스파인 리소스를 처음 임포트하고 확인해보는 과정에서는 json형태로 데이터를 저장하는게 유리할 수 있다. 그러나 인게임에서는 이러한 장점이 필요 없고, 오히려 로드시 성능에 악영향을 미친다는 단점만 남는다. 실제 환경에서의 성능을 위해 binary형태로 데이터를 저장하는 방식으로 설정을 변경하였다.</p>
<h3 id="스파인-버전-업">스파인 버전 업</h3>
<p>기존에 사용하던 스파인 3.8 버전을 4.1 버전으로 업그레이드했다.
특히 대규모 프로젝트 환경에서 성능이 크게 개선되었다고 한다.</p>
<p>추가로 버전 관리를 용이하게 하기 위해 스파인을 Git 패키지 형태로 프로젝트에 임포트하도록 수정했다.</p>
<p><a href="https://ko.esotericsoftware.com/spine-changelog#v4-0-00-beta">스파인 4-0-00 체인지로그</a>
<a href="https://github.com/planetarium/NineChronicles/pull/4119">스파인 업그레이드PR</a></p>
<h3 id="스파인-성능비교">스파인 성능비교</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/67b123f0-3347-41ef-b54a-757e38c943c9/image.png" alt="스파인 변경 전 이미지"></p>
<p>변경 전 스파인 프로파일링</p>
<hr>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/75bb8871-4d37-4cba-923c-a468076ed0ff/image.png" alt="스파인 변경 후 이미지"></p>
<p>변경 후 스파인 프로파일링</p>
<hr>
<p>같은 스테이지에서 몬스터 생성 과정을 비교한 결과, <code>2.5MB / 162.94ms</code>에서 <code>0.7MB / 27.06ms</code>로 성능이 개선된 것을 확인할 수 있었다.</p>
<h2 id="마치며">마치며</h2>
<p>내용을 정리하면서 텍스처 압축에 대해 다시 한 번 정리할 수 있었고, 내가 이런 일을 했었구나도 정리해볼 수 있었다. 관련 자료를 검색하면서 좋은 블로그들도 몇 개 찾았다. 아래 참고에 봤던 블로그들을 모아놨으니 해당 주제에 관심이 있다면 하나씩 같이 보는걸 추천한다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/0415a8b3-1cd9-4a3a-8f92-f947b720b0a7/image.png" alt="잘자요"></p>
<hr>
<h3 id="참고">참고</h3>
<p> <a href="https://namu.wiki/w/%ED%85%8D%EC%8A%A4%EC%B2%98%20%EC%95%95%EC%B6%95%20%ED%8F%AC%EB%A7%B7">나무위키-텍스처 압출 포맷</a>
 <a href="https://www.youtube.com/watch?v=BeEjoTa9sSo">[유니티 TIPS] 알쓸유잡 | 효율적인 텍스처 압축 이해하기&amp; 꿀팁</a>
 <a href="https://mentum.tistory.com/585">아틀라스 리소스 폴더 주의</a>
 <a href="https://mentum.tistory.com/586">아틀라스 이중압축</a>
 <a href="https://chulin28ho.tistory.com/362">&quot;유니티에서는&quot;어째서 PNG보다는 TGA가 더 쓸모있는 파일 포맷인가?</a>
 <a href="https://hotfoxy.tistory.com/116">텍스처 압축과 ASTC</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내 캐릭터의 발이 돌을 뚫고 들어갔어요]]></title>
            <link>https://velog.io/@eugene-doobu/Feet-ik-%EB%95%85-%EC%9C%84%EB%A5%BC-%EA%B1%B8%EC%96%B4%EB%8B%A4%EB%8B%88%EA%B3%A0-%EC%9E%88%EC%96%B4%EC%9A%94</link>
            <guid>https://velog.io/@eugene-doobu/Feet-ik-%EB%95%85-%EC%9C%84%EB%A5%BC-%EA%B1%B8%EC%96%B4%EB%8B%A4%EB%8B%88%EA%B3%A0-%EC%9E%88%EC%96%B4%EC%9A%94</guid>
            <pubDate>Tue, 28 Jan 2025 17:16:14 GMT</pubDate>
            <description><![CDATA[<p>3D 게임을 만들다 보면, 언젠가 꼭 마주치게 되는 장면이 있다. “<strong>캐릭터가 울퉁불퉁한 바위 위에 서 있는데, 발이 바위 표면을 뚫고 들어가거나 공중에 둥둥 떠 있는 모습</strong>”을 보게 되는 순간 그래픽의 몰입감이 떨어짐과 함께 &#39;개발자들이 급했나보군&#39;라는 생각이 들게 된다. 이러한 현상은 게임을 하다보면 금방 잊게 되지만, 게임의 완성도를 의심하게 하기도 하고 개발자의 자존심에도 상처를 주기 때문에 오늘은 이와 같은 현상을 방지하는 기능을 구현해보고자 한다.</p>
<p>간단한 레이캐스트와 Unity Animator기본 기능의 조합만으로도 캐릭터의 발이 바닥(또는 지형)에 착 밀착되도록 만들 수 있다. 이러한 기능을 <strong>Feet IK</strong>(Foot IK)라고 정의하고 기능을 구현해보자.</p>
<h2 id="1-이게-왜-문제일까">1. 이게 왜 문제일까</h2>
<hr>
<h3 id="발이-바닥에-묻히거나-떠-있는-상황">발이 바닥에 묻히거나 떠 있는 상황</h3>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/cb350864-e13c-44c9-9201-c3716454e9cc/image.png" alt="발 파묻힘 장애물"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/21a13e58-4b49-4e42-8674-7001913ce993/image.png" alt="발 파묻힘 경사"></th>
</tr>
</thead>
</table>
<p>3D 캐릭터가 디자이너가 만든 예쁜 돌무더기나 지형 위를 걸을 때, 애니메이션은 기본적으로 평지에서 걷는 것만 가정하고 있다. 당연히 경사가 달라지거나 바닥 높이가 변하면, 발바닥이 바위에 반쯤 파묻히거나 뜨게 된다. 아무리 모델이 멋있어도, 발이 어딘가에 박혀 있으면 시각적 몰입감이 크게 줄어들게 된다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/cb2b4b3c-bac5-45fa-b2b6-51d953e13793/image.png" alt="좌절핑"></p>
<p> (사실 이런거 신경쓰는 개발자의 기분이 제일 큰 피해를 본다.)
<del>일정에 조금만 여유가 있었다면...</del></p>
<h2 id="2-foot-ik의-기본-아이디어">2. Foot IK의 기본 아이디어</h2>
<hr>
<h3 id="ik란">IK란?</h3>
<blockquote>
<p>?IK란 역운동학(Inverse Kinematics)이라는 의미로 쉽게 풀어서 설명하자면 <code>위치</code>를 통해 <code>각도</code>를 구하는 것이라고 생각하면 된다. </p>
</blockquote>
<p>일반적으로 애니메이션은 스켈레톤에 조인트 각도를 미리 정해진 값으로 회전하여 만들어진다. 부모로부터 회전 값이 가중되며 관절의 끝 점의 최종 포지션이 결정되는 형태인데 이를 <strong>순운동학(FK)</strong>이라고 부른다.</p>
<p>이러한 조인트 포즈 작업을 반대 시각에서 바라 보는 것이 유용한 경우도 있다. 예를 들어 아바타가 특정 물체를 잡는 경우 특정 물체의 포지션으로 부터 역으로 각 관절의 회전 값을 계산하여 적용하는 것이다. 이와 같은 방식이 오늘 적용할 <strong>역운동학(IK)</strong>방식이다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/7c3f1a17-c242-4977-b65e-8fefc752d86b/image.png" alt="IK"></p>
<p>우리는 아바타의 발이 발 아래쪽에 위치한 지면에 딱 붙을 수 있는 위치를 구하고, 양 발의 위치를 기반으로 전체 스켈레톤의 포지션을 조정해주는 IK방식으로 이 문제를 해결해 볼 것이다.</p>
<h3 id="레이캐스트--animator-ik">레이캐스트 + Animator IK</h3>
<p><code>Foot IK</code>는 크게 두 단계로 작동한다.</p>
<ol>
<li><strong>레이캐스트</strong>: 캐릭터의 발 아래로 레이를 쏴서 “실제 바닥이 어디인지”를 찾습니다.  <ul>
<li>“아, 여기 돌이 있으니 y좌표가 원래보다 0.3m 높구나!” 식으로 계산.  </li>
<li>이 때 레이캐스트로 검출할 메시 데이터(Collider)와 레이어에 대한 셋팅은 해줘야 한다.</li>
</ul>
</li>
<li><strong>Animator IK</strong>: 유니티에서 지원하는 <strong><code>OnAnimatorIK(int layerIndex)</code></strong> 메서드로 발(LeftFoot, RightFoot)의 위치·회전값을 직접 설정한다.<ul>
<li>이때 Foot IK가 적용되는 모델은 <code>Humanoid Avatar</code>형태이여야 한다. 인간형이 아닌 병아리나 강아지와 같은 모델에 해당 기능을 적용할 수 없다. </li>
</ul>
</li>
</ol>
<p>이렇게 하면 발이 지면을 따라 딱 달라붙는 연출을 그래픽스나 휴머노이드에 대한 지식 없이 쉽게 구현할 수 있다.</p>
<h3 id="높낮이--경사도-보정">높낮이 &amp; 경사도 보정</h3>
<ul>
<li><strong>경사도 보정</strong>: 기울기를 계산해 발이 기울어지도록 설정해준다. 발의 포지션이 바뀌어도 회전값이 그대로면 어색한 상황이 지속된다.</li>
<li><strong>골반 보정</strong>: 발이 올라갔을 때 캐릭터 전체가 조금씩 올라와 자연스럽게 지면 위에 서 있게 보정해준다.</li>
</ul>
<p>이 부분이 제대로 구현 되면 언덕, 돌무더기, 경사진 경로 등에서도 발이 그럴싸하게 따라간다.</p>
<h2 id="3-실제-코드-구현">3. 실제 코드 구현</h2>
<hr>
<p>이번에는 <strong>Feet IK</strong>를 구현한 실제 코드 예시를 살펴보자. 아래 코드는 크게 두 부분으로 나눌 수 있다.</p>
<ol>
<li><strong><code>SolveFeetPositions()</code></strong>: 발이 지면에 닿는 위치(및 회전)를 미리 계산해 두는 과정 </li>
<li><strong><code>SetFeetIK()</code></strong>: Unity 애니메이션 IK(<code>OnAnimatorIK</code>)에서 실제 발 위치/회전을 적용하고, 골반(Pelvis)을 보정해주는 과정</li>
</ol>
<h2 id="unity-event-functions">Unity Event Functions</h2>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/c646a61a-6eb0-4391-b988-464c28d344b7/image.png" alt="Unity Event Call"></p>
<p>유니티의 이벤트 메서드들은 일정한 주기로 실행된다. 우리는 아바타의 애니메이션 계산이 종료되고 렌더링 하기 전에 발의 위치를 이동시키는 일종의 <strong>&#39;후처리 애니메이션&#39;</strong>을 진행 할 것이다. 전체적인 렌더링 순서는 아래와 같이 진행된다.</p>
<ol>
<li><code>Update</code> 메서드에서 사용자의 입력 값에 따라 캐릭터의 위치값 변경과 애니메이션 파라메터에 값 전달</li>
<li><code>ProcessAnimation</code>에서 유니티 애니메이터에 지정된 내용대로 애니메이션 수행</li>
<li>애니메이션이 수행 된 이후 캐릭터 파츠의 좌표 값을 이용하여 <code>OnAnimatorIK</code> 메서드에서 위치값 조정</li>
<li><code>WriteTransform</code>에서 <code>OnAnimatorIK</code>에서 지정해 준 값대로 트랜스폼 적용</li>
<li>아바타 렌더링</li>
</ol>
<br>

<h3 id="31-solvefeetpositions">3.1. SolveFeetPositions</h3>
<pre><code class="language-C#">        private FootIkSolverData FeetPositionSolver(Vector3 fromSkyPosition)
        {
            if (!Physics.Raycast(fromSkyPosition, Vector3.down, out var feetOutHit,
                                 raycastDownDistance + heightFromGroundRaycast, environmentLayer))
            {
                // return Default Value
                ...
            }

            var feetIkPositions = fromSkyPosition;
            feetIkPositions.y = feetOutHit.point.y;
            var feetIkRotations = Quaternion.FromToRotation(Vector3.up, feetOutHit.normal) * transform.rotation;

            return new FootIkSolverData
            {
                IsDetectGround = true,
                FootPosition   = feetIkPositions,
                FootRotation   = feetIkRotations
            };
        }</code></pre>
<p>IK를 적용시키기 전 미리 발의 포지션과 로테이션 값을 계산해준다.</p>
<p><code>fromSkyPosition</code>은 실제 발의 포지션에 up방향으로 <code>heightFromGroundRaycast</code>을 곱해 더해준 값이며, 이 위치에서 down방향으로 레이캐스트를 발사한다. 이 과정에서 검출되는 메시가 없다면 실패했다고 가정하고 default value를 리턴하며, 성공하면 적당한 Position과 Rotation값을 셋팅해준다.</p>
<ul>
<li><code>FootPosition</code>: 기존 발의 포지션에서 y값만 실제 바닥에 해당하는(raycast가 충돌한) 값으로 셋팅해준다.</li>
<li><code>FootRotation</code>: 발의 각도를 현재 경사에 딱 맞는 각도로 변환시킨다. 앞의 <code>FromToRotation()</code>은 up벡터를 지면의 normal벡터로 회전시킬 수 있는 회전 값을 계산해 주는 함수이다.</li>
</ul>
<br>

<h3 id="32-movepelvisheight">3.2. MovePelvisHeight</h3>
<pre><code class="language-C#">            var transformPositionY = transform.position.y;
            var lOffsetPosition    = _leftFootSolverData.FootPosition.y - transformPositionY;
            var rOffsetPosition    = _rightFootSolverData.FootPosition.y - transformPositionY;

            var totalOffset = lOffsetPosition &lt; rOffsetPosition ? lOffsetPosition : rOffsetPosition;
            var newPelvisPosition = ObjAnimator.bodyPosition + Vector3.up * totalOffset;

            newPelvisPosition.y = Mathf.Lerp(_lastPelvisPositionY, newPelvisPosition.y, pelvisUpAndDownSpeed);

            ObjAnimator.bodyPosition = newPelvisPosition;
            _lastPelvisPositionY = ObjAnimator.bodyPosition.y;</code></pre>
<p>현재 양 발의 위치가 원래의 오브젝트 포지션(발 끝의 포지션)에서부터 얼마나 y방향으로 멀어졌는지 체크하여 골반을 보정해주는 함수이다. 우리의 아바타는 밀.집모자 루피처럼 발이 늘어나지 않기 때문에 양 발의 위치가 변함에 따라 전체적인 스켈레톤의 위치가 이동해줘야 할 필요가 있다.</p>
<p>안타깝게도 유니티 Animator 시스템에서는 이와 같은 스마트한 기능이 구현되어 있지 않기 때문에 우리가 알아서 전체 바디 포지션의 위치를 조절해줘야 한다. 이는 신체의 중심(bodyPosition)인 골반(Pelvis)만 조정해주면 손쉽게 구현이 가능하다.</p>
<h4 id="골반보정-전후">골반보정 전/후</h4>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/341fd3b9-472b-476f-9368-2d7f85b5ad1e/image.png" alt="골반보정 전">(골반 보정 전)</th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/798c461c-2eea-4608-8187-24d55a41f633/image.png" alt="골반보정 후">(골반 보정 후)</th>
</tr>
</thead>
</table>
<p>골반 보정 전 후를 비교해보면 그 차이를 쉽게 알아볼 수 있을 것이다. 골반 보정 전에는 기존 BodyPosition에서 발만 아래로 뻗고 제대로 바닥과 발이 밀착되지 못한 모습을 볼 수 있는데, 골반 보정 후에는 전체적인 스켈레톤 포지션이 내려옴과 동시에 발이 바닥에 닿고(희미한 그림자 참고) 무릎도 이쁘게 굽히고 있는걸 볼 수 있다.</p>
<p>골반 위치를 조정해주는 부분에 디버깅을 걸어보면 전체적인 바디포지션이 <code>-0.26</code> 정도 낮아지는 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/3b870dae-0346-411a-aadb-e955ab3cb248/image.png" alt="골반보정 오프셋"></p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/521fa4d8-fabc-4b7b-be1b-4a5bbaf79a0b/image.png" width="50%"> 
</center>

<p><del>킹든갓택2와 같은 게임만이 고무고무 능력을 제어할 수 있다.</del></p>
<br>

<h3 id="33-movefeettoikpoint">3.3 MoveFeetToIkPoint</h3>
<p>이제 위에서 계산된 값을 바탕으로 IK를 적용해보자.</p>
<pre><code class="language-C#">        private void MoveFeetToIkPoint(AvatarIKGoal foot, FootIkSolverData solverData, ref float lastFootPositionY)
        {
            if (!solverData.IsDetectGround) return;
            var positionIkHolder = solverData.FootPosition;
            var rotationIkHolder = solverData.FootRotation;
            var targetIkPosition = ObjAnimator.GetIKPosition(foot);

            targetIkPosition = transform.InverseTransformPoint(targetIkPosition);
            positionIkHolder = transform.InverseTransformPoint(positionIkHolder);

            var yVariable = Mathf.Lerp(lastFootPositionY, positionIkHolder.y, feetToIkPositionSpeed);
            targetIkPosition.y += yVariable;
            lastFootPositionY = yVariable;
            targetIkPosition = transform.TransformPoint(targetIkPosition);

            ObjAnimator.SetIKRotation(foot, rotationIkHolder);
            ObjAnimator.SetIKPosition(foot, targetIkPosition);
        }</code></pre>
<ol>
<li>먼저 <code>GetIKPosition</code> 함수를 통해 현재 프레임에서 애니메이션 된 발의 포지션을 가져온다. 해당 프레임에서 이 함수를 호출하기 전 별도로 IK셋팅을 하지 않았다면 순운동학으로 애니메이션이 계산된 포지션의 값을 리턴한다.</li>
<li>이후 현재의 아바타의 발의 포지션과 기존에 계산된 타겟 IK포지션을 <code>모델 좌표계</code>로 변환시킨다.</li>
<li>이전 프레임에서 계산된 보정값과 기존에 계산된 타겟값을 <code>선형보간</code>하여 IK포지션이 부드럽게 셋팅될 수 있게 한다.</li>
<li>보정치를 타겟 IK포지션에 더해주고 월드좌표계로 변환 뒤 실제 IK Position에 적용된다.</li>
<li>이후 애니메이션을 보정해주는 작업은 유니티에게 맡기고 구현된 기능을 확인한다.</li>
</ol>
<br>

<h4 id="보충-모델-좌표계">보충: 모델 좌표계</h4>
<p>게임 세계에는 다양한 좌표계가 존재한다. 그 중 모델 좌표계는 모델의 중심(휴머노이드 아바타의 경우 일반적으로 발끝)을 원점으로 하는 좌표계를 의미한다. 해당 좌표계에서는 모델이 게임에 배치됨에 따라 적용되는 변환값들에 영향을 받지 않고 좌표계산을 진행할 수 있다.</p>
<h4 id="보충-선형보간">보충: 선형보간</h4>
<p>1차원 직선상에서 두 점의 값이 주어졌을 때 그 사이의 값을 추정하기 위해 선형적으로 계산(비례식)하는 방법이다. Lerp함수의 마지막에 넣어주는 값(<code>feetToIkPositionSpeed</code>)에 따라 <code>lastFootPositionY</code>에 가까운 값을 반환할지, <code>positionIkHolder.y</code>에 가까운 값을 반환할지 결정된다. 게임에서는 오브젝트의 부드러운 변환을 보여주기 위해 이러한 보간 함수를 자주 이용한다.</p>
<h2 id="4-최종-적용-영상">4. 최종 적용 영상</h2>
<hr>
<p>!youtube[Cn24_EwDsOw?si=YZAnQpuBfTKblLM2]</p>
<p>와... 정말 Chill하다..</p>
<h3 id="더-해볼만한-것">더 해볼만한 것</h3>
<p>사실 위 영상만 봐도 파라메터 셋팅이 제대로 되지 않았다.</p>
<ul>
<li>쉐도우에 대한 처리가 제대로 되지 않아 위치감을 느끼기 어려움</li>
<li>교복 캐릭터가 위로 경사로 위로 올라가면서 발 끝이 오브젝트에 묻힘</li>
<li>고양이귀 캐릭터가 움직이면서 발이 떨리는 포인트가 있음</li>
</ul>
<h4 id="파라메터-셋팅">파라메터 셋팅</h4>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/e4a4dbbb-056c-4473-b159-fced6f183306/image.png" alt="끼용"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/cd872be2-a9d9-43a1-808c-ef187f6b315d/image.png" alt="끄앙"></th>
</tr>
</thead>
</table>
<p>게임에 따라 디테일한 씬의 구성이 다르고 사용하는 모델에 따라서 추가적인 위치 보정이 필요할 수도 있다. 따라서 사용할 아바타 마다 직접 테스트 씬을 돌아다니며 디테일한 파라메터를 조절해 줄 필요가 있다. 위의 아름다운 케이스들은 사용하는 파라메터를 과하게 조정했을때 보이는 현상들이다.</p>
<ul>
<li>추가적인 Pelvis Offset</li>
<li>Pelvis와 Foot의 포지션 보정때 사용하는 선형보간의 파라메터</li>
</ul>
<p>보통 회사에서는 어떤 파라메터를 제어할지는 클라이언트(혹은 TA)개발자가 지정하고 이를 기획자가 원하는 씬을 돌아다니며 파라메터를 조절하고 아무리 조절해도 마음에 안드는 경우 클라이언트에게 추가 수정을 요청하는 식으로 업무가 진행된다. 하지만 이러한 업무분담이 안되는 안타까운 상황이라면 클라 개발자가 알잘딱깔센으로 다 해야한다.</p>
<h4 id="특정-상황에서만-ik-적용">특정 상황에서만 IK 적용</h4>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/2930e190-cd9d-4ac3-9ea7-5da1fc9bfdd5/image.png" alt="발이 쭉"></p>
<p>특정 상태에서는 발에 땅이 닿으면 어색한 경우도 있다.</p>
<p>점프를 하거나 특정 애니메이션을 수행하는 경우 IK를 지정하기 싫다면 함수 시작부 early return을 하는 부분을 참고하여 추가적인 리턴 조건을 추가하면 될 것이다.
(예를 들어 특정 애니메이션 상태인 경우에만 Set IK로직을 수행하는 방식 등..)</p>
<h3 id="보충-animation-rigging">보충: Animation Rigging</h3>
<p>만약 애니메이션 리깅을 사용한다면, 아래 프로젝트를 참고하면 될 것 같다. 위와 유사한 방식으로 Animation Rigging패키지를 사용하여 IK를 구현하였다.</p>
<p>!youtube[0muAp4M7I5s?si=H1q08ppuzpmtdxbI]</p>
<p><a href="https://github.com/rob1997/Grounder">rob1997/Grounder</a></p>
<p>(Animation Rigging에 대해서는 별도의 글로 다룰 수 있으면 좋겠다)</p>
<h2 id="5-마무리">5. 마무리</h2>
<hr>
<p><strong>Foot IK</strong>는:  </p>
<ul>
<li>간단한 레이캐스트 + Unity Animator IK를 통해 </li>
<li>캐릭터 발이 지형 높이에 맞춰지도록 하여</li>
<li>발이 파묻히거나 공중에 뜨는 어색함을 없애줍니다.</li>
</ul>
<p>게임의 몰입감은 디테일에서 온다고 생각한다. 발 하나만 제대로 붙여줘도 플레이어가 느끼는 세상의 몰입도가 크게 올라갈 수 있다. 한 발만 제대로 내딛어도 게임은 더욱 게임다워진다. 이러한 테크닉을 하나씩 적용해가며 더욱 사실감 있는 게임개발을 진행해 보자.</p>
<p><a href="https://github.com/eugene-doobu/Ganshin-Impact/pull/162">구현 PR</a></p>
<br>

<h3 id="참고">참고</h3>
<hr>
<p><a href="https://docs.unity3d.com/kr/2019.4/Manual/InverseKinematics.html">역운동학</a>
<a href="https://docs.unity3d.com/6000.0/Documentation/Manual/execution-order.html">execution-order(Unity Event Functions)</a>
<a href="https://docs.unity3d.com/Manual/AvatarCreationandSetup.html">HumanoidAvatar</a>
<a href="https://www.youtube.com/watch?v=MonxKdgxi2w">Build a Foot IK System from Scratch for Unity (C#)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[라이브 게임 에셋 관리 개선기 - 2.메모리 사용 절감]]></title>
            <link>https://velog.io/@eugene-doobu/%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EA%B2%8C%EC%9E%84-%EC%97%90%EC%85%8B-%EA%B4%80%EB%A6%AC-%EA%B0%9C%EC%84%A0%EA%B8%B0-2.%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%82%AC%EC%9A%A9-%EC%A0%88%EA%B0%90%EB%82%98%EC%9D%B8%ED%81%AC%EB%A1%9C%EB%8B%88%ED%81%B4</link>
            <guid>https://velog.io/@eugene-doobu/%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EA%B2%8C%EC%9E%84-%EC%97%90%EC%85%8B-%EA%B4%80%EB%A6%AC-%EA%B0%9C%EC%84%A0%EA%B8%B0-2.%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%82%AC%EC%9A%A9-%EC%A0%88%EA%B0%90%EB%82%98%EC%9D%B8%ED%81%AC%EB%A1%9C%EB%8B%88%ED%81%B4</guid>
            <pubDate>Sun, 29 Sep 2024 17:56:33 GMT</pubDate>
            <description><![CDATA[<h1 id="인트로">인트로</h1>
<hr>
<p>저번 글에서, 게임이 오래 지속적으로 업데이트되면 컨텐츠가 계속해서 늘어나게 되고, 이를 따로 관리하기 위해 에셋 번들 등의 기법을 통해 에셋을 빌드와 분리해서 관리하기도 한다는 것에 대해 이야기하였다. 이렇게 빌드와 에셋이 분리되더라도 게임을 실행시키기 위해서는 빌드 파일과 온라인에서 별도로 다운받은 에셋들을 동시에 메모리에 올려야 한다. 즉, 게임이 계속 개발되며 리소스가 늘어나게 되면 메모리에 로드해야 하는 에셋들의 수도 늘어난다는 뜻이다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/87876574-6623-4eff-8876-586698d8541b/image.png" alt="또리코네"></p>
<p>현재 애플 iOS의 최신 기기인 iPhone 16 Pro Max의 경우에도 RAM 용량은 8GB이며, 조금 구버전의 디바이스인 iPhone 12의 경우 RAM 용량은 4GB로, 위 프리코네의 추가 리소스를 한 번에 담을 수 없는 크기이다. 그리고 전 세계 사람들이 플레이하는 게임을 만들기 위해서는 현 시대 최고 스펙의 장비가 아닌 저사양 디바이스를 타겟으로 테스트를 해야 많은 사람들이 안정적으로 플레이할 수 있는 게임이 된다.</p>
<p>이와 같은 논리는 PC 게임에도 동일하게 적용된다. 사람들은 데스크탑 기준으로 보통 16GB 램을 주로 사용하는 것으로 보이며, 신경 좀 쓰면 32GB, 돈 좀 쓰면 64GB의 램을 사용하는 게 일반적일 것이다.<em>(뇌피셜임)</em> 하지만 요즘 나오는 PC 게임들의 용량은 점점 커지고 있으며 이제는 100GB 게임의 시대라고 해도 될 정도이다. 아래 리스트는 최근 출시된 고용량의 게임들을 정리해놓은 것이다.</p>
<ul>
<li>ARK: Survival Evolved - 275GB</li>
<li>Call of Duty: Modern Warfare - 250GB</li>
<li>final fantasy: 15 - 160GB</li>
<li>Star Wars Jedi: Survivor - 130GB</li>
<li>The Last of Us Part 1 - 100GB</li>
<li>Diablo 4 - 90GB</li>
</ul>
<p>이처럼 게임에는 리소스를 로드하기 위해 큰 메모리가 필요하고, 모든 게임 리소스를 한 번에 RAM에 올릴 수 없다는 것을 알 수 있을 것이다. 그렇다고 게임 리소스를 필요한 순간마다 즉시 로드하고 해제한다면 게임이 뚝뚝 끊길 것이며 이는 유저에게 불쾌한 경험이 될 것이다. 그리고 디바이스의 메모리에는 우리가 만든 게임뿐만 아니라 다양한 프로세스가 올라갈 수 있음을 명심해야 한다.</p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/c5b8889a-e2bf-41d7-9427-699a82ac4dba/image.png" width="70%"> 
</center>

<p>이러한 이유로 에셋 로드/언로드 타이밍을 지정해 메모리 관리를 하는 것은 게임 개발에서 특히 중요한 부분이고, 이번 글에서는 나인크로니클(9C)에서 메모리 관리를 위해 어떤 작업을 하였는지 작성해보겠다. 또한 현재 타겟 플랫폼에 맞게 사용할 리소스를 최적화하여 게임에 사용되는 리소스를 줄이는 작업도 진행하였는데, 이 부분에 대해서도 같이 다뤄보도록 하겠다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/d6e0e64a-f04a-4b67-84e5-8bbce5db2744/image.png" alt="고기굽기"></p>
<p>1부. 어드레서블 에셋 도입<br><strong>2부. 메모리 사용 구조 개선 / 리소스 최적화</strong> &lt;- 현재 글<br>번외1. 리소스 최적화 기법<br>3부. DLC를 통한 패치 시스템</p>
<h2 id="개요">개요</h2>
<hr>
<h4 id="오브젝트-풀-개요">오브젝트 풀 개요</h4>
<p>몇 메가바이트 단위 혹은 그 이상의 크기를 가진 리소스를 메인 스레드를 블록하고 로드하려고 시도하면, 일정한 FPS를 유지해야 하는 게임의 프레임에 큰 영향을 주게 된다. 로드를 비동기로 한다고 하더라도 상황에 따라 프레임에 영향을 주지 않을 뿐, 게임 플레이 자체가 어색해지는 상황이 생길 수 있다. 예를 들어, 총을 발사하려고 공격 키를 눌렀는데 총알 리소스가 로드되기 위해 0.5초 동안 발사가 안 된다고 생각해보자. 유저 경험이 매우 불편할 것이다.</p>
<p>또한 로드된 오브젝트를 사용 직후 바로 파괴한다고 하면, C#의 가비지 컬렉터에 좋지 않은 영향을 줄 수 있다. 특히 총알과 같이 자주 사용되는 오브젝트들은 메모리 파편화를 일으키고 Full GC의 호출을 가속화시킬 것이다. 유니티의 GC는 .NET의 GC보다 효율적이지 못하다. 세대 구분, SOH와 LOH, 메모리 재정렬 같은 개념이 없기 때문에 더욱 예민하게 다루어주어야 한다.</p>
<p>GC는 언제 호출될지 파악할 수 없기 때문에, 이러한 점을 고려하지 않고 개발을 하다 보면 게임이 지속해서, 혹은 중요한 순간에 뚝 끊길 수 있는 위험이 생기게 된다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/669b11a3-2c58-4121-8012-8ce1d80edc23/image.png" alt="GC"></p>
<p>이러한 문제를 해결하기 위해 <strong>오브젝트 풀링(Object Pooling)</strong> 기법을 활용할 수 있다. 오브젝트 풀링은 자주 사용되는 오브젝트를 미리 생성해 두고, 필요하지 않을 때는 일시적으로 비활성화했다가 다시 필요해지면 재사용하는 방법이다. 이를 통해 메모리 할당과 해제를 최소화하여 가비지 컬렉션의 빈도를 낮추고, 게임의 성능을 향상시킬 수 있다. 예를 들어, 총알 객체를 매번 생성하고 파괴하는 대신, 미리 일정 수의 총알 객체를 생성해 두고 필요할 때 가져와 사용한 후 다시 반환하는 방식이다. 이렇게 하면 메모리 파편화와 GC로 인한 성능 저하를 효과적으로 줄일 수 있다.
<br></p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/52bbe188-d36f-40a4-9e05-bab95e0fe1d7/image.png" width="70%"> 
</center>

<p>위 사진에서 곰이 레몬이 필요할 때마다 들고 있는 상황이라고 생각해보자. 기존의 방식이 레몬이 필요할 때마다 씻고 옷을 입고 밖으로 나가서 시장에서 레몬을 구매해 오는 것이었다면, 오브젝트 풀은 레몬을 미리 여러 개 구매해 뒀다가 필요할 때마다 필요한 만큼 들고 있는 것이라고 할 수 있다.</p>
<h4 id="오브젝트-풀-개선">오브젝트 풀 개선</h4>
<p>9C에서는 자주 사용될 오브젝트를 오브젝트 풀링을 위해 사용하도록 구현되어 있다. 아래 스크린샷과 같이 다양한 콘텐츠들에 사용될 오브젝트들이 풀에 등록이 되어 있었다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/58325cf4-70b6-4e4a-bba1-d4e7c9ddec09/image.png" alt="UI 풀"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/08db71e2-9239-417a-807c-79b6c01b83d1/image.png" alt="스테이지 풀"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/a808393d-6645-4133-8761-a935ef24ebcd/image.png" alt="오디오 풀"></th>
</tr>
</thead>
</table>
<ul>
<li>게임에 사용하는 모든 UI 1종씩</li>
<li>게임에 사용하는 이펙트 3~5개씩, 필요한 경우 추가 생성</li>
<li>모든 사운드 오브젝트 1종씩</li>
</ul>
<p>이 풀의 문제는 게임 콘텐츠의 모든 오브젝트들의 수명이 영구적이었다는 것이다. 이로 인해 메모리가 계속 쌓이는 상황이었으며, iOS의 경우 특정 디바이스에서 앱이 종료되는 문제가 발생할 정도로 메모리 이슈가 있어 한 번의 응급처치가 들어간 상황이었다.</p>
<p>이후 신규 콘텐츠의 업데이트가 연달아서 예정되어 있던 상황이라, 언제 터질지 모르는 메모리 문제를 빨리 해결해야겠다고 생각했고, 빠른 시간 안에 메모리 사용량을 줄일 방법을 찾기 위해 프로파일링 툴을 통해 현재 게임을 위해 사용하고 있는 메모리를 검사해보았다.</p>
<h1 id="에디터-프로파일링">에디터 프로파일링</h1>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/d190a35a-cb0f-4760-bcad-bafc8372d7ec/image.png" alt="스크린샷"></p>
<p>위 오브젝트 풀에 저장되어 있는 오브젝트들이 얼마만큼의 메모리를 점유하고 있는지 확인하기 위한 테스트를 진행했다. 먼저 비교 대상인 A의 경우 기존 게임에서 스테이지에 돌입 후 메모리 캡처를 한 것이고, B는 현재 사용 중이지 않는 UI(Widget), 이펙트, 사운드를 제거 후 <code>GC.Collect()</code>를 강제로 돌린 후 캡처를 한 결과이다.</p>
<ul>
<li>(In Used) Native Memory -&gt; 393.5 MB 차이 발생</li>
<li>Untracked Memory -&gt; 366.5 MB 차이 발생</li>
</ul>
<p>에디터에서 진행한 프로파일링은 에디터의 UI, 내부적인 시스템으로 인해 많은 노이즈가 끼어있어 정확한 수치를 믿을 수는 없다. 그리고 문제가 되는 모바일 디바이스와 에디터가 구동되고 있는 윈도우의 플랫폼 차이가 존재하기에 더욱 그렇다. 하지만 현재 사용하지 않을 오브젝트만 적당히 제거 한다면 1~200MB 정도의 절감할 수 있을 것이라는 기대를 할 수 있게 되었다.</p>
<p>하지만 여기서 문제가 있다. 기존 모든 리소스를 일단 생성해 두고 사용하던 풀 방식에서, 필요한 오브젝트만 생성하여 사용하는 방식으로 바꾸는 것은 기존 방식에서 큰 변화가 필요했고, 그중에서도 가장 많은 메모리를 차지하는 것으로 추정되는 UI의 구조를 변경하는 것은 현재 게임의 핵심 구조를 뜯어고치는 정도의 대규모 리메이크가 필요한 작업이었다.</p>
<p>심지어 팀원들이 관련하여 콘텐츠 작업을 지속적으로 진행해야 했기에, 특히 관리하기 힘든 게임 에셋의 작업 충돌을 피하기 위해 이와 같은 대규모 개선 작업은 진행하기 어려웠다. 게다가, 라이브 서비스 중인 게임의 경우 업데이트와 패치가 실시간으로 이루어지기 때문에, 대규모 변경은 예상치 못한 버그를 초래할 수 있다는 우려도 있다. 이는 유저들에게 직접적인 영향을 줄 수 있어 더욱 신중한 접근이 필요했다. 현실적으로 단기간에 메모리 사용량을 줄이기 위한 방법을 찾기 위해 어떤 에셋이 얼마만큼 메모리를 사용하는지 분석해 보기로 하였다.</p>
<p>(이 글을 쓰는 지금 시점 UI구조를 바꾼것으로 인한 버그가 발생해서 급하게 고치고 오는 길이다)</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/6a8dba62-3d12-4c50-9ad8-79d0320316ec/image.png" alt="메모리분석"></p>
<p>위 A와 B 스냅샷에서 리소스가 사용하는 메모리 사용량의 변화를 체크한 결과이다. 메모리 사용량을 기준으로 내림차순 정렬을 하였더니, 같은 리소스가 A와 B에서 다른 해시 값을 가지며 <code>new/delete</code>가 반복되어 표시되는 경향이 있어 어떤 리소스를 얼마만큼 줄였는지에 대해서 파악이 힘들었다.</p>
<p>하지만 어떤 일을 하면 메모리 사용량을 줄일 수 있는지 파악할 수는 있었다. 대부분의 메모리가 <code>Texture2D</code>, <code>AudioClip</code>을 위해 할당된 것을 확인할 수 있었다. 가장 상위에 위치한 2048x2048 사이즈의 이미지(아틀라스)가 압축되지 않은 사이즈인 32MB 용량으로 존재하는 것을 확인할 수 있고, 이와 비슷한 Texture2D 여러 개 존재하다. AudioClip도 이상할 정도의 메모리를 점유하고 있는 걸 보니 이 둘만 줄여도 많은 양의 메모리가 줄어들 것으로 기대할 수 있었다.</p>
<ul>
<li>압축되지 않은 텍스처들이 많이 존재</li>
<li>사운드 옵션이 통일되어 있지 않고, 특정 사운드에서 많은 메모리 사용 중</li>
<li>시트 데이터로 보이는 것들 중 많은 메모리를 점유 중인 것이 있음</li>
<li>렌더 텍스처들이 먹는 메모리도 컸음</li>
<li>특정 콘텐츠에서만 사용하는 리소스들이 항상 많은 크기의 메모리를 점유 중</li>
</ul>
<p>그 외에도 당장 적용하기 힘들지만 장기적으로 적용할 만한 개선 사항들을 파악할 수 있었다. 일단은 텍스처와 사운드에 대한 용량을 줄이는 것으로 빠르게 효율적인 메모리 사용량 감소 작업을 진행할 것이다.</p>
<p>(스프라이트 아틀라스는 여러 스프라이트 이미지를 하나의 큰 이미지 파일로 결합하는 기술이다. 그래픽 처리 효율성을 향상시키고, 렌더링 속도를 높이며, 메모리 사용을 최적화하는 데 도움을 준다. 이에 대한 자세한 설명과 적용 과정은 별도의 글에서 다뤄보도록 하겠다.)</p>
<h1 id="ios-프로파일링">iOS 프로파일링</h1>
<p>iOS에서 메모리 프로파일링을 하는 이유는 다음과 같다</p>
<ul>
<li>PC와 모바일을 동시에 지원할 경우, 메모리 문제는 보통 iOS 디바이스에서 가장 먼저 발생한다</li>
<li>iOS프로파일러가 쓰기 편하다</li>
<li>이전에 에디터에서 프로파일링을 했지만, 그 결과에는 에디터 자체의 데이터가 포함되어 불필요한 노이즈가 발생한다. 이러한 노이즈를 제거하고 정확한 메모리 사용량을 측정하기 위해 특정 디바이스에서 앱을 실행하고 프로파일링하는 것이 좋다.</li>
</ul>
<h3 id="1-기존-프로젝트에서-성능측정">1. 기존 프로젝트에서 성능측정</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/4e20be6b-a335-4eed-8216-54aa733cfafe/image.png" alt="iOS 프로파일링"></p>
<p>기존 프로젝트에서 콘텐츠를 진행하면서 메모리 사용량을 측정해보았다. 콘텐츠를 실행할 때 필요한 리소스들이 누적되어 메모리 사용량이 지속적으로 증가하는 것을 확인할 수 있었다.</p>
<pre><code>로비 : 1.51GB
마켓 : 1.56GB
워크샵-소환 : 1.67GB
소환영상 시청중 : 1.73GB
월드보스 전투중 : 1.82GB
로비 : 1.8GB
몬스터콜렉션 : 1.83GB
아레나 전투 로딩 : 1.94GB
아레나 전투 후 로비 : 1.96GB
제작강화룬업글분해 후 로비: 2.01GB
시즌패스 : 2.14GB
컬랙션 : 2.14GB
스테이지 329 : 2.2GB
로비 : 2.17GB
어드벤처보스 1층 전투 : 2.32GB
어드벤처보스 돌파 : 2.43GB
로비 : 2.41GB</code></pre><p><img src="https://velog.velcdn.com/images/eugene-doobu/post/fe239a4d-152c-4969-ada4-b4e537324ca0/image.png" alt="gpu"></p>
<p>이렇게 iOS에서 GPU 성능측정과 리소스별 메모리 사용량을 점검할 수 있다. 위 에디터 프로파일링 결과와 비슷하게 압축되지 않은 아틀라스들이 상당히 많은 메모리를 차지하는 것을 확인할 수 있다. 실제 게임이 돌아가는 모바일 디바이스에서도 텍스처 압축만 잘 하면 많은 메모리 여유를 확보할 수 있음을 확신하는 순간이였다.</p>
<h3 id="2-메모리-정리-기능-추가">2. 메모리 정리 기능 추가</h3>
<p>텍스처 압축에 앞서, &#39;로비로 갈 때마다&#39; 메모리를 수동으로 정리하는 기능을 추가했다. 이전에 9C에서는 대부분의 오브젝트들이 풀에 저장되어 지속적으로 메모리가 쌓이는 형태라고 이야기했지만, 그렇지 않은 콘텐츠들도 존재했다. 기본적으로 전투에서 등장하는 몬스터 스파인 리소스들은 필요할 때 로드하고 전투가 끝나면 파괴하는 구조로 구현되어 있었다.</p>
<p>또한 이전에 iOS에서 메모리 사용량이 많아 앱이 종료되는 이슈가 있었고, 이에 따른 응급처치가 이루어졌는데, 로그인 단계에서 사용하는 오브젝트들을 로비로 들어오는 과정에서 파괴하는 기능이 구현되어 있었다. 하지만 이러한 작업에서도 메모리가 깔끔하게 정리되지 않는 문제가 있었다.</p>
<p>유니티에서는 특정 리소스를 사용하던 오브젝트가 해제되어도 사용하던 리소스를 즉시 해제하지 않고, 씬이 종료될 때 해제하도록 구현되어 있다. 이는 메모리 관리 측면에서 이점이 있을 수 있지만, 우리가 원하는 메모리 최적화에는 걸림돌이 될 수 있다. 예를 들어, 게임 플레이 도중에 필요하지 않게 된 거대한 텍스처나 오디오 클립이 있다면, 해당 오브젝트를 파괴해도 메모리에서 즉시 해제되지 않기 때문에 메모리 사용량이 계속 높게 유지된다.</p>
<p>문제는 9c가 단일 씬 구조로 되어있었기에 로비에 진입하면서 파괴된 오브젝트들에서 사용하던 리소스가 제대로 정리되지 않던 문제가 있었던 것이다. 오브젝트 풀에 쌓이고 있는 오브젝트를 정리하기 전에, 이러한 리소스 청소 과정을 도입해야 겠다고 생각하였고, 해당 기능을 추가하고 메모리 사용량을 다시 측정해보았다. 변경된 코드는 매우 간단하다.</p>
<pre><code class="language-c#">        // Clear Memory
        Resources.UnloadUnusedAssets();
        GC.Collect();</code></pre>
<p><a href="https://github.com/planetarium/NineChronicles/pull/5319">PR: clear memory on room enter</a></p>
<p>이와 같이 코드를 수정하고 나서 다시 메모리를 측정 한 결과이다. 최종 결과 기준 약 200MB 정도의 메모리 사용이 절감되었다.</p>
<pre><code>로비: 1.44GB (기존 1.51GB)
마켓→소환→월드보스전투
로비: 1.61GB (기존 1.8GB)
몬콜→아레나 전투
로비:  1.63GB (기존 1.96GB)
제작강화룬업글분해
로비: 1.72GB (기존 2.01GB)
시즌패스 컬랙션 스테이지329, 330,331(원래 329만 했었는데 실수로 두스테이지 더돔)
로비: 1.92GB(기존 2.17GB 이상)</code></pre><h3 id="3-오디오아틀라스-압축-옵션을-적용하고-다시-테스트">3. 오디오/아틀라스 압축 옵션을 적용하고 다시 테스트</h3>
<p>다음으로, 아틀라스들이 압축될 수 있도록 설정하고 사운드 옵션을 통일하여 다시 빌드를 진행했다. 이때 사용한 옵션들과 세부 사항은 이 글에서 다루기에는 분량이 많아질 것 같아 별도의 글에서 따로 다뤄보도록 하겠다.</p>
<p>[PR: 아틀라스 셋팅 통일]
(<a href="https://github.com/planetarium/NineChronicles/pull/5318">https://github.com/planetarium/NineChronicles/pull/5318</a>)
[PR: audio controller polishing]
(<a href="https://github.com/planetarium/NineChronicles/pull/5283">https://github.com/planetarium/NineChronicles/pull/5283</a>)</p>
<pre><code>로비: 1.13GB(기존 1.44GB) [초기: 1.51GB]
마켓→소환→월드보스전투
로비: 1.3GB(기존 1.61GB) [초기: 1.51GB]
몬콜→아레나 전투
로비:  1.31GB(기존 1.63GB) [초기: 1.96GB]
제작강화룬업글분해
로비: 1.4GB(기존 1.72GB) [초기: 2.01GB]
시즌패스 컬랙션 스테이지329
로비: 1.48GB(기존 1.92GB) [초기: 2.17GB]</code></pre><h3 id="결론">결론</h3>
<ul>
<li>사용하지 않는 리소스 정리로 인한 여유 메모리 확보</li>
<li>오디오 및 아틀라스 설정을 정리하여 리소스 메모리 사용량 감소</li>
</ul>
<p>클라이언트 메모리 사용량을 30% 감소(2.1gb → 1.4gb)</p>
<h1 id="앱-용량">앱 용량</h1>
<hr>
<p>이번 글은 메모리 최적화에 대한 주제를 다루는 글이지만, 관련 작업을 하면서 앱 용량도 효과적으로 줄일 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/9354bf71-652f-4d8d-a333-a950540ca25f/image.jpg" alt="8월드"></p>
<h3 id="1-기존-빌드-결과물-용량">1. 기존 빌드 결과물 용량</h3>
<p>앱 용량 비교는 많은 리소스가 추가되었던 8월드를 기준으로 한다. 실제 앱 용량을 비교하기 전에, 모바일 빌드 CI에서 나온 결과물의 용량을 먼저 확인하여 얼마나 용량이 줄었는지 파악했다. 이 결과물은 실제 앱 용량이 아니라 해당 플랫폼에 업데이트를 준비하기 위한 것으로, 실제 앱 용량은 이보다 더 가볍다.</p>
<p><strong>안드로이드: 1.2gb</strong>
<strong>IOS: 2.76gb</strong></p>
<p><a href="https://github.com/planetarium/NineChronicles/pull/4336">8월드 리소스 PR</a></p>
<h3 id="2-스프라이트-리소스-정리">2. 스프라이트 리소스 정리</h3>
<p>이번에는 기존 프로젝트에서 사용하는 많은 양의 텍스처가 압축되지 않고 있다는 문제를 발견했다. 또한 하나의 텍스처가 여러 아틀라스에서 중복 포함되는 문제가 있었고, 모든 스파인 아틀라스가 <code>POT(Power of Two)</code> 형태가 아니라 특정 디바이스에서 압축이 불가능한 상황이었다.</p>
<p>이를 해결하기 위해 먼저 안드로이드에서의 압축을 위해 스파인 아틀라스 텍스처를 POT 형태로 고정하여 추출하였다. 이후 모든 텍스처에 대해 압축 설정과 아틀라스 정리를 진행했다.</p>
<p><a href="https://github.com/planetarium/NineChronicles/pull/4373">PR: 스파인 atlas pot</a>
<a href="https://github.com/planetarium/NineChronicles/pull/4419">PR: 텍스처 패킹</a></p>
<p><strong>안드로이드: 899MB</strong> - 8월드 대비 -301MB
<strong>IOS: 2.07GB</strong> - 8월드 대비 -690MB</p>
<p>해당 챕터에서 사용한 최적화 기법들에 대한 상세 내용은 추후 별도의 글에서 다뤄보도록 하겠다.</p>
<h3 id="실제-앱-용량-비교">실제 앱 용량 비교</h3>
<p>빌드 산출물은 위와 같은 결과가 나왔다. 이제 실제 앱 스토어에 등록되어있는 앱의 크기를 통해 유저들이 다운받을 앱 용량이 얼마나 줄었는지 확인해보자.</p>
<p>iOS: <code>625MB</code> -&gt; <code>460MB</code>  (앱 스토어 커넥트에서  <code>압축 파일 크기</code>)
Android: <code>648MB</code> -&gt; <code>478MB</code> (구글 플레이 콘솔에서 <code>원본 파일</code>)</p>
<p>iOS의 경우 <strong>165mb차이로 26.4%</strong>
Android의 경우 <strong>170mb차이로 26.2%</strong></p>
<p>평균 26.3% 정도의 용량을 절감시킬 수 있었다. 이로 인해 앱 다운로드를 통하여 소모될 전력을 절감시켜 지구 온난화의 가속을 둔화시킬 수 있었다.</p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/9d2c5536-1000-49cd-b07e-208537b63c4a/image.png" width="70%"> 
</center>

<h1 id="마무리">마무리</h1>
<hr>
<p>사실 아직도 최적화 할 일이 많이 남아있다. </p>
<h3 id="오브젝트-풀의-메모리-사용량">오브젝트 풀의 메모리 사용량</h3>
<p>처음에 9c의 오브젝트 풀 구조에 대한 문제점을 이야기하였다. 하지만 이를 근본적으로 고치는 작업은 진행하지 못했다. 이를 해결하지 못하면 콘텐츠가 추가됨에 따라 앱 사용 메모리가 지속해서 증가하는 현상이 계속 존재할 것이고, 언젠가는 발목을 크게 잡게 될 가능성이 있다. </p>
<p>이를 개선하기 위한 첫 삽으로 로그인 씬을 분리하는 작업을 진행해 보았다. 실제 메모리 사용량을 절감하는 데 큰 도움은 안되지만, 앞으로는 콘텐츠 별로 씬을 분리할 수 있고, 모든 오브젝트가 항상 살아있는 것이 아닌 필요한 순간마다 생성해서 풀에 등록해 놓을 수 있도록 적용하는 작업을 시작할 수 있게 되었다.</p>
<p><a href="https://github.com/planetarium/NineChronicles/pull/5452">PR: 로그인 씬 분리</a></p>
<h3 id="아틀라스-정리">아틀라스 정리</h3>
<p>위에서 스프라이트 리소스 정리를 진행하였지만 모든 아틀라스에 대한 정리를 진행하지 못했다. 여전히 중복해서 아틀라스에 등록되는 텍스처가 존재하고, 아틀라스 구조에 대한 일관성을 확보하지 못했다. 한 화면에 그려질 가능성이 있는 오브젝트들만 패킹하여 렌더링할 수 있도록 아틀라스가 구성이 되어야 할 텐데, 이에 대한 점검도 필요할 것으로 보인다. 이에 대응하기 위한 이슈는 만들어 두었지만, 손을 대고 있지 못하는 상황이다.</p>
<p><a href="https://github.com/planetarium/NineChronicles/issues/4324">issue: 아틀라스 리소스 정리</a></p>
<p>이는 모두 9c에서 사용하는 근본 구조에서 기인한 문제라고도 볼 수 있다. 프로젝트의 초기 단계부터 구조가 설계되고, 그 위에 추가적인 콘텐츠 코드들이 쌓이고 에셋들도 그것에 맞게 쌓아가다 보면 후반에 와서 최적화를 하기에는 곤란해진다. 언젠가 이를 개선할 기회가 생기거나 다시 메모리 문제가 생기기 직전의 상태까지 온다면 버그와 작업 충돌이 생길 수 있음을 각오하고 해당 구조를 뜯어고쳐야 할 순간이 올지도 모르겠다.</p>
<p>이 외에도 몬스터 스파인 에셋 데이터를 저장하는 방식을 바꾸어 로드시간을 단축하기도 하였다. 이와 관련된 내용과 해당 글에서 적용한 최적화 기법에 대한 자세한 내용들은 다음에 작성할 <code>번외1. 리소스 최적화 기법</code>글에서 자세히 다뤄보도록 하겠다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/7ac2a11d-0b69-4e2d-83a3-b4e2dc6c8360/image.png" alt="끝"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[홍정모 연구소 자료구조 압축코스 완강 후기]]></title>
            <link>https://velog.io/@eugene-doobu/%ED%99%8D%EC%A0%95%EB%AA%A8-%EC%97%B0%EA%B5%AC%EC%86%8C-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%95%95%EC%B6%95%EC%BD%94%EC%8A%A4-%EC%99%84%EA%B0%95-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@eugene-doobu/%ED%99%8D%EC%A0%95%EB%AA%A8-%EC%97%B0%EA%B5%AC%EC%86%8C-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%95%95%EC%B6%95%EC%BD%94%EC%8A%A4-%EC%99%84%EA%B0%95-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sat, 24 Aug 2024 17:30:59 GMT</pubDate>
            <description><![CDATA[<p>갑자기 생긴 휴일 기념으로 이전에 듣다가 묵혀놨었던 &#39;<strong>홍정모 연구소의 자료구조 압축코스</strong>&#39; 강의를 완강하였다. 학교에서 자료구조 공부도 해보고 코테를 위해 문제를 어느정도 풀어봤던 사람으로써, 이번 강의를 듣는 목표는 스스로 생각하는 힘을 기르기, 그를 위해 힌트를 듣지 않고 바로 강의의 문제 풀어보기이다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/4a94bc50-09a9-43b9-ab69-663df29b7306/image.png" alt="멈추고 직접~"></p>
<p>일반적으로 홍정모 교수님의 강의 순서는 다음과 같이 진행된다.</p>
<ol>
<li>이번에 학습할 것 개요</li>
<li>코드와 함께 이번에 해볼 것 상세 설명</li>
<li>영상을 멈추고 직접 풀어보세요(위 스크린샷)</li>
<li>추가설명</li>
</ol>
<p>나는 여기서 1번에 해당하는 내용만 듣고 바로 코드 실습을 진행하는걸 목표로 하였다. 내 기억상 대부분의 챕터에서 이를 성공할 수 있었다. 내가 강의를 들으며 기억하고 싶던 부분들을 정리할 겸, 좋은 강의를 홍보하기 위해 글을 작성하였다.
(강의의 상세한 커리큘럼이 궁금하신 분은 글 하단의 강의링크에서 확인해주세요!) </p>
<h3 id="기초">기초</h3>
<p>c++에서 포인터, 레퍼런스를 사용하는 기본적인 구현에 대한 설명과 o^n 시간복잡도의 정렬 친구들의 성능 측정, 복잡도에 대한 학습을 하는 챕터이다. 머리를 말랑말랑하게 만들기 좋은 챕터였던 것 같다.</p>
<p>이 강의의 장점중 하나가 강의에서 사용한 추가 자료를 정말 잘 정리해주셨다는 점이다. 아래 스크린샷은 강의에 첨부되어 있는 탐색 알고리듬의 성능을 비교한 표이다. 이런식으로 비쥬얼적으로 볼 수 있는걸 자료를 특히 잘 만들어주시지 않나 싶다. 그 외에도 보충적으로 학습할 수 있는 자료들을 정리해서 강의에 올려주시는 편이라, 원한다면 시간을 투자하여 더 깊은 공부를 진행할 수도 있다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/d49a4f2d-d824-4868-9f7a-9571f89d974a/image.png" alt="시복"></p>
<h3 id="스택">스택</h3>
<p>옛날 스택과 재귀를 공부한지 얼마 안되었을 때 하노이 탑 문제에게 좌절하고 무릎을 꿇었던 기억이 있다. 하지만 이번에는 프로그래밍을 해오며 쌓인 짬과, 강의를 들으며 생각하는 힘을 기른 탓인가 이번에는 금방 문제를 해결할 수 있었다.</p>
<p>이전까지의 문제와 달랐던 점은 이번 챕터부터 &#39;디버깅&#39;을 이용했다는 것. 이전까지는 그냥 생각 한번하고 코드를 구현하면 거의 정답이였지만, 하노이의 탑 문제는 그렇게 풀 수 있는 쉬운 문제는 아니였다. 교수님도 디버깅의 중요성을 강조하시고, 강의를 듣는 학습자가 디버깅에 익숙해질 수 있도록 도와주시는 편이라 실습을 진행하며 디버깅에 익숙해질 수 있어서 좋은 것 같다.</p>
<p>이번에도 아래 스크린샷과 같은 비쥬얼적으로 하노이탑을 볼 수 있는 자료와 하노이 알고리듬에 대한 참고 자료를 올려주셨다!</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/e0f909b8-4bdd-438f-bc0c-5000aaa212b4/image.png" alt="하노이 탑"></p>
<h3 id="트리">트리</h3>
<p>요번 강의에서 제일 막혔던 부분이다. 나는 트리에 약한편이구나..</p>
<p>흔하게 사용하는 재귀를 사용하는 이진 트리의 구현은 정말 막힘없이 슈슈슉 진행했었다. 하지만 이번 실습에서는 재귀를 사용하지 않는 이진 트리의 구현도 섞여있었다는 것이다. Preorder, Inorder, Postorder 등 정말 아무렇지 않게 구현하던 트리 순회를 재귀를 사용하지 않고 구현하려니까 뇌가 저리는 느낌이 들었던 것 같다. 한번에 다 풀지 못해서 머리를 식힐 겸 샤워를 갔다오고 다시 문제를 풀기도 하였다.</p>
<p>그래도 끝까지 힌트를 보고 풀지 않겠다는 다짐을 지키고 머리를 혹사시킨 결과, 결국 구현을 성공하였고 트리에 대해 더 깊게 이해할 수 있게 되었던 것 같다. 여기서 얻은 경험은 이후 더 어려운 알고리듬을 학습하는데 큰 도움이 될 것 같다.</p>
<h3 id="이진-탐색-트리">이진 탐색 트리</h3>
<h4 id="균형잡힌-탐색-트리avl">균형잡힌 탐색 트리(AVL)</h4>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/ecfe7c60-d212-4ff6-9fc6-25a66164dbd7/image.png" alt="AVL"></p>
<p>Rotate개념이 재미있었던 파트. 처음에 가시화 도구에서 노드들이 빙빙 돌아가는거 보고 저게 뭐지 하면서 뇌정지가 왔었는데, 노드들이 움직이는걸 자세히 보며 코드를 만져보니까 한순간에 이해가 되서 구현할 수 있었다. 자료구조나 알고리듬을 공부하면서 가장 재밌는 순간이 이럴 때가 아닐까.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/91ed53b4-b1cc-4a29-9cd1-71a5eba2ca6c/image.png" alt="디버깅"></p>
<p>위 스크린샷은 실습 중에 캡쳐한 것인데, 트리를 생성되는 과정을 확인할 수 있도록 각 실습에 디버깅 코드를 준비해 주셨다. 현재 디스코드에서 제작중이신 알고리듬 강의에 사용될 &#39;A* 쿼드트리&#39;도 비쥬얼 디버깅을 할 수 있게 작업중이신 영상을 올려주셨는데 대박이라고 생각이 들었고 이렇게까지 필요한 학습에만 집중할 수 있게 떠먹여주는 강의가 또 있나 싶다..</p>
<h4 id="영어-사전만들기">영어 사전만들기</h4>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/b236ab89-a797-4376-a39f-5ad6bc5e71a3/image.png" alt="영어사전"></p>
<p>모범 답안이나 해설이 없는 미니 프로젝트 만들기 형태의 챕터. 위에서 실습했던 AVL트리가 범용성이 좋아서 걱정했던 것 보다 정말 금방 구현했던 것 같다.</p>
<h3 id="그래프">그래프</h3>
<p>DFS BFS와 같은 탐색 알고리듬은 내가 가장 자신있는 유형이였는데.. 내가 모르던게 있었다. 그것은 바로 스택만 사용해서 탐색을 진행한다고 DFS알고리듬이 아니였다는 사실.. 아래 강의에 첨부된 자료를 보자면 스택기반 탐색과 DFS탐색의 결과 차이를 알 수 있다. 이러한 디테일 차이로 문제를 틀릴 수도 있겠다고 생각하니까 섬뜩했다.</p>
<p><a href="https://11011110.github.io/blog/2013/12/17/stack-based-graph-traversal.html">https://11011110.github.io/blog/2013/12/17/stack-based-graph-traversal.html</a></p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/31467350-8d97-4dd0-a8c1-61b493286cb1/image.png" alt="충격과 공포"></p>
<h3 id="추천">추천!</h3>
<p>그냥 앉아서 멍하니 영상만 보며 듣기만 하는 강의가 아니라, 학습자에게 지속적으로 스스로 생각하도록 유도해주는 강의이다. 스스로 생각하고 문제를 풀며 기본기를 탄탄하게 길러보고 싶은 사람들에게 이 강의를 추천한다. 이 이후에 알고리듬 파트1,2 강의도 진행되고 있는데, 커리큘럼이 탄탄한거 같아서 만족하며 학습중이다.</p>
<p>!youtube[VWtu5gaxBEE?si=ZYYX6rl1HIGuCJNT]</p>
<p>알고리듬 파트1</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/f5f8fd34-216c-478d-aa90-bf6fa22ae46b/image.png" alt="더 빠르게"></p>
<p>위에서 올린 영어 사전 실습을 디스코드 게시판에 올려놨더니 새로운 생각할 거리를 주셨다.</p>
<br>
<br>

<p>강의주소:
<a href="https://honglab.co.kr/courses/data-structures">https://honglab.co.kr/courses/data-structures</a></p>
<p>강의자료:
<a href="https://github.com/HongLabInc/HongLabDataStructures">https://github.com/HongLabInc/HongLabDataStructures</a></p>
<br>

<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/7eb40b2d-1e7d-44a2-a0d7-63c9fc6e89d5/image.png" width="70%"> 
  자료구조를 열심히 공부하여 진짜 남(여)친감이 되어보자!
</center>]]></description>
        </item>
        <item>
            <title><![CDATA[라이브 게임 에셋 관리 개선기 - 1.어드레서블 에셋 도입]]></title>
            <link>https://velog.io/@eugene-doobu/%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EA%B2%8C%EC%9E%84-%EC%97%90%EC%85%8B-%EA%B4%80%EB%A6%AC-%EA%B0%9C%EC%84%A0%EA%B8%B0-1.%EC%96%B4%EB%93%9C%EB%A0%88%EC%84%9C%EB%B8%94-%EC%97%90%EC%85%8B-%EB%8F%84%EC%9E%85</link>
            <guid>https://velog.io/@eugene-doobu/%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EA%B2%8C%EC%9E%84-%EC%97%90%EC%85%8B-%EA%B4%80%EB%A6%AC-%EA%B0%9C%EC%84%A0%EA%B8%B0-1.%EC%96%B4%EB%93%9C%EB%A0%88%EC%84%9C%EB%B8%94-%EC%97%90%EC%85%8B-%EB%8F%84%EC%9E%85</guid>
            <pubDate>Tue, 16 Jul 2024 16:51:16 GMT</pubDate>
            <description><![CDATA[<h1 id="인트로">인트로</h1>
<hr>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/e5dd641b-c37f-416d-ac5f-38666cf72a45/image.png" alt="나나이트"></p>
<p>게임에는 일반적으로 서비스 앱보다 훨씬 많은 리소스가 사용된다. 이는 게임이 사용자에게 지속해서 재미와 몰입감을 제공하기 위해 풍부한 컨텐츠와 동적인 경험이 필요하기. 만약 우리가 게임을 다운받았는데 게임 캐릭터는 다 똑같이 생긴 졸라맨 뿐이고 공격 커맨드가 하나뿐인 액션게임이었다고 생각해보자. <del>(엥 이거 개꿀잼 게임 슈퍼액션히어로아닌가?)</del> 이렇게 만들어도 게임이 재밌을 수는 있으나, 대부분은 대충 만든 디지털 쓰레기 게임이라고 생각하고 게이머들이 관심을 안 줄 것이다.</p>
<p>게임에는 보통 대규모의 이미지와 3D모델들이 필요하며, 일반적인 서비스 애플리케이션보다 리소스가 집약적으로 사용된다. 현대의 컴퓨터와 모바일 기기가 성능이 개선되었음에도, 여전히 프로그램 최적화에 대한 필요성은 존재한다. 특히 모바일 기기에서는 메모리 관리가 중요한 고려사항으로 남아있다. 리소스를 무분별하게 추가하는 경우 기기의 메모리 한계에 빠르게 도달할 수 있다. 이는 간단한 게임의 개발에서는 큰 문제가 되지 않을 수 있지만, 지속해서 컨텐츠를 업데이트하고 확장해야 하는 게임의 경우 메모리 관리는 필수적인 요소가 된다.</p>
<p>게임이 단순한 실시간 메모리 사용량을 넘어서 장기적으로 업데이트되고, 시즌별 이벤트를 진행하면서, 이전 시즌의 이벤트 에셋들이 항상 필요하지 않은 경우가 많다. 이런 이벤트 에셋들은 다음 시즌에 어떻게 활용될지 불확실하므로 클라이언트 프로젝트에서 완전히 삭제하기는 어려운 상황일 수 있다. 이 경우 이벤트 에셋들을 게임 빌드에서 분리하여 필요할 때만 추가 데이터 다운로드를 통해 플레이어에게 제공하는 방법이 효과적일 수 있다. 이러한 데이터 분리 방식은 단지 이벤트 시에만 유용한 것이 아니라, DLC를 통해 게임 콘텐츠를 추가로 판매할 때에도 활용될 수 있다. 특히 모바일 게임에서는 이러한 분리 기능이 에셋 관리의 핵심적인 요소로 자리 잡을 것이다.</p>
<p>이처럼 게임에는 많은 양의 에셋들이 사용되기 때문에 게임개발을 장기간 안정적으로 진행하기 위해서 프로젝트의 에셋 관리는 필수적이라고 할 수 있다. 이 시리즈에서는 실제 라이브 서비스 중인 게임의 리소스 로드 구조를 개선하고, 게임의 사용성을 높이며 더 많은 리소스를 자유롭게 활용할 수 있도록 하는 다양한 에셋 관리 방법과 실 적용기를 다룰 예정이다. 이번 시리즈는 총 4개의 글로 이루어질 것으로 예상하며, 아래 순서대로 개선 방안을 제시하고 적용해보고자 한다.</p>
<p><strong>1부. 어드레서블 에셋 도입</strong> (현재글)
2부. 메모리 사용 구조 개선 / 리소스 최적화
번외1. 리소스 최적화 기법
3부. DLC를 통한 패치 시스템</p>
<p>(이런 내용들이 궁금하다면 미리 팔로우! 눌러주세요~~)</p>
<h1 id="어드레서블-에셋-이란">어드레서블 에셋 이란?</h1>
<hr>
<h2 id="어드레서블이-뭔가요">어드레서블이 뭔가요?</h2>
<p>어드레서블 도입과정을 설명하기 위해서는 유니티의 Resources 폴더와 에셋번들에 대한 이해가 필요한데, 이 글을 읽는 사람이 유니티를 깊게 사용해본 사람이 아니라는 가정하에 간단하게 설명해보고자 한다.</p>
<h3 id="에셋번들">에셋번들</h3>
<p>에셋번들은 게임에서 사용하는 <strong>에셋들을 묶은 그룹</strong>으로써 게임의 최종 빌드파일과 별도로 관리할 수 있는 특징을 가지고 있다. 이처럼 에셋을 묶어서 별도의 파일 개념으로 관리하는 이유는, 프로젝트를 새로 빌드하지 않고도 게임에서 사용하는 에셋의 수정을 편하게 하기 위해서인 경우가 많다.</p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/cf27ba81-8e8f-41f0-98bf-fd02cc588c81/image.png" width="55%"> 
</center>


<p>이렇게 에셋번들을 사용해서 프로젝트를 진행하려면 에셋을 관리해주는 많은 코드가 필요하다. 이 별도의 에셋 파일이 어떻게 묶여서 저장되고, 게임 실행시에는 어디에 있는 에셋을 불러와서 사용할 것이며, 메모리 관리적인 측면에서 게임 시작 시 모든 에셋을 들고 있을 수 없기에 적재적소에 에셋을 로드해야 하는데, 어떤 시점에 메모리에 로드된 에셋을 내리고 새로운 에셋을 로드할지에 대해서 모두 개발자가 코드로 정리를 해줘야 한다. 또한, 아래 설명할 에셋 관리 문제 때문에 에셋을 효율적으로 관리하기 위한 전략을 수립하고, 이에 대한 구현을 진행해줘야 한다.</p>
<h4 id="의존성-관리-문제">의존성 관리 문제</h4>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/2f94ce80-241e-4e17-abdf-06cd6862b6fb/image.png" alt="칼글이"></p>
<p>위 스크린샷처럼 칼을 든 빨간 동글이와 회색이가 각각 별도의 에셋번들로 묶인다고 하자. 각 에셋번들만 로드해도 우리가 빌드했던 오브젝트가 그대로 게임 씬에 생성할 수 있다고 해보자. 이때 에셋번들은 오브젝트를 구성할 서브에셋들을 로드하여 빌드된다. 빨간 동글이로 예시를 들자면, 빨간 동글이 오브젝트에는 (칼, 동글이 리소스)라는 두개의 서브 에셋이 포함된 것이다. 두 오브젝트를 동시에 메모리에 로드했을때 우리의 메모리는 어떻게 될까?</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/dec0c217-5bae-47c9-9e86-6f65dd47ec88/image.png" alt="칼글이 메모리"></p>
<p>빨간색 배경 -&gt; 빨간 동글이를 포함한 에셋번들이 올라간 메모리
회색 배경 -&gt; 회색이를 포함한 에셋번들이 올라간 메모리</p>
<p>위 이미지에서 보이는 것 처럼, 두 오브젝트가 들고있는 칼이 각각 별도의 에셋으로 구분되어 메모리에 중복되어 올라가게 될 것이다. 이런 식으로 에셋번들이 구성된다면, 에셋과 빌드된 게임을 분리하고자 하는 목적은 달성하겠지만, 메모리를 관리하는 측면에서는 큰 손해를 볼 수 있다. 심지어 이 에셋들을 s3와 같은 외부 서버에 저장한다고 했을 때, 같은 에셋들을 여러번 저장소에 올리고 유저가 다운받아야 하는 상황이 벌어진다. 우리는 지금 지구의 건강을 희생해가며 석탄연료를 통해 전기를 생산해서 쓰고 있는데, 이러한 소중한 전기를 불필요한 중복 메모리에 낭비해서는 안 될 것이다.</p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/a8c092ac-1bf5-4294-957f-7ae176da84c4/image.png" width="45%"> 
</center>

<h4 id="에셋-관리-문제">에셋 관리 문제</h4>
<p>프로젝트가 점점 커지다보면 에셋의 수는 구분하기 힘들 정도로 많아지게 된다. 이때 처음부터 에셋들을 올바른 기준으로 정리해놓지 않았다면 나중에는 에셋이 너무 많아 손을 대기 힘든 수준까지 갈 수 있다. 게임 프로젝트 GUI에서 보이는 에셋들도 관리하기 힘든데, 에셋번들은 어떤 파일에 어떤 에셋들이 포함되어 있는지 한눈에 파악하기가 힘들다.</p>
<p>별도의 자동화 코드를 만들어주지 않으면, 많은 에셋번들을 정해진 기준에 따라 관리해주는 것이 사실상 불가능에 가깝다. 프로젝트 바이너리에 기본적으로 포함되는 에셋들은 프로젝트의 적당한 경로에 폴더를 만들어서 처음 가져올 때 직접 한 번만 옮겨주는 것으로 충분하지만, 에셋번들의 목적은 프로젝트의 바이너리에 포함시키지 않고 별도로 빌드해서 관리하고 사용하는 것이고, 빌드된 번들들이 많아지면 그 복잡도가 늘어날 여지가 크기에, 매번 손으로 관리하는 것은 리스크가 큰 결정이다.</p>
<p>잘못하면 에셋들의 헬파티가 열릴 수 있다!!</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/a163e808-bcf7-45a6-b561-a504cc92ffd0/image.png" alt="unhappy"></p>
<h3 id="어드레서블-시스템">어드레서블 시스템</h3>
<p>어드레서블 시스템은 <strong>유니티 게임엔진</strong>에서 지원해주는 에셋 관리 시스템으로, 에셋에 특정 어드레스(에셋을 식별하는 키)를 부여하여 에셋을 로드할 수 있는 기능을 지원해주는 시스템이다. &quot;어떠한 이름으로 특정 에셋을 불러온다&quot;라는 기능자체는 매우 간단하지만, 어드레서블 시스템에는 이 외에도 기존 에셋 관리 시스템을 구성할때 가려웠던 부분들을 긁어주는 기능들을 포함하고 있다.</p>
<ul>
<li>내부적으로는 여전히 에셋번들 단위로 그룹핑하여 사용</li>
<li>&#39;어드레서블 그룹&#39;단위로 에셋번들을 관리, 그룹단위로 세부 설정 가능</li>
<li>에셋 관리 룰을 통한 전체 검사(에셋 의존성 관리도 가능)</li>
<li>원하면 에셋들을 앱 빌드 자체에 포함 가능</li>
</ul>
<h4 id="에셋-그룹-관리-지원">에셋 그룹 관리 지원</h4>
<p>위에서 에셋번들을 사용하면 에셋을 그룹별로 깔끔하게 정리하기 힘들다고 했었다. 어드레서블을 이용하면 유니티 에디터 자체에서 &#39;Addressables Groups&#39; 윈도우를 통해 에셋들을 원하는 그룹으로 묶어서 쉽게 관리할 수가 있게 된다. 그리고 이렇게 관리된 그룹별로 각기 다른 에셋번들을 관리하기 위한 세부 설정을 할 수가 있게 된다. 아래 스크린샷이 그 예시이다.</p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/e37815ec-bf34-4cec-a62c-e876ced07fc8/image.png" width="80%"> 
</center>

<p>좌측의 윈도우는 현재 프로젝트에서 사용하고 있는 어드레서블 그룹의 목록이고, 우측의 &#39;Inspector&#39;윈도우는 현재 선택된 에셋 그룹에 대한 세부 설정을 나타내고 있는 윈도우이다.</p>
<p>설정중 가장 이해하기 쉬운 것은 빨간 동그라미로 표시한 &#39;Build &amp; Load Paths&#39;이며 이 에셋 번들을 빌드후 에셋번들을 저장할 위치와 이 에셋을 로드할 위치를 설정해주는 부분이다. 현재 &#39;Local&#39;로 설정된 있는 옵션을 &#39;Remote&#39;로 바꾸면 원하는 웹 저장소에 에셋을 저장하기 위한 설정을 할 수도 있다. 이를 통해 특정 에셋그룹은 로컬에 빌드하고, 특정 그룹은 원격 저장소에 빌드하는 등의 셋팅도 가능하다.</p>
<p>현재 나인크로니클에서 사용되고 있는 어드레서블 그룹 목록이다. ActorPrefab, CharacterSpine, LocalAssets이라는 3개의 그룹으로 나뉘어져 있다.</p>
<ul>
<li>ActorPrefab: 현재 게임에 사용될 몬스터 오브젝트들을 관리하고 있는 그룹. 이 오브젝트들은 1개의 오브젝트가 1개의 에셋번들로 빌드되어 관리될 것이며, s3 클라우드 저장소에 에셋을 저장해 두었다가 클라이언트 빌드 없이 패치해서 사용할 수 있도록 구성을 할 것이기 때문에 Remote 저장소를 저장경로로 셋팅할 것 이다.</li>
<li>CharacterSpine: 게임 내에 사용될 캐릭터의 장비 파츠들을 관리하는 그룹. ActorPrefab 그룹과 동일한 셋팅.</li>
<li>LocalAssets: 빌드에 포함되어 프로젝트 내부에서 사용할 에셋들을 관리하는 그룹. 모든 에셋이 1개의 에셋번들로 묶여 거의 상시 메모리에 올라가 있는 에셋번들로 생각하고 있으며, 이처럼 프로젝트 내에서 상시 사용할 에셋들을 한개의 그룹으로 묶어서 어드레서블 그룹으로 관리한 이유는 위에서 이야기한 &#39;에셋의 의존성을 관리&#39;하여 중복 리소스 로드를 막기 위함이다. </li>
</ul>
<h4 id="의존성-문제-해결">의존성 문제 해결</h4>
<p>어드레서블은 패키지에서 제공해주는 툴로 동일한 에셋이 여러 에셋번들에 묶여있으면, 중복으로 로드될 수 있는 리소스를 별도의 번들로 묶어 이 리소스를 포함해야 하는 에셋들에게 <strong>자신의 번들을 참조하게 하는 식으로 중복 리소스 로드 문제를 방지</strong>할 수 있다. 하지만 이와 같은 과정을 어드레서블에서 자동으로 진행해주는건 아니고, 별도의 툴을 통해 개발자가 쉽게 수정할 수 있도록 도와주는 방식이다. 이 툴을 통해 에셋의 의존성을 파악하고 중복 리소스가 로드되는 문제를 아래 프로젝트 소개 이후 직접 진행해볼 예정이다.</p>
<h4 id="공통된-인터페이스">공통된 인터페이스</h4>
<p>어드레서블은 에셋을 관리해주는 &#39;공통된 인터페이스&#39;를 제공해준다는 측면에서도 이득이 있다. 만약 팀에서 자체개발한 에셋 관리 툴을 사용하다보면, 다른 팀이나 회사의 에셋 관리 툴은 다른 구조, 다른 용어를 사용하여 구현될 가능성이 높은데, 이러면 새로운 에셋 관리 구조를 파악하는데 시간이 많이 들 수 있다. (그리고 이러한 툴들은 엄청난 하드코딩으로 구현되어 있을 가능성이 높다)</p>
<p>이러한 이유로 기존 에셋번들을 관리하기 위한 복잡도를 어드레서블을 통해 크게 낮출 수 있다. 러닝커브가 있음에도 많은 팀이 어드레서블을 도입하려는 이유가 이런 것이며, 한번만 배워놓으면은 동일한 방식으로 여러 프로젝트의 에셋을 관리하기도 쉬울 것이다.</p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/5b9ea29d-de80-4396-9155-1e43fce86777/image.png" width="30%"> 
</center>

<p>(혹시 안 즐거우시다면 죄송합니다)</p>
<h2 id="패치-시스템">패치 시스템</h2>
<p>위에서 말했듯이 DLC를 통한 패치 시스템이 모바일 프로젝트의 에셋 관리에서 가장 핵심적인 부분이 될 가능성이 높다. 이미 패치 시스템이 도입된 다른 게임들을 예시로 그 이유를 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/16e88043-d629-48ed-b874-806831075476/image.png" alt="프커업뎃"></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">"게임 프린세스 커넥트! Re:Dive 추가 다운로드 팝업"</figcaption>

<p>위 게임은 내가 몰래 숨어서 플레이하고 있는 미소녀 수집형 게임인 &#39;프리코네&#39;의 업데이트 팝업이다. 2024년 4월 18일 기준, iOS 앱스토어에 업로드 된 이 게임의 기본 크기는 <strong>290.1MB</strong> 이정도는 &#39;일반적인 서비스 어플&#39;들보다 약간 큰 정도이다. 하지만 게임을 실제로 플레이하기 위해서는 추가적으로 10.73GB의 데이터를 다운로드 받아야 한다.</p>
<p>이처럼 대부분의 모바일 게임들은 실제 앱스토어에는 앱을 구동시키기 위한 코드와 최소한의 에셋들만 포함되어있는 빌드파일을 올려놓고, 대부분의 핵심 에셋들은 별도로 다운받는 구조로 제작되어 있다. 위에 예시로 든 프리코네의 경우, 신규 캐릭터 획득시마다 그 캐릭터와 관련된 리소스나 게임 스토리를 보기 위한 리소스도 필요시마다 추가로 다운로드 하고 있어 실제로 게임 플레이를 하면서 다운받는 리소스의 양은 훨씬 많아질 수 있다.</p>
<p>이렇게 외부에서 리소스를 다운 받는 이유는 다음과 같다.</p>
<ol>
<li>앱 스토어에서 보이는 앱 용량이 진입장벽으로 느껴질 수 있다.</li>
<li>안드로이드는 apk로 앱을 업로드 시 150MB 이하로 유지해야 하는 전통이 있었다.</li>
<li>그리고 패치 시스템을 이용하면 <strong>번거로운 앱 심사 과정을 회피하고 업데이트</strong>를 할 수 있다</li>
</ol>
<p>사실 개인적인 입장에서는 3번이 핵심이라고 생각한다. 게임개발을 하다 보면 추가로 빌드 과정을 진행하는 것도 엄청난 일이며 이를 앱스토어에 심사받고 정해진 기간까지 심사가 통과할지 기도하는 일도 회사 팀원들의 마음을 힘들게 한다. 이 과정은 많은 시간과 정성이 필요하며, 때로는 예상치 못한 심사 지연이나 거절 사유를 해결하기 위해 추가적인 노력(과 야근)이 필요할 수 있다.</p>
<h3 id="앱-패치-심사">앱 패치 심사</h3>
<pre><code>애플의 앱스토어 심사는 구글 마켓에 비해
까다롭고 오래 걸리는 것으로 유명합니다.
몇 시간 정도 걸리는 안드로이드 심사와 달리

애플 앱 심사는 약 일주일 정도 걸리는데요.
일주일을 기다린 앱 심사 결과가
거부(reject)면 재심사까지 더해 약 2주일,
한번 더 reject되면 3주까지 늘어나기도 하죠.

그렇기에 앱스토어 심사를 가장 빨리 통과하는
방법은 거부(reject)당하지 않는 거죠.
- 디스이즈게임, [카드뉴스] 거부를 거부한다! 앱스토어 심사에서 리젝당하지 않는 법</code></pre><p><a href="https://www.thisisgame.com/webzine/nboard/257/?n=61254?n">https://www.thisisgame.com/webzine/nboard/257/?n=61254?n</a></p>
<p>모바일게임을 만들고 앱 마켓에 등록하기 위해서는 필수적으로 심사 과정을 거쳐야 한다. 앱 심사는 대부분 첫 심사가 매우 까다롭고 이후 심사는 상대적으로 너그럽게 진행되는 편인 것 같다. 하지만 첫 심사가 아니라고 방심하면은 안 된다. 앱 심사 거절은 언제 어디서 일어날지 모른다.</p>
<p>만약 코드 수정이 아닌 단순 리소스 수정이라면, 위와 같은 패치 시스템을 이용해서 앱 심사를 우회하고 바로 유저들에게 콘텐츠 업데이트를 할 수 있다. 한번 DLC 업데이트에 익숙해지고 나면,  단순 리소스 수정 같은 일로 하루 이상의 딜레이를 가질 수 있는 앱 심사 과정을 거쳐야 하는 것이 매우 번거로운 일로 느껴질 수 있을 것이다.</p>
<p>나인크로니클에도 이러한 패치 시스템이 도입될 것으로 예상하며, 이러한 작업은 후에 진행한 후 따로 글을 작성해 볼 예정이다. 이에 대해서 당장 시도해본 내용은 아래 프로젝트 소개 후 녹화해둔 영상과 함께 이야기해보도록 하겠다.</p>
<h3 id="ios-리소스-다운로드">iOS 리소스 다운로드</h3>
<p>iOS의 경우 이러한 리소스 다운로드 정책에 대해 민감하게 심사를 진행하는 편이었다. 나는 과거 iOS에서 어플을 개발하여 업로드하고, 첫 심사를 통과하고 앱 관리를 해오다가 추후 외부 게임 리소스들을 어셋번들로 분리하고 다운로드해서 사용하는 DLC 구조를 구현한 적이 있었다. 그 이후 테스트를 마치고 앱 업데이트 요청을 진행했었는데, 이때 약 일주일 동안 고통스러운 리젝대응기간을 보냈던 기억이 있다. iOS 심사 지침을 확인해보자.</p>
<hr>
<p><strong>4.2 최소 기능</strong></p>
<ul>
<li><strong>4.2.3</strong><ul>
<li><strong>(i)</strong> 앱은 다른 앱을 설치할 필요 없이 단독으로 작동할 수 있어야 합니다.</li>
<li><strong>(ii)</strong> 초기 실행 시 작동하기 위해 추가 리소스를 다운로드해야 하는 앱은 다운로드하기 전에 리소스 크기를 공개하고 사용자에게 승인을 요청해야 합니다.</li>
</ul>
</li>
</ul>
<h3 id="변경된-심사-지침220606-문서">변경된 심사 지침(22.06.06 문서)</h3>
<ul>
<li>4.2.3: 출시 초기에도 앱이 충분히 기능할 수 있도록 바이너리에 풍부한 콘텐츠가 있어야 한다는 요구 사항을 삭제했습니다.</li>
</ul>
<hr>
<p>iOS에서 외부 리소스 다운로드와 관련된 조항은 바로 &#39;<strong>4.2 최소기능</strong>&#39; 파트일 것이다. 먼저 맨 처음에는 사용자가 다운받는 리소스 크기를 명시하지 않고 리소스를 다운로드시켜서 리젝을 받았었다. 이에 대한 부분은 콘텐츠 다운로드를 위한 ok/cancel 팝업을 추가하고, 다운받을 리소스 용량을 표기하였다. 해결했다고 생각하고 다시 앱 심사를 진행했었다. 이후 저 삭제된 &#39;4.2.3&#39; 조항 때문에 엄청난 삽질을 거듭하다 통과했지만, 여기서 라떼이야기는 생략하도록 하겠다.</p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/897cbef3-cbd8-4a26-a7a2-7ac28586c76a/image.png" width="20%"> 
</center>


<h2 id="나인크로니클9c">나인크로니클(9C)</h2>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/06720c11-7d52-4fbd-b921-82560bedff34/image.png" alt="9c"></p>
<p>요번 글에서 에셋 로드 구조를 개선할 게임은 오픈소스로 개발하고 서비스되고있는 풀 블록체인 기반의 2D RPG 게임인 <a href="https://github.com/planetarium/NineChronicles">나인크로니클</a>이다. 풀 블록체인 기반 게임이라는 것이 핵심적이고 재미있는 특징이지만, 이번 시리즈에서는 에셋 로드와 관련된 클라이언트 영역에 대해서만 다룰 예정이다. 이 프로젝트는 오픈소스로써 실제로 작업한 내용들을 직접 확인해볼 수 있다. 이번 시리즈에서도 작업 PR의 링크를 달거나 코드 일부분을 글에 첨부해볼 예정이다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/14065f1b-139a-4627-8bff-b0a99c3bf7c2/image.png" alt="전투"></p>
<p> 나인크로니클은 기본적으로 스텟을 기반으로 한 전투 시뮬레이션 게임이다. 체인 위에 기록된 나의 캐릭터의 스텟과 스킬을 기반으로, 테이블 데이터에 기록된 스테이지의 몬스터와 랜덤을 기반으로 한 전투가 시뮬레이션 되며, 그 결과가 클라이언트에게 전송되어 전투 과정을 렌더링하여 위 스크린샷처럼 전투를 진행하게 된다.</p>
<h3 id="어드레서블-패키지-적용">어드레서블 패키지 적용</h3>
<p>먼저 사용되는 리소스 중 다른 기능들과 의존성이 적은 부분을 찾아서 먼저 적용해보고자 했다. 그중 많은 리소스를 사용하면서도 가장 독립적인 부분이 바로 &#39;스테이지의 몬스터 리소스&#39;라고 판단되어 해당 부분을 우선하여 어드레서블을 이용하여 로드하도록 분리하고, 리모트 다운로드 테스트까지 진행할 계획을 세웠다. 먼저 어드레서블 리소스를 관리하기 위한 리소스 매니저를 프로젝트에 추가하였다.</p>
<h4 id="리소스-매니저">리소스 매니저</h4>
<p>개인적인 경험상 에셋을 여러 스크립트에서 로드하고 릴리즈하는 과정을 진행하다 보면, 꼭 리소스가 제대로 관리되지 못하고 릴리즈되지 못하는 에셋들이 생기게 되는 것 같았다. 어드레서블에서 에셋은 레퍼런스 카운트를 기반으로 관리되며(c++의 스마트포인터를 생각하면 쉬울 것), 이 에셋의 핸들을 관리해주는게 가장 기본적인 요소 중 하나이다.
(레퍼런스카운트 관리 및 어드레서블의 자세한 내용은 나중에 별도의 글로 다뤄볼까 한다)</p>
<p>나는 이러한 에셋 핸들과 레퍼런스 카운트 관리를 위하여 <strong>ResourceManager</strong>를 구현해서 사용하는 것을 선호한다. 프로젝트마다 디테일한 에셋 로드/언로드 타이밍은 다르겠지만, 프로젝트 전역적으로 사용되는 소수의 에셋을 제외하고는 대부분의 에셋은 씬 전환 시 날려버려도 상관 없다고 생각하기에 씬이 바뀔 때마다 <strong>ResourceManager</strong>에 캐시된 모든 에셋들을 날려주는 식으로 구현했다.</p>
<p><a href="https://github.com/planetarium/NineChronicles/pull/4725">https://github.com/planetarium/NineChronicles/pull/4725</a></p>
<h4 id="battle-renderer">Battle Renderer</h4>
<p>구현된 리소스 매니저를 기반으로 에셋을 로드하는 시스템을 적용하였다. 자세한 작업 내용은 아래 PR의 Desription에 적어두었다. 나인크로니클의 전투가 턴 기반이라는 것을 생각해보면, 처음 전투를 시작할 때 현재 플레이어의 스텟과 도전하고자 하는 스테이지의 데이터를 기반으로 미리 전투를 시뮬레이션할 수 있으며, 이때 시뮬레이션 된 배틀을 렌더링할 때 필요한 리소스들을 긁어와 미리 로드해둘 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/4d4cc089-4e69-49bd-8ac3-d4f5583d973a/image.png" alt="배틀로그"></p>
<p>위는 배틀 시작 시 클라이언트에서 받는 시뮬레이션 결과이며, SpawnWave이벤트 안에는 어떠한 몬스터가 스폰되는지에 대한 정보가 모두 담겨있다. 이를 통해 로딩 타이밍에 스폰될 몬스터 리소스들을 미리 로드하였다가 필요한 타이밍에 스폰하면, 클라이언트는 필요한 메모리만 올려서 사용하며 몬스터 생성 시 리소스를 메모리에 올리기 위한 부하(프레임드랍) 없이 게임을 진행할 수 있게 된다. 이러한 작업의 핵심 로직은 아래 스크립트에서 확인할 수 있다.</p>
<p>// in <strong>BattleRenderer.cs</strong></p>
<pre><code class="language-cs">public IEnumerator LoadStageResources(BattleLog battleLog)
{
    ReleaseMonsterResources();
    yield return LoadMonsterResources(battleLog.GetMonsterIds());
    _onStageStart?.Invoke(battleLog);
}

// TODO: 필요한 것만 로드
private IEnumerator LoadMonsterResources(HashSet&lt;int&gt; monsterIds)
{
    var resourceManager = ResourceManager.Instance;
    foreach (var monsterId in monsterIds)
    {
        yield return resourceManager.LoadAsync&lt;GameObject&gt;(monsterId.ToString()).ToCoroutine();
        loadedMonsterIds.Add(monsterId);
    }
}

public void ReleaseMonsterResources()
{
    var resourceManager = ResourceManager.Instance;
    foreach (var loadedMonsterId in loadedMonsterIds)
    {
        resourceManager.Release(loadedMonsterId.ToString());
    }
    loadedMonsterIds.Clear();
}</code></pre>
<ol>
<li>먼저 스테이지 시작 시 전투 시뮬레이션 결과를 불러와 렌더링에 필요한 모든 에셋목록을 긁어온다</li>
<li>로드할 에셋들을 차례대로 비동기로 로드하며, 로딩이 끝날 때까지 로딩 UI를 출력한다</li>
<li>모든 리소스 로드가 끝나면 OnStageStart 이벤트를 실행해 구독한 객체들에 메시지를 전송한다.</li>
<li>스테이지가 끝나면 ReleaseMonsterResources 메서드로 모든 리소스를 해제해준다.</li>
</ol>
<p><a href="https://github.com/planetarium/NineChronicles/pull/4736">https://github.com/planetarium/NineChronicles/pull/4736</a></p>
<h3 id="어드레서블-패키지를-적용하면-모든게-다-해결될까">어드레서블 패키지를 적용하면 모든게 다 해결될까?</h3>
<p>그건 아니다. 패키지를 적용하더라도 에셋을 직접적으로 관리해주는 코드가 필요하다. 아까 에셋번들을 이야기하면서 결국 어떤 에셋들을 묶어서 사용할 것인지, 에셋을 언제 메모리에 올릴지 등에 대해 많은 관리 코드가 필요하다고 했는데 이건 어드레서블을 사용해도 근본적으로 해결되지 않는 영역이다. 어드레서블을 사용해도 결국 이러한 관리 코드가 필요하기 때문에, 굳이 러닝 커브가 있는 어드레서블을 사용하지 않고 순수 에셋번들만을 이용해서 프로젝트를 진행하는 때도 있다.</p>
<p>하지만 앞으로 오랜 시간 게임개발자로 밥 벌어먹고 살고자 한다면 에셋 관리 시스템은 어드레서블로 해보는 것을 추천한다. 실제로 많은 팀에서 사용하고 있고, 사용하고 있지 않다면 도입을 고려하고 있는 팀들이 많아서 면접에서 단골로 물어보는 주제이기도 하다. 소형팀이든, 대형팀이든 이런 식으로 에셋 관리하는 구조를 구축해볼 경험이 있다면 좋은 가산점이 될 것이다.(그리고 이러한 주제가 일반적인 게임개발 취준생들이 쉽게 고려해보지 못하는 주제이기도 해서 좋은 점수를 받기 좋은 포인트라 생각한다.)</p>
<h3 id="중복-사용-에셋-감지">중복 사용 에셋 감지</h3>
<p>이는 어드레서블의 Analyze Rules중 <strong>&quot;Check Resources to Addressable Duplicate Dependencies&quot;</strong> 기능을 통해 확인할 수 있다. 이 기능은 Unfixable Rules 중 하나로, 이는 어드레서블에서 툴을 통한 검사는 해주겠지만, 자동으로 고칠 수는 없어서 개선을 원한다면 개발자가 수동으로 작업해야 하는 분석 규칙임을 뜻한다. 현재 작업 중인 프로젝트에 이 규칙을 검사해보면..</p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/d51f11b1-1517-4380-8c84-fd754c3f10e9/image.png" width="85%"> 
</center>

<p>짜잔.. 위에서 말한 어드레서블에서 중복으로 로드될 수 있는 에셋들을 감지하는 툴을 통해 에셋을 검사해 본 결과이다. 현재 905개의 에셋에서 중복으로 로드될 수 있는 서브에셋들의 목록들이 나왔고, 우측에 표시된 숫자들은 이 에셋에서 중복 사용 중인 서브 에셋들의 개수를 나타낸다. 현재 보이는 스크린샷에서 한 에셋당 4개 정도의 서브에셋들이 중복으로 사용되고 있으니 대략 계산해서 3,600개의 서브 에셋들이 중복으로 로드될 수 있는 상황이라는 것이다.</p>
<p> <img src="https://velog.velcdn.com/images/eugene-doobu/post/5f8cd57f-ee28-48be-a606-79fdbd2a2819/image.png" alt="망곰울먹"></p>
<p>이는 안타깝지만, 처음부터 어드레서블 에셋을 도입하여 사용하지 않아 생긴 부작용이라고도 볼 수 있다. 기존 레거시한 방식으로 다량의 에셋들이 관리되고 있었기에, 어드레서블로 관리되는 에셋이 일부분 생김에 따라 기존 어드레서블에서 관리되던 에셋과 연관성이 있던 서브 에셋들이 이처럼 중복 로드될 가능성이 있는 에셋으로 감지되는 것이다. 실제로 현재 감지된 duplicate dependencies의 파일 목록을 자세히 보면 경로가 ＇Assets/Resources..＇로 시작하는 것을 볼 수 있다. 이 경로에 있는 에셋들은 모두 레거시한 방법으로 빌드시 무조건 포함되는 에셋들에 해당한다.</p>
<p> 실제 라이브 서비스 중인 프로젝트이기도 하고, 관련해서 피쳐 작업들이 계속 진행되고 있기에 이러한 에셋들을 한번에 어드레서블로 관리되도록 수정하는 것은 거의 불가능에 가깝다고 생각한다. 따라서 단기적으로는 메모리에 중복으로 로드되는 에셋들이 많더라도 감수하고 개선 작업을 천천히 진행해야 할 것이다. 이것이 가능하면 어드레서블 에셋을 초기부터 도입해야 하는 이유 중 하나라고 볼 수 있다. 유니티 메뉴얼에도 아래와 같은 내용이 적혀있다.</p>
<p> <img src="https://velog.velcdn.com/images/eugene-doobu/post/e63c0556-2d00-481e-8744-96869d6e8f62/image.png" alt="유니티 메뉴얼"></p>
<p>위 메뉴얼 링크: <a href="https://docs.unity3d.com/kr/Packages/com.unity.addressables@1.21/manual/AddressableAssetsMigrationGuide.html">https://docs.unity3d.com/kr/Packages/com.unity.addressables@1.21/manual/AddressableAssetsMigrationGuide.html</a></p>
<h3 id="에셋-패치를-하려면">에셋 패치를 하려면?</h3>
<p>에셋을 패치 하려면 유저들이 앱 실행 중 항상 접근할 수 있는 웹 서버에 사용할 에셋들을 업로드해야 한다. 나는 회사에서 적극 사용 중인 AWS에서 제공해주는 S3 버킷에 에셋을 업로드하고 게임에서 이를 로드해서 사용해보는 테스트를 하였다.</p>
<p>!youtube[6qXSP9vJ06s?si=RusxbkGKXnIVDUKv]</p>
<p>(위 영상에서 &#39;바로 로드 안됨&#39;이라고 나오는 부분의 문제는 해결했는데, 어이없게도 에셋 로드를 하고 로드가 완료되는 걸 기다리지 않고 오브젝트를 생성하고 있었다.)</p>
<p>위 어드레서블의 장점에서 &#39;Remote&#39;로 에셋을 저장해주는 세팅을 하면 에셋이 원격 저장소에 저장된다고 말했다. 위 영상이 바로 <strong>ActorPrefab</strong> 그룹을 Remote에 저장되도록 세팅하고 그 에셋들을 s 3원격 저장소에 올려서 사용하는 영상이다. 영상을 보면 알 수 있듯이, 어드레서블에서 제공해주는 기능만을 이용하면 프로젝트의 외딴곳에 에셋이 빌드되고 이를 수동으로 손으로 옮겨서 S3 버킷에 업로드해주는걸 볼 수 있을 것이다.</p>
<p>또한 현재 클라이언트에 있는 에셋이 최신 에셋인지 파악하기 위해 해시 값들을 관리해야 하고, 이러한 해시 값을 관리해주는 카탈로그 개념에 대해 인지하고 있어야 한다. 이에 대한 경험과 자세한 설명들은 추후 DLC를 통한 패치 시스템에 대한 글을 쓸 때 다뤄보도록 하겠다.</p>
<h3 id="에셋-로드-시스템을-바꾸는-겸-시스템-개편">에셋 로드 시스템을 바꾸는 겸 시스템 개편</h3>
<p>또한, 에셋 로드 시스템을 어드레서블 패키지로 바꾸는 겸 기존의 에셋 로드 시스템들을 개선하고 있다. 이는 주로 메모리 관리와 관련된 부분으로, 이어서 바로 작성할 2번째 글인 <strong>&#39;메모리 사용 구조 개편&#39;</strong> 에서 자세히 다뤄보도록 하겠다. 해당 글에서는 mac의 Xcode를 통해 앱을 프로파일링 하며 실제 메모리 사용량과 앱 용량까지 절감시키고 있는 이야기를 살펴볼 수 있을 것이다.</p>
<h1 id="끝">끝</h1>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/6b5d1129-ee9d-4a2c-b206-de8783f5d974/image.png" alt="끝"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리듬 어따 써요? 1편 - 길찾기 알고리듬(길찾기 알고리즘)]]></title>
            <link>https://velog.io/@eugene-doobu/%EC%95%8C%EA%B3%A0%EB%A6%AC%EB%93%AC-%EC%96%B4%EB%94%B0-%EC%8D%A8%EC%9A%94-1%ED%8E%B8-%EA%B8%B8%EC%B0%BE%EA%B8%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EB%93%AC%EA%B8%B8%EC%B0%BE%EA%B8%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@eugene-doobu/%EC%95%8C%EA%B3%A0%EB%A6%AC%EB%93%AC-%EC%96%B4%EB%94%B0-%EC%8D%A8%EC%9A%94-1%ED%8E%B8-%EA%B8%B8%EC%B0%BE%EA%B8%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EB%93%AC%EA%B8%B8%EC%B0%BE%EA%B8%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Sun, 25 Feb 2024 15:25:38 GMT</pubDate>
            <description><![CDATA[<p> 이 글은 실제 프로젝트에서 사용되는 알고리듬에 대해 간단한 소개를 하기 위한 글로, 해당 알고리듬들에 대한 자세한 설명이 포함되어 있지 않습니다.</p>
<p> 해당 글에서 사용한 코드들은 이 <a href="https://github.com/eugene-doobu/pathfinding-example">저장소</a>에 있습니다.</p>
<p>(알고리즘 -&gt; 알고리듬 캠페인을 실천중입니다.)</p>
<h2 id="길찾기란-무엇인가">길찾기란 무엇인가</h2>
<p>게임에서 캐릭터가 움직이는건 어떻게 구현할까? 유저가 특정 키를 누르면 캐릭터가 해당 방향으로 갈 수 있는지 판단하고 캐릭터의 위치를 조금 이동시킨다. 근데 이건 &#39;나의 캐릭터&#39;가 움직일때의 이야기이다. </p>
<p>그럼 플레이어가 조종하지 않는, 예를 들어 몬스터와 같은 오브젝트의 이동은 어떻게 처리할까? 게임회사 직원들이 몬스터를 하나하나 클릭해서 wasd키를 눌러 이동시켜주는건 아니다. 게임회사 직원들의 워라밸을 위해 이걸 자동화해줄 시스템이 필요하다. 이를 위해 필요한 것이 길찾기 알고리듬이다.</p>
<center>
    <img src ="https://velog.velcdn.com/images/eugene-doobu/post/d37d4ada-74f8-4d8b-9316-a5f7236eaaab/image.png" width="70%"> 
</center>


<p>몬스터의 케이스는 아니더라도 길찾기 알고리듬은 게이머에게도 매우 유용하다. 퀘스트를 위해 걸어서 10분정도 걸리는 거리를 계속 키를 누르며 이동해야 한다고 하면 유저들은 매우 피곤해할 것이다. 그냥 적당히 목적지를 정해 길찾기를 시켜두고 커피한잔 사온 다음 느긋하게 월드를 구경하는 여유로운 시간을 갖게 해주기 위해서는 길찾기 알고리듬의 구현이 필요하다.</p>
<p>이번 글에서는 가장 간단한 형태의 길찾기 알고리듬에서 시작하여 실제 프로젝트에 많이 적용해서 사용하는 A* 길찾기까지 발전시키며 왜 이 알고리듬이 실제 프로젝트에 쓰이고 있는지 알아가볼 것이다. 이 글에 나오는 알고리듬의 구현은 Unity 게임엔진에서 C#으로 작성되었다.</p>
<h2 id="우수법">우수법</h2>
<p>미로 찾기 알고리듬 가장 간단한 알고리듬인 우수법에 대해 알아보자. 우수법은 오른손을 벽에 짚고 따라가다보면 결국 언젠가는 길을 찾게된다는 점에서 구현된 알고리듬이다. 이를 알고리듬으로 구현한 후 실행해보면 다음과 같은 결과가 나온다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/6f9809f8-d857-4243-aab5-cc727642aebb/image.gif" alt="righthandmaze"></p>
<p>(위 움짤은 <a href="https://www.inflearn.com/course/%EC%9C%A0%EB%8B%88%ED%8B%B0-mmorpg-%EA%B0%9C%EB%B0%9C-part2">C# MMO시리즈</a> 강의에 나온 코드를 실행한 것입니다)</p>
<p>보기에는 굉장히 신기하지만, 실제 게임 캐릭터가 이런방식으로 움직인다고 생각해보자. 딱봐도 아무것도 없을것같은 우측 통로에 캐릭터가 머리를 박고 다시 왔던 길을 되돌아오는 모습을 본다면 속이 터져 길찾기 기능을 봉인해버리고 말것이다.</p>
<p>플레이어의 캐릭터와 몬스터들이 바보로 불리는 일이 일어나기 전에 똑똑해보이는 길찾기 알고리듬을 구현을 해보자. 우리가 실제 플레이하는 게임은 모두 위 예시처럼 네모네모한 미로안에 구성되어 있지 않다. 우리가 개발하고 있는 게임의 길찾기 기능을 구현한다고 생각하고, 길을 찾기 위해서 길이란 무엇인지 정의해보도록 하자.</p>
<h2 id="길을-정의하자">&#39;길&#39;을 정의하자</h2>
<p>길찾기 알고리듬을 실행시키기 위해서는 &#39;길&#39;이 무엇인지에 대한 정의가 필요하다. 우리가 게임에서 캐릭터를 조작할 수 있는 맵의 유형에 어떤 것들이 있는지 생각해보자</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/f31eaf7d-7da2-40f3-b4af-f929a5fbafc0/image.png" alt="메"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/dae6c291-ea7a-4c7f-9cbb-940f66fd3966/image.png" alt="팩맨"></th>
<th><img src="https://velog.velcdn.com/images/eugene-doobu/post/8df5c451-d053-4223-bb59-115fbc7cd8dc/image.png" alt="와우"></th>
</tr>
</thead>
</table>
<p>첫번째로, 메이플 스토리의 월드맵처럼 특정 구역과 구역들이 연결되어있는 그래프에서의 경로를 생각해볼 수 있다. 두번째로는 팩맨처럼 바둑판과같은 직사각형의 행렬위에서 오브젝트들을 이동시키는 형태를 생각해볼 수 있다. 이 케이스의 경우에도 한개의 요소가 상하좌우의 요소와 연결되어 있는 그래프로도 구현할 수 있지만, 2차원 그리드로 갈 수 있는 길, 갈 수 없는 길, 오브젝트들의 위치를 표현하는것이 더 직관적일 수 있다. 세번째로는 월드 오브 워크래프트와 같은 3D 지형에서의 길이다.</p>
<p>사실 이 세가지 예시 모두 본질적으로 같은 방법으로 구현할 수 있다. 하지만 맵의 형태에 따라 효율적인 방법은 존재한다. 단순히 그래프 알고리듬을 구현하는 경우에도, 그래프의 특징과 표현방식을 어떻게 하느냐에 따라 성능이 달라지기도 한다. 따라서 지금 프로젝트의 맵이 어떻게 기획되었는지를 먼저 생각해보고 기획 구조에 맞는 맵의 표현 방식을 정하고, 그 위에서 길찾기 알고리듬을 구현하고 발전시켜나가야 한다.</p>
<p>이 글에서는 길찾기 알고리듬을 설명하기 위해 가장 구현하기 편한 형태인 2차원 그리드형태의 맵 위에서 길찾기 과정을 시각적으로 표현하는 예제를 구현해볼 것이다.</p>
<p>글쓴이는 길찾기 예제를 구현하기 위해 <a href="https://unity.com/kr">Unity</a>게임엔진을 사용하였으며, 맵 구현에는 <a href="https://catlikecoding.com/unity/tutorials/hex-map/">CatLikeCoding의 튜토리얼</a>을 참고하였다. 그리고 길찾기에 사용되는 캐릭터의 리소스는 <a href="https://github.com/planetarium/NineChronicles">NineChronicles</a>의 스파인 에셋을 사용하였다. 그렇게 준비된 맵의 모양은 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/2c5739d8-b676-4ff0-8e00-57ec6484fb37/image.png" alt="MAP"></p>
<p>네모네모한 2차원 미로 형태로 맵울 구성 하려다가 그건 심심해보여서 벌집모양의 형태로 조금 변형을 했다. 맵을 이렇게 변경해도 본질적인 길찾기 알고리듬자체는 바뀌지 않는다는것을 보여주고 싶기도 했다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/a761f538-fb92-4898-bedf-455f7a69d310/image.png" alt="Map Color"></p>
<p>맵에서는 영역의 구분을 위해 총 5가지 컬러를 사용하고 있다. 왼쪽부터 <span style="color:#73E097">컬러0</span>, <span style="color:#49CFBF">컬러1</span>, <span style="color:#4A96CF">컬러2</span>, <span style="color:#4A4ECF">컬러3</span> 그리고 색칠되지 않은 기본 컬러값인 흰색으로 구성되어있다. 길찾기 알고리즘 구현마다 노드의 상태를 표현하기 위해 이 컬러들을 이용할 것이다.</p>
<h2 id="bfsbreadth-first-search">BFS(Breadth-first search)</h2>
<p>코딩테스트를 위해 알고리듬을 공부하는 사람들은 한번 쯤 들어봤을 알고리듬이다. BFS는 &#39;너비 우선 탐색&#39;이라고도 불린다. 이는 <strong>하나의 노드를 방문한 후, 그 노드와 인접한 모든 노드들을 우선적으로 방문하는 방법</strong>이다. 주로 queue를 이용하여 구현하며, 현재 방문한 노드에서 다음에 방문할 수 있는 모든 노드들을 queue에 등록함으로 현재 노드에서 인접한 노드들을 우선적으로 탐색하게 되는 것이다.</p>
<p>여기까지가 일반적으로 알고 있는 &#39;탐색 알고리듬&#39;으로써의 BFS일 것이고, 여기의 조금의 변형만 가해주면 &#39;항상 최단거리를 찾아주는&#39; 길찾기 알고리듬으로 만들어줄 수 있다. 바로 <strong>노드 방문시 자신을 queue에 넣었던 노드의 정보를 기록해줌으로써 특정 노드부터 시작노드까지의 경로를 구할 수 있다.</strong> 이 때, 시작 노드는 자기 자신을 queue에 넣은 노드로 정보를 구성하여 다른 노드들과 구분할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/3a82b0c4-31ed-4c8e-afd4-4aa80d7aefb8/image.gif" alt="bfsmaze"></p>
<p>(위 움짤은 <a href="https://www.inflearn.com/course/%EC%9C%A0%EB%8B%88%ED%8B%B0-mmorpg-%EA%B0%9C%EB%B0%9C-part2">C# MMO시리즈</a> 강의에 나온 코드를 실행한 것입니다)</p>
<p>이를 기반으로 아까 우수법에서 탈출했던 미로를 BFS기반 길찾기 알고리듬을 통해 탈출하면 위 움짤과 같은 결과가 나온다. 이렇게 미로를 탈출하는 결과를 보면 정말 군더더기없이 깔끔하게 미로를 탈출하는 것 처럼 보인다. 실제로 BFS알고리듬를 통해 얻은 길찾기 경로는 최단경로임이 보장된다.</p>
<br>

<p>이를 기반으로 우리가 설계했던 맵에서 길찾기를 구현해보자. 아직 탐색하지 않은 경로는 흰색, 이미 지나온 길들은 <span style="color:#73E097">Color0</span>, 도착지점은 <span style="color:#49CFBF">Color1</span>, 도달할 수 없는 경로는 <span style="color:#FF0000">Red</span>색상으로 채워보도록 하겠다</p>
<p>!youtube[qwibqp_-lZo?si=558InPY4B6NT10Fa]</p>
<p><a href="https://github.com/eugene-doobu/pathfinding-example/blob/main/Assets/Project/Scripts/PathFinding/BFSPathFinding.cs">코드참고</a>
<br></p>
<p>BFS기반으로 길찾기를 구현해본 영상이다. 동글이가 통통 뛰어가며 경로를 향해 뛰어가고 이동 경로가 색칠되는것을 확인해볼 수 있다. 마지막 35초쯤 동글이를 벽으로 가둔 후 벽 넘어 길찾기를 시도했을 때 경로 탐색에 실패해 클릭했던 셀이 빨간색으로 변하는것을 확인할 수 있다. 이후 벽을 다른 색상으로 변경 후 다시 해당 지점으로 길찾기를 했을 경우 정상적으로 길을 찾아 갔다.</p>
<p>이렇게 해서 BFS기반으로 길찾기 알고리듬이 구현되었지만, 실제 게임 프로젝트에서 BFS를 길찾기를 위해 사용하는 경우는 많이 없을것이다. 게임에서의 길은 모든 경로가 평등하게 구성된다고 보장되지 않는다. BFS방법에서는 현재 내가 갈 수 있는 길들을 모두 평등하게 큐에 넣고, 순서대로 접근하기 때문에 &#39;가기 어려운 길, 가기 쉬운 길&#39; 이라는 개념을 적용하기가 힘들다. </p>
<p>실제 게임에서는 지형에 물이 있을 수도 있고, 경사가 있는 길이 있을 수 있다. 전략적 게임에서 이러한 지형적 특성에따라 이동에 필요한 자원이 달라지는 기능은 쉽게 찾아볼 수 있다. 그리고 캐릭터 특성에 따라 누구는 날 수 있고 수영할 수 있고 하는등 이동에 대한 특징들이 다르기도 하다. 만약 &#39;평지에서는 이동당 1, 물로 이동할때는 이동당 2의 스태미너를 소모하게 해주세요&#39;하는 기획을 만난다면 어떻게 할 것인가?</p>
<ol>
<li>그런 기획은 안된다고 말한다.</li>
<li>경로에 가중치를 두어서 연산하는 알고리듬을 사용한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/beb910b9-90ed-4f4d-b77d-12cecc7b4ac1/image.png" alt="안돼"></p>
<p>프로그래머의 구현이 힘들어 기획적인 제한이 생긴다는건 매우 슬픈 일이다. 이러한 상황을 방지하기 위해 가중치가 있는 그래프에서 사용 가능한 경로 탐색 알고리듬인 다익스트라(Dijkstra) 알고리듬에 대해 알아보자. 그리고 BFS를 실전에서 사용하기 힘든 또 다른 이유가 있는데, 이것은 Dijkstra 알고리듬을 설명하면서 같이 다루도록 하겠다.</p>
<h2 id="dijkstra">Dijkstra</h2>
<p>다익스트라 알고리듬은 가중치가 있는 그래프에서 경로를 찾기 위해 사용하는 알고리듬이다. 이에 대한 설명은 다음과 같다.</p>
<ol>
<li>도달하는데 가중치가 가장 적게 드는 정점을 찾는다</li>
<li>해당 정점에 대해 그 정점을 지나 이웃 정점들까지 이동하는데 드는 가중치를 조사한다.</li>
<li>이 과정을 모든 정점에 대해 반복한다.</li>
<li>최종 경로를 계산한다.</li>
</ol>
<p>(출처: <a href="https://medium.com/@pyeonjy97/%EB%8B%A4%EC%9D%B5%EC%8A%A4%ED%8A%B8%EB%9D%BC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-dijkstra-algorithm-5d64a6cffced">다익스트라 알고리즘(Dijkstra algorithm)</a>)</p>
<p>다익스트라에 대한 내용은 알았으니 이를 한번 구현해보자. 이번에는 노드와 노드간의 가중치를 정하기 위해 <span style="color:#73E097">컬러0</span>은 BFS와 동일하게 이동한 경로, <span style="color:#49CFBF">컬러1</span>은 목표지점으로 설정했다. <span style="color:#4A96CF">컬러2</span>은 이동시 필요한 가중치가 2, <span style="color:#4A4ECF">컬러3</span>은 이동시 필요한 가중치가 3으로 설정했다. BFS와 달리 벽과 같은 개념이 없어 결과적으로 모든 노드로 이동이 가능하기에 빨간색 노드는 사용하지 않았다.</p>
<br>

<p>!youtube[mRG3a1NY5C4?si=rAzvt4DnvHgC5PYD]</p>
<p><a href="https://github.com/eugene-doobu/pathfinding-example/blob/main/Assets/Project/Scripts/PathFinding/DijkstraPathFinding.cs">코드참고</a>
<br></p>
<p>이렇게 다익스트라 알고리듬을 사용함으로써 우리가 구현할 수 있는 기획의 범위가 늘어났다. 하지만 이정도로 실제 게임 프로젝트에서 사용하는 길찾기 알고리듬을 구현할 수 있다고 말할 수 있을까? 아쉽지만 이정도로도 충분하지 않다.</p>
<p>BFS는 &#39;Flood Fill&#39;알고리듬에서도 사용되는 기법이다. Flood Fill은 그래프에서 특정 노드와 연결된 모든 노드들을 찾는 알고리듬으로, 그림판에서 색을 채우는 명령을 통해 특정 픽셀과 인접한 모든 픽셀의 색을 바꿔버리는 기능에 사용되기도 한다. 그렇다. BFS를 통해 길찾기를 하게 되면, 해당 지점까지의 경로를 찾기 위해 인접한 모든 노드들을 탐색 해버리게 된다. 이와 같은 현상은 Dijkstra알고리듬 에서도 개선되지 못하였다.</p>
<p>지금 예제처럼 작은 형태의 맵이라면 상관 없겠지만, 초-MMORPG의 초대형 맵에서 이러한 방식으로 길찾기를 해버린다면 어떻게 될까? 클라이언트 입장에서는 길찾기 딸깍 명령 후 엄청난 시간 후에 응답을 받게 될 것이며, 게임회사 입장에서는 막대한 서버연산을 감당하지 못하고 파산해버리고 말 것이다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/c18e7565-8a4f-42ad-a71c-67442891026c/image.png" alt="감자"></p>
<center>
서버: 크윽.. 이정도 연산량이면 감자가 폭발해버리고말아..!!
</center>

<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/17a144c1-cb7e-4231-8931-aa9d6522cf6c/image.png" alt="기다리다지쳐"></p>
<center>
유저:
</center>

<h2 id="a-알고리듬">A* 알고리듬</h2>
<p>모든 경로를 탐색하지 않고 길을 탐색하려면 &#39;휴리스틱&#39;한 방법을 사용할 필요가 있다.</p>
<p>휴리스틱이란 불충분한 시간이나 정보로 합리적인 판단을 할 수 없거나, 체계적이면서 합리적인 판단이 굳이 필요하지 않은 상황에서 사람들이 빠르게 사용할 수 있게 보다 용이하게 구성된 간편추론의 방법이다. (출처: <a href="https://ko.wikipedia.org/wiki/%ED%9C%B4%EB%A6%AC%EC%8A%A4%ED%8B%B1_%EC%9D%B4%EB%A1%A0">위키백과</a>)</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/3041e45c-f9cf-4e8b-b499-c1a96efa7e7d/image.png" alt="끝까지어쩌구"></p>
<p>결국 &#39;모든 경로를 탐색하지 않기 위해서&#39;는 &#39;항상 최선의 길을 찾을 각오&#39;를 버려야 하고, 알잘딱깔센하게 연산하여 보기에 나쁘지 않은 정도의 결과를 보여주는 길찾기를 구현하는 것이다. 하지만 길찾기를 처음 구현해보는 입장에선 &#39;알잘딱깔센&#39;을 어떻게 해야하는지에 대해서 혼동이 온다.. 이를 이해하기 위해 일단 Astar알고리듬이 어떻게 구현되는지 먼저 알아보자.
<br></p>
<p>a* 알고리듬의 공식은 &#39;<strong>f(x)=g(x)+h(x)</strong>&#39; 로 표현할 수 있다. </p>
<ul>
<li>g(x)는 시작노드부터 현재 노드까지의 가중치의 합</li>
<li>h(x)는 현재 노드부터 도착 노드까지의 &#39;예상 가중치&#39;, 위에서 설명한 휴리스틱한 값이 들어가</li>
<li>결국 Astar알고리듬은 이 <strong>휴리스틱 함수에 의해 좌지우지된다.</strong> 상황에 맞는 적절한 휴리스틱 함수를 결정해야만 효율적인 길찾기를 할 수 있게 된다.<br>

</li>
</ul>
<p>사실 이렇게 설명을 들어도 휴리스틱한 방법을 어떻게 구성해야할지 감이 안온다. 여기서는 일단 가장 간단한 경험적인 방법인 <strong>맨해튼 거리</strong>를 사용해보도록 하자. 2차원에서의 맨해튼거리는 두 점(p1, p2)와 (q1, q2)가 주어졌을 때, abs(q1-p1) + abs(p2-q2)연산으로 구할 수 있다.</p>
<p>이제 이를 코드로 구현해보자. 일단 기존과 동일하게 <span style="color:#73E097">컬러0</span>은 이동한 경로, <span style="color:#49CFBF">컬러1</span>은 목표지점으로 설정했다. 그리고 <span style="color:#4A96CF">컬러2</span>는 길찾기 알고리듬 수행시 방문한 노드, <span style="color:#4A4ECF">컬러3</span>은 이동할 수 없는 벽으로 설정하였다. 기존 알고리듬과 다르게 목적지 방향을 중심으로 길찾기 알고리듬을 수행한다는 것을 시각적으로 보여주기 위해 방문한 노드를 색칠해보았다.</p>
<br>

<p>!youtube[ZmyATJPxUSk?si=X7w5pFUfZ-CxGLs6]</p>
<p><a href="https://github.com/eugene-doobu/pathfinding-example/blob/main/Assets/Project/Scripts/PathFinding/AStarPathFinding.cs">코드참고</a>
<br></p>
<p>길찾기가 아름답게 구현되었다! 우리가 구현한 방법으로는 장애물이 없는 곳에서 특히나 효율적으로 길을 찾는것으로 보인다. 이 경우 거의 목적지 방향으로 일직선으로 이동하는것을 확인할 수 있다. Astar를 적용함으로써 BFS보다 얼마나 효율적인지 비교해보기 위해 BFS알고리듬에서 방문한 노드를 색칠해보도록 하겠다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/9552af17-0403-4c04-aa7f-a952505d178d/image.png" alt="BFS가 안되는이유"></p>
<p>와우, BFS알고리즘과의 비교를 위해 이전 BFS코드에서 방문한 노드를 <span style="color:#4A4ECF">컬러3</span>으로 색칠해보도록 변경한 결과이다. 모든 노드가 파랗고 아름답게 물든것을 볼 수 있다. Astar영상과 비교해보면 왜 실전에서 BFS를 쓰기 힘든지 쉽게 알 수 있을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/c8511685-4555-4d1c-801a-bee962306555/image.png" alt="돼"></p>
<p>BFS와 Dijsktra를 넘어 A* 까지 왔다!! 이제 길찾기 기능구현 요청에 &#39;된다&#39;라고 자신있게 말하고 다닐 수 있을것같다. 하지만 이를 실제 프로젝트에 적용할 걸 생각해본다면 현실은 쉽지 않음을 알 수 있다. 예를 들어 이러한 문제들을 생각해보자</p>
<ol>
<li>지금까지 오브젝트는 한 노드에 1개씩 존재했다. 하지만 짱크고 강력한 캐릭터가 등장해 5x5크기의 노드를 차지해야한다면? (스타의 저글링, 울라리를 생각해보자)</li>
<li>근데 저 저글링과 울라리가 100마리씩 맵의 끝에서 끝까지 길찾기 연산을 시켜주려면?? 아무리 A* 라고 해도 감당이 될까?</li>
<li>온라인 게임이라고 한다면, 맵의 노드 정보는 서버가 어떻게 들고있어야 할까?</li>
<li>과연.. 노드에서 노드로 이동하는 길찾기가 <strong>보기 좋을까?</strong></li>
<li>게임에서 벗어나, 길찾기 알고리듬을 실생활에 적용한다고 생각해보자. 만약 A* 알고리듬을 기반으로 실시간으로 변하는 교통량에 적응하는 네비게이션을 만든다고 했을때 이게 제대로 동작할 것인가?</li>
</ol>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/b5f75105-267c-42cc-bf17-89e0933e97d1/image.png" alt="답이없네~"></p>
<p>그렇다. 사실 길찾기 알고리듬이란 답이 없다. 말 그대로 &#39;알잘딱깔센&#39;해서 현재 구현할 수 있는 만큼 적당히 연산부하 덜면서 보기좋을정도로 계속해서 손질하는 수 밖에 없다.</p>
<h2 id="a-실적용-예시">A* 실적용 예시</h2>
<p>상황에따라 위에 구현된 Astar 만으로는 실제 프로젝트에 적용하기 부족할 수 있다. 이제 실제 프로젝트에 맞게 A* 알고리듬을 개조시켜서 적용하는 예시 몇가지를 소개하고 글을 마치도록 하겠다.</p>
<h3 id="지그재그-워킹-문제">지그재그 워킹 문제</h3>
<p>우리는 육각형으로 시각화된 맵 위에서 길찾기를 했다. 동글이는 이동 경로는 육각형의 중앙에서 중앙으로 이동하며, 이러한 행동이 부자연스러워 보이진 않았다. 하지만 논리적으로는 쪼개져있지만 아트 에셋상에서 이러한 논리적인 경계가 뚜렷하게 보이지 않는 맵에서 이렇게 이동하면 어떻게 될까? 아래 영상을 봐보도록 하자.</p>
<p>!youtube[vWSXTyiCmmI?si=rqFOXPbVpb9TNs22]</p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">"게임 Voxel Horizon의 길찾기 개발영상"</figcaption>

<p>게임 맵의 바닥이 삼각형들로 분할되어있다. 이 삼각형 하나하나가 길찾기에 사용되는 노드들이라고 보면 된다. 특정 노드를 클릭시 해당 삼각형이 노란색으로 바뀌며, 얻게된 길찾기 경로에 초록색 선이 표현되며 캐릭터는 해당 경로를 따라 달려간다. 이 때 경로의 노드들은 삼각형의 무게중심을 기준으로 생성된다. 실제 게임에서 캐릭터가 저런 무빙을 보여주면 어떨까?</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/d7389e9c-fb54-4b2d-81ec-2e137fac8744/image.png" alt="깊은빡침"></p>
<p>바로 이런 표정을 지어버리면서 길찾기는 봉인한 상태로 키보드 수동 입력으로 이동을 하려 할 것이다. 위에서 이야기했던 &#39;노드에서 노드로 이동하는 길찾기가 보기 좋을까&#39;라고 물었던 이유가 바로 이것이다. 유저가 보기에 논리적인 경계가 뚜렷하게 보이지 않는 맵에서 A* 로 주어진 길찾기 경로를 지그재그로 이동하며 굉장히 멍청하게 이동하는 것처럼 보일 것이다.</p>
<p>이걸 보고 &#39;저 삼각형을 잘게 쪼개면 되는거 아닌가요?&#39;라고 생각할 수 있다. 어떤 상황에서는 그게 정답이 될 수도 있지만, 팰월드와 같은 넓은 월드에서 삼각형을 잘게 쪼개버리면 연산량이 폭발적으로 늘어나게 될 것이고, 우리의 감자 서버는 다시 한번 폭발을 하게 될 것이다. 그럼 위 게임의 제작자는 이러한 문제를 어떻게 해결했는지 구경해보자.</p>
<br>

<p>!youtube[qcm1nqCHN6k?si=vLmdL70gemp49MXh]</p>
<br>

<p>영상을 보면 길찾기 도착 위치까지의 초록색 삼각형이 렌더링되는것을 확인할 수 있다. 이는 Stupid Funnel 알고리즘과 유사한 방법을 적용해 구현된 것으로, 각 삼각형 노드들에 대해 가시성 검사와 유사한 방식으로 테스트를 해 한번에 직선으로 이동할 수 있는 만큼 최대한 이동하게끔 알고리즘이 구현되었다. 현재 캐릭터의 위치에서 다음 노드의 맞닿아 있는 면까지를 최초 프러스텀으로 하여, 다음 경로의 맞닿은 면이 이전 프러스텀에 포함되게끔 재귀적으로 다음 프러스텀을 구하며, 프러스텀 면의 크기가 캐릭터의 크기보다 작아지게 되면 그 위치까지 직선으로 이동하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/2f7eabf2-cc14-4076-ac8c-31ae2103a50f/image.png" alt="프러스텀"></p>
<p><a href="https://megayuchi.com/2020/11/14/3d-%ea%b8%b8%ec%b0%be%ea%b8%b0-%ea%b5%ac%ed%98%84%ec%a4%91-3-visibility-%ed%85%8c%ec%8a%a4%ed%8a%b8%ec%99%80-%ec%9c%a0%ec%82%ac%ed%95%9c-stupid-funnel-%ec%95%8c%ea%b3%a0%eb%a6%ac%ec%a6%98-%ec%a0%81/">블로그 글</a> / <a href="https://github.com/megayuchi/ppt/blob/main/docs/2021_0706_%EB%84%A4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98%20%EB%A7%A4%EC%8B%9C%EB%A5%BC%20%EC%9D%B4%EC%9A%A9%ED%95%9C%203D%EA%B2%8C%EC%9E%84%20%EA%B8%B8%EC%B0%BE%EA%B8%B0%20%EC%A0%84%EB%9E%B5.pdf">강좌</a></p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/e4213f70-bc31-4e89-b869-f10445471729/image.png" alt="Stupid funnel"></p>
<p><a href="https://arongranberg.com/astar/documentation/dev_4_1_7_6425cc50/class_pathfinding_1_1_funnel_modifier.php">FunnelModifier 문서</a></p>
<p>이러한 개선방식의 단점으로는 벽을 끼고 이동하는 경우 캐릭터가 벽에 붙어서 이동하는듯한 느낌을 준다는 것이다. 위 사진의 가장 오른쪽에 있는 &#39;Shortest path in funnel&#39; 이미지에 있는 경로같은 느낌으로 캐릭터가 이동한다고 생각하면 된다.</p>
<h3 id="단체-길찾기-문제">단체 길찾기 문제</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/3c3953c3-9979-45fd-803a-15c932447d0f/image.gif" alt="단체 길찾기 이상편"></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">"단체 길찾기 이상편"</figcaption>

<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/3ad8e715-7634-4901-99dc-67b2715299ec/image.gif" alt="단체 길찾기 현실편"></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">"단체 길찾기 현실편"</figcaption>

<p>(<a href="https://gamedev.stackexchange.com/questions/191954/how-can-i-create-the-arriving-engaging-in-combat-movement-like-in-starcraft-2">움짤 출처</a>)</p>
<p>스타와 같은 게임에서 대규모 군대가 충돌한다고 했을때는 어떻게 구현해야 유닛들의 동작이 이쁘게 구현될 수 있을까? 위 단체 길찾기 현실편 예시만 보아도 이와 같은 구현이 쉽지 않음을 알 수 있다. 이와 관련하여 <a href="https://gdcvault.com/play/1014514/AI-Navigation-It-s-Not">스타크래프트2에서의 길찾기 방법에 대한 강연</a>에서 찾아볼 수 있다고 한다.</p>
<p>스타2에서는 델로네 삼각분할로 제한된 노드 위에서 Astar알고리즘과 Funnel알고리즘을 이용하여 기본적인 길찾기 알고리즘을 구현하였고, 부대 안에서 유닛들의 간격은 Boid알고리즘을 통해 구현되었다고 한다. 이에 대한 내용은 여백이 부족하여 적지 않겠다..</p>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/e8dd6af2-8e22-407e-8cf6-636e36c0b469/image.png" alt="여백이모잘라"></p>
<p>이외에도 길찾기 알고리듬을 실전에 적용하기 위해서나 변태들이 지적 유희를 위해 변형시킨 Astar알고리듬들이 존재한다. 전쟁게임의 경우 부대가 굉장히 넓은 맵에서 이동 경로를 완벽하게 결정한 후 이동해야하기 때문에 넓은 맵에서 특히 유용한 <a href="https://www.youtube.com/watch?v=rfOgaPXCADQ">JPS</a>라는 알고리듬을 사용하기도 하고, 넓은 지형에 사용하면서도 유동적인 도로 상태를 실시간으로 반영하기 위해 내비게이션에서는 <a href="https://tech.kakao.com/2021/05/10/kakaomap-cch/">CCH</a>알고리즘을 사용한다고도 한다.</p>
<h2 id="다음편-예고">다음편 예고..</h2>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/df77c521-f8c3-4001-8410-2a93f1936b33/image.png" alt="공간분할"></p>
<h3 id="끝">끝</h3>
<p><img src="https://velog.velcdn.com/images/eugene-doobu/post/36e57082-b7c0-4aa9-a0e6-a251d740cf27/image.png" alt="땡큐"></p>
<hr>
<h4 id="참고">참고</h4>
<p><a href="https://www.inflearn.com/course/%EC%9C%A0%EB%8B%88%ED%8B%B0-mmorpg-%EA%B0%9C%EB%B0%9C-part2/dashboard">C#과 유니티로 만드는 MMORPG 게임 개발 시리즈</a>
<a href="https://www.youtube.com/watch?v=8-4KzycX_9o&amp;list=PL00yTT-RECdWsBjP-rQcDBelgehOyToy3&amp;index=19">네비게이션 매시를 이용한 3D게임 길찾기 전략</a>
<a href="https://www.jdxdev.com/blog/2021/03/19/boids-for-rts/">Boids for RTS</a>
<a href="https://zprooo915.tistory.com/78">[알고리즘][길찾기] A* 알고리즘</a>
<a href="https://github.com/megayuchi/ppt/blob/main/docs/2021_0706_%EB%84%A4%EB%B9%84%EA%B2%8C%EC%9D%B4%EC%85%98%20%EB%A7%A4%EC%8B%9C%EB%A5%BC%20%EC%9D%B4%EC%9A%A9%ED%95%9C%203D%EA%B2%8C%EC%9E%84%20%EA%B8%B8%EC%B0%BE%EA%B8%B0%20%EC%A0%84%EB%9E%B5.pdf">네비게이션 매시를 이용한 3D게임 길찾기 전략</a></p>
]]></description>
        </item>
    </channel>
</rss>