<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>치킨 날다</title>
    <link>https://redchiken.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 19 Jun 2026 10:10:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>RedChiken</managingEditor>
    <item>
      <title>[회고] Common UI, UMG ViewModel 사용 후기</title>
      <link>https://redchiken.tistory.com/397</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Common UI, UMG ViewModel 플러그인을 근 2달 가량 RnD 한 후기를 두서없이 간략하게 남겨봅니다.&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;Common UI&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티 플랫폼, 멀티 컨트롤러에 대해 크게 강조를 하였지만 실상은 그다지 효과적이지는 않음.&lt;/li&gt;
&lt;li&gt;물론 PC-콘솔 멀티 플랫폼에서는 할만 함.&lt;/li&gt;
&lt;li&gt;하지만 모바일이 들어가면 답이 없어 짐.&lt;/li&gt;
&lt;li&gt;Common UI가 소개하는 기능 대부분이 InputAction을 UI에 Bind하는 것.&lt;/li&gt;
&lt;li&gt;아직 UI Event에 InputAction Bind는 제대로 동작 안함.&lt;/li&gt;
&lt;li&gt;게다가 UI의 Focus를 강력하게 체크하는데 모바일은 특성 상 UI의 터치, 드래그 등의 조작이 빈번함.&lt;/li&gt;
&lt;li&gt;이것저것 편의 기능을 만들었지만 아직 실무에서 활용하기에는 부족함이 큼.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UMG ViewModel&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel의 근간이 되는 FieldNotify는 UE5 순정 기능.&lt;/li&gt;
&lt;li&gt;MVVMViewModelBase에 선언 된 매크로와 SetProperty 템플릿 함수만 따로 분리해서 사용하는 것도 충분히 좋음.&lt;/li&gt;
&lt;li&gt;ViewModel Creation 방식이 제한적임.&lt;/li&gt;
&lt;li&gt;가장 무난하게 사용 가능한게 Global Context, 그 다음이 Manual.&lt;/li&gt;
&lt;li&gt;Resolve, Property Path는 구현이 덜 되었거나 제대로 분석이 안되어 사용 불가.&lt;/li&gt;
&lt;li&gt;Creation은 사용 가능하나 현실적인 이유로 사용 불가.&lt;/li&gt;
&lt;li&gt;현실적인 이유: ViewModel을 BP로 안 만든다.&lt;/li&gt;
&lt;li&gt;UMG ViewModel의 Bind가 BP에서 이루어 지지만, Model과 View의 중간자인 만큼 생성과 데이터 입력은 코드에서 일어날 가능성이 높음.&lt;/li&gt;
&lt;li&gt;그런데 Native Class에서 선언한 ViewModel 변수를 Bind 받지 못함.&lt;/li&gt;
&lt;li&gt;BP 함수로 Set을 명시적으로 해줘야 함.&lt;/li&gt;
&lt;li&gt;이 지점에서 일단 사용성이 기대한 것만큼 안 나옴.&lt;/li&gt;
&lt;li&gt;여기에 ViewModel이 모두 UObject이므로 LifeCycle을 조절해줘야 함.&lt;/li&gt;
&lt;li&gt;이 부분이 ViewModel을 순정으로 사용 못하고 상당한 별도의 아키텍쳐 설계를 요구함.&lt;/li&gt;
&lt;li&gt;즉, 어느정도 Utility가 있어야 사용 가능함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 2가지를 모두 섞어서 쓰다가 Common UI는 아직 시기상조이지만 UMG ViewModel은 편의기능만 만들면 쓰기 좋겠다 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 개발자 입장에서는 View에 해당하는 Native C++ 작업을 ViewModel 작업으로 대체.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel이 제공하는 정보의 종류만 공유가 되면 아티스트와 완전한 병행 작업이 가능하기 때문에 꼭 쓰고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 한국에서는 아직 대다수의 구성원들이 업무를 좁게 해석하더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자이너들도 BP 작업을 완강히 거절하였고, 아티스트도 시각디자인이 아닌 작업을 할 이유를 못 찾겠다 하네요....&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;혹 이 Plugin에 관심을 가지시는 분들이 계시다면 지나가면서 살짝 보시고 RnD 해보시기 바랍니다.&lt;/p&gt;</description>
      <category>UE5/UI</category>
      <author>RedChiken</author>
      <guid isPermaLink="true">https://redchiken.tistory.com/397</guid>
      <comments>https://redchiken.tistory.com/397#entry397comment</comments>
      <pubDate>Sat, 28 Sep 2024 03:12:15 +0900</pubDate>
    </item>
    <item>
      <title>[UI] Common UI FAQ</title>
      <link>https://redchiken.tistory.com/396</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Common UI의 사용 여부 판단&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주로 다음 상황에서 Common UI 사용이 권장된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 Multiple Layer UI를 제공해야 하는 경우&lt;/li&gt;
