<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>맨텀</title>
    <link>https://mentum.tistory.com/</link>
    <description>memento3525@nate.com</description>
    <language>ko</language>
    <pubDate>Sun, 17 May 2026 01:50:37 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>맨텀</managingEditor>
    <image>
      <title>맨텀</title>
      <url>https://tistory1.daumcdn.net/tistory/1963568/attach/030ef65e274f487cadb04e8810d0017c</url>
      <link>https://mentum.tistory.com</link>
    </image>
    <item>
      <title>쉐이더 어드레서블 관련 메모</title>
      <link>https://mentum.tistory.com/980</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어드레서블에 쉐이더를 포함하지 않은 경우 쉐이더를 사용하는 머테리얼이 있는 모든 번들에 쉐이더가 복사된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 쉐이더를 넣을 경우 쉐이더가 중복되지는 않지만 스트리핑을 하지않고 가능한 모든 키워드의 배리언트를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히나 URP/Lit, URP/SimpleLit 같은건 범용 쉐이더기 때문에 스트리핑이 없다면 말그대로 빌드시간이 폭발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 IPreprocessShaders.OnProcessShader 를 사용하여 빌드할때 스트리핑을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키워드를 관리해야되기 때문에 URP/List, URP/SimpleLit는 사용하지않고 커스텀 쉐이더만을 사용하는게 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Unity/최적화</category>
      <author>맨텀</author>
      <guid isPermaLink="true">https://mentum.tistory.com/980</guid>
      <comments>https://mentum.tistory.com/980#entry980comment</comments>
      <pubDate>Tue, 17 Feb 2026 12:17:56 +0900</pubDate>
    </item>
    <item>
      <title>Unity 터레인 어둡게 나오는 버그</title>
      <link>https://mentum.tistory.com/978</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니티에서 windows 플랫폼으로 개발하다가 안드로이드로 스위칭 후&amp;nbsp; &lt;br /&gt;Terrain&amp;nbsp;텍스쳐가&amp;nbsp;1~4번&amp;nbsp;까지는&amp;nbsp;어둡고&amp;nbsp;5번부터는&amp;nbsp;정상적으로&amp;nbsp;나오는&amp;nbsp;버그&amp;nbsp;발생. &lt;br /&gt;&lt;br /&gt;unity&amp;nbsp;6000.2.12f1&amp;nbsp; &lt;br /&gt;URP&amp;nbsp; &lt;br /&gt;Forward+&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터레인에서 1~4번 텍스쳐 레이어에 노멀맵이 포함되어있는 경우에 발생하는 버그. &lt;br /&gt;노멀맵을&amp;nbsp;삭제하니까&amp;nbsp;정상&amp;nbsp;작동함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;첨언&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 이 버그와는 별도로 &lt;br /&gt;안드로이드&amp;nbsp;스위칭시&amp;nbsp;터레인이&amp;nbsp;너무&amp;nbsp;반짝거리는&amp;nbsp;문제가&amp;nbsp;발생시에는 &lt;br /&gt;레이어&amp;nbsp;설정에서&amp;nbsp;Channel&amp;nbsp;Default&amp;nbsp;Values&amp;nbsp;의&amp;nbsp;Smoothness를&amp;nbsp;0으로&amp;nbsp;바꿔주자.&amp;nbsp;&lt;/p&gt;</description>
      <category>  Unity/버그 및 오류 해결</category>
      <author>맨텀</author>
      <guid isPermaLink="true">https://mentum.tistory.com/978</guid>
      <comments>https://mentum.tistory.com/978#entry978comment</comments>
      <pubDate>Thu, 15 Jan 2026 17:08:39 +0900</pubDate>
    </item>
    <item>
      <title>Unity6 git-for-Unity 오류 수정</title>
      <link>https://mentum.tistory.com/974</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;출처 : &lt;a href=&quot;https://github.com/spoiledcat/git-for-unity/issues/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/spoiledcat/git-for-unity/issues/34&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spoilercat의 git for unity는 유니티 6부터 지원하지 않고 지원도 중단되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에디터에서 수정사항에 대한 아이콘만 보고 싶은 경우 코드를 수정하면 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음의 경로에서&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755157951993&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Library\PackageCache\cohttp://m.spoiledcat.git@a5968714214e\Editor\Misc\Utility.cs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Utility.cs파일의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;75번째줄&amp;nbsp;StreamExtensions 클래스 내용을 다음으로 대체한다.&lt;/p&gt;
