<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코딩 부부</title>
    <link>https://wecandev.tistory.com/</link>
    <description>게임개발자와 웹개발자의 기록 보관소</description>
    <language>ko</language>
    <pubDate>Tue, 18 Aug 2026 10:40:05 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>테디슨</managingEditor>
    <image>
      <title>코딩 부부</title>
      <url>https://tistory1.daumcdn.net/tistory/4802615/attach/71a91ec55ded4133917b381549cbbd6b</url>
      <link>https://wecandev.tistory.com</link>
    </image>
    <item>
      <title>[언리얼엔진] Localization</title>
      <link>https://wecandev.tistory.com/231</link>
      <description>&lt;h1&gt;언리얼 엔진 현지화&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 개발을 하다 보면 언젠가 마주치게 되는 &lt;b&gt;현지화(Localization)&lt;/b&gt;. 언리얼 엔진은 강력한 내장 현지화 시스템을 제공하지만, 처음 접하면 &quot;어디서부터 시작해야 하지?&quot; 하는 생각이 들기 마련이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 현지화에 대해 아무것도 모르는 상태에서 시작해, 실제로 동작하는 시스템을 구축하기까지의 과정을 정리한 글이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 현지화 방식 선택 : 내장 vs 자체 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼에서 현지화를 구현하는 방식은 크게 두 가지다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언리얼 내장 (String Table + Localization Dashboard)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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 위젯에서 바로 연동 (별도 코드 없이 자동 교체)&lt;/li&gt;