&lt;li&gt;Cross-Platform을 지원하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;반대로 이 두 케이스에 모두 해당되지 않으면 Common UI를 사용하지 않아도 무방하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RTS 게임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이와 별개로, WidgetComponent를 사용하여 배치된 Widget은 가급적 Common UI 사용을 지양하는 것이 좋다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 WidgetComponent라 함은 3D 월드 상에 표기되는 Widget들을 지칭한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐릭터 닉네임, 체력바, 상태 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Common UI는 Cursor-Focus Search, Activate Order, Paint Order(LayerID)에 의존한다.&lt;/li&gt;
&lt;li&gt;때문에 2D 게임 HUD도 처리 할 수는 있지만, Game World에 배치 된 Widget과 함께 작동하지는 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Keyboard와 Mouse 입력을 동시에 처리하는 방법&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 Common UI는 Mouse와 Keyboard의 입력을 동시에 지원하지는 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 동시에 지원하게 되면 UI 입장에서는 Mouse가 2개인 것처럼 느껴진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHG3ys/btsIs5xRztN/dtpZQzAKKcxu3V55tfb0qK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHG3ys/btsIs5xRztN/dtpZQzAKKcxu3V55tfb0qK/img.jpg&quot; data-alt=&quot;붕괴 스타레일은 PC에서 마우스 커서를 띄우기 위해서는 ALT 키를 누르고 있어야만 한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHG3ys/btsIs5xRztN/dtpZQzAKKcxu3V55tfb0qK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHG3ys%2FbtsIs5xRztN%2FdtpZQzAKKcxu3V55tfb0qK%2Fimg.jpg&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;640&quot; height=&quot;296&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;붕괴 스타레일은 PC에서 마우스 커서를 띄우기 위해서는 ALT 키를 누르고 있어야만 한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 간단한 방법은 입력을 Toggle로 관리하는 것이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보편적인 상황에서는 Keyboard Input으로 메뉴를 띄운다.&lt;/li&gt;
&lt;li&gt;특정 키를 입력하고 있으면 Mouse Cursor가 노출된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 때, 모든 키보드 입력은 막히게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mouse Cursor가 노출되고 있는 동안에는 마우스 커서만으로 입력을 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;UI 성능에 영향을 미치는 Tick이나 일시정지를 우회하는 방법&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI는 LocalPlayerSubsystem으로 동작하여 게임이 일시정지 하면 Tick이 되지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tick이 멈추면 CommonBoundActionBar를 포함한 Common UI가 동작하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이를 우회하려면 관련 Actor와 Widget에 tickable when paused 옵션을 활성화 해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI의 의도된 기능이나 퍼포먼스와 상충되지 않는지 확인 후 작업하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;KeyHandler Method에서 Analog Input을 얻게 되는 이유&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 InputKey/InputAxis는 PlayerController 단위에서 작업이 통합되어 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 가짜 Input 추가를 쉽게 하고, Input Parameter를 하나로 묶어 쉽게 업데이트 하기 위함이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;UE5 이전의 코드가 있으면 Analog Input이 Key Handler Callback을 트리거하거나, 반대로 트리거 될 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI는 이러한 기능으로부터 영향을 받지 않도록 하는 작업이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;하지만 Input Pipeline 초기에 디버깅 중이라면, 이러한 교차 Trigger는 쉽게 발견할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 FCommonAnalogCursor는 Input Processor이므로 Input Pipeline 초기 부분과 상호작용한다.&lt;/li&gt;
&lt;li&gt;이 때문에 FCommonAnalogCursor::HandleKeyDownEvent에서 Corss Trigger 현상이 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Input Mode가 Menu일 때 Game Input을 받을 수 있는 이유&lt;/h2&gt;
&lt;pre id=&quot;code_1720597918102&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool UCommonUIActionRouterBase::CanProcessNormalGameInput() const
{
	if (GetActiveInputMode() == ECommonInputMode::Menu)
	{
		// We still process normal game input in menu mode if the game viewport has mouse capture
		// This allows manipulation of preview items and characters in the world while in menus. 
		// If this is not desired, disable viewport mouse capture in your desired input config.
		const ULocalPlayer&amp;amp; LocalPlayer = *GetLocalPlayerChecked();
		if (TSharedPtr&amp;lt;FSlateUser&amp;gt; SlateUser = FSlateApplication::Get().GetUser(GetLocalPlayerIndex()))
		{
			return LocalPlayer.ViewportClient &amp;amp;&amp;amp; SlateUser-&amp;gt;DoesWidgetHaveCursorCapture(LocalPlayer.ViewportClient-&amp;gt;GetGameViewportWidget());
		}
	}
	return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 함수로 인해 Game Viewport에 Mouse Capture가 있으면 Menu Mode에서의 Game Input이 가능하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 때문에 Menu에 있을 때 World 내 Preview Item과 Character를 조작할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;만약 Game Input을 받고 싶지 않다면 원하는 InputMode에서 Mouse Capture를 비활성화 하면 된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>UE5/UI</category>
      <author>RedChiken</author>
      <guid isPermaLink="true">https://redchiken.tistory.com/396</guid>
      <comments>https://redchiken.tistory.com/396#entry396comment</comments>
      <pubDate>Wed, 10 Jul 2024 16:52:21 +0900</pubDate>
    </item>
    <item>
      <title>[UI] CommonUI Technical Guide</title>
      <link>https://redchiken.tistory.com/395</link>
      <description>&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/commonui-input-technical-guide-for-unreal-engine&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/commonui-input-technical-guide-for-unreal-engine&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/input-fundamentals-for-commonui-in-unreal-engine&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/input-fundamentals-for-commonui-in-unreal-engine&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/using-commonui-with-enhnaced-input-in-unreal-engine&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/using-commonui-with-enhnaced-input-in-unreal-engine&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI의 Input System은 Cross-Platform을 대응할 뿐 아니라 복잡한 다중 Layer Menu 관리에도 용이하다.&lt;/li&gt;
&lt;li&gt;아래 내용은 Common UI Input System의 작동 방식에 대한 내용이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Gamepad Navigation using Synthetic Cursor&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI에서 Gamepad Input은 보이지 않는 Synthetic Cursor를 기반으로 동작한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;때문에 Mouse를 사용하도록 UI를 설정할 경우, Common UI가 동작함에 있어 필요한 것은 2가지 뿐이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보이지 않은 Cursor가 올바른 위치에 있을 것&lt;/li&gt;
&lt;li&gt;Mouse와 같은 Click 입력을 받을 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 구조는 Cross-Platform에서의 모든 Input을 하나의 Input Path로 간소화 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Click with Synthetic Cursor/Gamepad&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gamepad의 Accept 혹은 Default Click을 한다고 가정하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L5kGV/btsIuIPZdfR/tXkrfdzC50lw6HtNJxxdL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L5kGV/btsIuIPZdfR/tXkrfdzC50lw6HtNJxxdL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L5kGV/btsIuIPZdfR/tXkrfdzC50lw6HtNJxxdL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL5kGV%2FbtsIuIPZdfR%2FtXkrfdzC50lw6HtNJxxdL0%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;1280&quot; height=&quot;1414&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Virtual Accept Key는 대체로 EKeys::Virtual_Accept로 Mapping 된다.&lt;/li&gt;
&lt;li&gt;내부적으로 Input Flow는 GenericApplication으로부터 파생된 Platform Application에서 시작된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 Windows의 경우 FWindowsApplication을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312448&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FSlateApplication::OnKeyDown( const int32 KeyCode, const uint32 CharacterCode, const bool IsRepeat ) 
{
	FKey const Key = FInputKeyManager::Get().GetKeyFromCodes( KeyCode, CharacterCode );
	FKeyEvent KeyEvent(Key, PlatformApplication-&amp;gt;GetModifierKeys(), GetUserIndexForKeyboard(), IsRepeat, CharacterCode, KeyCode);

	return ProcessKeyDownEvent( KeyEvent );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 Input은 FSlateApplication::ProcessKeyDownEvent로 처리가 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ProcessKeyDownEvent는 Input 처리를 위해 IInputProcessor Interface를 Implement하고,&lt;br /&gt;가능하면 Input을 처리하는 Input Processor를 사용한다.&lt;/li&gt;
&lt;li&gt;Input이 처리되면, 추가 Input 처리가 막히게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&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_1720764312448&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bool FSlateApplication::ProcessKeyDownEvent( const FKeyEvent&amp;amp; InKeyEvent )
{
/*...*/
	// Analog cursor gets first chance at the input
	if (InputPreProcessors.HandleKeyDownEvent(*this, InKeyEvent))
	{
		return true;
	}
/*...*/
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FCommonAnalogCursor는 FAnalogCursor와 마찬가지로 IInputProcessor의 구현체이다.&lt;/li&gt;
&lt;li&gt;FcommonAnalogCursor는 현재 Widget의 Bound Action으로 캡처되지 않은&lt;br /&gt;Gamepad의 표준 Accept Action Input을 처리하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312449&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FCommonAnalogCursor::HandleKeyDownEvent(FSlateApplication&amp;amp; SlateApp, const FKeyEvent&amp;amp; InKeyEvent)
{
	if (IsRelevantInput(InKeyEvent))
	{
/*....*/
		if (bIsVirtualAccept &amp;amp;&amp;amp; ActionRouter.ProcessInput(InKeyEvent.GetKey(), InputEventType) == ERouteUIInputResult::Handled)
		{
			return true;
		}
		else if (!bIsVirtualAccept || ShouldVirtualAcceptSimulateMouseButton(InKeyEvent, IE_Pressed))
		{
			// There is no awareness on a mouse event of whether it's real or not, so mark that here.
			UCommonInputSubsystem&amp;amp; InputSubsytem = ActionRouter.GetInputSubsystem();
			InputSubsytem.SetIsGamepadSimulatedClick(bIsVirtualAccept);
			bool bReturnValue = FAnalogCursor::HandleKeyDownEvent(SlateApp, InKeyEvent);
/*....*/
		}
	}
	return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대신 Input을 HandleKeyDownEvent로 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312450&quot; class=&quot;rust&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FAnalogCursor::HandleKeyDownEvent(FSlateApplication&amp;amp; SlateApp, const FKeyEvent&amp;amp; InKeyEvent)
{
	if (IsRelevantInput(InKeyEvent))
	{
		FKey Key = InKeyEvent.GetKey();
/*...*/

		// Bottom face button is a click
		if (Key == EKeys::Virtual_Accept)
		{
			if (!InKeyEvent.IsRepeat())
			{
				if (TSharedPtr&amp;lt;FSlateUser&amp;gt; SlateUser = SlateApp.GetUser(InKeyEvent))
				{
					const bool bIsPrimaryUser = FSlateApplication::CursorUserIndex == SlateUser-&amp;gt;GetUserIndex();
					FPointerEvent MouseEvent(
						SlateUser-&amp;gt;GetUserIndex(),
						FSlateApplication::CursorPointerIndex,
						SlateUser-&amp;gt;GetCursorPosition(),
						SlateUser-&amp;gt;GetPreviousCursorPosition(),
						bIsPrimaryUser ? SlateApp.GetPressedMouseButtons() : TSet&amp;lt;FKey&amp;gt;(),
						EKeys::LeftMouseButton,
						0,
						bIsPrimaryUser ? SlateApp.GetModifierKeys() : FModifierKeysState()
					);

					TSharedPtr&amp;lt;FGenericWindow&amp;gt; GenWindow;
					return SlateApp.ProcessMouseButtonDownEvent(GenWindow, MouseEvent);
				}
			}

			return true;
		}
	}

	return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후, FSlateApplication에서 처리할 Synthetic Mouse Click Event를 생성한다.&lt;/li&gt;
&lt;li&gt;여기까지 진행되면 Mouse Event는 Regular Click과 유사한 Input Process를 거치고, 최종 Click을 Trigger한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;좀 더 자세한 흐름을 보고 싶다면 SButton::OnMouseButtonDown에 중단점을 찍고 디버깅을 해보길 추천한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;TroubleShooting&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI의 Synthetic Cursor Click이 예상과 다른 경우, FPointerEvent의 문제일 가능성이 높다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FPointEvent에서 올바른 User Input을 처리하지 않은 경우&lt;/li&gt;
&lt;li&gt;Synthetic Cursor가 예상 위치와 멀리 떨어져 있는 경우&lt;/li&gt;
&lt;li&gt;Click이 FSlateApplication::ProcessMouseButtonDownEvent에서 처리될 때&lt;br /&gt;Capture가 있는 다른 Widget이 FWidgetPath에 영향을 미친 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Input Config의 MouseCaptureMode를 기반으로 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;FWidgetPath가 FSlateApplication::LocateWindowUnerMouse를 사용하는 위치를 기반으로 자연 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FWidgetPath에는 Input이 Route되는 Widget List가 포함되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;How Synthetic Cursor/Gamdpad Navigate and Focus&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순 Navigation 측면에서는 Common UI와 UMG의 기본 구현 과정은 크게 다를게 없다.&lt;/li&gt;
&lt;li&gt;늘 그렇든 Input의 시작은 특정 Platform Application에서 시작한다.&lt;/li&gt;
&lt;li&gt;아래 예시는 화살표나 Analog 이동의 시나리오를 예시로 삼는다.&lt;/li&gt;
&lt;li&gt;Input Louter System은 이 UI Navigation Input을 UI 내 Widget으로 Route한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wsdmZ/btsIuQUNyLH/kKiQvmk019hnnI3A6mug9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wsdmZ/btsIuQUNyLH/kKiQvmk019hnnI3A6mug9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wsdmZ/btsIuQUNyLH/kKiQvmk019hnnI3A6mug9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwsdmZ%2FbtsIuQUNyLH%2FkKiQvmk019hnnI3A6mug9k%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;1280&quot; height=&quot;1036&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Navigation Input은 보통 SWidget::OnKeydown이나 SWidget::OnAnalogValueChanged에 의해 처리된다.&lt;/li&gt;
&lt;li&gt;하지만 이러한 기본 Method는 Widget Focus를 직접 변경하지 않고 아래와 같이 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312451&quot; class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;FReply SWidget::OnKeyDown( const FGeometry&amp;amp; MyGeometry, const FKeyEvent&amp;amp; InKeyEvent )
{
	if (bCanSupportFocus &amp;amp;&amp;amp; SupportsKeyboardFocus())
	{
		EUINavigation Direction = FSlateApplicationBase::Get().GetNavigationDirectionFromKey(InKeyEvent);
		// It's the left stick return a navigation request of the correct direction
		if (Direction != EUINavigation::Invalid)
		{
			const ENavigationGenesis Genesis = InKeyEvent.GetKey().IsGamepadKey() ? ENavigationGenesis::Controller : ENavigationGenesis::Keyboard;
			return FReply::Handled().SetNavigation(Direction, Genesis);
		}
	}
	return FReply::Unhandled();
}

FReply SWidget::OnAnalogValueChanged( const FGeometry&amp;amp; MyGeometry, const FAnalogInputEvent&amp;amp; InAnalogInputEvent )
{
	if (bCanSupportFocus &amp;amp;&amp;amp; SupportsKeyboardFocus())
	{
		EUINavigation Direction = FSlateApplicationBase::Get().GetNavigationDirectionFromAnalog(InAnalogInputEvent);
		// It's the left stick return a navigation request of the correct direction
		if (Direction != EUINavigation::Invalid)
		{
			return FReply::Handled().SetNavigation(Direction, ENavigationGenesis::Controller);
		}
	}
	return FReply::Unhandled();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FSlateApplication::GetNavigationDirectionFromKey나 FSlateApplication::GetNavigationDirectionFromAnalog를&lt;br /&gt;이용해 Input을 Navigation Direction으로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312452&quot; class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;EUINavigation FSlateApplication::GetNavigationDirectionFromKey(const FKeyEvent&amp;amp; InKeyEvent) const
{
	TSharedRef&amp;lt;FNavigationConfig&amp;gt; RelevantNavConfig = GetRelevantNavConfig(InKeyEvent.GetUserIndex());
	return RelevantNavConfig-&amp;gt;GetNavigationDirectionFromKey(InKeyEvent);
}

EUINavigation FSlateApplication::GetNavigationDirectionFromAnalog(const FAnalogInputEvent&amp;amp; InAnalogEvent)
{
	TSharedRef&amp;lt;FNavigationConfig&amp;gt; RelevantNavConfig = GetRelevantNavConfig(InAnalogEvent.GetUserIndex());
	return RelevantNavConfig-&amp;gt;GetNavigationDirectionFromAnalog(InAnalogEvent);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;이 과정에서 Widget에 대한 Navigation Config을 고려한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312452&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;/** An event should return FReply::Handled().SetNavigation( NavigationType ) as a means of asking the system to attempt a navigation*/
FReply&amp;amp; SetNavigation(EUINavigation InNavigationType, const ENavigationGenesis InNavigationGenesis, const ENavigationSource InNavigationSource = ENavigationSource::FocusedWidget)
{
	this-&amp;gt;NavigationType = InNavigationType;
	this-&amp;gt;NavigationGenesis = InNavigationGenesis;
	this-&amp;gt;NavigationSource = InNavigationSource;
	this-&amp;gt;NavigationDestination = nullptr;
	return Me();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Navigation Direction은 Capture되어 FReply::Handled에 포함된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FReply::Handled는 FReply::SetNavigation을 통해 전송된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312453&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void FSlateApplication::ProcessReply( const FWidgetPath&amp;amp; CurrentEventPath, const FReply&amp;amp; TheReply, const FWidgetPath* WidgetsUnderMouse, const FPointerEvent* InMouseEvent, const uint32 UserIndex )
{
/*...*/
	
	// If we have a valid Navigation request attempt the navigation.
	if (TheReply.GetNavigationDestination().IsValid() || TheReply.GetNavigationType() != EUINavigation::Invalid)
	{
		FWidgetPath NavigationSource;
/*...*/
		if (NavigationSource.IsValid())
		{
			if (!GSlateEnableGamepadEditorNavigation &amp;amp;&amp;amp; TheReply.GetNavigationGenesis() == ENavigationGenesis::Controller &amp;amp;&amp;amp; !NavigationSource.GetLastWidget()-&amp;gt;GetPersistentState().bIsInGameLayer)
			{
				// Gamepad navigation while not in a game layer, do nothing as specified by GSlateEnableGamepadEditorNavigation
			}
			else if (TheReply.GetNavigationDestination().IsValid())
			{
				const bool bAlwaysHandleNavigationAttempt = false;
				ExecuteNavigation(NavigationSource, TheReply.GetNavigationDestination(), UserIndex, bAlwaysHandleNavigationAttempt);
			}
			else
			{
                                TSharedRef&amp;lt;SWindow&amp;gt; NavigationWindow = NavigationSource.GetDeepestWindow();
                                FNavigationEvent NavigationEvent(PlatformApplication-&amp;gt;GetModifierKeys(), UserIndex, TheReply.GetNavigationType(), TheReply.GetNavigationGenesis());
                                FNavigationReply NavigationReply = FNavigationReply::Escape();
                                for (int32 WidgetIndex = NavigationSource.Widgets.Num() - 1; WidgetIndex &amp;gt;= 0; --WidgetIndex)
                                {
                                        FArrangedWidget&amp;amp; SomeWidgetGettingEvent = NavigationSource.Widgets[WidgetIndex];
                                        if (SomeWidgetGettingEvent.Widget-&amp;gt;IsEnabled())
                                        {
                                                NavigationReply = SomeWidgetGettingEvent.Widget-&amp;gt;OnNavigation(SomeWidgetGettingEvent.Geometry, NavigationEvent).SetHandler(SomeWidgetGettingEvent.Widget);
                                                if (NavigationReply.GetBoundaryRule() != EUINavigationRule::Escape || SomeWidgetGettingEvent.Widget == NavigationWindow || WidgetIndex == 0)
                                                {
                                                        AttemptNavigation(NavigationSource, NavigationEvent, NavigationReply, SomeWidgetGettingEvent);
                                                        break;
                                                }
                                        }
                                }
			}
		}
	}

/*...*/

	// Set focus if requested.
	TSharedPtr&amp;lt;SWidget&amp;gt; RequestedFocusRecepient = TheReply.GetUserFocusRecepient();
	if (TheReply.ShouldSetUserFocus() &amp;amp;&amp;amp; RequestedFocusRecepient.IsValid())
	{
		if (TheReply.AffectsAllUsers())
		{
			SetAllUserFocus(RequestedFocusRecepient, TheReply.GetFocusCause());
		}
		else
		{
			SlateUser-&amp;gt;SetFocus(RequestedFocusRecepient.ToSharedRef(), TheReply.GetFocusCause());
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slate가 FSlateApplication::ProcessREply를 사용하여 Reply를 처리한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 과정에서 Navigation이 발생한다.&lt;/li&gt;
&lt;li&gt;Navigation Event가 Direction에 의해 대략 정의 된 경우, FSlateApplication::AttemptNavigation은 Navigation 할 Widget을 찾으려 한다.&lt;/li&gt;
&lt;li&gt;Navigation이 가능한 경우, FSlateExecuteNavigation이 대상 Widget으로 Navigation한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;대상 Widget이 유효한 경우, FSlateApplication::SetUserFocus가 호출된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 대상 Widget이 지정 되었는지, 다른 Widget을 찾았는지와 무관하게 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312456&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void FSlateApplication::FinishedInputThisFrame()
{
	const float DeltaTime = GetDeltaTime();

	PlatformApplication-&amp;gt;FinishedInputThisFrame();
	
	// Any preprocessors are given a chance to process accumulated values (or do whatever other tick things they want)
	// after we've finished processing all of the input for the frame
	if (PlatformApplication-&amp;gt;Cursor.IsValid())
	{
		InputPreProcessors.Tick(DeltaTime, *this, PlatformApplication-&amp;gt;Cursor.ToSharedRef());
	}

/*....*/
}

void FSlateApplication::InputPreProcessorsHelper::Tick(const float DeltaTime, FSlateApplication&amp;amp; SlateApp, TSharedRef&amp;lt;ICursor&amp;gt; Cursor)
{
	TGuardValue&amp;lt;bool&amp;gt; IteratingGuard(bIsIteratingPreProcessors, true);

	for (const TSharedPtr&amp;lt;IInputProcessor&amp;gt;&amp;amp; Preprocessor : InputPreProcessorList)
	{
		if (Preprocessor)
		{
			Preprocessor-&amp;gt;Tick(DeltaTime, SlateApp, Cursor);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slate Focus Navigation이 종료된 후,&lt;br /&gt;FSlateAnalogCursor::Tick에서 자동으로 다음 Tick 도중 Synthetic Cursor를 Focused Widget의 중앙으로 이동시킨다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 통해 Gamepad에서 Hover Effect를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Customize Synthetic Cursor Behavior in Common UI&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI를 이용하면 고유한 Analogue Cursor나 Synthetic Cursor를 제공할 수 있다.&lt;/li&gt;
&lt;li&gt;Common UI를 통해 고유 Cursor를 제공하는 것에는 몇 가지 이점이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gamepad와 유사하게 작동하는 Keyboard Navigation을 만들려 하는 경우,&lt;br /&gt;Gamepad를 사용하지 않을 때에도 FCommonAnalogCursor::Tick이 Widget의 중앙에 Snap되도록 할 수 있다.&lt;/li&gt;
&lt;li&gt;Synthetic Mouse를 보이도록 하고, Focus가 달라질 때 Tween을 구현할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tween: 그래픽 상으로 두 상태나 프레임 사이를 자동으로 생성하는 Process&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Custom cursor를 만드는 방법은 다음과 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UCommonUIActionRouterBase를 상속받은 Custom ActionRouter Class 생성&lt;/li&gt;
&lt;li&gt;FCommonAnalogCursor를 상속받은 Custom AnalogCursor Class 생성&lt;/li&gt;
&lt;li&gt;Custom ActionRouter Class에서 MakeAnalogCursor를 Override하여 Custom AnalogCursor Class를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Custom Router Class에서 ShouldCreateSubsystem은 Override 된 경우 Instance를 생성하지 않는다.&lt;/li&gt;
&lt;li&gt;이러한 Input Processor를 Customize 할 때에는 한가지 주의할 점이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Input Processor는 모든 Input에서 실행 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Editor도 포함한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아래 함수를 이용하면 Application에서 발생한 Input과 그렇지 않은 것을 구분하는데 도움이 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312457&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FCommonAnalogCursor::IsGameViewportInFocusPathWithoutCapture() const
{
	if (const UGameViewportClient* ViewportClient = GetViewportClient())
	{
		if (TSharedPtr&amp;lt;SViewport&amp;gt; GameViewportWidget = ViewportClient-&amp;gt;GetGameViewportWidget())
		{
			TSharedPtr&amp;lt;FSlateUser&amp;gt; SlateUser = FSlateApplication::Get().GetUser(GetOwnerUserIndex());
			if (SlateUser &amp;amp;&amp;amp; !SlateUser-&amp;gt;DoesWidgetHaveCursorCapture(GameViewportWidget))
			{
#if PLATFORM_DESKTOP
				// Not captured - is it in the focus path?
				return SlateUser-&amp;gt;IsWidgetInFocusPath(GameViewportWidget);
#endif
				// If we're not on desktop, focus on the viewport is irrelevant, as there aren't other windows around to care about
				return true;
			}
		}
	}
	return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Input Routing&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간략하게 정리하면 다음과 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI는 ActivableWidget을 Navigation을 처리하는 Node Tree로 구성한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Inactive Widget은 추가되지 않고, 그에 따라 Router 대상으로 고려되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CommonGameviewportClient가 Input을 Capture하고, Hierarchy의 최상단에 표시되는 Node를 탐색한다.&lt;/li&gt;
&lt;li&gt;탐색된 Node에서 사용 가능 한 Input Handler를 이용해 Input을 처리할 수 있는지 확인한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Player Input과 일치한 Handler가 없는 경우 자손 Node로 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;적절한 Node를 찾을 때까지 탐색을 반복한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 Process는 모든 Node를 탐색할 때까지 반복한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312458&quot; class=&quot;rust&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FActivatableTreeNode::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const
{
	if (IsReceivingInput())
	{
		for (const FActivatableTreeNodeRef&amp;amp; ChildNode : Children)
		{
			if (ChildNode-&amp;gt;ProcessNormalInput(ActiveInputMode, Key, InputEvent))
			{
				return true;
			}
		}
		return FActionRouterBindingCollection::ProcessNormalInput(ActiveInputMode, Key, InputEvent);
	}
	return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;탐색 방식은 BFS에 가깝다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Execution Flow&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Synthetic Cursor와 마찬가지로 Input Processor는 Input Routing 발생 전에 Event를 처리할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Input Processor에서 처리하지 않는 경우, Input Event는 Common UI의 Input Routing을 진행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Click Event Process&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1379&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJPIvp/btsIxjVXO5i/rYZ3HqMkUx2xcFLXjLaX7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJPIvp/btsIxjVXO5i/rYZ3HqMkUx2xcFLXjLaX7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJPIvp/btsIxjVXO5i/rYZ3HqMkUx2xcFLXjLaX7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJPIvp%2FbtsIxjVXO5i%2FrYZ3HqMkUx2xcFLXjLaX7k%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;1280&quot; height=&quot;1379&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1379&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 Platform Application이 FSlateApplication::ProcessKeyDownEvent를 Trigger 할 때 Input이 시작된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312459&quot; class=&quot;rust&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FSlateApplication::ProcessKeyDownEvent( const FKeyEvent&amp;amp; InKeyEvent )
{
/*...*/
	// Send out key down events.
    if ( !Reply.IsEventHandled() )
    {
        Reply = FEventRouter::RouteAlongFocusPath(this, FEventRouter::FBubblePolicy(EventPath), InKeyEvent, [] (const FArrangedWidget&amp;amp; SomeWidgetGettingEvent, const FKeyEvent&amp;amp; Event)
        {
            if (SomeWidgetGettingEvent.Widget-&amp;gt;IsEnabled())
            {
                const FReply TempReply = SomeWidgetGettingEvent.Widget-&amp;gt;OnKeyDown(SomeWidgetGettingEvent.Geometry, Event);
                return TempReply;
            }
            return FReply::Unhandled();
        }, ESlateDebuggingInputEvent::KeyDown);
    }
/*...*/
	return Reply.IsEventHandled();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slate Application은 현재 Focus 경로를 기반으로 Widget에 Input Event를 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312460&quot; class=&quot;livescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;FReply SViewport::OnKeyDown( const FGeometry&amp;amp; MyGeometry, const FKeyEvent&amp;amp; KeyEvent )
{
	return ViewportInterface.IsValid() ? ViewportInterface.Pin()-&amp;gt;OnKeyDown(MyGeometry, KeyEvent) : FReply::Unhandled();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Game 내에서는 보통 SViewport::OnKeyDown Input Event이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 Input Event는 현재 Viewport Interface Implemented Class로 키 누름을 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312460&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;FReply FSceneViewport::OnKeyDown( const FGeometry&amp;amp; InGeometry, const FKeyEvent&amp;amp; InKeyEvent )
{
	// Start a new reply state
	CurrentReplyState = FReply::Handled(); 

	FKey Key = InKeyEvent.GetKey();
	if (Key.IsValid())
	{
		KeyStateMap.Add(Key, true);

		//@todo Slate Viewports: FWindowsViewport checks for Alt+Enter or F11 and toggles fullscreen.  Unknown if fullscreen via this method will be needed for slate viewports. 
		if (ViewportClient &amp;amp;&amp;amp; GetSizeXY() != FIntPoint::ZeroValue)
		{
			// Switch to the viewport clients world before processing input
			FScopedConditionalWorldSwitcher WorldSwitcher(ViewportClient);

			if (!ViewportClient-&amp;gt;InputKey(FInputKeyEventArgs(this, InKeyEvent.GetInputDeviceId(), Key, InKeyEvent.IsRepeat() ? IE_Repeat : IE_Pressed, 1.0f, false)))
			{
				CurrentReplyState = FReply::Unhandled();
			}
		}
	}
	else
	{
		CurrentReplyState = FReply::Unhandled();
	}
	return CurrentReplyState;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통은 키 누름이 전달되면 FSceneViewPort::OnKeyDown이 트리거 된다.&lt;/li&gt;
&lt;li&gt;마지막으로 SceneViewPort는 Input을 현재 GameViewportClient의 InputKey Method로 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312461&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool UCommonGameViewportClient::InputKey(const FInputKeyEventArgs&amp;amp; InEventArgs)
{
	FInputKeyEventArgs EventArgs = InEventArgs;

	if (IsKeyPriorityAboveUI(EventArgs))
	{
		return true;
	}

	// Check override before UI
	if (OnOverrideInputKey().IsBound())
	{
		if (OnOverrideInputKey().Execute(EventArgs))
		{
			return true;
		}
	}

	// The input is fair game for handling - the UI gets first dibs
#if !UE_BUILD_SHIPPING
	if (ViewportConsole &amp;amp;&amp;amp; !ViewportConsole-&amp;gt;ConsoleState.IsEqual(NAME_Typing) &amp;amp;&amp;amp; !ViewportConsole-&amp;gt;ConsoleState.IsEqual(NAME_Open))
#endif
	{		
		FReply Result = FReply::Unhandled();
		if (!OnRerouteInput().ExecuteIfBound(EventArgs.InputDevice, EventArgs.Key, EventArgs.Event, Result))
		{
			HandleRerouteInput(EventArgs.InputDevice, EventArgs.Key, EventArgs.Event, Result);
		}

		if (Result.IsEventHandled())
		{
			return true;
		}
	}

	return Super::InputKey(EventArgs);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI를 사용할 때에는 이것이 UCommonGameViewportClient::InputKey이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;때문에 Common UI를 사용하려면 GameViewportClass가 CommonViewportClient로 설정되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Action Router Process&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;1119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4WI8O/btsIvdJZotd/WU9aIKzj9yw4sBoAkVUvW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4WI8O/btsIvdJZotd/WU9aIKzj9yw4sBoAkVUvW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4WI8O/btsIvdJZotd/WU9aIKzj9yw4sBoAkVUvW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4WI8O%2FbtsIvdJZotd%2FWU9aIKzj9yw4sBoAkVUvW0%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;811&quot; height=&quot;1119&quot; data-origin-width=&quot;811&quot; data-origin-height=&quot;1119&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1720764312462&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bool UCommonGameViewportClient::InputKey(const FInputKeyEventArgs&amp;amp; InEventArgs)
{
	FInputKeyEventArgs EventArgs = InEventArgs;

	if (IsKeyPriorityAboveUI(EventArgs))
	{
		return true;
	}

	// Check override before UI
	if (OnOverrideInputKey().IsBound())
	{
		if (OnOverrideInputKey().Execute(EventArgs))
		{
			return true;
		}
	}

	// The input is fair game for handling - the UI gets first dibs
#if !UE_BUILD_SHIPPING
	if (ViewportConsole &amp;amp;&amp;amp; !ViewportConsole-&amp;gt;ConsoleState.IsEqual(NAME_Typing) &amp;amp;&amp;amp; !ViewportConsole-&amp;gt;ConsoleState.IsEqual(NAME_Open))
#endif
	{		
		FReply Result = FReply::Unhandled();
		if (!OnRerouteInput().ExecuteIfBound(EventArgs.InputDevice, EventArgs.Key, EventArgs.Event, Result))
		{
			HandleRerouteInput(EventArgs.InputDevice, EventArgs.Key, EventArgs.Event, Result);
		}

		if (Result.IsEventHandled())
		{
			return true;
		}
	}

	return Super::InputKey(EventArgs);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI의 Action Router Process는 Input이 CommonGameViewportClient에 전달되면서 시작합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312463&quot; class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UCommonGameViewportClient::HandleRerouteInput(FInputDeviceId DeviceId, FKey Key, EInputEvent EventType, FReply&amp;amp; Reply)
{
	FPlatformUserId OwningPlatformUser = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(DeviceId);
	ULocalPlayer* LocalPlayer = GameInstance-&amp;gt;FindLocalPlayerFromPlatformUserId(OwningPlatformUser);
	Reply = FReply::Unhandled();

	if (LocalPlayer)
	{
		UCommonUIActionRouterBase* ActionRouter = LocalPlayer-&amp;gt;GetSubsystem&amp;lt;UCommonUIActionRouterBase&amp;gt;();
		if (ensure(ActionRouter))
		{
			ERouteUIInputResult InputResult = ActionRouter-&amp;gt;ProcessInput(Key, EventType);
			if (InputResult == ERouteUIInputResult::BlockGameInput)
			{
				// We need to set the reply as handled otherwise the input won't actually be blocked from reaching the viewport.
				Reply = FReply::Handled();
				// Notify interested parties that we blocked the input.
				OnRerouteBlockedInput().ExecuteIfBound(DeviceId, Key, EventType, Reply);
			}
			else if (InputResult == ERouteUIInputResult::Handled)
			{
				Reply = FReply::Handled();
			}
		}
	}	
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UCommonGaameViewportClient::InputKey는 ActionLouter에&lt;br /&gt;UCommonGameViewportClient::HandleRerouteInput의 입력을 처리할 기회를 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312463&quot; class=&quot;gauss&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ERouteUIInputResult UCommonUIActionRouterBase::ProcessInput(FKey Key, EInputEvent InputEvent) const
{
/*....*/
	const auto ProcessNormalInputFunc = [Key, ActiveMode, this](EInputEvent Event)
		{
			bool bHandled = PersistentActions-&amp;gt;ProcessNormalInput(ActiveMode, Key, Event);

			if (!bHandled)
			{
				if (bIsActivatableTreeEnabled &amp;amp;&amp;amp; ActiveRootNode)
				{
					bHandled = ActiveRootNode-&amp;gt;ProcessNormalInput(ActiveMode, Key, Event);
				}

				if (!bHandled)
				{
					bHandled = ProcessInputOnActionDomains(ActiveMode, Key, Event);
				}
			}

			return bHandled;
		};

	bool bHandledInput = ProcessHoldResult == EProcessHoldActionResult::Handled;
	if (!bHandledInput)
	{
		if (ProcessHoldResult == EProcessHoldActionResult::GeneratePress)
		{
			// A hold action was in progress but quickly aborted, so we want to generate a press action now for any normal bindings that are interested
			ProcessNormalInputFunc(IE_Pressed);
		}

		// Even if no widget cares about this input, we don't want to let anything through to the actual game while we're in menu mode
		bHandledInput = ProcessNormalInputFunc(InputEvent); 
	}

	if (bHandledInput)
	{
		return ERouteUIInputResult::Handled;
	}
	return CanProcessNormalGameInput() ? ERouteUIInputResult::Unhandled : ERouteUIInputResult::BlockGameInput;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성공하면, UCommonUIActionRouterBase::ProcessInput을 호출한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Action Router는 ActivatableWidget Tree에서 현재 Active 중인 Root Node를 유지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312465&quot; class=&quot;rust&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FActivatableTreeNode::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const
{
	if (IsReceivingInput())
	{
		for (const FActivatableTreeNodeRef&amp;amp; ChildNode : Children)
		{
			if (ChildNode-&amp;gt;ProcessNormalInput(ActiveInputMode, Key, InputEvent))
			{
				return true;
			}
		}
		return FActionRouterBindingCollection::ProcessNormalInput(ActiveInputMode, Key, InputEvent);
	}
	return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ProcessInput은 Root Node에서 FActivatableTreeNode::ProcessNormalInput을 호출해&lt;br /&gt;Input 처리 시도를 지시합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 반복적으로 모든 Child Node에 ProcessNormalInput을 지시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312465&quot; class=&quot;rust&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FActionRouterBindingCollection::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const
{
	for (FUIActionBindingHandle BindingHandle : ActionBindings)
	{
		if (TSharedPtr&amp;lt;FUIActionBinding&amp;gt; Binding = FUIActionBinding::FindBinding(BindingHandle))
		{
			if (ActiveInputMode == ECommonInputMode::All || ActiveInputMode == Binding-&amp;gt;InputMode)
			{
				auto TryConsumeInput = [&amp;amp;](const FKey&amp;amp; InKey, const UInputAction* InInputAction)
				{
					// A persistent displayed action skips the normal rules for reachability, since it'll always appear in a bound action bar
					const bool bIsDisplayedPersistentAction = Binding-&amp;gt;bIsPersistent &amp;amp;&amp;amp; Binding-&amp;gt;bDisplayInActionBar;
					if (InKey == Key &amp;amp;&amp;amp; Binding-&amp;gt;InputEvent == InputEvent &amp;amp;&amp;amp; (bIsDisplayedPersistentAction || IsWidgetReachableForInput(Binding-&amp;gt;BoundWidget.Get())))
					{
						// Just in case this was in the middle of a hold process with a different key, reset now
						Binding-&amp;gt;CancelHold();

						// If injecting enhanced input. don't fire 'OnExecuteAction' since that can be manually done if desired in BP
						bool bEnhancedInputInjected = false;
						if (InInputAction)
						{
							if (TObjectPtr&amp;lt;const UCommonInputMetadata&amp;gt; CommonInputMetadata = CommonUI::GetEnhancedInputActionMetadata(InInputAction))
							{
								// Non generic actions should inject enhanced input so users can bind to enhanced input events
								if (!CommonInputMetadata-&amp;gt;bIsGenericInputAction)
								{
									FInputActionValue RawValue = FInputActionValue(true);
									CommonUI::InjectEnhancedInputForAction(GetActionRouter().GetLocalPlayerChecked(), InInputAction, RawValue);
									bEnhancedInputInjected = true;
								}
							}
						}
						
						if (!bEnhancedInputInjected)
						{
							Binding-&amp;gt;OnExecuteAction.ExecuteIfBound();
						}

						if (Binding-&amp;gt;bConsumesInput)
						{
							return true;
						}
					}

					return false;
				};


				if (CommonUI::IsEnhancedInputSupportEnabled() &amp;amp;&amp;amp; Binding-&amp;gt;InputAction.IsValid())
				{
					if (const UInputAction* InputAction = Binding-&amp;gt;InputAction.Get())
					{
						TArray&amp;lt;FKey&amp;gt; InputActionKeys;
						CommonUI::GetEnhancedInputActionKeys(GetActionRouter().GetLocalPlayerChecked(), InputAction, InputActionKeys);
						for (const FKey&amp;amp; InputActionKey : InputActionKeys)
						{
							if (TryConsumeInput(InputActionKey, InputAction))
							{
								return true;
							}
						}
					}
				}

				for (const FUIActionKeyMapping&amp;amp; KeyMapping : Binding-&amp;gt;NormalMappings)
				{
					if (TryConsumeInput(KeyMapping.Key, nullptr))
					{
						return true;
					}
				}
			}
		}
	}
	return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FActivatableTreeNode는 FActionRouterBindingCollection으로,&lt;br /&gt;Node의 ActivatableWidget에 모든 ActionBinding List를 유지한다.&lt;/li&gt;
&lt;li&gt;현재 Node 내의 모든 Child Node에서 Input을 처리하지 못하면,&lt;br /&gt;FActivatableTreeNode::ProcessNormalInput을 호출한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 때 FActivatableTreeNode는 Binding Collection으로서 현재 Node는 Widget의 모든 Action Binding을 확인한다.&lt;/li&gt;
&lt;li&gt;Action Binding이 해당 Key와 일치하는 경우, 관련 동작이 실행되고 Input이 처리된 것으로 간주된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Modify the Input Routing System&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UCommonGameViewportClient를 상속받고 모든 Input Handling Method를 Override 하라.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후 Project Setting에서 이 Class를 GameVieportClss로 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;UCommonUIActionRouterBase를 상속받고 모든 Virtual Function을 Override한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, FUIInputconfig에서 ApplyUIInputConfig를 Override 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Change UI Input Handling with Input config&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 활성화 된 Widget을 기반으로 Application의 Input Handling이 변경되어야 하는 경우가 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 Social Sidebar 혹은 Pause Menu가 열려 있을 때 Player Input을 막는다던가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이를 처리하기 위해 Common UI에서는 ActivatableWidget의 Inputconfig 옵션을 지원한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Common UI 사용에 필수적인 사안은 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Use Input Config&lt;/h3&gt;
&lt;pre id=&quot;code_1720764312467&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;/**
 * Input Config that can be applied on widget activation. Allows for input setup  (Mouse capture, 
 * UI-only input, move / look ignore, etc), to be controlled by widget activation.
 */
USTRUCT(BlueprintType)
struct COMMONUI_API FUIInputConfig
{
	GENERATED_BODY()

	ECommonInputMode GetInputMode() const { return InputMode; }
	EMouseCaptureMode GetMouseCaptureMode() const { return MouseCaptureMode; }
	EMouseLockMode GetMouseLockMode() const { return MouseLockMode; }
	bool HideCursorDuringViewportCapture() const { return bHideCursorDuringViewportCapture; }

	FUIInputConfig();
	FUIInputConfig(ECommonInputMode InInputMode, EMouseCaptureMode InMouseCaptureMode, bool bInHideCursorDuringViewportCapture = true);
	FUIInputConfig(ECommonInputMode InInputMode, EMouseCaptureMode InMouseCaptureMode, EMouseLockMode InMouseLockMode, bool bInHideCursorDuringViewportCapture = true);

	bool operator==(const FUIInputConfig&amp;amp; Other) const
	{
		return bIgnoreMoveInput == Other.bIgnoreMoveInput
			&amp;amp;&amp;amp; bIgnoreLookInput == Other.bIgnoreLookInput
			&amp;amp;&amp;amp; InputMode == Other.InputMode
			&amp;amp;&amp;amp; MouseCaptureMode == Other.MouseCaptureMode
			&amp;amp;&amp;amp; MouseLockMode == Other.MouseLockMode
			&amp;amp;&amp;amp; bHideCursorDuringViewportCapture == Other.bHideCursorDuringViewportCapture;
	}

	bool operator!=(const FUIInputConfig&amp;amp; Other) const
	{
		return !operator==(Other);
	}

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	bool bIgnoreMoveInput = false;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	bool bIgnoreLookInput = false;

	/** Simplification of config as string */
	FString ToString() const;

protected:


	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	ECommonInputMode InputMode;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	EMouseCaptureMode MouseCaptureMode;


	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	EMouseLockMode MouseLockMode;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	bool bHideCursorDuringViewportCapture = true;
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 InputConfig는 여러 Input의 상태를 추적한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mouse Capture Option&lt;/li&gt;
&lt;li&gt;이동 및 시야 축 처리&lt;/li&gt;
&lt;li&gt;Common UI 전체의 Input Mode등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312469&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;/**
 * Gets the desired input configuration to establish when this widget activates and can receive input (i.e. all parents are also active).
 * This configuration will override the existing one established by any previous activatable widget and restore it (if valid) upon deactivation.
 */
 TOptional&amp;lt;FUIInputConfig&amp;gt; UCommonActivatableWidget::GetDesiredInputConfig() const
{
	// Check if there is a BP implementation for input configs
	if (GetClass()-&amp;gt;IsFunctionImplementedInScript(GET_FUNCTION_NAME_CHECKED(UCommonActivatableWidget, BP_GetDesiredInputConfig)))
	{
		return BP_GetDesiredInputConfig();
	}

	// No particular config is desired by default
	return TOptional&amp;lt;FUIInputConfig&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ActivatableWidget을 활성화하면 GetDesiredInputConfig를 사용하여 InputConfig를 가져온다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로는 null을 반환하지만 Override가 가능하다.&lt;/li&gt;
&lt;li&gt;GetDesiredInputConfig가 Null을 반환할 때마다 Common UI는 마지막으로 유효했던 InputConfig로 되돌아간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312470&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;/**
* Controls whether a default Input Config will be set when the active CommonActivatableWidgets do not specify a desired one.
* Disable this if you want to control the Input Mode via alternative means.
*/
UPROPERTY(config, EditAnywhere, Category = &quot;Input&quot;)
bool bEnableDefaultInputConfig = true;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 Common UI는 ActivatableWidget으로 지정되지 않는 경우 예비로 InputConfig를 적용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 위 변수를 사용해 이를 비활성화 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312470&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void FActivatableTreeRoot::ApplyLeafmostNodeConfig()
{
#if WITH_EDITOR
	if (!IsViewportWindowInFocusPath(GetActionRouter()))
	{
		return;
	}
#endif
	if (FActivatableTreeNodePtr PinnedLeafmostNode = LeafmostActiveNode.Pin())
	{
		GetActionRouter().SetActiveActivationMetadata(PinnedLeafmostNode-&amp;gt;FindActivationMetadata());

		if (ensure(PinnedLeafmostNode-&amp;gt;IsReceivingInput()))
		{
			UE_LOG(LogUIActionRouter, Display, TEXT(&quot;Applying input config for leaf-most node [%s]&quot;), *PinnedLeafmostNode-&amp;gt;GetWidget()-&amp;gt;GetName());

			TOptional&amp;lt;FUIInputConfig&amp;gt; DesiredConfig = PinnedLeafmostNode-&amp;gt;FindDesiredInputConfig();
			if(DesiredConfig.IsSet())
			{
				GetActionRouter().SetActiveUIInputConfig(DesiredConfig.GetValue(), PinnedLeafmostNode-&amp;gt;GetWidget());
			}
			else if(ICommonInputModule::GetSettings().GetEnableDefaultInputConfig())
			{
				// Nobody in the entire tree cares about the config and the default is enabled so fall back to the default
				GetActionRouter().SetActiveUIInputConfig(FUIInputConfig());
			}

			FocusLeafmostNode();
		}
		else
		{
			UE_LOG(LogUIActionRouter, Log, TEXT(&quot;Didn't apply input config for leaf-most node [%s] because it's not receiving input right now&quot;), *PinnedLeafmostNode-&amp;gt;GetWidget()-&amp;gt;GetName());
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget이 비활성화 되면 Common UI는 이전 InputConfig를 복구한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 현재 Widget을 지원할 적절한 Inputconfig가 없이 중단되는 것을 방지하기 위함이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;만약 UI의 모든 Widget이 비활성화 되면&lt;br /&gt;CommonUI는 마지막으로 비활성화 된 Widget의 Default InputConfig를 사용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우 마지막으로 비활성화 된 Widget이 Soft Lock 방지를 위해&lt;br /&gt;합리적인 InputConfig 상태를 다시 적용하도록 해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Recommended Use&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InputConfig를 사용하는 경우 UI에서 표준 InputConfig를 사용하지 않아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&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_1720764312471&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UCommonUIActionRouterBase::ApplyUIInputConfig(const FUIInputConfig&amp;amp; NewConfig, bool bForceRefresh)
{
	if (bForceRefresh || NewConfig != ActiveInputConfig.GetValue())
	{
		UE_LOG(LogUIActionRouter, Display, TEXT(&quot;UIInputConfig being changed. bForceRefresh: %d&quot;), bForceRefresh ? 1 : 0);
		UE_LOG(LogUIActionRouter, Display, TEXT(&quot;\tInputMode: Previous (%s), New (%s)&quot;),
			ActiveInputConfig.IsSet() ? *StaticEnum&amp;lt;ECommonInputMode&amp;gt;()-&amp;gt;GetValueAsString(ActiveInputConfig-&amp;gt;GetInputMode()) : TEXT(&quot;None&quot;), *StaticEnum&amp;lt;ECommonInputMode&amp;gt;()-&amp;gt;GetValueAsString(NewConfig.GetInputMode()));

		const ECommonInputMode PreviousInputMode = GetActiveInputMode();

		TOptional&amp;lt;FUIInputConfig&amp;gt; OldConfig = ActiveInputConfig;
		ActiveInputConfig = NewConfig;

		ULocalPlayer&amp;amp; LocalPlayer = *GetLocalPlayerChecked();

		// Note: may not work for splitscreen. We need per-player viewport client settings for mouse capture
		if (UGameViewportClient* GameViewportClient = LocalPlayer.ViewportClient)
		{
			if (TSharedPtr&amp;lt;SViewport&amp;gt; ViewportWidget = GameViewportClient-&amp;gt;GetGameViewportWidget())
			{
				if (APlayerController* PC = LocalPlayer.GetPlayerController(GetWorld()))
				{
					if (!OldConfig.IsSet() || OldConfig.GetValue().bIgnoreMoveInput != NewConfig.bIgnoreMoveInput)
					{
						PC-&amp;gt;SetIgnoreMoveInput(NewConfig.bIgnoreMoveInput);						
					}
					
					if (!OldConfig.IsSet() || OldConfig.GetValue().bIgnoreLookInput != NewConfig.bIgnoreLookInput)
					{
						PC-&amp;gt;SetIgnoreLookInput(NewConfig.bIgnoreLookInput);						
					}

					if (bAutoFlushPressedKeys &amp;amp;&amp;amp; NewConfig.GetInputMode() == ECommonInputMode::Menu &amp;amp;&amp;amp; PreviousInputMode != NewConfig.GetInputMode())
					{
						// Flushing pressed keys after switching to the Menu InputMode. This prevents the inputs from being artificially &quot;held down&quot;.
						// This needs to be delayed by one frame to successfully clear input captured at the end of this frame
						GetWorld()-&amp;gt;GetTimerManager().SetTimerForNextTick(this, &amp;amp;ThisClass::FlushPressedKeys);
					}

					const bool bWasCursorHidden = !PC-&amp;gt;ShouldShowMouseCursor();

					GameViewportClient-&amp;gt;SetMouseCaptureMode(NewConfig.GetMouseCaptureMode());
					GameViewportClient-&amp;gt;SetHideCursorDuringCapture(NewConfig.HideCursorDuringViewportCapture() &amp;amp;&amp;amp; !ShouldAlwaysShowCursor());
					GameViewportClient-&amp;gt;SetMouseLockMode(NewConfig.GetMouseLockMode());

					FReply&amp;amp; SlateOperations = LocalPlayer.GetSlateOperations();
					const EMouseCaptureMode CaptureMode = NewConfig.GetMouseCaptureMode();
					switch (CaptureMode)
					{
					case EMouseCaptureMode::CapturePermanently:
					case EMouseCaptureMode::CapturePermanently_IncludingInitialMouseDown:
					{
						PC-&amp;gt;SetShowMouseCursor(ShouldAlwaysShowCursor() || !NewConfig.HideCursorDuringViewportCapture());

						TSharedRef&amp;lt;SViewport&amp;gt; ViewportWidgetRef = ViewportWidget.ToSharedRef();
						SlateOperations.UseHighPrecisionMouseMovement(ViewportWidgetRef);
						SlateOperations.SetUserFocus(ViewportWidgetRef);
						SlateOperations.CaptureMouse(ViewportWidgetRef);

						if (GameViewportClient-&amp;gt;ShouldAlwaysLockMouse() || GameViewportClient-&amp;gt;LockDuringCapture() || !PC-&amp;gt;ShouldShowMouseCursor())
						{
							SlateOperations.LockMouseToWidget(ViewportWidget.ToSharedRef());
						}
						else
						{
							SlateOperations.ReleaseMouseLock();
						}
					}
					break;
					case EMouseCaptureMode::NoCapture:
					case EMouseCaptureMode::CaptureDuringMouseDown:
					case EMouseCaptureMode::CaptureDuringRightMouseDown:
					{
						PC-&amp;gt;SetShowMouseCursor(true);

						SlateOperations.ReleaseMouseCapture();

						if (GameViewportClient-&amp;gt;ShouldAlwaysLockMouse())
						{
							SlateOperations.LockMouseToWidget(ViewportWidget.ToSharedRef());
						}
						else
						{
							SlateOperations.ReleaseMouseLock();
						}
					}
					break;
					}

					// If the mouse was hidden previously, set it back to the center of the viewport now that we're showing it again 
					if (!bForceRefresh &amp;amp;&amp;amp; bWasCursorHidden &amp;amp;&amp;amp; PC-&amp;gt;ShouldShowMouseCursor())
					{
						const ECommonInputType CurrentInputType = GetInputSubsystem().GetCurrentInputType();
						
						bool bCenterCursor = true;
						switch (CurrentInputType)
						{
							// Touch - Don't do it - the cursor isn't really relevant there.
							case ECommonInputType::Touch:
								bCenterCursor = false;
								break;
							// Gamepad - Let the settings tell us if we should center it.
							case ECommonInputType::Gamepad:
								break;
						}

						if (bCenterCursor)
						{
							TSharedPtr&amp;lt;FSlateUser&amp;gt; SlateUser = LocalPlayer.GetSlateUser();
							TSharedPtr&amp;lt;IGameLayerManager&amp;gt; GameLayerManager = GameViewportClient-&amp;gt;GetGameLayerManager();
							if (ensure(SlateUser) &amp;amp;&amp;amp; ensure(GameLayerManager))
							{
								FGeometry PlayerViewGeometry = GameLayerManager-&amp;gt;GetPlayerWidgetHostGeometry(&amp;amp;LocalPlayer);
								const FVector2D AbsoluteViewCenter = PlayerViewGeometry.GetAbsolutePositionAtCoordinates(FVector2D(0.5f, 0.5f));
								SlateUser-&amp;gt;SetCursorPosition(AbsoluteViewCenter);

								UE_LOG(LogUIActionRouter, Verbose, TEXT(&quot;Moving the cursor to the viewport center.&quot;));
							}
						}
					}
				}
				else
				{
					UE_LOG(LogUIActionRouter, Warning, TEXT(&quot;\tFailed to commit change! Local player controller was null.&quot;));
				}
			}
			else
			{
				UE_LOG(LogUIActionRouter, Warning, TEXT(&quot;\tFailed to commit change! ViewportWidget was null.&quot;));
			}
		}
		else
		{
			UE_LOG(LogUIActionRouter, Warning, TEXT(&quot;\tFailed to commit change! GameViewportClient was null.&quot;));
		}

		if (PreviousInputMode != NewConfig.GetInputMode())
		{
			OnActiveInputModeChanged().Broadcast(NewConfig.GetInputMode());
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UCommonUIActionRouterBase::ApplyInputConfig에서는 Config Process 과정에서&lt;br /&gt;다음 Unreal의 표준 InputConfig들을 기능의 일부로 호출한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764312474&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void AController::SetIgnoreMoveInput(bool bNewMoveInput)
{
	IgnoreMoveInput = FMath::Max(IgnoreMoveInput + (bNewMoveInput ? +1 : -1), 0);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1720764312474&quot; class=&quot;livescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void UGameViewportClient::SetMouseCaptureMode(EMouseCaptureMode Mode)
{
	if (MouseCaptureMode != Mode)
	{
		UE_LOG(LogViewport, Display, TEXT(&quot;Viewport MouseCaptureMode Changed, %s -&amp;gt; %s&quot;),
			*StaticEnum&amp;lt;EMouseCaptureMode&amp;gt;()-&amp;gt;GetNameStringByValue((uint64)MouseCaptureMode),
			*StaticEnum&amp;lt;EMouseCaptureMode&amp;gt;()-&amp;gt;GetNameStringByValue((uint64)Mode)
		);

		MouseCaptureMode = Mode;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1720764312475&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void UGameViewportClient::SetHideCursorDuringCapture(bool InHideCursorDuringCapture)
{
	if (bHideCursorDuringCapture != InHideCursorDuringCapture)
	{
		UE_LOG(LogViewport, Display, TEXT(&quot;Viewport HideCursorDuringCapture Changed, %s -&amp;gt; %s&quot;),
			bHideCursorDuringCapture ? TEXT(&quot;True&quot;) : TEXT(&quot;False&quot;),
			InHideCursorDuringCapture ? TEXT(&quot;True&quot;) : TEXT(&quot;False&quot;)
		);

		bHideCursorDuringCapture = InHideCursorDuringCapture;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;때문에 다른 곳에서 이런 함수를 호출하며 Common UI의 InputConfig와 혼용할 경우 서로를 Override하여 관리할 때 혼란이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;Input Config 관리를 간소화하기 위해&lt;br /&gt;Widget의 Enum값을 기반으로 주로 사용하는 InputConfig를 할당하는 기본 구현을 생성할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Widget별로 몇 개의 고정적인 Async InputConfig만 필요한 경우에 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Input Handling State Reference&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;FUIInputConfig&lt;/h4&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&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_1720764312475&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * Input Config that can be applied on widget activation. Allows for input setup  (Mouse capture, 
 * UI-only input, move / look ignore, etc), to be controlled by widget activation.
 */
USTRUCT(BlueprintType)
struct COMMONUI_API FUIInputConfig
{
	GENERATED_BODY()

	ECommonInputMode GetInputMode() const { return InputMode; }
	EMouseCaptureMode GetMouseCaptureMode() const { return MouseCaptureMode; }
	EMouseLockMode GetMouseLockMode() const { return MouseLockMode; }
	bool HideCursorDuringViewportCapture() const { return bHideCursorDuringViewportCapture; }

	FUIInputConfig();
	FUIInputConfig(ECommonInputMode InInputMode, EMouseCaptureMode InMouseCaptureMode, bool bInHideCursorDuringViewportCapture = true);
	FUIInputConfig(ECommonInputMode InInputMode, EMouseCaptureMode InMouseCaptureMode, EMouseLockMode InMouseLockMode, bool bInHideCursorDuringViewportCapture = true);

	bool operator==(const FUIInputConfig&amp;amp; Other) const
	{
		return bIgnoreMoveInput == Other.bIgnoreMoveInput
			&amp;amp;&amp;amp; bIgnoreLookInput == Other.bIgnoreLookInput
			&amp;amp;&amp;amp; InputMode == Other.InputMode
			&amp;amp;&amp;amp; MouseCaptureMode == Other.MouseCaptureMode
			&amp;amp;&amp;amp; MouseLockMode == Other.MouseLockMode
			&amp;amp;&amp;amp; bHideCursorDuringViewportCapture == Other.bHideCursorDuringViewportCapture;
	}

	bool operator!=(const FUIInputConfig&amp;amp; Other) const
	{
		return !operator==(Other);
	}

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	bool bIgnoreMoveInput = false;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	bool bIgnoreLookInput = false;

	/** Simplification of config as string */
	FString ToString() const;

protected:


	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	ECommonInputMode InputMode;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	EMouseCaptureMode MouseCaptureMode;


	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	EMouseLockMode MouseLockMode;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
	bool bHideCursorDuringViewportCapture = true;
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;ECommonInputMode&lt;/h4&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&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_1720764312477&quot; class=&quot;crystal&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UENUM(BlueprintType)
enum class ECommonInputMode : uint8
{
	Menu	UMETA(Tooltip = &quot;Input is received by the UI only&quot;),
	Game	UMETA(Tooltip = &quot;Input is received by the Game only&quot;),
	All		UMETA(Tooltip = &quot;Input is received by UI and the Game&quot;),

	MAX UMETA(Hidden)
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;EMouseCaptureMode&lt;/h4&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&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_1720764312478&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UENUM()
enum class EMouseCaptureMode : uint8
{
	/** Do not capture the mouse at all */
	NoCapture,
	/** Capture the mouse permanently when the viewport is clicked, and consume the initial mouse down that caused the capture so it isn't processed by player input */
	CapturePermanently,
	/** Capture the mouse permanently when the viewport is clicked, and allow player input to process the mouse down that caused the capture */
	CapturePermanently_IncludingInitialMouseDown,
	/** Capture the mouse during a mouse down, releases on mouse up */
	CaptureDuringMouseDown,
	/** Capture only when the right mouse button is down, not any of the other mouse buttons */
	CaptureDuringRightMouseDown,
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;EMouseLockMode&lt;/h4&gt;
&lt;div style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot; data-text-less=&quot;닫기&quot; data-text-more=&quot;더보기&quot; data-ke-type=&quot;moreLess&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_1720764312479&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UENUM()
enum class EMouseLockMode : uint8
{
	/** Do not lock the mouse cursor to the viewport */
	DoNotLock,
	/** Only lock the mouse cursor to the viewport when the mouse is captured */
	LockOnCapture,
	/** Always lock the mouse cursor to the viewport */
	LockAlways,
	/** Lock the cursor if we're in fullscreen */
	LockInFullscreen,
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Change Widget Input Respons Using FReply&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FReply는 InputEvent의 처리 상태를 추적한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720764795840&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * An event should return a FReply::Handled() to let the system know that an event was handled.
 */
static FReply Handled( )
{
	return FReply(true);
}

/**
 * An event should return a FReply::Unhandled() to let the system know that an event was unhandled.
 */
static FReply Unhandled( )
{
	return FReply(false);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slate의 Input은 대부분 FReply::Handled나 FReply::Unhandled를 결과로 반환한다.&lt;/li&gt;
&lt;li&gt;SWidget에서 사용되는 수 많은 Input Event들은 대부분 FReply를 반환한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 이용해 특정 FReply를 반환하여 원하는 결과를 얻게할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를들어 특정 InputType의 처리를 허용 또는 중지한다던가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;하지만 대부분은 순정 FReply를 사용해도 크게 문제 없을 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;FReply를 Customize 할 때에는 Slate Widget 작업에서 문제가 발생하는 경우가 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cK1HFE/btsIycCUoq4/OR59swsTunSaNEtkZsbu41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cK1HFE/btsIycCUoq4/OR59swsTunSaNEtkZsbu41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cK1HFE/btsIycCUoq4/OR59swsTunSaNEtkZsbu41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcK1HFE%2FbtsIycCUoq4%2FOR59swsTunSaNEtkZsbu41%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;1280&quot; height=&quot;1137&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1137&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;FReply Settings&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통은 FReply의 Handled/UnHandled로만 상태를 추적하지만, 몇 가지 추가 데이터를 제공하기도 한다.&lt;/li&gt;
&lt;li&gt;이러한 Method는 UMG나 Slate의 일부 Method와 비슷해 보일 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 기본적으로 FReply Namespace에 존재하며, Slate에서 FReply를 처리할 때 발생하는 동작을 수정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;FReply에서 이런 Method를 호출하면 FReply 외부의 동등한 Method를 호출한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 쉽게 복제할 수 없는 조금 다른 동작이 나올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CaptureMouse&lt;/h4&gt;
&lt;pre id=&quot;code_1720765213443&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** An event should return a FReply::Handled().CaptureMouse( SomeWidget ) as a means of asking the system to forward all mouse events to SomeWidget */
FReply&amp;amp; CaptureMouse( TSharedRef&amp;lt;SWidget&amp;gt; InMouseCaptor )
{
	this-&amp;gt;MouseCaptor = InMouseCaptor;
	return Me();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;System에 특정 Widget으로 모든 MouseEvent를 전달하도록 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ClearUserFocus&lt;/h4&gt;
&lt;pre id=&quot;code_1720765263315&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** An event should return a FReply::Handled().ClearUserFocus() to ask the system to clear user focus*/
FReply&amp;amp; ClearUserFocus(bool bInAllUsers = false)
{
    return ClearUserFocus(EFocusCause::SetDirectly, bInAllUsers);
}

/** An event should return a FReply::Handled().ClearUserFocus() to ask the system to clear user focus*/
SLATECORE_API FReply&amp;amp; ClearUserFocus(EFocusCause ReasonFocusIsChanging, bool bInAllUsers = false)
{
	this-&amp;gt;FocusRecipient = nullptr;
	this-&amp;gt;FocusChangeReason = ReasonFocusIsChanging;
	this-&amp;gt;bReleaseUserFocus = true;
	this-&amp;gt;bSetUserFocus = false;
	this-&amp;gt;bAllUsers = bInAllUsers;
	return Me();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;System에 User Focus를 지우기를 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ReleaseMouseCapture&lt;/h4&gt;
&lt;pre id=&quot;code_1720765339842&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 
 * An event should return a FReply::Handled().ReleaseMouse() to ask the system to release mouse capture
 * NOTE: Deactivates high precision mouse movement if activated.
 */
FReply&amp;amp; ReleaseMouseCapture()
{
	this-&amp;gt;MouseCaptor.Reset();
	this-&amp;gt;bReleaseMouseCapture = true;
	this-&amp;gt;bUseHighPrecisionMouse = false;
	return Me();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;System에 Mouse Capture 해제를 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SetUserFocus&lt;/h4&gt;
&lt;pre id=&quot;code_1720765422905&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** An event should return FReply::Handled().SetUserFocus( SomeWidget ) as a means of asking the system to set users focus to the provided widget*/
SLATECORE_API FReply&amp;amp; SetUserFocus(TSharedRef&amp;lt;SWidget&amp;gt; GiveMeFocus, EFocusCause ReasonFocusIsChanging = EFocusCause::SetDirectly, bool bInAllUsers = false)
{
	this-&amp;gt;bSetUserFocus = true;
	this-&amp;gt;FocusRecipient = GiveMeFocus;
	this-&amp;gt;FocusChangeReason = ReasonFocusIsChanging;
	this-&amp;gt;bReleaseUserFocus = false;
	this-&amp;gt;bAllUsers = bInAllUsers;
	return Me();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;System에 User의 Focus를 제공된 Widget으로 설정하도록 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SetNavigation&lt;/h4&gt;
&lt;pre id=&quot;code_1720765443433&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** An event should return FReply::Handled().SetNavigation( NavigationType ) as a means of asking the system to attempt a navigation*/
FReply&amp;amp; SetNavigation(EUINavigation InNavigationType, const ENavigationGenesis InNavigationGenesis, const ENavigationSource InNavigationSource = ENavigationSource::FocusedWidget)
{
	this-&amp;gt;NavigationType = InNavigationType;
	this-&amp;gt;NavigationGenesis = InNavigationGenesis;
	this-&amp;gt;NavigationSource = InNavigationSource;
	this-&amp;gt;NavigationDestination = nullptr;
	return Me();
}

/** An event should return FReply::Handled().SetNavigation( NavigationDestination ) as a means of asking the system to attempt a navigation to the specified destination*/
FReply&amp;amp; SetNavigation(TSharedRef&amp;lt;SWidget&amp;gt; InNavigationDestination, const ENavigationGenesis InNavigationGenesis, const ENavigationSource InNavigationSource = ENavigationSource::FocusedWidget)
{
	this-&amp;gt;NavigationType = EUINavigation::Invalid;
	this-&amp;gt;NavigationGenesis = InNavigationGenesis;
	this-&amp;gt;NavigationSource = InNavigationSource;
	this-&amp;gt;NavigationDestination = InNavigationDestination;
	return Me();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;System에 지정된 대상으로의 Navigation을 시도하도록 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;When would we set FReply?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 특정 Key Input으로 Widget Focus를 설정하거나, 지워야 한다고 가정해보자.&lt;/li&gt;
&lt;li&gt;보통은 FSlateInputApplication에서 Key Input Handler 관련 함수를 직접 호출할 것이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 방식은 모든 경우에, 특히 Input Routing을 사용할 때 작동하지 않을 수 있다.&lt;/li&gt;
&lt;li&gt;현재 Widget에서 Input이 처리되는 동안 Focus 변경/지우기가 발생할 수 있기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이를 대신하여 Input이 완전히 처리되고 나서 발생되는 FReply를 사용하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;과거에는 FReply API가 제어 또는 노출되는 상태일 때에는 FReply만 사용 하도록 하였다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 이 방식은 너무 제한적이라 위와 같이 언제든 사용 가능하도록 바뀌었다.&lt;/li&gt;
&lt;li&gt;현재 버전에서 기존의 가이드 이상으로 강력히 권장되는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Customize Navigation&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Navigation Config&lt;/h3&gt;
&lt;pre id=&quot;code_1720766444801&quot; class=&quot;autohotkey&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;/**
 * Navigation context for event
 */
UENUM(BlueprintType)
enum class EUINavigation : uint8
{
	/** Four cardinal directions*/
	Left,
	Right,
	Up,
	Down,

	/** Conceptual next and previous*/
	Next,
	Previous,

	/** Number of navigation types*/
	Num UMETA(Hidden),

	/** Denotes an invalid navigation, more important used to denote no specified navigation*/
	Invalid,
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slate는 CommonUI 사용 여부와 무관하게 기본 Navigation을 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720766567556&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * Sets the navigation config.  If you need to control navigation config dynamically, you
 * should subclass FNavigationConfig to be dynamically adjustable to your needs.
 */
SLATE_API void SetNavigationConfig(TSharedRef&amp;lt;FNavigationConfig&amp;gt; InNavigationConfig)
{
	NavigationConfig-&amp;gt;OnUnregister();
	NavigationConfig = InNavigationConfig;
	NavigationConfig-&amp;gt;OnRegister();

#if WITH_SLATE_DEBUGGING
	TryDumpNavigationConfig(NavigationConfig);
#endif // WITH_SLATE_DEBUGGING
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NavigationConfig를 설정하려면 FSlateApplication::NetNavigationConfig를 호출한다.&lt;/li&gt;
&lt;li&gt;대체로 FNavigationConfig에서 파생된 Custom Navigation 구성을 할 때 이 함수를 호출한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 WASD로 UI라 상호작용 한다면 이 부분에서부터 시작하는게 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720766683674&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void FSlateUser::SetUserNavigationConfig(TSharedPtr&amp;lt;FNavigationConfig&amp;gt; InNavigationConfig)
{
	if (UserNavigationConfig)
	{
		UserNavigationConfig-&amp;gt;OnUnregister();
	}

	UserNavigationConfig = InNavigationConfig;
	
	if (InNavigationConfig)
	{
		InNavigationConfig-&amp;gt;OnRegister();
	}

#if WITH_SLATE_DEBUGGING
	FSlateApplication::Get().TryDumpNavigationConfig(UserNavigationConfig);
#endif // WITH_SLATE_DEBUGGING
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;또한 FSlateUser::SetUserNavigationConfig를 호출해 User 기반으로 Navigation 구성을 설정할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Manually Control Navigation&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;171&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfgjsx/btsIxJVIHIV/XKSaTnv8TTRR9rjAID7wMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfgjsx/btsIxJVIHIV/XKSaTnv8TTRR9rjAID7wMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfgjsx/btsIxJVIHIV/XKSaTnv8TTRR9rjAID7wMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfgjsx%2FbtsIxJVIHIV%2FXKSaTnv8TTRR9rjAID7wMK%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;724&quot; height=&quot;171&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;171&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UMG에서 Widget을 선택하고 Details -&amp;gt; Navigation을 선택하면 &lt;br /&gt;Navigation Event 발생 시 어떻게 될지 수동으로 지정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720766880357&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UENUM(BlueprintType)
enum class EUINavigationRule : uint8
{
	/** Allow the movement to continue in that direction, seeking the next navigable widget automatically. */
	Escape,
	/** Move to a specific widget. */
	Explicit,
	/**
	 * Wrap movement inside this container, causing the movement to cycle around from the opposite side, 
	 * if the navigation attempt would have escaped.
	 */
	Wrap,
	/** Stops movement in this direction */
	Stop,
	/** Custom navigation handled by user code. */
	Custom,
	/** Custom navigation handled by user code if the boundary is hit. */
	CustomBoundary,
	/** Invalid Rule */
	Invalid
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Activatable Widgets and Action Bind&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Set Focus for Activatable Widget on Activation&lt;/h3&gt;
&lt;pre id=&quot;code_1720767310968&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UWidget* UCommonActivatableWidget::GetDesiredFocusTarget() const
{
	return NativeGetDesiredFocusTarget();
}

UWidget* UCommonActivatableWidget::NativeGetDesiredFocusTarget() const
{
	// Prioritize BP implementation of this function.
	UWidget* DesiredFocusTarget = BP_GetDesiredFocusTarget();

	if (!DesiredFocusTarget)
	{
		// BP didn't specify focus target, fallback to DesiredFocusWidget property on UserWidget.
		DesiredFocusTarget = GetDesiredFocusWidget();
	}

	return DesiredFocusTarget;
}

UWidget* UUserWidget::GetDesiredFocusWidget() const
{
	return DesiredFocusWidget.GetWidget();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ActivatableWidget을 Activate할 때마다 GetDesiredFocusTarget이 호출된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 함수는 User Input 발생 시 Common UI가 Focus해야 할 Widget을 선택한다.&lt;/li&gt;
&lt;li&gt;해당 함수를 Customize 하지 않을 시 Common UI에서 &lt;br /&gt;Widget이 Active/Inactive 할 때마다 Focus 할 위치를 파악하는데 어려울 수 있다.&lt;/li&gt;
&lt;li&gt;그러니 반드시 Customize를 하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Lyra에서는 ActivatableWidget에서 원하는 Focus Target을 가져오는데 사용할 Method를 Enum Type으로 결정한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Focus 대상이 고정된 Async Method를 사용하는 경우에 권장된다.&lt;/li&gt;
&lt;li&gt;대부분의 Menu에서라던가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Change when Input Action Fire Triggered&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Action Bind를 위해 FBindUIActionArgs를 생성할 때 KeyEvent를 &lt;br /&gt;InputAction Event를 Trigger해야 할 Type으로 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Console Variable Reference&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1201&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgtAQP/btsIytSaWNd/QXd6aOkNNGbckOpD3Yf641/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgtAQP/btsIytSaWNd/QXd6aOkNNGbckOpD3Yf641/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgtAQP/btsIytSaWNd/QXd6aOkNNGbckOpD3Yf641/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgtAQP%2FbtsIytSaWNd%2FQXd6aOkNNGbckOpD3Yf641%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;1201&quot; height=&quot;592&quot; data-origin-width=&quot;1201&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Use With Enhanced Input&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Unreal Engine 5.2 이전 버전에서는 사용을 권장하지 않음.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Enhanced Input이 5.2버전 기준으로 테스트가 충분히 거치지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;주요 기능인 EnhancedInput의 PlayerMappableKeySettings가 아직 Experimental 단계임.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적어도 Beta가 되기 전까지는 시도를 안 하는 편이 좋음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;필수 사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI와 Enhanced Input을 모두 사용할 것&lt;/li&gt;
&lt;li&gt;ViewportClass가 CommonGameViewportClient나 그 하위 Class일 것&lt;/li&gt;
&lt;li&gt;CommonInputData에서 Accept/Back에 대한 Input Action이 등록되어 있을 것&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;설정법&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3279&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO3tON/btsIuP2tk3w/yQ2BVu4QgpKcX09CuKknd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO3tON/btsIuP2tk3w/yQ2BVu4QgpKcX09CuKknd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO3tON/btsIuP2tk3w/yQ2BVu4QgpKcX09CuKknd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO3tON%2FbtsIuP2tk3w%2FyQ2BVu4QgpKcX09CuKknd1%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;3279&quot; height=&quot;774&quot; data-origin-width=&quot;3279&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Project Settings -&amp;gt; Game -&amp;gt; Common Input Settings에서 Enable Enhanced Input Support를 체크&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzqji/btsItjjzfpf/jwecDqLhEKO4lBRegv71f0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzqji/btsItjjzfpf/jwecDqLhEKO4lBRegv71f0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzqji/btsItjjzfpf/jwecDqLhEKO4lBRegv71f0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkzqji%2FbtsItjjzfpf%2FjwecDqLhEKO4lBRegv71f0%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;1225&quot; height=&quot;619&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Miscellaneous -&amp;gt; DataAsset을 선택해 CommonMappingContextMetadataInterface를 선택해 Asset 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 Asset은 Common UI Input Data를 설정할 수 있는 Metadata Object를 제공한다.&lt;/li&gt;
&lt;li&gt;여기서 쓰이는 Nav Bar Priority는 Common UI와 동일한 개념이다.&lt;/li&gt;
&lt;li&gt;만약 기능이 필요하다면 CommonInputMetaData를 상속받아 확장 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;위와 같이 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Per Action Enhanced Input Metadata
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대응 될 InputAction BP&lt;/li&gt;
&lt;li&gt;Action마다 Asset을 각자 만드는 대신,&lt;br /&gt;Per Action Enhanced Input Metadata를 사용해 한 Asset에서 여러 Action을 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Is Generic Iput Action
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;true로 설정한 이유는 Common UI가 Input Action을 Broadcast하는 것을 방지하기 위함이다.&lt;/li&gt;
&lt;li&gt;만약 Generic InputAction이 아닌 InputAction에서 사용하고 싶다면 이 값을 해제하면 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우, 해당 InputAction에는 Input을 Bind할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1799&quot; data-origin-height=&quot;1044&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c637Ic/btsItJWttnG/Vtvuk10nBQq6AXuKUgzSZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c637Ic/btsItJWttnG/Vtvuk10nBQq6AXuKUgzSZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c637Ic/btsItJWttnG/Vtvuk10nBQq6AXuKUgzSZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc637Ic%2FbtsItJWttnG%2FVtvuk10nBQq6AXuKUgzSZ0%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;1799&quot; height=&quot;1044&quot; data-origin-width=&quot;1799&quot; data-origin-height=&quot;1044&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InputAction에서 Player Mappable Key Settings에서 Metadata 필드를 설정한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 사용하는 Metadata는 위에서 만든 Asset이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;1155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tVeoY/btsIuQ8dvRc/9sKa0xAJDGbHN7tvuhMyDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tVeoY/btsIuQ8dvRc/9sKa0xAJDGbHN7tvuhMyDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tVeoY/btsIuQ8dvRc/9sKa0xAJDGbHN7tvuhMyDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtVeoY%2FbtsIuQ8dvRc%2F9sKa0xAJDGbHN7tvuhMyDk%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;995&quot; height=&quot;1155&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;1155&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이렇게 만들어진 InputAction은 Input을 받을 수 있는 Common UI에 연결해 사용할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonButtonBase&lt;/li&gt;
&lt;li&gt;CommonActionWidget
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI가 아닌 InputAction의 Key를 표시할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CommonUIInputData
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 Default Navigation Action이 정의된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;만약 이 옵션이 보이지 않는다면 Project Settings에서 Enable Enhanced Input Support 옵션을 확인해보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;1006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZmX9y/btsItorxBpm/moiBwSkgsPJeO55CV2ukO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZmX9y/btsItorxBpm/moiBwSkgsPJeO55CV2ukO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZmX9y/btsItorxBpm/moiBwSkgsPJeO55CV2ukO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZmX9y%2FbtsItorxBpm%2FmoiBwSkgsPJeO55CV2ukO0%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;1001&quot; height=&quot;1006&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;1006&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonActivatableWidget의 경우, Active 여부에 따라 적용/제거 될 IMC를 지정할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 좋은 구조를 위해 게임 IMC를 적용 할 때 Common UI IMC를 같이 적용할 것을 권장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>UE5/UI</category>
      <author>RedChiken</author>
      <guid isPermaLink="true">https://redchiken.tistory.com/395</guid>
      <comments>https://redchiken.tistory.com/395#entry395comment</comments>
      <pubDate>Wed, 10 Jul 2024 16:50:50 +0900</pubDate>
    </item>
    <item>
      <title>[UI] Common UI Widget</title>
      <link>https://redchiken.tistory.com/394</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://benui.ca/unreal/common-ui-intro/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://benui.ca/unreal/common-ui-intro/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720597637189&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Common UI Introduction&quot; data-og-description=&quot;What is the plugin for? What does it contain? Why should we care?&quot; data-og-host=&quot;benui.ca&quot; data-og-source-url=&quot;https://benui.ca/unreal/common-ui-intro/&quot; data-og-url=&quot;https://benui.ca/unreal/common-ui-intro/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b5KHGi/hyWzEE6GDs/tkln2zDOo6ztSknLq9vj40/img.jpg?width=400&amp;amp;height=225&amp;amp;face=0_0_400_225,https://scrap.kakaocdn.net/dn/cd0rHh/hyWvO3IF2f/7qFGoa4IsKPKXj5GAZRCYk/img.jpg?width=400&amp;amp;height=225&amp;amp;face=0_0_400_225&quot;&gt;&lt;a href=&quot;https://benui.ca/unreal/common-ui-intro/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://benui.ca/unreal/common-ui-intro/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b5KHGi/hyWzEE6GDs/tkln2zDOo6ztSknLq9vj40/img.jpg?width=400&amp;amp;height=225&amp;amp;face=0_0_400_225,https://scrap.kakaocdn.net/dn/cd0rHh/hyWvO3IF2f/7qFGoa4IsKPKXj5GAZRCYk/img.jpg?width=400&amp;amp;height=225&amp;amp;face=0_0_400_225');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Common UI Introduction&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;What is the plugin for? What does it contain? Why should we care?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;benui.ca&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://benui.ca/unreal/common-ui-button/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://benui.ca/unreal/common-ui-button/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720597639230&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Common UI Button Widgets&quot; data-og-description=&quot;Centralized styling, selected state, and more!&quot; data-og-host=&quot;benui.ca&quot; data-og-source-url=&quot;https://benui.ca/unreal/common-ui-button/&quot; data-og-url=&quot;https://benui.ca/unreal/common-ui-button/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/6TEsX/hyWvOWS4MQ/hHP9vJ0hKkee46lWY2Aq0K/img.jpg?width=400&amp;amp;height=225&amp;amp;face=0_0_400_225,https://scrap.kakaocdn.net/dn/WaUHg/hyWzBVVoA9/2cRc6clvbKtajRhsVnLusK/img.jpg?width=400&amp;amp;height=225&amp;amp;face=0_0_400_225&quot;&gt;&lt;a href=&quot;https://benui.ca/unreal/common-ui-button/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://benui.ca/unreal/common-ui-button/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/6TEsX/hyWvOWS4MQ/hHP9vJ0hKkee46lWY2Aq0K/img.jpg?width=400&amp;amp;height=225&amp;amp;face=0_0_400_225,https://scrap.kakaocdn.net/dn/WaUHg/hyWzBVVoA9/2cRc6clvbKtajRhsVnLusK/img.jpg?width=400&amp;amp;height=225&amp;amp;face=0_0_400_225');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Common UI Button Widgets&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Centralized styling, selected state, and more!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;benui.ca&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://x157.github.io/UE5/CommonUI/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://x157.github.io/UE5/CommonUI/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720597645473&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Common UI Plugin&quot; data-og-description=&quot;Overview of the Common UI plugin for Unreal Engine 5&quot; data-og-host=&quot;x157.github.io&quot; data-og-source-url=&quot;https://x157.github.io/UE5/CommonUI/&quot; data-og-url=&quot;https://x157.github.io/UE5/CommonUI/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://x157.github.io/UE5/CommonUI/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://x157.github.io/UE5/CommonUI/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Common UI Plugin&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Overview of the Common UI plugin for Unreal Engine 5&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;x157.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/using-the-common-bound-action-bar-in-unreal-engine&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/using-the-common-bound-action-bar-in-unreal-engine&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI에서 제공하는 class들에 대한 간단한 용도 설명&lt;/li&gt;
&lt;li&gt;실질적인 코드는 Engine/Plugin/Runtime/CommonUI/Source/CommonUI/Public/ 경로에서 확인 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonUserWidget&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 사용하는 BaseWidget&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonActionWidget&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Platform에 따라 다르게 표현되는 Icon을 표시하고 이에 대한 Input Action을 처리하는 Widget&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonActivatableWidgetSwitcher&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lifetime 중에 Activate/Inactivate 될 수 있는 Widget
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 이 외에 수정되거나 파괴되지는 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;보통은 다음 기능을 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 상으로 빈번하게 사용되기 때문에 Construct/Destruct만으로 대응이 힘든 상황
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hierarchy에 미리 생성해두고 Active/Inactive를 조절하여 사용&lt;/li&gt;
&lt;li&gt;ex) 모바일에서 조건부로 나타나는 입력 키&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;현재 Widget에서 뒤로가기를 해야 하는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뒤로 가는 경로를 따라가는 경우&lt;/li&gt;
&lt;li&gt;모달을 닫는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 Widget은 Hierarchy에서 Input을 Routing하는 ActivatableWidget Tree에서 하나의 Node로 작용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;해당 Class는 다음 기본 설정이 되어 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성 시 자동으로 Activate되지 않는다.&lt;/li&gt;
&lt;li&gt;뒤로가기, 앞으로가기 동작을 수신하도록 등록되어 있지 않다.&lt;/li&gt;
&lt;li&gt;뒤로가기 Handler로 구분되어 있을 시, 뒤로가기 Action을 수신하면 자동으로 Deactivate된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ActivatableWidget을 UI로부터 제거하는 것은 UWidget이 제거되지 않았더라도 항상 Widget을 Deactivate한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 때 AutoActivate가 가능하다면, &lt;br /&gt;기본 SWidget을 다시 생성하는 것만이 유일하게 다시 Widget을 Activate하는 방법이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonAnimatedSiwtcher&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonActivatableWidget 전용 Widget Switcher&lt;/li&gt;
&lt;li&gt;관련 Animation을 Trigger 하는 기능을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CommonActivatableWidgetSwitcher&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonActivableWidgets의 기능을 보존하는 Animation Switcher&lt;/li&gt;
&lt;li&gt;다른 Widget도 포함될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonBorder&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI Project에서 Default로 설정된 Border Style Template&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonButtonBase&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized Button&lt;/li&gt;
&lt;li&gt;Widget 전체를 Disable하지 않고 Click을 Disable 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonCustomNavigation&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized Border&lt;/li&gt;
&lt;li&gt;Custom Behavior로 Default Border Navigation을 대체할 수 있는 Event를 노출시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CommonTextBlock&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized TextBlock&lt;/li&gt;
&lt;li&gt;다음 기능들을 기본적으로 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FX를 이용해 자동 스크롤링&lt;/li&gt;
&lt;li&gt;Large Text&lt;/li&gt;
&lt;li&gt;더 많은 Styling Option 제공&lt;/li&gt;
&lt;li&gt;Mobile 플랫폼에서의 Custom Scaling&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonDateTimeTextBlock&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DateTime이나 TimeSpan을 직접 입력받아 날짜/시간 정보를 Text로 표시해주는 CommonTextBlock&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CommonNumericTextBlock&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Numeric 값을 입력 받아 Text로 표시해주는 CommonTextBlock&lt;/li&gt;
&lt;li&gt;다음 기능들을 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;근사(반올림/올림/내림)&lt;/li&gt;
&lt;li&gt;숫자 표시&lt;/li&gt;
&lt;li&gt;퍼센티지 표시&lt;/li&gt;
&lt;li&gt;초 단위 표시&lt;/li&gt;
&lt;li&gt;거리 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CommonRichTextBlock&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized RichTextBlock&lt;/li&gt;
&lt;li&gt;다음 기능들을 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mobile 플랫폼에서의 Custom Scaling&lt;/li&gt;
&lt;li&gt;공간이 부족할 때 Icon만 표시하는 옵션&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonUIRichTextData&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RichTextData를 구현하는 Class&lt;/li&gt;
&lt;li&gt;Project Settings -&amp;gt; Common UI Setting에서 등록할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonHardwareVisibilityBorder&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Platform이나 Input에 따라 Visbility를 조절하는 기능을 제공하는 CommonBorder&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonHierarchicalScrollBox&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized ScrollBox&lt;/li&gt;
&lt;li&gt;임의의 Scroll이 가능한 Widget Collection&lt;/li&gt;
&lt;li&gt;10~100개 정도를 표시하는데 적절하다.&lt;/li&gt;
&lt;li&gt;Virtualization을 제공하지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Virtualization: List 등에서 화면에 표시되는 영역의 Widget만 Memory에서 들고 있는 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonLazyImage&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized Image&lt;/li&gt;
&lt;li&gt;Image Resource가 Load되지 않을 때 미리 지정 된 Unloaded Image를 표시해주는 Widget&lt;/li&gt;
&lt;li&gt;SLoadGuard의 또 다른 Wrapper이지만, Image Loading과 Loading 중 Throbber만 관리한다.&lt;/li&gt;
&lt;li&gt;만약 이 Class가 Text를 표시하도록 바뀌면, 기본적으로 CoreStyle을 유지하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonLazyWidget&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget이 Load되지 않을 때 미리 지정된 Unloaded Widget을 표시해주는 Widget&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonListView&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized ListView&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonNativeListItem&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 UMG ListView Class에서 BaseItem으로 사용될 수 있는 Native non-UObject Item&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonLoadGuard&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized ContentWidget
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Border와 비슷하게 동작한다.&lt;/li&gt;
&lt;li&gt;필요한 Contents가 Load되거나 모종의 준비가 끝날 때까지 기본 Contents를 숨기고 &lt;br /&gt;Loading Spinner와 Message를 출력할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GuardAndLoadAsset 함수를 사용하면 자신이 Load 될 때까지 Loading 상태를 자동으로 표시할 수 있다.&lt;/li&gt;
&lt;li&gt;수동으로 Guard의 Loading State를 설정할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 Async Backend call이 종료될 때까지 대기 한다던가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonPoolableWidgetInterface&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WidgetFactory가 Widget Object 재사용을 구현할 경우에 Widget Pool 기능을 제공하는 Interface&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonRotator&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주어진 Text Label을 순회하는 기능을 제공하는 CommonButtonBase
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slide Banner처럼 Text를 Shift 할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonTabListWidgetBase&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Selectable Tab 기능을 제공하는 Base Class&lt;/li&gt;
&lt;li&gt;다음 기능들을 제공한다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연관된 Tab을 Activate&lt;/li&gt;
&lt;li&gt;연결된 Switchdr에서 연관된 Tab을 표시해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonTileView&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized TileView&lt;/li&gt;
&lt;li&gt;다음 기능들을 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consol에서 Focus Navigation에 특화 됨.&lt;/li&gt;
&lt;li&gt;Touch로 Focus되지 않을 경우에 Scroll을 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonTreeView&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 TreeView&lt;/li&gt;
&lt;li&gt;다음 기능들을 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consol에서 Focus Navigation에 특화 됨.&lt;/li&gt;
&lt;li&gt;Touch로 Focus되지 않을 경우에 Scroll을 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonVideoPlayer&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Media 재생 용 Widget&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonVisibilitySwitcher&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized Overlay
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Child Widget들의 Visibility를 Toggle하여 한번에 1개의 Widget만 표시하는 Siwtcher의 Base Class&lt;/li&gt;
&lt;li&gt;표시되는 Widget이 ActivatableWidget인 경우, 해당 Widget을 Activate한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonVisibilitySwitcherSlot&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized OverlaySlot&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonvisualAttachment&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized SizeBox&lt;/li&gt;
&lt;li&gt;Widget을 다른 Widget에 Zero-Size로 부착할수 있도록 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 Icon을 Label에 추가하되, Label의 사이즈가 변하지 않도록 한다던가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;CommonWidgetCarousel&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized PanelWidget&lt;/li&gt;
&lt;li&gt;최대 하나의 Widget만 표시되는 Widget
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Carousel로 미루어 보아 SlideBanner의 Widget 버전으로 추정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonWidgetCarouselNavBar&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonWidgetCarousel의 Navigation control을 담당하는 Widget&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonButtonTableRow&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonButtonBase를 인식할 수 있는 CommonUI 버전의 Object Table Row&lt;/li&gt;
&lt;li&gt;Mouse Event를 직접적으로 처리하는 대신, 그 자체가 Button이 되어 Event에 반응한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonHierarchicalScrollBox&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customized ScrollBox&lt;/li&gt;
&lt;li&gt;임의의 숫자의 Widget을 Scroll 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommonTileView&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 제공하는 Customzied TileView&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>UE5/UI</category>
      <author>RedChiken</author>
      <guid isPermaLink="true">https://redchiken.tistory.com/394</guid>
      <comments>https://redchiken.tistory.com/394#entry394comment</comments>
      <pubDate>Wed, 10 Jul 2024 16:44:26 +0900</pubDate>
    </item>
    <item>
      <title>[UI] Common UI Introduction</title>
      <link>https://redchiken.tistory.com/393</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/common-ui-plugin-for-advanced-user-interfaces-in-unreal-engine&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/common-ui-plugin-for-advanced-user-interfaces-in-unreal-engine&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@yoo06/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84-UE5-Common-UI-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@yoo06/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84-UE5-Common-UI-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720512832549&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;언리얼 엔진 UE5 Common UI 알아보기&quot; data-og-description=&quot;멀티플랫폼에서 UI를 생성하는 CommonUI 알아보기&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@yoo06/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84-UE5-Common-UI-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@yoo06/언리얼-엔진-UE5-Common-UI-알아보기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bnGjjt/hyWzDMOZlI/DPEP0xJoXpz95EaJqDcSik/img.png?width=860&amp;amp;height=234&amp;amp;face=0_0_860_234,https://scrap.kakaocdn.net/dn/zflOO/hyWzAo19po/UNE0hap1IhAQyuMQPkKgE0/img.png?width=860&amp;amp;height=234&amp;amp;face=0_0_860_234,https://scrap.kakaocdn.net/dn/d0LDmW/hyWzy5OzOo/FMXdhKFzd02MzVKleqN8g0/img.png?width=1068&amp;amp;height=784&amp;amp;face=0_0_1068_784&quot;&gt;&lt;a href=&quot;https://velog.io/@yoo06/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84-UE5-Common-UI-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@yoo06/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84-UE5-Common-UI-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bnGjjt/hyWzDMOZlI/DPEP0xJoXpz95EaJqDcSik/img.png?width=860&amp;amp;height=234&amp;amp;face=0_0_860_234,https://scrap.kakaocdn.net/dn/zflOO/hyWzAo19po/UNE0hap1IhAQyuMQPkKgE0/img.png?width=860&amp;amp;height=234&amp;amp;face=0_0_860_234,https://scrap.kakaocdn.net/dn/d0LDmW/hyWzy5OzOo/FMXdhKFzd02MzVKleqN8g0/img.png?width=1068&amp;amp;height=784&amp;amp;face=0_0_1068_784');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;언리얼 엔진 UE5 Common UI 알아보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;멀티플랫폼에서 UI를 생성하는 CommonUI 알아보기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ctdidier27.medium.com/common-ui-plugin-ue5-b12050bf1fc0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ctdidier27.medium.com/common-ui-plugin-ue5-b12050bf1fc0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720512847508&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Common UI Plugin UE5&quot; data-og-description=&quot;Go to Edit&amp;gt;plugins&amp;gt;Common UI and check and restart Unreal.&quot; data-og-host=&quot;ctdidier27.medium.com&quot; data-og-source-url=&quot;https://ctdidier27.medium.com/common-ui-plugin-ue5-b12050bf1fc0&quot; data-og-url=&quot;https://ctdidier27.medium.com/common-ui-plugin-ue5-b12050bf1fc0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bc6vEr/hyWvXe5oEh/373MZstR43r34AjyKK7dW0/img.gif?width=1200&amp;amp;height=814&amp;amp;face=0_0_1200_814&quot;&gt;&lt;a href=&quot;https://ctdidier27.medium.com/common-ui-plugin-ue5-b12050bf1fc0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ctdidier27.medium.com/common-ui-plugin-ue5-b12050bf1fc0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bc6vEr/hyWvXe5oEh/373MZstR43r34AjyKK7dW0/img.gif?width=1200&amp;amp;height=814&amp;amp;face=0_0_1200_814');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Common UI Plugin UE5&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Go to Edit&amp;gt;plugins&amp;gt;Common UI and check and restart Unreal.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ctdidier27.medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/community/learning/tutorials/BKJ7/unreal-engine-common-ui-tutorial-create-cross-platform-ui-easily-with-this-new-ue5-system&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.epicgames.com/community/learning/tutorials/BKJ7/unreal-engine-common-ui-tutorial-create-cross-platform-ui-easily-with-this-new-ue5-system&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면을 그대로 유지하면서 Overlay 되는 UI를 노출하고, 화면입력을 막고 싶은 경우&lt;/li&gt;
&lt;li&gt;플랫폼 별로 UI Element를 다르게 관리하고 싶을 때&lt;/li&gt;
&lt;li&gt;Popup들의 버튼을 누를 시 UI가 특정 상태가 되도록 흐름을 제어하고 싶을 때&lt;/li&gt;
&lt;li&gt;키보드나 콘솔 컨트롤러의 방향키로 UI 선택을 이동시키고 싶을 때(Cardinal Navigation)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 개념&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Input Routing&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Selective Interaction을 구현하기 위해 채택한 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력을 수신하는 조건과 시기를 컨트롤 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예를 들어, 서로 다른 Widget에 버튼 별 Input을 분배해서 배치할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 입력이 각 Widget에서 처리되는 것이 아니라 공용 Class에 전달되어 일괄처리 되기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Node&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI는 Widget을 Node로 변환해 Visual Hierarchy에 따라 상위에 Rendering 된 Widget의 입력을 Route한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;726&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pAkHQ/btsItlG9o92/4kHcJpKLbqStsKyKxrBl20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pAkHQ/btsItlG9o92/4kHcJpKLbqStsKyKxrBl20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pAkHQ/btsItlG9o92/4kHcJpKLbqStsKyKxrBl20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpAkHQ%2FbtsItlG9o92%2F4kHcJpKLbqStsKyKxrBl20%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;572&quot; height=&quot;726&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;726&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;몇 가지 예외를 제외하고, 대부분의 Common UI는 Slate의 Hierarchy와 동일하게 구성된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Tree는 Viewport에 직접 배치 된 Widget을 Root Node로, &lt;br /&gt;Button과 같은 개별 UI Element를 Leaf Node로 취급하여 구성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Common UI는 Tick당 1번씩 다른 Tree보다 상단에 Render 된 Tree를 탐색해 Root Node로 Input을 Route한다.&lt;/li&gt;
&lt;li&gt;그럼 Root Widget은 Input을 처리할 수 있는 첫번째 Leaf Node에 입력을 전달한다.&lt;/li&gt;
&lt;li&gt;입력을 전달 받은 Leaf Node는 이를 처리하거나, 필요하다면 다른 곳으로 재전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ActivatableWidget&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI 중 Input 처리를 위해 Node로 변환되고, 이를 수신할 수 있는 Widget
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Input 수신 시 Active 상태로 간주한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Activatable Widget은 다음 기능을 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Input 수신 가능 여부(Active/Inactive) 상태 토글&lt;/li&gt;
&lt;li&gt;같은 Tree 내 다른 Activatable Widget에 Input 전달&lt;/li&gt;
&lt;li&gt;뒤로가기 등 특정 상황에서 Inactivate&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 Widget을 이용해 현재 Input을 수신중인 Overlay UI가 닫힐 경우, &lt;br /&gt;적절한 Element로 복구해주는 기능을 작업할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Input은 항상 최상단에 Layer에만 Route 되기에 하단 Layer의 Widget도 문제 없이 Active 상태로 둘 수 있다.&lt;/li&gt;
&lt;li&gt;이 때 상단 Layer가 닫히면 자연스럽게 하단 Layer에 Input이 Route 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Viewport Configure&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kKOQJ/btsIsN5vho1/8xdGNngptLy0tKoxrae3Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kKOQJ/btsIsN5vho1/8xdGNngptLy0tKoxrae3Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kKOQJ/btsIsN5vho1/8xdGNngptLy0tKoxrae3Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkKOQJ%2FbtsIsN5vho1%2F8xdGNngptLy0tKoxrae3Wk%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;592&quot; height=&quot;182&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Edit -&amp;gt; Project Settings -&amp;gt; Engine -&amp;gt; General Settings으로 이동&lt;/li&gt;
&lt;li&gt;Game Viewport Client Class를 CommonGameViewportClient 혹은 이를 상속받은 Custom Class로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Create InputActionDataTable&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한가지 잊지 말아야 할 점이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI InputAction DataTable은 Project Setting, Advanced Input System에 사용하는 것과 무관하다.&lt;/li&gt;
&lt;li&gt;오직 UI Input 관리에만 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;539&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pe3LZ/btsIuRFzY7N/dNPqlPh6mDk42s5OKqmlW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pe3LZ/btsIuRFzY7N/dNPqlPh6mDk42s5OKqmlW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pe3LZ/btsIuRFzY7N/dNPqlPh6mDk42s5OKqmlW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPe3LZ%2FbtsIuRFzY7N%2FdNPqlPh6mDk42s5OKqmlW1%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;539&quot; height=&quot;242&quot; data-origin-width=&quot;539&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Content Browser 영역 우클릭 -&amp;gt; Miscellaneous -&amp;gt; CommonUI ActionInput Data Table선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;679&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uZ78T/btsIt97LUBx/5fvaU85yUKTI3gBkr2zQz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uZ78T/btsIt97LUBx/5fvaU85yUKTI3gBkr2zQz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uZ78T/btsIt97LUBx/5fvaU85yUKTI3gBkr2zQz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuZ78T%2FbtsIt97LUBx%2F5fvaU85yUKTI3gBkr2zQz1%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;717&quot; height=&quot;679&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;679&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같이 Row를 추가해 Input을 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1794&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGX6Da/btsIuPVunxr/LzikBjLSJKEb2XhhPYKwk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGX6Da/btsIuPVunxr/LzikBjLSJKEb2XhhPYKwk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGX6Da/btsIuPVunxr/LzikBjLSJKEb2XhhPYKwk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGX6Da%2FbtsIuPVunxr%2FLzikBjLSJKEb2XhhPYKwk1%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;1794&quot; height=&quot;872&quot; data-origin-width=&quot;1794&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이렇게 추가된 InputAction은 CommonUI Widget에서 Mapping할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 사진은 CommonButtonBase의 Class Defaults에서 DataTable과 RowName을 통해 &lt;br /&gt;InputAction을 Button에 Mapping 한 예시이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;좀 더 용이하게 관리하려면 관련된 Action들을 하나의 DataTable에서 작업하여 그룹화 하는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CommonInputActionDataBase&lt;/h4&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_1720592751394&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;USTRUCT(BlueprintType)
struct COMMONUI_API FCommonInputActionDataBase : public FTableRowBase
{
	GENERATED_BODY()

	FCommonInputActionDataBase();
	
	/** User facing name (used when NOT a hold action) */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;CommonInput&quot;)
	FText DisplayName;

	/** User facing name used when it IS a hold action */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;CommonInput&quot;)
	FText HoldDisplayName;

	/** Priority in nav-bar */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;CommonInput&quot;)
	int32 NavBarPriority = 0;

protected:
	/**
	* Key to bind to for each input method
	*/
	UPROPERTY(EditAnywhere, Category = &quot;CommonInput&quot;)
	FCommonInputTypeInfo KeyboardInputTypeInfo;

	/**
	* Default input state for gamepads
	*/
	UPROPERTY(EditAnywhere, Category = &quot;CommonInput&quot;)
	FCommonInputTypeInfo DefaultGamepadInputTypeInfo;

	/**
	* Override the input state for each input method
	*/
	UPROPERTY(EditAnywhere, Category = &quot;CommonInput&quot;, Meta = (GetOptions = &quot;CommonInput.CommonInputBaseControllerData.GetRegisteredGamepads&quot;))
	TMap&amp;lt;FName, FCommonInputTypeInfo&amp;gt; GamepadInputOverrides;

	/**
	* Override the displayed brush for each input method
	*/
	UPROPERTY(EditAnywhere, Category = &quot;CommonInput&quot;)
	FCommonInputTypeInfo TouchInputTypeInfo;
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DisplayName
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 InputAction의 이름&lt;/li&gt;
&lt;li&gt;Navigation Bar가 있는 경우 해당 Bar에 표시된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HoldDisplayName
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버튼 Hold 동작이 필요한 InputAction의 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;NavBarPriority
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Navibation Bar Action을 왼쪽에서 오른쪽으로 Sort할 때 사용하는 Priority&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;KeybaordInputTypeInfo
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마우스 및 키보드 Action의 InputType&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DefaultGamepadInputTypeInfo
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gamepad Action의 InputType&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GamepadInputOverrides
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 Gamepad에서 해당 Action에 사용되는 키&lt;/li&gt;
&lt;li&gt;콘솔 플랫폼 별 버튼 Override에 유용하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Nintendo Switch Gamepad의 앞뒤 버튼을 바꾸는 것&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TouchInputTypeInfo
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Touch Interface Action의 InputType&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CommonInputTypeInfo&lt;/h4&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_1720592773837&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;USTRUCT(BlueprintType)
struct COMMONUI_API FCommonInputTypeInfo
{
	GENERATED_USTRUCT_BODY()

	FCommonInputTypeInfo();
private:
	/** Key this action is bound to	*/
	UPROPERTY(EditAnywhere, Category = &quot;CommonInput&quot;)
	FKey Key;
public:

	/** Get the input type key bound to this input type, with a potential override */
	FKey GetKey() const;

	/** Get the input type key bound to this input type, with a potential override */
	void SetKey(FKey InKey)
	{
		Key = InKey;
	};

	/** EInputActionState::Enabled means that the state isn't overriden and the games dynamic control will work */
	UPROPERTY(EditAnywhere, Category = &quot;CommonInput&quot;)
	EInputActionState OverrrideState;

	/** Enables hold time if true */
	UPROPERTY(EditAnywhere, Category = &quot;CommonInput&quot;)
	bool bActionRequiresHold;

	/** The hold time in seconds */
	UPROPERTY(EditAnywhere, Category = &quot;CommonInput&quot;, meta = (EditCondition = &quot;bActionRequiresHold&quot;, ClampMin = &quot;0.0&quot;, UIMin = &quot;0.0&quot;))
	float HoldTime;

	/**
	*	Time (in seconds) for hold progress to go from 1.0 (completed) to 0.0.
	*	If the hold interaction was interrupted, then hold progress starts to roll back decreasing its value.
	*	Set to 0.0 to disable the rollback functionality.
	*/
	UPROPERTY(EditAnywhere, Category = &quot;CommonInput&quot;, meta = (EditCondition = &quot;bActionRequiresHold&quot;, ClampMin = &quot;0&quot;, UIMin = &quot;0&quot;, ClampMax = &quot;10.0&quot;, UIMax = &quot;10&quot;))
	float HoldRollbackTime;
	
	/** Override the brush specified by the Key Display Data  */
	UPROPERTY(EditAnywhere, Category = &quot;CommonInput&quot;)
	FSlateBrush OverrideBrush;
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bind 될 Input Key&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OverrideState
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Input 및 Callback 호출 여부의 상태를 담당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;bActionRequiredsHold
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Input이 Hold로 작동되어야 하는지 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HoldTime
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Input이 Hold로 작동 되어야 할 때, Hold해야 하는 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HoldRollbackTime
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hold가 작동하고 나서 완전히 비활성화 상태로 되돌아갈 때까지의 시간.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OverrideBrush
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key Display Data에서 정의된 Brush로 Override&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;EInputActionState&lt;/h4&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_1720592790729&quot; class=&quot;crystal&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;UENUM(BlueprintType)
enum class EInputActionState : uint8
{
	/** Enabled, will call all callbacks */
	Enabled,
	/** Disabled, will call all the disabled callback if specified otherwise do nothing */
	Disabled,
	
	/** The common input reflector will not visualize this but still calls all callbacks. NOTE: Use this sparingly */
	Hidden,
	
	/** Hidden and disabled behaves as if it were never added with no callbacks being called */
	HiddenAndDisabled,	
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Enabled
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Input이 활성화 되어 있음&lt;/li&gt;
&lt;li&gt;Callback이 호출 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Disabled
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Disable Callback이 선언되어 있는 경우 이를 호출 함&lt;/li&gt;
&lt;li&gt;그 외의 모든 Callback이 반응하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Hidden
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reflector가 시각적으로 보이지는 않지만 Callback이 호출 됨.&lt;/li&gt;
&lt;li&gt;자주 사용하지 않을 것을 권장 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HiddenAndDisabled
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reflector가 시각적으로 보이지 않고, Callback도 동작하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Default Navigation Action Configure&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Unreal Engine에서 Native Navigation을 지원하지만, &lt;br /&gt;CommonUI을 사용하면 CommonUIInputData를 기반으로 한 별도의 Navigation이 정의되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QGZO7/btsIu6ioh7r/wJVdbzhkxIvc0yXC1dPCK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QGZO7/btsIu6ioh7r/wJVdbzhkxIvc0yXC1dPCK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QGZO7/btsIu6ioh7r/wJVdbzhkxIvc0yXC1dPCK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQGZO7%2FbtsIu6ioh7r%2FwJVdbzhkxIvc0yXC1dPCK0%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;964&quot; height=&quot;446&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Create New Plueprint Class에서 CommonUIInputData를 선택해 BP 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성한 파일 내에 CommonUI InputActionDataTable과 Row를 지정하여 Navigation을 지정&lt;/li&gt;
&lt;li&gt;HoldData의 경우 CommonUIHoldInputData를 기반으로 신규 BP를 생성한 뒤, 해당 파일을 연결해줘야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click Action
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버튼이나 기타 상호작용 가능한 Element를 Highlight 할 때 Mouse Click을 대체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Back Action
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 Menu에서 이전 Menu로 이동할 때 공통으로 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1807&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TUxQE/btsIvbDP4sm/sp9GysLRpEaA6pwlRoIZYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TUxQE/btsIvbDP4sm/sp9GysLRpEaA6pwlRoIZYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TUxQE/btsIvbDP4sm/sp9GysLRpEaA6pwlRoIZYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTUxQE%2FbtsIvbDP4sm%2Fsp9GysLRpEaA6pwlRoIZYK%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;1807&quot; height=&quot;799&quot; data-origin-width=&quot;1807&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Project Settings -&amp;gt; Game -&amp;gt; Common Input Settings의 InputData에 연결한다.&lt;/li&gt;
&lt;li&gt;설정 시, 지정된 Asset을 Default Navigation에 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Bind Controller Data per Platform&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller Data Asset은 Key-Action을 UI Elemnt에 연결해준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Controller Data Asset은 Input Type, Gamepad, Platform과 연관되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CommonUI는 이 정보를 이용해 현재 Platform과 Input Type을 기반으로 &lt;br /&gt;올바른 Platform 별 UI Element를 자동으로 사용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 통해 다수의 Input Type이나 고유한 Gamepad를 지원하는 Platform에서 &lt;br /&gt;User Input을 올바른 GamePad에 전달하거나, Runtime에서 UI Elemnt를 교체할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;1183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwLoV8/btsItjKnvt0/Lv4GfJNAckgyFykGNyPkW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwLoV8/btsItjKnvt0/Lv4GfJNAckgyFykGNyPkW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwLoV8/btsItjKnvt0/Lv4GfJNAckgyFykGNyPkW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwLoV8%2FbtsItjKnvt0%2FLv4GfJNAckgyFykGNyPkW0%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;959&quot; height=&quot;1183&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;1183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Create New Blueprint Class에서 CommonInputBaseControllerData를 선택해 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2401&quot; data-origin-height=&quot;1597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mvX85/btsItJ9OxD6/zOk1N6xoKDG8P6XNuon9xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mvX85/btsItJ9OxD6/zOk1N6xoKDG8P6XNuon9xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mvX85/btsItJ9OxD6/zOk1N6xoKDG8P6XNuon9xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmvX85%2FbtsItJ9OxD6%2FzOk1N6xoKDG8P6XNuon9xK%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;2401&quot; height=&quot;1597&quot; data-origin-width=&quot;2401&quot; data-origin-height=&quot;1597&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성한 모든 Controller Data Asset은 &lt;br /&gt;Project Settings -&amp;gt; Game -&amp;gt; Common Input Settings의 Platform Input에 추가되어야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Platform Input에서 입력하는 Default Gamepad Name은 &lt;br /&gt;Controller Data Asset들의 Gamepad Name 필드들로만 입력되어야 한다.&lt;/li&gt;
&lt;li&gt;이 값이 일치하지 않으면 Controller Data를 인식하지 못하고 Icon이 표시되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;보통은 한 Platform의 Controller Data Array에 여러 개의 Gamepad Data를 작성해 다양한 컨트롤러를 지원한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 별도의 Controller에 대한 게임플레이 지원을 해야 한다면,&lt;br /&gt;Gamepad Data를 새로 작성해 Controller Data에 추가하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CommonInputBaseControllerData&lt;/h4&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_1720595872091&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;/* Derive from this class to store the Input data. It is referenced in the Common Input Settings, found in the project settings UI. */
UCLASS(Abstract, Blueprintable, ClassGroup = Input, meta = (Category = &quot;Common Input&quot;))
class COMMONINPUT_API UCommonInputBaseControllerData : public UObject
{
	GENERATED_BODY()

public:
	virtual bool NeedsLoadForServer() const override;
	virtual bool TryGetInputBrush(FSlateBrush&amp;amp; OutBrush, const FKey&amp;amp; Key) const;
	virtual bool TryGetInputBrush(FSlateBrush&amp;amp; OutBrush, const TArray&amp;lt;FKey&amp;gt;&amp;amp; Keys) const;

	virtual void PreSave(FObjectPreSaveContext ObjectSaveContext) override;
	virtual void PostLoad() override;

private:
#if WITH_EDITORONLY_DATA
	UPROPERTY(Transient, EditAnywhere, Category = &quot;Editor&quot;)
	int32 SetButtonImageHeightTo = 0;
#endif

public:
	UPROPERTY(EditDefaultsOnly, Category = &quot;Default&quot;)
	ECommonInputType InputType;
	
	UPROPERTY(EditDefaultsOnly, Category = &quot;Gamepad&quot;, meta=(EditCondition=&quot;InputType == ECommonInputType::Gamepad&quot;, GetOptions = GetRegisteredGamepads))
	FName GamepadName;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Gamepad&quot;, meta = (EditCondition = &quot;InputType == ECommonInputType::Gamepad&quot;))
	FText GamepadDisplayName;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Gamepad&quot;, meta=(EditCondition=&quot;InputType == ECommonInputType::Gamepad&quot;))
	FText GamepadCategory;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Gamepad&quot;, meta = (EditCondition = &quot;InputType == ECommonInputType::Gamepad&quot;))
	FText GamepadPlatformName;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Gamepad&quot;, meta=(EditCondition=&quot;InputType == ECommonInputType::Gamepad&quot;))
	TArray&amp;lt;FInputDeviceIdentifierPair&amp;gt; GamepadHardwareIdMapping;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Display&quot;)
	TSoftObjectPtr&amp;lt;UTexture2D&amp;gt; ControllerTexture;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Display&quot;)
	TSoftObjectPtr&amp;lt;UTexture2D&amp;gt; ControllerButtonMaskTexture;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Display&quot;, Meta = (TitleProperty = &quot;Key&quot;))
	TArray&amp;lt;FCommonInputKeyBrushConfiguration&amp;gt; InputBrushDataMap;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Display&quot;, Meta = (TitleProperty = &quot;Keys&quot;))
	TArray&amp;lt;FCommonInputKeySetBrushConfiguration&amp;gt; InputBrushKeySets;

	UFUNCTION()
	static const TArray&amp;lt;FName&amp;gt;&amp;amp; GetRegisteredGamepads();

private:
#if WITH_EDITOR
	virtual void PostEditChangeProperty(struct FPropertyChangedEvent&amp;amp; PropertyChangedEvent) override;
#endif
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InputType
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonUI에서 인식하는 Input의 종류&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720595872093&quot; class=&quot;crystal&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;UENUM(BlueprintType)
enum class ECommonInputType : uint8
{
	MouseAndKeyboard,
	Gamepad,
	Touch,
	Count
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GamepadName
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller가 Gamepad인 경우에 해당 Gamepad가 대응 할 Platform&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720595872094&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;USTRUCT()
struct FInputDeviceIdentifierPair
{
	GENERATED_BODY()

	UPROPERTY(EditDefaultsOnly, Category = &quot;Gamepad&quot;)
	FName InputDeviceName;

	UPROPERTY(EditDefaultsOnly, Category = &quot;Gamepad&quot;)
	FString HardwareDeviceIdentifier;
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GamepadhardwareIdMapping
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gamepad Hardware ID를 게임에서 사용하는 Key값과 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720595872095&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;USTRUCT(Blueprintable)
struct COMMONINPUT_API FCommonInputKeyBrushConfiguration
{
	GENERATED_BODY()

public:
	FCommonInputKeyBrushConfiguration();

	const FSlateBrush&amp;amp; GetInputBrush() const { return KeyBrush; }

public:
	UPROPERTY(EditAnywhere, Category = &quot;Key Brush Configuration&quot;)
	FKey Key;

	UPROPERTY(EditAnywhere, Category = &quot;Key Brush Configuration&quot;)
	FSlateBrush KeyBrush;
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InputBrushDataMap
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI Element 및 Icon에 대한 Key Mapping&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720595872096&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;USTRUCT(Blueprintable)
struct COMMONINPUT_API FCommonInputKeySetBrushConfiguration
{
	GENERATED_BODY()

public:
	FCommonInputKeySetBrushConfiguration();

	const FSlateBrush&amp;amp; GetInputBrush() const { return KeyBrush; }

public:
	UPROPERTY(EditAnywhere, Category = &quot;Key Brush Configuration&quot;, Meta = (TitleProperty = &quot;KeyName&quot;))
	TArray&amp;lt;FKey&amp;gt; Keys;

	UPROPERTY(EditAnywhere, Category = &quot;Key Brush Configuration&quot;)
	FSlateBrush KeyBrush;
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InputBrushKeySets
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 UI Element에 대한 다수의 Key Mapping&lt;/li&gt;
&lt;li&gt;D-Pad 및 기타 여러 Axis에 Mapping 될 가능성이 있는 Input에 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Common UI widget&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common UI Plugin에서 제공하는 Widget&lt;/li&gt;
&lt;li&gt;기존에 자주 사용하는 Widget의 기능을 거의 그대로 제공
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다만 Base UMG Widget Style 기능은 제공하지 않는다.&lt;/li&gt;
&lt;li&gt;대신 Common Style Asset을 참조해 다수의 Menu와 HUD에 일관된 Style을 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;Styple Asset을 변경하면 모든 Common UI Widget에 효과가 나타난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자세한 지원 Widget 항목에 대해서는 아래 개별 포스트 참고&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://redchiken.tistory.com/394&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://redchiken.tistory.com/394&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Common Style Asset&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dl3mgY/btsItmG5Kg9/lMu3e1YizhNi8VvJ9Hs6e0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dl3mgY/btsItmG5Kg9/lMu3e1YizhNi8VvJ9Hs6e0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dl3mgY/btsItmG5Kg9/lMu3e1YizhNi8VvJ9Hs6e0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdl3mgY%2FbtsItmG5Kg9%2FlMu3e1YizhNi8VvJ9Hs6e0%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;930&quot; height=&quot;880&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Create New Blueprint Class를 통해 사진과 같이 Common Style BP를 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;963&quot; data-origin-height=&quot;1209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/REh4i/btsItTkeINU/4qi1YD4kBH0citWvOltcM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/REh4i/btsItTkeINU/4qi1YD4kBH0citWvOltcM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/REh4i/btsItTkeINU/4qi1YD4kBH0citWvOltcM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FREh4i%2FbtsItTkeINU%2F4qi1YD4kBH0citWvOltcM0%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;963&quot; height=&quot;1209&quot; data-origin-width=&quot;963&quot; data-origin-height=&quot;1209&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성한 Common Style BP의 Detail 항목에 필요한 정보를 채워놓는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3831&quot; data-origin-height=&quot;1303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tvMPo/btsItC35mQ4/hY8vYPQGXXQiKoBvyw7WYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tvMPo/btsItC35mQ4/hY8vYPQGXXQiKoBvyw7WYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tvMPo/btsItC35mQ4/hY8vYPQGXXQiKoBvyw7WYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtvMPo%2FbtsItC35mQ4%2FhY8vYPQGXXQiKoBvyw7WYk%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;3831&quot; height=&quot;1303&quot; data-origin-width=&quot;3831&quot; data-origin-height=&quot;1303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성한 Common Style BP는 이를 필요로 하는 다른 CommonUI에 연결하여 적용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 사진의 경우, Common Button의 Widget Element인 Common Text에 CommonTextStyle을 적용하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2299&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR9O7i/btsIuoD1cBa/AgKWQsp4LbKpkO3znhmBOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR9O7i/btsIuoD1cBa/AgKWQsp4LbKpkO3znhmBOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR9O7i/btsIuoD1cBa/AgKWQsp4LbKpkO3znhmBOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR9O7i%2FbtsIuoD1cBa%2FAgKWQsp4LbKpkO3znhmBOK%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;2299&quot; height=&quot;1080&quot; data-origin-width=&quot;2299&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성된 Common Style BP는 Project Settings -&amp;gt; Plugins -&amp;gt; Common UI Editor의 Template Styles에 할당할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그럼 수동으로 설정되지 않은 Common BP에서 Template Style을 자동으로 사용하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2331&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hyb1O/btsIvsSX8sg/EFdRizIB540ckwdzwCwTZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hyb1O/btsIvsSX8sg/EFdRizIB540ckwdzwCwTZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hyb1O/btsIvsSX8sg/EFdRizIB540ckwdzwCwTZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHyb1O%2FbtsIvsSX8sg%2FEFdRizIB540ckwdzwCwTZ1%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;2331&quot; height=&quot;1125&quot; data-origin-width=&quot;2331&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이와 같은 맥락으로, Project Settings -&amp;gt; Plugins -&amp;gt; Common UI Framework에서 &lt;br /&gt;몇 가지 Global Assset을 추가로 지원한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Loading 화면에서 쓰는 Default Throbber Material&lt;/li&gt;
&lt;li&gt;Load되지 않은 UI Asset에 표시되는 Default Image Resource Object 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Technical Guide&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Designer나 Artist는 크게 신경 안 써도 되지만 Tech는 알아둬야 할 코드 영역 내용&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redchiken.tistory.com/395&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://redchiken.tistory.com/395&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FAQ&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Design이나 구조적으로 자주 나오는 질문에 대한 모음&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redchiken.tistory.com/396&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://redchiken.tistory.com/396&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1720598237181&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[UI] Common UI FAQ&quot; data-og-description=&quot;Common UI의 사용 여부 판단주로 다음 상황에서 Common UI 사용이 권장된다.복잡한 Multiple Layer UI를 제공해야 하는 경우Cross-Platform을 지원하는 경우반대로 이 두 케이스에 모두 해당되지 않으면 Common U&quot; data-og-host=&quot;redchiken.tistory.com&quot; data-og-source-url=&quot;https://redchiken.tistory.com/396&quot; data-og-url=&quot;https://redchiken.tistory.com/396&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c52rT5/hyWvXfeodf/5KSTW9gmLs2lyZQ0ZxKjZ1/img.jpg?width=640&amp;amp;height=296&amp;amp;face=0_0_640_296,https://scrap.kakaocdn.net/dn/bSh0Hz/hyWvUpjd8t/bPzA7xwYcfx1tx7W6ah3h0/img.jpg?width=640&amp;amp;height=296&amp;amp;face=0_0_640_296,https://scrap.kakaocdn.net/dn/bfMeXL/hyWzsxTTez/4xTQ9rRuvaHR5broke3eTk/img.jpg?width=640&amp;amp;height=296&amp;amp;face=0_0_640_296&quot;&gt;&lt;a href=&quot;https://redchiken.tistory.com/396&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://redchiken.tistory.com/396&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c52rT5/hyWvXfeodf/5KSTW9gmLs2lyZQ0ZxKjZ1/img.jpg?width=640&amp;amp;height=296&amp;amp;face=0_0_640_296,https://scrap.kakaocdn.net/dn/bSh0Hz/hyWvUpjd8t/bPzA7xwYcfx1tx7W6ah3h0/img.jpg?width=640&amp;amp;height=296&amp;amp;face=0_0_640_296,https://scrap.kakaocdn.net/dn/bfMeXL/hyWzsxTTez/4xTQ9rRuvaHR5broke3eTk/img.jpg?width=640&amp;amp;height=296&amp;amp;face=0_0_640_296');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[UI] Common UI FAQ&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Common UI의 사용 여부 판단주로 다음 상황에서 Common UI 사용이 권장된다.복잡한 Multiple Layer UI를 제공해야 하는 경우Cross-Platform을 지원하는 경우반대로 이 두 케이스에 모두 해당되지 않으면 Common U&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;redchiken.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>UE5/UI</category>
      <author>RedChiken</author>
      <guid isPermaLink="true">https://redchiken.tistory.com/393</guid>
      <comments>https://redchiken.tistory.com/393#entry393comment</comments>
      <pubDate>Wed, 10 Jul 2024 16:42:08 +0900</pubDate>
    </item>
    <item>
      <title>[UI] UMG ViewModel</title>
      <link>https://redchiken.tistory.com/392</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/umg-viewmodel?application_version=5.3#heading=h.qln36ehrtib7&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/umg-viewmodel?application_version=5.3#heading=h.qln36ehrtib7&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://harrisbarra.medium.com/ue5-mvvm-36907bdb34d9&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://harrisbarra.medium.com/ue5-mvvm-36907bdb34d9&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720416044029&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;UE5 &amp;amp; MVVM&quot; data-og-description=&quot;Clean &amp;amp; Modular solution w/ example&quot; data-og-host=&quot;harrisbarra.medium.com&quot; data-og-source-url=&quot;https://harrisbarra.medium.com/ue5-mvvm-36907bdb34d9&quot; data-og-url=&quot;https://harrisbarra.medium.com/ue5-mvvm-36907bdb34d9&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cRSE8K/hyWvTwQkxm/DtKvpVaKKcKisdkfAE20uk/img.png?width=1200&amp;amp;height=199&amp;amp;face=0_0_1200_199&quot;&gt;&lt;a href=&quot;https://harrisbarra.medium.com/ue5-mvvm-36907bdb34d9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://harrisbarra.medium.com/ue5-mvvm-36907bdb34d9&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cRSE8K/hyWvTwQkxm/DtKvpVaKKcKisdkfAE20uk/img.png?width=1200&amp;amp;height=199&amp;amp;face=0_0_1200_199');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;UE5 &amp;amp; MVVM&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Clean &amp;amp; Modular solution w/ example&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;harrisbarra.medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/community/learning/talks-and-demos/pw3Y/unreal-engine-umg-viewmodels-building-more-robust-and-testable-uis-using-mvvm-unreal-fest-2023?locale=ko-kr&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.epicgames.com/community/learning/talks-and-demos/pw3Y/unreal-engine-umg-viewmodels-building-more-robust-and-testable-uis-using-mvvm-unreal-fest-2023?locale=ko-kr&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://miltoncandelero.github.io/unreal-viewmodel&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://miltoncandelero.github.io/unreal-viewmodel&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720436756118&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Model View ViewModel for Game Devs&quot; data-og-description=&quot;How to turn something meant for the boring app world into something useful for programming our game UIs&quot; data-og-host=&quot;miltoncandelero.github.io&quot; data-og-source-url=&quot;https://miltoncandelero.github.io/unreal-viewmodel&quot; data-og-url=&quot;https://miltoncandelero.github.io/unreal-viewmodel&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/biUXLW/hyWzAPXMPR/U9pd2djWk2KFvRk7mvvHtk/img.png?width=2878&amp;amp;height=2590&amp;amp;face=0_0_2878_2590,https://scrap.kakaocdn.net/dn/rRIhY/hyWvPus5Jj/FkhUxx8IAYBr9e0CMlW5ik/img.png?width=1252&amp;amp;height=681&amp;amp;face=0_0_1252_681&quot;&gt;&lt;a href=&quot;https://miltoncandelero.github.io/unreal-viewmodel&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://miltoncandelero.github.io/unreal-viewmodel&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/biUXLW/hyWzAPXMPR/U9pd2djWk2KFvRk7mvvHtk/img.png?width=2878&amp;amp;height=2590&amp;amp;face=0_0_2878_2590,https://scrap.kakaocdn.net/dn/rRIhY/hyWvPus5Jj/FkhUxx8IAYBr9e0CMlW5ik/img.png?width=1252&amp;amp;height=681&amp;amp;face=0_0_1252_681');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Model View ViewModel for Game Devs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;How to turn something meant for the boring app world into something useful for programming our game UIs&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;miltoncandelero.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&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;WorkFlow&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2sUg9/btsIsNJwfkr/2jUEimh6ch4oN9VxIAsUik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2sUg9/btsIsNJwfkr/2jUEimh6ch4oN9VxIAsUik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2sUg9/btsIsNJwfkr/2jUEimh6ch4oN9VxIAsUik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2sUg9%2FbtsIsNJwfkr%2F2jUEimh6ch4oN9VxIAsUik%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;1280&quot; height=&quot;211&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Programmer&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View Model 자체를 생성, Build
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View Model은 UI에서 사용할 수 있는 변수들을 포함 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Application의 코드와 결합&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Designer&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View Binding 패널을 사용해 UI에서 ViewModel의 변수에 Bind&lt;/li&gt;
&lt;li&gt;UMG Widget에 ViewModel 추가 시 다음 기능들을 이용할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Access&lt;/li&gt;
&lt;li&gt;함수 호출&lt;/li&gt;
&lt;li&gt;변수 업데이트&lt;/li&gt;
&lt;li&gt;변수 업데이트에 대한 Push 이벤트 Delegate
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 변수 값이 수정 될 때 등록된 Widget만 업데이트 하기 때문에 Attribute Bind보다 훨씬 효과적이다.&lt;/li&gt;
&lt;li&gt;동시에 시간 설정을 수동으로 구현 할 필요도 없어 Event Driven UI Framework의 이점을 살리기 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Config&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R3e7R/btsIrxt9C5x/wvtmziLcS2nmLKy6K2iMtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R3e7R/btsIrxt9C5x/wvtmziLcS2nmLKy6K2iMtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R3e7R/btsIrxt9C5x/wvtmziLcS2nmLKy6K2iMtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR3e7R%2FbtsIrxt9C5x%2FwvtmziLcS2nmLKy6K2iMtk%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;927&quot; height=&quot;400&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;View Model&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI에 필요한 변수의 Manifest 관리&lt;/li&gt;
&lt;li&gt;UI와 Application의 기타 요소간 Communication을 위한 매개체&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;UI가 변수를 인지해야 하는 경우&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel에 변수 추가&lt;/li&gt;
&lt;li&gt;Widget에 ViewModel 추가&lt;/li&gt;
&lt;li&gt;Widget의 Field를 ViewModel에 Bind&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;변수를 업데이트 해야 하는 경우&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel의 Reference를 가지고 있다면 언제든 직접 접근하여 수정&lt;/li&gt;
&lt;li&gt;변경 된 변수에 Bind된 Widget을 Notify하고 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ViewModel in BP&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Creation&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pKtcC/btsIrQfZMPJ/XKNx9wioRwk9khkSo6jgSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pKtcC/btsIrQfZMPJ/XKNx9wioRwk9khkSo6jgSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pKtcC/btsIrQfZMPJ/XKNx9wioRwk9khkSo6jgSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpKtcC%2FbtsIrQfZMPJ%2FXKNx9wioRwk9khkSo6jgSk%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;620&quot; height=&quot;529&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Content Borwser를 우클릭하여 New Blueprint를 통해 ViewMode BP 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel은 Widget이 아니기 때문에 User Interface에 없고 일반 BP 생성 방식을 통해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;FieldNotify Variable&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CA0cZ/btsIqeoRHSt/ITV1BrzpdK48fmOefDTun0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CA0cZ/btsIqeoRHSt/ITV1BrzpdK48fmOefDTun0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CA0cZ/btsIqeoRHSt/ITV1BrzpdK48fmOefDTun0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCA0cZ%2FbtsIqeoRHSt%2FITV1BrzpdK48fmOefDTun0%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;261&quot; height=&quot;84&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;84&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BP의 변수 옆에 종 모양 UI가 FieldNotify 활성화 여부이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YxyeF/btsIsviaoF5/IJUy0vGZFjUfOIGBKn8cY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YxyeF/btsIsviaoF5/IJUy0vGZFjUfOIGBKn8cY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YxyeF/btsIsviaoF5/IJUy0vGZFjUfOIGBKn8cY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYxyeF%2FbtsIsviaoF5%2FIJUy0vGZFjUfOIGBKn8cY0%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;909&quot; height=&quot;254&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FieldNotify가 활성화 된 변수의 Set은 BP 라벨의 이름이 Set w/ Broadcast로 지정된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같이 설정 된 변수들은 값이 변경될 때마다 Bind된 Widget에 Update 메시지를 전송한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;FieldNotify Function&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1795&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uXBZF/btsIsIO5nXY/LsbceblYLpiSVbrEyKBp3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uXBZF/btsIsIO5nXY/LsbceblYLpiSVbrEyKBp3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uXBZF/btsIsIO5nXY/LsbceblYLpiSVbrEyKBp3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuXBZF%2FbtsIsIO5nXY%2FLsbceblYLpiSVbrEyKBp3K%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;1795&quot; height=&quot;914&quot; data-origin-width=&quot;1795&quot; data-origin-height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Function 역시 FieldNotify로 취급 될 수 있으나 변수에 비해 몇 가지 조건을 요구한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pure Function일 것&lt;/li&gt;
&lt;li&gt;Const 마킹이 되어 있을 것&lt;/li&gt;
&lt;li&gt;하나의 값만 반환할 것&lt;/li&gt;
&lt;li&gt;Input Parameter가 없을 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;FieldNotify를 사용한다면 가급적 값을 반환하는 목적만 있는 Getter 생성은 지양해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;차후에 Widget을 Bind하려 할 때 추가되는 Getter 함수와 햇갈릴 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Bind FieldNotify to Another FieldNotify&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1776&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edweS4/btsIqOQJHV6/WV4RVdKKJcACYav4Kh13kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edweS4/btsIqOQJHV6/WV4RVdKKJcACYav4Kh13kk/img.png&quot; data-alt=&quot;변수인 CurrentHP, MaxHP 뿐 아니라 Function인 GetHPPercent까지 FieldNotify 항목에 추가됨을 확인할 수 있따.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edweS4/btsIqOQJHV6/WV4RVdKKJcACYav4Kh13kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FedweS4%2FbtsIqOQJHV6%2FWV4RVdKKJcACYav4Kh13kk%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;1776&quot; height=&quot;864&quot; data-origin-width=&quot;1776&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;변수인 CurrentHP, MaxHP 뿐 아니라 Function인 GetHPPercent까지 FieldNotify 항목에 추가됨을 확인할 수 있따.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 FieldNotify 변수가 변경 되는 경우, 그 변수에 FieldNotify Function을 Bind 해줘야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 비단 Function 뿐 아니라 FieldNotify 변수에 다른 FieldNotify 변수를 Bind할 수도 있다.&lt;/li&gt;
&lt;li&gt;FieldNotify에 다른 FieldNotify 변수나 함수가 Bind되어 있다면, &lt;br /&gt;변수가 수정될 때 Bind 된 함수의 실행은 물론 Bind된 변수에 Bind 된 Widget에까지 Update가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ViewModel in C++&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Creation&lt;/h3&gt;
&lt;pre id=&quot;code_1720420528596&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;MVVMViewModelBase.h&quot;
#include &quot;MyMVVMViewModelBase.generated.h&quot;

UCLASS()
class MYTEST_API UMyMVVMViewModelBase : public UMVVMViewModelBase
{
	GENERATED_BODY()
	
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적인 ViewModel은 UMVVMViewModelBase를 상속 받아 생성할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720420773760&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class INotifyFieldValueChanged : public IInterface
{
	GENERATED_BODY()

public:
	// using &quot;not checked&quot; user policy (means race detection is disabled) because this delegate is stored in a container and causes its reallocation
	// from inside delegate's execution. This is incompatible with race detection that needs to access the delegate instance after its execution
	using FFieldValueChangedDelegate = TDelegate&amp;lt;void(UObject*, UE::FieldNotification::FFieldId), FNotThreadSafeNotCheckedDelegateUserPolicy&amp;gt;;

public:
	/** Add a delegate that will be notified when the FieldId is value changed. */
	virtual FDelegateHandle AddFieldValueChangedDelegate(UE::FieldNotification::FFieldId InFieldId, FFieldValueChangedDelegate InNewDelegate) = 0;

	/** Remove a delegate that was added. */
	virtual bool RemoveFieldValueChangedDelegate(UE::FieldNotification::FFieldId InFieldId, FDelegateHandle InHandle) = 0;

	/** Remove all the delegate that are bound to the specified UserObject. */
	virtual int32 RemoveAllFieldValueChangedDelegates(const void* InUserObject) = 0;

	/** Remove all the delegate that are bound to the specified Field and UserObject. */
	virtual int32 RemoveAllFieldValueChangedDelegates(UE::FieldNotification::FFieldId InFieldId, const void* InUserObject) = 0;

	/** @returns the list of all the field that can notify when their value changes. */
	virtual const UE::FieldNotification::IClassDescriptor&amp;amp; GetFieldNotificationDescriptor() const = 0;

	/** Broadcast to the registered delegate that the FieldId value changed. */
	virtual void BroadcastFieldValueChanged(UE::FieldNotification::FFieldId InFieldId) = 0;
};&lt;/code&gt;&lt;/pre&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;하지만 이보다 더 근본적으로, INotifyFieldValueChanged만을 Implement 하여 생성할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720420749465&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;/** Base class for MVVM viewmodel. */
UCLASS(Blueprintable, Abstract, DisplayName=&quot;MVVM Base Viewmodel&quot;)
class MODELVIEWVIEWMODEL_API UMVVMViewModelBase : public UObject, public INotifyFieldValueChanged
{
	GENERATED_BODY()

public:
	//~ Begin INotifyFieldValueChanged Interface
	virtual FDelegateHandle AddFieldValueChangedDelegate(UE::FieldNotification::FFieldId InFieldId, FFieldValueChangedDelegate InNewDelegate) override final;
	virtual bool RemoveFieldValueChangedDelegate(UE::FieldNotification::FFieldId InFieldId, FDelegateHandle InHandle) override final;
	virtual int32 RemoveAllFieldValueChangedDelegates(const void* InUserObject) override final;
	virtual int32 RemoveAllFieldValueChangedDelegates(UE::FieldNotification::FFieldId InFieldId, const void* InUserObject) override final;
	virtual const UE::FieldNotification::IClassDescriptor&amp;amp; GetFieldNotificationDescriptor() const override;
	virtual void BroadcastFieldValueChanged(UE::FieldNotification::FFieldId InFieldId) override;
	//~ End INotifyFieldValueChanged Interface

/*etc*/
};&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sum of Code&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C++에서 ViewModel을 구성하는 방법은 대략 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720421511066&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS(BlueprintType)
class UVMCharacterHealth : public UMVVMViewModelBase
{
GENERATED_BODY()

private:
UPROPERTY(BlueprintReadWrite, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess))
    int32 CurrentHealth;

    UPROPERTY(BlueprintReadWrite, FieldNotify, Setter, Getter, meta=(AllowPrivateAccess))
    int32 MaxHealth;

public:
    void SetCurrentHealth(int32 NewCurrentHealth)
    {
        if (UE_MVVM_SET_PROPERTY_VALUE(CurrentHealth, NewCurrentHealth))
        {
            UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthPercent);
        }
    }

    void SetMaxHealth(int32 NewMaxHealth)
    {
        if (UE_MVVM_SET_PROPERTY_VALUE(MaxHealth, NewMaxHealth))
        {
            UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthPercent);
        }

    }

    int32 GetCurrentHealth() const
    {
        return CurrentHealth;
    }

    int32 GetMaxHealth() const
    {
        return MaxHealth;
    }

public:

    UFUNCTION(BlueprintPure, FieldNotify)

    float GetHealthPercent() const
    {
        if (MaxHealth != 0)
        {
            return (float) CurrentHealth / (float) MaxHealth;
        }
        else
            return 0;
    }

};&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;FieldNotify Variable&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FieldNotify 변수를 선언하려면 UPROPERTY에서 다음 지정자들을 지정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1821&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qB4ep/btsIqebn1AD/PdlT2S5C9Yo2sLbcOqo7K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qB4ep/btsIqebn1AD/PdlT2S5C9Yo2sLbcOqo7K1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qB4ep/btsIqebn1AD/PdlT2S5C9Yo2sLbcOqo7K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqB4ep%2FbtsIqebn1AD%2FPdlT2S5C9Yo2sLbcOqo7K1%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;1821&quot; height=&quot;542&quot; data-origin-width=&quot;1821&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FieldNotify를 선언하지 않은 경우 Onetime, &lt;br /&gt;즉 변수가 최초로 변경될 때에만 Notify가 발생하고 그 이후에는 발생하지 않는다.&lt;/li&gt;
&lt;li&gt;Setter와 Getter는 필요에 따라 추가한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단, 추가하지 않으면 해당 동작에 대한 연산은 해당 Class는 물론 하위 Class에서도 수행할 수 없다.&lt;/li&gt;
&lt;li&gt;Getter/Setter은 Bind 된 Function 실행이나 FieldNotify Update 외에 변수값을 얻기 전에 연산을 해야 하는 경우에도 적절하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Getter/Setter함수는 UFUNCTION으로 생성할 경우 BP에서 상당히 많은 목록을 생성하기에 이를 지양하는 것이 좋다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UPROPERTY에서 이미 FieldNotify의 Get/Set에 연결을 해준 상태다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Unreal Engine은 FiendNotify 변수에 대한 Getter/Setter 함수 호출을 강제하지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 귀책 실수를 줄이고 싶다면 접근제어자를 조절하는 것이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BP에서는 FieldNotify 변수가 변경될 때 Bind된 FieldNotify 변수나 함수가 자동으로 Update되지만.&lt;br /&gt;C++로 작업한다면 이들을 직접 호출해줘야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;FieldNotify Function&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FieldNotify Function은 다음 조건을 만족해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UFUNCTION에서 BlueprintPure, FieldNotify 지정자 선언&lt;/li&gt;
&lt;li&gt;Parameter가 없어야 함&lt;/li&gt;
&lt;li&gt;const 선언이 되어 있어야 함&lt;/li&gt;
&lt;li&gt;out 인자 없이 단일 값을 반환해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Widget이 특정 변수에 Bind 되어 있으면서 동시에 값을 직접 사용하지 않고 연산을 거쳐야 하는 경우에 유용하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일종의 임시 변수 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;FieldNotify Macro&lt;/h4&gt;
&lt;pre id=&quot;code_1720423460852&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** After a field value changed. Broadcast the event. */
#define UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(MemberName) \
	BroadcastFieldValueChanged(ThisClass::FFieldNotificationClassDescriptor::MemberName)

/** If the property value changed then set the new value and notify. */
#define UE_MVVM_SET_PROPERTY_VALUE(MemberName, NewValue) \
	SetPropertyValue(MemberName, NewValue, ThisClass::FFieldNotificationClassDescriptor::MemberName)

/** Use this version to set property values that can't be captured as a function arguments (i.e. bitfields). */
#define UE_MVVM_SET_PROPERTY_VALUE_INLINE(MemberName, NewValue) \
	[this, InNewValue = (NewValue)]() { if (MemberName == InNewValue) { return false; } MemberName = InNewValue; BroadcastFieldValueChanged(ThisClass::FFieldNotificationClassDescriptor::MemberName); return true; }()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Add ViewModel to Widget&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;343&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbEI5d/btsIs63mE7m/SHsFTM03A3bkAPlFQooHu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbEI5d/btsIs63mE7m/SHsFTM03A3bkAPlFQooHu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbEI5d/btsIs63mE7m/SHsFTM03A3bkAPlFQooHu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbEI5d%2FbtsIs63mE7m%2FSHsFTM03A3bkAPlFQooHu1%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;343&quot; height=&quot;324&quot; data-origin-width=&quot;343&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget을 생성하고 Window-&amp;gt;Viewmodels항목을 선택하면 다음 창이 뜬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;606&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rBJLS/btsIsuXYxti/4gDTJzwiQ8jri6EejXzaOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rBJLS/btsIsuXYxti/4gDTJzwiQ8jri6EejXzaOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rBJLS/btsIsuXYxti/4gDTJzwiQ8jri6EejXzaOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrBJLS%2FbtsIsuXYxti%2F4gDTJzwiQ8jri6EejXzaOK%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;606&quot; height=&quot;564&quot; data-origin-width=&quot;606&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 창에서 미리 만든 ViewModel을 선택하면 된다.&lt;/li&gt;
&lt;li&gt;ViewModel은 여러 개 등록 할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그 말은 Widget과 ViewModel의 관계는 1:1이 아니라 다:다라는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Initialize ViewModel&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GtUsj/btsIry1bUWR/K44JAzIWxFANOp6bMZDj21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GtUsj/btsIry1bUWR/K44JAzIWxFANOp6bMZDj21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GtUsj/btsIry1bUWR/K44JAzIWxFANOp6bMZDj21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGtUsj%2FbtsIry1bUWR%2FK44JAzIWxFANOp6bMZDj21%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;1229&quot; height=&quot;525&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel을 처음 추가하면 위와 같은 Noti를 볼 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 현재 Widget에서 등록된 ViewModel에 Bind가 없기에 굳이 Initialize를 하지 않겠다는 의미이다.&lt;/li&gt;
&lt;li&gt;View Binding에서 Bind를 추가해주면 관계가 생성되며 자동으로 Instance가 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Create Instance&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget Instance 별로 각각 새로운 ViewModel Instance를 자동 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일한 Widget이 Viewport 상 여러 개 존재하더라도 &lt;br /&gt;하나의 변수를 수정하면 그 ViewModel에 해당하는 Widget만 Update 된다.&lt;/li&gt;
&lt;li&gt;이와 동일하게, 여러 개의 서로 다른 Widget을 생성할 때에도 다른 Widget의 정보 변경을 인지하지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;C++의 Call-back 초기화 다음, 혹은 BP의 Call-back 초기화 도중에 ViewModel을 할당할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel이 설정되지 않으면 새 Instance만 생성한다.&lt;/li&gt;
&lt;li&gt;ViewModel은 PreConstruct와 Construct 사이에 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Manual&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 상 특정 위치에서 Instance를 생성하고 Widget에 할당하는 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget은 Reference를 가지지만, 할당되기 전까지는 Null 값을 갖는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;495&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmuKr2/btsIs205ztJ/LMjS4za9bBWTx1X3BkkDsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmuKr2/btsIs205ztJ/LMjS4za9bBWTx1X3BkkDsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmuKr2/btsIs205ztJ/LMjS4za9bBWTx1X3BkkDsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmuKr2%2FbtsIs205ztJ%2FLMjS4za9bBWTx1X3BkkDsk%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;1280&quot; height=&quot;495&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;495&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Create Widget 노드에서 생성 시 ViewModel을 할당할 수도 있다.&lt;/li&gt;
&lt;li&gt;ViewModel을 할당하면 Widget에 대한 Reference를 구하지 않고 UI를 Update 할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 방법을 통해 UI가 하나의 Actor Class로부터 &lt;br /&gt;서로 다른 다수의 Widget에 동일한 ViewModel을 할당할 수 있게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Global Viewmodel Collection&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MVVMSubsystem에서 Global로 Access 할 수 있는 ViewModel 목록&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720431104076&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS(DisplayName=&quot;Viewmodel Engine Subsytem&quot;)
class MODELVIEWVIEWMODEL_API UMVVMSubsystem : public UEngineSubsystem
{
	GENERATED_BODY()

public:
	//~ Begin UEngineSubsystem interface
	virtual void Initialize(FSubsystemCollectionBase&amp;amp; Collection) override;
	virtual void Deinitialize() override;
	//~ End UEngineSubsystem interface

	UFUNCTION(BlueprintCallable, Category = &quot;Viewmodel&quot;, meta = (DisplayName = &quot;Get View From User Widget&quot;))
	UMVVMView* K2_GetViewFromUserWidget(const UUserWidget* UserWidget) const;

	static UMVVMView* GetViewFromUserWidget(const UUserWidget* UserWidget);

	UFUNCTION(BlueprintCallable, Category = &quot;Viewmodel&quot;)
	bool DoesWidgetTreeContainedWidget(const UWidgetTree* WidgetTree, const UWidget* ViewWidget) const;

	/** @return The list of all the AvailableBindings that are available for the Class. */
	UFUNCTION(BlueprintCallable, Category = &quot;Viewmodel&quot;, meta = (DisplayName = &quot;Get Available Bindings&quot;))
	TArray&amp;lt;FMVVMAvailableBinding&amp;gt; K2_GetAvailableBindings(const UClass* Class, const UClass* Accessor) const;

	static TArray&amp;lt;FMVVMAvailableBinding&amp;gt; GetAvailableBindings(const UClass* Class, const UClass* Accessor);

	/**
	 * @return The list of all the AvailableBindings that are available from the SriptStuct.
	 * @note When FMVVMAvailableBinding::HasNotify is false, a notification can still be triggered by the owner of the struct. The struct changed but which property of the struct changed is unknown.
	 */
	static TArray&amp;lt;FMVVMAvailableBinding&amp;gt; GetAvailableBindingsForStruct(const UScriptStruct* Struct);

	static TArray&amp;lt;FMVVMAvailableBinding&amp;gt; GetAvailableBindingsForEvent(const UClass* Class, const UClass* Accessor);

	/** @return The AvailableBinding from a BindingName. */
	UFUNCTION(BlueprintCallable, Category = &quot;Viewmodel&quot;, meta = (DisplayName = &quot;Get Available Binding&quot;))
	FMVVMAvailableBinding K2_GetAvailableBinding(const UClass* Class, FMVVMBindingName BindingName, const UClass* Accessor) const;

	static FMVVMAvailableBinding GetAvailableBinding(const UClass* Class, FMVVMBindingName BindingName, const UClass* Accessor);

	/** @return The AvailableBinding from a field. */
	static FMVVMAvailableBinding GetAvailableBindingForField(UE::MVVM::FMVVMConstFieldVariant Variant, const UClass* Accessor);

	static FMVVMAvailableBinding GetAvailableBindingForEvent(UE::MVVM::FMVVMConstFieldVariant FieldVariant, const UClass* Accessor);

	static FMVVMAvailableBinding GetAvailableBindingForEvent(const UClass* Class, FMVVMBindingName BindingName, const UClass* Accessor);
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Option과 같이 UI를 통해 Access되어야 하는 변수 처리에 이상적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ulKdh/btsIrJOXu4j/qvNN8Y0eakKzxMuEinCWrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ulKdh/btsIrJOXu4j/qvNN8Y0eakKzxMuEinCWrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ulKdh/btsIrJOXu4j/qvNN8Y0eakKzxMuEinCWrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FulKdh%2FbtsIrJOXu4j%2FqvNN8Y0eakKzxMuEinCWrk%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;1188&quot; height=&quot;306&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Property Path&lt;/span&gt;&lt;/h4&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 방식에 비해 좀 더 명확하고 코드 작업을 덜 요구하는 방식&lt;/li&gt;
&lt;li&gt;다른 Class들이 Viewmodel Reference를 구하기 위해 Widget 내에 접근하는 대신,&lt;br /&gt;특정 함수 호출을 통해 ViewModel Reference를 갖는다.&lt;/li&gt;
&lt;li&gt;Editor의 Property Path 필드에 &quot;.&quot;로 구분된일련의 Member 이름을 입력할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이러한 함수 호출의 시작점을 Self, 즉 편집하고 있는 Widget에서 항상 시작한다.&lt;/li&gt;
&lt;li&gt;Property Path에 Self를 직접 지정하면 안된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Property Path는 BP에서 선언된 함수 이름도 사용할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 방식을 사용하면 로직이 간소화하여 더 높은 유연성을 확보할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Access ViewModel Variable&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;431&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/73vPA/btsIqAlej9q/8KI1zcsLrgPKzI1ZjybTtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/73vPA/btsIqAlej9q/8KI1zcsLrgPKzI1ZjybTtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/73vPA/btsIqAlej9q/8KI1zcsLrgPKzI1ZjybTtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F73vPA%2FbtsIqAlej9q%2F8KI1zcsLrgPKzI1ZjybTtk%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;906&quot; height=&quot;431&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;431&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget 에 ViewModel을 할당하면 BP에서 Widget의 Property를 통해 거꾸로 변수에 Access 할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물론 이는 Viewmodel 옵션에 따라 다르긴 하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Work with Array&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 Array는 ViewModel에서 Access 할 수 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 위해서는 ViewModel 자체적으로 배열을 직접 추가/삭제/탐색 할 수 있는 FieldNoitfy 함수를 만들어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다만 ListView, TreeView, TileView 등과는 함께 Array를 사용할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우, Element가 Array에 추가/제거/이동 될 경우에 Notify를 해줘야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;View Binding&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Add to Widget&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Drag and Drop&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ViewModel에서 Widget에 Bind할 변수나 함수를 클릭하고 Bind 할 영역의 Bind 드롭다운을 드래그&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Detail Panel&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1247&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/76qrt/btsIsZwAmBp/kK7kIRkJQB7PJl4H8q3IiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/76qrt/btsIsZwAmBp/kK7kIRkJQB7PJl4H8q3IiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/76qrt/btsIsZwAmBp/kK7kIRkJQB7PJl4H8q3IiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F76qrt%2FbtsIsZwAmBp%2FkK7kIRkJQB7PJl4H8q3IiK%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;1247&quot; height=&quot;352&quot; data-origin-width=&quot;1247&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Accessibility -&amp;gt; Override Accessibility 항목 오른쪽의 Bind 드롭다운을 눌러 필요한 FieldNotify Bind&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oLVtG/btsIsIIBVcq/e8cgnuTW5wzIXK0up3HIL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oLVtG/btsIsIIBVcq/e8cgnuTW5wzIXK0up3HIL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oLVtG/btsIsIIBVcq/e8cgnuTW5wzIXK0up3HIL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoLVtG%2FbtsIsIIBVcq%2Fe8cgnuTW5wzIXK0up3HIL0%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;669&quot; height=&quot;154&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 방식은 기존의 Property Binding과 혼동할 수 있는데, 이를 막아주는 옵션이 존재한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Project Settings -&amp;gt; Editor -&amp;gt; Widget Designer(Team) -&amp;gt; Property Binding Rule을 Prevent로 설정&lt;/li&gt;
&lt;li&gt;이 설정을 해주면 기존 Property를 Widget의 Parameter에 Binding하는 옵션이 제거된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u6RuM/btsIsY5xhMI/mgZvzUHkDOfXMYdKwrI8y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u6RuM/btsIsY5xhMI/mgZvzUHkDOfXMYdKwrI8y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u6RuM/btsIsY5xhMI/mgZvzUHkDOfXMYdKwrI8y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu6RuM%2FbtsIsY5xhMI%2FmgZvzUHkDOfXMYdKwrI8y1%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;576&quot; height=&quot;117&quot; data-origin-width=&quot;576&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Plugin -&amp;gt; Model View ViewModel -&amp;gt; Allow Binding from Detail View를 비활성화 하면 &lt;br /&gt;ViewModel에 대한 Detail 패널 Binding도 비활성화 할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 설정을 쓰더라도 View Binding Menu를 통해 여전히 Bind를 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;View Binding Menu&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buM72j/btsIqv5fdW1/stuINx0xi1hN6K7olyAGYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buM72j/btsIqv5fdW1/stuINx0xi1hN6K7olyAGYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buM72j/btsIqv5fdW1/stuINx0xi1hN6K7olyAGYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuM72j%2FbtsIqv5fdW1%2FstuINx0xi1hN6K7olyAGYK%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;295&quot; height=&quot;297&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;297&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UMG Designer -&amp;gt; Window -&amp;gt; View Binding 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;223&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbtcL9/btsIrtMosZ1/Z1lHp3DJ29LecVjPLdZFQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbtcL9/btsIrtMosZ1/Z1lHp3DJ29LecVjPLdZFQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbtcL9/btsIrtMosZ1/Z1lHp3DJ29LecVjPLdZFQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbtcL9%2FbtsIrtMosZ1%2FZ1lHp3DJ29LecVjPLdZFQK%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;1512&quot; height=&quot;223&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;223&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Add Widget을 통해 View Binding 목록에 추가하고 Bind 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Configure&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Select Target Widget&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;327&quot; data-origin-height=&quot;413&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyrlal/btsIsuX8TB5/NK2rWwZtnMVtH0llLkLa8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyrlal/btsIsuX8TB5/NK2rWwZtnMVtH0llLkLa8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyrlal/btsIsuX8TB5/NK2rWwZtnMVtH0llLkLa8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcyrlal%2FbtsIsuX8TB5%2FNK2rWwZtnMVtH0llLkLa8k%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;327&quot; height=&quot;413&quot; data-origin-width=&quot;327&quot; data-origin-height=&quot;413&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View Binding을 추가 할 Widget 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Create View Binding Entry&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;993&quot; data-origin-height=&quot;153&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvLp9t/btsIqTENYrB/EbDGOnKLdUedxFOSiCeRL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvLp9t/btsIqTENYrB/EbDGOnKLdUedxFOSiCeRL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvLp9t/btsIqTENYrB/EbDGOnKLdUedxFOSiCeRL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvLp9t%2FbtsIqTENYrB%2FEbDGOnKLdUedxFOSiCeRL1%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;993&quot; height=&quot;153&quot; data-origin-width=&quot;993&quot; data-origin-height=&quot;153&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Target Widget 하위에 있는 개별 Property마다 별도의 Bind를 걸 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물론 하나의 Property에 여러 Bind를 걸 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Select Widget Property&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HDBne/btsIsBCWSbR/viATLYKhj6Ua1CmqLPI8T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HDBne/btsIsBCWSbR/viATLYKhj6Ua1CmqLPI8T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HDBne/btsIsBCWSbR/viATLYKhj6Ua1CmqLPI8T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHDBne%2FbtsIsBCWSbR%2FviATLYKhj6Ua1CmqLPI8T1%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;485&quot; height=&quot;670&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Target Widget의 변수 및 함수 목록이 표시된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 항목에는 C++로 정의된 UFUNCTION(), UPROPERTY()도 포함된다.&lt;/li&gt;
&lt;li&gt;BP에서 정의한 변수나 함수는 자동으로 사용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Select ViewModel Property&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;412&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/climcI/btsIrr8SISE/R2lJCF4DKtX5aKsUYbXe01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/climcI/btsIrr8SISE/R2lJCF4DKtX5aKsUYbXe01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/climcI/btsIrr8SISE/R2lJCF4DKtX5aKsUYbXe01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclimcI%2FbtsIrr8SISE%2FR2lJCF4DKtX5aKsUYbXe01%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;412&quot; height=&quot;242&quot; data-origin-width=&quot;412&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Target ViewModel과 Target Property를 선택한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Set Bind Direction&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zAiwt/btsIsuDRKaT/MAnVYyd81M4K4MkMdoA7Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zAiwt/btsIsuDRKaT/MAnVYyd81M4K4MkMdoA7Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zAiwt/btsIsuDRKaT/MAnVYyd81M4K4MkMdoA7Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzAiwt%2FbtsIsuDRKaT%2FMAnVYyd81M4K4MkMdoA7Qk%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;655&quot; height=&quot;163&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;163&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bind Direction을 선택해 Widget과 ViewModel간의 정보가 흐르는 방식을 결정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;545&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfK8zS/btsIsJOiTKL/lso2kPKbN7sFqLOQqPNFC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfK8zS/btsIsJOiTKL/lso2kPKbN7sFqLOQqPNFC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfK8zS/btsIsJOiTKL/lso2kPKbN7sFqLOQqPNFC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfK8zS%2FbtsIsJOiTKL%2Flso2kPKbN7sFqLOQqPNFC1%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;1828&quot; height=&quot;545&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 모든 ViewModel은 PreConstruct와 Construct 사이에 한 번 실행된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bind Direction이 Two Way인 경우에는 One Way Bind만 실행된다.&lt;/li&gt;
&lt;li&gt;ViewModel 값이 SetViewModel을 이용해 변경되면 모든 Bind가 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Set Execution Mode&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bind 된 Widget 등의 실행 방식을 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1720436857491&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UENUM()
enum class EMVVMExecutionMode : uint8
{
	/** Execute the binding as soon as the source value changes. */
	Immediate = 0,
	/** Execute the binding at the end of the frame before drawing when the source value changes. */
	Delayed = 1,
	/** Always execute the binding at the end of the frame. */
	Tick = 2,
	/** When the binding can be triggered from multiple fields, use Delayed. Else, uses Immediate. */
	DelayedWhenSharedElseImmediate = 3 UMETA(DisplayName=&quot;Auto&quot;),
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Use Conversion Function&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LcSe8/btsIqwXm5Og/AHzVKQd7iFtQGOtkloO1g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LcSe8/btsIqwXm5Og/AHzVKQd7iFtQGOtkloO1g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LcSe8/btsIqwXm5Og/AHzVKQd7iFtQGOtkloO1g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLcSe8%2FbtsIqwXm5Og%2FAHzVKQd7iFtQGOtkloO1g1%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;544&quot; height=&quot;562&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변수에 대한 직접 Bind 대신 Conversion Function을 채택할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Conversion Function은 ViewModel의 변수를 다른 타입의 Data로 Convert하기 위한 Interface를 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/exFKvD/btsIsZJ83T1/yuttTiHpifXzQbENqlkTzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/exFKvD/btsIsZJ83T1/yuttTiHpifXzQbENqlkTzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/exFKvD/btsIsZJ83T1/yuttTiHpifXzQbENqlkTzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FexFKvD%2FbtsIsZJ83T1%2FyuttTiHpifXzQbENqlkTzK%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;1101&quot; height=&quot;630&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Convert 함수를 선택하면 설정할 수 있는 창이 드롭다운 아래 나타난다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;만약 이미 드롭다운 데이터가 있다면 이 기능이 비정상 동작한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이 때에는 Clear를 해서 한번 날려주고 하면 잘 된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;새로운 Conversion Function은 Global 단위 혹은 UserWIdget에 추가될 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단, 이 함수는 Event, Network, Deprecated, EditorOnly로 선언되면 안된다.&lt;/li&gt;
&lt;li&gt;BP에 표시되어야 하고, 하나의 Parameter와 하나의 Return Value를 가지고 있어야 한다.&lt;/li&gt;
&lt;li&gt;Global로 정의되는 경우에는 static으로 선언되어야 한다.&lt;/li&gt;
&lt;li&gt;UserWidget에서 정의되는 경우에는 pure 및 const여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ViewModel 작업 팁&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거대한 하나의 ViewModel 대신 작고 간결한 ViewModel을 권장한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 UI 디버깅을 하기 훨씬 용이하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예를 들어 Ability,Inventory 등으로 구성된 Array를 사용해 RPG에서 Character를 나타내는 ViewModel을 상상하자.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 ViewModel에 Bind 된 Widget 중 일부를 디버깅 하기 위해서는 &lt;br /&gt;전체 Character를 Spawn해 ViewModel의 데이터를 채워줘야 한다.&lt;/li&gt;
&lt;li&gt;이 때, 서로 다른 Component로 분할하면 Debug 할 때 Test Data로 더 쉽게 채울 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;또한 ViewModel은 다른 ViewModel 내부에 중첩하면, 복잡한 데이터 작업에서 유연성을 높일 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, HP ViewModel과 Attribute ViewModel을 각각 생성하고 이를 Character ViewModel에 중첩할 수 있다.&lt;/li&gt;
&lt;li&gt;이 경우, Test에서 개별 Widget은 각자 연관 된 ViewModel에서 Data를 취할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;체력 표시줄이 Character의 HP를 REference 한다던가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동시에 최종 결과물에서는 중첩된 ViewModel에서 전체 Set를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>UE5/UI</category>
      <category>MVVM</category>
      <category>UMG</category>
      <author>RedChiken</author>
      <guid isPermaLink="true">https://redchiken.tistory.com/392</guid>
      <comments>https://redchiken.tistory.com/392#entry392comment</comments>
      <pubDate>Mon, 8 Jul 2024 20:09:14 +0900</pubDate>
    </item>
    <item>
      <title>[UI] Optimization</title>
      <link>https://redchiken.tistory.com/391</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/optimization-guidelines-for-umg-in-unreal-engine&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/optimization-guidelines-for-umg-in-unreal-engine&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://coding-hell.tistory.com/78&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://coding-hell.tistory.com/78&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719887481430&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[UE4] 언리얼 ui 최적화 기법&quot; data-og-description=&quot;[영어원문] https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html UI optimization tips in Unreal Engine 4 At the Unreal Open Day 2017 event, Epic Games developer support engineer Mr. Guo Chunbiao introduced the UI op&quot; data-og-host=&quot;coding-hell.tistory.com&quot; data-og-source-url=&quot;https://coding-hell.tistory.com/78&quot; data-og-url=&quot;https://coding-hell.tistory.com/78&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/BWnqE/hyWrOCB7sG/xdYp6IJVI90HhhAg1ZzdkK/img.png?width=223&amp;amp;height=88&amp;amp;face=0_0_223_88,https://scrap.kakaocdn.net/dn/slZbx/hyWrQtGxCl/071I3pDqjXDKgx0P395jzK/img.png?width=223&amp;amp;height=88&amp;amp;face=0_0_223_88,https://scrap.kakaocdn.net/dn/bcIOGl/hyWrNX0FPZ/saPvny2NRppJOkwuJ0fkH0/img.jpg?width=782&amp;amp;height=652&amp;amp;face=0_0_782_652&quot;&gt;&lt;a href=&quot;https://coding-hell.tistory.com/78&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://coding-hell.tistory.com/78&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/BWnqE/hyWrOCB7sG/xdYp6IJVI90HhhAg1ZzdkK/img.png?width=223&amp;amp;height=88&amp;amp;face=0_0_223_88,https://scrap.kakaocdn.net/dn/slZbx/hyWrQtGxCl/071I3pDqjXDKgx0P395jzK/img.png?width=223&amp;amp;height=88&amp;amp;face=0_0_223_88,https://scrap.kakaocdn.net/dn/bcIOGl/hyWrNX0FPZ/saPvny2NRppJOkwuJ0fkH0/img.jpg?width=782&amp;amp;height=652&amp;amp;face=0_0_782_652');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[UE4] 언리얼 ui 최적화 기법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[영어원문] https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html UI optimization tips in Unreal Engine 4 At the Unreal Open Day 2017 event, Epic Games developer support engineer Mr. Guo Chunbiao introduced the UI op&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;coding-hell.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&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;&lt;a href=&quot;https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719886530566&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;UI optimization tips in Unreal Engine 4&quot; data-og-description=&quot;At the Unreal Open Day 2017 event, Epic Games developer support engineer Mr. Guo Chunbiao introduced the UI optimization techniques in Unreal Engine 4 to the developers present. The following is a speech record. Hello everyone, I'm Guo Chunbiao, a develope&quot; data-og-host=&quot;topic.alibabacloud.com&quot; data-og-source-url=&quot;https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html&quot; data-og-url=&quot;https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gOsYG/hyWrXGkuxn/bPnPyqBHRCSIPQ7KT6Jwek/img.jpg?width=1560&amp;amp;height=615&amp;amp;face=0_0_1560_615,https://scrap.kakaocdn.net/dn/bzdaDs/hyWrNjpGec/gnvplMPsUnxO8XZNolHzf0/img.jpg?width=982&amp;amp;height=645&amp;amp;face=0_0_982_645,https://scrap.kakaocdn.net/dn/ckg6Oi/hyWvJGoIhO/PiplEpoT64pch2GNPzlKLk/img.jpg?width=798&amp;amp;height=402&amp;amp;face=0_0_798_402&quot;&gt;&lt;a href=&quot;https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://topic.alibabacloud.com/a/ui-optimization-tips-in-unreal-engine-4_8_8_10274886.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gOsYG/hyWrXGkuxn/bPnPyqBHRCSIPQ7KT6Jwek/img.jpg?width=1560&amp;amp;height=615&amp;amp;face=0_0_1560_615,https://scrap.kakaocdn.net/dn/bzdaDs/hyWrNjpGec/gnvplMPsUnxO8XZNolHzf0/img.jpg?width=982&amp;amp;height=645&amp;amp;face=0_0_982_645,https://scrap.kakaocdn.net/dn/ckg6Oi/hyWvJGoIhO/PiplEpoT64pch2GNPzlKLk/img.jpg?width=798&amp;amp;height=402&amp;amp;face=0_0_798_402');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;UI optimization tips in Unreal Engine 4&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;At the Unreal Open Day 2017 event, Epic Games developer support engineer Mr. Guo Chunbiao introduced the UI optimization techniques in Unreal Engine 4 to the developers present. The following is a speech record. Hello everyone, I'm Guo Chunbiao, a develope&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;topic.alibabacloud.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.nullbus.net/94&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.nullbus.net/94&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719895696415&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;UMG 드로우콜 분석&quot; data-og-description=&quot;한줄요약 드로우 콜은 같은 리소스와 같은 레이어 ID라는 두 가지 조건이 동시에 만족되어야 하나로 합칠 수 있습니다. UMG에서의 드로우 콜 언리얼 공식 문서에는 드로우 콜과 관련하여 이런 설&quot; data-og-host=&quot;blog.nullbus.net&quot; data-og-source-url=&quot;https://blog.nullbus.net/94&quot; data-og-url=&quot;https://blog.nullbus.net/94&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ccNo9Q/hyWvXxUKD8/UHfloyI38klQeLMUUitoHK/img.png?width=428&amp;amp;height=138&amp;amp;face=0_0_428_138,https://scrap.kakaocdn.net/dn/JNS22/hyWvRxFEX2/F4mA2tEA9jXc49XeLoa9n0/img.png?width=428&amp;amp;height=138&amp;amp;face=0_0_428_138,https://scrap.kakaocdn.net/dn/RlKQp/hyWvNWma7i/5vbfbV0mCCKjSh3TnkYKRK/img.png?width=452&amp;amp;height=366&amp;amp;face=0_0_452_366&quot;&gt;&lt;a href=&quot;https://blog.nullbus.net/94&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.nullbus.net/94&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ccNo9Q/hyWvXxUKD8/UHfloyI38klQeLMUUitoHK/img.png?width=428&amp;amp;height=138&amp;amp;face=0_0_428_138,https://scrap.kakaocdn.net/dn/JNS22/hyWvRxFEX2/F4mA2tEA9jXc49XeLoa9n0/img.png?width=428&amp;amp;height=138&amp;amp;face=0_0_428_138,https://scrap.kakaocdn.net/dn/RlKQp/hyWvNWma7i/5vbfbV0mCCKjSh3TnkYKRK/img.png?width=452&amp;amp;height=366&amp;amp;face=0_0_452_366');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;UMG 드로우콜 분석&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;한줄요약 드로우 콜은 같은 리소스와 같은 레이어 ID라는 두 가지 조건이 동시에 만족되어야 하나로 합칠 수 있습니다. UMG에서의 드로우 콜 언리얼 공식 문서에는 드로우 콜과 관련하여 이런 설&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.nullbus.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&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;Slate Render Process&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyUbwV/btsIibsrFKl/H3Tw1aFC15XCBoUjto3PE1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyUbwV/btsIibsrFKl/H3Tw1aFC15XCBoUjto3PE1/img.jpg&quot; data-alt=&quot;일반적인 Rendring Process의 개략도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyUbwV/btsIibsrFKl/H3Tw1aFC15XCBoUjto3PE1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyUbwV%2FbtsIibsrFKl%2FH3Tw1aFC15XCBoUjto3PE1%2Fimg.jpg&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;1280&quot; height=&quot;504&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;일반적인 Rendring Process의 개략도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Game Thread&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Game Thread에서 Slate Tick은 Frame당 2번 WidgetTree를 순회한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 번은 Paint할 Widget의 크기를 계산하기 위해 PrePass 과정에서.&lt;/li&gt;
&lt;li&gt;다른 한 번은 Paint 과정에서 Draw Elements를 계산하기 위해 OnPaint 과정에서.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;조금 더 풀어 설명하면 다음과 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Common Widget의 Type과 Parameter에 해당하는 Vertex Buffer 생성&lt;/li&gt;
&lt;li&gt;Widget의 Render Transform이 Vertex Buffer로 계산 되어 Layer ID 및 Material 정보에 따라 Batch Merge 수행&lt;/li&gt;
&lt;li&gt;마지막 User Widget이 하나 이상의 Draw Elements 생성&lt;/li&gt;
&lt;li&gt;각 Draw Element가 Draw call에 해당하는 Render Thread에 Draw Element를 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Render Thread&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Render Thread에서의 Slate Rendering 작업은 다음 순서를 거친다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Widget Render&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI의 RTT(Render to Texture) 수행&lt;/li&gt;
&lt;li&gt;Retainer Box를 사용하면 Draw Elements가 Retainer Box의 Retain Target으로 Rendering 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Slate Render&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Draw Elements를 Back Buffer에 Render&lt;/li&gt;
&lt;li&gt;Retainer Box를 사용하는 경우, Retainer Box의 Texture Resource가 Back Buffer에 Rendering 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Invalidation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/en-us/unreal-engine/invalidation-in-slate-and-umg-for-unreal-engine&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.epicgames.com/documentation/en-us/unreal-engine/invalidation-in-slate-and-umg-for-unreal-engine&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slate Widget을 캐싱하고 Paint, Layout, 계층 정보 등의 변경점을 관리하는 기능.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget의 위와 같은 정보가 변경되지 않으면, 매 Frame마다 Widget을 다시 그리는 대신 Cache를 출력한다.&lt;/li&gt;
&lt;li&gt;위 정보들에 유효한 변경사항이 발생하면 Slate가 이를 재 계산하여 다시 그려준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3RXk6/btsIjaFT4fv/wTqeNonJOBdv3QXKbLknPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3RXk6/btsIjaFT4fv/wTqeNonJOBdv3QXKbLknPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3RXk6/btsIjaFT4fv/wTqeNonJOBdv3QXKbLknPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3RXk6%2FbtsIjaFT4fv%2FwTqeNonJOBdv3QXKbLknPk%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;514&quot; height=&quot;602&quot; data-origin-width=&quot;514&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Invalidation Box&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Child Widget의 Geometry를 Cache 하고 관리하는 UI
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 Widget의 Geometry가 바뀌지 않는 한 Cache 된 Geometry로 대체되어 CPU 사용량을 크게 줄여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Invalidation Box는 감싸진 UI 뿐 아니라 그 하위 계층의 모든 UI에 대해 Geometry Cache를 진행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Global Invalidation&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SWindow의 Invalidation을 이용해 효과적으로 모든 UI를 Invalidation Box로 Wrapping하는 기능.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 SWindow에 포함 된 모든 Invalidation Box는 무효화 되고, SWindow의 Invalidation Box만 동작하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Slate.EnableGlobalInvalidation을 true로 트리거하여 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719818605568&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void SWidget::Invalidate(EInvalidateWidgetReason InvalidateReason)
{
	SLATE_CROSS_THREAD_CHECK();

	if (InvalidateReason == EInvalidateWidgetReason::None || !IsConstructed())
	{
		return;
	}

	SCOPED_NAMED_EVENT_TEXT(&quot;SWidget::Invalidate&quot;, FColor::Orange);

	// Backwards compatibility fix:  Its no longer valid to just invalidate volatility since we need to repaint to cache elements if a widget becomes non-volatile. So after volatility changes force repaint
	if (EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::Volatility))
	{
		InvalidateReason |= EInvalidateWidgetReason::PaintAndVolatility;
	}

	if (EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::Prepass))
	{
		MarkPrepassAsDirty();
		InvalidateReason |= EInvalidateWidgetReason::Layout;
	}

	if (EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::ChildOrder) || !PrepassLayoutScaleMultiplier.IsSet())
	{
		MarkPrepassAsDirty();
		InvalidateReason |= EInvalidateWidgetReason::Prepass;
		InvalidateReason |= EInvalidateWidgetReason::Layout;
	}

	const bool bVolatilityChanged = EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::Volatility) ? Advanced_InvalidateVolatility() : false;

	if(FastPathProxyHandle.IsValid(this))
	{
		// Current thinking is that visibility and volatility should be updated right away, not during fast path invalidation processing next frame
		if (EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::Visibility))
		{
			SCOPED_NAMED_EVENT(SWidget_UpdateFastPathVisibility, FColor::Red);
			UpdateFastPathVisibility(FastPathProxyHandle.GetProxy().Visibility.MimicAsParent(), FastPathProxyHandle.GetInvalidationRoot_NoCheck()-&amp;gt;GetHittestGrid());
		}

		if (bVolatilityChanged)
		{
			SCOPED_NAMED_EVENT(SWidget_UpdateFastPathVolatility, FColor::Red);

			TSharedPtr&amp;lt;SWidget&amp;gt; ParentWidget = GetParentWidget();

			UpdateFastPathVolatility(ParentWidget.IsValid() ? ParentWidget-&amp;gt;IsVolatile() || ParentWidget-&amp;gt;IsVolatileIndirectly() : false);

			ensure(!IsVolatile() || IsVolatileIndirectly() || EnumHasAnyFlags(UpdateFlags, EWidgetUpdateFlags::NeedsVolatilePaint));
		}

		FastPathProxyHandle.MarkWidgetDirty_NoCheck(InvalidateReason);
	}
	else
	{
#if WITH_SLATE_DEBUGGING
		FSlateDebugging::BroadcastWidgetInvalidate(this, nullptr, InvalidateReason);
#endif
		UE_TRACE_SLATE_WIDGET_INVALIDATED(this, nullptr, InvalidateReason);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Retainer Panel&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Child Widgets을 유저의 화면에 Render 하기 전에 하나의 Texture로 병합&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;305&quot; data-origin-height=&quot;88&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3xPa9/btsIkETuSef/KOLrf9MZKRRudbB4LQswg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3xPa9/btsIkETuSef/KOLrf9MZKRRudbB4LQswg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3xPa9/btsIkETuSef/KOLrf9MZKRRudbB4LQswg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3xPa9%2FbtsIkETuSef%2FKOLrf9MZKRRudbB4LQswg0%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;305&quot; height=&quot;88&quot; data-origin-width=&quot;305&quot; data-origin-height=&quot;88&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 Phase는 Render를 시작하는 Frame, Phase Count는 Render가 되는 Frame 주기이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 위의 경우 0 Frame에서 시작하여 3 Frame 단위로 Retainer Panel의 Child Widgets이 Render 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이러한 기능들은 매 Frame마다 호출되는 UI의 Draw call을 줄여주는 기능을 제공한다.&lt;/li&gt;
&lt;li&gt;하지만 Retainer Panel은 다시 그려질 때 큰 overhead를 가지면서, &lt;br /&gt;Widget 개개인이 점유하는 Memory가 Invalidation Box보다 크다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Retainer Panel이 각 Widget의 Invalidation Data 뿐 아니라 고유한 Render Target도 가지기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;때문에 UI의 CPU 점유율을 낮추려 한다면 우선 Invalidation Box부터 써야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그럼에도 Draw Call을 줄이고 싶다면, Retainer Panel을 이용함으로 CPU 사용량을 더 압축할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이는 성능 제약이 빡빡한 저성능 모바일에서 유용다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;How Invalidation works&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget이 화면에 그려질 때 다음 작업들이 순차적으로 발생한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계층 구조 - Slate가 root widget과 그들의 child를 모두 포함한 계층에 따라 widget tree를 생성한다.&lt;/li&gt;
&lt;li&gt;Layout - Slate가 Render Transform을 기반으로 Widget의 크기와 스크린 상 위치를 계산한다.&lt;/li&gt;
&lt;li&gt;Paint - Slate가 각 Widget의 Geometry를 계산한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;각 작업들은 수행 할 때 뒤이은 작업을 반드시 수행해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 계층 구조 작업이 수행되면 반드시 Layout, Paint 작업이 수행되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Invalidation System은 위 과정에서 발생하는 모든 데이터를 Memory에 Cache한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget이 Invalidation Box를 사용하든, SWidget의 Global Invalidation을 사용하든.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Memory에 Cache된 Widget들은 변경사항이 발생하지 않는 한 재연산을 하지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 변경사항이 발생하게 되면 Dirty List에 추가되고,&lt;br /&gt;이 List의 Widget들은 다음 Frame에서 재연산이 이루어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Invalidation에서 Cache된 Data를 갱신하게 되는 타입은 대략 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719820716182&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * The different types of invalidation that are possible for a widget.
 */
enum class EInvalidateWidgetReason : uint8
{
	None = 0,

	/**
	 * Use Layout invalidation if your widget needs to change desired size.  This is an expensive invalidation so do not use if all you need to do is redraw a widget
	 */
	Layout = 1 &amp;lt;&amp;lt; 0,

	/**
	 * Use when the painting of widget has been altered, but nothing affecting sizing.
	 */
	Paint = 1 &amp;lt;&amp;lt; 1,

	/**
	 * Use if just the volatility of the widget has been adjusted.
	 */
	Volatility = 1 &amp;lt;&amp;lt; 2,

	/**
	 * A child was added or removed.   (this implies prepass and layout)
	 */
	ChildOrder = 1 &amp;lt;&amp;lt; 3,

	/**
	 * A Widgets render transform changed
	 */
	RenderTransform = 1 &amp;lt;&amp;lt; 4,

	/**
	 * Changing visibility (this implies layout)
	 */
	Visibility = 1 &amp;lt;&amp;lt; 5,

	/**
	 * Attributes got bound or unbound (it's used by the SlateAttributeMetaData)
	 */
	AttributeRegistration = 1 &amp;lt;&amp;lt; 6,

	/**
	 * Re-cache desired size of all of this widget's children recursively (this implies layout)
	 */
	Prepass = 1 &amp;lt;&amp;lt; 7,

	/**
	 * Use Paint invalidation if you're changing a normal property involving painting or sizing.
	 * Additionally if the property that was changed affects Volatility in anyway, it's important
	 * that you invalidate volatility so that it can be recalculated and cached.
	 */
	PaintAndVolatility = Paint | Volatility,
	/**
	 * Use Layout invalidation if you're changing a normal property involving painting or sizing.
	 * Additionally if the property that was changed affects Volatility in anyway, it's important
	 * that you invalidate volatility so that it can be recalculated and cached.
	 */
	LayoutAndVolatility = Layout | Volatility,


	/**
	 * Do not use this ever unless you know what you are doing
	 */
	All UE_DEPRECATED(4.22, &quot;EInvalidateWidget::All has been deprecated.  You probably wanted EInvalidateWidget::Layout but if you need more than that then use bitwise or to combine them&quot;) = 0xff
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Invalidation은 자주 변경되지 않은 UI에서 최적화된 기능이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget이 오랫동안 변경되지 않을수록 Slate가 Cache Data를 오래 들고 있고, 그 만큼 CPU 부하를 줄여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이는 특히 MMORPG나 라이브 서비스 게임의 Depth가 있는 메뉴와 같이 규모가 크고 복잡한 UI 작업에서 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Volatile Widget&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간혹 특정 Widget은 가능한 매 Frame마다 업데이트가 되어야 하는 경우가 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우 Widget은 변경사항이 있을 때마다 매 tick이 Invalidate 된다.&lt;/li&gt;
&lt;li&gt;하지만 CPU 부하는 Invalidation을 사용할 때와 동일하게 발생한다.&lt;/li&gt;
&lt;li&gt;더불어 Hiearchy를 Cache하기 위해 Memory도 점유하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이를 해결하기 위해 자주 사용하는 Widget에 Volatile 선언을 해주는 것이 좋다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Volatile 선언이 된 Widget과 그 Child Widget들은 Paint Data가 Cache되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비록 Geometry는 매 Frame마다 재계산되어 다시 그려지겠지만, &lt;br /&gt;Slate는 직접적인 변경사항이 없는 이상 Layout 계산은 계속해서 Skip한다.&lt;/li&gt;
&lt;li&gt;이는 UI를 전반적으로 Invalidation 하고 싶지만, &lt;br /&gt;자주 갱신되는 소수의 Widget으로 인해 이득을 보지 못할 때 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발자가 주의해야 할 점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OnTick/OnPaint 사용 지양&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OnTick이나 OnPaint에서 작업을 하게 되면 그 작업이 매 Frame마다 호출하게 됨.&lt;/li&gt;
&lt;li&gt;가급적 Event Dispatch나 Delegate를 이용할 것을 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Attribute Bind 대신 Event-driven Update 사용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Unreal Engine에서 제공하는 Attribute Bind 역시 매 Frame마다 할당 됨.&lt;/li&gt;
&lt;li&gt;이 역시 Event와 Delegate를 이용해 변경사항이 있을 때에만 Widget에 적용 되도록 작업할 것을 권장 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Widget Construction&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reduce unused widgets&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget의 모든 child는 시각화 여부와 무관하게 항상 construct 됨.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Render가 되지 않더라도 Loading time, Construction time, Memory를 점유한다는 의미.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;때문에 당장 사용되지 않는 Widget들은 Hierarchy에서 제거하는 것이 옳다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Break complex widgets&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특히 Main System에서 사용되는 Widget의 경우 실제로 표시되는 것은 몇 없지만 Cild가 수 천개씩 있는 경우가 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이러한 Widget을 한꺼번에 Load하게 되면 불필요한 Child로 인해 Loading이 지연되고 Memory를 점유하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;때문에 일정 규모 이상의 Widget은 어느정도 Child Widget을 세분화 하는 것이 좋다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;예를 들어, 다음과 같이 Widget을 구분하여 작업 방향성을 정할 수 있다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;항상 표시되는 Widget
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BaseWidget과 같이 Load하면서 화면에 바로 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가능한 빨리 표시되어야 하는 Widget
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;당장 사용하지 않을 수도 있지만 반응성이 높아야 함.&lt;/li&gt;
&lt;li&gt;BaseWidget과 같이 Load 하되, 화면에는 출력하지 않고 Visibility로 컨트롤.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;조금 늦게 표시되어도 괜찮은 Widget&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가끔 사용되거나 아얘 사용되지 않는 경우도 있음.&lt;/li&gt;
&lt;li&gt;BaseWidget과 별개로 필요할 때마다 Async Load.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이러한 방식은 Memory를 크게 절약할 뿐 아니라 Load 할 때의 CPU 영향도 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layout&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CanvasPanel 사용 지양&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CanvasPanel은 좌표 평면과 Widget 별 Anchor를 이용해 다른 Widget의 위치를 지정할 수 있는 강력한 Widget이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Widget을 원하는 위치에 정확하게 지정하면서, &lt;br /&gt;동시에 Screen의 외각을 기준으로 Widget의 위치를 유지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;하지만 그와 동시에 높은 성능을 요구하는 Widget이기도 하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slate의 Draw call은 Widget의 Layer ID별로 발생한다.&lt;/li&gt;
&lt;li&gt;VerticalBox나 HorizontalBox등의 다른 Container Widget들은 Child Widget의 Layer ID를 통합한다.&lt;/li&gt;
&lt;li&gt;하지만 CanvalsPanel은 Child Widget이 필요할 때 다른 Widget 위에 Render 될 수 있도록 ID를 증가시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결과적으로 CanvasPanel은 단기로 여러 개의 Draw call을 발생시켜 높은 CPU 사용량을 요구하게 된다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비록 CanvasPanel보다 용처가 제한적이지만, OverlayPanel 역시 Draw call을 증가시킨다.&lt;/li&gt;
&lt;li&gt;GridPanel은 Slot마다 Layer를 직접 지정할 수 있지만, 보통은 Child를 Iterate하며 LayerID를 새로 부여한다.&lt;/li&gt;
&lt;li&gt;ScrollBox는 Scroll Bar가 Layer ID를 증가시킨다.&lt;/li&gt;
&lt;li&gt;Border 역시 Layer ID를 증가시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HUD나 Menu System의 Root Widget으로 CanvasPanel을 사용하는 것은 크게 문제가 되지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우에는 상세한 위치 조정이나 복잡한 Z-Order 배치가 필요할 가능성이 높기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다만 Template 속성이 있는 Widget은 CanvasPanel 위에서 작업하는 것을 지양해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TextBox, Custom Button과 같이 다른 Widget의 구성요소로 사용되는 Custom Widget들&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;또는 CanvasPanel을 다수의 Layer에서 과도하게 사용하면, 최종 Layer를 혼돈하기 쉽다.&lt;/li&gt;
&lt;li&gt;일반적으로 하나의 요소로 구성된 Widget은 Canvas Panel로 감쌀 필요가 전혀 없다.&lt;/li&gt;
&lt;li&gt;또한 HUD나 Menu 같은 경우에도, &lt;br /&gt;Overlay나 SizeBox를 HorizontalBox, VerticalBox, GridBox와 같이 사용하여 CanvasPanel 사용을 대체할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SizeBox 대신 가능한 Spacer 사용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SizeBox는 자신의 크기를 계산하고 Render하는데 다양한 값을 사용한다.&lt;/li&gt;
&lt;li&gt;만약 Widget이 특정 Width와 Height를 고정적으로 가진다면, Spacer가 훨씬 가볍다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ScaleBox와 SizeBox를 같이 사용하지 않기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ScaleBox와 SizeBox를 같이 사용하면 매 Frame마다 각자 서로의 Size를 오가며 Update하는 Loop에 빠지게 된다.&lt;/li&gt;
&lt;li&gt;이 둘에 의존하기 보다는 Layout이 Content의 Native Size에 따라 동작하도록 만드는 것이 적절하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RichTextWidget 사용 지양&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RichTextWidget은 강력한 Format 지정 기능을 제공하는 만큼 표준 TextBox보다 훨씬 무겁다.&lt;/li&gt;
&lt;li&gt;만약 RichTextWidget의 모든 기능이 필요한게 아니라 디자인적인 표현만이 필요하다면,&lt;br /&gt;원하는 외형을 표현할 수 있는 Font를 제작하여 TextWidget에서 사용하는 것이 훨씬 가볍다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Visibility&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 출력되어야 하는 Widget이 Visible인 경우, 클릭 시 Click 반응이 동작해 오버헤드가 발생 함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;visible 대신 HitTestInvisible, SelfHitTestInvisible을 사용할 것을 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Hidden의 경우 화면에 보이지 않더라도 영역을 차지하기 위해 Layout Space를 사용 함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Layout Space를 사용하면 Prepass 계산이 매 Frame마다 수행 됨.&lt;/li&gt;
&lt;li&gt;완전히 화면에서 숨기면서 영역을 차지할 필요가 없다면 Collapse를 사용하는 것을 권장.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Texture&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Merged Texture&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Widget에서 여러 개의 Texture를 사용하는 경우, Texture의 갯수만큼 Draw call이 발생한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;때문에 가능하면 Merge Texture 1개를 사용하는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Widget의 Sprite를 사용하면 Merged Texture를 사용하거나 편집할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Atlas Group&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@devkcy/ue5-textureatlas&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@devkcy/ue5-textureatlas&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719896139485&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[UE5] Texture Atlas&quot; data-og-description=&quot;텍스처 아틀라스(Texture Atlas)란 여러 텍스처를 포함하는 텍스처로, 빈 공간에 다른 텍스처를 추가하여 메모리 낭비를 줄인다.&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@devkcy/ue5-textureatlas&quot; data-og-url=&quot;https://velog.io/@devkcy/ue5-textureatlas&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/caW8Ed/hyWvNB3Vx5/iYkBkK2nhwGFk9kne1pD0k/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://velog.io/@devkcy/ue5-textureatlas&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@devkcy/ue5-textureatlas&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/caW8Ed/hyWvNB3Vx5/iYkBkK2nhwGFk9kne1pD0k/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[UE5] Texture Atlas&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;텍스처 아틀라스(Texture Atlas)란 여러 텍스처를 포함하는 텍스처로, 빈 공간에 다른 텍스처를 추가하여 메모리 낭비를 줄인다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;Atlas를 효율적으로 사용하는 방안
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 Merged Texture를 Sprite로 사용하는 것도 Atlas를 거친다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;보통 Texture는 2의 지수로 저장이 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;300 * 300인 경우 실제 크기는 512 * 512가 된다.&lt;/li&gt;
&lt;li&gt;이 때 필요한 영역 외의 Pixel 너비 만큼 메모리를 낭비하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Atlas Group은 이런 Texture를 하나의 Texture로 묶는 방식이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 같은 Widget에서 사용되는 Image들에게 주로 사용되며, Context Switching을 줄이는 효과가 있다.&lt;/li&gt;
&lt;li&gt;반대로 다른 Widget의 Image를 묶어주면 Context Switch가 증가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일반적인 Atlas Group은 2048 * 2048 크기를 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Animation Cost&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Material만 있는 Animation&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GPU로 처리되기 때문에 CPU 비용이 발생하지 않는다.&lt;/li&gt;
&lt;li&gt;발광 이펙트, 배경 Scroll, Material 변화로만 표현 가능한 Effect들이 포이에 포함된다.&lt;/li&gt;
&lt;li&gt;Animation을 Material에 포함시킬 수 있다면, 가장 우선적으로 사용하는 것을 권장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Blueprint로 작업 되었지만 Sequencer가 필요 없는 Animation&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행 비용은 거의 같지만, &lt;br /&gt;Sequencer Animation은 실행 전 Animation Object Initialize와 담당 Property Path 해석이 요구된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, Sequencer Animation이 BP Animation보다 CPU 비용이 조금 더 높다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비교적 짧고 자주 사용되는 Animation의 경우 Sequencer를 지양하고 BP Script로 작업할 것을 권장.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;UMG의 Animation Editor로 만들어진 Sequencer Animation&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UMG의 Animation Editor은 Sequencer 구현체이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Color 같이 Attribute를 변경하는 경우에는 Widget을 다시 그리게 하지만 Layout을 Invalidate 하지 않는다.&lt;/li&gt;
&lt;li&gt;반대로 Render Transform의 변화를 가져오는 모든 Animation은 Layout을 Invalidate한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;가급적 이러한 작업은 피하거나, Volatile 선언을 해줘야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Layout 변경을 유발하는 Animation&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU 비용이 가장 높음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기타 최적화 방안&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Switch Material&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성능이 낮은 Machine에서 Material Effect의 부하를 줄이거나 제거하는 방안&lt;/li&gt;
&lt;li&gt;DYNAMIC_MULTICAST Framework를 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Manager Class&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 USerWidget과 Brush, Font등 UI Resource를 관리하는 Manager Class를 만드는 것을 권장.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Free Texture Memory&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Texture를 미리 Setting 하는 것이 아니라 코드를 통해 수동으로 Load/Set/Destroy 하는 것을 전제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Editor에서는 Texture를 설정하지 않으면 CDO에서 이 Texture 객체를 참조하는 것을 피할 수 있다.&lt;/li&gt;
&lt;li&gt;CDO Reference는 ShardPtr의 Reference Count를 최소 1로 만들어 앱이 종료될 때까지 제거되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Editor에서 Image Property가 설정되어 있고 Texture를 삭제하는 경우,&lt;br /&gt;CookStage에서 UImage와 UTexture 사이 Reference를 제거하여 UserWidget의 CDO가 &lt;br /&gt;UTexture를 Reference 하지 않아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719895030186&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UWeakRefImage::Serialize(FArchive&amp;amp; Ar)
{
    Super::Serialize(Ar);
    
    if (Ar.IsCooking() &amp;amp;&amp;amp; Ar.IsSaving())
    {
        // Let's suppose user always intend to set a valid resource texture here.
        UObject* ImageRes Brush.GetResourceObject();
        if (ImageRes != nullptr)
        {
            ImageWeakRef = ImageRes;
            Brush.SetResourceObject(nullptr);
            SetBrush(Brush);
        }
    }
    Ar &amp;lt;&amp;lt; ImageWeakRef;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 외에 Texture를 Load하는 코드는 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719895319091&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UWeakRefImage::LoadTextureResource(bool bAsync/*= true*/)
{
    if (ImageWeakRef.ResolveObject() || (!bAsync &amp;amp;&amp;amp; ImageWeakRef.TryLoad()))
    {
    	Brush.SetResourceObject(ImageWeakRef.ResolveObject()); 
        SetBrush(Brush);
    }
    else
    {
        if (StreamableMgr == nullptr)
        {
        	StreamableMgr = new FStreamableManager();
        }
        StreamableMgr-&amp;gt;RequestAsyncLoad(ImageWeakRef,
        	FStreamableDelegate::CreateUObject(this, &amp;amp;UWeakRefImage:: LoadTextureDeferred));
    }
}

void UWeakRefImage::LoadTextureDeferred()
{
    if (ImageWeakRef.ResolveObject())
    {
    	Brush.SetResourceObject(ImageWeakRef.ResolveObject()); 
        SetBrush (Brush);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Texture Unload하는 코드는 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719895374494&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UWeakRefImage::UnloadTextureResource()
{
    TSharedPtr&amp;lt;FSlateRenderer&amp;gt; Renderer = FSlateApplicationBase::Get().GetRenderer(); 
    if (Renderer.IsValid())
    {
        FSlateApplicationBase::Get().GetRenderer()-&amp;gt;ReleaseDynamicResource(Brush);
        FSlateApplicationBase::Get().GetRenderer()-&amp;gt;ReleaseAccessedResources(true);
    }
    Brush.SetResourceObject(nullptr);
    SetBrush (Brush);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3D RTT 최적화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SceneCaptureComponent2D는 매 Frame에서 tick이 호출된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매 Frame마다 Image Update가 발생하는 것을 취소할 수는 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 때 Animation의 Update 빈도는 30 fps 정도로 충분하므로 BP에서 Tick 간격을 설정할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우 BP에서 Capture을 수동으로 호출해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이와 별개로, SceneCapturecomponent2D는 Render Target이 크면 그 자체만으로 성능이 악화된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>UE5/UI</category>
      <author>RedChiken</author>
      <guid isPermaLink="true">https://redchiken.tistory.com/391</guid>
      <comments>https://redchiken.tistory.com/391#entry391comment</comments>
      <pubDate>Mon, 1 Jul 2024 18:56:59 +0900</pubDate>
    </item>
    <item>
      <title>[Network] Replication Execution Order</title>
      <link>https://redchiken.tistory.com/390</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replicated-object-execution-order-in-unreal-engine?application_version=5.3&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replicated-object-execution-order-in-unreal-engine?application_version=5.3&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Actor Property&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 Unreliable하고 Single Bunch로 전송된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Single Bunch에 Unreliable하기 때문에 다른 RPC보다는 나중에 전송된다.&lt;/li&gt;
&lt;li&gt;하지만 ForceQueue RPC보다는 먼저 전송된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Replicated Using Order&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서로 다른 Replicated Property에 대한 RepNotify의 순서는 명확하게 정해진 것이 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 상에서 Property의 Dirty 마킹 순서나 메모리상 위치와 아무런 연관이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;때문에 몇몇 Property가 동시에 Replicated 되어야 한다면 Struct로 묶어서 관리하는 것을 권장한다.&lt;/li&gt;
&lt;li&gt;만약 Gameplay에 중요한 Replicated Property가 있다면, &lt;br /&gt;RepNotify를 구현해 Property의 변경사항을 프레임 단위로 대응 하는 것이 좋다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replication을 통해 변경된 값을 전달받고 RepNotify가 호출된 후에, &lt;br /&gt;UObject::PostRepNotifies 함수에서 변경사항을 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;이 때, 변경된 값이 사용될 준비가 될 때까지 각각의 RepNotify에 저장하는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RPC&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC에 대한 대전제는 다음과 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reliable이 Unreliable보다 먼저 전송된다.&lt;/li&gt;
&lt;li&gt;Unicast는 Reliability와 무관하게 가장 먼저 전송된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Order Accross Actors&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replicated Actor에 대한 RPC 호출에 대해서는 명확한 법칙이 없다.&lt;/li&gt;
&lt;li&gt;때문에 RPC 함수 호출 순서와 실제 RPC 전달 순서는 일치하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Order Inside an Actor&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replication System은 동일한 Actor에 대한 reliable RPC 호출의 순서는 보장해준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Actor의 Subobject에도 적용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이는 reliable RPC의 경우, 함수를 호출한 순서와 실제 RPC의 전달 순서가 일치한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Unreliable VS Reliable&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Unreliable RPC와 Reliable RPC가 섞여 있을 때에는 순서가 보장되는 것처럼 보이지만, 실상은 보장되지 않는다.&lt;/li&gt;
&lt;li&gt;Packet 손실이나 재배치가 일어나지 않는다면, Unreliable RPC와 Reliable RPC의 전달 순서는 호출 순서와 일치한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 손실/재배치가 일어나면 순서가 달라지게 된다.&lt;/li&gt;
&lt;li&gt;정확히는 Reliable RPC끼리는 호출 순서와 전달 순서가 일치한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Multicast VS Unicast&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Multicast RPC와 Unicast RPC가 섞여 있을 때에는 항상 호출 순서와 전송 순서가 일치하지는 않아 더욱 복잡하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reliable Multicast&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reliable Multicast의 경우 Reliable Unicast와 섞여 있을 경우 호출 순서와 전송 순서가 일치한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Unreliable Multicast&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Unreliable Multicast의 경우는 절대로 다른 Reliable/Unreliable Multicast와의 순서를 보장해주지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RPC Send Policy&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ERemoteFunctionSendPolicy를 정의하여 Send Policy를 명시해 RPC의 전송 순서에 영향을 줄 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://redchiken.tistory.com/389&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://redchiken.tistory.com/389&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1719558162010&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Network] Remote Procedure Calls(RPCs)&quot; data-og-description=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.3RPCLocal 단위에서 호출하여 Remote로 연결된 1개 이상의 Machinge에서 실행되는 함수Return이 없는 단방향 &quot; data-og-host=&quot;redchiken.tistory.com&quot; data-og-source-url=&quot;https://redchiken.tistory.com/389&quot; data-og-url=&quot;https://redchiken.tistory.com/389&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gqEaf/hyWrYEoBae/CkqTzvCsScqhTEVspvYGIK/img.png?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/GVdZr/hyWrOV799l/frzKkQZHP9AFEevhj7HWy1/img.png?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/B4zlM/hyWr0bde4o/zaTZIzLg40SdXnW3X2dfO0/img.png?width=1236&amp;amp;height=493&amp;amp;face=0_0_1236_493&quot;&gt;&lt;a href=&quot;https://redchiken.tistory.com/389&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://redchiken.tistory.com/389&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gqEaf/hyWrYEoBae/CkqTzvCsScqhTEVspvYGIK/img.png?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/GVdZr/hyWrOV799l/frzKkQZHP9AFEevhj7HWy1/img.png?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/B4zlM/hyWr0bde4o/zaTZIzLg40SdXnW3X2dfO0/img.png?width=1236&amp;amp;height=493&amp;amp;face=0_0_1236_493');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Network] Remote Procedure Calls(RPCs)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.3RPCLocal 단위에서 호출하여 Remote로 연결된 1개 이상의 Machinge에서 실행되는 함수Return이 없는 단방향&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;redchiken.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Force Send&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Unreliable Multicast RPC의 순서를 바꾸고 이들이 Queue되는 것을 방지한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 Queue 되지 않는다는 것은 대기하지 않고 바로 전송이 된다는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Force Queue&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 Force Queue RPC와 Unreliable Multicast를 제외한 RPC와의 순서를 보장하지 않는다.&lt;/li&gt;
&lt;li&gt;이 말은 ForceQueue, Unreliable Multicast와는 절대적인 순서가 보장된다는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Order Between RPCs and Actor Properties&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC와 Property Replicate 사이에는 대략 다음과 같은 규칙이 적용된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC가 먼저 실행된다.&lt;/li&gt;
&lt;li&gt;그리고 Property가 나중에 update된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Property Replicate는 단일의 Unreliable 데이터 단위로 전송된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Bunch Payload는 다음 규칙을 따라 생성된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Queue 되지 않은 PRC 직렬화&lt;/li&gt;
&lt;li&gt;Replicated Property 직렬화&lt;/li&gt;
&lt;li&gt;Queue 된 RPC 직렬화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이로 인해 한가지 주의해야 할 점이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC 내부에서 변경된 Replicated Property의 값이 뒤이어 발생할 Property Replicate에 의해 손실 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>UE5/Network</category>
      <author>RedChiken</author>
      <guid isPermaLink="true">https://redchiken.tistory.com/390</guid>
      <comments>https://redchiken.tistory.com/390#entry390comment</comments>
      <pubDate>Fri, 28 Jun 2024 16:20:20 +0900</pubDate>
    </item>
    <item>
      <title>[Network] Remote Procedure Calls(RPCs)</title>
      <link>https://redchiken.tistory.com/389</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/remote-procedure-calls-in-unreal-engine?application_version=5.3&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RPC&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Local 단위에서 호출하여 Remote로 연결된 1개 이상의 Machinge에서 실행되는 함수&lt;/li&gt;
&lt;li&gt;Return이 없는 단방향 함수 호출이 특징&lt;/li&gt;
&lt;li&gt;RPC는 주로 일시적이거나, 외형적으로 드러나는 Unreliable Gameplay Event에 사용된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사운드 재생&lt;/li&gt;
&lt;li&gt;Particle 생성&lt;/li&gt;
&lt;li&gt;Animation 재생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RPC는 Replicated/ReplicatedUsing 선언이 된 Property의 Replication을 보완하는 중요한 기능이다.&lt;/li&gt;
&lt;li&gt;RPC를 호출하려면 다음 2가지 조건이 성립되어야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Actor나 Actor Component일 것.&lt;/li&gt;
&lt;li&gt;RPC를 호출하는 Object가 Replicate되어 있을 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;마지막으로 RPC를 잘 사용하기 위해서는 Ownership을 잘 이해하는 편이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Type&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Client&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 Actor에 대해 Client Connection을 소유한 Client에서 실행되는 Unicast RPC&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719546483399&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#pragma once

#include &quot;DerivedActor.generated.h&quot;

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:

    // Client RPC Function
    UFUNCTION(Client)
    void ClientRPC();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719546498623&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;DerivedActor.h&quot;

ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP) : Super(PCIP)
{
    bReplicates = true;
}

void ADerivedActor::ClientRPC_Implementation()
{
    // This log will print on every machine that executes this function.
    UE_LOG(LogTemp, Log, TEXT(&quot;ClientRPC executed.&quot;))
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719546524908&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Call from client to run on server
ADerivedClientActor* MyDerivedClientActor;
MyDerivedClientActor-&amp;gt;ClientRPC();&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Execution Matrix&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;493&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CH0lT/btsIggMqfNW/vPMQrIMhvgeUPLRoM27rxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CH0lT/btsIggMqfNW/vPMQrIMhvgeUPLRoM27rxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CH0lT/btsIggMqfNW/vPMQrIMhvgeUPLRoM27rxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCH0lT%2FbtsIggMqfNW%2FvPMQrIMhvgeUPLRoM27rxk%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;1236&quot; height=&quot;493&quot; data-origin-width=&quot;1236&quot; data-origin-height=&quot;493&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Server&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 Actor를 소유하는 Client에서 호출하여 Server에서 실행되는 Unicast RPC&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719546908070&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#pragma once

#include &quot;DerivedActor.generated.h&quot;

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:
    // Server RPC Function
    UFUNCTION(Server)
    void ServerRPC();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719546963740&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;DerivedActor.h&quot;
ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP) : Super(PCIP)
{
    bReplicates = true;
}

void ADerivedActor::ServerRPC_Implementation()
{	
    // This function only executes if ServerRPC_Validate returns true.
    // This log will print on every machine that executes this function.
    UE_LOG(LogTemp, Log, TEXT(&quot;ServerRPC executed.&quot;))
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719546989812&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Call from client to run on server
ADerivedClientActor* MyDerivedClientActor;
MyDerivedClientActor-&amp;gt;ServerRPC();&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Execution Matrix&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;491&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbuFvy/btsIeRtzLE0/AQPYiTIt6zc43BVPQKPQb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbuFvy/btsIeRtzLE0/AQPYiTIt6zc43BVPQKPQb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbuFvy/btsIeRtzLE0/AQPYiTIt6zc43BVPQKPQb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbuFvy%2FbtsIeRtzLE0%2FAQPYiTIt6zc43BVPQKPQb1%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;1228&quot; height=&quot;491&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;491&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;WithValidation&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Server RPC에서만 사용할 수 있는 Specifier
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Server RPC의 신뢰성과 Network Policy를 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719551825392&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#pragma once
#include &quot;DerivedActor.generated.h&quot;

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:

    UPROPERTY(Replicated)
    int32 Health;
    int32 MAXHEALTH = 100;

    // Server Unreliable RPC Function
    UFUNCTION(Server, Unreliable, WithValidation)
    void ServerUnreliableRPC(int32 RecoverHealth); 
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Validate 함수는 내부 로직을 통해 RPC 함수를 Server에서 실행할지 여부를 판단한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그렇기에 Validate Specifier가 선언된 Server RPC가 실행될 때 Validate 함수가 가장 먼저 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719551865904&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;DerivedActor.h&quot;

// RPC Validation Implementation
bool ServerUnreliableRPC_Validate(int32 RecoverHealth)
{
    if (Health + RecoverHealth &amp;gt; MAXHEALTH)
    {
        return false;
    }
return true;
}

// RPC Implementation
void ServerUnreliableRPC_Implementation(int32 RecoverHealth)
{
    Health += RecoverHealth;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 Validate 함수에서 false를 반환하면, 해당 Server RPC를 전송한 Client는 Server로부터 연결이 끊긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NetMulticast&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Server에서 호출&lt;/li&gt;
&lt;li&gt;호출한 Actor와 Relevant한 모든 Client에서 실행되는 Multicast RPC&lt;/li&gt;
&lt;li&gt;Client에서도 호출할 수 있으나 Local에서만 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719547449385&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#pragma once

#include &quot;DerivedActor.generated.h&quot;

UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()

public:
    // Multicast RPC Function
    UFUNCTION(NetMulticast)
    void MulticastRPC();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719547479841&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &quot;DerivedActor.h&quot;

ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP) : Super(PCIP)
{
    bReplicates = true;
}

void ADerivedActor::MulticastRPC_Implementation()
{
    // This log will print on every machine that executes this function.
    UE_LOG(LogTemp, Log, TEXT(&quot;MulticastRPC executed.&quot;))	
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719547494681&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Call from server to run on server and all relevant clients
ADerviedServerActor* MyDerivedServerActor;
MyDerievedServerActor-&amp;gt;MulticastRPC();&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Execution Matrix&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1227&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B16y5/btsIgYEwUmd/pk2HsFPyS9DJsozbdfxPO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B16y5/btsIgYEwUmd/pk2HsFPyS9DJsozbdfxPO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B16y5/btsIgYEwUmd/pk2HsFPyS9DJsozbdfxPO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB16y5%2FbtsIgYEwUmd%2Fpk2HsFPyS9DJsozbdfxPO1%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;1227&quot; height=&quot;492&quot; data-origin-width=&quot;1227&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reliability&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client/Server/NetMulticast와 같이 사용되는 Specifier&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Reliable&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC 수신자로부터 ACK를 받지 못하면 RPC를 재전송한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음 RPC 호출은 앞선 RPC의 ACK를 수신할 때 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;순서대로 도착하는 것을 보장해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Unreliable&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC Packet이 Drop되면 실행되지 않는다.&lt;/li&gt;
&lt;li&gt;도착 순서를 보장하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Send Policy&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ERemoteFunctionSendPolicy를 지정하여 RPC의 전송 순서를 명시적으로 조정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719550187270&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enum class ERemoteFunctionSendPolicy
{		
	/** Unreliable multicast are queued. Everything else is send immediately */
	Default, 

	/** Bunch is send immediately no matter what */
	ForceSend,

	/** Bunch is queued until next actor replication, no matter what */
	ForceQueue,
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Send Policy 조절은 NetDriver::ProcessRemoteFunctionForChannel을 통해 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719549771211&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** Process a remote function on given actor channel. This is called by ::ProcessRemoteFunction.*/
ENGINE_API void ProcessRemoteFunctionForChannel(
	UActorChannel* Ch,
	const class FClassNetCache* ClassCache,
	const FFieldNetCache* FieldCache,
	UObject* TargetObj,
	UNetConnection* Connection,
	UFunction* Function,
	void* Parms,
	FOutParmRec* OutParms,
	FFrame* Stack,
	const bool IsServer,
	const ERemoteFunctionSendPolicy SendPolicy = ERemoteFunctionSendPolicy::Default);

void UNetDriver::ProcessRemoteFunctionForChannel(
	UActorChannel* Ch,
	const FClassNetCache* ClassCache,
	const FFieldNetCache* FieldCache,
	UObject* TargetObj,
	UNetConnection* Connection,
	UFunction* Function,
	void* Parms,
	FOutParmRec* OutParms,
	FFrame* Stack,
	const bool bIsServer,
	const ERemoteFunctionSendPolicy SendPolicy)
{
	EProcessRemoteFunctionFlags UnusedFlags = EProcessRemoteFunctionFlags::None;
	ProcessRemoteFunctionForChannelPrivate(Ch, ClassCache, FieldCache, TargetObj, Connection, Function, Parms, OutParms, Stack, bIsServer, SendPolicy, UnusedFlags);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Default&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC가 bunch에 직렬화 된다.&lt;/li&gt;
&lt;li&gt;Bunch는 다음 Frame 마지막에 NetUpdate에서 전송된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ForceSend&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC가 NetDriver::PostTickDispatch에서 trigger되면 bunch에 즉시 직렬화 되고 Network에 전송된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;tick의 나머지 부분이 동작하는 도중에 trigger 되면, Default로 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 특별한 RPC 최적화 기법은 아래 조건 하에서 동작한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replication Graph나 Iris를 사용할 때에만 동작한다.&lt;/li&gt;
&lt;li&gt;NetWroldTickTime에서 호출된 RPC에서 동작.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수신한 패킷되고 수신한 RPC가 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ForceQueue&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Network Update이 마무리 될 때 Bandwidth가 남아 있다면 Bunch에 직렬화된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>UE5/Network</category>
      <category>network</category>
      <category>Remote Procedure Call</category>
      <category>RPC</category>
      <category>UE5</category>
      <category>Unreal Engine</category>
      <author>RedChiken</author>
      <guid isPermaLink="true">https://redchiken.tistory.com/389</guid>
      <comments>https://redchiken.tistory.com/389#entry389comment</comments>
      <pubDate>Fri, 28 Jun 2024 14:22:05 +0900</pubDate>
    </item>
    <item>
      <title>[Network] Property Replication</title>
      <link>https://redchiken.tistory.com/388</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replicate-actor-properties-in-unreal-engine?application_version=5.3&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replicate-actor-properties-in-unreal-engine?application_version=5.3&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replicated-subobjects-in-unreal-engine?application_version=5.3&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replicated-subobjects-in-unreal-engine?application_version=5.3&lt;/a&gt;&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;Actor&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Replicated&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Actor 내에서 Replicate 할 Property에 UPROPERTY() 선언&lt;/li&gt;
&lt;li&gt;UPROPERTY() 내에 'Replicated' Specifier를 입력&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166313&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#pragma once 
 
#include &quot;DerivedActor.generated.h&quot;
 
UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()
 
public:
    // Property to replicate
    UPROPERTY(Replicated)
    uint32 Health;
 
    // Derived Actor constructor
    ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP);
 
    // Override Replicate Properties function
    virtual void GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const override;
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 Actor의 bReplicates 옵션을 활성화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성자나 BP 옵션에서 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GetLifetimeReplicatedProps() 함수를 override하여 DOREPLIFETIME 매크로를 선언&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166314&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#include &quot;DerivedActor.h&quot;
#include &quot;Net/UnrealNetwork.h&quot;
 
ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP) : Super(PCIP)
{
    bReplicates = true;
}
 
void ADerivedActor::GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const
{
    // Call the Super
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
    // Add properties to replicated for the derived class
    DOREPLIFETIME(ADerivedActor, Health);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같은 방식으로 Object 또한 Replicate를 할 수 있다.&lt;/li&gt;
&lt;li&gt;Network를 거쳐서 참조되는 Object는 반드시 Network 기능이 지원되어야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 확인하려면 UObject::IsSupportedForNetworking() 함수를 이용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Network Replicated Reference&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replicated Actor&lt;/li&gt;
&lt;li&gt;Replicated Component&lt;/li&gt;
&lt;li&gt;Stably-named 하지만 Replicated 하지 않은 Actor나 Component&lt;/li&gt;
&lt;li&gt;Package로부터 Load 된 UObject(Actor, Component도 아닌)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Stably-named Object&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;Server와 Client 양쪽에 같은 이름으로 존재하는 Object를 지칭한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Actor가 Gameplay 중 Spawn되지 않고 Package로부터 직접 Load 되면 Stably-named하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Actor Component는 다음 케이스일 경우 Stably-named 하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pacakge로부터 직접 load 된 경우&lt;/li&gt;
&lt;li&gt;간단한 생성자 호출로 추가 된 경우&lt;/li&gt;
&lt;li&gt;UActorComponent::SetNetAddressable로 마킹 된 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Component의 이름을 Server와 Client가 모두 명확하게 알고 있는 경우&lt;/li&gt;
&lt;li&gt;AActor C++ 생성자에서 추가되는 Component들이 그 대표적인 예시이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;ReplicatedUsing&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&quot;OnRep_&quot;으로 시작하는 함수를 지정해&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;Replicate 될 때마다 특정 행동을 부여할 수 있는 Specifier&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166315&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#pragma once 
 
#include &quot;DerivedActor.generated.h&quot;
 
UCLASS()
class ADerivedActor : public AActor
{
	GENERATED_BODY()
 
public:
 
	// Replicated Property using OnRep_Value
	UPROPERTY(ReplicatedUsing=OnRep_Value)
	int32 HealthValue1;
 
	// Replicated Property using OnRep_ConstRef
	UPROPERTY(ReplicatedUsing=OnRep_ConstRef)
	int32 HealthValue2;
 
	// Replicated Property using OnRep_NoParam
	UPROPERTY(ReplicatedUsing=OnRep_NoParam)
	int32 HealthValue3;
 
	// Signature to pass copy of the last value
	UFUNCTION()
	void OnRep_Value(int32 LastHealthValue);
 
	// Signature to pass const reference
	UFUNCTION()
	void OnRep_ConstRef(const int32&amp;amp; LastHealthValue);
 
	// Signature to pass no parameter
	UFUNCTION()
	void OnRep_NoParam();
 
	// Derived Actor constructor
	ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP);
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ReplicatedUsing에 연결 될 함수들은 UFUNCTION()으로 선언되어 있어야 한다.&lt;/li&gt;
&lt;li&gt;또한 경우에 따라서 Parameter를 받을 수도 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Parameter에 전달되는 값은 Replicated Property가 변경되기 이전의 값이다.&lt;/li&gt;
&lt;li&gt;변경된 이후의 값은 Property가 직접 들고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166316&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#include &quot;DerivedActor.h&quot;
#include &quot;Net/UnrealNetwork.h&quot;
 
ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP) : Super(PCIP)
{
	bReplicates = true;
}
 
void ADerivedActor::GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const
{
	// Call the Super
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
	// Add properties to replicated for the derived class
	DOREPLIFETIME(ADerivedActor, HealthValue1);
	DOREPLIFETIME(ADerivedActor, HealthValue2);
	DOREPLIFETIME(ADerivedActor, HealthValue3);
}
 
void ADerivedActor::OnRep_Value(int32 LastHealthValue)
{
	UE_LOG(LogTemp, Log, TEXT(&quot;OnRep_Value with value. Last value: %d&quot;), LastHealthValue)
	// Add custom OnRep logic
}
 
void ADerivedActor::OnRep_ConstRef(const int32&amp;amp; LastHealthValue)
{
	UE_LOG(LogTemp, Log, TEXT(&quot;OnRep_ConstRef with const ref. Last value: %d&quot;), *LastHealthValue)
	// Add custom OnRep logic
}
 
void ADerivedActor::OnRep_NoParam()
{
	UE_LOG(LogTemp, Log, TEXT(&quot;OnRep_NoParam with no parameter.&quot;))
	// Add custom OnRep logic
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RepNotify는 C++과 BP에서 조금 다르게 동작한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BP로 RepNotify가 선언되어 있다면, Replicated Property에 대해 Set 함수가 호출될 때 RepNotify도 호출된다.&lt;/li&gt;
&lt;li&gt;하지만 기본적으로 Replicated Property의 Reference를 갖는 Function, Macro에서는&lt;br /&gt;값이 변경되더라도 RepNotify가 호출되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;NotReplicated&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replicated 되는 Actor나 Struct 내에서 특정 Property를 Replicate 되지 않도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166317&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#pragma once 
 
#include &quot;DerivedActor.generated.h&quot;
 
USTRUCT()
struct FMyStruct
{
	GENERATED_BODY()
 
	UPROPERTY()
	int32 ReplicatedProperty;
 
	// Not Replicated even though encompassing struct is Replicated
	UPROPERTY(NotReplicated)
	int32 NotReplicatedProperty;
};
 
UCLASS()
class ADerivedActor : public AActor
{
	GENERATED_BODY()
 
public:
	UPROPERTY(Replicated)
	FMyStruct ReplicatedStruct;
 
	// Derived Actor constructor
	ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP);
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5.4버전 엔진 코드 기준으로는 NotReplicated Property는 GetLifetimeReplicatedProps() 내에서 DISABLE_REPLICATED_PROPERTY 선언을 해주어야 한다.&lt;/li&gt;
&lt;li&gt;다만 이는 Warning이 발생할 뿐이고 Error가 발생하지는 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166318&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#include &quot;DerivedActor.h&quot;
#include &quot;Net/UnrealNetwork.h&quot;
 
ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP) : Super(PCIP)
{
	bReplicates = true;
}
 
void ADerivedActor::GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
	DOREPLIFETIME(ADerivedActor, ReplicatedStruct);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Conditional Replication&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replicated Property는 한번 등록되면 Lifetime 동안 해제할 수 없다.&lt;/li&gt;
&lt;li&gt;기본적으로 Replicated Property는 값이 바뀔 때에 Replicate 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그 즉슨, 값이 바뀌지 않으면 Replicate 되지 않고 Bandwidth를 점유하지 않는다.&lt;/li&gt;
&lt;li&gt;때문에 Replicate 조건을 부여하여 불필요한 Replicate를 줄이면 이는 성능 향상으로 이어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Replication Condition&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GetLifetimeReplicatedProps() 함수에서 DOREPLIFETIME() 매크로 대신&lt;br /&gt;DOREPTIME_CONDITION() 매크로를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166319&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#define DOREPLIFETIME_CONDITION(c,v,cond) \
{ \
	static_assert(cond != COND_NetGroup, &quot;COND_NetGroup cannot be used on replicated properties. Only when registering subobjects&quot;); \
	FDoRepLifetimeParams LocalDoRepParams; \
	LocalDoRepParams.Condition = cond; \
	DOREPLIFETIME_WITH_PARAMS(c,v,LocalDoRepParams); \
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 Replicated 뿐 아니라 ReplicatedUsing Specifier도 적용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166319&quot; class=&quot;dts&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#include &quot;DerivedActor.h&quot;
#include &quot;Net/UnrealNetwork.h&quot;
 
void ADerivedActor::GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const
{
	// Call the Super
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
	// Add property replication with a condition
	DOREPLIFETIME_CONDITION(ADerivedActor, Health, COND_OwnerOnly);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;RepNotify Condition&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replication System은 Replicate 뿐 아니라 RepNotify 호출에 대해서도 조건을 부여할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이를 위해서는 DOREPLIFETIME_CONDITION_NOTIFY가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166319&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;/** Allows gamecode to specify RepNotify condition: REPNOTIFY_OnChanged (default) or REPNOTIFY_Always for when repnotify function is called  */
#define DOREPLIFETIME_CONDITION_NOTIFY(c,v,cond,rncond) \
{ \
	static_assert(cond != COND_NetGroup, &quot;COND_NetGroup cannot be used on replicated properties. Only when registering subobjects&quot;); \
	FDoRepLifetimeParams LocalDoRepParams; \
	LocalDoRepParams.Condition = cond; \
	LocalDoRepParams.RepNotifyCondition = rncond; \
	DOREPLIFETIME_WITH_PARAMS(c,v,LocalDoRepParams); \
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드를 보면 알 수 있듯이,&lt;br /&gt;DOREPLIFETIME_CONDITION_NOTIFY에서는 RepNotify 조건 외에 Replicate 조건도 지정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166320&quot; class=&quot;smali&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#include &quot;DerivedActor.h&quot;
#include &quot;Net/UnrealNetwork.h&quot;
 
void ADerivedActor::GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const
{
	// Call the Super
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
 
	// Add property replication with a condition
	/** 	Use this to always execute RepNotify
	*	Associated OnRep called on client every time property replicates
	*/
	DOREPLIFETIME_CONDITION_NOTIFY(ADerivedActor, Health, COND_OwnerOnly, REPNOTIFY_Always);
 
	/** 	Use this to only execute RepNotify when property changes
	*	Associated OnRep called on client only when property changes
	*/
	DOREPLIFETIME_CONDITION_NOTIFY(ADerivedActor, Health, COND_OwnerOnly, REPNOTIFY_OnChanged);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Custom Condition&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음 작업을 통해 엔진에서 제공하는 조건 외의 조건으로 Replicate를 조절할 수 있다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replication Condition을 COND_Custom으로 지정&lt;/li&gt;
&lt;li&gt;bool 타입 혹은 이를 반환하는 함수를 PreReplication에 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166320&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#pragma once 
 
#include &quot;DerivedActor.generated.h&quot;
 
UCLASS()
class ADerivedActor : public AActor
{
    GENERATED_BODY()
 
public:
    UPROPERTY(Replicated)
    int32 Health;
 
    virtual void GetLifetimeReplicatedProps(TArray&amp;lt;FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const override;
 
    // Derived Actor constructor
    ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP);
 
    virtual void PreReplication(IRepChangedPropertyTracker&amp;amp; ChangedPropertyTracker) override;
 
    // Custom Replication Condition override function
    bool IsInvincible();
};&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PreReplication에 Custom Condition을 지정할 때에는 DOREPLIFETIME_ACTIVE_OVERRIDE 매크로를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166321&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;// Built on Compile time and Disable to work with Array
#define DOREPLIFETIME_ACTIVE_OVERRIDE_FAST(c,v,active) \
{ \
	static_assert(ValidateReplicatedClassInheritance&amp;lt;c, ThisClass&amp;gt;(), #c &quot;.&quot; #v &quot; is not accessible from this class.&quot;); \
	UE::Net::Private::FNetPropertyConditionManager::SetPropertyActiveOverride(ChangedPropertyTracker, this, (int32)c::ENetFields_Private::v, active); \
}

// Built on Compile time and Enable to Work with Array
#define DOREPLIFETIME_ACTIVE_OVERRIDE_FAST_STATIC_ARRAY(c,v,active) \
{ \
	static_assert(ValidateReplicatedClassInheritance&amp;lt;c, ThisClass&amp;gt;(), #c &quot;.&quot; #v &quot; is not accessible from this class.&quot;); \
	for (int32 i = 0; i &amp;lt; (int32)c::EArrayDims_Private::v; ++i) \
	{ \
		UE::Net::Private::FNetPropertyConditionManager::SetPropertyActiveOverride(ChangedPropertyTracker, this, (int32)c::ENetFields_Private::v##_STATIC_ARRAY + i, active); \
	} \
}

// Built on Runtime and Enable to Work with Array
#define DOREPLIFETIME_ACTIVE_OVERRIDE(c,v,active) \
{ \
	static_assert(ValidateReplicatedClassInheritance&amp;lt;c, ThisClass&amp;gt;(), #c &quot;.&quot; #v &quot; is not accessible from this class.&quot;); \
	static FProperty* sp##v = GetReplicatedProperty(StaticClass(), c::StaticClass(),GET_MEMBER_NAME_CHECKED(c,v)); \
	for (int32 i = 0; i &amp;lt; sp##v-&amp;gt;ArrayDim; i++) \
	{ \
		UE::Net::Private::FNetPropertyConditionManager::SetPropertyActiveOverride(ChangedPropertyTracker, this, sp##v-&amp;gt;RepIndex + i, active); \
	} \
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 Health는 IsInvincible()이 false일 때에만 Replicate된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166322&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;#include &quot;DerivedActor.h&quot;
#include &quot;Net/UnrealNetwork.h&quot;

ADerivedActor::ADerivedActor(const class FPostConstructInitializeProperties &amp;amp; PCIP) : Super(PCIP)
{
	bReplicates = true;
}

void ADerivedActor::GetLifetimeReplicatedProps(TArray&amp;amp; OutLifetimeProps) const 
{
	// Call the Super 
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	// Add properties to replicated for the derived class
    DOREPLIFETIME_CONDITION(ADerivedActor, Health, COND_Custom);
}

/* Function where the Custom condition is registered. */ 
void ADerivedActor::PreReplication(IRepChangedPropertyTracker&amp;amp; ChangedPropertyTracker)
{
	// Call the Super
    Super::PreReplication(ChangedPropertyTracker);

	/* Use a custom property replication condition In this case, a function IsInvincible() If the actor is invincible, don't replicate Health */
    DOREPLIFETIME_ACTIVE_OVERRIDE(ADerivedActor, Health, !IsInvincible());
}

bool IsInvincible()
{
	bool bIsInvincible = false;

	// Custom logic to determine invincibility...
	return bIsInvincible;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Custom Condition은 매우 편리해 보이지만 2가지 큰 이유로 자주 사용하지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫째, 작업 시간과 소모 리소스가 크다&lt;/li&gt;
&lt;li&gt;둘째, Connection 기준을 변경할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;ELifetimeCondition&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DOREPLIFETIME_CONDITION(_NOTIFY) 매크로의 3번째 parameter로 전달하는 값들.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166323&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;/** Secondary condition to check before considering the replication of a lifetime property. */
UENUM(BlueprintType)
enum ELifetimeCondition : int
{
	COND_None = 0							UMETA(DisplayName = &quot;None&quot;),							// This property has no condition, and will send anytime it changes
	COND_InitialOnly = 1					UMETA(DisplayName = &quot;Initial Only&quot;),					// This property will only attempt to send on the initial bunch
	COND_OwnerOnly = 2						UMETA(DisplayName = &quot;Owner Only&quot;),						// This property will only send to the actor's owner
	COND_SkipOwner = 3						UMETA(DisplayName = &quot;Skip Owner&quot;),						// This property send to every connection EXCEPT the owner
	COND_SimulatedOnly = 4					UMETA(DisplayName = &quot;Simulated Only&quot;),					// This property will only send to simulated actors
	COND_AutonomousOnly = 5					UMETA(DisplayName = &quot;Autonomous Only&quot;),					// This property will only send to autonomous actors
	COND_SimulatedOrPhysics = 6				UMETA(DisplayName = &quot;Simulated Or Physics&quot;),			// This property will send to simulated OR bRepPhysics actors
	COND_InitialOrOwner = 7					UMETA(DisplayName = &quot;Initial Or Owner&quot;),				// This property will send on the initial packet, or to the actors owner
	COND_Custom = 8							UMETA(DisplayName = &quot;Custom&quot;),							// This property has no particular condition, but wants the ability to toggle on/off via SetCustomIsActiveOverride
	COND_ReplayOrOwner = 9					UMETA(DisplayName = &quot;Replay Or Owner&quot;),					// This property will only send to the replay connection, or to the actors owner
	COND_ReplayOnly = 10					UMETA(DisplayName = &quot;Replay Only&quot;),						// This property will only send to the replay connection
	COND_SimulatedOnlyNoReplay = 11			UMETA(DisplayName = &quot;Simulated Only No Replay&quot;),		// This property will send to actors only, but not to replay connections
	COND_SimulatedOrPhysicsNoReplay = 12	UMETA(DisplayName = &quot;Simulated Or Physics No Replay&quot;),	// This property will send to simulated Or bRepPhysics actors, but not to replay connections
	COND_SkipReplay = 13					UMETA(DisplayName = &quot;Skip Replay&quot;),						// This property will not send to the replay connection
	COND_Dynamic = 14						UMETA(Hidden),											// This property wants to override the condition at runtime. Defaults to always replicate until you override it to a new condition.
	COND_Never = 15							UMETA(Hidden),											// This property will never be replicated
	COND_NetGroup = 16						UMETA(Hidden),											// This subobject will replicate to connections that are part of the same group the subobject is registered to. Not usable on properties.
	COND_Max = 17							UMETA(Hidden)
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;ELifetimeRepnotifyCondition&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DOREPLIFETIME_CONDITION_NOTIFY 매크로의 4번째 parameter로 전달되는 값들&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166325&quot; class=&quot;crystal&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;enum ELifetimeRepNotifyCondition
{
	REPNOTIFY_OnChanged = 0,		// Only call the property's RepNotify function if it changes from the local value
	REPNOTIFY_Always = 1,		// Always Call the property's RepNotify function when it is received from the server
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;SubObject&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;ReplicateSubobjects&lt;/h3&gt;
&lt;pre id=&quot;code_1719543166325&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;class AMyActor : public AActor
{
    UPROPERTY(Replicated)
    UMySubObjectClass* MySubObject;
}

class UMySubObjectClass : public UObject
{
    UPROPERTY(Replicated)
    int32 Counter = 0;
}

void AMyActor::CreateMyClass()
{
    MySubObject = NewObject&amp;lt;UMySubObjectClass&amp;gt;();
    MySubObject-&amp;gt;Counter = 10;
}

void AMyActor::ReplicateSubobjects(...)
{
    Super::ReplicateSubobjects(...);
    Channel-&amp;gt;ReplicateSubobject(MySubObject); // Becomes a subobject here
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Channel에 Subobject를 등록하지 않는다면, Client에서 해당 Subobject는 항상 null일 것이다.&lt;/li&gt;
&lt;li&gt;Subobject를 Replicate 하더라도 그 안의 Property를 Replicate하려면 Specifier 선언을 따로 해줘야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Registered Subobject&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Subobject를 Owning Actor/ActorComponent List에 등록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;List에 등록 된 Object들은 Actor Channel에 의해 자동으로 replicate 된다.&lt;/li&gt;
&lt;li&gt;등록하 때 ELifetimeCondition을 통해 Replicate condition을 부여할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 방식은 ReplicateSubObjects 함수 사용에 대한 업무 부담을 줄여줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166326&quot; class=&quot;lasso&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;AMyActor::AMyActor()
{
    bReplicateUsingRegisteredSubObjectList = true;
}

void AMyActor::CleanupSubobject()
{
    if (MySubobject)
    {
        RemoveReplicatedSubobject(MySubObject);
    }
}

void AMyActor::CreateMyClass()
{
    CleanupSubobject();

    MySubObject= NewObject&amp;lt;UMySubObjectClass&amp;gt;();
    MySubObject-&amp;gt;Counter = 10;
    AddReplicatedSubObject(MySubObject);
}

void AMyActor::CreateMyDerivedClass()
{
    CleanupSubobject();

    MySubObject = NewObject&amp;lt;UMyDerivedSubObjectClass&amp;gt;();
    AddReplicatedSubObject(MySubObject);
}

void AMyActor::ReplicateSubobjects(...)
{
    //deprecated and not called anymore
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AddReplicatedSubObject 함수는 ReadyForReplication이나 BeginPlay, 또는 Subobject를 생성할 때 호출한다.&lt;/li&gt;
&lt;li&gt;이 중 ReadyForReplication에서 함수 호출 시 주의할 점이 있따.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ActorComponent에서 ReadyForReplication 함수는 InitComponent와 BeginPlay 사이에 호출된다.&lt;/li&gt;
&lt;li&gt;즉슨, Component를 이 함수 안에서 등록하게 되면 BeginPlay에서는 RPC를 수행할 수 있다는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Subobject를 수정하거나 삭제 할 때에는 반드시 RemoveReplicateSubObject 함수를 호출해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 함수를 호출하지 않고 수정/삭제 시, Object의 Destruction 과정에서 한번 더 Destroy가 Mark된다.&lt;/li&gt;
&lt;li&gt;이는 GC가 동작할 때 Crash를 유발할 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 부분의 코드를 수정 할 때, net.SubObjects.CompareWithLegacy를 Console Command로 설정하면&lt;br /&gt;Runtime에서 Registered SubObjectList와 이전 함수를 비교할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;차이점이 감지되면 ensure가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Components&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replicated Component도 기본적으로 Subobject와 동일하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Component의 경우에는 AllowActorComponentToReplicate 함수를 override 한다.&lt;/li&gt;
&lt;li&gt;이 때 각 Component의 Replicate Condition은 내부 조건문에 맞춰 판정, 반환해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;만약 BeginPlay가 호출 된 이후에 Component의 ReplicateCondition을 바꾸고 싶다면,&lt;br /&gt;SetReplicatedComponentNetCondition 함수를 이용한다.&lt;/li&gt;
&lt;li&gt;Owning Component List는 Condition이 확인되기 전에 각 Connection에 Replicate 되어야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 Componenet가 SkipOwner인 경우,&lt;br /&gt;SubObject가 OwnerOnly이더라도 Owner에게 Replicate 되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166327&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;ELifetimeCondition AMyWeaponClass::AllowActorComponentToReplicate(const UActorComponent* ComponentToReplicate) const
{
    // Do not replicate some components while the object is on the ground.
    if (!bIsInInventory)
    {
        if (IsA&amp;lt;UDamageComponent&amp;gt;(ComponentToReplicate))
        {
            return COND_Never;
        }
    }
    Super::AllowActorComponentToReplicate(ComponentToReplicate);
}

void AMyWeaponClass::OnPickup()
{
    // Now replicate the component to all
    SetReplicatedComponentNetCondition(UDamageComponent, COND_None);
    bIsInInventory = true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Complex Replication Condition&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NetConditionGroupManager와 COND_NetGroup을 이용해 Replicate Condition을 새로 만들 수 있다.&lt;/li&gt;
&lt;li&gt;이는 Subobject와 PlayerController가 여러 Group에 동시에 속해 있을 때,&lt;br /&gt;이 중 하나의 Group에만 속해 있어도 Replicate된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166328&quot; class=&quot;isbl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;FName NetGroupAlpha(TEXT(&quot;NetGroup_Alpha&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원하는 Subobject를 위에서 추가한 NetGroupAlpha Group에 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166328&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;FNetConditionGroupManager::RegisterSubObjectInGroup(MyAlphaSubObject, NetGroupAlpha)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Subobject를 Replicate 할 Client의 PlayerController를 이용해 같은 Group에 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719543166328&quot; class=&quot;isbl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;PlayerControllerAlphaOwner-&amp;gt;IncludeInNetConditionGroup(NetGroupAlpha)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 PlayerControllerAlphaOwner의 Client는 Owner Actor가 해당 Client의 Connection에 Replicate 될 때마다&lt;br /&gt;등록된 MyAlphaSubobject를 Replicate 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Client Subobject List&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Server가 Replicated Subobject List를 관리하는 것처럼, Client도 자체적으로 Subobject List를 관리해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이는 특히 Client에서 Replay를 녹화할 때 중요하다.&lt;/li&gt;
&lt;li&gt;이 경우, Client의 Actor는 Replay에 기록할 때 일시적으로 Local Authority Role로 전환된다.&lt;/li&gt;
&lt;li&gt;그렇기에 Replay에 기록된 Actor은 Local Role에 상관 없이&lt;br /&gt;Client에서 자체적으로 Subobject List를 유지해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;만약 Subobject가 Replicated Property라면, RepNotify를 사용함으로 더 쉽게 관리할 수 있따.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client는 RepNotify를 통해 SubObject가 변경되었음을 확인하여,&lt;br /&gt;이전 포인터를 제거하고 새로운 것을 추가할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Server의 Subobject List에서 Replicated Subobject를 제거하면&lt;br /&gt;해당 Object의 Replicated Property가 Client에 Replicate 되지는 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 SubObject의 포인터는 UObject가 스스로를 Grabage라고 마크 하기 전까지 Net-Referencable하다.&lt;/li&gt;
&lt;li&gt;Server가 UObject가 Invalid하다 탐지하게 되면,&lt;br /&gt;다음 Reflection 업데이트에서 Client로 하여금 자체적으로 해당 Subobject를 지우도록 알린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>UE5/Network</category>
      <author>RedChiken</author>
      <guid isPermaLink="true">https://redchiken.tistory.com/388</guid>
      <comments>https://redchiken.tistory.com/388#entry388comment</comments>
      <pubDate>Fri, 28 Jun 2024 11:55:13 +0900</pubDate>
    </item>
  </channel>
</rss>