&lt;pre id=&quot;code_1755157990506&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    static class StreamExtensions
    {
        private static MethodInfo loadImage;
        private static Func&amp;lt;Texture2D, MemoryStream, Texture2D&amp;gt; invokeLoadImage;
        delegate bool LoadImageDelegate(Texture2D tex, System.ReadOnlySpan&amp;lt;byte&amp;gt; data);

        static StreamExtensions()
        {
            // 5.6
            // looking for Texture2D.LoadImage(byte[] data)
            loadImage = typeof(Texture2D).GetMethods().FirstOrDefault(x =&amp;gt; x.Name == &quot;LoadImage&quot; &amp;amp;&amp;amp; x.GetParameters().Length == 1);
            if (loadImage != null)
            {
                invokeLoadImage = (tex, ms) =&amp;gt;
                {
                    loadImage.Invoke(tex, new object[] { ms.ToArray() });
                    return tex;
                };
            }
            else
            {
                // 2017.1
                var t = typeof(Texture2D).Assembly.GetType(&quot;UnityEngine.ImageConversion&quot;, false, false);
                if (t == null)
                {
                    // 2017.2 and above
                    t = Assembly.Load(&quot;UnityEngine.ImageConversionModule&quot;).GetType(&quot;UnityEngine.ImageConversion&quot;, false, false);
                }

                if (t != null)
                {
                    // looking for ImageConversion.LoadImage(this Texture2D tex, byte[] data)
                    loadImage = t.GetMethods().FirstOrDefault(x =&amp;gt; x.Name == &quot;LoadImage&quot; &amp;amp;&amp;amp; x.GetParameters().Length == 2);
                    invokeLoadImage = (tex, ms) =&amp;gt;
                    {
                        //loadImage.Invoke(null, new object[] { tex, ms.ToArray() });
                        var loadImageDelegate = (LoadImageDelegate)Delegate.CreateDelegate(typeof(LoadImageDelegate), loadImage);
                        loadImageDelegate(tex, ms.ToArray());
                        return tex;
                    };
                }
            }

            if (loadImage == null)
            {
                LogHelper.LogAdapter.Error(&quot;Utility&quot;, &quot;Could not find ImageConversion.LoadImage method&quot;);
            }
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아이콘은 노출 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 코드를 수정하는거기 때문에 그냥 복사해서 Package 폴더로 이동시켜야 다른작업자들과 공유된다.&amp;nbsp;&lt;/p&gt;</description>
      <category>  Unity/에셋, 플러그인</category>
      <author>맨텀</author>
      <guid isPermaLink="true">https://mentum.tistory.com/974</guid>
      <comments>https://mentum.tistory.com/974#entry974comment</comments>
      <pubDate>Thu, 14 Aug 2025 16:54:20 +0900</pubDate>
    </item>
    <item>
      <title>유니티 컴파일 타임을 로그로 출력하는 예제</title>
      <link>https://mentum.tistory.com/973</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;gist의 코드를 참조함.&lt;br /&gt;&lt;a href=&quot;https://gist.github.com/filod/ba1e1522c1821cd24ca1a0c9090eb440#file-asmdefdebug-cs-L59&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gist.github.com/filod/ba1e1522c1821cd24ca1a0c9090eb440#file-asmdefdebug-cs-L59&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gist의 코드를 참조하고 30초를 최대로 그래프로 나타내는 커스텀로그를 추가하였음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에디터 폴더에 넣으면 동작합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;73&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5ho45/btsOZy0BPWo/2KnLlxyjLjDdpnEIGrsVL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5ho45/btsOZy0BPWo/2KnLlxyjLjDdpnEIGrsVL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5ho45/btsOZy0BPWo/2KnLlxyjLjDdpnEIGrsVL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5ho45%2FbtsOZy0BPWo%2F2KnLlxyjLjDdpnEIGrsVL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;591&quot; height=&quot;73&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;73&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 버전 Unity 6.1.2f1&lt;/p&gt;
&lt;pre id=&quot;code_1751341482078&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using UnityEditor;
using UnityEditor.Compilation;

/// &amp;lt;summary&amp;gt;
/// https://gist.github.com/filod/ba1e1522c1821cd24ca1a0c9090eb440#file-asmdefdebug-cs-L59
/// &amp;lt;/summary&amp;gt;
[InitializeOnLoad]
public class CompileTimeChecker
{
    const string AssemblyReloadEventsEditorPref = &quot;AssemblyReloadEventsTime&quot;;
    const string AssemblyCompilationEventsEditorPref = &quot;AssemblyCompilationEvents&quot;;
    static readonly int ScriptAssembliesPathLen = &quot;Library/ScriptAssemblies/&quot;.Length;
    private static string AssemblyTotalCompilationTimeEditorPref = &quot;AssemblyTotalCompilationTime&quot;;

    static Dictionary&amp;lt;string, DateTime&amp;gt; s_StartTimes = new Dictionary&amp;lt;string, DateTime&amp;gt;();

    static StringBuilder s_BuildEvents = new StringBuilder();
    static double s_CompilationTotalTime;

    static CompileTimeChecker()
    {
        CompilationPipeline.assemblyCompilationStarted += CompilationPipelineOnAssemblyCompilationStarted;
        CompilationPipeline.assemblyCompilationFinished += CompilationPipelineOnAssemblyCompilationFinished;
        AssemblyReloadEvents.beforeAssemblyReload += AssemblyReloadEventsOnBeforeAssemblyReload;
        AssemblyReloadEvents.afterAssemblyReload += AssemblyReloadEventsOnAfterAssemblyReload;
    }

    static void CompilationPipelineOnAssemblyCompilationStarted(string assembly)
    {
        s_StartTimes[assembly] = DateTime.UtcNow;
    }

    static void CompilationPipelineOnAssemblyCompilationFinished(string assembly, CompilerMessage[] arg2)
    {
        var timeSpan = DateTime.UtcNow - s_StartTimes[assembly];
        s_CompilationTotalTime += timeSpan.TotalMilliseconds;
        s_BuildEvents.AppendFormat(&quot;{0:0.00}s {1}\n&quot;, timeSpan.TotalMilliseconds / 1000f,
            assembly.Substring(ScriptAssembliesPathLen, assembly.Length - ScriptAssembliesPathLen));
    }

    static void AssemblyReloadEventsOnBeforeAssemblyReload()
    {
        var totalCompilationTimeSeconds = s_CompilationTotalTime / 1000f;
        s_BuildEvents.AppendFormat(&quot;------- compilation total: {0:0.00}s\n&quot;, totalCompilationTimeSeconds);
        EditorPrefs.SetString(AssemblyReloadEventsEditorPref, DateTime.UtcNow.ToBinary().ToString());
        EditorPrefs.SetString(AssemblyCompilationEventsEditorPref, s_BuildEvents.ToString());
        EditorPrefs.SetString(AssemblyTotalCompilationTimeEditorPref, totalCompilationTimeSeconds.ToString(CultureInfo.InvariantCulture));
    }

    static void AssemblyReloadEventsOnAfterAssemblyReload()
    {
        var binString = EditorPrefs.GetString(AssemblyReloadEventsEditorPref);
        var totalCompilationTimeSeconds = float.Parse(EditorPrefs.GetString(AssemblyTotalCompilationTimeEditorPref), CultureInfo.InvariantCulture); 

        long bin;
        if (long.TryParse(binString, out bin))
        {
            var date = DateTime.FromBinary(bin);
            var time = DateTime.UtcNow - date;
            var compilationTimes = EditorPrefs.GetString(AssemblyCompilationEventsEditorPref);
            var totalTimeSeconds = totalCompilationTimeSeconds + time.TotalSeconds;
            if (!string.IsNullOrEmpty(compilationTimes))
            {
                Debug.Log($&quot;&amp;lt;color=cyan&amp;gt;- 컴파일 시간 : {LogProgress(totalTimeSeconds)} ---------------------------&amp;lt;/color&amp;gt;\n&quot; + compilationTimes + &quot;Assembly Reload Time: &quot; + time.TotalSeconds + &quot;s\n&quot;);
            }
        }
    }

    static string LogProgress(double elapsedSeconds)
    {
        const double maxTime = 30.0;
        const int totalBlocks = 15;
        const double secondsPerBlock = maxTime / totalBlocks;

        // 채워질 블록 수 계산
        int filledBlocks = (int)(elapsedSeconds / secondsPerBlock);
        if (filledBlocks &amp;gt; totalBlocks) filledBlocks = totalBlocks;

        // 블록 문자열 생성
        string filled = new string('■', filledBlocks);
        string empty = new string('□', totalBlocks - filledBlocks);

        return $&quot;[ {filled}{empty} ] / {elapsedSeconds:0.0}초&quot;;
    }
}
#endif&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Unity/유니티 엔진 동작 관련</category>
      <author>맨텀</author>
      <guid isPermaLink="true">https://mentum.tistory.com/973</guid>
      <comments>https://mentum.tistory.com/973#entry973comment</comments>
      <pubDate>Tue, 1 Jul 2025 12:45:26 +0900</pubDate>
    </item>
    <item>
      <title>[영상필기] [Unite Seoul 2025] Addressables을 사용한 최신 모범 사례</title>
      <link>https://mentum.tistory.com/970</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=UgOdwds7udc&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=UgOdwds7udc&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=UgOdwds7udc&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/cG0Nf5/hyY1aIcMxX/GDPXpmXx1REeMwLZmAnZk1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/LnTSv/hyY1jd49mN/hGDvikbTni2qDvTsoXfC90/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;[Unite Seoul 2025] Addressables을 사용한 최신 모범 사례&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/UgOdwds7udc&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표 PPT :&amp;nbsp;&amp;nbsp;&lt;a href=&quot;https://on.unitysquare.co.kr/4kFgTgg&quot;&gt;https://on.unitysquare.co.kr/4kFgTgg&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새로 알게된 사실 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 하드웨어가 완전히 동일한 사양이 아니라면 에셋번들은 빌드하는 PC마다 해시가 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모바일 게임에서 흔히 사용되는 필수번들만 다운받기, 전체번들 다운받기는 Label을 통해서 구현하면된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에셋 번들의 결정성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 에셋 번들에서 비결정성을 줄이는 것이 중요하다. 그렇다면 에셋번들의 결정성은 무엇인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 같은 에셋이 에셋번들에 포함되면, 언제나 같은 에셋 번들 바이너리를 내놓게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 왜 이게 중요한가? 캐시된 에셋번들이 최신이 아니라서 다운로드 할 시점을 파악해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 빌드과정이 비 결정적이라면 에셋번들이 변하지도 않았는데 유저는 다시 다운을 받아야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;u&gt;동일한 에셋이라면 어떤 머신에서 빌드하던지 같은 바이너리 결과&lt;/u&gt;가 나오는게 &lt;u&gt;에셋번들의 결정성&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 비 결정적인 경우가 발생한다. 예외사항을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 유니티 엔진의 버그 (예를들어 애니메이션 컨트롤러가 포함된 번들을 빌드할 때 다른 아키텍쳐를 가진 머신 에서 빌드하면 서로 다른 결과의 바이너리가 도출된다)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 하드웨어 아키텍쳐가 다르면 부동소수점 연산이 차이가 있어서 이 또한 비결정성으로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 텍스쳐 압축도 하드웨어에 따라 달라질 수 있어서 결정성을 보장받지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 맥과 윈도우는 줄바꿈을 다르게 처리하기 때문에 이 또한 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 에셋 포스트 프로세스 등으로 임포트 과정에서 개입하는 경우도 비결정성이 생길 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이러한 비결정성을 어떻게 줄일 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;게임의 릴리스 빌드를 만들 때는 특정 머신을 지정해서 사용&lt;/u&gt;하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 머신이 여러대라면 하드웨어 사양이 최대한 비슷해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에셋 번들 결정성 디버깅하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Build LayoutReport 를 통해 빌드 상세과정을 확인할 수 있으니 이걸로 디버깅 하기 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에셋번들에는 Hash 값이 있는데, 이 값이 바뀌었다면 에셋 내용이 바뀌었음을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Hash가 바뀌었다면 에셋 디펜던시에서 어떤 에셋의 Hash가 바뀌어서 변경을 유발시켰는지 디버깅 해보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BuildLayoutReport의 API를 사용해서 이를 분석하는데 활용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1748332007314&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void CompareBuildLayouts(string buildLayoutPathA, string buildLayoutPathB)
{
    var buildLayoutA = BuildLayout.Open(buildLayoutPathA, readFullFile: true);
    var buildLayoutB = BuildLayout.Open(buildLayoutPathB, readFullFile: true);

    foreach (var bundleA in BuildLayoutHelpers.EnumerateBundles(buildLayoutA))
    {
        foreach (var bundleB in BuildLayoutHelpers.EnumerateBundles(buildLayoutB))
        {
            if (bundleA.InternalName != bundleB.InternalName)
                continue;

            if (bundleA.Hash != bundleB.Hash)
                Debug.LogWarning($&quot;Asset Bundle Hash changed: {bundleA.Name}&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어드레서블 Hash의 계산방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 압축되지 않은 값을 기준으로 해시를 생성한다. 그러면 압축방법이 달라도 해시가 같아지기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 해시 계산에서 헤더는 제외된다. 유니티의 다른 버전에서 빌드할때 값이 달라지지 않게 하기 위해서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 하지만 헤더가 계산에서 제외되기 때문에 어떤 경우에는 헤더가 바뀌면 아카이브 컨텐츠가 바뀌지만 해시에서는 반영되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 에셋번들 해시는 에셋번들의 카탈로그파일에 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 런타임중에 카탈로그에서 에셋번들 해시를 읽어들어서 이미 캐시된 번들을 사용할지 다운로드할지 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bundle Naming Mode&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에셋번들 그룹 설정에서 Bundle Naming Mode에 대해서 질문이 많이 들어온다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Append Hash , No Hash 가 권장사항이다. 나머지 두개는 디버깅이 어렵다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;357&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO6B7b/btsOf2gNufr/XLsKnd9Id8NQzeWDODI481/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO6B7b/btsOf2gNufr/XLsKnd9Id8NQzeWDODI481/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO6B7b/btsOf2gNufr/XLsKnd9Id8NQzeWDODI481/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO6B7b%2FbtsOf2gNufr%2FXLsKnd9Id8NQzeWDODI481%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;357&quot; height=&quot;293&quot; data-origin-width=&quot;357&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfEtRO/btsOeDWJyaE/Xj0oi0JD2r6Z8sqt62KUlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfEtRO/btsOeDWJyaE/Xj0oi0JD2r6Z8sqt62KUlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfEtRO/btsOeDWJyaE/Xj0oi0JD2r6Z8sqt62KUlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfEtRO%2FbtsOeDWJyaE%2FXj0oi0JD2r6Z8sqt62KUlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;440&quot; height=&quot;131&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppendHash 의 장점은 디버깅이 쉽다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번들이 바뀐 것을 명시적으로 알 수 있고, 어떤 번들이 로드되어있는지 파악하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 버전의 번들(or 다른플랫폼)이 같은 경로에 저장될 수 있는 장점도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppendHash의 단점으로는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 파일을 덮어쓰지 않기 때문에 점점 쓰레기 번들이 늘어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 작은 변경이 있는 경우 콘솔에서 파일 수준 패치를 방지되버린다. 작은 변경사항을 패치를 통해서 적용할 수 있는데, 파일이름 자체가 바뀌어버리다보니 전체를 다시 받아야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Remote Catalog Namig&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 카탈로그는 에셋번들의 가용성, 종속성, 위치를 알려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 빌드에 포함되는 로컬 카탈로그, 서버에 올라가는 리모트 카탈로그로 구분된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 리모트 카탈로그는 런타임시에 서버에서 다운받아서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 플레이어 빌드에는 리모트 카탈로그의 URL이 포함되어 이를 다운로드하고, 이 카탈로그를 보고 번들을 받아서 캐싱한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Player Version Override&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 플레이어 버전 오버라이드 속성을 수정하면 카탈로그의 이름을 제어할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;363&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKCdnm/btsOeIRdoCk/gBPt0mnHHGZVqc02eRGc1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKCdnm/btsOeIRdoCk/gBPt0mnHHGZVqc02eRGc1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKCdnm/btsOeIRdoCk/gBPt0mnHHGZVqc02eRGc1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKCdnm%2FbtsOeIRdoCk%2FgBPt0mnHHGZVqc02eRGc1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;363&quot; height=&quot;336&quot; data-origin-width=&quot;363&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저기에 값을 입력하면 아래와 같이 catalog_XXXX.json 형식으로 파일이 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값으로는 번들버전으로 지정되어있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;369&quot; data-origin-height=&quot;75&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JGory/btsOeU40uaE/dMuOkhkArN5qJQPvy6kRDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JGory/btsOeU40uaE/dMuOkhkArN5qJQPvy6kRDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JGory/btsOeU40uaE/dMuOkhkArN5qJQPvy6kRDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJGory%2FbtsOeU40uaE%2FdMuOkhkArN5qJQPvy6kRDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;369&quot; height=&quot;75&quot; data-origin-width=&quot;369&quot; data-origin-height=&quot;75&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) Timestamp를 사용하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅이 편하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOPBOS/btsOejqzGSq/eJkJ2WGiykVxIVeqo8HEk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOPBOS/btsOejqzGSq/eJkJ2WGiykVxIVeqo8HEk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOPBOS/btsOejqzGSq/eJkJ2WGiykVxIVeqo8HEk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOPBOS%2FbtsOejqzGSq%2FeJkJ2WGiykVxIVeqo8HEk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;408&quot; height=&quot;257&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) BundleVersion (디폴트설정) : 플레이어 버전을 사용하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 다른 플레이어 버전에서 같은 번들을 사용하지 못하는 문제가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;416&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCJ2Qk/btsOedYqiLM/cDgYCltev9IP4VfnkUhQi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCJ2Qk/btsOedYqiLM/cDgYCltev9IP4VfnkUhQi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCJ2Qk/btsOedYqiLM/cDgYCltev9IP4VfnkUhQi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCJ2Qk%2FbtsOedYqiLM%2FcDgYCltev9IP4VfnkUhQi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;416&quot; height=&quot;265&quot; data-origin-width=&quot;416&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) Constant String : 고정 문자열 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 호환되지 않을때 수동으로 문자열을 수정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 수동이기 때문에 실수하기 쉽다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;415&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRIKog/btsOd5fa8V2/ZNeApwaAdcPXORfcGZtgdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRIKog/btsOd5fa8V2/ZNeApwaAdcPXORfcGZtgdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRIKog/btsOd5fa8V2/ZNeApwaAdcPXORfcGZtgdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRIKog%2FbtsOd5fa8V2%2FZNeApwaAdcPXORfcGZtgdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;415&quot; height=&quot;268&quot; data-origin-width=&quot;415&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리모트 카탈로그 로드하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리모트 카탈로그를 사용하도록 설정한 경우 자동으로 다운받는게 기본설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 LoadContentCatalogAsync 를 사용하여 수동으로 받아올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨텐츠 전용 프로젝트 분리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규모가 큰 프로젝트의 경우 프로젝트를 여러개로 분리해서 카탈로그를 만드는 워크플로우를 사용하기도 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm6lD0/btsOecZzGaG/6Hg3Znd0BcLICyiLUiXUWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm6lD0/btsOecZzGaG/6Hg3Znd0BcLICyiLUiXUWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm6lD0/btsOecZzGaG/6Hg3Znd0BcLICyiLUiXUWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm6lD0%2FbtsOecZzGaG%2F6Hg3Znd0BcLICyiLUiXUWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;202&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 당연히 이런 프로세스를 만드는 것은 어렵고 도전적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 스크립트를 공유해야하는 경우 패키지 형태로 디펜던시를 관리해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;라벨을 사용하여 선택적으로 로드하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이 중 추가 컨텐츠를 다운받을지 선택지가 주어지는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Prologue 라벨을 다운받고서 프롤로그를 플레이 하는 동안에 Essential 라벨을 백그라운드에서 다운받는 식으로 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 라벨은 동일한 용도의 에셋의 다른 버전을 로드하는데도 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저사양 폰에서는 저화질 에셋은 다운로드 하겠다 하면 라벨을 구분지어서 분기 로딩할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미지원 CPU에서는 특정 쉐이더를 로드안하게 할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Load Path brancing&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어드레서블의 로드를 분리하기 위한 방법은 2가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 어드레서블 프로파일을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 프로파일로 개발용, QA용 인지에 따라 나눌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 단점은 엔드포인트가 바뀌었을 때, CND 서버가 바뀌었을 때 카탈로그를 재생성하려면 빌드를 다시 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 여러 조합이 생기면 관리할 프로필 갯수가 급격하게 늘어날 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;203&quot; data-origin-height=&quot;129&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eDu772/btsOd6Sx2FF/uT3vZ0HwrlZOkBqNBjqWb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eDu772/btsOd6Sx2FF/uT3vZ0HwrlZOkBqNBjqWb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eDu772/btsOd6Sx2FF/uT3vZ0HwrlZOkBqNBjqWb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeDu772%2FbtsOd6Sx2FF%2FuT3vZ0HwrlZOkBqNBjqWb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;203&quot; height=&quot;129&quot; data-origin-width=&quot;203&quot; data-origin-height=&quot;129&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 어드레서블 프로파일에 Remote Path 를 분기시켜서 로드 주소를 바꾼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 1의 단점을 해결하기 위해 RemotePath에 동적인 값을 넣을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- { } 를 넣어서 동적변수로 프로퍼티를 지정하면 런타임 중에 동적으로 해석된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 여기에 넣을 수 있는 건 &lt;u&gt;런타임 클래스의 정의된 정적 속성이&lt;/u&gt;여야한다. (네임스페이스 포함해서 적어야한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 아쉽게도 게임이 시작될 때 어드레서블 초기화 단계에서 한 번만 초기화되고, 런타임 중에 바꿀수는 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpKdfA/btsOdDiR9Hk/WRO4HHGNHDIcJzDuiCHvlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpKdfA/btsOdDiR9Hk/WRO4HHGNHDIcJzDuiCHvlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpKdfA/btsOdDiR9Hk/WRO4HHGNHDIcJzDuiCHvlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpKdfA%2FbtsOdDiR9Hk%2FWRO4HHGNHDIcJzDuiCHvlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;439&quot; height=&quot;186&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. InternalIdTransformFunc 을 이용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 이는 델리게이션 방법으로 유저가 커스텀 메서드를 명시하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 어드레서블이 불러오는 URL을 개별적으로 수정할 수 있고, 초기화 단계가 끝났어도 동적으로 수정할 수도있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 주의할 점은 델리게이션 메서드를 로딩전에 할당해야한다. 그렇지 않았다면 디폴트 URL이 사용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1748334646135&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UsingInternalIdTransformFuncSample()
{
    Addressables.InternalIdTransformFunc = MyCustomTransform;
    opHandle = Addressables.InstantiateAsync(asset);
    opHandle.Completed += OnInstantiateComplete;
}

// Implement a method to transform the internal ids of locations
static string MyCustomTransform(IResourceLocation location)
{
    if (location.ResourceType == typeof(IAssetBundleResource)
        &amp;amp;&amp;amp; !location.InternalId.StartsWith(&quot;http&quot;))
    {
        Debug.Log($&quot;Replace local identifier with remote URL : {location.InternalId}&quot;);
        return &quot;file:///&quot; + location.InternalId;
    }

    return location.InternalId;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. WebRequestOverride 속성을 이용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 3번에서 인증이 필요한 경우에는 어떻게 할까? 직접 웹 요청을 조작하면된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 요청 자체에 헤더 필드를 추가할 수도있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748334761079&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void Start()
{
    Addressables.WebRequestOverride = AddPrivateToken;
}

// Demonstrate adding an Authorization header to access a Cloud Content Delivery private bucket
private void AddPrivateToken(UnityWebRequest request)
{
    var encodedToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($&quot;{bucketAccessToken}&quot;));
    request.SetRequestHeader(&quot;Authorization&quot;, $&quot;Bearer {encodedToken}&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Remapper 와 어드레서블&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Remapper는 디바이스에 저장된 물리적 시리얼라이즈 된 아이디를 IO로 읽어들인 후에 런타임 환경에서 실제 오브젝트로의 참조로 연동해주는 컴포넌트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 해시맵을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 문제는 해시맵이 꽉찬 상태로 추가하려고 하면 용량이 두배(기본 16MB &amp;gt; 32MB)로 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 급증은 로드된 번들의 안에 포함된 오브젝트 수와 관련있다. 사용되지 않아도 엔트리에 기여하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종속성도 조심해야한다. 작은 번들하나를 로드했는데, 그 번들과 디펜더시가 걸려있는 번들이 크기가 큰 경우도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;399&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uKnR1/btsOeAZ5Wpt/uXgwVJRmqnApL5f7aoL1Ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uKnR1/btsOeAZ5Wpt/uXgwVJRmqnApL5f7aoL1Ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uKnR1/btsOeAZ5Wpt/uXgwVJRmqnApL5f7aoL1Ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuKnR1%2FbtsOeAZ5Wpt%2FuXgwVJRmqnApL5f7aoL1Ak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;399&quot; height=&quot;273&quot; data-origin-width=&quot;399&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 어떻게 해결할 것인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Concurrent Content Grouping&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 에셋 번들을 구성할 때 사용되지 않는 에셋이 포함되는 것을 최소화 하는 것을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 즉, 같은 시점에 사용되는 에셋만 같은 번들로 묶는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 언제나 이게 가능한 것이 아니기 때문에 트레이드 오프가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 프로파일러를 사용하여 어떤 영향을 미치는지 확인하기 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AssetReference, AssetLabelReference&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 고정된 string으로 에셋을 로드하는 경우 휴먼에러에 취약하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 에셋레퍼런스를 인스펙터에 할당하고 이를 사용하면 된다. 다만 타입을 제한하지 않기 때문에 주의 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 에셋라벨레퍼런스도 동일하게 사용가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- AssetReferenceGameObject 등 미리 특수타입으로 제한한 클래스로 만들어진 경우에는 그걸 사용하고, 없는 경우에는 직접 만들 수 있다. 예를들어 AudioClip은 미리 생성된 클래스가 없기 때문에 아래와 같이 만들 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748336282585&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[System.Serializable]
public class AssetReferenceAudioClip : AssetReferenceT&amp;lt;AudioClip&amp;gt;
{
    public AssetReferenceAudioClip(string guid) : base(guid) {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Unity/영상 필기</category>
      <author>맨텀</author>
      <guid isPermaLink="true">https://mentum.tistory.com/970</guid>
      <comments>https://mentum.tistory.com/970#entry970comment</comments>
      <pubDate>Tue, 27 May 2025 19:59:29 +0900</pubDate>
    </item>
    <item>
      <title>유니티 프리팹 애니메이션 Prefab Transform Serialize Revert</title>
      <link>https://mentum.tistory.com/969</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Animator 샘플링의 Serialize 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 작업해놓은 애니메이션 컨트롤러는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 커스텀 인스펙터에서 Slider로 애니메이션을 조정함&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 조정된 슬라이더의 타임을 따라 애니메이션대로 실제 애니메이션이 재생됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의 기능을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spine 기반으로 작업했던 것을 포팅하는 과정에서 시리얼라이즈 관련 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spine의 경우 별도 처리 없어도 Transform값이 시리얼라이즈되지 않았던 것으로 기억하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transform 값이 전부 시리얼라이즈 되서 gif diff 에 길게 나타나고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3D 모델링의 경우 다음의 코드로 애니메이션을 강제 샘플링할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1747131474982&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;float normalizedTime = (time / clip.length);
_animator.Play(stateName, 0, normalizedTime);
_animator.Update(0f); // 즉시 반영&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스테이트 명을 기반으로 overrideAnimator 에서 clip을 가져오는 에디터 코드를 만드는 부분이 껄끄러웠던 것은 뒤로하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법으로 재생한 경우 Transform이 마지막 포즈대로 저장되버리는 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 기능상 문제는 아니지만, 모든 본에 Transform override가 발생하는 것이니 프리팹의 텍스트파일로서의 용량이 커지게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결법은?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 PrefabStage 에서 처리를 해줘야 된다는 것은 확실.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 생각해보면 방법은 3개정도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 애초에 저장되지 않는 내장 프리뷰 기능(Animation Window)를 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 리플렉션을 통해 접근할 수 있다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 애니메이션 재생할 때 메쉬를 바꿔치기해서 hideflag를 저장하지않게 설정 후, 닫을 때 원본을 다시 활성화 하는건?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 실제 오브젝트가 아니니 별도로 메쉬나 파티클을 본에 붙일 수 없게된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 시리얼라이즈된 트랜스폼 오버라이드를 전부 되돌리기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 간단해보인다. 다만 본 아래에 별도로 메쉬나 파티클을 부착하는 경우 그것도 Transform의 override니 문제가 될 수 있겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번은 기능이 제한되고, 1번은 리플렉션이 까다로우니 가장 간단해 보이는 3번으로 진행.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리팹 스테이지에서 처리를 하면 되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 OnPrefabStageClosing를 시도해봤는데 콜백이 호출되는 시점에 값을 변경해도 저장되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장과 관련있는 PrefabStage.prefabSaving 의 콜백에서 시도해보니 정상 적용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1747131998508&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[InitializeOnLoad]
public static class Editor_PrefabStage_AnimationController
{
    static Editor_PrefabStage_AnimationController()
    {
        PrefabStage.prefabSaving += OnPrefabSaving;
    }

    static void OnPrefabSaving(GameObject stage)
    {
        // 내용을 채우면 된다.
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 Mesh나 Particle을 붙이는 경우가 있으니 예외처리만 더해서 끝내면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체코드는 실제 프로젝트에 사용한 코드라서 제네릭 하지 않으니 주의.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체코드&lt;/h2&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1747132217671&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;using KindHero.Battle;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;

/// &amp;lt;summary&amp;gt;
/// 프리팹을 닫을 때, 애니메이션 프리뷰 때문에 발생한 Transform 오버라이드 변경을 되돌립니다.
/// &amp;lt;/summary&amp;gt;
[InitializeOnLoad]
public static class Editor_PrefabStage_AnimationController
{
    static Editor_PrefabStage_AnimationController()
    {
        //PrefabStage.prefabStageClosing += OnPrefabStageClosing; // Closing 콜백 시점은 저장이 되지않는다.
        PrefabStage.prefabSaving += OnPrefabSaving;
    }

    static void OnPrefabSaving(GameObject stage)
    {
        if (stage == null) return;
        if (!stage.TryGetComponent(out UnitController unitController)) return;
        if (unitController == null) return;
        if (unitController.UnitAnimControl == null) return;
        if (unitController.UnitAnimControl.Animator == null) return;
        if (unitController.UnitAnimControl.Animator.gameObject == null) return;

        RemoveOverridesRecursive(unitSpineAnimation.UnitAnimControl.Animator.gameObject);
    }

    static void RemoveOverridesRecursive(GameObject obj)
    {
        foreach (var t in obj.GetComponentsInChildren&amp;lt;Transform&amp;gt;(true))
        {
            if (PrefabUtility.IsPartOfPrefabInstance(t.gameObject))
            {
                var propertyModifications = PrefabUtility.GetPropertyModifications(t);
                if (propertyModifications != null)
                {
                    if (t.TryGetComponent(out ParticleSystem _)) continue;
                    if (t.TryGetComponent(out Renderer _)) continue;

                    var filtered = new System.Collections.Generic.List&amp;lt;PropertyModification&amp;gt;();
                    foreach (var mod in propertyModifications)
                    {
                        if (mod.propertyPath.StartsWith(&quot;m_LocalPosition&quot;) ||
                            mod.propertyPath.StartsWith(&quot;m_LocalRotation&quot;) ||
                            mod.propertyPath.StartsWith(&quot;m_LocalScale&quot;))
                        {
                            continue; // 제거 대상이므로 포함 안 함
                        }

                        filtered.Add(mod); // 유지할 것만 남김
                    }

                    if (filtered.Count != propertyModifications.Length)
                    {
                        // 프리팹 스테이지에서는 일반적인 Prefab Instance가 아니라 prefab 원본 편집 모드이기 때문에
                        // SetPropertyModifications는 잘 작동하지 않을 수 있습니다.
                        // PrefabUtility.SetPropertyModifications(t, filtered.ToArray());

                        PrefabUtility.RevertObjectOverride(t.transform, InteractionMode.AutomatedAction);
                        PrefabUtility.RevertObjectOverride(t.transform, InteractionMode.UserAction);
                        //Debug.Log(&quot;RevertObjectOverride: &quot; + t.gameObject.name);
                    }
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Unity/유니티 프로그래밍</category>
      <author>맨텀</author>
      <guid isPermaLink="true">https://mentum.tistory.com/969</guid>
      <comments>https://mentum.tistory.com/969#entry969comment</comments>
      <pubDate>Tue, 13 May 2025 19:35:39 +0900</pubDate>
    </item>
    <item>
      <title>[영상 필기][Unite Seoul 2025] Unity 프로젝트 개발 시 반드시 체크해야 할 최적화 관련 기능 공유</title>
      <link>https://mentum.tistory.com/968</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Unite&amp;nbsp;Seoul&amp;nbsp;2025]&amp;nbsp;Unity&amp;nbsp;프로젝트&amp;nbsp;개발&amp;nbsp;시&amp;nbsp;반드시&amp;nbsp;체크해야&amp;nbsp;할&amp;nbsp;최적화&amp;nbsp;관련&amp;nbsp;기능&amp;nbsp;공유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/oj87uQp5waA?si=u1-AO2HhuEqEGpmD&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/oj87uQp5waA?si=u1-AO2HhuEqEGpmD&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=oj87uQp5waA&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/uBKc4/hyYThAYVr5/llAGp8j90Uabn8kc5aFxU0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/vKHyw/hyYVbAjPu0/KixjikpKMsnrgKurgvjMjK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;[Unite Seoul 2025] Unity 프로젝트 개발 시 반드시 체크해야 할 최적화 관련 기능 공유&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/oj87uQp5waA&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 쉐이더의 높은 메모리 점유&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1) Shader Variant&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 쉐이더에 키워드가 추가될 때 마다 2의 지수형태로 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 키워드가 켜진 쉐이더, 안켜진 쉐이더를 각각 경우의 수로 빌드를 해야되니 배리언트가 증가하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 특히 에셋스토어에서 구매한 쉐이더의 경우 주의할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2) Dynamic Shader loading&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 Dynamic Shader loading 이라는 기능이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Chunk 단위 variants 압축/해제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Player Settings &amp;gt; Shader Variant Loading Settings&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://unity.com/kr/blog/engine-platform/2021-lts-improvements-to-shader-build-times-and-memory-usage&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://unity.com/kr/blog/engine-platform/2021-lts-improvements-to-shader-build-times-and-memory-usage&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기본값은 청크갯수가 0으로 설정(비활성화)되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 청크 갯수를 늘리고 청크 사이즈는 작은 값부터 시작해서 테스트 해보길 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-3) Shader Variants Stripping&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Editor Option에서 Project Settings &amp;gt; Graphics 에서 Shader Stripping 설정이 있으니 게임 내에서 사용하는 피쳐를 제한하여 배리언츠를 줄이도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- #pragma 지시문을 점검하자. multi_compile 로 지정된 것이 쉐이더 배리언츠를 늘리는 주 원인이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모든 것을 다 했는데도 배리언츠의 사용량이 높다면 &lt;b&gt;직접 스트리핑&lt;/b&gt;을 하는 것이 중요해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Play log 에서 실제 사용하는 variant의 정보를 추출하고 IPreprocessShaders 를 구현하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://unity.com/blog/engine-platform/shader-variants-optimization-troubleshooting-tips&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://unity.com/blog/engine-platform/shader-variants-optimization-troubleshooting-tips&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-4) Unity Subsystems&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- PersistentManager.Remapper&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; Instance ID와 File GUID, Local ID Mapping 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- SerializedFile&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 에셋 번들 메타 데이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &lt;a href=&quot;https://unity.com/kr/blog/technology/tales-from-the-optimization-trenches-saving-memory-with-addressables&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://unity.com/kr/blog/technology/tales-from-the-optimization-trenches-saving-memory-with-addressables&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 둘 다 에셋 번들 때문에 커지는 문제가 있다. 장면에 필요한 모든 에셋을 포함한 번들을 만드는 것은 현실적으로 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 최소한으로 필요한 번들들을 로드해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- AssetBundle.Unload(false); 로 에셋 번들을 내리면 관련된 인스턴스가 같이 내려가기 때문에 Custom AssetBundle Provider 를 만들어서 제어하는 것도 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-5) IL2CPP metadata&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 메모리 프로파일러에서 Virtual Machine 라는 이름의 영역으로 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 일부 고정 크기, 일부 실행 중 크기가 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 전체 크기를 알기위해서는 빌드 옵션에서 Platform Settings 에 Script Debugging 옵션을 켜주게 되면 프로파일러에서 il2cpp 필터링을 켜서 확인할 수 있다. (물론 디버그 정보를 포함해서 실제보다 더 커진다)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 최적화하기 위해서 3가지 고려사항이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Player옵션의 Optimization &amp;gt; Managed Stripping Level &amp;gt; Medium 으로 설정하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 많은 개발사들이 이 옵션을 올렸다가 실제 사용하는 코드가 삭제되버려서 사용하지 않는 경향이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; link.xml 을 사용하여 네임스페이스를 추가할 수 있으니 이를 설정하면 Medium 이상을 사용하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; High랑 Medium의 차이가 20% 내외라서 Medium을 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Player옵션의 IL2CPP Code Generation - Faster (Smaller) Builds 을 사용하면 제네릭 코드의 양을 줄일 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Pacakage Manager에서 불필요한 패키지와 플러그인 제거하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 모듈의 사이즈는 Library/Bee/artifacts/Platform/ManagedStripped 에서 확인하고 지우는 것을 고려하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-6) Memory Fragmentation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 잦은 작은 할당에 의해 메모리 단편화가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 필요한 최대 메모리를 미리 할당하여 재사용하고 임시할당은 최소화 하는게 현실적인 대안이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 텍스트 데이터를 파싱할 때 단편화가 급격하게 일어나는 현상이 많으니 주의하길.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Object.name 같은 복사본 반환 멤버 주의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. CPU Stuttering&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1) Unity Profiler - CPU module&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 유니티 프로파일러는 태그를 수집하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Depp profiler 를 쓰지 않더라도 call stack 옵션을 켜서 확인 가능하니 참고바람.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2) Instantiate&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 복잡한 인스턴스를 생성할 때 발생하는 히칭현상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Serialized field 규모에 비례한 CPU 부하&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;생성 시점에 분리됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Instantiate도 Object.InstantiateAsync 로 비동기로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 생성은 Produce(비동기), Copy(비동기), Awake(동기) 3단계로 구분되는데, 앞 두 단계가 deserialize되면서 굉장히 부하가 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3) GC.Collect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- GC allocation을 최소화해서 GCCollect를 최대한 줄이는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- GarbageCollector.GCMode 를 사용해서 GCCollect 자체를 아예 비활성화 할 수 있지만, 추천하긴 어려움&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 주요 CPU 부하 요인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1) 렌더링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 그릴 때는 SRP Batcher나 Instacing을 사용하는게 중요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Culling을 통해 그릴 물체를 사전에 제거하는 것도 유효&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - Camera.layerCullDistances,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - Light.layerShadowCullDistances,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - LOD culled&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - CullingGroup의 사용 (하지만 그림자문제가 있을 수 있다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2) UGUI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 당연하지만 UI는 동적 요소(Animator, Scroll)와 정적 요소를 분리하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- UI에서 Layout System을 사용하는 경우 부하를 Additional Layout Update라는 이름으로 프로파일러에 표시되니 확인하고 최소화하거나 비활성화 하는게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- RectMask2D 는 컬링할 때 CPU 부하가 꽤 있는 편. 특히 Text 포함시 급격한 증가&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;3-3) Animator&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기본적으로 애니메이터는 잡시스템을 사용해서 효율적으로 동작하게 설계되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 하지만 특정 이벤트를 사용하면 메인 쓰레드에서 돌아가도록 강제된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 애니메이터자체로도 부하가 있기 때문에 작은 클립 (애니메이션 커브가 400 이하)만 재생할 꺼라면 애니메이터 보다는 레거시Animation 컴포넌트를 사용하는게 성능적으로 이득이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 애니메이터가 씬에 많은 경우 한 프레임에 다같이 업데이트하는건 부하가 클 수 있어서 playable을 사용해서 그룹 단위로 묶어서 잡시스템으로 Animator.Update 를 활용해 재생할 수 도 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-4) Skninning&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 씬에 스킨드메쉬가 많은 경우 부하가 걸릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;- Quality Settings &amp;gt; Skin weight or SkinnedMeshRenderer &amp;gt; Quality 에서 최대 영향을 주는 본 갯수를 제한 해서 성능을 올릴 수 있다. ( 스킨드메쉬렌더러 컴포넌트에서도 개별 설정 가능 )&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp; Skinned Mesh Renderer &amp;gt; UpdateWhenOffscreen 이 기본으로 켜져있는데, 비활성화 하면 안보일때 메쉬를 업데이트 하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Player Settings &amp;gt; GPU Skinning 에서 CPU, GPU, GPU (Batched) 를 설정할 수 있는데, GPU-Batched는 대상이 적을 경우 병합처리가 부하가 걸려 오히려 불리하다 (Static 배칭처럼)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-5) 카메라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 활성된 Camera를 최소화하자. UICamera 대신 Screen overlay를 사용할 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Camera의 목적에 맞는 SRP asset 지정하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-6) Multiple FixedUpdate&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 FixedTimeUpdate는 이전 프레임에서 렉이 발생했을때 현재 프레임에서 보상하기 위해 여러번 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 TimeStep을 Update와 동일하게 맞춰주거나, (기본값은 FixedTimeStep이 0.02라서 50프레임 기준이다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트로 수동호출하는 방식을 고려할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 세팅 점검하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Project Settings &amp;gt; Time &amp;gt; FixedTimeStep&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Project Settings &amp;gt; Physics &amp;gt; Simulation Mode&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.7) 레이캐스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 레이캐스트는 수집한다음 필터하는 방식으로 구현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기본 distance는 무한대로 설정되어있기 때문에 꼭 적절한 값으로 수정하여 사용하기 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 레이어마스크를 통해 수집되는 대상을 줄이도록 하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.8) Renderer의 Bounding Volume 갱신 부하&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 트랜스폼은 잡시스템을 통해 갱신되지만 root Transform 단위로 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모든 객체가 하나의 단일 루트를 가지고 있다면 비효율적으로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 변경이 작은 경우 rootTransform 을 적절하게 분산하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 트랜스폼 변경이 필요없는 경우 vertex shader에서 변환하는 편이 부하가 적다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. GPU 부하 요인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1) Pixel overdraw&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- URP Rendering Debugger &amp;gt; Overdraw Mode 를 통해 오버드로우를 디버깅 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 파티클 등은 투명한 영역이 생각보다 많은데 투명 영역이 제거된 meshf를 사용하여 최적화하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- UI의 캔버스 렌더러에는 Canvas.cullTransparentMesh 옵션이 있다. 알파가 0인경우 UI요소를 그리지 않는 것. 최신버전에서는 기본값으로 켜져있는데, 이전버전에서는 꺼져있었기 때문에 이전버전에서 마이그레이션을 한 경우라면 별도로 체크해줘야한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Unity/영상 필기</category>
      <author>맨텀</author>
      <guid isPermaLink="true">https://mentum.tistory.com/968</guid>
      <comments>https://mentum.tistory.com/968#entry968comment</comments>
      <pubDate>Tue, 13 May 2025 11:15:36 +0900</pubDate>
    </item>
    <item>
      <title>유니티 어드레서블 + 빌드시 Shader Variant가 너무 오래 걸릴 때 체크사항</title>
      <link>https://mentum.tistory.com/966</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니티에서 어드레서블을 사용하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 시작 씬은 어드레서블에 등록하지 않고 나머지 씬과 에셋만 어드레서블로 등록하여 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에서는 시작씬에서 어드레서블로 씬과 에셋을 로딩하는 방식으로 구현하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 어느 시점에 빌드를 해보니 빌드시 Shader Variant가 수십만개를 생성하는 문제가 발생했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 시간이 5분에서 2시간 이상으로 증가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 크기는 260MB에서 600MB로 증가했다. 이 중 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;URP Lit는 188MB 나 되는 크기가 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인은?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 Full 3D인데, 아직 시작씬에 넣을 타이틀 이미지가 없어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토이기도 하고, 임시로 3D 배경 오브젝트의 프리팹을 어드레서블이 아닌 시작씬에 배치했고, 이게 화근이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어드레서블로 지정된 프리팹의 경우 약한 참조를 가지며, 언제든 업데이트가 되어 다른 에셋으로 교체가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니티는 이를 고려하여 한건지 어드레서블이 아닌 씬에 어드레서블이 프리팹이 참조된 경우 내부에서 어떤 쉐이더를 사용했는지는 알고 있지만 어떤 키워드의 배리언트를 사용했는지는 파악하지 못하는 듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 가능한 모든 배리언트를 빌드시생성해두는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 비 어드레서블 씬에서 어드레서블 배경 프리팹을 직접 포함하지 말자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 어드레서블 프리팹이 필요하다면 씬에 직접 포함하지말고 시작한 뒤에 어드레서블 시스템을 통해 불러오자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비슷한 현상 참고글&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://discussions.unity.com/t/addressable-and-shader-variants/841836?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://discussions.unity.com/t/addressable-and-shader-variants/841836?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;</description>
      <category>  Unity/유니티 엔진 동작 관련</category>
      <author>맨텀</author>
      <guid isPermaLink="true">https://mentum.tistory.com/966</guid>
      <comments>https://mentum.tistory.com/966#entry966comment</comments>
      <pubDate>Thu, 8 May 2025 15:27:31 +0900</pubDate>
    </item>
    <item>
      <title>Unity URP Render Texture not showing in UI on android build</title>
      <link>https://mentum.tistory.com/965</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. RenderTexture depth stencil Format 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 빌드에서는 RenderTexture에 설정된 depth stencil Format이 32비트일 때 보이지 않는 현상이 있다고 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;D16_UNORM 으로 변경&lt;/b&gt;한다. (24도 괜찮다고 한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. addressable 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하지만 안드로이드 모바일 게임이면 어드레서블을 사용할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 UI에서만 안보이는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 카메라와 UI 그리고 &lt;b&gt;렌더텍스쳐가 전부 어드레서블로 등록되어 있는지 확인&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;렌더텍스쳐가 어드레서블에 등록되어 있지 않은 경우 카메라와 UI의 어드레서블에 각각 분리되어 복사되기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 텍스쳐가 아니게 된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Unity/버그 및 오류 해결</category>
      <author>맨텀</author>
      <guid isPermaLink="true">https://mentum.tistory.com/965</guid>
      <comments>https://mentum.tistory.com/965#entry965comment</comments>
      <pubDate>Thu, 24 Apr 2025 16:40:00 +0900</pubDate>
    </item>
    <item>
      <title>유니티 애니메이션 재생 중인지 확인</title>
      <link>https://mentum.tistory.com/963</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애니메이션 재생 중인지 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Animator에서&amp;nbsp;&lt;s&gt;GetCurrentAnimatorStateInfo(레이어번호)&lt;/s&gt;를&amp;nbsp;사용하여&amp;nbsp;&lt;s&gt;AnimatorStateInfo&lt;/s&gt;를&amp;nbsp;가져온&amp;nbsp;뒤,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를&amp;nbsp;활용해&amp;nbsp;원하는&amp;nbsp;애니메이션이&amp;nbsp;맞는지&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hash 값을 사용할 경우 &amp;rarr; &lt;s&gt;shortNameHash&lt;/s&gt;를 비교&lt;/li&gt;
&lt;li&gt;문자열로 확인할 경우 &amp;rarr; AnimatorStateInfo의 &lt;s&gt;IsName(애니메이션 이름)&lt;/s&gt; 함수 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트랜지션 중이라면&amp;nbsp;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 애니메이션: &lt;s&gt;GetCurrentAnimatorStateInfo(0)&lt;/s&gt;&lt;/li&gt;
&lt;li&gt;전환&amp;nbsp;중인&amp;nbsp;애니메이션:&amp;nbsp;&lt;s&gt;GetNextAnimatorStateInfo(0)&lt;/s&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;둘&amp;nbsp;중&amp;nbsp;하나에서&amp;nbsp;원하는&amp;nbsp;애니메이션을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;나는 특정 애니메이션이 아닌 현재 어떤 애니메이션이든 재생 중인지 확인하고 싶어 아래와 같이 구현했다.&lt;/p&gt;
&lt;pre id=&quot;code_1739425915295&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public bool IsAnimationPlaying(int hash)
{
    AnimatorStateInfo currentInfo = _animator.GetCurrentAnimatorStateInfo(0);
    if (currentInfo.shortNameHash == hash)
        return true;

    AnimatorStateInfo nextInfo = _animator.GetNextAnimatorStateInfo(0);
    if (nextInfo.shortNameHash == hash)
        return true;

    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션은&amp;nbsp;재생을&amp;nbsp;요청한&amp;nbsp;다음&amp;nbsp;프레임에서&amp;nbsp;갱신되므로,&amp;nbsp;반드시&amp;nbsp;한&amp;nbsp;프레임&amp;nbsp;이상&amp;nbsp;대기한&amp;nbsp;후&amp;nbsp;체크해야&amp;nbsp;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;Play, &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;CrossFade(0f), CrossFade(0.1f) 전부 다 같은 프레임에서는 검출이 보장되지 않는다.&amp;nbsp;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내&amp;nbsp;경우,&lt;b&gt;&amp;nbsp;BT&amp;nbsp;노드가&amp;nbsp;시작될&amp;nbsp;때&amp;nbsp;애니메이션을&amp;nbsp;재생&lt;/b&gt;하고, &lt;br /&gt;&lt;b&gt;Update에서&amp;nbsp;해당&amp;nbsp;애니메이션이&amp;nbsp;재생&amp;nbsp;중이&amp;nbsp;아니면&amp;nbsp;실패&lt;/b&gt;를&amp;nbsp;반환하도록&amp;nbsp;설계했다. &lt;br /&gt;&lt;br /&gt;이를&amp;nbsp;위해&amp;nbsp;한&amp;nbsp;프레임을&amp;nbsp;대기할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;별도의&amp;nbsp;플래그를&amp;nbsp;추가했다.&lt;/p&gt;
&lt;pre id=&quot;code_1739426226514&quot; class=&quot;csharp&quot; data-ke-language=&quot;csharp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 해당 애니메이션이 재생중이거나 트랜지션 중이지 않음 
// 재생한 같은 프레임에서는 검출 안되니 주의
if (_didFirstFrame &amp;amp;&amp;amp; !AnimatorController.IsAnimationPlaying(_animationHash))
    return TaskStatus.Failure;

_didFirstFrame = true;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  Unity/애니메이션, 연출</category>
      <author>맨텀</author>
      <guid isPermaLink="true">https://mentum.tistory.com/963</guid>
      <comments>https://mentum.tistory.com/963#entry963comment</comments>
      <pubDate>Thu, 13 Feb 2025 14:59:37 +0900</pubDate>
    </item>
  </channel>
</rss>