&lt;li&gt;LOCTEXT / NSLOCTEXT 매크로로 코드에서 간편하게 사용&lt;/li&gt;
&lt;li&gt;한국어 조사 자동 처리 (&lt;code&gt;{변수}(을,를)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;복수형(Plural Form) 처리 내장&lt;/li&gt;
&lt;li&gt;숫자/날짜/통화 자동 포맷&lt;/li&gt;
&lt;li&gt;.po 표준 포맷 지원 &amp;rarr; PoEdit 등 외부 툴과 호환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자체 구현 (CSV/JSON 직접 로드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추천 기준&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;추천&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;패키지 게임 (스팀, 콘솔)&lt;/td&gt;
&lt;td&gt;언리얼 내장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라이브 서비스 모바일&lt;/td&gt;
&lt;td&gt;자체 구현 또는 내장 + Pak 패치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소규모 팀, 빠른 개발&lt;/td&gt;
&lt;td&gt;언리얼 내장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 &lt;b&gt;언리얼 내장 방식&lt;/b&gt;을 다룬다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 한국어 조사 자동 처리 - 가장 큰 강점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국어 현지화에서 가장 골치 아픈 부분이 &lt;b&gt;조사 처리&lt;/b&gt;다. 동적인 아이템 이름이 들어갈 때마다 받침 유무에 따라 조사가 바뀌어야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 내장 시스템은 이를 매크로 문법 하나로 해결해준다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FText Format = LOCTEXT(&quot;ItemGet&quot;, &quot;{ItemName}(을,를) 획득했습니다.&quot;);

FFormatNamedArguments Args;
Args.Add(&quot;ItemName&quot;, FText::FromString(&quot;사과&quot;));
// 결과 : &quot;사과를 획득했습니다.&quot;

Args.Add(&quot;ItemName&quot;, FText::FromString(&quot;사탕&quot;));
// 결과 : &quot;사탕을 획득했습니다.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원하는 조사 쌍&lt;/p&gt;
&lt;pre class=&quot;scheme&quot;&gt;&lt;code&gt;(을,를) (이,가) (은,는) (와,과) (아,야)
(으로,로) (이어,여) (이에,에) (이었,였)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것만으로도 언리얼 내장 시스템을 쓸 이유가 충분하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념 이해&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Target과 Namespace의 차이&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;.locres 파일 단위 (배포 단위)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Namespace&lt;/td&gt;
&lt;td&gt;코드 내에서의 논리적 그룹핑&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 Target 안에 여러 Namespace가 들어갈 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Target : Game  (= Game.locres 하나)
└── Namespace들 (한 파일 안에 다 들어감)
    ├── &quot;Game&quot;
    ├── &quot;GameQuest&quot;
    ├── &quot;GameItem&quot;
    └── &quot;GameUI&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수집 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gather는 &lt;b&gt;Search Directory 하위의 모든 LOCTEXT&lt;/b&gt;를 Namespace 이름과 무관하게 모두 수집한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// Search Directory가 Source/Client/ 일 때

// Source/Client/Player.cpp
#define LOCTEXT_NAMESPACE &quot;Game&quot;
LOCTEXT(&quot;Hello&quot;, &quot;안녕&quot;)          &amp;rarr; 수집됨

// Source/Client/Quest.cpp
#define LOCTEXT_NAMESPACE &quot;GameQuest&quot;
LOCTEXT(&quot;StartQuest&quot;, &quot;시작&quot;)     &amp;rarr; 수집됨 (Namespace 달라도)

// Source/Editor/Tool.cpp  &amp;larr; Search Directory 밖
LOCTEXT(&quot;Tool&quot;, &quot;도구&quot;)           &amp;rarr; 수집 안됨&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 전체 워크플로우 5단계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 1 : 코드에서 텍스트 작성&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// .cpp 파일 상단에 네임스페이스 정의
#define LOCTEXT_NAMESPACE &quot;Game&quot;

void AMyCharacter::ShowGreeting()
{
    // LOCTEXT(Key, DefaultText)
    FText Greeting = LOCTEXT(&quot;HelloKey&quot;, &quot;안녕하세요&quot;);

    // 동적 값을 포맷에 끼워넣기
    LevelText-&amp;gt;SetText(
        FText::Format(
            NSLOCTEXT(&quot;Game&quot;, &quot;GearLevel&quot;, &quot;Lv. {0}&quot;),
            Gear-&amp;gt;GetLevel()
        )
    );
}

#undef LOCTEXT_NAMESPACE&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Namespace&lt;/td&gt;
&lt;td&gt;&quot;Game&quot; &amp;rarr; 그룹 식별자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key&lt;/td&gt;
&lt;td&gt;&quot;HelloKey&quot; &amp;rarr; 고유 식별자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DefaultText&lt;/td&gt;
&lt;td&gt;&quot;안녕하세요&quot; &amp;rarr; 원본 텍스트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 2 : Gather (수집)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Tools &amp;rarr; Localization Dashboard&lt;/code&gt;에서 Target을 생성하고 Gather를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 시 가장 중요한 항목&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Search Directories&lt;/b&gt; : 코드가 있는 폴더 (예: &lt;code&gt;Source/Game/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Native Culture&lt;/b&gt; : 원본 언어 (예: &lt;code&gt;ko&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Supported Cultures&lt;/b&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;983&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxEG3a/dJMcadhtveg/xkYh5DgL5o3PhrQNXqOijk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxEG3a/dJMcadhtveg/xkYh5DgL5o3PhrQNXqOijk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxEG3a/dJMcadhtveg/xkYh5DgL5o3PhrQNXqOijk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxEG3a%2FdJMcadhtveg%2FxkYh5DgL5o3PhrQNXqOijk%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;983&quot; height=&quot;480&quot; data-origin-width=&quot;983&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gather 실행 후 생성되는 파일들&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Content/Localization/Game/
├── Game.manifest         &amp;larr; 원본 텍스트 마스터 (자동 생성)
├── Game.csv              &amp;larr; 전체 텍스트 요약 (참고용)
├── Game_Conflicts.txt    &amp;larr; Key 충돌 보고서
├── en/Game.archive       &amp;larr; 영어 번역 데이터 (현재 비어있음)
└── ko/Game.archive       &amp;larr; 한국어 번역 데이터&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 3 : Export &amp;rarr; 번역 &amp;rarr; Import&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역가에게 전달할 .po 파일을 추출한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Localization Dashboard
└── Cultures 섹션
    └── 각 언어별 Export Translations 클릭
        └── ko/Game.po 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 .po 파일 내용&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;msgctxt &quot;Game,GearLevel&quot;
msgid &quot;Lv. {0}&quot;
msgstr &quot;&quot;           &amp;larr; 여기에 번역 입력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역 작업은 &lt;b&gt;PoEdit&lt;/b&gt; 같은 외부 툴을 사용한다. (PoEdit은 언리얼이 제공하는 게 아니라 .po가 GNU gettext 표준 포맷이라 호환되는 것이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역 완료 후&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;msgctxt &quot;Game,GearLevel&quot;
msgid &quot;Lv. {0}&quot;
msgstr &quot;Lv. {0}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역된 .po를 Localization Dashboard에서 Import하면 archive 파일에 반영된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 4 : Compile (.locres 생성)&lt;/h3&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Localization Dashboard &amp;rarr; Compile Text 클릭&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과물&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Content/Localization/Game/
├── ko/Game.locres   &amp;larr; 게임 런타임에 실제로 로드되는 파일
├── en/Game.locres
└── ja/Game.locres&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 5 : 게임에서 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PIE 실행 후 콘솔(`)에서&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;culture=en   &amp;rarr; 영어로 즉시 전환
culture=ko   &amp;rarr; 한국어로 즉시 전환&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. UMG에서 번역 대상 제외하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UMG 에디터에서 레이아웃 미리보기용으로 임시 텍스트(&lt;code&gt;100%&lt;/code&gt;, &lt;code&gt;999/999&lt;/code&gt; 같은 값)를 입력하는 경우가 많다. 이런 텍스트들은 번역 대상에서 제외해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Is Localized 체크 해제&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;TextBlock 선택
└── Details 패널
    └── Content 섹션
        └── Text 우측의 ▼ 화살표
            └── ☐ Is Localized   &amp;larr; 체크 해제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;효과&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에디터/디자인 타임에는 그대로 표시 (레이아웃 확인 OK)&lt;/li&gt;
&lt;li&gt;Gather 시 수집 대상에서 자동 제외&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동적 값은 코드에서 SetText&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 동적 값은 FText::FromString으로 처리
HealthText-&amp;gt;SetText(
    FText::FromString(FString::Printf(TEXT(&quot;%d%%&quot;), Percent))
);

// FText::FromString은 런타임 생성이라 Gather 대상이 아님&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 라이브 서비스를 위한 Target 분리 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 라이브 서비스에서는 &lt;b&gt;앱 본체는 가볍게, 본문 텍스트는 다운로드 후 로드&lt;/b&gt; 패턴이 유리하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Target을 2개로 분리&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;GamePatch.locres  (앱에 항상 포함, 가벼움)
├── 패치 안내 문구
├── 다운로드 진행 텍스트
├── 네트워크 에러 메시지
└── 점검 안내

Game.locres  (리소스 다운로드 후 사용, 큼)
├── UI 텍스트
├── 퀘스트
├── 아이템 설명
└── 스토리&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의할 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드 이전 단계에서 보이는 모든 텍스트는 반드시 GamePatch에 포함해야 한다. Game.locres가 다운로드되기 전이라 표시할 수 없기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;런타임 동적 로드&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;// 다운로드 완료 후 호출
FTextLocalizationManager::Get().LoadLocalizationResourcesForCulture(
    TEXT(&quot;Game&quot;),
    FInternationalization::Get().GetCurrentCulture()-&amp;gt;GetName(),
    ELocalizationLoadFlags::Game
);&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Perforce / Git 형상 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;항상 등록해야 할 파일&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Config/Localization/Game_*.ini       &amp;larr; Target 설정 파일 (필수)
DefaultEditor.ini, DefaultEngine.ini &amp;larr; Target 추가 시 자동 수정됨

Content/Localization/Game/
├── Game.manifest         &amp;larr; 원본 마스터
├── Game.csv              &amp;larr; 참고용
├── Game_Conflicts.txt    &amp;larr; 충돌 추적
├── en/Game.archive       &amp;larr; 영어 번역
├── ko/Game.archive       &amp;larr; 한국어 번역
└── *.po                  &amp;larr; 번역 작업 파일&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.locres 등록 여부는 팀 정책&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.locres 등록&lt;/td&gt;
&lt;td&gt;빌드 시 즉시 사용&lt;/td&gt;
&lt;td&gt;저장소 용량 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.locres 미등록 (CI 자동 생성)&lt;/td&gt;
&lt;td&gt;저장소 깔끔&lt;/td&gt;
&lt;td&gt;빌드 파이프라인 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소규모 팀은 등록, 빌드 자동화가 갖춰진 팀은 미등록을 권장한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 자주 하는 실수와 해결법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Gather가 동작하지 않을 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔한 원인은 &lt;b&gt;Search Directories 미설정&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Localization Dashboard
└── Target : Game
    └── Gather Text 섹션
        └── Gather from Text Files
            └── Search Directories : Source/Game/ 추가  &amp;larr; 90% 이 문제&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Native Culture 미설정&lt;/h3&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;Cultures 섹션
└── Native Culture : ko (또는 en) 반드시 설정&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 내장 현지화 시스템의 핵심 흐름을 다시 한 번 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;[코드 작성] LOCTEXT(&quot;Key&quot;, &quot;원본 텍스트&quot;)
        &amp;darr;
[Gather]   Localization Dashboard에서 수집 &amp;rarr; .manifest
        &amp;darr;
[Export]   번역가에게 전달할 .po 파일 추출
        &amp;darr;
[번역]     PoEdit 등 외부 툴로 번역 작업
        &amp;darr;
[Import]   번역된 .po를 다시 가져오기 &amp;rarr; .archive
        &amp;darr;
[Compile]  .archive &amp;rarr; .locres 바이너리 생성
        &amp;darr;
[런타임]   FInternationalization이 .locres 로드 &amp;rarr; 자동 교체&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단계가 많아 보이지만, 한 번 파이프라인을 구축해두면 이후 텍스트 추가는 매우 빠르게 진행된다. 무엇보다 &lt;b&gt;한국어 조사 자동 처리&lt;/b&gt; 하나만으로도 자체 구현 대비 엄청난 시간을 절약할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 현지화를 시작하시는 분들께 이 글이 도움이 되었으면 한다.&lt;/p&gt;</description>
      <category>⭐ Game Programming/Unreal Programming</category>
      <category>localization</category>
      <category>UE5</category>
      <category>현지화</category>
      <author>테디슨</author>
      <guid isPermaLink="true">https://wecandev.tistory.com/231</guid>
      <comments>https://wecandev.tistory.com/231#entry231comment</comments>
      <pubDate>Fri, 24 Apr 2026 11:55:51 +0900</pubDate>
    </item>
    <item>
      <title>[회고] 주부의 삶 회고</title>
      <link>https://wecandev.tistory.com/230</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;육아 휴직 기록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20240426_110253401.jpg&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5x42M/btsGXlhuRnK/WMo6Au6aUlAFu96arbEp70/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5x42M/btsGXlhuRnK/WMo6Au6aUlAFu96arbEp70/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5x42M/btsGXlhuRnK/WMo6Au6aUlAFu96arbEp70/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5x42M%2FbtsGXlhuRnK%2FWMo6Au6aUlAFu96arbEp70%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;298&quot; height=&quot;397&quot; data-filename=&quot;KakaoTalk_20240426_110253401.jpg&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;2048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이가 태어나기 전 우리 부부의 퇴근 후 일상은 이랬다. 저녁 간단히 해결 후 밤 9시까지 적당히 공부하고 그날 공부한 내용 정리 후 놀다가 취침. 이걸 매일 반복. 왜냐면 습관이라는 건 하루 미루면 무뎌지니까...&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;그리고 출산 한지 약 10개월이 지났다. 육아 전만 해도 애는 혼자서 놀고 나는 책이나 보면서 자기개발 할 수 있을 줄 알았는데 개뿔... 밤 9시가 되면 겨우 육퇴하고 그때부터 밀린 집안일 시작. 나도 씻고 정신 좀 차리면 11시. 아 이제 자야지. 새벽에 이앓이라도 하는 날에는 너도 나도 초주검.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어린이집에 가면 이제 내 공부 다시 하겠구나 싶었는데. 약 두 달 동안 겨우 반정도 등원한 듯하다. 무슨 감기가 이렇게 오래가는지 당최 이해할 수 없다. 귀여운 건 귀여운 거고 아이란 정말 내 시간, 내 공간, 내 체력을 잡아먹는 괴... 아니 귀여운 생명체다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 이 글은 복직 전 다시 내 뇌에 시동을 걸기 위한 다짐 같은 글!!! 5월에는 반드시 하루에 조금씩 뇌에 기름칠하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 출산 직전까지 공부하던 쿠버네티스 자격증은 떨어졌다. 나는 바보니까 더 열심히 공부해야 해.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 5월의 목표 - 일상&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 가계부 정리&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 옵시디언 사용법 정리 후 새로 세팅&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 5월의 목표 - 자기개발&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 커리어 스킬 책 완독&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 말해보카 재신청&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 유데미 웹 강의 하나 들어보기&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  끄적끄적</category>
      <author>최리</author>
      <guid isPermaLink="true">https://wecandev.tistory.com/230</guid>
      <comments>https://wecandev.tistory.com/230#entry230comment</comments>
      <pubDate>Fri, 26 Apr 2024 11:04:11 +0900</pubDate>
    </item>
    <item>
      <title>[언리얼엔진] 입력 개요</title>
      <link>https://wecandev.tistory.com/229</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어 입력(PlayerInput) 오브젝트는 플레이어의 입력을 플레이어 컨트롤러 또는 폰과 같은 액터가 이해하고 사용할 수 있는 데이터로 변환하는 기능을 한다. 이는 플레이어의 하드웨어 입력을 플레이어 입력 매핑 및 입력 컴포넌트를 통한 움직임과 게임 이벤트로 변환하는 입력 처리 흐름의 일부다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하드웨어 입력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어의 하드웨어 입력은 일반적으로 키 누름, 마우스 클릭 또는 마우스 움직임, 컨트롤러 버튼 누름 또는 조이스틱 움직임 등이 있다. 표준 축 또는 버튼 인덱스를 따르지 않거나 특이한 입력 범위를 갖는 특수 입력 디바이스는 RawInput 플러그인을 사용하여 수동으로 설정할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;플레이어 입력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlayerInput은 플레이어 입력을 관리하는 플레이어 컨트롤러 클래스 내의 UObject다. 클라이언트 측에서만 스폰된다. 두 구조체가 PlayerInput 내에서 정의된다. FInputActionKeyMapping 은 액션 매핑을 정의하고, FInputAxisKeyMapping 은 축 매핑을 정의한다. 액션 매핑과 축 매핑에 사용되는 하드웨어 입력 정의는 InputCoreTypes에서 확립한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;액션 매핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개별 버튼 또는 키 입력을 이후 이벤트 주도 행동에 바인딩될 '친근한 이름'으로 매핑한다. 최종 이펙트는 키, 마우스 버튼, 키패드 버튼을 누르거나 놓는 입력으로 일부 게임 행동을 직접 트리거한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;축 매핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키보드, 컨트롤러, 마우스 입력을 이후 이동과 같은 지속적인 게임 행동에 바인딩될 '친근한 이름'으로 매핑한다. 축 매핑에 매핑된 입력은 현재 입력값을 0이라고 보고해도 지속적으로 조사된다. 액션 매핑 입력으로 트리거되는 이산적 게임 이벤트는 분리된 반면에와 달리 축 매핑은 게임 행동 사이의 부드러운 전환을 가능하게 한다. 트랜지션이 가능하다. 예를 들어 플레이어 이동 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 조이스틱과 같은 하드웨어 축은 1(눌림) 또는 0(눌리지 않음)과 같이 분리된 입력이 아니라 입력의 강도를 제공한다. 축은 살짝 움직일 수도 있고 크게 움직일 수도 있으며, 그에 따라 캐릭터의 움직임이 달라질 수 있습니다. 이러한 입력 방법은 이동 입력에 스케일 조절 가능한 강도를 제공한다는 점이다. WASD 또는 화살표 같은 일반 이동 키 또한 축 매핑으로 지속적으로 조사되는 게임 행동에 매핑할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력 매핑 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 매핑은 환경설정 파일에 저장되며, 프로젝트 세팅의 입력 섹션에서 편집 가능하다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;레벨 에디터에서 편집 &amp;gt; 프로젝트 세팅을 선택&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D13Rr/btsiacwvq0I/fUfo9k3uIVJDdQMStDN8vK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D13Rr/btsiacwvq0I/fUfo9k3uIVJDdQMStDN8vK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D13Rr/btsiacwvq0I/fUfo9k3uIVJDdQMStDN8vK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD13Rr%2Fbtsiacwvq0I%2FfUfo9k3uIVJDdQMStDN8vK%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;490&quot; height=&quot;305&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로젝트 세팅 탭에서 입력을 클릭&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dPBfjp/btsiaBpgPNf/FEiYKQeRH3Q7A7jZWMofC0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dPBfjp/btsiaBpgPNf/FEiYKQeRH3Q7A7jZWMofC0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dPBfjp/btsiaBpgPNf/FEiYKQeRH3Q7A7jZWMofC0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdPBfjp%2FbtsiaBpgPNf%2FFEiYKQeRH3Q7A7jZWMofC0%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;1102&quot; height=&quot;666&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;축 입력의 프로퍼티 변경&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biX9ry/btsidXqPf7a/NElkCUjjXaYhfhfHeQQflK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biX9ry/btsidXqPf7a/NElkCUjjXaYhfhfHeQQflK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biX9ry/btsidXqPf7a/NElkCUjjXaYhfhfHeQQflK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiX9ry%2FbtsidXqPf7a%2FNElkCUjjXaYhfhfHeQQflK%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;1102&quot; height=&quot;666&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액션 매핑 편집&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q2v5M/btsiahK6bnO/ZSgMuNVJeqkLdsXZtc1JVk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q2v5M/btsiahK6bnO/ZSgMuNVJeqkLdsXZtc1JVk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q2v5M/btsiahK6bnO/ZSgMuNVJeqkLdsXZtc1JVk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq2v5M%2FbtsiahK6bnO%2FZSgMuNVJeqkLdsXZtc1JVk%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;1102&quot; height=&quot;666&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;축 매핑 편집&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYoT74/btsicjupo9p/FHsBClimkXvJjoomoY2DPK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYoT74/btsicjupo9p/FHsBClimkXvJjoomoY2DPK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYoT74/btsicjupo9p/FHsBClimkXvJjoomoY2DPK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYoT74%2Fbtsicjupo9p%2FFHsBClimkXvJjoomoY2DPK%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;1102&quot; height=&quot;666&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력 컴포넌트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 컴포넌트(InputComponent)는 보통 폰과 컨트롤러에 있지만 필요한 경우 다른 액터나 레벨 스크립트에서도 설정할 수 있다. 입력 컴포넌트는 프로젝트의 축 매핑과 액션 매핑을 C++ 코드 또는 블루프린트 그래프로 게임 액션에 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 컴포넌트의 입력 처리 우선순위 스택은 다음과 같은 우선순위를 따른다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;최소로 최근에 사용한거부터 최대로 최근에 사용한것 까지 &quot;입력 허용&quot;이 활성화된 모든 액터&lt;/li&gt;
&lt;li&gt;컨트롤러&lt;/li&gt;
&lt;li&gt;레벨 스크립트.&lt;/li&gt;
&lt;li&gt;폰&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력을 받은 입력 컴포넌트는 더 이상 스택 아래로 내려갈 수 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력 프로세싱 절차&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEq5ul/btsiaY5xX3R/0TKWGjCg26fY4XKJyIK9k1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEq5ul/btsiaY5xX3R/0TKWGjCg26fY4XKJyIK9k1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEq5ul/btsiaY5xX3R/0TKWGjCg26fY4XKJyIK9k1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEq5ul%2FbtsiaY5xX3R%2F0TKWGjCg26fY4XKJyIK9k1%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;500&quot; height=&quot;499&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;499&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 - 앞으로 이동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1인칭 템플릿에서 가져옴&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;플레이어의 하드웨어 입력 : 플레이어가 w를 누른다.&lt;/li&gt;
&lt;li&gt;플레이어 입력 매핑 : 축 매핑이 w를 스케일이 1인 'MoveForward'로 변환&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1joQ9/btsibeNQMAK/uOpmusC8vVk4nSOST1CL71/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1joQ9/btsibeNQMAK/uOpmusC8vVk4nSOST1CL71/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1joQ9/btsibeNQMAK/uOpmusC8vVk4nSOST1CL71/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1joQ9%2FbtsibeNQMAK%2FuOpmusC8vVk4nSOST1CL71%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;1102&quot; height=&quot;666&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;입력 컴포넌트 우선순위 스택 : 입력 컴포넌트 우선순위 스택을 거칠 때 'MoveForward' 입력의 첫 바인딩은 AFirstPersonBaseCodeCharacter 클래스다. 현재 플레이어의 폰으로, 입력 컴포넌트가 마지막으로 체크된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;    void AFirstPersonBaseCodeCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent)
        {
            // 게임플레이 키 바인딩 설정
            check(InputComponent);
            ...
            InputComponent-&amp;gt;BindAxis(&quot;MoveForward&quot;, this, &amp;amp;AFirstPersonBaseCodeCharacter::MoveForward);
            ...
        } &lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;4&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;게임로직 : AFirstPersonBaseCodeCharacter의 MoveForward 함수가 실행.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;    void AFirstPersonBaseCodeCharacter::MoveForward(float Value)
        {
            if ( (Controller != NULL) &amp;amp;&amp;amp; (Value != 0.0f) )
            {
                // 앞쪽 찾기
                FRotator Rotation = Controller-&amp;gt;GetControlRotation();
                // 걷거나 떨어질 때 피치 제한
                if ( CharacterMovement-&amp;gt;IsMovingOnGround() || CharacterMovement-&amp;gt;IsFalling() )
                {
                    Rotation.Pitch = 0.0f;
                }
                // 해당 방향으로 이동 추가
                const FVector Direction = FRotationMatrix(Rotation).GetScaledAxis(EAxis::X);
                AddMovementInput(Direction, Value);
            }
        }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;터치 인터페이스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 터치 디바이스에서 실행되는 게임에는 콘솔 컨트롤러처럼 두 가지 가상 조이스틱이 있다. 프로젝트 세팅(Project Settings) 의 입력(Input) 섹션에 있는 기본 터치 인터페이스(Default Touch Interface) 프로퍼티로 이를 변경할 수 있다. 이는 터치 인터페이스 구성 에셋을 가리킨다. 디폴트는 DefaultVirtualJoysticks이며 공유 엔진 콘텐츠에 있다. 카메라를 돌릴 필요가 없는 게임을 위해 왼쪽 스틱만 있는 버전인 LeftVirtualJoystickOnly 도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TgAnk/btsibdg6CD0/gKqNRVvXRKrbKo48gV4hQ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TgAnk/btsibdg6CD0/gKqNRVvXRKrbKo48gV4hQ0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TgAnk/btsibdg6CD0/gKqNRVvXRKrbKo48gV4hQ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTgAnk%2Fbtsibdg6CD0%2FgKqNRVvXRKrbKo48gV4hQ0%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;1102&quot; height=&quot;666&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오브젝트 선택 툴 뷰 옵션(View Options) 세팅에서 엔진 콘텐츠 표시(Show Engine Content) 체크박스를 체크해야 하는 점을 유의하자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;230&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIErEG/btsibpBPtS2/h0KN63d7SuLxxbnHFbiHs1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIErEG/btsibpBPtS2/h0KN63d7SuLxxbnHFbiHs1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIErEG/btsibpBPtS2/h0KN63d7SuLxxbnHFbiHs1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIErEG%2FbtsibpBPtS2%2Fh0KN63d7SuLxxbnHFbiHs1%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;230&quot; height=&quot;310&quot; data-origin-width=&quot;230&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 조이스틱이 필요하지 않은 경우 디폴트 터치 인터페이스 프로퍼티만 지운다. 또한 항상 터치 인터페이스 표시(Always Show Touch Interface)에 체크하거나 PC 게임을 -faketouches로 실행하여 플랫폼과 무관하게 게임에 터치 인터페이스를 강제할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;향상된 입력 플러그인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험단계 기능인 향상된 입력 플러그인은 복잡한 입력처리나 런타임 제어 리매핑 등의 고급 입력 기능이 필요한 프로젝트에서 개발자가 엔진의 디폴트 입력 시스템을 쉽게 업그레이드하고 하위 호환도 확보하도록 지원한다. 이 플러그인은 방사상 데드존, 조화된 액션, 컨텍스트 입력 및 우선순위, 원본 입력 데이터의 필터링 및 프로세싱 확장 기능 등을 에셋 기반 환경에서 제공한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인을 활성화 하기위해 에디터의 편집(Edit) 드롭다운 메뉴를 열고 플러그인(Plugin) 을 선택한다. 플러그인 목록의 입력(Input) 섹션에서 향상된 입력 플러그인을 활성화하고 에디터를 재시작한다.&lt;/p&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;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EtYjh/btsibv2Upk8/wmjgjW0leIaJTra56WzxA0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EtYjh/btsibv2Upk8/wmjgjW0leIaJTra56WzxA0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EtYjh/btsibv2Upk8/wmjgjW0leIaJTra56WzxA0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEtYjh%2Fbtsibv2Upk8%2FwmjgjW0leIaJTra56WzxA0%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;296&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에디터가 재시작되면 프로젝트가 기본 언리얼 엔진 입력 핸들러 대신 향상된 입력 플러그인 클래스를 사용하도록 설정할 수 있다. 편집(Edit) 드롭다운 메뉴에서 프로젝트 세팅(Project Settings) 을 선택한다. 엔진(Engine) 소제목 아래의 입력(Input) 섹션에서 Default Classes 세팅을 찾는다. 초기에는 표준 PlayerInput 및 InputComponent 클래스가 포함되어 있다.&lt;/p&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;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ky7fY/btsiaCVObRb/GFRkiU2FuqQs1eK9jvjknk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ky7fY/btsiaCVObRb/GFRkiU2FuqQs1eK9jvjknk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ky7fY/btsiaCVObRb/GFRkiU2FuqQs1eK9jvjknk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fky7fY%2FbtsiaCVObRb%2FGFRkiU2FuqQs1eK9jvjknk%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;293&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향상된 입력을 사용하려면 Dafulat Player Input Class를 EnhancedPlayerInput 으로 Default Input Component Class를 EnhancedInputComponent 로 변경한다&lt;/p&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;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqekEU/btsiadWu8ju/IVNn0Y2iAo1d0NK4PXXrw0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqekEU/btsiadWu8ju/IVNn0Y2iAo1d0NK4PXXrw0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqekEU/btsiadWu8ju/IVNn0Y2iAo1d0NK4PXXrw0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqekEU%2FbtsiadWu8ju%2FIVNn0Y2iAo1d0NK4PXXrw0%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;293&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향상된 입력 시스템에는 다음 4가지 주요 개념이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력 액션은 향상된 입력 시스템과 프로젝트 코드 사이의 커뮤니케이션 링크이다. 입력 액션은 점프, 문 열기 등 인터랙티브 캐릭터가 할 수 있는 모든 것을 아우르며, 버튼을 길게 눌러서 걷기를 달리기로 바꾸는 등 사용자 입력 상태를 나타내는 데도 사용될 수 있다. 입력 액션은 원본 입력과 구분된다. 입력 액션은 어떤 입력에 의해 트리거됐는지와 무관하게 현재 상태를 알고 있으며 입력 값을 최대 3개의 독립 부동 소수점 축에서 보고할 수 있다. 예를 들어 '아이템 획득' 액션은 사용자가 뭔가를 획득했는지 여부를 나타내는 켜기/끄기 상태만을 필요로 하는 반면 '걷기' 액션은 사용자가 걷는 방향과 속도를 나타내는 두 개의 축을 필요로 한다.&lt;/li&gt;
&lt;li&gt;입력 매핑 컨텍스트(Input Mapping Context)는 사용자의 입력을 액션으로 매핑하며 각 사용자에 대해 동적으로 추가, 제거, 우선순위 지정될 수 있다. 향상된 입력의 로컬 플레이어 서브시스템을 통해 로컬 플레이어에게 하나 이상의 컨텍스트를 적용할 수 있으며, 동일한 입력을 소화하려는 여러 액션 사이의 충돌을 해소하도록 우선순위를 지정할 수 있다. 이에 대한 일반적인 예시는 캐릭터가 월드에서 걷는 동안 문을 여는 버튼, 또는 캐릭터의 가방을 보는 동안 아이템을 선택하는 버튼 등이다. 캐릭터가 가방을 열 때마다 '아이템 선택' 입력 매핑 컨텍스트를 추가하여 '문 열기' 컨텍스트보다 높은 우선순위를 부여하고, 캐릭터가 가방을 닫으면 '아이템 선택' 컨텍스트를 제거하는 것이다. 이를 통해 입력 처리 레벨에서 문과 가방 시스템을 인지하도록 코드를 프로그래밍하지 않아도 사용자의 입력을 캐릭터의 상황에 맞춰 올바르게 해석할 수 있습니다.&lt;/li&gt;
&lt;li&gt;모디파이어(Modifiers)는 사용자 디바이스의 원본 입력 값을 조절한다. 입력 매핑 컨텍스트는 각 입력 액션의 원시 입력(raw input)과 관련된 모디파이어를 얼마든지 가질 수 있다. 일반적인 모디파이어로는 데드존, 다수 프레임에서 입력 스무딩, 입력 벡터를 로컬에서 월드 스페이스로 변환, 기타 플러그인에 포함되는 것들이 있다. 개발자는 자신만의 모디파이어를 생성할 수도 있다.&lt;/li&gt;
&lt;li&gt;트리거는 모디파이어 이후 입력 값이나 다른 입력 액션의 출력 규모를 사용하여 입력 액션을 활성화할지 여부를 결정한다. 입력 매핑 컨텍스트 내의 입력 액션은 각 입력에 대해 하나 이상의 트리거를 가질 수 있다. 예를 들어 사진을 찍으려면 사용자는 왼쪽 마우스 버튼을 4분의 1초 동안 눌러야 하는데, 이때 카메라를 조준하는 별도의 입력 액션 또한 활성화되어 있는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력 액션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 액션은 시스템과 프로젝트 코드 사이의 연결 고리이다. 컨텍스트 브라우저(Context Browser) 에서 우클릭하고 입력(Input) 옵션을 펼친 다음 입력 액션(Input Action) 을 선택하여 입력 액션을 생성할 수 있다. 입력 액션을 트리거하려면 입력 매핑 컨텍스트에 포함시키고 해당 입력 매핑 컨텍스트를 로컬 플레이어의 향상된 입력 로컬 플레이어 서브시스템(Enhanced Input Local Player Subsystem) 에 추가해야 한다.&lt;/p&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;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m2DUa/btsield7TmC/YSRIf2UFI6npPZNf1HVXXK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m2DUa/btsield7TmC/YSRIf2UFI6npPZNf1HVXXK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m2DUa/btsield7TmC/YSRIf2UFI6npPZNf1HVXXK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm2DUa%2Fbtsield7TmC%2FYSRIf2UFI6npPZNf1HVXXK%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;993&quot; height=&quot;982&quot; data-origin-width=&quot;993&quot; data-origin-height=&quot;982&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폰 클래스가 트리거된 입력 액션에 반응하게 하려면 SetupPlayerInputComponent에서 해당 폰 클래스를 적절한 유형의 트리거 이벤트(Trigger Event) 에 바인딩해야 한다. 다음은 SetupPlayerInputComponent를 오버라이드하여 입력 액션인 MyInputAction과 MyOtherInputAction을 핸들러 함수에 바인딩하는 샘플 코드다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt; // UEnhancedInputComponent를 사용하는지 확인해야 한다. 그러지 않으면 프로젝트가 제대로 설정되지 않음.
 if (UEnhancedInputComponent* PlayerEnhancedInputComponent = Cast&amp;lt;UEnhancedInputComponent&amp;gt;(PlayerInputComponent))
 {
    // UInputAction*을 핸들러 함수 및 관심 대상인 각종 ETriggerEvent에 바인딩하는 방법은 여러 가지.

    // 액션 버튼을 누를 때와 같이 MyInputAction이 시작될 때 틱에서 핸들러 함수를 호출.
    if (MyInputAction)
    {
        PlayerEnhancedInputComponent-&amp;gt;BindAction(MyInputAction, ETriggerEvent::Started, this, &amp;amp;AMyPawn::MyInputHandlerFunction);
    }

    // 이동 키를 길게 누를 때 등 입력 조건이 충족되면 모든 틱에서 이름으로 핸들러 함수(UFUNCTION)를 호출.
    if (MyOtherInputAction)
    {
        PlayerEnhancedInputComponent-&amp;gt;BindAction(MyOtherInputAction, ETriggerEvent::Triggered, this, TEXT(&quot;MyOtherInputHandlerFunction&quot;));
    }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 액션에는 블루프린트 스크립팅에 노출되는 내장 이벤트가 있다. C++ 개발자는 블루프린트 스크립팅 개발자에게 입력 액션 이벤트 액세스를 제공하려고 패스스루 함수를 만들 필요가 없다. 일반적인 핸들러 함수는 다음과 같은 양식을 취할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt; void AMyPawn::MyFirstAction(const FInputActionValue&amp;amp; Value)
 {
    // 핸들러 함수가 실행되고 있음을 확인해 주는 디버그 로그 출력.
    UE_LOG(LogTemp, Warning, TEXT(&quot;%s called with Input Action Value %s (magnitude %f)&quot;), TEXT(__FUNCTION__), *Value.ToString(), Value.GetMagnitude());
    // GetType() 함수를 사용하여 값의 유형을 결정하고, [] 연산자를 0~2 사이의 인덱스와 함께 사용하여 데이터에 액세스.
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 사용 사례에서 void (const FInputActionValue&amp;amp;) 시그니처를 사용하는 것이 권장된다. 입력 액션을 핸들러 함수와 바인딩하면 폰이 트리거된 특정 방식에 따라 반응하게 만들 수 있다. 가장 일반적인 트리거 유형은 버튼을 누른 즉시 한 번 발생하는 Started, 입력을 누르고 있는 동안 매 프레임마다 지속적으로 액션이 발생하는 Triggered 등이 있으며, 전체 목록은 Plugins/EnhancedInput/ETriggerEventETriggerEvent의 API 레퍼런스 페이지에서 볼 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력 매핑 컨텍스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나 이상의 입력 액션을 트리거하는 규칙을 입력 매핑 컨텍스트(Input Mapping Context)라고 한다. 최상위 레벨의 입력 액션 목록을 갖춘 계층구조를 기본구조로 한다. 입력 액션 레벨 아래에는 키, 버튼, 움직임 축 등 각 입력 액션을 트리거할 수 있는 사용자 입력 목록이 있다. 하단 레벨에는 각 사용자 입력에 대한 입력 트리거와 입력 모디파이어 목록이 있다. 여기서는 입력의 원본 값을 어떻게 필터링하고 프로세스할지, 어떤 제한을 충족해야 계층구조 상위의 입력 액션을 주도할 수 있는지 결정한다. 입력은 다수의 입력 모디파이어와 입력 트리거를 가질 수 있다. 각 단계의 출력을 다음 단계의 입력으로 사용하여 순차적으로 평가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 매핑 컨텍스트를 생성하려면 컨텍스트 브라우저(Context Browser) 에서 우클릭하고 입력(Input) 옵션을 펼친 다음 입력 매핑 컨텍스트(Input Mapping Context) 를 선택한다.&lt;/p&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;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qiv2Z/btsidmxx2Eo/qBYhH8sPP4GL3cvoxiaC5k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qiv2Z/btsidmxx2Eo/qBYhH8sPP4GL3cvoxiaC5k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qiv2Z/btsidmxx2Eo/qBYhH8sPP4GL3cvoxiaC5k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqiv2Z%2Fbtsidmxx2Eo%2FqBYhH8sPP4GL3cvoxiaC5k%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;993&quot; height=&quot;982&quot; data-origin-width=&quot;993&quot; data-origin-height=&quot;982&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 매핑 컨텍스트에 연관성 있는 입력 액션을 모두 채우자. 간단한 프로젝트의 경우 입력 매핑 컨텍스트 하나에 모든 입력 액션을 넣는 것이 가능할 수도 있다. 보다 복잡한 프로젝트는 다수의 입력 매핑 컨텍스트가 있어야 더 잘 작동한다. 로컬 플레이어가 한 번에 하나 이상의 활성 입력 매핑 컨텍스트를 가질 수 있기 때문. 또한 상호 배타적인 입력 매핑 컨텍스트를 통해 서로 다른 입력 액션에 사용되는 사용자 입력 간의 입력 콜리전을 피할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciD5MP/btsh6zZW98a/Ubi8O78R3qoPtdQ9aT5Ewk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciD5MP/btsh6zZW98a/Ubi8O78R3qoPtdQ9aT5Ewk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciD5MP/btsh6zZW98a/Ubi8O78R3qoPtdQ9aT5Ewk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciD5MP%2Fbtsh6zZW98a%2FUbi8O78R3qoPtdQ9aT5Ewk%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;774&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 입력 매핑 컨텍스트는 달리기를 위한 입력 액션이다. 게임패드의 왼쪽 섬스틱 기울이기 등 다수의 입력을 통해 실행되며, 두 축을 하나의 입력으로 결합한다. 위 입력의 원본 값은 '데드존' 입력 모디파이어를 지나며, 결과 값은 '홀드' 입력 트리거로 전달되어 'RunAction' 입력 액션을 주도한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8LWXa/btsidmElOAA/Kt7aXgqStBuTBWT46VZghK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8LWXa/btsidmElOAA/Kt7aXgqStBuTBWT46VZghK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8LWXa/btsidmElOAA/Kt7aXgqStBuTBWT46VZghK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8LWXa%2FbtsidmElOAA%2FKt7aXgqStBuTBWT46VZghK%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;1042&quot; height=&quot;360&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드롭다운 목록에서 다수의 입력 바인딩을 이용할 수 있다. 입력 바인딩을 더 빠르게 선택하려면 드롭다운 왼쪽의 작은 버튼을 누른 다음 바인딩하고 싶은 키나 버튼을 누르면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJPtbg/btsibvIA124/ZmYLbf0wHfkk3SykVOCZO1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJPtbg/btsibvIA124/ZmYLbf0wHfkk3SykVOCZO1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJPtbg/btsibvIA124/ZmYLbf0wHfkk3SykVOCZO1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJPtbg%2FbtsibvIA124%2FZmYLbf0wHfkk3SykVOCZO1%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;1015&quot; height=&quot;1144&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;1144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 입력 매핑 컨텍스트는 달리기와 점프를 위한 입력 액션을 지원한다. 입력 매핑 컨텍스트를 채우면 폰의 플레이어 컨트롤러와 연결된 로컬 플레이어에게 이를 추가할 수 있다. 이를 위해 PawnClientRestart 함수를 오버라이드하고 다음과 같은 코드 블록을 추가하였다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt; // 유효한 플레이어 컨트롤러가 있는지 확.
 if (APlayerController* PC = Cast&amp;lt;APlayerController&amp;gt;(GetController()))
 {
    // 향상된 입력 로컬 플레이어 서브시스템을 플레이어 컨트롤러와 관련된 로컬 플레이어로부터 가져옴
    if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem&amp;lt;UEnhancedInputLocalPlayerSubsystem&amp;gt;(PC-&amp;gt;GetLocalPlayer()))
    {
        // PawnClientRestart가 액터의 수명에서 2회 이상 실행될 수 있으므로 잔여 매핑을 우선 제거
        Subsystem-&amp;gt;ClearAllMappings();

        // 각 매핑 컨텍스트를 우선순위 값과 함께 추가. 높은 값이 낮은 값에 우선
        Subsystem-&amp;gt;AddMappingContext(MyInputMappingContext, MyInt32Priority);
    }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 매핑 컨텍스트를 추가하면 폰이 SetupPlayerInputComponent에 바인딩했거나 블루프린트 스크립팅 사용자가 구성한 입력 액션 이벤트에 반응할 수 있게 된다. 게임플레이 도중 이용 가능한 입력 매핑 컨텍스트를 변경하는 이벤트가 발생하면 ClearAllMappings, AddMappingContext, RemoveMappingContext를 사용하여 이용 가능한 명령 세트를 동적으로 업데이트할 수 있다. 자세한 정보는 Plugins/EnhancedInput/IEnhancedInputSubsystemInterfaceIEnhancedInputSubsystemInterface API 레퍼런스 페이지를 참조하자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력 모디파이어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 모디파이어(Input Modifier)는 언리얼 엔진이 수신한 원본 입력을 입력 트리거로 전달하기 전에 변경하는 프리 프로세서이다. 모디파이어들은 축 순서 변경, '데드존' 구현, 축 입력을 월드 스페이스로 변환 등 다양한 작업을 수행한다. 입력 매핑 컨텍스트 내의 입력 액션과 연결된 각 입력은 사용자가 정의한 일련의 입력 모디파이어 시리즈를 통과한 이후에 해당 입력의 입력 트리거로 진행한다. 입력 모디파이어는 목록에 나열된 순서대로 적용되며 각 입력 모디파이어의 출력 값은 다음 입력 모디파이어의 입력 값이 된다. 향상된 입력 플러그인에 포함된 입력 모디파이어 목록 전체를 보려면 Plugins/EnhancedInput/UInputModifierUInputModifier API 레퍼런스 페이지를 참조하자. 아직 존재하지 않는 입력 모디파이어가 프로젝트에 필요하다면 UInputModifier 클래스를 직접 만들 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력 트리거&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 트리거(Input Trigger)는 사용자 입력이 입력 모디파이어 목록을 통과한 뒤 입력 매핑 컨텍스트 내에서 해당되는 입력 액션을 활성화할지 여부를 결정한다. 대다수 입력 트리거는 최소 활성화 값을 확인하고 짧은 탭, 길게 누르기, 일반적인 '누르기' 또는 '놓기' 이벤트와 같은 패턴을 검증하여 입력 자체를 분석한다. 'Chorded Action' 입력 트리거만 예외적으로 이 규칙에서 다른 입력 액션이 트리거되어야 한다. 기본으로 입력에 대한 사용자 활동은 모든 틱에서 트리거된다. 향상된 입력 플러그인에 포함된 입력 트리거 목록 전체를 보려면 Plugins/EnhancedInput/UInputTriggerUInputTrigger API 레퍼런스 페이지를 참조하자. 향상된 입력 플러그인에 존재하지 않는 입력 트리거가 프로젝트에 필요하다면 UInputTrigger 클래스를 직접 만들 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.unrealengine.com/5.2/ko/input-overview-in-unreal-engine/&quot;&gt;https://docs.unrealengine.com/5.2/ko/input-overview-in-unreal-engine/&lt;/a&gt;&lt;/p&gt;</description>
      <category>⭐ Game Programming/Unreal Document</category>
      <category>input</category>
      <category>언리얼엔진</category>
      <author>테디슨</author>
      <guid isPermaLink="true">https://wecandev.tistory.com/229</guid>
      <comments>https://wecandev.tistory.com/229#entry229comment</comments>
      <pubDate>Wed, 31 May 2023 22:34:22 +0900</pubDate>
    </item>
    <item>
      <title>[언리얼엔진] BindWidget</title>
      <link>https://wecandev.tistory.com/228</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 UI 로직을 만들려고 할 때 블루프린트에서 디자인한 위젯을 제어하려할때 BindWidget meta 지정자를 이용하여 제어할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BindWidget 예제&lt;/h2&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
class UTextBlock* TextName;

UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
class UImage* ImgIcon;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정작 UPROPERTY 공식문서에서는 없지만 UI개발시 가장 많이 사용하는 태그이다.&lt;br /&gt;블루프린트에 동일한 클래스와 동일한 이름을 가진 위젯이 있다면 해당 위젯포인터로 코드(C++)에서 런타임에 엑세스 할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;적용 방법&lt;/h2&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#pragma once 

#include &quot;CoreMinimal.h&quot;
#include &quot;Blueprint/UserWidget.h&quot;
#include &quot;MyBindWidget.generated.h&quot;

UCLASS(Abstract)
class UMyBindWidget : public UUserWidget
{
    GENERATED_BODY()

protected:
    UPROPERTY(BlueprintReadWrite, meta=(BindWidget))
    class UTextBlock* TextName;

    UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
    class UImage* ImgIcon;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 클래스를 추가하고 MyBindWidget 클래스를 상속받은 위젯블루프린트를 생성한다. 해당 블루프린트 안에 TextName 이름을 가진 TextBlock을 반드시 추가하도록 하자 없다면 컴파일에 실패하게 된다. 무조건 해당 위젯이 있어야 블루프린트 컴파일이 되므로 해당 포인터는 널이 아님을 보장하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/73IuR/btshlY43AD9/lxO5UZJNiAk3LfNGr6AkK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/73IuR/btshlY43AD9/lxO5UZJNiAk3LfNGr6AkK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/73IuR/btshlY43AD9/lxO5UZJNiAk3LfNGr6AkK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F73IuR%2FbtshlY43AD9%2FlxO5UZJNiAk3LfNGr6AkK1%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;651&quot; height=&quot;135&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;135&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BindWidgetOptional&lt;/h2&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;UPROPERTY(BlueprintReadWrite, meta = (BindWidgetOptional))
class UTextBlock* TextName;

UPROPERTY(BlueprintReadWrite, meta = (BindWidgetOptional))
class UImage* ImgIcon;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 위젯을 BindWidgetOptional 지정자로 입력하게 되면 블루프린트에서 해당 이름의 위젯이 없어도 컴파일 오류가 발생하지 않는다. 하지만 코드에서 사용할 때에는 항상 nullptr 이 아닌지 확인하고 사용하여야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BindWdigetAnim&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위젯애니메이션도 바인딩 가능하다. BindWidgetAnim / BindWidgetAnimOptional 로 사용가능하다&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;UPROPERTY(Transient, meta = (BindWidgetAnim))
UWidgetAnimation* Anim_Show;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 엔진 5.0 부터는 해당 프로퍼티를 Transient로 선언해야한다. Trasient로 하지 않으면 다음과 같이 컴파일 에러가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;689&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbsFyZ/btshiHXJiUo/aZt8jXqWptOrGJa8Q6y87k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbsFyZ/btshiHXJiUo/aZt8jXqWptOrGJa8Q6y87k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbsFyZ/btshiHXJiUo/aZt8jXqWptOrGJa8Q6y87k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbsFyZ%2FbtshiHXJiUo%2FaZt8jXqWptOrGJa8Q6y87k%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;689&quot; height=&quot;86&quot; data-origin-width=&quot;689&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드에서 블루프린트 위젯을 바인딩 하고자 할때 BindWidget/BindWidgetOptional 메타 지정자를 이용하자&lt;/li&gt;
&lt;li&gt;위젯 애니메이션을 바인딩할때는 BindWidgetAnim / BindWidgetAnimOptional 메타 지정자를 이용하자&lt;/li&gt;
&lt;/ul&gt;
&lt;div id=&quot;gtx-trans&quot; style=&quot;position: absolute; left: -39px; top: 69px;&quot;&gt;
&lt;div class=&quot;gtx-trans-icon&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>⭐ Game Programming/Unreal Programming</category>
      <category>UI</category>
      <category>언리얼엔진</category>
      <author>테디슨</author>
      <guid isPermaLink="true">https://wecandev.tistory.com/228</guid>
      <comments>https://wecandev.tistory.com/228#entry228comment</comments>
      <pubDate>Wed, 24 May 2023 22:15:48 +0900</pubDate>
    </item>
    <item>
      <title>[언리얼엔진] 비헤이비어 트리 개요</title>
      <link>https://wecandev.tistory.com/227</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;언리언 엘진의 비헤이비어 트리 개념과 일반적인 비헤이비어 트리와의 차이점을 알아보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 엔진 5(언리얼 엔진)에서 캐릭터에 사용할 인공 지능(AI)을 생성하는 방법은 다양하다. 블루프린트 비주얼 스크립팅(Blueprint Visual Scripting)을 사용하면 캐릭터가 애니메이션을 재생하거나, 특정 위치로 이동하거나, 충돌에 반응하는 등 '무언가를 하도록' 지시할 수 있다. AI 캐릭터가 스스로 생각하고 결정을 내리도록 하려면 비헤이비어 트리(Behavior Tree)를 사용해보자. 아래 비헤이비어 트리는 AI 캐릭터가 순찰과 플레이어 추격 사이에서 행동을 전환하는 예시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;641&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fc8yO/btsgkI4T5Cj/QbdRATZMOVIYL4ANl8ozBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fc8yO/btsgkI4T5Cj/QbdRATZMOVIYL4ANl8ozBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fc8yO/btsgkI4T5Cj/QbdRATZMOVIYL4ANl8ozBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFc8yO%2FbtsgkI4T5Cj%2FQbdRATZMOVIYL4ANl8ozBk%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;1164&quot; height=&quot;641&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;641&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비헤이비어 트리 기본&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비헤이비어 트리는 블루프린트와 유사한 시각적 방식으로 생성된다. 기능이 탑재된 다양한 노드를 추가하고 연결하여 비헤이비어 트리 그래프(Behavior Tree Graph)를 만드는 방식이다. 비헤이비어 트리가 로직을 실행하는 한편, 비헤이비어 트리가 결정을 내리는 데 필요한 정보인 블랙보드 키(Blackboard Key) 는 블랙보드(Blackboard) 라는 별도의 에셋에 저장된다. 일반적인 워크플로는 블랙보드를 생성하고, 블랙보드 키를 추가한 다음, 블랙보드 에셋을 사용하는 비헤이비어 트리를 생성한다. 블랙보드는 아래에서처럼 비헤이비어 트리에 할당된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4EpWc/btsgkI4T5D4/sjehfML11EDTQJQ0HqtzZ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4EpWc/btsgkI4T5D4/sjehfML11EDTQJQ0HqtzZ0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4EpWc/btsgkI4T5D4/sjehfML11EDTQJQ0HqtzZ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4EpWc%2FbtsgkI4T5D4%2FsjehfML11EDTQJQ0HqtzZ0%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;752&quot; height=&quot;272&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 엔진의 비헤이비어 트리는 왼쪽에서 오른쪽, 위에서 아래 순서로 로직을 실행한다. 실행 순서는 그래프에 배치된 노드의 우측 상단에 표시되는 숫자로 확인할 수 있다. 아래 이미지의 비헤이비어 트리 그래프에서 가장 왼쪽의 샘플 분기에 연결된 노드들은 블랙보드 키 HasLineOfSight 가 설정된 경우 AI가 플레이어를 추격하도록 지시한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cykntH/btsgluSBRye/J2uFyKdUKoA8aH6ucM0HdK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cykntH/btsgluSBRye/J2uFyKdUKoA8aH6ucM0HdK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cykntH/btsgluSBRye/J2uFyKdUKoA8aH6ucM0HdK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcykntH%2FbtsgluSBRye%2FJ2uFyKdUKoA8aH6ucM0HdK%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;581&quot; height=&quot;466&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파란색 노드는 데코레이터(Decorator)이다. 다른 비헤이비어 트리 시스템의 조건문에 해당한다. 데코레이터는 컴포짓(Composite)노드에 어태치되어 블랙보드 키가 True인지 검증하는데 사용된다. 이를 통해 나머지 분기를 실행할지 결정된다. 보라색 노드는 태스크(Task) 노드이며, AI가 수행할 수 있는 액션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비헤이비어 트리와 로직을 생성했다면 트리를 실행해보자. 보통은 폰(Pawn) 또는 캐릭터거나 다른 엔티티인 AI의 '바디'가 있고, 거기 연결된 AI 컨트롤러(AI Controller)를 사용해서 폰을 제어하고 액션을 지시한다. 비헤이비어 트리를 실행하는 것도 액션 중 하나다. 아래는 폰에 커스텀 AI 컨트롤러 클래스를 할당했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;799&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckVDRq/btsgniw1J9W/fTiBAuRygdbdo5DQCWGyz0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckVDRq/btsgniw1J9W/fTiBAuRygdbdo5DQCWGyz0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckVDRq/btsgniw1J9W/fTiBAuRygdbdo5DQCWGyz0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckVDRq%2Fbtsgniw1J9W%2FfTiBAuRygdbdo5DQCWGyz0%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;799&quot; height=&quot;271&quot; data-origin-width=&quot;799&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 AI 컨트롤러에서는 컨트롤러가 폰에 '빙의'할 때 비헤이비어 트리를 실행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xIW96/btsgniDNS4M/kZHAAcL1hZLagvt4RN1UP1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xIW96/btsgniDNS4M/kZHAAcL1hZLagvt4RN1UP1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xIW96/btsgniDNS4M/kZHAAcL1hZLagvt4RN1UP1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxIW96%2FbtsgniDNS4M%2FkZHAAcL1hZLagvt4RN1UP1%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;695&quot; height=&quot;300&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폰이 환경을 탐색하게 하려면 내비메시 바운드 볼륨(Nav Mesh Bounds Volume) 을 레벨에 추가해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;언리얼 엔진 비헤이비어 트리의 차별점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 주도형 비헤이비어 트리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 엔진 비헤이비어 트리가 다른 비헤이비어 트리 시스템과 다른 점 중 하나는 이벤트로 주도되기 때문에 프레임마다 불필요한 작업이 수행되지 않는다는 점이다. 관련 변경 사항이 발생했는지 지속적으로 확인하는 대신에, 비헤이비어 트리는 트리 내 변경 사항을 트리거하는 데 사용할 수 있는 '이벤트'를 수동적으로 리스닝한다. 아래는 블랙보드 키 HasLineOfSight를 업데이트하는 데 이벤트가 사용되었다.&lt;/p&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;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwN89N/btsgk0c74bt/iMbfope6yyb79YhwO5mXgk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwN89N/btsgk0c74bt/iMbfope6yyb79YhwO5mXgk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwN89N/btsgk0c74bt/iMbfope6yyb79YhwO5mXgk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwN89N%2Fbtsgk0c74bt%2FiMbfope6yyb79YhwO5mXgk%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;652&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 주도형 아키텍처가 있으면 퍼포먼스와 디버깅이 모두 개선된다. 그러나 이러한 개선 사항을 최대한 활용하기 위해서는 언리얼 엔진 비헤이비어 트리의 나머지 차이점도 이해하여 비헤이비어 트리의 구조를 적절하게 구성해야 한다. 코드가 매 틱마다 전체 트리를 반복작업할 필요가 없기 때문에 퍼포먼스가 훨씬 뛰어나다. 개념적으로는, 도착했는지 계속해서 묻지 않고 도착할 때까지 쉬다가 도착했다는 알림을 받는 것과 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비헤이비어 트리의 실행 히스토리를 한 단계씩 앞뒤로 이동하면서 비헤이비어를 시각적으로 디버깅하는 경우, 연관성이 있는 변경 사항만 히스토리에 표시하고 무관한 사항은 표시하지 않는 것이 이상적이다. 언리얼 엔진의 이벤트 주도형 구현 방식에서는 트리를 반복작업하여 이전과 동일한 비헤이비어를 선택하는 연관성 낮은 단계를 필터로 걸러낼 필요가 없다. 추가적인 반복 작업이 애초에 발생하지 않기 때문, 대신에 트리 내에서의 실행 위치 또는 블랙보드 값의 변경만 고려하면 되고, 이러한 차이점을 표시하는 것은 쉽다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건문이 리프 노드가 아님&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비헤이비어 트리 표준 모델에서의 조건문은 '태스크' 리프 노드로, 성공이나 실패 이외에는 아무것도 하지 않는다. 물론 전통적인 조건문 태스크를 만들지 못할 이유는 없지만, 조건문에는 데코레이터(Decorator)를 사용하는 것을 권장함&lt;br /&gt;조건문을 태스크가 아닌 데코레이터로 만들면 다음과 같은 이점이 있다.&lt;/p&gt;
&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;li&gt;모든 리프가 액션 태스크이므로 트리를 통해 실제 어떤 액션이 지시되는지 알기가 더 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건문은 자신이 제어하는 서브트리의 루트에 있으므로 조건이 충족되지 않은 경우 트리의 어떤 부분이 '닫혔는지' 바로 알 수 있다. 또한 모든 리프가 액션 태스크이므로 실제 액션을 알기가 더 쉽다. 기존에는 조건문이 리프 사이에 있으므로 어떤 리프가 조건문이고 어떤 리프가 액션인지 알아내기 어려웠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eiABMe/btsglxaDGqw/NwOnsP8694ks1E8vfcsLO1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eiABMe/btsglxaDGqw/NwOnsP8694ks1E8vfcsLO1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eiABMe/btsglxaDGqw/NwOnsP8694ks1E8vfcsLO1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeiABMe%2FbtsglxaDGqw%2FNwOnsP8694ks1E8vfcsLO1%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;723&quot; height=&quot;388&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 비헤이비어 트리 섹션에서는 'Close Enough'와 블랙보드 데코레이터가 시퀀스 노드의 자손이 실행되는 것을 방지한다. 조건문 데코레이터의 또 다른 이점은 트리 내 중요 노드에서 이벤트를 기다리는 관찰자 역할로 만들기 쉽다는 것이다. 트리의 이벤트 주도형 속성을 최대한 활용하는 데 매우 중요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시 발생 비헤이비어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준적인 비헤이비어 트리는 보통 병렬 컴포짓 노드를 사용하여 동시 발생 행동을 처리하며, 이 병렬 노드는 모든 자손에서 동시에 실행되기 시작한다. 자손 트리 중 하나 이상이 종료되었을 때 어떤 액션을 취할지는 (원하는 행동에 따라) 특수 규칙으로 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 엔진 비헤이비어 트리는 복잡한 병렬 노드 대신 단순 병렬(Simple Parallel) 노드, 서비스(Services)라는 특수 노드, 데코레이터의 관찰자 중단(Observer Aborts) 프로퍼티를 사용하여 동일한 유형의 행동을 달성한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단순 병렬 노드&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;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s1tNl/btsgmF68Ndo/KuszEgodr3usPcEbkoi9Hk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s1tNl/btsgmF68Ndo/KuszEgodr3usPcEbkoi9Hk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s1tNl/btsgmF68Ndo/KuszEgodr3usPcEbkoi9Hk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs1tNl%2FbtsgmF68Ndo%2FKuszEgodr3usPcEbkoi9Hk%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;327&quot; height=&quot;193&quot; data-origin-width=&quot;327&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 병렬 노드는 자손을 두 개만 갖는다. 한 자손은 반드시 단일 태스크 노드여야 하며(데코레이터 선택 가능), 나머지 자손은 완전한 서브트리일 수 있다. 단순 병렬 노드는 'A를 수행하는 동안 B도 수행한다'로 생각하면 된다. 기본적으로 A는 주요 태스크이고, B는 A가 완료되기까지 기다리는 도중의 부가 태스크 또는 필러 태스크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부가 태스크(태스크 B)를 처리하는 방법에는 몇 가지 옵션이 있지만, 노드 개념은 전통적인 병렬 노드에 비해 비교적 단순하다. 그러면서도 병렬 노드가 사용되는 일반적인 경우를 대부분 지원한다. 단순 병렬 노드를 사용하면 이벤트 주도형 최적화를 활용하기 쉽지만, 완전 병렬 노드는 최적화하기 훨씬 까다롭다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;서비스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스는 셀렉터, 시퀀스, 단순 병렬 등 모든 컴포짓 노드와 연관되는 특수 노드로, 지정된 시간(초)마다 콜백을 등록하고 주기적으로 발생시킬 필요가 있는 다양한 유형의 업데이트를 수행한다. 예를 들어 AI 폰이 현재의 적을 쫓는 비헤이비어 트리를 정상적으로 따라가는 동안 어느 적이 최적의 대상인지 결정하기 위해 서비스를 사용할 수 있다. 서비스는 자신이 추가된 컴포짓 노드의 서브트리에 실행이 머물러 있는 동안만 활성화된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;관찰자 중단&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준 병렬 노드의 흔한 용도 중 하나는 조건을 지속적으로 확인하여 요구 조건이 False가 되는 경우 태스크를 중단하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 '쉭쉭거리기' 와 '달려들기' 시퀀스를 수행하는 고양이가 있다면, 쥐가 쥐구멍으로 도망치는 순간 즉시 포기하도록 하는 것이 좋다. 병렬 노드로는 쥐에게 달려들 수 있는지 확인하는 자손과 시퀀스가 수행할 자손을 가질 수 있다. 언리얼 엔진 비헤이비어 트리는 이벤트 주도형이므로 이렇게 하는 대신에 조건문 데코레이터에서 해당 값을 관찰하게 하고 필요시 중단시키는 방식으로 처리한다. 이 예시에서는 '쥐에게 달려들 수 있는가?'를 들 수 있다. 이 경우, 시퀀스의 데코레이터에서 '관찰자 중단'을 '셀프(Self)'로 설정하면 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동시 발생 비헤이비어에 대한 언리얼 엔진 접근법의 장점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 엔진에서 동시 발생 비헤이비어를 처리하는 방법에는 다음과 같은 세 가지 주요 장점이 있다.&lt;/p&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;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.unrealengine.com/5.2/ko/behavior-tree-in-unreal-engine---overview/&quot;&gt;https://docs.unrealengine.com/5.2/ko/behavior-tree-in-unreal-engine---overview/&lt;/a&gt;&lt;/p&gt;</description>
      <category>⭐ Game Programming/Unreal Document</category>
      <category>비헤이비어트리</category>
      <category>언리얼엔진</category>
      <author>테디슨</author>
      <guid isPermaLink="true">https://wecandev.tistory.com/227</guid>
      <comments>https://wecandev.tistory.com/227#entry227comment</comments>
      <pubDate>Wed, 17 May 2023 21:54:12 +0900</pubDate>
    </item>
    <item>
      <title>[언리얼엔진] 슬레이트 위젯 예제</title>
      <link>https://wecandev.tistory.com/226</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;공용 슬레이트 인수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 공통 인수이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IsEnabled - 상호작용(Interation) 가능한 위젯인지를 지정한다. 끄면 회식으로 비활성화 된다.&lt;/li&gt;
&lt;li&gt;ToolTip - 툴팁에 사용할 커스텀 SToolTip 위젯 종류를 지정한다. 지정되지 않으면 툴팁이 나타나지 않는다.&lt;/li&gt;
&lt;li&gt;ToolTipText - 간단한 툴팁으로 표시할 텍스트 유형을 지정한다. 지정되지 않거나, ToolTip 특성이 사용된 경우 나타나지 않는다.&lt;/li&gt;
&lt;li&gt;Cursor - 마우스를 올렸을 때 표시할 커서를 지정합니다.&lt;/li&gt;
&lt;li&gt;Visibility - 아래 Visibility 부분을 참고.&lt;/li&gt;
&lt;li&gt;Text - 텍스트가 적용 가능하다면 지정할 문구&lt;/li&gt;
&lt;li&gt;Content - 콘텐츠가 적용 가능하다면 지정할 위젯&lt;/li&gt;
&lt;li&gt;ReadOnly - True 면 이 위젯은 수정할 수 없다.&lt;/li&gt;
&lt;li&gt;Style - 위젯에 사용되는 이미지나 텍스트 스타일을 지정한다. 적용 방식은 위젯마다 다르다.&lt;/li&gt;
&lt;li&gt;[...]ColorAndOpacity - 위젯이나 지정된 시스템의 색과 불투명도&lt;/li&gt;
&lt;li&gt;Padding - 위젯의 상하좌우 주변에 패딩으로 댈 Slate 유닛 단위 여백의 양이다.&lt;/li&gt;
&lt;li&gt;HAlign - 위젯 안 콘텐츠의 가로 정렬 방식&lt;/li&gt;
&lt;li&gt;VAlign - 위젯 안 콘텐츠의 세로 정렬 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Visibility&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Visibility는 위젯을 표시할 것인지와 상호작용 여부도 결정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Visible - 위젯이 보통의 경우 나타난다. (Default)&lt;/li&gt;
&lt;li&gt;Collapsed - 위젯이 보이지 않으며 레이아웃에서 자리를 차지하지 않는다. 상호작용되지도 않는다.&lt;/li&gt;
&lt;li&gt;Hidden - 위젯이 보이지는 않으며 레이아웃에서 자리는 차지한다. 상호작용되지는 않는다.&lt;/li&gt;
&lt;li&gt;HitTestInvisible - 보이지만 상호작용되지는 않는다.&lt;/li&gt;
&lt;li&gt;SelfHitTestInvisible - HitTestInvisible 와 같지만 자손 위젯에 적용되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Alignment&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Alignment는 위젯의 부모 안에서 그 위젯의 위치를 결정한다. 위젯 부모 크기가 위젯과 같다면 정렬은 의미가 없다.위젯에 &quot;Fill&quot;을 지정하거나 박스 슬롯에 Fill Size가 지정된 경우, 그 박스 슬롯에 기본으로 적용된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HAlign_Fill / VAlign_Fill - 가로 세로 가득 채운 정렬&lt;/li&gt;
&lt;li&gt;HAlign_Left - 왼쪽 정렬&lt;/li&gt;
&lt;li&gt;VAlign_Top - 위쪽 정렬&lt;/li&gt;
&lt;li&gt;HAlign_Center / VAlign_Center - 가로 세로 가운데 정렬&lt;/li&gt;
&lt;li&gt;HAlign_Right - 오른쪽 정렬&lt;/li&gt;
&lt;li&gt;VAlign_Bottom - 아래쪽 정렬&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Box 패널&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SHorizontalBox, SVerticalBox 는 레이아웃 배치에 있어 가장 흔한 위젯이다. 이 박스 패널은 위젯으로 선언되나, 그 이후 슬롯이 삽입된다. 이 슬롯의 배치 순서는 SHorizontalBox 의 경우 좌에서 우, SVerticalBox 는 위에서 아래다.&lt;br /&gt;SScrollBox 는 SVerticalBox 와 비슷하게 작동하는데, 자손 슬롯의 세로 스크롤이 가능하다는 점이 다르다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Slot 특성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Width or Height (폭 또는 높이) 세팅 (다음 옵션은 상호 배제적이다):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Auto Size (자동 크기, 기본) - 슬롯에 딱 필요한 만큼만 공간을 지정. 슬롯 내 정렬은 중요치 않음.&lt;/li&gt;
&lt;li&gt;Fill Size (필 크기) - 필 계수를 지정하면 다른 슬롯의 필 계수에 따라 공간을 채움. 슬롯 정렬은 중요치 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Max Size (최대 크기) - 슬롯의 최대 크기를 Slate 유닛 단위로 지정.&lt;/li&gt;
&lt;li&gt;Padding (패딩) - 패널 네 슬롯의 패딩을 지정.&lt;/li&gt;
&lt;li&gt;Alignment (정렬) - 슬롯 내 자손 위젯의 정렬 방식을 결정. 이 옵션은 같은 방향의 Fill Size 가 적용되어 있으면 의미가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;SNew(SScrollBox)
+SScrollBox::Slot() .Padding(10,5)
[
    SNew(SHorizontalBox)
    +SHorizontalBox::Slot() .HAlign(HAlign_Left)
    [
        ...
    ]
    +SHorizontalBox::Slot() .HAlign(HAlign_Center)
    [
        ...
    ]
    +SHorizontalBox::Slot() .HAlign(HAlign_Right)
    [
        ...
    ]
]
+SScrollBox::Slot() .Padding(10,5)
[
    SNew(SHorizontalBox)
    +SHorizontalBox::Slot() .FillWidth(2)
    [
        ...
    ]
    +SHorizontalBox::Slot() .FillWidth(1)
    [
        ...
    ]
    +SHorizontalBox::Slot() .FillWidth(3)
    [
        ...
    ]
]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Uniform Grid Panel&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SUniformGridPanel 은 자손 위젯을 가로 세로 모두 고르게 분배하는 패널이다. 자손 슬롯은 자손의 인덱스를 나타내는 정수 한 쌍으로 지정된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;SNew(SUniformGridPanel)
.SlotPadding( FMargin( 5.0f ) )
+SUniformGridPanel::Slot(0,0)
.HAlign(HAlign_Right)
[
    ...
]
+SUniformGridPanel::Slot(0,1)
.HAlign(HAlign_Right)
[
    ...
]
+SUniformGridPanel::Slot(0,2)
.HAlign(HAlign_Center)
[
    ...
]
+SUniformGridPanel::Slot(1,0)
[
    ...
]
+SUniformGridPanel::Slot(1,1)
[
    ...
]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Warp Box&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWrapBox 는 줄바꿈 상자, 즉 일정 폭까지 위젯이 가로로 배치되다가 다음 줄에 놓이는 식이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;SNew(SWrapBox)
.PreferredWidth( 300.f )
+SWrapBox::Slot()
.Padding( 5 )
.VAlign(VAlign_Top)
[
    ...
]
+SWrapBox::Slot()
.Padding( 5 )
.VAlign(VAlign_Bottom)
[
    ...
]
+SWrapBox::Slot()
.Padding( FMargin(20, 5, 0, 5) )
.VAlign(VAlign_Center)
[
    ...
]
+SWrapBox::Slot()
.Padding( 0 )
.VAlign(VAlign_Fill)
[
    ...
]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Radio Button&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라디오 버튼은 Slate의 체크박스다. 체크 박스는 체크 여부 결정을 위해 델리게이트가 필요하기 때문, 라디오 버튼 시리즈를 만드는 가장 쉬운 방법은 어떤 체크박스가 체크되었는지 결정하는 enum을 만드는 것이다. 현재 선택한 내용을 enum 값으로 저장한다. 그 다음 검사하는 델리게이트에 대해 함수를 전달하고, 파라미터로 같이 전달된 enum값과 현재 선택된 내용을 검사한다. 선택 변경시에는 현재 내용을 업데이트 해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;ERadioChoice CurrentChoice;

...

ECheckBoxState::Type IsRadioChecked( ERadioChoice ButtonId ) const
{
    return (CurrentChoice == ButtonId)
        ? ECheckBoxState::Checked
        : ECheckBoxState::Unchecked;
}

...

void OnRadioChanged( ERadioChoice RadioThatChanged, ECheckBoxState::Type NewRadioState )
{
    if (NewRadioState == ECheckBoxState::Checked)
    {
        CurrentChoice = RadioThatChanged;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메뉴와 툴바&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메뉴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메뉴나 툴바를 생성하기 위해 멀티박스를 사용한다. Slate의 UI_COMMAND 시스템을 사용하지만 동적으로 생성된 버튼,컨트롤로는 불가능하다.&lt;br /&gt;메뉴를 만들려면 FMenuBarBuilder를 생성하여 FUICommandList를 전달한다. 마지막으로 메뉴 바 빌더에서 MakeWidget()을 호출하여 위치를 정한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;FMenuBarBuilder MenuBarBuilder( CommandList );
{
    MenuBarBuilder.AddPullDownMenu( TEXT(&quot;Menu 1&quot;), TEXT(&quot;Opens Menu 1&quot;), FNewMenuDelegate::CreateRaw( &amp;amp;FMenus::FillMenu1Entries ) );

    MenuBarBuilder.AddPullDownMenu( TEXT(&quot;Menu 2&quot;), TEXT(&quot;Opens Menu 2&quot;), FNewMenuDelegate::CreateRaw( &amp;amp;FMenus::FillMenu2Entries ) );
}

return MenuBarBuilder.MakeWidget();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목, 항목, 구분자, 서브메뉴, 편집가능 텍스트, 커스텀 위젯등을 추가할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;static void FillMenu1Entries( FMenuBuilder&amp;amp; MenuBuilder )
{
    MenuBuilder.AddEditableText( TEXT( &quot;Editable Item&quot; ), TEXT( &quot;You can edit this item's text&quot; ), NAME_None, TEXT( &quot;Edit Me!&quot; ) );

    MenuBuilder.AddMenuEntry( FMultiBoxTestCommandList::Get().EighthCommandInfo );

    MenuBuilder.AddMenuSeparator();

    MenuBuilder.AddSubMenu( TEXT(&quot;Sub Menu&quot;), TEXT(&quot;Opens a submenu&quot;), FNewMenuDelegate::CreateRaw( &amp;amp;FMenus::FillSubMenuEntries ) );

    MenuBuilder.AddWidget(SNew(SVolumeControl), TEXT(&quot;Volume&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨텍스트 메뉴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메뉴 중 하나를 컨텍스트 메뉴로 불러오기 위해서는, FMenuBuilder 에서 생성된 위젯을 구한 다음, 그것을 PushMenu 함수에 다음과 같이 전해준다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;FSlateApplication::Get().PushMenu(
    MenuBuilder.MakeWidget(),
    MouseCursorLocation,
    FPopupTransitionEffect( FPopupTransitionEffect::ContextMenu )
    );&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;툴바&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FToolBarBuilder 를 사용하여 툴바를 생성한다. 자손의 경우 툴바 버튼, 콤보 버튼, 일반 버튼, 풀다운 메뉴 등을 추가할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;FToolBarBuilder GameToolBarBuilder( InCommandList );
{
    GameToolBarBuilder.AddToolBarButton( FLevelEditorCommands::Get().Simulate );

    GameToolBarBuilder.AddToolBarButton( 
        FLevelEditorCommands::Get().RepeatLastPlay, 
        LOCTEXT(&quot;RepeatLastPlay&quot;, &quot;Play&quot;).ToString(),
        TAttribute&amp;lt; FString &amp;gt;::Create( TAttribute&amp;lt; FString &amp;gt;::FGetter::CreateRaw( &amp;amp;FLevelEditorActionCallbacks::GetRepeatLastPlayToolTip ) ),
        TAttribute&amp;lt; const FSlateBrush* &amp;gt;::Create( TAttribute&amp;lt; const FSlateBrush* &amp;gt;::FGetter::CreateRaw( &amp;amp;FLevelEditorActionCallbacks::GetRepeatLastPlayIconSlateBrush ) )
        );

    GameToolBarBuilder.AddComboButton(
        SpecialPIEOptionsMenuAction,
        FOnGetContent::CreateRaw( &amp;amp;FLevelEditorToolBar::GeneratePIEMenuContent, InCommandList ),
        FString(),
        LOCTEXT(&quot;PIEComboToolTip&quot;, &quot;Play-In-Editor options&quot;).ToString() );
}

return GameToolBarBuilder.MakeWidget();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아이템 뷰&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이템 뷰는 데이터에 대한 공유 포인터 템플릿 인수를 받는다. Tarray나 TSharedPtr로 채워지고, 그 내부 위젯은 전달된 OnGenerateRow나 OnGenerateTile 로 동적으로 채워지며, 각 열 마다 다양한 위젯으로 생성된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리스트 뷰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 뷰는 자손 위젯 목록을 저장하는 위젯이다. SListView&amp;lt;...&amp;gt;로 만든다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SNew( SListView&amp;lt; TSharedPtr&amp;lt;FTestData&amp;gt; &amp;gt; )
.ItemHeight(24)
.ListItemsSource( &amp;amp;Items )
.OnGenerateRow( this, &amp;amp;STableViewTesting::OnGenerateWidgetForList )
.OnContextMenuOpening( this, &amp;amp;STableViewTesting::GetListContextMenu )
.SelectionMode( this, &amp;amp;STableViewTesting::GetSelectionMode )
.HeaderRow
(
    SNew(SHeaderRow)
    + SHeaderRow::Column(&quot;Name&quot;)
    [
        SNew(SBorder)
        .Padding(5)
        .Content()
        [
            SNew(STextBlock)
            .Text(TEXT(&quot;Name&quot;))
        ]
    ]
    + SHeaderRow::Column(&quot;Number&quot;) .DefaultLabel(TEXT(&quot;Number&quot;))
    + SHeaderRow::Column(&quot;TextField&quot;) .DefaultLabel(TEXT(&quot;Text Field&quot;))
    + SHeaderRow::Column(&quot;TextBlock&quot;) .DefaultLabel(TEXT(&quot;Text Block&quot;))
    + SHeaderRow::Column(&quot;AddChild&quot;) .DefaultLabel(TEXT(&quot;Add Child&quot;))
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트리 뷰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리뷰는 STreeView&amp;lt;...&amp;gt; 로 만들고 리스트뷰와 비슷하지만 리스트 내 다른 위젯에 대한 부모 위젯이 지원된다. OnGetChildren 델리게이트를 사용해서 자손 위젯을 전달한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SNew( STreeView&amp;lt; TSharedPtr&amp;lt;FTestData&amp;gt; &amp;gt; )
.ItemHeight(24)
.TreeItemsSource( &amp;amp;Items )
.OnGenerateRow( this, &amp;amp;STableViewTesting::OnGenerateWidgetForTree )
.OnGetChildren( this, &amp;amp;STableViewTesting::OnGetChildrenForTree )
.OnContextMenuOpening( this, &amp;amp;STableViewTesting::GetTreeContextMenu )
.SelectionMode( this, &amp;amp;STableViewTesting::GetSelectionMode )
.HeaderRow
(
    SNew(SHeaderRow)
    + SHeaderRow::Column(&quot;Name&quot;) .DefaultLabel(TEXT(&quot;Name&quot;))
    + SHeaderRow::Column(&quot;Number&quot;) .DefaultLabel(TEXT(&quot;Number&quot;))
    + SHeaderRow::Column(&quot;TextField&quot;) .DefaultLabel(TEXT(&quot;Text Field&quot;))
    + SHeaderRow::Column(&quot;TextBlock&quot;) .DefaultLabel(TEXT(&quot;Text Block&quot;))
    + SHeaderRow::Column(&quot;AddChild&quot;) .DefaultLabel(TEXT(&quot;Add Child&quot;))
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타일 뷰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타일뷰는 STileView&amp;lt;...&amp;gt; 로 만들고 리스트 뷰와 비슷하지만, 자손 위젯이 리스트가 아닌 그리드로 배치된다. 그래서 열과 열 제목을 지원하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;SNew( STileView&amp;lt; TSharedPtr&amp;lt;FTestData&amp;gt; &amp;gt; )
.ItemWidth(128)
.ItemHeight(64)
.ListItemsSource( &amp;amp;Items )
.OnGenerateTile( this, &amp;amp;STableViewTesting::OnGenerateWidgetForTileView )
.OnContextMenuOpening( this, &amp;amp;STableViewTesting::GetTileViewContextMenu )
.SelectionMode( this, &amp;amp;STableViewTesting::GetSelectionMode )&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.unrealengine.com/5.1/ko/slate-ui-widget-examples-for-unreal-engine/&quot;&gt;https://docs.unrealengine.com/5.1/ko/slate-ui-widget-examples-for-unreal-engine/&lt;/a&gt;&lt;/p&gt;</description>
      <category>⭐ Game Programming/Unreal Document</category>
      <category>슬레이트</category>
      <category>언리얼엔진</category>
      <author>테디슨</author>
      <guid isPermaLink="true">https://wecandev.tistory.com/226</guid>
      <comments>https://wecandev.tistory.com/226#entry226comment</comments>
      <pubDate>Wed, 10 May 2023 20:59:38 +0900</pubDate>
    </item>
    <item>
      <title>[언리얼엔진] 슬레이트 아키텍처</title>
      <link>https://wecandev.tistory.com/225</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;동기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위젯에서 UI를 만들 때 어려운 부분
&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;li&gt;데이터 흐름 제어 : 위젯과 내재된 데이터끼리 묶어주는것&lt;/li&gt;
&lt;li&gt;UI 기술(description)을 위한 외국어 습득&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;IMGUI : Immediate Mode Graphics User Interface
&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;프로그래머는 UI 기술이 데이터에서 구하기 쉽다는 점에서 코드와 비슷하다는 점을 선호한다.&lt;/li&gt;
&lt;li&gt;실효(Invalidation)은 문제가 되지 않고, 그냥 데이터를 직접 폴링(poll)한다.&lt;/li&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;애니메이션과 스타일링 추가가 어려움&lt;/li&gt;
&lt;li&gt;UI기술이 명령형 코드라서, 데이터 주도형으로 만들기 어려움&lt;/li&gt;
&lt;/ul&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;모델의 코드와 데이터에 쉽게 접근&lt;/li&gt;
&lt;li&gt;UI 절차적 생성 지원&lt;/li&gt;
&lt;li&gt;UI 설명이 꼬이지 않도록&lt;/li&gt;
&lt;li&gt;애니메이션과 스타일을 반드시 지원&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불투명 캐시와 중복 스테이트를 피함. UI는 스테이트를 캐시에 담고 실효(Invalidation)는 명시적으로 해야한다.
&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;불투명 캐시에 드물게(low-grain) 실효(invalidation)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;UI 구조가 변할 때, 알림(notification)보다 폴링(Polling)을 선호 (알림을 사용하고자 한다면 빈도가 낮은곳에서 사용하자)&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;스크롤바가 UI 스테이트를 시각화시키는 경우처럼 UI 스테이트가 모델이 되는 경우는 예외다.&lt;/li&gt;
&lt;li&gt;퍼포먼스 떄문이라기 보다 프로그래머의 분별을 위해서이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;많은 양의 난잡한 즉성 UI에 대한 계획을 세우고, 용도를 이해하고 난 후 깔끔한 시스템으로 일반화 하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;델리게이트와 데이터 흐름 폴링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI는 Model을 시각화시키고 조작한다. Slate는 Model의 데이터를 읽고 쓸 필요가 있는 위젯의 통로로써 델리게이트(delegate)를 사용한다.&lt;br /&gt;Slate 위젯은 Model의 데이터를 표시할 필요가 있을 때 읽어들인다. 사용자가 동작을 하면, Slate 위젯은 데이터 수정을 위해 write 델리게이트를 부른다(invoke)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특성(Attribute)과 인수(Argument)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;델리게이트를 사용하는 것이 항상 바람직한 것은 아니다. 용도에 따라 Slate 위젯에 붙일 인수는 상수값이나 함수일 수도 있다. 이러한 개념은 TAttribute 클래스를 통해 캡슐화시킨다. 이러한 특성은 상수나 델리게이트로 설정 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;퍼포먼스 고려사항&lt;/h2&gt;
&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;li&gt;콘텐츠의 스크롤 작업은 가능하면 가상화 시킨다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 밖에 있는 위젯 수가 많으면 Slate 퍼포먼스를 망칠 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;큰 화면 사용자는 우월한 사양의 하드웨어를 사용하므로, 다수의 위젯을 처리해 낼 수 있다 가정 (왜?)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실효(invalidation) vs 폴링(polling)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실효의 경우 Model의 구조가 급격히 변하는 상황에서는 보통 기존 UI를 파기하고 재생성 한다. 그러나 스테이트는 잃게 될테니 꼭 필요할 때만 사용하자. 실효는 빈도가 낮은 이벤트에 쓰는것을 규칙으로 삼자&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자손 슬롯&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Slate 위젯에서 자손은 자손 슬롯에 저장한다. 자손 슬롯은 항상 유효한 위젯이며, 기본적으로 시각화나 상호작용이 없는 위젯인 SNullWidget을 저장한다. 각 위젯 유형은 자체적인 요구를 충족시키는 자손 슬롯 유형을 별도로 선언 할 수 있다. 각 패널 유형은 슬롯을 통해 자손의 배치에 영향을 끼치는 자손별 세팅 집합을 요청할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;위젯 규칙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 3가지 변종이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잎 위젯(Leaf Widgets) - 자손 슬롯이 없는 위젯. STextBlock은 텍스트를 표시하기에 더이상 자손슬롯이 필요 없다.&lt;/li&gt;
&lt;li&gt;패널 (Panels) - 자손 슬롯의 수가 가변적인 위젯. SVerticlaBox는 몇개의 자손도 수직 배치가 가능하다.&lt;/li&gt;
&lt;li&gt;복합 위젯(Compound Widgets) - 명시적으로 이름붙은 자손 슬롯의 갯수가 고정된 위젯. SButton에는 Content라는 슬롯이 있어 버튼 안에 몇 개의 위젯이든 포함 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레이아웃&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slate 레이아웃은 두 패스로 이루어 진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pass 1 : Cache Desired Size - 연관된 함수는 SWidget::CacheDesiredSize and SWidget::ComputeDesiredSize&lt;/li&gt;
&lt;li&gt;Pass 2 : ArrangeChildren - 연관된 함수는 SWidget::ArrangeChildren&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pass 1 : Cache Desired Size&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패스의 목표는 각 위젯이 얼마만큼 공간을 차지하려는지 알아내는 것이다. 자손이 없는(잎)위젯같은 경우 내재된(instrinsic) 프로퍼티에 따라 크기를 계산하여 캐시하도록 요청받는다. 다른 위젝과 합쳐지는 위젯(복합 위젯과 패널)은 자손 크기 결정 함수로서 크기를 결정하는데 특수한 로직을 사용한다. ComputeDesiredSize()를 구현할 때만 각 위젯 유형이 필요하다는 점에 유의하고, 캐시나 탐색 로직은 Slate가 구현한다. 위젯에서 ComputeDesiredSize()가 호출될 때, 자손에는 이미 원하는 크기가 계산 및 캐시되어있음을 Slate가 보장해준다. 이건 상향식(bottom up) 패스이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pass 2 : ArrangeChildren&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Arrange children은 하향식(Top down) 패스다. Slate는 최상위 창 부터 시작해서 프로그래머가 제공한 제약(Constraint)에 따라 자식을 배치할 것인지 각 창에 묻는다. 각 자손에 할당된 공간이 알려지면 Slate는 재귀를 통해 자손의 자손을 배치할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Slate 그리기 : OnPaint&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패인트 패스 도중 Slate는 보이는 모든 위젯에 대해 반복하면서 렌더링 시스템이 소비하게 될 그리기 요소 목록을 만든다. 이 목록은 매프레임 새로 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상위 창부터 시작하여 계층구조를 따라 내려오면서, 모든 위젯의 그리기 요소를 그리기 목록에 덧붙인다. 위젯은 페인트 도중 두 가지 작업을 한다. 실제 그리기 요소를 출력하거나, 자손 위젯이 있을 위치를 알아낸 다음 자손 위젯더러 스스로 그리라고 요청한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SWidget 해부도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slate에서 SWidget의 주요 함수&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ComputeDesiredSize() - 사이즈 크기를 계산한다.&lt;/li&gt;
&lt;li&gt;ArrangeChildren() - 부모 할당 영역 내 자손의 배치를 담당&lt;/li&gt;
&lt;li&gt;OnPaint() - 출력을 담당&lt;/li&gt;
&lt;li&gt;이벤트 핸들러 - OnSomething 형태의 것으로, 이들은 다양한 시점에서 Slate 가 위젯에서 부를 수 있는 함수들&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Composition&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Composition 이란 어느 슬롯도 임의의 위젯 내용을 담을 수 있어야 한다는 개념이다. 이로인해 Slate 사용자들은 엄청난 유연성을 확보할 수 있다. Composition 은 가능하면 언제든지, 핵심 Slate 위젯에서 사용된다.&lt;br /&gt;어떤 경우엔 위젯에 특정 유형의 자손을 포함하기도 하는데, 그러한 경우 Composition 조건은 더이상 적용되지 않는다. 이들은 절대 Slate 코어 속 위젯이라기 보다는, 일정 영역 밖에서 재사용되도록 고안되지는 않은 도메인 전용 위젯이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;선언형 문법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 Slate를 직접 접근할 필요가 있어서, UI 기술에 선언형(declarative) 언어가 필요했고, C++ 함수 바인딩에 컴파일 시간 검사도 가능하도록 하였다.&lt;br /&gt;그래서 C++ 부분집합으로써 선언형 UI 기술 언어를 만들게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.unrealengine.com/5.1/ko/understanding-the-slate-ui-architecture-in-unreal-engine/&quot;&gt;https://docs.unrealengine.com/5.1/ko/understanding-the-slate-ui-architecture-in-unreal-engine/&lt;/a&gt;&lt;/p&gt;</description>
      <category>⭐ Game Programming/Unreal Document</category>
      <category>슬레이트</category>
      <category>언리얼엔진</category>
      <author>테디슨</author>
      <guid isPermaLink="true">https://wecandev.tistory.com/225</guid>
      <comments>https://wecandev.tistory.com/225#entry225comment</comments>
      <pubDate>Wed, 3 May 2023 20:11:39 +0900</pubDate>
    </item>
    <item>
      <title>[언리얼엔진] 슬레이트 개요</title>
      <link>https://wecandev.tistory.com/224</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Slate는 Unreal Editor 또는 게임내 사용자 인터페이스와 같은 도구 및 응용 프로그램용 사용자 인터페이스를 효율적으로 구축하도록 설계된 플랫폼에 구애받지 않는 맞춤형 사용자 인터페이스 프레임워크이다. 서술형(declarative) 문법에 쉬운 디자인, 레이아웃, 스타일 요소가 결합된 Slate를 통해 쉬운 UI제작 및 반복 작업이 가능할 것이다. Slate UI 솔루션으로 툴과 어플리케이션에 사용할 GUI를 매우 쉽게 조합시킬수 있으며, 반복처리도 빠르게 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서술형 문법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slate의 서술형 문법(Declarative syntax)은 프로그래머가 인디렉션(indirection) 레이어를 추가하지 않고도 UI제작을 가능하게 해준다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SLATE_BEGIN_ARGS( SSubMenuButton )
    : _ShouldAppearHovered( false )
    {}
    /** The label to display on the button */
    SLATE_ATTRIBUTE( FString, Label )
    /** Called when the button is clicked */
    SLATE_EVENT( FOnClicked, OnClicked )
    /** Content to put in the button */
    SLATE_NAMED_SLOT( FArguments, FSimpleSlot, Content )
    /** Whether or not the button should appear in the hovered state */
    SLATE_ATTRIBUTE( bool, ShouldAppearHovered )
SLATE_END_ARGS()&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포지션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slate의 컴포지션(Composition) 프레임워크는 프로토타이핑이나 반복처리를 위해 UI요소를 빠르게 재배치하는 작업을 간단하게 만들어 준다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;// 스태틱 메시용 섹션을 새로 추가
ContextualEditingWidget-&amp;gt;AddSlot()
.Padding( 2.0f )
[
    SNew( SDetailSection )
    .SectionName(&quot;StaticMeshSection&quot;)
    .SectionTitle( LOCTEXT(&quot;StaticMeshSection&quot;, &quot;Static Mesh&quot;).ToString() )
    .Content()
    [
        SNew( SVerticalBox )
        + SVerticalBox::Slot()
        .Padding( 3.0f, 1.0f )
        [
            SNew( SHorizontalBox )
            + SHorizontalBox::Slot()
            .Padding( 2.0f )
            [
                SNew( SComboButton )
                .ButtonContent()
                [
                    SNew( STextBlock )
                    .Text( LOCTEXT(&quot;BlockingVolumeMenu&quot;, &quot;Create Blocking Volume&quot;).ToString() ) 
                    .Font( FontInfo )
                ]
                .MenuContent()
                [
                    BlockingVolumeBuilder.MakeWidget()
                ]
            ]
        ]

    ]
];&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;309&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be6lOC/btscQWEIqia/pjI7mujIiywUyhtEmSdJK1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be6lOC/btscQWEIqia/pjI7mujIiywUyhtEmSdJK1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be6lOC/btscQWEIqia/pjI7mujIiywUyhtEmSdJK1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe6lOC%2FbtscQWEIqia%2FpjI7mujIiywUyhtEmSdJK1%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;309&quot; height=&quot;268&quot; data-origin-width=&quot;309&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스타일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타일은 위젯의 여러 부분에 적용할 수 있다. UI 컴포넌트의 모양을 쉽게 반복하고 스타일 공유 및 재사용도 편해진다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;// 툴바
{
    Set( &quot;ToolBar.Background&quot;, FSlateBoxBrush( TEXT(&quot;Common/GroupBorder&quot;), FMargin(4.0f/16.0f) ) );

    Set( &quot;ToolBarButton.Normal&quot;, FSlateNoResource() );      // 주: 고의적인 투명 배경
    Set( &quot;ToolBarButton.Pressed&quot;, FSlateBoxBrush( TEXT(&quot;Old/MenuItemButton_Pressed&quot;), 4.0f/32.0f ) );
    Set( &quot;ToolBarButton.Hovered&quot;, FSlateBoxBrush( TEXT(&quot;Old/MenuItemButton_Hovered&quot;), 4.0f/32.0f ) );

    // Tool bar buttons are sometimes toggle buttons, so they need styles for &quot;checked&quot; state
    Set( &quot;ToolBarButton.Checked&quot;, FSlateBoxBrush( TEXT(&quot;Old/MenuItemButton_Pressed&quot;),  4.0f/32.0f, FLinearColor( 0.3f, 0.3f, 0.3f ) ) );
    Set( &quot;ToolBarButton.Checked_Hovered&quot;, FSlateBoxBrush( TEXT(&quot;Old/MenuItemButton_Hovered&quot;),  4.0f/32.0f ) );
    Set( &quot;ToolBarButton.Checked_Pressed&quot;, FSlateBoxBrush( TEXT(&quot;Old/MenuItemButton_Pressed&quot;),  4.0f/32.0f, FLinearColor( 0.5f, 0.5f, 0.5f ) ) );

    // 툴바 버튼 레이블 폰트
    Set( &quot;ToolBarButton.LabelFont&quot;, FSlateFontInfo( TEXT(&quot;Roboto-Regular&quot;), 8 ) );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포지션에 사용된 스타일 예시&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;SNew( SBorder )
.BorderImage( FEditorStyle::GetBrush( &quot;ToolBar.Background&quot; ) )
.Content()
[
    SNew(SHorizontalBox)

    // 컴파일 버튼 (멀티박스 버튼처럼 보이도록 설정)
    +SHorizontalBox::Slot()
    [
        SNew(SButton)
        .Style(TEXT(&quot;ToolBarButton&quot;))
        .OnClicked( InKismet2.ToSharedRef(), &amp;amp;FKismet::Compile_OnClicked )
        .IsEnabled( InKismet2.ToSharedRef(), &amp;amp;FKismet::InEditingMode )
        .Content()
        [
            SNew(SVerticalBox)
            +SVerticalBox::Slot()
            .Padding( 1.0f )
            .HAlign(HAlign_Center)
            [
                SNew(SImage)
                .Image(this, &amp;amp;SBlueprintEditorToolbar::GetStatusImage)
                .ToolTipText(this, &amp;amp;SBlueprintEditorToolbar::GetStatusTooltip)
            ]
            +SVerticalBox::Slot()
            .Padding( 1.0f )
            .HAlign(HAlign_Center)
            [
                SNew(STextBlock)
                .Text(LOCTEXT(&quot;CompileButton&quot;, &quot;Compile&quot;).ToString())
                .Font( FEditorStyle::GetFontStyle( FName( &quot;ToolBarButton.LabelFont&quot; ) ) )
                .ToolTipText(LOCTEXT(&quot;CompileButton_Tooltip&quot;, &quot;Recompile the blueprint&quot;).ToString())
            ]
        ]
    ]
]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slate에는 마우스와 키보드 입력을 받는 기능이 지원된다. 유연한 키 바인딩 시스템으로 어떤 키조합이든 바인딩 가능하며, 동적으로 변경도 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slate는 타겟에 무관하게 렌더링 프리미티브를 사용하기에 어느 플랫폼에서도 실행 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레이아웃 프리미티브&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이아웃 프리미티브로 정적, 동적 레이아웃으로 빌드를 쉽게 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;FString DefaultLayout =
                TEXT( &quot; {&quot; )
                TEXT( &quot;     \&quot;type\&quot;: \&quot;tabstack\&quot;,&quot; )
                TEXT( &quot;     \&quot;sizecoeff\&quot;: 1,&quot; )
                TEXT( &quot;     \&quot;children\&quot;:&quot; )
                TEXT( &quot;     [&quot; )
                TEXT( &quot;         {&quot; )
                TEXT( &quot;             \&quot;type\&quot;: \&quot;tab\&quot;,&quot; )
                TEXT( &quot;             \&quot;content\&quot;: \&quot;Widget Inspector Tab\&quot;&quot; )
                TEXT( &quot;         },&quot; )
                TEXT( &quot;         {&quot; )
                TEXT( &quot;             \&quot;type\&quot;: \&quot;tab\&quot;,&quot; )
                TEXT( &quot;             \&quot;content\&quot;: \&quot;Plugin Manager Tab\&quot;&quot; )
                TEXT( &quot;         },&quot; )
                TEXT( &quot;         {&quot; )
                TEXT( &quot;             \&quot;type\&quot;: \&quot;tab\&quot;,&quot; )
                TEXT( &quot;             \&quot;content\&quot;: \&quot;Debug Tools\&quot;&quot; )
                TEXT( &quot;         }&quot; )
                TEXT( &quot;     ]&quot; )
                TEXT( &quot; }&quot; );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 레이아웃 코드는 아래와 같이 표현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[사진 첨부]&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자 주도형 레이아웃&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slate의 도킹 프레임워크는 자유롭게 탭 패널을 재배치하고 도킹하는 기능이 있다. 사용자가 환경을 커스터마이징할 수 있다면, 원하는 툴을 가지고 원하는 방식으로 작업할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;떠다니는 탭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y2r4V/btscS5gAMr5/VpjeEwNusVOw8yrLqs8z9K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y2r4V/btscS5gAMr5/VpjeEwNusVOw8yrLqs8z9K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y2r4V/btscS5gAMr5/VpjeEwNusVOw8yrLqs8z9K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY2r4V%2FbtscS5gAMr5%2FVpjeEwNusVOw8yrLqs8z9K%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;686&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탭을 도킹 타깃에 드래그&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYa4Bx/btscTi1fJhb/F9UFcdyIWhmmvBhpGlWbkk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYa4Bx/btscTi1fJhb/F9UFcdyIWhmmvBhpGlWbkk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYa4Bx/btscTi1fJhb/F9UFcdyIWhmmvBhpGlWbkk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYa4Bx%2FbtscTi1fJhb%2FF9UFcdyIWhmmvBhpGlWbkk%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;687&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;687&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도킹된 탭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ObsvJ/btscMHviOfn/uvHdGosTgBXDpKSpSAXGaK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ObsvJ/btscMHviOfn/uvHdGosTgBXDpKSpSAXGaK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ObsvJ/btscMHviOfn/uvHdGosTgBXDpKSpSAXGaK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FObsvJ%2FbtscMHviOfn%2FuvHdGosTgBXDpKSpSAXGaK%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;686&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발자 툴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slate 위젯 리플렉터는 UI와 관련 코드를 디버깅하고 분석할 수 있다. 버그와 예상치 못한 행위를 추적하는데 뿐 아니라, 유저 인터페이스의 프로파일링과 최적화하는 데도 도움된다.&lt;/p&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;687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FNFnL/btscSoUZ69J/j3vUt9RHlP84pk2K5AdCGK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FNFnL/btscSoUZ69J/j3vUt9RHlP84pk2K5AdCGK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FNFnL/btscSoUZ69J/j3vUt9RHlP84pk2K5AdCGK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFNFnL%2FbtscSoUZ69J%2Fj3vUt9RHlP84pk2K5AdCGK%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;687&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;687&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;엔진 접근&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slate UI 시스템은 프로그래머에게 엔진과 에디터 직접 접근 권한을 제공하여 어떤 요구사항이든 수용할 수 있는 신기능과 툴 구현 작업을 쉽게 만들어 준다.&lt;/p&gt;</description>
      <category>⭐ Game Programming/Unreal Document</category>
      <category>슬레이트</category>
      <category>언리얼엔진</category>
      <author>테디슨</author>
      <guid isPermaLink="true">https://wecandev.tistory.com/224</guid>
      <comments>https://wecandev.tistory.com/224#entry224comment</comments>
      <pubDate>Wed, 26 Apr 2023 20:45:50 +0900</pubDate>
    </item>
    <item>
      <title>[게임수학] 17. 캐릭터: 게임에 생기를 불어넣는 기술</title>
      <link>https://wecandev.tistory.com/223</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;17.1 스켈레탈 애니메이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스켈레탈 애니메이션(Skeletal animation) : 가상의 뼈대인 본(Bone)을 캐릭터 메시에 심은 후, 해당 본의 움직임에 맞춰 캐릭터 메시가 변형되는 방식을 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본을 구성하는 3가지 정보&lt;/p&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;바인드포즈(Bindpose) : 최초의 배치 정보, 향후 본이 움직일 때 상대적인 변화량을 파악하는데 사용&lt;/li&gt;
&lt;li&gt;리깅(Rigging) : 메시에 본 정보를 추가한 후 본의 움직임에 따라 메시의 정점이 움직이도록 본과 정점을 연결하는 작업&lt;/li&gt;
&lt;li&gt;웨이트(Weight) : 한 정점에 영향을 주는 본들의 연결 정보&lt;/li&gt;
&lt;li&gt;게임 엔진은 시간에 따라 값의 변화를 저장한 키프레임(Keyframe) 데이터를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;17-1 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스켈레탈 애니메이션의 구현&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EPu7b/btsblD8gEIH/9Ks70gJTd5iog0MvjK1Ng0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EPu7b/btsblD8gEIH/9Ks70gJTd5iog0MvjK1Ng0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EPu7b/btsblD8gEIH/9Ks70gJTd5iog0MvjK1Ng0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/EPu7b/btsblD8gEIH/9Ks70gJTd5iog0MvjK1Ng0/img.gif&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;796&quot; height=&quot;626&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;17.2 트랜스폼 계층 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 트랜스폼과 자식 트랜스폼이 가져야 하는 규칙&lt;/p&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 트랜스폼 (Root Transform) : 계층 구조에서 부모가 없는 최상단 트랜스폼&lt;br /&gt;월드 트랜스폼 (World transform) : 계층 구조와 상관없이 자식이 속한 공간에서의 트랜스폼. 절대 트랜스폼(Absolute transform)&lt;br /&gt;로컬 트랜스폼 (Local transform) : 부모로 부터 상대적인 트랜스폼. 상대 트랜스폼(Relative transform)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜스폼 계층 구조의 변환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 - 나 - 자식 의 구조에서 사례별 정리&lt;/p&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;부모의 월드 트랜스폼, 나의 월드 트랜스폼, 자식의 월드 트랜스폼 순서대로 계산&lt;/li&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;나의 로컬 트랜스폼, 나의 원월드 트랜스폼, 자식의 월드 트랜스폼 순서대로 계산&lt;/li&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;나의 월드 트랜스폼, 나의 로컬 트랜스폼, 자식의 월드 트랜스폼 순서대로 계산&lt;/li&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;나의 로컬 트랜스폼을 월드 트랜스 폼으로 변경하고 새로운 부모로 부터의 로컬 트랜스폼을 계산&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬 트랜스폼으로부터 월드 트랜스폼의 계산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모의 월드 트랜스폼과 내 로컬 트랜스폼을 사용해 내 월드 트랜스폼을 구하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크기&lt;br /&gt;S'_world = s-&amp;gt; * s'-&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회전&lt;br /&gt;q'_world = q' &amp;middot; q&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동&lt;br /&gt;M_world = M_parent &amp;middot; M_local&lt;br /&gt;t'_world = (q &amp;middot; t'-&amp;gt;) * s-&amp;gt; + t&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;월드 트랜스폼으로부터 로컬 트랜스폼의 계산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모의 월드 트랜스폼 정보와 나의 월드 트랜스폼 정보를 비교해 나의 로컬 트랜스폼을 계산하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크기&lt;br /&gt;S^-1-&amp;gt; = (1/s_x, 1/s_y, 1/s_z)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회전&lt;br /&gt;q*&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동&lt;br /&gt;t^-1-&amp;gt; = (q* &amp;middot; t-&amp;gt;) * s^-1-&amp;gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;17-2 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계층 구조를 활용한 태양계 시스템&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o4oNb/btsbbrHBDel/CIRJ9j5yHbWWn9ZAyim5h1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o4oNb/btsbbrHBDel/CIRJ9j5yHbWWn9ZAyim5h1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o4oNb/btsbbrHBDel/CIRJ9j5yHbWWn9ZAyim5h1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/o4oNb/btsbbrHBDel/CIRJ9j5yHbWWn9ZAyim5h1/img.gif&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;796&quot; height=&quot;626&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐릭터 메시와 애니메이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐릭터를 구성하는 본의 계층 구조&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Root
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Plevis
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spine
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LeftArm&lt;/li&gt;
&lt;li&gt;RightArm&lt;/li&gt;
&lt;li&gt;Neck&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LeftLeg&lt;/li&gt;
&lt;li&gt;RightLeg&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;17-3 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐릭터 애니메이션의 구현&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lls4b/btsbmcbqhMz/dKYHCjeCMrzeQbFpDeRyeK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lls4b/btsbmcbqhMz/dKYHCjeCMrzeQbFpDeRyeK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lls4b/btsbmcbqhMz/dKYHCjeCMrzeQbFpDeRyeK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/lls4b/btsbmcbqhMz/dKYHCjeCMrzeQbFpDeRyeK/img.gif&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;796&quot; height=&quot;626&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;17.4 정리&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;트랜스폼의 계층구조&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  Book/이득우 게임수학</category>
      <category>게임수학</category>
      <category>이득우</category>
      <author>테디슨</author>
      <guid isPermaLink="true">https://wecandev.tistory.com/223</guid>
      <comments>https://wecandev.tistory.com/223#entry223comment</comments>
      <pubDate>Wed, 19 Apr 2023 21:03:07 +0900</pubDate>
    </item>
    <item>
      <title>[게임수학] 16. 사원수: 4차원 수로 설계한 3차원 회전</title>
      <link>https://wecandev.tistory.com/222</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사원수(Quaternion) : 4개의 독립된 체계로 구성된 4차원의 수집합, 2차원의 복소평면을 4차원 공간으로 확장한 개념&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;16.1 사원수 대수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사원수를 구성하는 세 허수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사원수는 3개의 허수부 i,j,k 로 구성되어있지만 허수단위 i와 같은 성질을 가진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사원수의 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사원수의 덧셈&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;q1 + q2 = (a1 + a2) + (b1 + b2)i + (c1 + c2)j + (d1 + d2)k&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;결합법칙 : q1 + (q2 + q3) = (q1 + q2) + q3
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;      = (a1 + a2 + a3) + (b1 + b2 + b3)i + (c1 + c2 + c3)j + (d1 + d2 + d3)k&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;교환법칙 : q1 + q2 = q2 + q1 = (a1 + a2) + (b1 + b2)i + (c1 + c2)j + (d1 + d2)k&lt;/li&gt;
&lt;li&gt;항등원 : (a + bi + cj + dk) + (0 + 0i + 0j + 0k) = (a + bi + cj + dk)&lt;/li&gt;
&lt;li&gt;역원 : (a + bi + cj + dk) + (-a - bi - cj - dk) = (0 + 0i + 0j + 0k)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사원수의 곱셈&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;q1 &amp;middot; q2 = (a1a2 - b1b2 - c1c2 - d1d2) + (a1b2 + a2b1 + c1d2 - d1c2)i + (a1c2 + a2c1 + b2d1 - b1d2)j + (a1d2 + a2d1 + b1c2 - c1b2)k&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;결합법칙 : q1 &amp;middot; (q2 &amp;middot; q3) = (q1 &amp;middot; q2) &amp;middot; q3&lt;/li&gt;
&lt;li&gt;분배법칙 : q1 &amp;middot; (q2 + q3) = q1 &amp;middot; q2 + q1 &amp;middot; q3&lt;/li&gt;
&lt;li&gt;항등원 : (a + bi + cj + dk) &amp;middot; (1 + 0i + 0j + 0k) = (a + bi + cj + dk)&lt;/li&gt;
&lt;li&gt;교환법칙은 성립하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사원수의 크기는 기호 | | 를 사용하여 표기한다 사원수의 크기를 노름(Norm)이라 부른다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;|q| = sqrt(a^2 + b^2 + c^2 + d^2)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사원수와 벡터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;q = w + xi + yj + zk = (w, v-&amp;gt;)&lt;br /&gt;q1 + q2 = (w1 + w2, v1-&amp;gt; + v2-&amp;gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;q1&amp;middot;q2 = (w1w2 - (v1-&amp;gt;&amp;middot;v2-&amp;gt;), w1v2-&amp;gt; + w2v1-&amp;gt; + v1 x v2)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순허수 사원수(Pure imaginary quaternion)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;xi + yj + zk = (0, v-&amp;gt;)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;16.2 사원수의 회전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;e^i&amp;theta; = cos &amp;theta; + i sin &amp;theta; = (cos &amp;theta;, sin &amp;theta;)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사원수와 오일러 공식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회전 사원수 (Rotation quaternion) : 4차원 공간에서 회전축 n-&amp;gt;을 중심으로 회전 변환하는 성질을 가지는 사원수 (cos &amp;theta;, sin &amp;theta;)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회전 사원수를 이용한 3차원 공간에서의 회전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회전 사원수와 순허수 사원수 벡터 v-&amp;gt;의 곱셈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v'-&amp;gt; = q &amp;middot; v-&amp;gt;&lt;br /&gt;= (cos &amp;theta;, sin &amp;theta; &amp;middot; n-&amp;gt;)&amp;middot;(0, v-&amp;gt;)&lt;br /&gt;= (-sin &amp;theta;(n-&amp;gt; &amp;middot; v-&amp;gt;), cos 0 v-&amp;gt; + sin 0(n-&amp;gt; x v-&amp;gt;))&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;16.3 사원수의 변환&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오일러 각에서 사원수로의 변환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;q = (cos 0/2, sin 0/2&amp;middot;x-&amp;gt;) = cos 0/2 + sin 0/2 i&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사원수에서 오일러 각으로의 전환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중략&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사원수에서 회전 변환 행렬로의 변환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;┌ 1-2(y^2+z^2) 2(xy-zw) 2(xz+yw) ┐&lt;br /&gt;│ 2(xy+zw) 1-2(x^2+z^2) 2(yz-xw) |&lt;br /&gt;└ 2(xz-yw) 2(yz+xw) 1-2(x^2+y^2) ┘&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회전 변환행렬에서 사원수로의 변환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중략&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;16.4 사원수의 보간&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사원수의 장점&lt;/p&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;4개의 숫자만 사용하여 저장 공간을 효율적으로 쓸 수 있음&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;저장 공간이 가장 작다. 공간의 수 3&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;li&gt;행렬
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저장 공간이 크다. 공간의 수 9&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;li&gt;사원수
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저장 공간이 작다. 공간의 수 4&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;h3 data-ke-size=&quot;size23&quot;&gt;16-1 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유로운 3차원 회전의 구현&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo2CqI/btr9Npw4BMh/0obuvczBmkaktS8YEH76E0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo2CqI/btr9Npw4BMh/0obuvczBmkaktS8YEH76E0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo2CqI/btr9Npw4BMh/0obuvczBmkaktS8YEH76E0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bo2CqI/btr9Npw4BMh/0obuvczBmkaktS8YEH76E0/img.gif&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;796&quot; height=&quot;626&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&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;사원수를 활용해 3차원 공간의 회전에 사용되는 수학&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  Book/이득우 게임수학</category>
      <category>게임수학</category>
      <category>이득우</category>
      <author>테디슨</author>
      <guid isPermaLink="true">https://wecandev.tistory.com/222</guid>
      <comments>https://wecandev.tistory.com/222#entry222comment</comments>
      <pubDate>Wed, 12 Apr 2023 21:16:21 +0900</pubDate>
    </item>
  </channel>
</rss>