<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>kyu_.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 13 Aug 2026 14:44:46 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>kyu_.log</title>
            <url>https://velog.velcdn.com/images/kyu_/profile/d8a6e0ba-1e19-4cc0-8662-c2f99d1d59b6/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. kyu_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kyu_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[TIL - CS, 알고리즘]]></title>
            <link>https://velog.io/@kyu_/TIL-CS-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@kyu_/TIL-CS-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Thu, 13 Aug 2026 14:44:46 GMT</pubDate>
            <description><![CDATA[<h1 id="cs">CS</h1>
<h2 id="복습">복습</h2>
<h3 id="c에서-소멸자에서-예외를-밖으로-던지면-안-되는-이유를-설명">C++에서 소멸자에서 예외를 밖으로 던지면 안 되는 이유를 설명</h3>
<p>이미 다른 예외를 처리하면서 스택을 되돌리는 중에 소멸자가 호출될 수 있다. &lt;&lt; 이게 핵심
예를들어 함수 안에서 예외가 발생하면 C++은 catch를 찾으면서 스택에 있던 지역 객체들을 정리함, 이 과정을 stack unwinding이라고 함</p>
<pre><code class="language-c++">void Foo(){
    SomeObject obj;
    throw std::runtime_error(&quot;error&quot;);
}</code></pre>
<p>여기서 예외가 발생하면 Foo()를 빠져나가기 전에 obj의 소멸자가 호출된다.
근데 그 소멸자가 또 예외를 밖으로 던지게 되면</p>
<pre><code>첫번째 예외처리중
stack unwinding
obj의 소멸자 호출
소멸자에서 두 번째 예외 발생
std::terminate()</code></pre><p>이미 예외하나가 전파되는 중인데 소멸자에서 또다른 예외가 밖으로 전파되는 상황이 위험한것
C++11 이후에는 일반적으로 소멸자가 기본적으로 noexcept 취급을 받는다.</p>
<h3 id="objecttrace-channel">Object/Trace Channel</h3>
<p>Object Channel : 이 물체가 무엇인지를 분류 (Pawn, Character, WorldDynamic)
Trace Channel : 이 트레이스가 어떤 종류의 검사인지 분류(Visibility, Camera)</p>
<p>대상 오브젝트는 Trace Channel에 대해 어떻게 반응할지 설정함(Block / Overlap / Ignore)</p>
<h3 id="castt는-언제-사용하는가">Cast<T>()는 언제 사용하는가</h3>
<pre><code class="language-c++">AMyPlayerState* MyPS = Cast&lt;AMyPlayerState&gt;(PlayerState);</code></pre>
<p><AMyPlayerState> -&gt; 어떤 타입으로 캐스팅할지
(PlayerState) -&gt; 캐스팅할 실제 UObject 포인터</p>
<p>NewObject<T>()랑 다름 여기는 T는 상속받는 타입이고 ()는 Outer잖아 구분잘하자</p>
<h3 id="gamemode와-gamestate의-차이">GameMode와 GameState의 차이</h3>
<p>GameMode는 서버 전용으로 게임의 규칙과 진행 로직을 담당하고, GameState는 모든 플레이어가 알아야 하는 공용 상태를 담아 서버에서 클라이언트로 복제한다.</p>
<h3 id="포워드디퍼드-렌더링의-차이와-디퍼드-렌더링에서-g-buffer를-왜쓰는가">포워드/디퍼드 렌더링의 차이와 디퍼드 렌더링에서 G-Buffer를 왜쓰는가?</h3>
<p>포워드 렌더링은 물체를 그릴 때 그 물체에 영향을 주는 조명을 같이 계산하는 방식이고, 디퍼드 렌더링은 먼저 화면에 보이는 표면의 색상, 노멀, 깊이 등의 정보를 G-Buffer에 저장한 뒤 별도 패스에서 조명을 계산하는 방식, 많은 동적 조명을 처리하기 유리하지만 G-Buffer 때문에 메모리와 대역폭을 많이 사용하고 투명 물체 처리가 어렵다는 단점이 있다.</p>
<h3 id="프로세스와-스레드-메모리-공유-관점에서-설명">프로세스와 스레드 메모리 공유 관점에서 설명</h3>
<p>프로세스는 독립적인 가상 주소 공간을 가지는 실행중인 프로그램, 프로세스끼리는 기본적으로 서로의 힙이나 전역변수에 직접 접근하지 못함
스레드는 그 프로세스 안에서 실제 코드를 실행하는 작업 단위, 같은 프로세스에 속한 스레드끼리는 같은 메모리 공간을 쓰기때문에 코드 영역, 전역/정적 데이터 영역, 힙 영역을 공유함. 하지만 각 스레드는 서로 다른 함수를 실행할 수 있으므로 스택, 레지스터값, 프로그램 카운터 같은 실행상태는 따로 가짐</p>
<p>컨텍스트 스위칭할때 같은 프로세스의 다른 스레드로 바뀌는 경우에는 주로 현재 스레드의 레지스터, 스택 포인터, 프로그램 카운터 같은 실행 상태를 저장하고 다음 스레드의 상태를 복원하면 된다.
반면에 다른 프로세스에 속한 스레드로 전환하면 실행 상태뿐 아니라 주소 공간 자체도 바뀌어서 Thread상태 교체, 페이지 테이블 기준 변경, TLB에 영향 때문에 일반적으로 비용이 더 크다.</p>
<p>여기서 실행중이던 스레드 정보 이런것들은 TCB라고 해서 커널 자료구조인 스레드 제어블록에 저장된다.
당연히 커널이기 때문에 운영체제가 관리</p>
<h3 id="데이터-레이스와-레이스-컨디션의-차이-및-해결법">데이터 레이스와 레이스 컨디션의 차이 및 해결법</h3>
<p>데이터 레이스는 여러 스레드가 같은 메모리에 동시 접근하고 하나이상이 쓰기 작업이며 적절한 동기화가 없을때 C++에서는 정의되지 않은 동작이 터질 수 있음 mutex, atomic등으로 공유 메모리를 안전하게 만듬
레이스 컨디션은 스레드들의 실행 순서나 타이밍에 따라 결과가 달라지는 더 넓은 문제, 단순히 순서만 정하는것이 아니라, 문제가 되는 연산 전체를 하나의 원자적인 흐름으로 보호하거나 상태 전이를 동기화해서 해결</p>
<h3 id="uobject와-aactor의-차이">UObject와 AActor의 차이</h3>
<p>UObject는 언리얼 오브젝트 시스템의 기본 클래스, 데이터 객체/에셋/ 여러 시스템 객체의 기반으로 사용, 기본적으로 월드에 배치되거나 트랜스폼을 가지는 객체는 아님
AActor는 UObject를 상속하고 월드에 배치되거나 Spawn될 수 있는 게임 오브젝트, 루트 컴포넌트를 통해 트랜스폼을 가질 수 있고, Beginplay나 Tick 네트워크 복제같은 월드 기능을 사용할 수 있다.</p>
<h3 id="uobject의-outer가-무엇이고-outer와-uproperty강한-참조의-차이를-설명">UObject의 Outer가 무엇이고, Outer와 UPROPERTY/강한 참조의 차이를 설명</h3>
<p>Outer는 이 UObject는 누구 소속인가? 누구 밑에 속한 객체로 취급할지
UPROPERTY/TObjectPtr 강한 참조 = 이 UObject를 아직 사용하고 있으니 GC가 추적해야 한다.</p>
<h1 id="알고리즘">알고리즘</h1>
<h2 id="개념">개념</h2>
<h3 id="list에서-sortlistbegin-listend를-못쓰는-이유">list에서 sort(list.begin(), list.end())를 못쓰는 이유</h3>
<p>우선 end()는 이터레이터 반복자를 원소 끝 바로전에 접근하는것
sort를 못쓰는 이유는 sort는 랜덤접근을 해야하는데 list는 인덱스를 이용한 랜덤 접근이 불가능하다.
list.sort()로 정렬해야됨</p>
<h3 id="cbegin--cend">cbegin / cend</h3>
<p>rbegin/rend : 역순순회용 이터
cbegin/cend : const를 먹여서 이터레이터로 원소를 읽을수는 있지만 수정은 불가하게 함</p>
<h3 id="map의-lower_bound-upper_bound">map의 lower_bound, upper_bound</h3>
<p>같은키가 들어갈 수 있을까? insert로 넣으면 값이 안들어 갈수 있음, 그냥 단순 map[key] = newValue 이렇게 넣으면 newValue로 갱신
키 찾을때도 find말고 map[key] 이렇게 찾으면 map[key] = 0 으로 값이 들어가게됨 되도록이면 find로 찾자
lower_bound(x) : x 이상인 첫번째 원소
upper_bound(x) : x 이하인 첫번쨰 원소</p>
<h3 id="set">set</h3>
<p>set도 내부적으로 레드 블랙트리 기반이고 기본적으로 정렬된 상태유지, 값만 저장하고 값 정렬
그래서 삽입/삭제/탐색이 O(log N) find, count다 map과 똑같이 동작</p>
<h3 id="multiset-multimap">multiset, multimap</h3>
<p>그냥 중복 되고 안되고 차이, 솔직히 잘안씀, 내부적으로 얘네도 레드블랙트리인거만 알고있자</p>
<h3 id="우선순위큐">우선순위큐</h3>
<p>기본적으로 최대힙이라서 최대값이 먼저나옴</p>
<pre><code class="language-c++">priority_queue&lt;int, vector&lt;int&gt;, greater&lt;int&gt;&gt; pq;</code></pre>
<p>최소힙은 이렇게 greater붙여서 구현</p>
<h3 id="partial_sum">partial_sum</h3>
<p>누적합 구하는 함수</p>
<pre><code class="language-c++">#include &lt;numeric&gt;

vector&lt;int&gt; v = {1, 2, 3, 4};
vector&lt;int&gt; psum(4);

partial_sum(v.begin(), v.end(), psum.begin());
// psum = {1, 3, 6, 10};</code></pre>
<h3 id="삽입정렬">삽입정렬</h3>
<p>값을 하나 잡고 앞쪽이 정렬되어있다고 생각하고 비교한다. 뒤에서부터 비교하면서 더 큰 값들을 한칸씩 오른쪽으로 옮기면서 정렬, 정렬이 거의 다되어있을때 빠르다. 평균 N제곱이고 최선은 O(N)</p>
<h3 id="해시충돌">해시충돌</h3>
<p>체이닝, 서로 다른 키들이 같은 해시 인덱스로 충돌했을때 그 벜시안에 여러 원소들을 연결해서 저장하는 방식
개방 주소법은 다른 빈 버킷을 찾아서 이동하는 방식</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - CS복습]]></title>
            <link>https://velog.io/@kyu_/TIL-CS%EB%B3%B5%EC%8A%B5-1bquvh0e</link>
            <guid>https://velog.io/@kyu_/TIL-CS%EB%B3%B5%EC%8A%B5-1bquvh0e</guid>
            <pubDate>Wed, 12 Aug 2026 16:29:15 GMT</pubDate>
            <description><![CDATA[<h1 id="cs">CS</h1>
<h2 id="복습">복습</h2>
<h3 id="프로세스와-스레드의-차이를-메모리-공유-관점에서-설명">프로세스와 스레드의 차이를 메모리 공유 관점에서 설명</h3>
<p>프로세스는 각자 독립된 메모리공간을 가진 실행중인 프로그램
스레드는 하나의 프로세스 안에서 실행되는 작업 흐름, 같은 프로세스의 스레드끼리는 전역/정적, 힙을 공유한다.
각 스레드는 스택과 레지스터를 따로 가진다.
같은 프로세스 내 스레드 전환에는 주로 레지스터, 프로그램 카운터, 스택 포인터같은 스레드 상태를 교체하면 되지만, 다른 프로세스의 스레드로 전환하면 주소 공간도 달라지므로 페이지 테이블 기준도 변경해야 한다. 이 과정에서 TLB캐시에도 영향을 줄 수 있어 일반적으로 비용이 더 크다.</p>
<h3 id="drawcall이-무엇이고-많이-호출하면-왜-느려지는가">DrawCall이 무엇이고 많이 호출하면 왜 느려지는가?</h3>
<p>CPU가 GPU에게 특정 메시를 특정 머티리얼/셰이더/렌더 상태로 그리라고 제출하는 명령
많이 호출하면 성능 저하가 오는 이유는 Draw Call마다 CPu가 렌더 상태를 설정하고 명령을 제출하는 비용이 반복되므로 GPU보다 CPu에서 병목이 걸릴 수 있다.
GPU Instancing : 같은 메시와 같은 머티리얼을 사용하는 여러 객체를 한번의 Draw Call로 그림, 위치, 회전, 크기 같은 값만 인스턴스 데이터로 따로 넘김
Batching : 여러 렌더링 작업을 묶어서 Draw Call의 수를 줄이는 방법
gpu인스턴싱은 배칭의 한 방식이라고 보면 됨</p>
<h3 id="c에서-부모-클래스-포인터가-실제로-자식-객체를-가리키고-있을-때-다운캐스팅이-필요하다면-어떤-캐스팅을-사용하는-게-안전할까">C++에서 부모 클래스 포인터가 실제로 자식 객체를 가리키고 있을 때 다운캐스팅이 필요하다면 어떤 캐스팅을 사용하는 게 안전할까?</h3>
<p>dynamic_cast는 RTTI(런타임 타입 검사)를 하기 때문에 다형적 계층에서 안전한 다운캐스팅이 필요할 때 적합하다.
static_cast는 런타임 타입 검사를 안하기 때문에 실제 객체 타입이 맞지 않으넫도 캐스팅이 성립할 수 있고, 이후 잘못 사용하면 정의하지 않은 동작으로 이어질 수 있음</p>
<p>dynamic_cast로 다운캐스팅하려면 일반적으로 부모 클래스가 다형적이어야 함, 하나이상의 virtual함수가 있어야 하고 보통 virtual 소멸자를 두는 경우가 많다.</p>
<h3 id="데드락deadlock이-무엇인지-설명하고-대표적인-발생-상황을-하나-예로-들어보자">데드락(Deadlock)이 무엇인지 설명하고, 대표적인 발생 상황을 하나 예로 들어보자</h3>
<p>데드락은 여러 스레드가 서로 상대방이 가진 락이 해제되기를 기다리면서 아무도 진행하지 못하는 상태
대표적으로 A가 Lock1을 잡고 Lock2를 기다리고, B가 Lock2를 잡고 Lock1을 기다리는 경우입니다
예방하려면 프로그램 전체에서 락 획득 순서를 일관되게 정하거나, 여러 락을 안전하게 획득하는 std::scoped_lock 등을 사용할 수 있다.</p>
<p>락을 획득하는 순서를 전역적으로 통일한다고 표현하는 게 정확하다 예를 들어 모든 코드에서 반드시 Mutex1 → Mutex2 순서로만 락을 잡게 하면 서로 반대 방향으로 기다리는 상황을 막을 수 있다.</p>
<h3 id="스택-메모리와-힙-메모리의-차이를-설명">스택 메모리와 힙 메모리의 차이를 설명</h3>
<p>스택 : 함수 호출과 함께 스택 프레임 생성, 지역변수/매개변수 등이 저장되고 함수 종료 시 자동 정리
힙 : 런타임에 동적으로 할당하는 메모리, 수명을 개발자가 직접 관리하거나 RAII 객체가 관리
전역/정적 영역 : 전역 변수, static 변수등이 위치하고 일반적으로 프로그램 수명과 함께 유지</p>
<h3 id="프로세스-메모리">프로세스 메모리</h3>
<p>코드 영역(Text) : 컴파일된 함수의 기계어 코드
데이터 영역 : 전역, static변수, 초기값이 있는 전역/정적 변수 -&gt; Data, 0또는 초기화되지 않은 전역/정적 변수 -&gt; BSS
힙 : new/malloc 등으로 동적 할당
스택 : 함수 호출 프레임, 지역 변수, 매개변수 등</p>
<h3 id="uobject와-aactor의-차이">UObject와 AActor의 차이</h3>
<p>UObject는 언리얼 오브젝트 시스템의 기본 클래스, 데이터 객체나 에셋, 여러 시스템 객체에 사용된다. 기본적으로 월드에 배치되거나 Transform을 가지는 객체가 아니다.
AActor는 UObject를 상속하며 월드에 Spawn되거나 배치될 수 있는 게임 오브젝트이다. RootComponent를 통해 위치,회전,크기를 가지며 BeginPlay, Tick, 네트워크 복제관련 기능을 사용할 수 있다.</p>
<h3 id="actorcomponent와-scenecomponent의-차이">ActorComponent와 SceneComponent의 차이</h3>
<p>UActorComponent는 Actor에 기능을 추가하는 기본 컴포넌트, 자체적인 위치/회전/크기, 즉 Transform이 없다.
USceneComponent는 UActorComponent를 상속하고 Transform을 가질 수 있다. 또 다른 SceneComponent에 붙어서 부모/자식 계층구조를 만들 수 있다. 부모의 Transform변화도 따라갈 수 있다.
그래서 Actor의 RootComponent도 ScencComponent계열이어야 한다. Actor의 월드 위치/회전/크기 기준점 역할을 해야 하므로</p>
<h3 id="gamemode와-gamestate의-차이">GameMode와 GameState의 차이</h3>
<p>GameMode는 게임의 규칙을 담는 곳이다. 멀티플레이에서는 서버에서만 존재한다. ex) 사망 규칙, 룰 등등
GameState는 게임에서 모든 플레이어들이 알아야하는 상태를 저장하는 곳이다. 멀티플레이에서는 서버는 권한을 가지고 값을 관리하고, 필요한 상태가 클라이언트들에게 복제된다. ex) 점수판, 시간등</p>
<h3 id="reliable과-unrealiable의-차이">Reliable과 Unrealiable의 차이</h3>
<p>Reliable RPC는 반드시 전달되어야 하는 RPC로, 유실되면 재전송함. UnReliable RPC는 유실되어도 재전송하지 않음. 중요한 이벤트에는 Reliable을 쓰고 자주 발생하며 일부 유실되어도 상관없는 RPC에는 UnReliable을 사용한다. 모든 RPC를 Reliable하게 사용하면 재전송과 대기로 인해 네트워크 지연과 부하가 증가할 수 있다.</p>
<h3 id="tobjectptr-tweakobjectptr-tsoftobjectptr">TObjectPtr, TWeakObjectPtr, TSoftObjectPtr</h3>
<p>TObjectPtr : 이미 메모리에 로드된 UObject를 강하게 참조하는 포인터, GC가 이 참조를 인식해서 강한참조가 살아있는한 대상이 GC로 수집되지 않게 한다.
TWeakObjectPtr : 이미 존재하는 UObject를 소유하지 않고 관측만함. 대상이 GC로 사라질 수 있으므로 사용전에 IsValid()같은 검사가 필요
TSoftObjectPtr : 객에셋 경로 기반의 소프트 레퍼런스를 가진다. 아직 메모리에 없어도 가리킬 수 있고, 필요할때 동기/비동기 로드할 수 있다.</p>
<h3 id="tsubclassof와-uclass의-차이">TSubclassOf와 UClass*의 차이</h3>
<p>TSubclassOf<T>는 클래스 정보를 저장하는 타입</p>
<pre><code class="language-c++">TSubclassof&lt;AProjectile&gt; ProjectileClass;</code></pre>
<p>이렇게 해놓으면 AProjectile을 상속받은 모든 클래스들이 들어갈 수 있음
UClass*로 클래스 정보를 가리킬 수 있지만 어떤 계열의 클래스인지 타입 수준에서 제한하지 않음</p>
<pre><code class="language-c++">UPROPERTY(EditAnywhere)
TSubclassOf&lt;AProjectile&gt; ProjectileClass;</code></pre>
<p>TSubclassof로 해두면 에디터에서도 AProjectile계열 클래스만 선택할 수 있어서 안전하다.</p>
<h3 id="castt는-언제사용할까">Cast<T>()는 언제사용할까?</h3>
<p>Cast<T>()는 UObject계열 객체를 원하는 타입으로 안전하게 변환할때 사용한다.
캐스팅에 실패하면 nullptr을 반환함, Cast<T>()를 쓰는 이유는 언리얼의 리플렉션/타입 시스템을 이용해서 UObject계열 타입을 검사하기 때문</p>
<h3 id="isvaliduobject를-사용하는-이유">IsValid(UObject)를 사용하는 이유</h3>
<p>IsValid는 UObject가 nullptr이 아닌지, 이미 파괴/삭제 대상으로 처리된 상태는 아닌지 -&gt; 이 두가지를 확인한다. 지금 안전하게 사용할 수 있는 UObject인지를 판단하는데 사용</p>
<p>nullptr비교는 포인터 주소가 비어있는지만 확인하는데 UObject는 포인터 값이 남아 있어도 이미 파괴되었거나 유효하지않은 상태 (Destory는 호출되었지만 GC가 아직 수거하지 않은상태) 일수 있다. IsValid는 이런 상태까지 확인을 하기 때문에 IsValid를 사용해야 한다.</p>
<h3 id="newobject">NewObject</h3>
<p>CreateDefaultSubobject는 클래스가 기본적으로 항상 가져야 하는 서브오브젝트를 생성자에서 만듬
NewObject는 런타임 중 필요할 때 UObject를 동적으로 생성할때 사용한다. 월드에 존재하는 Actor를 생성할때는 SpawnActor사용</p>
<h3 id="oncomponentbeginoverlap">OnComponentBeginOverlap</h3>
<p>두 컴포넌트가 Overlap 반응을 하도록 설정되어있고, 실제로 겹치기 시작하면 호출, 물리적으로 서로 막지는 않음</p>
<h3 id="oncomponenthit">OnComponentHit</h3>
<p>서로 Block충돌이 발생했을때 호출, 실제 물리 충돌이나 이동 중 막힘과 관련된 이벤트</p>
<h3 id="nocollision">NoCollision</h3>
<p>충돌 시스템에 참여하지 않음
트레이스, 오버랩, 물리 충돌 전부 안 함</p>
<h3 id="queryonly">QueryOnly</h3>
<p>트레이스나 오버랩 같은 쿼리 검사에는 참여
물리 시뮬레이션 충돌은 하지 않음
예: 공격 판정 영역, 아이템 감지</p>
<h3 id="physicsonly">PhysicsOnly</h3>
<p>물리 시뮬레이션에는 참여
트레이스/오버랩 같은 쿼리 검사에는 참여하지 않음</p>
<h3 id="queryandphysics">QueryAndPhysics</h3>
<p>쿼리와 물리 둘 다 참여</p>
<h3 id="object-channel-vs-trace-channel">Object Channel vs Trace Channel</h3>
<p>Object Channel은 오브젝트 자신이 어떤 종류인지 나타내는 분류값
나는 Pawn이다 같은 식으로 객체에 붙는 이름표</p>
<p>Trace Channel은 라인 트레이스나 스윕 같은 쿼리가 어떤 기준으로 검사할지 정하는 채널
예를들어 총알 라인트레이스를 Visibility채널로 쐈다면, 각 오브젝트는 그 Visibility Trace Channel에 대해 Block / Overlap / Ignore 중 어떻게 반응할지를 정해둘 수 있다.</p>
<h3 id="udataasset과-datatable의-차이">UDataAsset과 DataTable의 차이</h3>
<p>UDataAsset은 하나의 논리적인 설정 묶음을 에셋 하나로 관리할 때 좋음, 무기 설정, 캐릭터설정처럼 데이터 종류가 하나의 객체 단위로 묶이는 경우
DataTable의 경우 행으로 구성된 표형태 데이터, 스텟처럼 같은 구조의 데이터를 대량으로 관리할때 좋음, 기획자가 CSV/에디터에서 수치 수정하기 편함</p>
<h3 id="abilitysystemcomponent">AbilitySystemComponent</h3>
<p>GAS의 중심 역할을 하는 컴포넌트 GA 부여/활성화, GE 적용/제거, GameplayTag관리, AttributeSet 연결 및 속성 변화 처리, 네트워크 복제와 예측 처리 등</p>
<h3 id="gas에서-owner-actor와-avatar-actor">GAS에서 Owner Actor와 Avatar Actor</h3>
<p>Owner Actor는 ASC의 소유 주체, 능력, AttributeSet같은 정보를 지속적으로 보관하는 쪽, 멀티플레이에서는 PlayerState에 두는 경우가 많음
Avatar Actor는 현재 월드에서 실제로 움직이고 능력을 사용하는 Pawn/Character, 리스폰하면 Avatar는 새 Character로 바뀔 수 있음
플레이어가 죽고 다시 살아났을때 능력이나 스텟을 유지하고 싶다면 PlayerState가 Owner Actor고 캐릭터가 Avatar Actor가 된다 (우리 프로젝트)</p>
<h3 id="subsystem이-무엇이고-왜-사용하는가">Subsystem이 무엇이고 왜 사용하는가??</h3>
<p>Subsystem은 GameInstance나 World같은 특정 생명주기에 맞춰 엔진이 자동으로 생성하고 관리하는 기능 객체, 여러 곳에서 사용하는 기능을 한 클래스에 몰아넣지 않고 분리해서 결합도를 낮추고 유지보수성을 높이는데 사용한다. (세이브/로드, 에셋 관리, 매치메이킹)</p>
<h3 id="c과-블루프린트의-차이-각자-어느-경우에-사용하는가">C++과 블루프린트의 차이 각자 어느 경우에 사용하는가</h3>
<p>C++은 성능이 중요한 반복 로직, 복잡한 시스템, 기반 기능, 수학 계산등
블루프린트는 UI연결, 애니메이션 이벤트, 이펙트/사운드 등
블루프린트는 언리얼 VM을 한번 거치기때문에 C++보다 비교적 느려서 이런 차이가 생긴다.</p>
<h3 id="uenum과-ustruct는-각각-어떤-용도로-쓰이는가">UENUM과 USTRUCT는 각각 어떤 용도로 쓰이는가?</h3>
<p>UENUM은 서로 구분되는 상태나 종류를 이름있는 값으로 정의할때 사용한다. 내부적으로는 정수값으로 표현되지만 블루프린트나 에디터에서 드롭다운에서는 정수값이 아닌 설정값으로 나오게 된다.</p>
<p>USTRUCT는 구조체고 관련된 여러 데이터를 하나의 타입으로 묶을때 사용한다. 아이템 정보나 상점 데이터같은 여러값을 하나로 묶을때 사용한다.</p>
<h3 id="cdoclass-default-object란">CDO(Class Default Object)란?</h3>
<p>CDO는 클래스별로 하나 존재하는 기본값 템플릿 객체. 클래스가 준비될 때 생성자가 실행되면서 CDO의 기본값과 기본 서브오브젝트가 구성되고, 블루프린트에서 수정한 기본값도 해당 클래스의 CDO에 저장된다. 이후 새로운 인스턴스를 생성할때 CDO의 값을 기준으로 초기화한다.</p>
<h3 id="persistent-level">Persistent Level</h3>
<p>UWorld는 게임 세계 전체를 관리하는 객체이고, Level은 그 World를 구성하는 Actor나 맵 데이터의 묶음이다. Persistent Level은 기준이 되는 항상 로드된 Level이고, Sub Level은 필요에 따라 추가로 로드하거나 언로드할 수 있는 레벨이다.</p>
<h3 id="buildcs">Build.cs</h3>
<p>Unreal Build Tool이 읽는 모듈 빌드 설정 파일
어떤 언리얼 모듈에 의존하는지(UMG, Core, Engine), 외부 라이브러리나 헤더 경로, 컴파일 옵션, 전처리기 정의등</p>
<h3 id="ubt-uht-uat">UBT, UHT, UAT</h3>
<p>UBT : C++ 모듈과 타깃을 어떻게 컴파일할지 관리, Build.cs와 Target.cs를 읽는다.
UHT : UCLASS, UPROPERTY, UFUNCTION, USTRUCT같은 리플렉션 매크로 분석, .generated.h, 생성 코드등을 만들어줌, GENERATED_BODY도 매크로중 하나, UHT가 이런 매크로들을 읽고 정보를 얻어서 .generated.h라는 자동 생성 코드를 만듬
UAT : Unreal Automation Tool, 빌드/Cook/Package/배포같은 자동화 작업 담당, 패키징시 많이 관여함</p>
<h3 id="event-graph-vs-anim-graph">Event graph vs Anim graph</h3>
<p>Event Graph : 애니메이션 판단에 필요한 값을 갱신하는 곳(블루프린트 노드)
Anim Graph : Event Graph에서 갱신한 값을 이용해서 실제 최종 애니메이션 포즈를 만드는곳 (Idle &lt;-&gt; Run 상태 전환, BS, State Machine등)</p>
<h3 id="애니메이션-montage와-상태-머신state-machine-애니메이션은-각각-어떤-상황에서-사용하는-게-좋은지-설명">애니메이션 Montage와 상태 머신(State Machine) 애니메이션은 각각 어떤 상황에서 사용하는 게 좋은지 설명</h3>
<p>Montage : 특정 행동이 발생했을때 끼워넣는 애니메이션 (공격, 피격, 재장전 등)
State Machine : 계속 유지되고 전환되는 기본 상태</p>
<h3 id="root-motion과-in-place-애니메이션의-차이를-설명">Root Motion과 In-Place 애니메이션의 차이를 설명</h3>
<p>RootMotion은 애니메이션의 RootBone 이동/회전 값을 실제 캐릭터 이동에 반영
In-Place는 애니메이션은 제자리에서 실행, 실제 이동은 CharacterMovementComponent나 코드에서 담당</p>
<h3 id="tarray와-vector의-차이">TArray와 vector의 차이</h3>
<p>둘다 메모리연속성, 동적할당의 공통점이 있고 TArray는 UPROPERTY와 함께 사용하면 리플렉션, 직렬화, 복제같은 언리얼 시스템과 연동가능</p>
<h3 id="언리얼-리플렉션-시스템">언리얼 리플렉션 시스템</h3>
<p>실행중에 클래스, 프로퍼티, 함수등의 타입 정보와 메타데이터를 조회할 수 있게하는 시스템. UHT가 매크로를 읽어서 자동생성코드만들고 GENERATED_BODY를 통해 그 코드가 클래스에 연결됨. 이를 기반으로 블루프린트 노출, 직렬화, 복제, 에디터 편집등의 기능이 동작한다.</p>
<h3 id="uobject의-outer는-무엇인가">UObject의 Outer는 무엇인가?</h3>
<p>Outer란 UObject가 어떤 객체에 소속되어 있는지를 나타내는 관계</p>
<pre><code class="language-c++">UInventoryItem* Item = NewObject&lt;UInventoryItem&gt;(PlayerCharacter);</code></pre>
<p>여기서는 PlayerCharacter가 Item의 Outer다. Item은 PlayerCharacter라는 컨텍스트/소속 안에 있는 객체다.
객체의 소속 관계 표현, 객체 이름 경로 구성</p>
<p>여기서 NewObject의 &lt;&gt;는 어떤 타입의 객체를 새로 만들것인가를 정의하고 ()는 Outer를 누구로 할것인가를 정의한다.
중요한건 Outer는 강한 참조가 아니다. 다시말해 PlayerCharacter가 살아있는 동안에 무조건 Item이 살아있음을 보장할수는 없다. 보통 계속사용할거면 캐릭터에 강한참조로 붙임</p>
<h3 id="actor의-생명주기">Actor의 생명주기</h3>
<p>생성자에서 기본값과 컴포넌트를 구성하고, OnConstrcution에서 배치/스폰 후 구성을 조정
게임 시작시 Beginplay가 호출되고, Tick이 활성화되어있으면 Tick을돌고, 제거되거나 레벨이 끝날때 Endplay가 호출되고 이후 UObject 시스템에 의해 최종적으로 정리된다.</p>
<h3 id="포워드-렌더링-디퍼드-렌더링-차이">포워드 렌더링, 디퍼드 렌더링 차이</h3>
<p>포워드 렌더링은 물체를 그리면서 바로 조명 계산을 하느냐이고
디퍼드 렌더링은 화면에 뭐가 보이는지 먼저 정리하고 조명을 나중에 한꺼번에 계산한다.</p>
<p>디퍼드는 동적 조명이 많은 경우에 물체마다 조명 계산을 반복하지 않고 화면에 보이는 최종적인 픽셀을 기준으로 조명처리가 가능해서 조명이 많은경우 유리하다. 단점은 G-Buffer를 여러장 사용해서 메모리/대역폭을 많이 쓰고 투명 물체 처리가 까다롭다.</p>
<h3 id="버텍스-픽셀-셰이더">버텍스, 픽셀 셰이더</h3>
<p>버텍스 셰이더 : 어디에 그릴지, 각 정점이 화면의 어디에 위치해야 하는지 계산, 그 다음 정점들이 삼각형을 만들고 화면의 픽셀들로 채워지면
픽셀 셰이더 : 무슨 색으로 그릴지, 각 픽셀들이 무슨 색으로 보일지 계산</p>
<h3 id="댕글링-포인터">댕글링 포인터</h3>
<p>객체나 메모리는 이미 해제되었는데 그 주소를 가리키는 포인터가 아직 남아있는 상태
스마트 포인터를 사용하거나, RAII로 객체 수명을 명확하게 관리, raw pointer를 직접 해제했다면 nullptr로 초기화</p>
<h3 id="몬스터가-1만-마리-있고-이-몬스터들이-모두-플레이어의-체력-같은-값을-변경하려고-한다면-스레드-구조를-어떻게-설계하는-게-좋을까">몬스터가 1만 마리 있고, 이 몬스터들이 모두 플레이어의 체력 같은 값을 변경하려고 한다면 스레드 구조를 어떻게 설계하는 게 좋을까?</h3>
<p>워커 스레드들이 플레이어의 공유 상태를 직접 수정하지 않도록 하는게 핵심
몬스터마다 스레드를 하나씩 만드는 게 아니라 고정 크기 스레드 풀을 두고 몬스터들을 작업 단위로 나눠 병렬 처리, 워커 스레드는 플레이어 상태를 직접 수정하지 않고 공격 결과만 로컬 버퍼에 모은 뒤, 작업이 끝나면 게임 스레드에서 결과를 취합해 플레이어 상태에 반영하는 방식으로 설계할 수 있다.</p>
<h3 id="스레드풀이-무엇이고-매작업마다-새로운-스레드를-생성하는-것-보다-왜-좋은지-설명">스레드풀이 무엇이고, 매작업마다 새로운 스레드를 생성하는 것 보다 왜 좋은지 설명</h3>
<p>스레드풀은 미리 정해진 수의 스레드를 생성해놓고, 작업이 들어오면 작업 큐에서 꺼내 처리하는 구조, 매 작업마다 스레드를 생성/삭제하는 비용을 줄일 수 있고, 스레드 수를 제한해서 불필요한 컨텍스트 스위칭을 줄일 수 있다.</p>
<h3 id="언리얼-대표-스레드">언리얼 대표 스레드</h3>
<p>Game Thread: 게임 로직, Actor Tick, 대부분의 UObject 처리
Render Thread: 렌더링 명령 준비 및 렌더링 관련 처리
RHI Thread: Render Hardware Interface 쪽 작업, 즉 실제 그래픽 API 명령 처리에 가까운 역할</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DX11 스터디 - 수학 개념 공부]]></title>
            <link>https://velog.io/@kyu_/DX11-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@kyu_/DX11-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Fri, 07 Aug 2026 11:27:26 GMT</pubDate>
            <description><![CDATA[<h1 id="directx-11-스터디">DirectX 11 스터디</h1>
<h2 id="수학-개념들">수학 개념들</h2>
<h3 id="벡터">벡터</h3>
<p>(x, y, z)처럼 위치나 방향을 나타냄</p>
<ul>
<li>덧셈/뺄셈 : 이동, 두 점 사이 방향</li>
<li>길이 : 거리</li>
<li>내적 : 두 방향이 얼마나 같은쪽을 보는지 -&gt; 조명 밝기, 앞/뒤 판정</li>
<li>외적 : 두 벡터에 수직인 방향 -&gt; 법선, 좌표축 생성</li>
</ul>
<h3 id="행렬">행렬</h3>
<p>여러 점을 한 번에 변환하는 규칙표, DX에서는 보통 4x4를 사용</p>
<ul>
<li>이동 (Translation)</li>
<li>회전 (Rotation)</li>
<li>크기 변경 (Scale)</li>
<li>행렬 곱 순서가 중요 -&gt; <code>회전 후 이동</code>과 <code>이동 후 회전</code>은 결과가 다름</li>
</ul>
<h3 id="좌표-공간과-변환-체인">좌표 공간과 변환 체인</h3>
<pre><code>Local/Object -&gt; World -&gt; View -&gt; Projection -&gt; Screen</code></pre><ul>
<li>Local : 모델 자체 기준 좌표</li>
<li>World : 게임 세계 안의 위치</li>
<li>View : 카메라 기준으로 본 좌표</li>
<li>Projection : 3D를 2D화면처럼 보이게 함</li>
<li>Screen : 실제 픽셀 위치</li>
</ul>
<h3 id="법선-벡터">법선 벡터</h3>
<p>면에 수직인 방향 <code>이 면이 어디를 향하는가</code>를 알려줌</p>
<ul>
<li>빛과 법선이 마주보면 밝음</li>
<li>직각이면 어두움</li>
<li>반대면이면 빛을 거의 못 받음</li>
</ul>
<h3 id="삼각-함수">삼각 함수</h3>
<ul>
<li>sin/cos : 원운동/회전</li>
<li>라디안 : DX 회전 함수가 자주사용</li>
</ul>
<h3 id="투영과-깊이">투영과 깊이</h3>
<ul>
<li>원근 투영 : 멀수록 작아 보임</li>
<li>깊이 버퍼 : 가까운 물체가 먼 물체를 가림</li>
<li>Near/Far plane : 카메라가 볼 수 있는 최소/최대 거리</li>
</ul>
<h3 id="내적">내적</h3>
<p>| 두 방향이 얼마나 같은 쪽을 보는지를 숫자 하나로 만드는 연산, 조명 계산의 핵심</p>
<p>내적은 두 벡터를 받아 숫자 하나를 돌려주는 연산으로, 그 숫자는 두 방향이 얼마나 같은 쪽을 보는지를 나타냄, 두 벡터가 정규화 되어있다면 두 방향 사이 각도의 코사인값(-1 ~ 1)과 같다.
렌더러는 이면이 얼마나 빛을 받는가를 매 프레임 계산해야 하는데 내적은 이 마주보는 정도를 숫자하나로 압축해주므로 각도를 구하지 않고도 밝기를 바로 계산할 수 있다.</p>
<p>머티리얼 에디터의 Dot Product노드가 그대로 이 연산, FVector::DotProduct도 이 연산</p>
<h3 id="노멀">노멀</h3>
<p>| 표면이 어느쪽을 향하는지 나타내는 수직 방향 벡터, 조명 계산의 재료</p>
<p>노멀은 표면에 수직으로 꽂힌 방향 벡터로 &#39;이 표면이 어느쪽을 향해 있는가&#39;를 나타냄, 보통 길이1로 정규화
빛이 얼마나 밝게 비추는지는 빛의 방향과 노멀 사이의 각도로 결정</p>
<h3 id="보간-lerp">보간 Lerp</h3>
<p>| 두 값 사이를 비율로 메우는 것, 애니메이션/블렌딩/네트워크 보정 어디에나 쓰임</p>
<p>보간은 두 값 A와 B 사이를 비율 t(0 ~ 1)로 잇는 계산
게임에서는 값을 부드럽게 바꿔야 할때 사용하고, 그래픽스에서는 <code>래스터화</code> 단계가 삼각형 세 정점의 값(색/UV/노멀)을 픽셀/프래그먼트 위치에 맞춰 자동 보간해 픽셀 셰이더에 넘겨줌</p>
<h3 id="외적-cross-product">외적 Cross Product</h3>
<p>| 두 방향에 동시에 수직인 새 방향을 구하는 연산 노멀/기준축을 만들 때 사용</p>
<p>외적은 두 벡터를 받아 새 벡터 하나를 돌려주는 연산, 결과 벡터는 두 입력이 만드는 평면에 수직. 숫자 하나가 나오는 내적과 달리 결과가 방향
삼각형 두 변으로 표면의 노멀을 구하거나 카메라의 오른쪽/위쪽 방향을 서로 수직으로 맞춰 기준축(좌표공간)을 만들 때 핵심적으로 사용</p>
<h3 id="정규화-normalize">정규화 Normalize</h3>
<p>| 벡터의 방향은 그대로 두고 길이만 1로 맞추는 작업</p>
<p>길이가 1인 벡터는 단위벡터라고 하는데 방향은 그대로 두고 단위벡터로 만듬
그래픽스에서 벡터는 어느쪽을 향하는가만 필요한 경우가 많다. 길이가 제각각이면 계산 결과가 길이에 따라 커지거나 작아져 버림</p>
<h3 id="좌표-공간-model--world-view--clip">좌표 공간 (Model / World/ View / Clip)</h3>
<p>| 정점이 화면 픽셀이 되기까지 갈아타는 4개의 기준, 각 단계마다 행렬을 하나씩 곱합니다.</p>
<p>좌표공간은 어디를 원점으로, <code>어느 방향을 축으로 삼아 좌표를 읽는가</code> 라는 기준. 같은 정점도 기준이 바뀌면 좌표값이 달라지며, 렌더링은 이 기준을 모델-&gt;월드-&gt;뷰-&gt;클립 순서로 갈아타는 과정</p>
<hr>
<ul>
<li><p>이번주 진행
노션 개념사전 공부(게임수학, 라이팅/텍스처, 셰이더 등), RasterTek 튜토리얼 2까지 진행</p>
</li>
<li><p>막힌 것 &amp; 배운 것
개념사전을 읽어보면서 잘 모르거나 헷갈리던 개념들에 대해서 공부
RasterTek 튜토리얼 전반의 구조 익히기</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL ]]></title>
            <link>https://velog.io/@kyu_/TIL-f6s0ekph</link>
            <guid>https://velog.io/@kyu_/TIL-f6s0ekph</guid>
            <pubDate>Thu, 06 Aug 2026 16:05:07 GMT</pubDate>
            <description><![CDATA[<h1 id="cs">CS</h1>
<h2 id="개념">개념</h2>
<h3 id="tobjectptr과-tweakobjectptr-tsoftobjectptr">TObjectPtr과 TWeakObjectPtr, TSoftObjectPtr</h3>
<p>TObjectPtr : 이미 존재하는 UObject를 강하게 참조한다. GC가 이 참조를 인식하므로, 이 참조가 살아있는동안 대상 UObject를 수집하지 않음
TWeakObjectPtr : 이미 존재하는 UObject를 소유하지않고 관찰만 함, GC가 대상을 수집할수 있으므로 사용전 IsValid() 확인이 필요
TSoftObjectPtr : 에셋을 직접 들고있지 않고 에셋 경로만 들고 있다가 필요할때 로드하는 포인터. 비동기 로드도 사용</p>
<h3 id="tsubclassof와-일반-uclass의-차이">TSubclassOf와 일반 UClass*의 차이</h3>
<p>TSubclassOf<T>는 객체가 아니라 클래스 자체를 저장할때 쓰고, 그 클래스가 반드시 T를 상속받도록 제한하는 타입</p>
<pre><code class="language-c++">UPROPERTY(EditAnywhere)
TSubclassOf&lt;AActor&gt; ProjectileClass;

GetWorld()-&gt;SpawnActor&lt;AActor&gt;(ProjectileClass);</code></pre>
<p>UClass*는 아무 클래스나 들어갈 수 있지만, TSubclassOf<AActor>는 AActor 계열만 들어가도록 타입 검사를 해준다. UClass*의 래퍼라고 보면 된다.</p>
<pre><code class="language-c++">UClass* ProjectilesClass;</code></pre>
<h3 id="언리얼-interface와-상속의-차이점">언리얼 Interface와 상속의 차이점</h3>
<p>인터페이스는 이 기능을 제공한다는 약속만 정의, 서로 상속 관계가 없는 클래스들이 같은 기능을 구현하게 할 때 사용(상호작용 시스템)
상속은 공통 데이터/기본 구현을 물려받는 관계, 인터페이스는 공통 기능의 규약만 맞추는 관계라고 보면 됨</p>
<h3 id="cast는-언제-사용하고-실패하면-무엇을-반환할까">Cast&lt;&gt;()는 언제 사용하고, 실패하면 무엇을 반환할까?</h3>
<p>Cast<T>()는 UObject계열을 원하는 하위타입으로 안전하게 캐스팅할 때 사용함
실패하면 nullptr을 반환한다</p>
<h3 id="uobejct포인터를-검사할-때-nullptr-비교만-하지않고-isvalid를-쓰는-이유">UObejct포인터를 검사할 때 nullptr 비교만 하지않고 IsValid()를 쓰는 이유</h3>
<p>nullptr 비교는 주소값이 비어있는지만 확인
IsValid()는 nullptr이 아닌지뿐 아니라, UObject가 이미 삭제 예약(Pending Kill)되었거나 GC 대상이 된 상태는 아닌지도 확인한다.
즉 주소가 남아있어도 더 이상 안전하게 쓰면 안되는 UObject를 걸러준다.</p>
<h3 id="createdefaultsubobject와-newobject는-각각-언제-사용하는가">CreateDefaultSubobject와 NewObject는 각각 언제 사용하는가?</h3>
<p>CreateDefaultSubobject : 액터나 UObject의 생성자 안에서, 그 클래스가 기본적으로 항상 가져야 하는 컴포넌트를 만들때 사용한다. ex) 캐릭터가 생성될 때마다 기본으로 카메라 컴포넌트를 가지게 된다.
NewObject : 게임 실행중에 필요할때 UObject나 컴포넌트를 동적으로 생성할때 쓴다.</p>
<pre><code class="language-c++">UInventory* Inventory = NewObject&lt;UInventory&gt;(this);</code></pre>
<p>참고로 월드에 존재하는 액터를 만들 때는 NewObject가 아니라 SpawnActor를 쓴다.</p>
<h3 id="oncomponentbeginoverlap과-oncomponenthit의-차이">OnComponentBeginOverlap과 OnComponentHit의 차이</h3>
<p>OnComponentBeginOverlap : 두 컴포넌트의 Collision Response가 Overlap일때 서로 겹치기 시작하면 호출된다.
OnComponentHit은 컴포넌트가 다른 물체와 Block 충돌했을때 호출된다.</p>
<p>Block : 서로 통과하지 못하고 물리적으로 막힘
Overlap : 서로 통과하지만 겹치기 시작/끝나는 이벤트 받을 수 있음
Ignore : 충돌 자체를 무시해서 막히지도 않고 오버랩 이벤트도 없음</p>
<p>Object Channel, Trace Channel은 충돌검사할때 붙이는 이름표라고 생각하면 된다. Object Channel은 물체가 자기가자신에게 붙이는 이름표, Trace Channel은 트레이스가 자기자신한테 붙이는 이름표
보통 Object Channel은 물체끼리 충돌, 오버랩할때 어떤 물체인지 구분하는 기준이고, Trace Channel은 라인 트레이스를 쐈을때 대상이 그 트레이스를 Ignore/Block할 기준</p>
<h3 id="collision-enabled">Collision Enabled</h3>
<p>NoCollision : 충돌 자체에 참여하지 않음
QueryOnly : 트레이스/오버랩 같은 검사에는 반응, 물리적으로 밀리거나 튕기지 않음 ex) 아이템 줍기, 공격 판정 범위
PhysicsOnly : 물리 시뮬레이션에는 반응하지만, 트레이스/오버랩 검사 대상은 아님 ex) 물리적으로 떨어지고 부딪히기만 하는 물체
QueryAndPhysics : 검사와 물리 시뮬레이션 둘 다 한다. ex) 물리상자, 총알 트레이스에도 맞고, 바닥에도 떨어짐</p>
<h3 id="월드좌표와-로컬좌표의-차이">월드좌표와 로컬좌표의 차이</h3>
<p>월드좌표는 월드 원점(0, 0, 0) 기준의 절대 위치
상대좌표는 부모 컴포넌트 기준의 상대 위치, 무기같은것들에 사용</p>
<p>SetActorLocation은 액터를 지정한 월드좌표로 이동시키는것이지만, AddActorLocalOffset은 액터가 바라보는 방향을 기준으로 이동량을 더함</p>
<h3 id="ftransform">FTransform</h3>
<p>FVector : 3차원 값, 위치/방향/크기등을 표현할 수 있음
FRotator : Pitch/Yaw/Roll로 표현한 회전값
FTransform : 위치(Translation)/회전(Rotation)/크기(Scale)를 한 번에 가진 구조체</p>
<h3 id="이동-방향-벡터를-정규화하지-않으면-어떤-문제가-생길-수-있는가">이동 방향 벡터를 정규화하지 않으면 어떤 문제가 생길 수 있는가</h3>
<p>앞 이동 방향이 (1, 0)이면 길이가 1이다. 근데 앞+오른쪽 방향이 (1, 1)이면 길이가 1.41이다. 둘다 같은 속도 값을 곱하면 대각선으로 이동할때 1.4배가 빨라진다.
방향벡터를 길이1로 정규화해서 어느방향이든 같은 속도로 움직이게 해야한다 (0.707, 0.707)이런식으로</p>
<h3 id="udataasset과-datatable은-각각-언제사용하는게-좋은가">UDataAsset과 DataTable은 각각 언제사용하는게 좋은가</h3>
<p>DataTable은 기획자가 쉽게 수치를 변경할 수 있으므로 수치값등에 사용
UDataAsset은 하나의 논리적인 설정 묶음을 에셋 하나로 만들때 사용한다.</p>
<h3 id="gameplaytag는-왜-사용하고-일반-fname이나-문자열과-어떤-차이가-있는가">GameplayTag는 왜 사용하고, 일반 FName이나 문자열과 어떤 차이가 있는가?</h3>
<p>GameplayTag는 GAS에서 어빌리티/이펙트 조건을 식별할때 많이 사용한다.
차이점은 계층 구조와 태그 전용기능이 있다. 태그는 우선 계층구조로 이루어져 있음, 또 태그 목록을 프로젝트에서 미리등록하므로 에디터에서 등록도 편하고, 오타방지 및 미리 검사할 수 있다는 장점이 있다.</p>
<h3 id="gameplay-ability-gameplay-effect-attributeset은-각각-어떤-역할을-하는가">Gameplay Ability, Gameplay Effect, AttributeSet은 각각 어떤 역할을 하는가?</h3>
<p>Gameplay Ability : 행동의 흐름
Gameplay Effect : 능력으로 인해 적용되는 결과
만약에 파이어볼을 날린다면
GA : 파이어볼, 입력을 받고, 쿨다운/마나를 확인하고, 애니메이션을 재생하고, 투사체를 생성한다.
GE : 맞은 대상의 체력을 30깎는다, 5초동안 매초 체력을 2씩 깎고 State.Burning 태그를 준다.</p>
<p>GA는 입력받기/조준하기/투사체 생성하기/애니메이션 재생하기 같은 행동흐름을 만들 수 있다.
GE는 수치/태그/버프/디버프를 데이터 기반으로 적용하는 용도</p>
<p>AttributeSet은 체력/최대체력/공격력/이동속도 같은 실제 속성값을 보관하고 변경을 처리한다.</p>
<h3 id="abilitysystemcomponent">AbilitySystemComponent</h3>
<p>AbilitySystemComponent는 GAs의 중심 컴포넌트이다. GA를 부여/활성화, GE를 적용/제거, Tag와 AttributeSet을 관리한다.
네트워크 복제와 클라이언트 예측도 ASC가 담당한다.</p>
<h3 id="gas에서-owner-actor와-avatar-actor">GAS에서 Owner Actor와 Avatar Actor</h3>
<p>Owner Actor : ASC를 소유하는 지속적인 주체, ex) PlayerState
Avatar Actor : 현재 월드에서 움직이고 능력을 쓰는 Pawn/Character, ex) 플레이어 캐릭터
Target Actor : 공격이나 Effect를 받는 대상 ex) 몬스터</p>
<p>단순 싱글 플레이 캐릭터에서는 Onwer와 Avatar가 둘다 캐릭터일 수 있다. 멀티플레이에서 리스폰해도 능력/스텟을 유지하려면 Owner를 PlayerState에 두고, 새로 스폰된 Character를 Avatar로 연결할 수 있다.</p>
<h3 id="언리얼-subsystem은-무엇인가">언리얼 Subsystem은 무엇인가</h3>
<p>Subsystem은 기능을 별도 객체로 분리해서 결합도를 낮추는 구조. GameInstanceSubsystem이라고 하면 게임 인스턴스에 모든 기능을 몰아넣지 않고, 매치메이킹/세이브로드/에셋 로딩관리 같은 기능을 각각 Subsystem으로 나눌 수 있다.</p>
<h3 id="c클래스와-블루프린트는-각각-어떤-역할로-나누어-사용하는게-좋은가">C++클래스와 블루프린트는 각각 어떤 역할로 나누어 사용하는게 좋은가?</h3>
<p>C++ : 성능이 중요한 반복 로직, 복잡한 시스템, 기반 기능
블루프린트 : UI연결, 애니메이션 이벤트, 이펙트/사운드 설정, 간단한 게임 연출, 기획 수치 조정</p>
<p>블루프린트는 노드가 언리얼의 VM을 거쳐 실행돼서 같은 일을 C++ 네이티브 코드로 돌리는것보다 비용이 더 든다.</p>
<h3 id="uenum과-ustruct">UENUM과 USTRUCT</h3>
<p>UENUM은 상태를 이름있는 값으로 구분하고, 언리얼 리플렉션에 등록하면 블루프린트나 에디터 드롭다운에서도 쓸 수 있다.
USTRUCT는 UObject처럼 월드에 독립적으로 존재할 필요는 없고, 아이템 정보/스탯처럼 관련 값들을 묶는 데이터 타입으로 사용한다.</p>
<h3 id="cdo">CDO</h3>
<p>Class Default Object, 클래스별 기본값 템플릿 객체
흐름은</p>
<ol>
<li>언리얼이 AEnemy 클래스를 처음 준비할때 AEnemy의 CDO를 만든다.</li>
<li>CDO를 만들면서 C++ 생성자가 실행된다. 기본값, 기본 컴포넌트가 여기서 정해짐</li>
<li>블루프린트 자식 클래스라면 에디터에서 설정한 값도 그 블루프린트 클래스의 CDO에 저장된다.</li>
<li>나중에 AEnemy 인스턴스를 Spawn하면, 그 인스턴스는 해당 클래스 CDO의 기본값을 바탕으로 초기화한다.</li>
</ol>
<p>CDO를 쓰는이유는 클래스의 기본 상태를 한곳에서 저장하고, 모든 인스턴스를 일관되게 초기화하기 위해서다.</p>
<h3 id="uworld와-level">UWorld와 Level</h3>
<p>UWorld는 현재 실행중인 하나의 게임 세계 전체를 관리함
레벨은 월드를 구성하는 맵 데이터/액터 묶음
예시
UWorld : 현재 플레이중인 오픈월드 전체
Persistent Level : 항상 로드된 기본 지형과 액터
Sub Level : 마을, 던전, 건물 내부처럼 필요할 때 로드/해제하는 구역</p>
<h3 id="persistent-level과-sub-level">Persistent Level과 Sub Level</h3>
<p>Persistent Level은 UWorld가 유지되는 동안 항상 로드되어 있는 기준 레벨
Sub Level은 Persistent Level에 붙는 부분 맵, 필요할때 로드하고, 멀어지면 해제할 수 있다. 하나의 PersistentLevel에 여러 SubLevel이 붙을 수 있다.</p>
<ul>
<li>Persistent Level: 하늘, 전역 환경, 기본 지형</li>
<li>Sub Level: 마을 A, 던전 B, 건물 내부</li>
</ul>
<p>플레이어가 마을 A 근처에 가면 Sub Level을 로드하고, 멀어지면 언로드해서 메모리와 로딩 비용을 아낀다.</p>
<h3 id="editanywhere-visibleanywhere">EditAnywhere, VisibleAnywhere</h3>
<p>EditAnywhere : 에디터의 디테일 패널에서 값을 볼수도있고 수정도 가능
VisibleAnywhere : 에디터의 디테일 패널에서 볼 수만 있고 수정은 불가
블루프린트 그래프에서 접근 가능한건 BlueprintReadOnly, BlueprintReadWrite가 담당</p>
<h3 id="buildcs의-역할">Build.cs의 역할</h3>
<p>모듈을 어떻게 컴파일할지 설정하는 파일이고 Unreal Build Tool이 읽는다.</p>
<ul>
<li>모듈이 의존하는 다른모듈(UMG, Engine, Core등)</li>
<li>외부 라이브러리와 헤더 경로</li>
<li>컴파일 옵션이나 전처리기 정의</li>
</ul>
<h3 id="behavior-tree와-black-board의-차이">Behavior Tree와 Black Board의 차이</h3>
<p>BT는 조건에 따라 어떤 행동을 실행할지 정한 AI 의사결정 트리이고 BB는 BT가 판단할때 쓰는 데이터를 저장하는 공간</p>
<h3 id="navmesh는-무엇이고-ai의-moveto가-동작하려면-왜-필요한가">NavMesh는 무엇이고, AI의 MoveTo가 동작하려면 왜 필요한가?</h3>
<p>NavMesh는 AI가 이동 가능한 바닥 영역을 엔진이 분석해서 만든 길찾기 데이터
MoveTo는 NavMesh를 보고 출발지에서 목표까지 갈 경로를 계산해서 벽이나 장애물을 피해 움직인다.
NavMesh가 없거나, 목적지가 NavMesh밖이면 경로를 찾지 못해서 실패할 수 있다.</p>
<h3 id="event-graph와-anim-graph">Event Graph와 Anim Graph</h3>
<p>Event Graph : 캐릭터 속도, 공중여부, 조준여부처럼 애니메이션 판단에 필요한 변수를 매 프레임 갱신하는 곳
Anim Graph : 그 변수들을 이용해서 Idle/Run 상태 전환, Blend, 레이어 적용등을 거쳐 최종 포즈를 만드는 곳</p>
<h3 id="애니메이션-몽타주는-무엇이고-상태-머신-애니메이션과-언제-구분해서-쓰는가">애니메이션 몽타주는 무엇이고, 상태 머신 애니메이션과 언제 구분해서 쓰는가</h3>
<p>애니메이션 몽타주는 필요한 순간에 재생하는 단발성 애니메이션
상태 머신 애니메이션은 Idle/Walk/Run/Jump처럼 계속 이어지는 기본 이동 상태를 관리하는데 적합</p>
<h3 id="tarray과-stdvector">TArray과 std::vector</h3>
<p>둘은 연속메모리 기반 동적 배열이라 인덱스 접근과 순회가 빠르고 캐시효율이 좋다.
TArray를 언리얼에서 많이쓰는이유는 UCLASS, USTRUCT안에서 UPROPERTY등록을 했을때 언리얼 리플렉션/복제/직렬화/블루프린트와 연동할 수 있다. std::vector는 등록이 안됨</p>
<h3 id="inputaction과-imc">InputAction과 IMC</h3>
<p>InputAction은 행동자체를 정의, IA_Jump, IA_Move, IA_Attacke등
Input Mapping Context는 어떤 키/패드를 어떤 IA에 연결할지 정의</p>
<h3 id="리플렉션-시스템">리플렉션 시스템</h3>
<p>실행중인 언리얼이 클래스/변수/함수의 정보와 설정을 알 수 있게 만드는 시스템
흐름</p>
<ol>
<li>헤더에 UCLASS, USTRUCT, UENUM, UPROPERTY, UFUNCTION을 쓴다</li>
<li>빌드전에 UHT(Unreal Header Tool)가 헤더를 읽는다 -&gt; UHT는 이 매크로가 붙은 클래스, 변수, 함수의 이름/타입/옵션을 분석한다.</li>
<li>UHT가 <code>.generated.h</code>, <code>.gen.cpp</code> 같은 자동 생성 코드를 만든다 -&gt; GENERATED_BODY()는 이 자동생성 코드와 우리가 작성한 클래스와 연결되는 자리다.</li>
<li>컴파일 후 실행 중에는 언리얼이 UClass, FProperty(UPROPERTY로 등록한 변수 하나를 표현하는 메타데이터 객체), UFunction, UEnum같은 메타데이터를 통해 그 정보를 읽을 수 있다. -&gt; UHT는 정보를 등록해서 에디터 Details패널 노출, 블루프린트 접근 같은 기능을 붙일 수 있게됨</li>
</ol>
<h3 id="uobject의-outer는-무엇이고-왜-필요한가">UObject의 Outer는 무엇이고 왜 필요한가?</h3>
<p>Outer는 UObject를 만들때 지정하는 소속 객체</p>
<pre><code class="language-c++">UInventoryItem* Item = NewObject&lt;UInventoryItem&gt;(PlayerCharacter);</code></pre>
<p>PlayerCharacter가 Item의 Outer이다.
이 아이템 객체는 PlayerCharacter에 소속되어 있다는 관계를 엔진에 알려주는것</p>
<ul>
<li>객체의 소속/이름 경로 구성</li>
<li>어떤 월드나 패키지에 속하는지 찾기</li>
<li>객체가 어느 생명주기/컨텍스트에 묶이는지 판단</li>
</ul>
<h3 id="쿠킹과-패키징">쿠킹과 패키징</h3>
<p>Cooking : 에디터용 원본 에셋을 목표 플랫폼에서 실행 가능한 데이터로 변환하는 과정
Packaging : 빌드된 실행 파일, DLL, Cook된 에셋, 설정파일등을 모아서 유저가 에디터없이 실행할 수 있는 배포 폴더를 만드는 과정</p>
<p>.exe와 .dll은 컴파일때 생기고
Cooking은 .umap, .uasset을 해당 플랫폼이 실행할 수 있는 형태로 변경
패키징은 이들을 합쳐서 배포가능한 형태로 만듬 이때 .pak설정을 했다면 .pak이 생기고, 최신버전에서 IoStore를 쓰는 프로젝트는 .utoc, .ucas가 생길 수 있다.</p>
<h3 id="usavegame">USaveGame</h3>
<p>USaveGame은 저장할 데이터를 담아 디스크의 저장 슬롯에 쓰고, 나중에 다시 읽기 위한 데이터 객체. 게임을 껐다 켜도 저장파일은 남음</p>
<h3 id="uprimarydataasset이랑-udataasset의-차이점">UPrimaryDataAsset이랑 UDataAsset의 차이점</h3>
<p>PrimaryAssetId로 식별되는지 안되는지 차이, 특히 AssetManager가 관리/로드할 수 있는 데이터에셋이 PrimaryDataAsset이고, UDataAsset은 일반적인 데이터에셋</p>
<h3 id="animnotify와-animnotifystate">AnimNotify와 AnimNotifyState</h3>
<p>AnimNotify는 애니메이션의 한 시점에 한 번 실행되는 알림
AnimNotifyState는 애니메이션의 일정 구간동안 유지되는 알림</p>
<h3 id="root-motion과-in-place">Root Motion과 In-Place</h3>
<p>RootMotion은 애니메이션 안의 Root Bone 이동량을 읽어서, 캐릭터의 실제 위치와 회전도 같이 이동시킴
In-place는 Root Bone은 제자리에 있고, 걷는 모션만 재생, 실제 캐릭터 이동은 코드가 처리</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - CS]]></title>
            <link>https://velog.io/@kyu_/TIL-CS</link>
            <guid>https://velog.io/@kyu_/TIL-CS</guid>
            <pubDate>Tue, 04 Aug 2026 06:24:21 GMT</pubDate>
            <description><![CDATA[<h1 id="cs">CS</h1>
<h2 id="복습">복습</h2>
<h3 id="tlb">TLB</h3>
<p>TLB는 최근 가상 페이지 -&gt; 물리페이지 변환 결과를 캐싱하는 CPU내부의 빠른 캐시</p>
<h3 id="데이터-레이스와-레이스-컨디션-차이">데이터 레이스와 레이스 컨디션 차이</h3>
<p>데이터 레이스 : 같은 메모리를 여러 스레드가 동기화 없이 접근하고, 그 중 하나이상이 쓰기를 하는 경우, C++에서는 정의되지 않은 동작
레이스 컨디션 : 스레드 실행순서에 따라 결과가 달라지는 더 넓은 문제</p>
<h3 id="프로세스와-스레드의-차이를-메모리-관점에서-설명">프로세스와 스레드의 차이를 메모리 관점에서 설명</h3>
<p>프로세스는 실행중인 프로그램의 독립적인 주소 공간, 다른 프로세스와는 기본적으로 메모리를 공유하지 않음. IPC나 공유메모리로 의도적으로 통신/공유는 가능
스레드는 프로세스 안에서 코드/힙/전역 데이터를 공유하고, 각자 스택과 레지스터를 따로 가진 실행 흐름, 스레드끼리 공유 데이터를 다룰 때 동기화가 필요하다.</p>
<h3 id="포인터와-레퍼런스-차이">포인터와 레퍼런스 차이</h3>
<p>포인터는 nullptr 초기화 가능 레퍼런스는 초기화할때 무조건 값넣어줘야함
포인터는 주소값 레퍼런스는 변수별칭
포인터는 재할당 가능 레퍼런스는 한번 지정한거로 쭉가야됨
포인터는 *이나 -&gt;로 접근, 레퍼런스는 일반 변수처럼 접근</p>
<h3 id="디퍼드-렌더링이란">디퍼드 렌더링이란&gt;</h3>
<p>물체의 색/노멀(표면 방향)/깊이 같은 정보를 G-Buffer에 저장하고 별도의 단계에서 조명을 계산하는 방식
많은 동적 조명을 처리하기 유리하지만 G-Buffer때문에 메모리/대역폭을 많이 쓰고, 투명 물체 처리가 까다로운 단점이 있다.</p>
<h3 id="불편하지만-수동으로-메모리-관리를-하는-이유는-뭘까">불편하지만 수동으로 메모리 관리를 하는 이유는 뭘까?</h3>
<p>객체의 생성/파괴 시점을 정확히 통제할 수 있기 때문이다. 게임에서 GC가 언제돌지 몰라 프레임이 끊기는 것을 피하기 위함이다.</p>
<h3 id="몬스터가-1만-마리라면-어떤-식으로-스레드-구조를-가져가야-할까-단-1만-마리가-모두-플레이어-변수를-건드린다면">몬스터가 1만 마리라면 어떤 식으로 스레드 구조를 가져가야 할까? 단, 1만 마리가 모두 플레이어 변수를 건드린다면?</h3>
<p>스레드 풀을 하나 만들고 코어수에 맞는 워커 스레드를 생성한다. 워커 마다 자기 로벌 커퍼에 결과를 모으고, 메인 스레드가 한번에 적용한다.
플레이어 값은 워커가 직접 수정하지 않고 읽기 전용으로 본다.</p>
<h3 id="버텍스-셰이더-픽셀-셰이더">버텍스 셰이더, 픽셀 셰이더</h3>
<p>버텍스 셰이더 : 정점 하나씩 처리해서 3D 정점 위치를 화면 좌표로 변환한다.
픽셀 셰이더 : 폴리곤 내부를 채운 픽셀 하나씩 처리한다. 텍스처/조명 등을 계산해서 그 픽셀의 최종색을 정한다.</p>
<h2 id="개념">개념</h2>
<h3 id="uobject와-aactor의-차이">UObject와 AActor의 차이</h3>
<p>UObject는 언리얼 오브젝트 시스템의 가장 기본 클래스
AActor는 UObject를 상속받아 월드에 배치될 수 있는 게임 오브젝트의 기본 클래스
차이점으로는
UObject는 월드 위치/회전/스케일이 없고, 데이터 객체/에셋/게임 시스템등에 사용
AActor는 월드에 존재할 수 있고, 위치/회전/스케일이 있고, Tick/네트워크 복제 같은 게임 월드 기능을 사용할 수 있음</p>
<h3 id="uactorcomponent와-uscenecomponent의-차이는-뭐야">UActorComponent와 USceneComponent의 차이는 뭐야?</h3>
<p>UActorComponent : 액터에 기능을 부여, 자기 위치/회전/크기가 없다.
USceneComponent : UActorComponent를 상속받고, 위치/회전/크기를 가짐, 다른 SceneComponent에 붙어 부모-자식구조를 만들 수 있고, 액터의 Root Component가 될 수 있다.
USceneComponent는 액터안에서 위치가 필요한 부품에 사용한다 (Capsule Component, Skeletal Mesh Component, Spring Arm Component, Camera Component), 컴포넌트에 계층구조가 생기면 트랜스폼 변화가 자식에게 전달된다. 캐릭터가 앞으로 이동하면 나머지 메시/카메라/스프링 암등이 따라 같이 이동한다.</p>
<h3 id="gamemode-gamestate">GameMode, GameState</h3>
<p>GameMode : 서버에만 존재, 게임 규칙, 승패 판정, 플레이어 스폰 같은 권한있는 로직 처리
GameState : 서버와 모든 클라이언트에 존재하고 복제됨, 현재 게임시간, 팀 점수처럼 모두가 봐야하는 상태를 담음</p>
<h3 id="playercontroller-playerstate">PlayerController, PlayerState</h3>
<p>PlayerController : 서버와 그 플레이어 본인 클라이언트에만 존재한다. 입력처리, Pawn빙의, 카메라/UI처럼 소유자만 알아야 하는 일을 맡는다.
PlayerState : 서버와 모든 클라이언트에 존재하며 복제된다. 플레이어 이름, 킬, 점수, 팀처럼 다른 플레이어도 봐야하는 상태값들이 들어있음</p>
<h3 id="rpc">RPC</h3>
<p>RPC는 네트워크에서 함수를 실행하게 하는 호출이다. RPC방향은 이름이 실행되는 목적지 기준이다.
서버RPC는 클라이언트에서 호출하고 서버에서 실행하는것(발사 요청), 클라이언트RPC는 서버에서 호출되어서 클라에서 실행되는것(개인 알림), 넷멀티캐스트RPC는 서버에서 호출하고 모든 클라이언트에서 실행하는것 (폭발 이펙트)</p>
<h3 id="reliable-rpc">Reliable RPC</h3>
<p>Reliable RPC는 반드시 도착해야 하는 RPC다. 유실되면 재전송한다. ex) 아이템 획득, 게임 시작
UnReliable RPC는 유실되어도 재전송하지 않는 RPC다. ex) 자주발생하는 이펙트, 위치관련 임시 알림
Reliable을 너무 자주쓰면 네트워크 재전송과 대기때문에 밀릴 수 있다</p>
<h3 id="replicatedusing">ReplicatedUsing</h3>
<p>이 프로퍼티를 복제하고, 클라이언트가 새 값을 받으면 지정한 OnRep함수를 호출해라라는 뜻
OnRep함수는 복제된 값이 클라이언트에 도착했을때 후처리하는 콜백 함수</p>
<h3 id="복제">복제</h3>
<pre><code class="language-c++">
bReplicates = true // 이 액터자체를 네트워크 복제 대상으로 만든다. 이 액터가 클라이언트에도 생성/동기화 될수 있게 한다. 자동으로 멤버변수들까지 복제되지는 않음

UPROPERTY(Replicated) // 특정 프로퍼티 Health값을 서버에서 클라이언트로 복제하겠다.
int32 Health;

UPROPERTY(ReplicatedUsing = OnRep_Health) // Health를 복제하는건 같은데 클라이언트가 새 Health값을 받으면 OnRep_Health()도 자동호출되어 UI갱신같은 후처리를 함
int32 Health;</code></pre>
<h3 id="ufunction-옵션">UFUNCTION 옵션</h3>
<p>BlueprintCallable : 블루프린트에서 C++함수를 호출할 수 있게 함
BlueprintImplementableEvent : C++에서 이벤트만 선언하고 실제 구현은 블루프린트에서
BlueprintNativeEvent : C++에 기본 구현을 두고, 필요하면 블루프린트가 덮어쓸 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - CS복습]]></title>
            <link>https://velog.io/@kyu_/TIL-CS%EB%B3%B5%EC%8A%B5</link>
            <guid>https://velog.io/@kyu_/TIL-CS%EB%B3%B5%EC%8A%B5</guid>
            <pubDate>Mon, 03 Aug 2026 14:16:29 GMT</pubDate>
            <description><![CDATA[<h1 id="cs">CS</h1>
<h2 id="복습">복습</h2>
<ol>
<li><p>std::vector에서 capacity가 꽉 찬 상태로 push_back()을 하면 내부적으로 어떤 일이 일어나고, 시간복잡도는 어떻게 되나?
주소공간, 메모리 재할당 그리고 복사/이동 때문에 시간복잡도 O(N)까지는 맞는데 이터레이터, 포인터, 레퍼런스가 무효화된다는 이야기가 같이 들어가면 더 좋을거같다.</p>
</li>
<li><p>프로세스와 스레드의 차이를 메모리 공유 관점에서 설명
스레드는 코드, 힙, 전역 데이터는 공유하고, 각자 스택,레지스터는 별도라는 점 추가하기, 컨텍스트 스위칭은 다른 프로세스에 있는 스레드로 전환될때 프로세스 간 컨텍스트 스위칭도 발생한다.
보통 그래서 컨텍스트 스위칭이 발생하면 운영체제가 TCB(스레드 제어 블록) 같은 자료구조에 레지스터 값, 현재 실행중이던 명령어 위치(프로그램 카운터), 스택 포인터 같은 스레드 상태를 변경하는데 다른 프로세스에 있는 스레드로 스위칭되면 여기에 주소 공간(페이지 테이블등)도 바꿔야 되어서 비용이 더 크다</p>
</li>
<li><p>페이지 테이블
가상 주소를 실제 RAM주소로 변환하는 표, 프로그램은 실제 RAM 위치를 직접 다루지 않고, 자기만의 가상 주소 공간을 쓴다.</p>
<pre><code> 프로세스 A가 보는 주소 0x1000 → 실제 RAM의 위치 X
 프로세스 B가 보는 주소 0x1000 → 실제 RAM의 위치 Y</code></pre><p>두 프로세스가 똑같이 0x1000 주소를 사용해도 각자 페이지 테이블이 달라서 서로 다른 실제 메모리에 접근한다. 이 덕분에 프로세스가 서로의 메모리를 마음대로 침범하지 못함
프로세스간 컨텍스트 스위칭시에는 &#39;이제 프로세스B의 페이지 테이블을 기준으로 주소를 해석해&#39;라고 바꿔야 해서 이때 TLB(주소 변환 캐시)에도 영향이 생겨 같은 프로세스내에서 컨텍스트 스위칭이 발생했을때 보다 비용이 더 발생한다.</p>
</li>
<li><p>TLB
Translation Lookaside buffer, 주소 변환 캐시, 페이지 테이블을 매 메모리 접근마다 찾아보면 느리므로 최근에 사용한 가상주소 -&gt; 물리주소 변환 결과를 TLB에 저장해둠
CPU의 MMU(Memory Management Unit)에서 다단계 페이지 테이블을 따라 주소를 변환하고 그 변환 결과를 TLB에 캐싱</p>
</li>
<li><p>데이터 레이스와 레이스 컨디션의 차이
데이터 레이스 : 여러 스레드가 같은 메모리에 동시 접근하고, 하나 이상이 쓰기인데 동기화가 없는경우
레이스 컨디션 : 스레드들의 실행 순서에 따라 결과가 달라져 로직이 잘못되는 더 넓은 문제
데이터 레이스는 mutex나 atomic같은 동기화로 메모리 접근을 안전하게 만들어서 해결가능하고<br>레이스 컨디션은 여러 스레드가 공유 데이터를 동시에 처리하면서 실행 순서에 따라 결과가 달라지는 문제, 그래서 공유 데이터를 확인하고 수정하는 구간을 뮤텍스로 잠가 한 스레드씩만 처리하게 방지함</p>
</li>
<li><p>세마포어와 뮤텍스의 차이
뮤텍스는 공유 자원에 접근하는 코드 구간을 한번에 한 스레드만 실행가능하도록 함
세마포어는 자원에 동시에 접근을 허용할 개수를 말함 즉, 카운트가3이면 최대 3개의 스레드가 해당 자원에 접근해서 동시에 사용할 수 있다는것을 말한다. 카운트가 0이면 다음 스레드는 대기한다.</p>
</li>
<li><p>프러스텀, 오클루전 컬링
프러스텀 컬링 : 카메라 밖에있는 요소들은 렌더링 하지 않음
오클루전 컬링 : 카메라 안에 있더라도 불투명한 객체에 가려진부분들은 렌더링하지 않음 -&gt; 대신에 해당 물체가 완전하게 전부다 가져져 있어야 렌더링 하지 않음 즉, 정확하게는 가려진 부분을 렌더링하지 않는게 아니고 전체가 가려진 객체는 렌더링하지 않음</p>
</li>
<li><p>DrawCall이 무엇이고 많이 호출하면 왜 느려지는가??
DrawCall은 CPU가 GPu에 특정 메시를 특정 머티리얼과 셰이더로 그리라고 제출하는 명령, DrawCall이 많으면 CPU가 렌더 상태 설정과 명령 제출을 반복하느라 병목이 생김, 배칭이나 GPU Instancing으로 호출 수를 줄임
GPU Instancing은 같은 메시, 머티리얼을 쓰는 여러 객체를 한번의 Draw Call로 그림, 각 객체의 위치,회전,크기 같은 값만 인스턴스 데이터로 따로 넘김
배칭은 조금 더 넓은 개념으로 여러 렌더링 작업을 묶어 Draw Call을 줄이는것 전체를 말함</p>
</li>
<li><p><code>map</code>과 <code>unordered_map</code>의 차이와 각각의 탐색 시간복잡도를 설명
map은 레드블랙트리, 키값으로 정렬unordered_map은 해시테이블, 탐색의 경우 map은 키정렬이 들어가서 O(log n)이고, unordered_map은 평균 O(1), 최악은 O(N)이다. (해시충돌)</p>
</li>
<li><p>포인터와 레퍼런스의 차이 3가지</p>
<ul>
<li>포인터는 nullptr로 초기화가 가능, 레퍼런스는 불가 초기화할때 값을 넣어줘야함</li>
<li>포인터는 주소를 저장하는 변수이고, 레퍼런스는 기존 변수의 별칭</li>
<li>포인터는 다른 주소를 다시 대입가능하지만, 레퍼런스는 다른 대상으로 바꿀 수 없다.</li>
</ul>
</li>
<li><p>C++에서 다운캐스팅이 필요할 때 어떤 캐스팅을 사용하고, 그 이유는 무엇인가
dynamic_cast를 사용한다.
static_cast는 타입이 실제로 맞는지 런타임에 검사하지 않아서, 잘못된 다운캐스팅이어도 컴파일은 되고 이후 정의되지 않은 동작이 생길 수 있다.
dynamic_cast는 실제 타입을 런타임에 검사, 포인터 캐스팅이 실패하면 nullptr, 레퍼런스 캐스팅이 실패하면 예외가 발생 (원본이 포인터인 경우 레퍼런스인 경우 차이가 있다)</p>
</li>
<li><p>데드락이란 무엇이고 어떻게 예방하는가?
여러 스레드가 서로 가진 락을 기다리면서 아무도 진행하지 못하는 상황, 모든곳에서 락을 잡는 순서를 똑같이 정하는것이 대표적이다.</p>
</li>
<li><p>스택 메모리, 힙 메모리</p>
<ul>
<li>스택 메모리 : 함수, 지역변수, 매개변수, 함수 호출 정보, 함수가 끝나면 자동으로 사라짐</li>
<li>힙 메모리 : new, malloc으로 직접 만든 객체, delete, free, 스마트 포인터 등으로 해제할때 사라짐</li>
<li>전역/정적 영역 : 전역 변수, static 변수, 프로그램 종료시 사라짐</li>
</ul>
</li>
<li><p>Actor의 생명주기
생성자 -&gt; OnConstruction -&gt; BeginPlay -&gt; Tick반복 -&gt; EndPlay -&gt; GC파괴</p>
</li>
<li><p>가상 함수가 뭔지, 그리고 부모 클래스의 소멸자를 virtual로 선언해야 하는 이유는 뭔지 설명
가상 함수는 부모포인터나 레퍼런스로 호출해도, 실제 객체가 자식이면 자식에서 오버라이딩한 함수가 호출되게 하는 함수
소멸자는 Base*로 Derived객체를 delete할때 중요하다. 부모 소멸자가 virtual이 아니면 부모 소멸자만 호출되고 자식 소멸자는 호출되지 않아서 자식이 가진 자원이 누수될 수 있다.</p>
</li>
<li><p>디퍼드 렌더링
포워드 렌더링 : 물체를 그릴 때마다 그 물체에 영향을 주는 조명을 같이 계산, 벽/상자/몬스터 각각에 조명 계산이 들어갈 수 있다.
디퍼드 렌더링 : 먼저 화면에 보이는 가장 앞표면의 정보만 G-Buffer에 기록한다. (벽의 색/방향/깊이(카메라에서 얼마나 먼지)) 그 뒤 화면의 벽 픽셀에만 조명을 계산한다. 조명 100개가 있어도 물체마다 조명100개를 계산하는 대신, 최종화면의 보이는 픽셀에만 필요한 조명을 계산해서 많은 동적 조명을 다루기 편하다.</p>
</li>
<li><p>TCP의 Nagle 알고리즘
TCP가 너무 작은 데이터를 자잘하게 계속 보내지 않도록, 작은 패킷들을 모아서 한번에 보내는 기능, 네트워크 효율은 좋아지지만, 게임 입력처럼 바로 보내야 하는 작은 데이터가 잠깐 대기해서 지연이 생길 수 있다. 그런 실시간 통신에서는 Nagle을 끄기도 함</p>
</li>
<li><p>버텍스 셰이더와 픽셀 셰이더
버텍스 셰이더 : 정점 하나씩 처리한다. 3D모델 정점을 카메라 기준의 화면 위치로 변환한다. 캐릭터 뼈대 애니메이션처럼 정점 위치를 변형하는 작업도 함
픽셀 셰이더 : 삼각형 내부가 픽셀로 채워진 뒤, 픽셀 하나씩 처리한다. 텍스처/조명등을 계산해서 그 픽셀의 최종색을 정한다.</p>
</li>
<li><p>소멸자에서 throw를 외부로 던지면 안되는 이유
C++에서 예외가 발생하면 C++런타임은 예외를 처리할 catch블록을 찾기 위해 스택 되돌리기를 수행하는데 이 과정에서 현재 스택 프레임에 생성되어있던 지역객체들의 소멸자가 순차적으로 호출됨 근데 C++은 기본적으로 이미 예외처리중에 또다른 예외가 소멸자 밖으로 터지게 되면 terminate함, 그리고 C++11부터 소멸자는 기본적으로 noexcept속성이 부여되어서 밖으로 던지면 무조건 종료됨</p>
</li>
<li><p>Iterator를 쓰는 이유
list나 map처럼 인덱스를 쓰지않는 컨테이너에서도 같은 방식으로 순회하기 위함, STL알고리즘들도 이터레이터 기준으로 동작함</p>
</li>
<li><p>C++에서 메모리를 해제한 뒤 그곳에 다시 접근해 생기는 댕글링 포인터를 방지하는 방법을 두 가지 설명
스마트 포인터를 사용 : unique_ptr로 소유권을 한곳에 두고 관찰하는쪽에서는 weak_ptr을 쓰면 방지가능
RAII잘 지키기, raw pointer를 해제해야 한다면 delete하고 꼭 nullptr로 초기화해주기</p>
</li>
<li><p>몬스터 만마리가 있다면 어떤 식으로 스레드 구조를 가져가야할까, 근데 만마리가 다 플레이어 값을 변수를 건드린다면?
CPU 코어수에 맞춘 고정 크기 스레드풀을 만듬
몬스터 1만 마리를 여러 묶음으로 나눠 워커 스레드에 맡김
워커들은 체력같은 값을 읽기만 하고, 직접 수정하지 않음
각 워커는 피해10, 중독 적용 같은 결과를 자기 버퍼에 모음
작업이 끝나면 메인 스레드가 결과를 모아 플레이어에게 한 번에 적용한다.</p>
</li>
<li><p>스레드 풀
스레드를 필요할때 마다 새로 만드는게 아니고 미리 정해진 개수만큼 만들어 두고 작업큐의 일을 가져가 처리하는 구조, CPU코어수보다 스레드가 훨씬 많으면 동시에 실행되지 못하고 전환 비용만 커짐</p>
</li>
<li><p>불편하지만 수동으로 메모리 관리를 하는 이유는 뭘까?
C++은 객체가 언제 생성되고 파괴되는지 개발자가 정확히 통제할 수 있음, 게임은 예측 불가능한 GC멈춤(GC가 메모리를 정리하는 동안 게임 실행 스레드들이 잠깐 멈추는 현상)을 피해야 하므로 필요한 시점에 자원을 확실히 해제하고 메모리 풀같은 최적화를 적용하기 위해 수동 관리 또는 RAII기반 관리를 사용한다.</p>
</li>
<li><p>임계영역(크리티컬 섹션)이란?
공유 자원에 접근하거나 수정하는 코드 구간, 예시로 <code>gold -= 100</code> 처럼 여러 스레드가 동시에 실행하면 문제가 생기는 코드가 임계영역이고 뮤텍스로 잠가서 한번에 한 스레드만 들어가게 한다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL]]></title>
            <link>https://velog.io/@kyu_/TIL-zc2woow8</link>
            <guid>https://velog.io/@kyu_/TIL-zc2woow8</guid>
            <pubDate>Fri, 31 Jul 2026 07:27:53 GMT</pubDate>
            <description><![CDATA[<h1 id="cs">CS</h1>
<pre><code>오늘 공부한 내용

1. 오클루전 컬링, 프러스텀 컬링
2. dynamic_cast와 static_cast의 차이 (다운캐스팅)
3. 컴파일 타임과 런타임
4. RTTI
5. shared_ptr, 순환참조
6. 뮤텍스와 세마포어
7. 디퍼드 렌더링의 처리순서와 장단
8. 언리얼 GC
9. LOD
10. atomic과 mutex
11. 데이터 레이스
12. 레이스 컨디션
13. 스레드 스케줄링</code></pre><h2 id="개념">개념</h2>
<h3 id="오클루전-컬링-프러스텀-컬링">오클루전 컬링, 프러스텀 컬링</h3>
<p>오클루전 컬링이란 시야안에 있지만 다른 불투명 객체에 완전히 가려진 객체를 렌더링 하지 않는 기법을 말함
프러스텀 컬링은 카메라 뒤쪽 뿐아니라 상하좌우 near/far범위를 포함한 시야밖의 객체를 렌더링하지 않는것</p>
<h3 id="dynamic_cast와-static_cast의-차이">dynamic_cast와 static_cast의 차이</h3>
<p>static_cast, dynamic_cast 모두 명시적 형변환
static_cast는 컴파일 타임에 형식상 가능한지만 검사하고, 다운캐스팅 대상이 실제로 그 파생 타입인지 런타임에 확인하지 않음, 잘못된 다운캐스팅은 오류가 아니라 정의되지 않은 동작이 될 수 있다.
dynamic_cast는 다형적 타입에서 RTTI를 이용해 런타임 타입 검사를 한다. 포인터 변환 실패 시 nullptr, 레퍼런스 변환 실패시 std::bad_cast예외를 던진다.</p>
<h4 id="다운캐스팅의-정확한-의미">다운캐스팅의 정확한 의미</h4>
<p>부모 타입의 포인터/레퍼런스를 자식 타입으로 변환하는것</p>
<pre><code class="language-c++">Animal* animal = new Dog();
Dog* dog = dynamic_cast&lt;Dog*&gt;(animal)</code></pre>
<p>Animal* -&gt; Dog*가 다운캐스팅이다. 실제 객체가 정말 Dog일때만 안전하다.
업캐스팅도 있는데 (자식-&gt;부모) 이건 항상 안전해서 보통 암묵적으로 가능하다.</p>
<h4 id="컴파일-타임과-런타임">컴파일 타임과 런타임</h4>
<p>컴파일 타임은 어제 공부한대로 .cpp 코드가 기계어로 변환되는 과정, 이때 문법검사, 선언된 변수 타입, 함수 인자타입처럼 코드만 보고 판단 가능한 문제를 검사한다.
런타임은 컴파일된 프로그램이 실제 실행되는 시점이다. 사용자 입력, 파일 존재 여부, 실제 객체가 Dog인지 Cat인지 같은 실행 중 데이터에 따라 달라지는 것은 이때 알 수 있다.</p>
<h4 id="rtti">RTTI</h4>
<p>Run-Time Type Information, 즉 런타임 타입 정보다. 다형적 클래스의 객체가 실행 중 실제로 어떤 타입인지 알아낼 수 있게하는 C++기능
dynamic_cast가 실제로 RTTI를 사용해 객체 타입을 검사한다.</p>
<h3 id="shared_ptr">shared_ptr</h3>
<p>설명 잘못한 부분이 있다. 몇개의 shared_ptr이 소유중인지를 센다고 하면 정확</p>
<h4 id="순환참조">순환참조</h4>
<pre><code class="language-c++">
struct Node{
    std::shared_ptr&lt;Node&gt; neighbor;
}

int main(){
    auto a = std::make_shared&lt;Node&gt;(); // 노드A의 레퍼런스카운트1
    auto b = std::make_shared&lt;Node&gt;(); // 노드B의 레퍼런스카운트1

    a-&gt;neighbor = b;
    b-&gt;neighbor = a;

    return 0;
}</code></pre>
<ul>
<li>main함수가 사라지면 지역변수 a, b가 사라짐 A와 B의 레퍼런스카운트가 1로 줄어든다.</li>
<li>하지만 A가 해제되려면 B의 neighbor가 해제되어야함 B도 마찬가지 그래서 서로가 서로의 해제를 기다리는 교착상태에 빠져 메모리가 해제되지 않음</li>
<li>여기서 Node안의 shared_ptr을 weak_ptr로 만들면 레퍼런스 카운트가 증가하지 않으므로 지역변수 a,b가 해제될때 A,B노드의 메모리도 정상으로 해제됨</li>
</ul>
<h3 id="뮤텍스와-세마포어">뮤텍스와 세마포어</h3>
<p>뮤텍스는 한번에 하나의 스레드만 임계영역에 들어가게 하는 락이고, 락을 획득한 스레드가 해제한다.
세마포어 공유자원의 사용가능개수를 세는 카운터, 예를들어 세마포어값이 3이면 최대 3개의 스레드까지 동시에 진입할 수 있고, 0이면 다음 스레드는 대기한다.</p>
<h3 id="디퍼드-렌더링의-처리순서와-장단점">디퍼드 렌더링의 처리순서와 장단점</h3>
<p>디퍼드 렌더링은 물체정보와 조명 계산을 분리하는 렌더링 방식</p>
<ol>
<li>먼저 화면에 보이는 물체를 그리면서 최종 색을 바로 계산하지 않고, 픽셀별 위치,깊이,노멀,기본색,머티리얼 정보 등을 G-buffer에 저장한다</li>
<li>그 뒤 G-buffer를 기준으로 조명을 계산해 최종 색을 만든다.</li>
<li>블룸, 톤 매핑같은 포스트 프로세싱을 적용</li>
</ol>
<p>장점 : 조명이 많아도 물체별로 모든 조명을 계산하지 않고 화면 픽셀 기준으로 처리하기 쉬워서 다수의 동적 광원에 유리
단점 : G-buffer때문에 메모리,메모리 대역폭 비용이 크고, 반투명 물체는 처리하기 어려워 별도 포워드 렌더링을 쓰는 경우가 많다.</p>
<h3 id="언리얼-gc">언리얼 GC</h3>
<p>UPROPERTY가 붙은 UObject* 참조는 GC가 추적하는 강한 참조가 되고, ,GC는 루트 객체에서 시작해 이런 참조를 따라가며 도달가능한 Uobejct를 살아있는것으로 판단한다 그래서 UPROPERTY로 참조되는 객체는 GC가 수집하지 않는다 허나 raw UObject* 멤버는 GC가 그 참조를 모를수 있어 대상 객체가 수집된 뒤 댕글링 포인터 위험이 있다.</p>
<h3 id="lod">LOD</h3>
<p>거리에 따라 그릴 품질을 낮추는것, 가까운 객체는 고폴리곤 메시와 고해상도 텍스처를 쓰고 멀어질수록 저폴리곤 메시, 낮은 해상도의 텍스처 또는 단순한 머티리얼로 바꿈, 화면에 작게 보이는 먼 객체에 같은 비용을 쓰지않아 렌더링 성능을 높임</p>
<h3 id="atomic과-mutex">atomic과 mutex</h3>
<p>atomic은 정수 증가, 플래그 설정처럼 단일 값에 대해 단순하고 독립적인 연산을 여러 스레드가 안전하게 처리할때 씀</p>
<pre><code class="language-c++">std::atomic&lt;int&gt; killCount = 0;
++killCount;</code></pre>
<p>mutex는 여러 변수, 컨테이너를 함께 읽고 수정하는 코드 구간 전체를 보호할 때 씀</p>
<pre><code class="language-c++">std::mutex mutex; // 미리 선언된 뮤텍스

{
    std::lock_guard&lt;std::mutex&gt; lock(mutex);
    // 여기부터 mutex 잠김

    inventory.push_back(item);
    gold -= item.price;

} // 여기서 lock 객체가 소멸하며 mutex 자동 해제</code></pre>
<h3 id="데이터-레이스">데이터 레이스</h3>
<p>데이터 레이스 : 여러 스레드가 같은 메모리를 동시에 접근하고, 그중 하나 이상이 쓰기인데 동기화가 없는 경우
원인 : 동기화 도구 (Mutex, Atomic등)없이 같은 메모리를 동시에 읽고 쓰려고 할때 발생
결과 : 정의되지 않은 동작, 메모리 오염
두 스레드가 일반 int score에 동시에 score++를 하면 둘 다 같은 기존 값을 읽고 같은값으로 써서, 2가 증가해야할것이 1만증가할 수 있다. 이를 막기위해 atomic이나 mutex를 쓴다.</p>
<h3 id="레이스-컨디션">레이스 컨디션</h3>
<p>레이스 컨디션 : 경쟁상태, 여러 스레드의 실행순서에 따라 결과가 달라져 버그가 생기는 넓은 개념
원인 : 작업들의 실행 순서가 보장되어야 하는데, 쓰레드 스케줄링 등으로 인해 순서가 꼬일때 발생</p>
<pre><code class="language-c++">if (gold &gt;= 100){
    gold -= 100;
    BuyItem();
}</code></pre>
<p>골드가 100일때 두 스레드가 동시에 실행하면</p>
<ol>
<li>A가 gold &gt;= 100 확인 -&gt; 참</li>
<li>B도 확인 -&gt; 참</li>
<li>A가 100 차감 -&gt; 0</li>
<li>B도 100 차감 -&gt; -100</li>
</ol>
<p>둘중 하나만 구매되어야 하는데 실행 순서때문에 둘다 구매되는 경우가 생김 이를 레이스 컨디션이라고 함, 이때도 뮤텍스나 atomic compare-and-swap등으로 해결</p>
<p>| 데이터 레이스는 메모리 접근 자체의 동기화 문제이고, 레이스 컨디션은 여러 단계 로직의 실행 순서 문제</p>
<h3 id="스레드-스케줄링">스레드 스케줄링</h3>
<p>지금 CPU를 어느 스레드에게 얼마나 실행시킬지를 결정하는 작업
CPU코어가 4개면 같은 순간에 실제로 실행되는 스레드는 최대 4개정도다. 실행할 스레드가 100개라면 운영체제가 빠르게 번갈아 CPU시간을 나눠준다. Ready, Running, Waiting/Blocking으로 나뉨
운영체제 스케줄러는 보통 우선순위와 공정성을 고려해 Ready 상태의 스레드중 하나를 고른다. 한 스레드가 너무 오래 CPU를 점유하면 일정 시간 단위인 타임 슬라이스가 끝난 뒤 다른 스레드에게 넘김 이를 <code>선점형 스케줄링</code>이라고 함</p>
<p>스레드를 바꿀때는 컨텍스트 스위칭이 발생한다. 운영체제는 기존 스레드의 레지스터 값, 실행 위치, 스택 관련 상태를 저장하고, 다음 스레드의 상태를 복원한다. 이 비용과 캐시 효율 저하 때문에 스레드를 과도하게 만들면 느려짐</p>
<p>프로세스 : 독립된 주소 공간과 자원을 가진 실행 중인 프로그램
스레드 : 그 프로세스 안에서 실제 코드를 실행하는 작업 단위</p>
<p>한 프로세스는 최소 한 개의 스레드를 가지며, 여러 스레드는 프로세스의 힙,전역변수,코드를 공유한다.
스레드마다 스택과 레지스터는 따로 가짐</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL]]></title>
            <link>https://velog.io/@kyu_/TIL-vkvxynfr</link>
            <guid>https://velog.io/@kyu_/TIL-vkvxynfr</guid>
            <pubDate>Thu, 30 Jul 2026 14:50:20 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘-진행한것">오늘 진행한것</h1>
<ul>
<li>DirectX 스터디 시작</li>
<li>인라인함수, 컴파일단계</li>
<li>코드카타</li>
</ul>
<h1 id="다이렉트-x-스터디">다이렉트 X 스터디</h1>
<h2 id="기본-문법들">기본 문법들</h2>
<ul>
<li><p>HWND</p>
<ul>
<li>윈도우 창을 가리키는 핸들, 이 창을 Windows에 알려줄때 사용</li>
</ul>
</li>
<li><p>HINSTANCE</p>
<ul>
<li>실행중인 모듈을 가리키는 핸들, 아이콘, 창 클래스 같은 프로그램 리소스를 등록할 때 사용</li>
</ul>
</li>
<li><p>LPCWSTR</p>
<ul>
<li>유니코드 문자열 포인터 타입</li>
<li>L : long, 여기서는 wide character</li>
<li>PC : pointer to const</li>
<li>WSTR : wide string</li>
</ul>
</li>
<li><p>LRESULT</p>
<ul>
<li>Windows 메시지를 처리한 뒤 Windows에 돌려주는 결과값 타입</li>
</ul>
</li>
</ul>
<h2 id="개념">개념</h2>
<ul>
<li>PeekMessage와 GetMessage<ul>
<li>PeekMessage : 논블로킹, 메시지가 없으면 바로 게임루프 돌릴 수 있음</li>
<li>GetMessage : 블로킹 함수라 메시지가 올때까지 멈춤</li>
<li>실시간 렌더링에서는 PeekMessage</li>
</ul>
</li>
</ul>
<p>아스키, 유니코드, utf8, utf16차이점, 언리얼 텍스트 매크로는 뭐냐</p>
<h1 id="cs">CS</h1>
<h2 id="개념-1">개념</h2>
<h3 id="인라인-함수">인라인 함수</h3>
<p>솔직히 왜쓰는지 제대로 이해도 안가고 잘 몰랐는데 확실히 공부해야겠다고 생각해서 찾아봄</p>
<p>일반함수는 컴파일 후 개념적으로</p>
<pre><code class="language-c++">result = Add(1, 2); // Add함수의 기계어 주소로 점프(call)</code></pre>
<p>Add의 구현은 다른 .cpp에 있어도, 컴파일,링크 과정에서 함수의 기계어 주소가 연결된다. CPU는 파일을 찾는게 아니라 해당 주소로 바로 점프</p>
<p>인라인 함수는 구현을 헤더에 두어 호출하는 .cpp가 컴파일시 함수 본문을 볼 수 있게 한다. 그러면 컴파일러가</p>
<pre><code class="language-c++">result = Add(1, 2);
// 위를 다음과 같이 판단한다.
result = 1 + 2;</code></pre>
<p>Add함수로가서 계산하고 호출하는쪽으로 오는게 아니라, 함수 호출자리에 함수 본문을 직접 삽입하는 식으로 계산됨</p>
<h3 id="컴파일-과정">컴파일 과정</h3>
<p>C++에서 h와 cpp는 컴파일 단계를 알아보자</p>
<pre><code class="language-c++">// Math.h
int Add(int a, int b)</code></pre>
<pre><code class="language-c++">// Math.cpp
#include &quot;Math.h&quot;

int Add(int a, int b){
    return a + b;
}</code></pre>
<pre><code class="language-c++">// Main.cpp
#include &quot;Math.h&quot;

int main(){
    return Add(1, 2);
}</code></pre>
<ol>
<li>전처리 단계
<code>#include &quot;Math.h&quot;</code>는 해당 헤더 내용을 코드에 그대로 붙여넣는것 처럼 처리된다.</li>
</ol>
<pre><code class="language-c++">int Add(int a, int b)

int main(){
    return Add(1, 2);
}</code></pre>
<p>Math.cpp도 마찬가지로 헤더 선언을 포함한 상태에서 컴파일 된다.</p>
<ol start="2">
<li>컴파일 단계</li>
</ol>
<p>각 .cpp파일은 서로 독립적으로 컴파일 된다.</p>
<ul>
<li>Main.cpp는 Add의 선언을 보고 이런 함수가 어딘가에 있다만 알고 Add 호출 코드를 만든다.</li>
<li>Math.cpp는 Add의 실제 구현을 보고 Add 함수의 기계어 코드를 만든다.</li>
</ul>
<p>이 결과가 각각의 .obj 목적파일이 된다.</p>
<ol start="3">
<li>링크 단계</li>
</ol>
<p>링커가 Main.obj와 Math.obj를 합침</p>
<ul>
<li>Main.obj에는 Add함수가 필요하다는 참조가 있음</li>
<li>Math.obj에는 실제 Add함수 구현이 있음</li>
</ul>
<p>링커가 둘을 연결해 최종실행파일을 만듬
실행할때는 .h나 .cpp파일을 읽지 않음. 최종 실행파일안에 이미 Add의 기계어 코드와 그 위치 정보가 들어있다.</p>
<h4 id="인라인함수">인라인함수</h4>
<pre><code class="language-c++">// Math.h
inline int Add(int a, int b){
    return a + b;
}</code></pre>
<p>인라인 함수는 이런식으로 헤더에 구현을 둔다. 그러면 Main.cpp를 컴파일할때 Add의 구현도 보이므로 컴파일러가 호출위치에 a+b 계산 코드를 직접 넣는 인라인 최적화를 할 수 있다.</p>
<h1 id="코드카타">코드카타</h1>
<h2 id="1차캐시">[1차]캐시</h2>
<pre><code class="language-c++">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;list&gt;
#include &lt;algorithm&gt;
#include &lt;cctype&gt;

using namespace std;

int solution(int cacheSize, vector&lt;string&gt; cities) {
    int answer = 0;
    list&lt;string&gt; cache;

    if (cacheSize == 0) return cities.size() * 5;

    for (string&amp; city : cities){
        transform(city.begin(), city.end(), city.begin(), ::tolower);
        auto it = find(cache.begin(), cache.end(), city);

        if (it != cache.end()){
            answer++;
            cache.erase(it);
            cache.push_front(city);
        } else {
            answer+=5;
            if ((int)cache.size() == cacheSize){
                cache.pop_back();
            }
            cache.push_front(city);
        }
    }

    return answer;
}</code></pre>
<ul>
<li>algorithm안에 transform이랑 find가 들어감</li>
<li>find와 .find의 차이<ul>
<li>find는 vector나 list같은 반복자(begin/end)를 제공하느 거의 모든 컨테이너에서 사용가능</li>
<li>.find는 컨테이너가 직접 제공하는 멤버 함수, set, map등에 사용함</li>
</ul>
</li>
<li>cctype은 tolower를 처리하기 위해 사용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - 기술적 의사결정 사례]]></title>
            <link>https://velog.io/@kyu_/TIL-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EC%82%AC%EB%A1%80</link>
            <guid>https://velog.io/@kyu_/TIL-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EC%82%AC%EB%A1%80</guid>
            <pubDate>Wed, 22 Jul 2026 12:05:13 GMT</pubDate>
            <description><![CDATA[<h1 id="기술적-의사결정-사례">기술적 의사결정 사례</h1>
<blockquote>
<p>시스템을 처음 도입할 때 여러 선택지를 놓고 고민했던 기록. git 커밋 로그뿐 아니라 당시 설계 메모(세션 기록)까지 다시 뒤져서, 왜 그 방식을 택했는지 이유까지 정리했다.</p>
</blockquote>
<hr>
<h2 id="1-드롭-아이템재화--복제-액터-대신-프록시-방식">1. 드롭 아이템(재화) — 복제 액터 대신 프록시 방식</h2>
<p><strong>해결하려는 문제</strong>: 몬스터가 죽을 때마다 재화를 드롭해야 하는데, 몹 하나당 드롭 개수가 여러 개일 수 있어서 방식에 따라 네트워크 비용이 크게 달라지는 상황이었다.</p>
<p><strong>고려한 방법</strong></p>
<ul>
<li><p><strong>방법 1 — 복제 액터 방식</strong>: 재화 하나당 실제 <code>AActor</code>를 서버에서 스폰하고 모든 클라이언트에 복제.</p>
<ul>
<li>장점: 구현이 직관적. 리플리케이션을 엔진이 알아서 처리.</li>
<li>단점: 몹당 드롭이 많아지면 액터 수만큼 복제 비용이 그대로 늘어남. 몹이 몰려 죽는 구간에서 트래픽이 급증할 위험.</li>
</ul>
</li>
<li><p><strong>방법 2 — 서버 레지스트리 + 클라 로컬 비주얼</strong>: 서버는 복제 액터를 두지 않고 데이터(레지스트리)만 들고 있다가, 각 플레이어에게는 &quot;너에게만 보이는&quot; 프록시로 필요한 정보만 전달하고, 실제 눈에 보이는 비주얼은 클라이언트가 로컬로 만든다.</p>
<ul>
<li>장점: 드롭 개수가 늘어나도 복제되는 액터 수는 늘지 않음(플레이어당 프록시 1개로 고정).</li>
<li>단점: 구조가 3단으로 나뉘어 초기 설계 비용이 큼. &quot;서버 데이터 - 프록시 - 로컬 비주얼&quot; 사이 동기화를 직접 관리해야 함.</li>
</ul>
</li>
</ul>
<p><strong>최종 선택</strong>: 방법 2</p>
<p><strong>선택 이유</strong>: 몹당 드롭 개수가 늘어나도 서버 복제 비용이 액터 수만큼 늘지 않는다는 점이 가장 컸다. 그리고 이미 투사체 시스템이 <code>ProjectileManagerComponent(레지스트리) + ReplicationProxy + ProjectileVisual(클라 로컬)</code>이라는 똑같은 3단 구조로 구현되어 있어서, 새 패턴을 발명하는 대신 이미 검증된 패턴을 그대로 미러링하기로 했다.</p>
<p><strong>구현 구조</strong></p>
<pre><code>[몬스터 처치] 서버
      │
      ▼
UNSCurrencyDropSubsystem.RegisterDrop(종류, 등급, 수량, 위치...)
      │   서버 전용 레지스트리. ActiveDrops 맵에 데이터만 기록 (복제 액터 없음)
      │   DropId 발급
      │
      ▼
등록된 플레이어별 프록시(Proxies)에 전부 SendSpawnEvent
      │
      ▼
ANSCurrencyReplicationProxy (플레이어당 1개, Owner-Only 복제)
      │   Client_SpawnCurrency(Event) RPC로 &quot;이 플레이어에게만&quot; 전달
      │
      ▼
ANSLocalCurrencyPickup 생성 (클라이언트 로컬, 서버에 복제 안 됨)
      │   실제로 화면에 보이는 비주얼. 로컬에서 오버랩 감지
      │
      ▼
[플레이어가 주움] 클라 → 서버에 TryCollect(DropId) 요청
      │
      ▼
서버가 ActiveDrops 기준으로 재검증
      ├─ 성공 → SendRemoveEvent → Client_RemoveCurrency → 로컬 픽업 제거, 재화 지급
      └─ 실패 → SendRestoreEvent → Client_RestoreCurrency → 로컬 픽업 다시 보이게</code></pre><p><strong>결과</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Before (방법 1 가정)</th>
<th>After (방법 2 적용)</th>
</tr>
</thead>
<tbody><tr>
<td>서버가 복제하는 액터 수</td>
<td>드롭 개수만큼 증가</td>
<td>플레이어당 프록시 1개로 고정</td>
</tr>
<tr>
<td>드롭 100개 발생 시</td>
<td>복제 액터 100개 관리</td>
<td>ActiveDrops 맵 데이터 100줄 + 이벤트 전송만</td>
</tr>
<tr>
<td>새 드롭 타입 추가</td>
<td>매번 새로운 복제 구조 설계</td>
<td>같은 3단 구조 재사용(파츠, 회복 아이템)</td>
</tr>
</tbody></table>
<p><strong>얻은 효과</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>성능</strong></td>
<td>드롭 개수가 늘어나도 리플리케이션 비용이 &quot;이벤트 데이터 크기 × 플레이어 수&quot;로 고정. 액터 복제(Transform 등 매 틱 동기화)가 없어 대량 드롭 상황에서도 트래픽이 액터 수에 비례해 늘지 않음</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>투사체 시스템과 동일 구조라 한쪽을 이해하면 다른 쪽도 바로 이해됨. &quot;서버 데이터 문제&quot;와 &quot;클라 비주얼 문제&quot;가 계층으로 분리돼 원인 추적이 쉬움</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>같은 패턴을 파츠 드롭(<code>UNSDroppedPartRegistrySubsystem</code>), 회복 아이템 드롭(<code>UNSHealDropSubsystem</code>)에도 재사용. 새 드롭 타입 추가 시 구조를 처음부터 고민할 필요 없음</td>
</tr>
<tr>
<td><strong>협업</strong></td>
<td>드롭 확률·수치는 DataTable로 분리해 디자이너가 직접 관리, 코드는 연결부만 담당</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-상호작용-시스템--상속-대신-인터페이스로-기능-분리">2. 상호작용 시스템 — 상속 대신 인터페이스로 기능 분리</h2>
<p><strong>해결하려는 문제</strong>: 상호작용 반응 로직이 <code>ANSInteractableActor(AActor)</code> 하나에 고정돼 있었는데, 파츠 상점 NPC나 펫 NPC는 추후 <code>AIController</code>로 순찰·추적처럼 자율적으로 길을 찾아 움직여야 하는 대상이었다. <code>AActor</code>는 <code>SetActorLocation</code> 등으로 위치 자체는 옮길 수 있지만, <code>Controller</code>에게 Possess될 수 없고 내비게이션 이동 컴포넌트(<code>UNavMovementComponent</code> 계열)가 없어서 AI가 NavMesh 기반으로 자율 이동시키는 표준 경로를 쓸 수 없다. 이 기능은 <code>APawn</code>부터 생기고, <code>ACharacter</code>가 <code>UCharacterMovementComponent</code>까지 기본으로 갖춘 표준 클래스다. 즉 NPC는 <code>ACharacter</code>를 상속해야 했는데, 언리얼은 다중 상속이 안 되므로 <code>AActor</code> 기반의 기존 상호작용 클래스를 그대로 상속하면서 동시에 <code>ACharacter</code>를 상속할 수 없었다.</p>
<p><strong>고려한 방법</strong></p>
<ul>
<li><p><strong>방법 1 — <code>AActor</code> 상속 구조를 유지</strong>: 상호작용 로직을 계속 <code>ANSInteractableActor</code>에 두고, NPC 쪽은 별도로 처리.</p>
<ul>
<li>장점: 기존 코드를 안 바꿔도 됨.</li>
<li>단점: NPC 종류가 늘어날 때마다 &quot;AI 이동은 되는데 상호작용은 안 되는&quot; 문제가 반복됨. 기능(상호작용)과 정체성(AActor/ACharacter)이 묶여 있어 구조적으로 막힘.</li>
</ul>
</li>
<li><p><strong>방법 2 — 인터페이스로 기능만 분리</strong>: 상호작용에 필요한 동작(<code>CanInteract</code>, <code>OnInteract</code>, 프롬프트 정보)을 <code>INSInteractable</code> 인터페이스로 빼서, NPC는 <code>ACharacter</code>를 상속하면서 이 인터페이스만 구현.</p>
<ul>
<li>장점: AI 이동이 필요한 대상(<code>ACharacter</code>)과 정적인 대상(<code>AActor</code>) 모두 같은 인터페이스로 상호작용 가능.</li>
<li>단점: 기존 <code>ANSInteractableActor</code>를 상속하던 코드를 인터페이스 구현으로 다시 작성해야 함.</li>
</ul>
</li>
</ul>
<p><strong>최종 선택</strong>: 방법 2</p>
<p><strong>선택 이유</strong>: NPC는 <code>AIController</code>에게 Possess되어 자율 이동해야 하니 <code>ACharacter</code>(→<code>APawn</code>)가 필수인데, 다중 상속이 안 되는 언리얼 구조에서는 &quot;기능&quot;을 상속이 아니라 인터페이스로 떼어내는 것만이 유일한 해법이었다.</p>
<p><strong>구현 구조</strong></p>
<pre><code>[Before]
ANSInteractableActor (AActor)
   └─ OnInteract 로직이 이 클래스에 고정
   └─ NPC는 AIController에게 Possess되어 자율 이동해야 함 → APawn/ACharacter 필요 → 다중상속 불가 → 재사용 불가

[After]
INSInteractable (인터페이스: CanInteract / OnInteract / GetPromptText / ...)
   ├─ ANSInteractableNPCBase : ACharacter, INSInteractable   (이동 가능 NPC)
   ├─ ANSDroppedPart        : AActor,     INSInteractable   (드롭 파츠)
   └─ ANSInteractableActor  : AActor,     INSInteractable   (정적 프롭 전용으로 범위 축소)

UNSInteractionComponent → INSInteractable::Execute_OnInteract(Target, PC)
   (대상이 뭔지 몰라도 인터페이스만 보고 호출)</code></pre><p><strong>결과</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td>NPC 추가 시</td>
<td><code>ACharacter</code> 필요 → 기존 상호작용 베이스 재사용 불가</td>
<td><code>INSInteractable</code>만 구현하면 됨</td>
</tr>
<tr>
<td>상호작용 컴포넌트 코드</td>
<td>대상 타입별 분기 필요</td>
<td>인터페이스 호출 하나로 통일</td>
</tr>
<tr>
<td>정적 프롭</td>
<td>상호작용 로직과 뒤섞임</td>
<td><code>ANSInteractableActor</code>로 역할 축소, 여전히 인터페이스 구현</td>
</tr>
</tbody></table>
<p><strong>얻은 효과</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>성능</strong></td>
<td>직접적 영향 없음 — 구조적 결정</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>새 NPC/오브젝트를 추가할 때 상속 트리를 고민할 필요 없이 인터페이스 구현 여부만 확인하면 됨</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>파츠 상점 NPC, 펫 NPC, 캐릭터 선택 NPC, 드롭 파츠까지 전부 같은 인터페이스로 확장됨</td>
</tr>
<tr>
<td><strong>협업</strong></td>
<td>상호작용 로직 담당자가 대상의 내부 구현을 몰라도 인터페이스 계약만으로 연동 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-상호작용-시스템--감지-컴포넌트를-어디에-붙일-것인가">3. 상호작용 시스템 — 감지 컴포넌트를 어디에 붙일 것인가</h2>
<p><strong>해결하려는 문제</strong>: 인터페이스로 기능은 분리했지만, &quot;누가 상호작용 후보를 감지하고 판정하는가&quot;는 별도로 정해야 했다.</p>
<p><strong>고려한 방법</strong></p>
<ul>
<li><p><strong>방법 1 — 대상 액터(NPC/드롭 파츠)에 감지 컴포넌트 부착</strong></p>
<ul>
<li>장점: 대상이 자기 감지 범위를 스스로 관리.</li>
<li>단점: 상호작용을 &quot;시작&quot;하는 주체는 결국 플레이어인데, 감지·판정 로직이 대상마다 중복됨.</li>
</ul>
</li>
<li><p><strong>방법 2 — PlayerController에 부착</strong></p>
<ul>
<li>장점: 플레이어 쪽으로 옮겨서 중복은 줄었음.</li>
<li>단점: 리슨서버에서는 컨트롤러가 클라이언트별로 다르게 존재해서, 로컬/원격 구분이 애매해짐.</li>
</ul>
</li>
<li><p><strong>방법 3 — 캐릭터(Pawn)에 부착</strong> (최종)</p>
<ul>
<li>장점: 판정 주체(플레이어)와 감지 위치가 정확히 일치. <code>IsLocallyControlled()</code>로 로컬/원격 구분도 명확.</li>
<li>단점: 없음 — 실제로 두 방법을 먼저 시도해보고 나서 확정된 결론.</li>
</ul>
</li>
</ul>
<p><strong>최종 선택</strong>: 방법 3</p>
<p><strong>선택 이유</strong>: 실제로 감지 컴포넌트를 대상 액터 → 컨트롤러 → 캐릭터 순서로 세 번 옮겨본 뒤에야 확정했다. 컨트롤러는 리슨서버에서 클라마다 다르게 존재하지만, 판정 로직은 캐릭터(폰) 기준이 자연스럽다는 걸 직접 붙여보고 나서 알게 됐다.</p>
<p><strong>구현 구조</strong></p>
<pre><code>[시도 1] 대상 액터(NPC/파츠) ← 상호작용 컴포넌트 부착
    → NPC/파츠마다 감지 로직 중복, 대상이 늘어날수록 반복 비용 증가

[시도 2] PlayerController ← 상호작용 컴포넌트 부착
    → 리슨서버에서 컨트롤러가 클라마다 다르게 존재 → 로컬/원격 구분 애매

[최종] ACharacter(Pawn) ← UNSInteractionComponent 부착
    → EnableLocalInteraction()에서 IsLocallyControlled() 확인 후에만 감지 활성화
    → 판정 주체(플레이어)와 감지 위치가 일치</code></pre><p><strong>결과</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Before (시도 1~2)</th>
<th>After (최종)</th>
</tr>
</thead>
<tbody><tr>
<td>감지 로직 위치</td>
<td>대상별 중복 또는 컨트롤러 종속</td>
<td>캐릭터 하나에 통일</td>
</tr>
<tr>
<td>리슨서버 로컬/원격 구분</td>
<td>애매함 (실제로 위젯 누출 버그로 드러남)</td>
<td><code>IsLocallyControlled()</code>로 명확</td>
</tr>
<tr>
<td>대상 추가 비용</td>
<td>대상마다 감지 로직 추가</td>
<td>인터페이스만 구현, 감지는 그대로</td>
</tr>
</tbody></table>
<p><strong>얻은 효과</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>성능</strong></td>
<td>오버랩 후보가 없으면 틱을 끄는 최적화를 캐릭터 컴포넌트 한 곳에서만 관리하면 됨</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>리슨서버 관련 버그(호스트 화면에 클라 위젯 노출 등)가 났을 때 확인할 곳이 한 컴포넌트로 좁혀짐</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>NPC/드롭 오브젝트 종류가 늘어도 캐릭터 쪽 감지 로직은 변경 없음</td>
</tr>
<tr>
<td><strong>협업</strong></td>
<td>&quot;상호작용이 이상하다&quot;는 리포트가 오면 항상 같은 컴포넌트부터 확인하면 되는 관례가 생김</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-파츠-시스템--능력치-적용-방식-개별-ge-→-공용-ge">4. 파츠 시스템 — 능력치 적용 방식 (개별 GE → 공용 GE)</h2>
<p><strong>해결하려는 문제</strong>: 파츠를 장착하면 능력치가 올라야 하는데, 파츠 종류가 계속 늘어날 예정이라 &quot;파츠 하나당 GameplayEffect 하나&quot;로 가면 애셋이 선형으로 늘어날 게 뻔했다.</p>
<p><strong>고려한 방법</strong></p>
<ul>
<li><p><strong>방법 1 — 부위별로 다른 GAS 연동 방식</strong> (초기 설계): 바디 파츠(체력↔실드 연동)는 <code>MMC</code>(ModifierMagnitudeCalculation)로 MaxHealth 기반 계산, 암/레그 파츠(단순 증가형)는 <code>SetByCaller</code>로 값 주입.</p>
<ul>
<li>장점: 각 부위 특성에 맞는 계산 방식을 쓸 수 있음.</li>
<li>단점: 파츠 종류가 늘어날수록 GE 애셋도 함께 늘어남. 부위별로 다른 방식을 쓰다 보니 관리 규칙이 두 갈래로 나뉨.</li>
</ul>
</li>
<li><p><strong>방법 2 — 공용 GE + StatTag 매핑</strong> (최종): 파츠 종류와 무관하게 GE 하나(<code>SharedPartEffectClass</code>)를 두고, 이 GE에 모든 스탯의 Modifier를 미리 정의해둔 뒤, 파츠가 담당하는 스탯 하나만 SetByCaller로 값을 채움.</p>
<ul>
<li>장점: 파츠가 몇 개든 GE 애셋은 1개. 새 파츠 추가 시 GE를 새로 만들 필요 없이 StatTag만 지정.</li>
<li>단점: 매핑 테이블(StatTag → SetByCaller 태그/Attribute) 관리가 필요하고, 값이 잘못 채워지면 원인 추적이 한 단계 더 필요.</li>
</ul>
</li>
</ul>
<p><strong>최종 선택</strong>: 방법 2. 다만 처음부터 방법 2로 시작한 게 아니라, 방법 1로 먼저 구현하고 파츠 수가 늘어나면서 애셋 관리 비용이 커지는 걸 실제로 겪은 뒤 방법 2로 다시 통합했다.</p>
<p><strong>선택 이유</strong>: 파츠 종류가 늘어날수록 개별 GE 방식의 관리 비용이 선형으로 커진다는 게 실제로 드러났기 때문이다. 처음부터 정답을 맞히기보다, 실제 파츠 수가 늘어났을 때의 비용을 겪어보고 통합하는 순서를 밟았다.</p>
<p><strong>구현 구조</strong></p>
<pre><code>[Before] 파츠 A → GE_PartA (Modifier: StatA)
         파츠 B → GE_PartB (Modifier: StatB)
         파츠 N → GE_PartN (...)              ← 파츠 종류만큼 GE 애셋 증가

[After]  파츠 A/B/N → SharedPartEffectClass (GE 1개, 모든 스탯 Modifier 내장)
              │
              ▼
   NSCombatStatAttribute::InitializeNeutralSetByCallers(Spec)   모든 칸을 중립값(Add=0, Multiply=1)으로 채움
              │
              ▼
   StatTag → 매핑 테이블에서 이 파츠가 채울 SetByCaller 태그 결정
              │
              ▼
   Spec.SetSetByCallerMagnitude(그 태그, Part-&gt;CurrentValue)   해당 칸만 실제 값으로 덮어씀</code></pre><p><strong>결과</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Before (개별 GE)</th>
<th>After (공용 GE)</th>
</tr>
</thead>
<tbody><tr>
<td>파츠 10종 추가 시</td>
<td>GE 애셋 10개 추가 필요</td>
<td>GE 애셋 0개, StatTag만 지정</td>
</tr>
<tr>
<td>새 스탯 종류 추가</td>
<td>관련 GE들을 각각 수정</td>
<td>매핑 테이블 + 공용 GE 한 곳만 수정</td>
</tr>
<tr>
<td>관리 방식</td>
<td>부위별로 MMC/SetByCaller 두 갈래</td>
<td>SetByCaller 방식으로 통일</td>
</tr>
</tbody></table>
<p><strong>얻은 효과</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>성능</strong></td>
<td>GE 애셋 수가 늘지 않아 로드/평가해야 할 GameplayEffect 종류가 고정됨</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>능력치 적용 버그가 나면 &quot;공용 GE 자체&quot;와 &quot;매핑 테이블&quot; 두 곳만 확인하면 됨</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>새 파츠 추가가 &quot;StatTag 지정 + 값 범위 DT 등록&quot;으로 끝남, GE 신규 제작 불필요</td>
</tr>
<tr>
<td><strong>협업</strong></td>
<td>파츠 밸런스 담당자가 DT에 값만 넣으면 되고, GE 구조는 건드릴 필요 없음</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-파츠-시스템--슬롯-식별-방식-enum-→-gameplaytag">5. 파츠 시스템 — 슬롯 식별 방식 (enum → GameplayTag)</h2>
<p><strong>해결하려는 문제</strong>: 파츠가 장착되는 부위(슬롯)를 어떻게 식별할지 정해야 했다. 초기엔 &quot;Slot/Rarity는 enum 유지, 태그 불필요&quot;로 결정했었는데, 슬롯 종류가 데이터 주도로 늘어날 필요가 생겼다.</p>
<p><strong>고려한 방법</strong></p>
<ul>
<li><p><strong>방법 1 — enum</strong> (초기 결정): <code>ENSPartSlot { Body, Arm, Leg }</code> 같은 고정 열거형.</p>
<ul>
<li>장점: 타입 안전, 코드에서 다루기 쉬움.</li>
<li>단점: 새 슬롯을 추가하려면 코드를 고치고 재컴파일해야 함. UI도 enum 값을 코드로 순회하며 하드코딩된 위젯을 만들어야 함.</li>
</ul>
</li>
<li><p><strong>방법 2 — GameplayTag</strong> (전환 후): <code>FGameplayTag Slot</code>, 실제 슬롯 목록은 DataTable 행으로 정의.</p>
<ul>
<li>장점: 슬롯을 코드 수정 없이 데이터 추가만으로 늘릴 수 있음. UI도 슬롯 DT를 순회해 동적으로 위젯을 만들 수 있음.</li>
<li>단점: 태그 오타/미등록 시 런타임에야 문제가 드러남(컴파일 타임 체크 불가).</li>
</ul>
</li>
</ul>
<p><strong>최종 선택</strong>: 방법 2</p>
<p><strong>선택 이유</strong>: 슬롯 개수/종류가 데이터 주도로 늘어날 필요가 생기면서, enum은 늘어날 때마다 재컴파일이 필요하다는 제약이 계속 걸림돌이 됐다.</p>
<p><strong>구현 구조</strong></p>
<pre><code>[Before] enum ENSPartSlot { Body, Arm, Leg }        ← 새 슬롯 추가 시 코드 수정 + 재컴파일
              │
              ▼
   UI가 enum 값을 하드코딩으로 순회 → 슬롯 위젯 3개 고정 생성

[After]  FGameplayTag Slot  (DT_PartSlot 행 = 슬롯 정의)  ← 데이터 추가만으로 슬롯 증가
              │
              ▼
   DataSubsystem::GetAllSlotRows() 순회 → 슬롯 수만큼 위젯 동적 생성</code></pre><p><strong>결과</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Before (enum)</th>
<th>After (GameplayTag)</th>
</tr>
</thead>
<tbody><tr>
<td>슬롯 추가</td>
<td>코드 수정 + 재컴파일</td>
<td>DT에 행 추가</td>
</tr>
<tr>
<td>UI 슬롯 목록</td>
<td>하드코딩 순회</td>
<td>DT 기반 동적 생성</td>
</tr>
<tr>
<td>장착/해제 판정 로직</td>
<td>enum 값 직접 비교</td>
<td>태그 매칭</td>
</tr>
</tbody></table>
<p><strong>얻은 효과</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>성능</strong></td>
<td>태그 비교는 enum 비교보다 약간 비용이 있지만, 슬롯 수가 적어 체감 차이는 없음</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>슬롯 관련 로직이 &quot;슬롯이 몇 개인지 몰라도&quot; 동작하도록 바뀜</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>새 슬롯 추가가 순수 데이터 작업으로 끝남 (단, 이 전환 도중 장착/해제 회귀 버그가 한 번 발생해서, 구조 전환 시 영향 범위를 미리 점검하는 습관이 생김)</td>
</tr>
<tr>
<td><strong>협업</strong></td>
<td>기획자가 슬롯 구성을 DT에서 직접 조정 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-증강-시스템--등급별-gas-연동-방식-legendary-전용-처리-vs-전체-태그-통일">6. 증강 시스템 — 등급별 GAS 연동 방식 (Legendary 전용 처리 vs 전체 태그 통일)</h2>
<p><strong>해결하려는 문제</strong>: 증강은 등급(Common/Rare/Epic/Legendary)이 있는데, Legendary만 &quot;보유 시 시너지가 발동하는&quot; 특별한 조건이 필요해 보였다. 이걸 등급별로 다르게 처리할지, 전체를 하나의 방식으로 통일할지 정해야 했다.</p>
<p><strong>고려한 방법</strong></p>
<ul>
<li><p><strong>방법 1 — Legendary만 특별 취급</strong>: C/R/E는 기존처럼 수치 적용만 하고, Legendary만 별도로 &quot;보유 여부&quot; 추적 로직을 추가.</p>
<ul>
<li>장점: 당장 필요한 것(Legendary 시너지)만 구현하면 됨.</li>
<li>단점: 나중에 C/R/E 등급에도 &quot;이 증강을 갖고 있으면&quot; 조건이 필요해지면 구조를 또 나눠야 함. 등급마다 처리 방식이 달라 코드가 두 갈래로 분기됨.</li>
</ul>
</li>
<li><p><strong>방법 2 — 모든 등급을 공통 태그로 관리</strong>: <code>UNSAugmentDefinition</code>에 <code>GrantedTag</code> 필드를 두고, 모든 등급이 이 방식을 공유. GE의 <code>GrantedTags</code>에 설정해 적용/제거 시 ASC 태그맵에 자동 반영.</p>
<ul>
<li>장점: 등급과 무관하게 &quot;이 증강 보유 시&quot; 조건을 코드 변경 없이 GE의 <code>RequiredTags</code>로 에디터에서만 연결 가능.</li>
<li>단점: Legendary 전용 로직보다 초기 설계가 한 단계 더 필요(태그 필드, GrantedTags 세팅).</li>
</ul>
</li>
</ul>
<p><strong>최종 선택</strong>: 방법 2</p>
<p><strong>선택 이유</strong>: C/R/E 등급도 나중에 &quot;이 증강을 갖고 있으면&quot; 조건이 생길 수 있다고 판단했고, Legendary만 따로 처리하면 그 시점에 구조를 다시 나눠야 하는 게 뻔했다. 처음부터 통일해두면 증강을 추가할 때마다 코드 변경 없이 태그만 설정하면 된다.</p>
<p><strong>구현 구조</strong></p>
<pre><code>[방법 1 - Legendary만 특별 취급]
   UNSAugmentDefinition (C/R/E)       → 수치만 관리
   UNSAugmentDefinition (Legendary)   → GrantedTag + 시너지 조건   ← 등급별로 다른 구조

[방법 2(최종) - 전체 공통 태그]
   UNSAugmentDefinition (모든 등급) → GrantedTag 필드
              │
              ▼
   GE.GrantedTags = 이 태그   → 적용/제거 시 ASC 태그맵 자동 반영 (&quot;보유 여부&quot; 추적)
   GE.RequiredTags = 다른 증강 태그  → &quot;이 증강 보유 시&quot; 시너지, 코드 수정 없이 에디터에서 연결
              │
   FNSAugmentInstance → 스택 수(&quot;몇 개 보유&quot;)는 별도로 관리 (태그와 역할 분리)</code></pre><p><strong>결과</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Before (방법 1 가정)</th>
<th>After (방법 2 적용)</th>
</tr>
</thead>
<tbody><tr>
<td>C/R/E 시너지 조건 추가 시</td>
<td>구조를 새로 나눠야 함</td>
<td>이미 있는 태그 방식 그대로 사용</td>
</tr>
<tr>
<td>증강 보유 추적</td>
<td>등급별로 다른 코드 경로</td>
<td>태그 하나로 통일</td>
</tr>
<tr>
<td>시너지 조건 연결</td>
<td>코드 수정 필요</td>
<td>GE <code>RequiredTags</code> 에디터 설정만</td>
</tr>
</tbody></table>
<p><strong>얻은 효과</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>성능</strong></td>
<td>GAS의 태그맵 조회는 이미 최적화된 경로라 추가 비용이 거의 없음</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>&quot;보유 여부&quot;(태그)와 &quot;스택 수&quot;(인스턴스 데이터)의 책임이 분리돼 헷갈릴 여지가 적음</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>신규 증강 추가 시 태그만 붙이면 기존 시너지 시스템에 바로 편입됨</td>
</tr>
<tr>
<td><strong>협업</strong></td>
<td>시너지 조건을 코드 담당자 없이 GE 에디터 설정만으로 기획자가 조정 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="7-증강-시스템--입력-처리-방식-imc-스위칭-→-비차단-게이팅">7. 증강 시스템 — 입력 처리 방식 (IMC 스위칭 → 비차단 게이팅)</h2>
<p><strong>해결하려는 문제</strong>: 증강 선택창이 열려 있을 때 캐릭터 입력(이동/공격)을 어떻게 처리할지 정해야 했다.</p>
<p><strong>고려한 방법</strong></p>
<ul>
<li><p><strong>방법 1 — IMC 스위칭</strong> (초기 설계): 증강창이 열리면 전용 <code>IMC_Augment</code>로 통째로 갈아끼워 게임 입력을 차단하는 모달 방식.</p>
<ul>
<li>장점: &quot;증강창이 열려 있으면 다른 입력은 무시&quot;라는 규칙을 IMC 레벨에서 강제할 수 있어 구현이 직관적.</li>
<li>단점: 게임 설계가 &quot;증강창이 떠 있어도 이동/전투가 되어야 한다&quot;로 바뀌면서 전제 자체가 깨짐. 다른 UI(파츠 패널 등)와 동시에 열리는 경우 IMC 우선순위 관리가 복잡해짐.</li>
</ul>
</li>
<li><p><strong>방법 2 — 비차단 게이팅 + 대기열</strong> (최종): 증강 관련 키(1/2/3/T)를 기존 Gameplay IMC에 그대로 두고, C++에서 <code>IsAugmentationPanelOpen()</code> 여부로만 게이팅. 여러 증강 제안이 겹칠 수 있는 상황을 대비해 대기열(<code>PoolQueue</code>, <code>PendingCount</code>)도 함께 도입.</p>
<ul>
<li>장점: 이동/전투가 항상 정상 동작. IMC를 하나만 유지해 다른 UI와의 충돌 여지가 줄어듦.</li>
<li>단점: 기존 IMC 스위칭 코드(<code>EnterAugmentInputMode</code>/<code>ExitAugmentInputMode</code> 등)를 전부 삭제하고 다시 만들어야 함.</li>
</ul>
</li>
</ul>
<p><strong>최종 선택</strong>: 방법 2</p>
<p><strong>선택 이유</strong>: 게임 설계 방향이 &quot;증강창은 비차단 오버레이&quot;로 확정됐고, 증강 관련 키(1/2/3/T)가 스킬 키와 안 겹쳐서 애초에 입력 모드를 통째로 바꿀 이유가 사라졌다.</p>
<p><strong>구현 구조</strong></p>
<pre><code>[Before] 증강창 오픈 → IMC_Augment로 전체 스위칭 → 이동/공격 등 게임 입력 차단(모달)
         패널 닫힘 → 원래 IMC로 복귀

[After]  증강 입력(1/2/3/T)이 항상 Gameplay IMC 안에 존재
              │
              ▼
   Input_AugmentAction 실행 → UIManager-&gt;IsAugmentationPanelOpen() 확인만으로 게이팅
              │
              ▼
   이동/공격/스킬은 항상 그대로 동작 (비차단 오버레이)
              │
   여러 증강 제안 발생 시 → PoolQueue(대기열)에 적재 → 순서대로 PendingCount만큼 순차 제시</code></pre><p><strong>결과</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Before (IMC 스위칭)</th>
<th>After (비차단 게이팅)</th>
</tr>
</thead>
<tbody><tr>
<td>증강창 열림 중 이동/공격</td>
<td>차단됨(모달)</td>
<td>정상 동작</td>
</tr>
<tr>
<td>다른 UI와 동시 오픈</td>
<td>IMC 우선순위 충돌 가능</td>
<td>IMC 하나로 유지, 충돌 없음</td>
</tr>
<tr>
<td>여러 증강 제안 동시 발생</td>
<td>처리 순서 불명확</td>
<td>대기열로 순차 처리</td>
</tr>
</tbody></table>
<p><strong>얻은 효과</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>성능</strong></td>
<td>IMC를 매번 갈아끼우는 비용이 없어짐</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>입력 게이팅 로직이 &quot;패널 열림 여부 체크&quot; 한 줄로 단순화</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>파츠 UI 등 이후 다른 오버레이 UI도 같은 게이팅 패턴을 참고해 설계됨</td>
</tr>
<tr>
<td><strong>협업</strong></td>
<td>기획 쪽 요구사항(&quot;비차단으로 바꿔달라&quot;)이 코드 구조 변경 없이 게이팅 조건 하나로 수용됨</td>
</tr>
</tbody></table>
<hr>
<h2 id="8-세이브-시스템--영구-데이터-저장-방식-구조체-전체-vs-id">8. 세이브 시스템 — 영구 데이터 저장 방식 (구조체 전체 vs ID)</h2>
<p><strong>해결하려는 문제</strong>: 파츠 보유 정보를 세이브 파일에 어떻게 저장할지 정해야 했다.</p>
<p><strong>고려한 방법</strong></p>
<ul>
<li><p><strong>방법 1 — 구조체 통째로 저장</strong> (초기 구현): 파츠의 모든 필드(정의, 등급, 수치 등)를 구조체 그대로 세이브 파일에 직렬화.</p>
<ul>
<li>장점: 로드 시 바로 쓸 수 있는 완성된 데이터.</li>
<li>단점: 파츠 정의(DT 수치)가 나중에 바뀌면, 이미 저장된 세이브 파일의 값과 최신 정의가 어긋나는 마이그레이션 문제가 생김. 저장 데이터 크기도 더 큼.</li>
</ul>
</li>
<li><p><strong>방법 2 — ID만 저장</strong> (전환 후): 파츠의 정의 ID만 저장하고, 로드 시 DT에서 최신 정의를 다시 조회해 복원.</p>
<ul>
<li>장점: 파츠 정의가 바뀌어도 항상 최신 값으로 복원되어 마이그레이션 문제가 없음. 저장 데이터 크기도 작음.</li>
<li>단점: 로드 시점에 DT 조회가 한 번 더 필요(단, 이미 DT는 캐싱돼 있어 비용은 작음).</li>
</ul>
</li>
</ul>
<p><strong>최종 선택</strong>: 방법 2</p>
<p><strong>선택 이유</strong>: 파츠 자체가 DA/DT로 정의돼 있으니, 저장 시엔 ID만 들고 있다가 로드 시 DT를 조회해 복원하는 쪽이 데이터량과 마이그레이션 안정성 면에서 유리하다는 걸 확인했기 때문이다.</p>
<p><strong>구현 구조</strong></p>
<pre><code>[Before] SaveGame.OwnedParts: TArray&lt;FNSPartFullStruct&gt;   ← 파츠 전체 스탯/정의를 통째로 저장
              │
              ▼
   파츠 DT 수치가 바뀌면 저장된 값과 최신 정의가 어긋남 (마이그레이션 문제)

[After]  SaveGame.OwnedParts: TArray&lt;FNSPartId&gt;           ← ID만 저장
              │
              ▼
   로드 시 DataSubsystem::GetPartRow(Id)로 최신 정의를 다시 조회해 복원</code></pre><p><strong>결과</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Before (구조체 저장)</th>
<th>After (ID 저장)</th>
</tr>
</thead>
<tbody><tr>
<td>파츠 정의 수치 변경 시</td>
<td>기존 세이브와 값이 어긋남</td>
<td>항상 최신 정의로 복원, 문제 없음</td>
</tr>
<tr>
<td>세이브 파일 크기</td>
<td>필드 수만큼 큼</td>
<td>ID만 저장, 작음</td>
</tr>
<tr>
<td>로드 로직</td>
<td>저장값 그대로 사용</td>
<td>DT 조회 1회 후 복원</td>
</tr>
</tbody></table>
<p><strong>얻은 효과</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>성능</strong></td>
<td>저장 파일 크기가 작아 저장/로드 자체가 가벼움</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>파츠 밸런스를 수정해도 세이브 마이그레이션 스크립트가 필요 없음</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>같은 &quot;ID만 저장, 로드 시 DT 복원&quot; 원칙이 이후 다른 시스템(가이드 진행 상태 등)의 저장 필드에도 참고 기준이 됨</td>
</tr>
<tr>
<td><strong>협업</strong></td>
<td>밸런스 담당자가 DT 값을 자유롭게 조정해도 기존 플레이어 세이브가 깨지지 않음</td>
</tr>
</tbody></table>
<hr>
<h2 id="9-가이드온보딩-시스템--안내-항목-구조-설계">9. 가이드(온보딩) 시스템 — 안내 항목 구조 설계</h2>
<p><strong>해결하려는 문제</strong>: 유저 테스트에서 &quot;조작법을 모르겠다&quot;, &quot;인런에서 뭘 해야 하는지 모르겠다&quot;는 피드백을 받았다. 화면에 안내를 띄워야 하는데, 안내할 내용이 하나가 아니라 여러 개(이동/점프/대시 3가지, 이후 캐릭터 콘솔·게임시작 콘솔·NPC 방문까지)이고, 이 개수는 앞으로도 계속 늘어날 걸로 예상됐다.</p>
<p><strong>고려한 방법</strong></p>
<ul>
<li><p><strong>방법 1 — 단일 텍스트 위젯</strong> (실제로 07/14에 먼저 구현했던 방식): HUD에 텍스트 위젯 하나를 두고, 지금 안내해야 할 문구를 DataTable에서 읽어와 그 위젯에 갈아 끼움.</p>
<ul>
<li>장점: 구현이 가장 빠르고 단순함. 위젯 하나, 문구 DT 하나로 끝남.</li>
<li>단점: 안내 항목이 &quot;동시에 여러 개&quot; 표시돼야 하는 순간(조작법 3단계) 표현이 안 됨. 항목별로 &quot;이건 끝났고 이건 안 끝났다&quot;는 상태를 표시할 방법이 없어, 결국 항목이 늘어날 때마다 위젯을 다시 설계해야 함.</li>
</ul>
</li>
<li><p><strong>방법 2 — 배열 기반 체크리스트 위젯</strong> (최종 채택): 안내 항목을 배열 데이터로 구성하고, 항목 하나짜리 엔트리 위젯 + 그걸 여러 개 담는 컨테이너 위젯으로 표시. 항목마다 개별 완료 처리가 가능.</p>
<ul>
<li>장점: 항목이 몇 개든 데이터(배열)만 추가하면 위젯/로직 변경 없이 반영됨. 개별 항목 완료와 &quot;전체 완료 시 컨테이너 닫기&quot; 판정을 컨테이너가 일괄 처리.</li>
<li>단점: 엔트리 위젯 + 컨테이너 위젯 두 개를 새로 만들어야 하고, 이미 만들어 둔 단일 텍스트 위젯을 통째로 버려야 함. 초기 구현 비용이 방법 1보다 큼.</li>
</ul>
</li>
</ul>
<p><strong>최종 선택</strong>: 방법 2</p>
<p><strong>선택 이유</strong>: 유저 피드백이 &quot;조작법&quot;에서 끝나지 않고 &quot;인런에서 뭘 해야 하는지&quot;까지 이어졌기 때문에, 안내 항목이 하나로 끝나지 않고 계속 늘어날 게 뻔했다. 실제로도 이동/점프/대시 3개 → 캐릭터 콘솔 → 게임시작 콘솔 → NPC 방문까지 늘어났다. 방법 1로 계속 밀고 갔다면 항목이 늘 때마다 위젯을 다시 만들어야 했을 것이고, 방법 2는 늘어나는 부분을 데이터로만 흡수하기 때문에 장기적으로 비용이 훨씬 적다고 판단했다.</p>
<p><strong>구현 구조</strong></p>
<pre><code>[씬 진입] PlayerController → GuideSubsystem.StartGuide()
      │
      ▼
세이브/DataTable 준비 확인 (아직 안 됐으면 준비 완료 콜백을 기다렸다가 재시도)
      │
      ▼
GetActiveStageRowName()  ← 아웃런/인런 파생 클래스가 &quot;지금 몇 번째 단계인지&quot;만 판단
      │
      ├─ 진행할 단계 없음 → HUDWidget.HideGuideChecklist()
      │
      ▼
BuildStageEntries(RowName, OutEntries)  ← 파생 클래스가 이 단계의 안내 항목을 배열로 구성
      │
      ▼
HUDWidget.ShowGuideChecklist(Entries)   컨테이너 위젯이 항목 수만큼 엔트리 위젯을 생성
      │
   [플레이어가 이동/점프/대시 입력, 콘솔 사용, NPC 방문 등 실제 행동]
      │
      ▼
NotifyXxxInput() → 세이브 플래그 갱신 → CompleteItem(ItemId)  해당 줄만 완료 애니메이션
      │
      ▼
(컨테이너의 모든 줄이 완료되어 비워짐) → NotifyChecklistEmptied()
      │
      ▼
GetActiveStageRowName() 재호출 → 다음 단계 있으면 위 과정 반복, 없으면 숨김</code></pre><p>공용 골격(<code>UNSGuideSubsystemBase</code>)이 &quot;준비 확인 → 단계 판단 → 항목 표시 → 완료 감지 → 재평가&quot;라는 흐름 전체를 갖고 있고, 아웃런/인런 파생 클래스는 <code>GetActiveStageRowName()</code>과 <code>BuildStageEntries()</code> 두 함수만 채운다.</p>
<p><strong>결과</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Before (방법 1, 07/14)</th>
<th>After (방법 2, 07/21)</th>
</tr>
</thead>
<tbody><tr>
<td>안내 항목 표현</td>
<td>텍스트 한 줄, 순차 교체</td>
<td>배열 항목, 동시에 여러 줄 + 개별 완료 표시</td>
</tr>
<tr>
<td>새 안내 단계 추가</td>
<td>위젯 로직을 다시 손봐야 함</td>
<td>DT/배열에 항목만 추가</td>
</tr>
<tr>
<td>대상 씬</td>
<td>아웃런 전용 서브시스템</td>
<td>공용 베이스 + 아웃런/인런 파생으로 확장</td>
</tr>
</tbody></table>
<p><strong>얻은 효과</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>성능</strong></td>
<td><code>CurrentChecklistStage</code>로 &quot;지금 표시 중인 단계와 같으면 재스폰하지 않음&quot; 가드를 둬서, 매 입력마다 위젯을 다시 만드는 대신 필요한 시점에만 컨테이너를 새로 그림</td>
</tr>
<tr>
<td><strong>유지보수</strong></td>
<td>신규 안내 단계를 추가할 때 건드릴 곳이 파생 클래스의 두 함수(<code>GetActiveStageRowName</code>, <code>BuildStageEntries</code>)로 고정돼 있어, 공용 골격 코드는 안 건드려도 됨</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>아웃런 전용으로 시작한 구조가 인런 튜토리얼까지 커버하도록 그대로 확장됐고, 이후 웨이포인트 마커 시스템도 이 서브시스템의 판단 결과를 그대로 받아 쓰는 식으로 얹혔음</td>
</tr>
<tr>
<td><strong>협업</strong></td>
<td>안내 문구는 GuideText DataTable로 분리돼 있어 기획자가 문구만 수정 가능하고, 코드는 &quot;몇 번째 단계인지&quot;와 &quot;그 단계에 뭘 보여줄지&quot;만 담당</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL]]></title>
            <link>https://velog.io/@kyu_/TIL-biacyckb</link>
            <guid>https://velog.io/@kyu_/TIL-biacyckb</guid>
            <pubDate>Tue, 21 Jul 2026 11:59:28 GMT</pubDate>
            <description><![CDATA[<p>점프 파츠 먹었을때 한번만 먹기만 하면 게임 끝 다음 아웃런갈때까지도 무조건 점프가 여러번됨 이게 안없어지는 버그</p>
<h2 id="예상">예상</h2>
<p>RemoveGEForSlot부분이 있는데 여기서</p>
<pre><code class="language-c++">if (UAbilitySystemComponent* ASC = GetOwnerASC()){
    ASC-&gt;RemoveActiveGameplayEffect(*Handle);
}
ActiveGEHandles.Remove(Slot);</code></pre>
<p>GetOwnerASC()는 PS-&gt;GetPawn()이 유효할때만 ASC를 반환하는데 그 순간 ASC를 못 찾으면 RemoveActiveGameplayEffect는 호출 안되고도 핸들 기록(ActiveGEHandles.Remove(Slot))은 그냥 지워짐. 그러면 그 Infinite GE는 ASC에 영원히 남고, 이걸 평생 지울 방법도 없기때문에 그냥 계속 지속되는게 아닐까? 추정</p>
<h3 id="가드패턴-하나더">가드패턴 하나더?</h3>
<p>ClearAll에서 GE를 없앨때 지금은 장착된 장비기준으로 정리했지만 ActiveGEHandles에 남아있는 모든 슬롯을 기준으로 정리하는식으로 교체?? -&gt; 만약에 이게 가능하다면 이미 교체되어서 없는 고아핸들들도 강제로 청소할 수 있게된다.</p>
<h2 id="교체">교체</h2>
<p>UNSPartEquipComponent::ClearAll에서</p>
<pre><code class="language-c++">TArray&lt;FGameplayTag&gt; HandleSlots;
ActiveGEHandles.GetKeys(HandleSlots);
for (const FGameplayTag&amp; Slot : HandleSlots)
{
    RemovePartEffects(Slot);
}
for (const FGameplayTag&amp; Slot : ClearedSlots)
{
    RemovePartEffects(Slot);
}</code></pre>
<p>이런식으로 추가하지않을까 싶음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - 조작법 가이드 + 파츠 드롭 버그 수정 및 UI수정]]></title>
            <link>https://velog.io/@kyu_/TIL-%EC%A1%B0%EC%9E%91%EB%B2%95-%EA%B0%80%EC%9D%B4%EB%93%9C-%ED%8C%8C%EC%B8%A0-%EB%93%9C%EB%A1%AD-%EB%B2%84%EA%B7%B8-%EC%88%98%EC%A0%95-%EB%B0%8F-UI%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@kyu_/TIL-%EC%A1%B0%EC%9E%91%EB%B2%95-%EA%B0%80%EC%9D%B4%EB%93%9C-%ED%8C%8C%EC%B8%A0-%EB%93%9C%EB%A1%AD-%EB%B2%84%EA%B7%B8-%EC%88%98%EC%A0%95-%EB%B0%8F-UI%EC%88%98%EC%A0%95</guid>
            <pubDate>Mon, 20 Jul 2026 12:05:46 GMT</pubDate>
            <description><![CDATA[<h1 id="아웃런-조작법-가이드-설계">아웃런 조작법 가이드 설계</h1>
<h2 id="배경">배경</h2>
<p>현재 <code>UNSOutRunGuideSubsystem</code>은 아웃런(허브)에서 &quot;캐릭터 선택 콘솔 → 게임시작 콘솔 → 신규 NPC&quot; 순으로 목적지 안내(마커+HUD 텍스트)만 제공한다. 이동/점프/대시 같은 기본 조작법 안내가 없어서, 처음 접속한 유저가 조작을 모를 수 있다</p>
<h2 id="목표">목표</h2>
<p>아웃런 진입 시 목적지 안내 앞 단계로 조작법 안내(이동 → 점프 → 대시)를 추가한다. 인런에는 적용하지 않는다.</p>
<h2 id="요구사항">요구사항</h2>
<ol>
<li>조작법 안내 순서: 이동(WASD) → 점프(Space) → 대시(Shift) → 기존 캐릭터 콘솔 안내 → 게임시작 콘솔 안내 → 신규 NPC 안내</li>
<li>각 조작법 단계는 <strong>해당 행동 1회 발동</strong> 또는 <strong>타임아웃 경과</strong> 중 먼저 오는 조건으로 완료 처리한다.<ul>
<li>이동: 이동 입력이 한 번이라도 들어오면 완료 (W/A/S/D 개별 체크 아님)</li>
<li>점프: 점프 액션 1회 발동</li>
<li>대시: 대시 어빌리티(<code>Ability_Common_Dash</code>) 입력 1회 발동</li>
</ul>
</li>
<li>상호작용 키 안내는 별도 단계를 만들지 않는다 (기존 캐릭터 콘솔 상호작용이 곧 상호작용 튜토리얼 역할).</li>
<li>타임아웃은 <code>DT_GuideText</code>의 <code>AutoAdvanceSeconds</code> 필드로 행마다 설정한다. <code>0</code>이면 타임아웃 없음(무한 대기) — 기존 콘솔/NPC 안내 행은 값 미설정으로 지금과 동일하게 동작.</li>
<li>타임아웃으로 자동 진행된 단계도 실제 행동으로 완료된 것과 동일하게 취급하여 영구 저장한다 (다음에 또 뜨지 않음).</li>
<li>조작법 안내는 계정당 1회 — <code>NSPermanentSaveGame</code>에 플래그를 저장한다.</li>
<li>아이콘은 지금 추가하지 않는다. 추후 <code>FNSGuideTextData</code>에 필드 추가로 확장 가능한 구조를 유지한다 (지금은 텍스트만)</li>
</ol>
<h2 id="데이터-변경">데이터 변경</h2>
<h3 id="nspermanentsavegameh"><code>NSPermanentSaveGame.h</code></h3>
<p>기존 <code>bCharacterConsoleGuideDone</code> 위쪽에 3개 플래그 추가:</p>
<pre><code class="language-cpp">bool bMoveGuideDone = false;
bool bJumpGuideDone = false;
bool bDashGuideDone = false;</code></pre>
<h3 id="fnsguidetextdata-nsguidetextdatah"><code>FNSGuideTextData</code> (<code>NSGuideTextData.h</code>)</h3>
<p>필드 추가:</p>
<pre><code class="language-cpp">// 0이면 타임아웃 없음 — 실제 행동/상호작용 전까지 무한 대기
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;UI&quot;)
float AutoAdvanceSeconds = 0.0f;</code></pre>
<h3 id="dt_guidetext-에디터-작업-사용자가-직접"><code>DT_GuideText</code> (에디터 작업, 사용자가 직접)</h3>
<p>행 3개 추가: <code>MoveInput</code>, <code>JumpInput</code>, <code>DashInput</code>. <code>AutoAdvanceSeconds</code>에 10 정도 입력 (밸런스는 에디터에서 조정). 기존 행(<code>CharacterSelectConsole</code>, <code>NSReadyStartActor</code>, <code>NewNPC</code>)은 그대로 둔다.</p>
<h2 id="코드-변경">코드 변경</h2>
<h3 id="nsoutrunguidesubsystemhcpp"><code>NSOutRunGuideSubsystem.h/.cpp</code></h3>
<ul>
<li><code>NotifyMoveInput()</code> / <code>NotifyJumpInput()</code> / <code>NotifyDashInput()</code> 추가 — 기존 <code>NotifyCharacterConsoleUsed()</code>와 동일한 패턴:<ul>
<li><code>bGuideStarted</code> 게이트 체크</li>
<li><strong>현재 안내 우선순위가 자기 단계일 때만</strong> 완료 처리 (순서 보장 — 예: 이동 안내가 떠 있지 않은데 우연히 이동 입력이 들어와도 무시하지 않고, 애초에 이동 안내 중에만 호출되도록 판정)</li>
<li>완료 → 플래그 세팅 → <code>SaveGuideState()</code> → <code>RefreshGuide()</code></li>
</ul>
</li>
<li><code>RefreshGuide()</code>/<code>UpdateGuideText()</code>의 우선순위 체인 맨 앞에 이동/점프/대시 3단계 추가 (기존 <code>bNeedCharacterGuide</code> 등과 동일한 방식의 <code>bNeedMoveGuide</code>/<code>bNeedJumpGuide</code>/<code>bNeedDashGuide</code>)</li>
<li>타임아웃 처리: 안내 텍스트를 세팅하는 시점에 해당 행의 <code>AutoAdvanceSeconds &gt; 0</code>이면 <code>FTimerHandle</code>로 타이머 시작. 만료 시 해당 단계 완료 경로(Notify와 동일)를 그대로 호출. 안내가 다른 단계로 바뀌거나 입력으로 먼저 완료되면 타이머를 취소한다.</li>
<li>조작법 3단계는 목적지가 없으므로 마커 토글 없음 — HUD 텍스트만 갱신.</li>
</ul>
<h3 id="nsinputbindercomponentcpp"><code>NSInputBinderComponent.cpp</code></h3>
<ul>
<li><code>Input_Move(const FInputActionValue&amp; Value)</code>: 이동값이 0이 아닐 때 <code>GetWorld()-&gt;GetSubsystem&lt;UNSOutRunGuideSubsystem&gt;()-&gt;NotifyMoveInput()</code> 호출 (조용히 무시 패턴, 기존 콘솔 Notify 호출부와 동일)</li>
<li><code>Input_Jump()</code>: 함수 진입 시 <code>NotifyJumpInput()</code> 호출</li>
<li><code>Input_AbilityPressed(FGameplayTag InputTag)</code>: <code>InputTag == NSGameplayTags::Ability_Common_Dash</code>일 때 <code>NotifyDashInput()</code> 호출</li>
</ul>
<h2 id="영향-범위-확인">영향 범위 확인</h2>
<ul>
<li>인런에서는 <code>bGuideStarted</code>가 <code>false</code>이므로(아웃런 진입 시에만 <code>StartGuide()</code> 호출) 모든 Notify가 조용히 무시됨 — 기존 게이트 재사용, 별도 분기 불필요.</li>
<li>이미 조작법 안내를 완료한 계정은 플래그가 모두 <code>true</code>이므로 아웃런 재진입 시 바로 기존 콘솔 안내부터 시작 (기존 동작과 동일).</li>
</ul>
<h2 id="확인된-전제">확인된 전제</h2>
<ul>
<li>아웃런(허브)에서 점프/대시 어빌리티가 실제로 활성화되어 있음 (사용자 확인 완료).</li>
</ul>
<h1 id="트러블슈팅">트러블슈팅</h1>
<p>파츠 시스템 및 NPC 상호작용 UI 관련 버그/이슈 기록. (2026-07-20, <code>hotfix/UI-Input-Error</code> 브랜치)</p>
<hr>
<h2 id="1-npc-상호작용-ui를-닫으면-게임-인풋이-안-먹는-버그">1. NPC 상호작용 UI를 닫으면 게임 인풋이 안 먹는 버그</h2>
<h3 id="증상">증상</h3>
<p>파츠 NPC(장착/업그레이드)는 정상인데, 펫 강화 NPC와 공용 업그레이드 NPC는 UI를 닫은 후 이동 등 게임 인풋이 전혀 먹지 않음.</p>
<h3 id="원인">원인</h3>
<ul>
<li>NPC 상호작용 시작 시 <code>ANSPlayerController::OpenInteractionWidget</code>이 위젯을 열면서 <code>InputBinder</code>의 활성 입력 태그를 UI 전용으로 바꿔 게임플레이 IMC를 제거함.</li>
<li>이 게임플레이 IMC 복원은 오직 <code>NotifyInteractionWidgetClosed()</code>(컨트롤러 측)에서만 일어나는데, 각 상호작용 위젯이 <code>CloseWidget()</code> 안에서 이 통지를 직접 호출해줘야 하는 구조였음.</li>
<li>파츠 장착/업그레이드 위젯 2개는 통지를 호출했지만, 펫 강화·공용 업그레이드 위젯은 빠뜨림 → <code>SetInputMode(GameOnly)</code>는 걸리는데 캐릭터의 게임플레이 IMC 자체는 복원되지 않아 키 입력이 무시됨.</li>
</ul>
<h3 id="근본-원인-진단">근본 원인 진단</h3>
<p><code>UNSNPCInteractionWidgetBase</code>가 빈 가상 함수 2개(<code>OpenForInteractor</code>/<code>CloseWidget</code>)만 있는 순수 껍데기였고, <code>OwningController</code> 멤버와 &quot;닫을 때 통지&quot; 절차를 파생 위젯 4개(<code>NSPartUpgradeWidget</code>, <code>NSPartEquipWidget</code>, <code>NSPetUpgradeWidget</code>, <code>NSCommonUpgradeWidget</code>)가 각자 복붙해서 구현하던 구조. 복붙 과정에서 통지 호출이 2곳에서 누락됨.</p>
<h3 id="해결">해결</h3>
<p><code>UNSNPCInteractionWidgetBase</code>를 템플릿 메서드 패턴으로 재구성:</p>
<ul>
<li><code>OwningController</code>를 베이스로 이동 (파생 클래스 중복 선언 제거)</li>
<li><code>CloseWidget()</code>을 <strong>non-virtual 최종 진입점</strong>으로 만들어 <code>OnCloseWidget()</code>(파생 클래스 커스텀 정리) → 컨트롤러 통지(<code>NotifyInteractionWidgetClosed</code>) → <code>RemoveFromParent()</code> 순서를 항상 보장</li>
<li>파생 위젯은 <code>CloseWidget()</code> 대신 <code>OnCloseWidget()</code>만 override — 통지를 빠뜨릴 수 없는 구조로 변경</li>
</ul>
<hr>
<h2 id="2-common-등급에-유효-스탯이-없는-파츠대시-파츠-등가-드랍상점에-노출">2. Common 등급에 유효 스탯이 없는 파츠(대시 파츠 등)가 드랍/상점에 노출</h2>
<h3 id="증상-1">증상</h3>
<p>대시 횟수 증가 파츠처럼 Common 등급 <code>ValueRangesByRarity</code> 키가 정의되지 않은 파츠가 필드 드랍, 인런 상점 재고, 아웃런 파츠 카탈로그에 스탯 없는 상태로 등장.</p>
<h3 id="원인-1">원인</h3>
<p>드랍/상점 생성 로직이 <strong>파츠를 먼저 뽑고 나서</strong> 스탯을 등급으로 필터링하는 순서였음. &quot;등급 키 없음 = 그 등급에서 제외&quot;라는 규칙이 스탯 선택 단계에만 적용되고, 파츠 선택 단계에는 적용되지 않아 유효 스탯이 하나도 없는 파츠도 그대로 선택될 수 있었음.</p>
<h3 id="해결-1">해결</h3>
<p>파츠 선택 이전에 &quot;이 등급에서 유효한 스탯이 1개 이상인 파츠&quot;만 후보로 거르도록 3곳 모두 순서를 바꿈:</p>
<ol>
<li><strong>필드 드랍</strong> (<code>NSRewardHandler::MakePartDataFromDropResult</code>): 등급 해석 → 후보 필터링 → 파츠 선택 순서로 변경</li>
<li><strong>인런 상점 재고</strong> (<code>NSPartEquipComponent::GenerateShopStock</code>): &quot;파츠 선택 → 등급 롤&quot;에서 &quot;등급 롤 → 그 등급 유효 파츠만 후보 수집 → 선택&quot;으로 순서 변경</li>
<li><strong>아웃런 카탈로그</strong> (<code>NSPartEquipWidget::BuildPartEntries</code>): Common에서 유효 스탯 없는 파츠는 카탈로그 자체에서 제외</li>
<li><strong>아웃런 구매 방어선</strong> (<code>NSProgressionSubsystem</code>의 구매 함수): UI 필터를 우회해 호출되더라도 유효 스탯 없으면 구매 자체를 차단 (2차 방어)</li>
</ol>
<hr>
<h2 id="3-설계-변경-아웃런-파츠-구매-수치를-랜덤-대신-고정값으로">3. 설계 변경: 아웃런 파츠 구매 수치를 랜덤 대신 고정값으로</h2>
<h3 id="배경-1">배경</h3>
<p>아웃런 파츠 구매는 재도전이 없는 1회성 구매라 랜덤 롤이 &quot;운&quot;의 영역이 되는 게 부적절하다고 판단, 해당 등급(Common) 범위의 <strong>최대값으로 고정</strong>하도록 설계 변경.</p>
<h3 id="해결-2">해결</h3>
<ul>
<li><code>NSProgressionSubsystem</code>의 구매 함수에서 <code>FMath::RandRange(Range.Min, Range.Max)</code> → <code>Range.Max</code> 고정값으로 변경</li>
<li><code>NSPartDetailWidget::SetupFromDefinition</code>(아웃런 카탈로그 미리보기)도 &quot;효과 : {스탯} {최소}~{최대}&quot; 범위 표시에서 &quot;효과 : {스탯} {최대값}&quot; 고정값 표시로 변경 (구매 로직과 일치)</li>
</ul>
<hr>
<h2 id="4-파츠-스탯-수치-표시-개선--단위·초--증가감소-라벨">4. 파츠 스탯 수치 표시 개선 — 단위(%·초) + 증가/감소 라벨</h2>
<h3 id="요구사항-1">요구사항</h3>
<ul>
<li>확률형 스탯(크리티컬 확률 등)은 수치 뒤에 <code>%</code> 표시</li>
<li>시간형 스탯(쿨다운 감소 등)은 수치 뒤에 <code>초</code> 표시</li>
<li>실제 수치 뒤에 부호에 따라 &quot;증가&quot;/&quot;감소&quot; 라벨 부착 (예: 쿨다운 감소 파츠가 -2를 가지면 &quot;2초 감소&quot;로 표시 — 절댓값 + 단어로 표현해 마이너스 기호와 라벨이 겹치지 않게 함)</li>
<li>범위 미리보기(리롤 변동폭, 다음 등급 범위)처럼 방향이 아직 확정되지 않은 곳은 단위만 적용하고 증가/감소는 생략</li>
</ul>
<h3 id="해결-3">해결</h3>
<ul>
<li><code>FNSStatDisplayInfoRow</code>에 <code>ENSStatDisplayUnit</code>(없음/%/초) 필드 <code>DisplayUnit</code> 추가 — 에디터에서 스탯별로 지정</li>
<li>공용 포맷 함수 <code>NSPartUtils::FormatStatValueText(WorldContext, StatTag, Value, bShowDirection=true)</code> 신설 — 기존에 3곳(NSPartDetailWidget, NSPartUpgradeWidget, NSPartSlotButton)에 흩어져 있던 &quot;소수점 내림 포맷&quot; 로컬 헬퍼를 이 함수 하나로 통합</li>
<li>아웃런 카탈로그/장착 상세, 인런 파츠 슬롯 버튼, 인런 업글/리롤 패널 전부 이 함수를 사용하도록 교체</li>
</ul>
<h3 id="에디터-작업-필요">에디터 작업 필요</h3>
<p>스탯 표시 DT(<code>DT_PartStatDisplay</code>)에서 확률형/시간형 스탯에 <code>DisplayUnit</code> 값을 지정해야 실제로 반영됨. 지정 안 하면 기존처럼 단위 없이 수치+증가/감소만 표시.</p>
<hr>
<h2 id="5-인런-리롤-패널에-리롤-전-기존-수치-표시-추가">5. 인런 리롤 패널에 리롤 전 기존 수치 표시 추가</h2>
<h3 id="요구사항-2">요구사항</h3>
<p>리롤 비용(<code>RerollCostText</code>) 아래에 리롤로 바뀌기 전 현재 스탯 수치를 &quot;기존 수치 : {값}&quot; 형태로 같이 보여줘서 변동폭과 비교 가능하게.</p>
<h3 id="해결-4">해결</h3>
<ul>
<li><code>NSPartUpgradeWidget</code>에 <code>RerollCurrentValueText</code> 위젯 바인딩 추가</li>
<li><code>RefreshUpgradePanels()</code>에서 선택된 파츠의 현재 수치를 표시, 파츠 미선택 시 빈 텍스트로 초기화</li>
</ul>
<h3 id="에디터-작업-필요-1">에디터 작업 필요</h3>
<p>WBP 업그레이드/리롤 페이지에서 <code>RerollCostText</code> 아래에 TextBlock 추가 후 이름을 <code>RerollCurrentValueText</code>로 지정.</p>
<hr>
<h2 id="6-인런-상점-구매가격-표시를-buypricetext-대신-costtext로-통합">6. 인런 상점 구매가격 표시를 BuyPriceText 대신 CostText로 통합</h2>
<h3 id="배경-2">배경</h3>
<p>인런 상점 재고 클릭 시 가격이 <code>NSPartUpgradeWidget</code> 자체의 <code>BuyPriceText</code>에 표시되고 있었는데, 바로 위 <code>WBP_PartDetail</code>(<code>StockDetailWidget</code>)에 이미 <code>CostText</code>가 있어서 중복 위젯이었음.</p>
<h3 id="해결-5">해결</h3>
<ul>
<li><code>UNSPartDetailWidget::SetupFromInstance</code>에 <code>int64 Price = -1</code> 파라미터 추가. <code>Price &gt;= 0</code>이면 <code>CostText</code>에 &quot;비용 : {Price}&quot; 표시, 기본값(-1)이면 비용 줄 생략(장착중인 파츠 표시 등 판매 대상이 아닌 경우)</li>
<li><code>NSPartUpgradeWidget::OnStockEntryClicked</code>에서 <code>EquipComp-&gt;GetShopPrice(...)</code>로 가격을 계산해 <code>StockDetailWidget-&gt;SetupFromInstance(Item, Def, Price)</code>로 전달</li>
<li><code>BuyPriceText</code> 필드 및 관련 코드 제거</li>
</ul>
<h3 id="에디터-작업-필요-2">에디터 작업 필요</h3>
<p>WBP에서 이제 안 쓰는 <code>BuyPriceText</code> 바인딩 위젯은 삭제해도 무방. <code>StockDetailWidget</code>(WBP_PartDetail)의 <code>CostText</code>가 화면에 노출되도록 배치 확인.</p>
<hr>
<h2 id="7-아웃런-조작법-가이드--대시-안내가-안-사라지는-버그">7. 아웃런 조작법 가이드 — 대시 안내가 안 사라지는 버그</h2>
<h3 id="증상-2">증상</h3>
<p>이동/점프 안내는 정상적으로 다음 단계로 넘어가는데, 대시(Shift) 안내만 아무리 눌러도 사라지지 않음.</p>
<h3 id="원인-2">원인</h3>
<p><code>NSInputBinderComponent::Input_AbilityPressed</code>에서 대시 입력을 감지하려고 <code>InputTag == NSGameplayTags::Ability_Common_Dash</code>로 비교했는데, 이건 <strong>어빌리티 자신을 식별하는 태그</strong>(<code>GA_Dash</code>의 <code>AssetTags</code>, <code>TryActivateAbilitiesByTag</code> 같은 데서 씀)였음. 실제로 <code>Input_AbilityPressed</code>에 넘어오는 <code>InputTag</code>는 GAS 입력 바인딩용 태그(<code>UNSAbilitySystemComponent::AbilityInputTagPressed</code>가 <code>AbilitySpec.GetDynamicSpecSourceTags()</code>와 매칭)라서, 서로 다른 태그 계층인 두 값이 절대 일치하지 않아 알림이 아예 호출되지 않았음.</p>
<h3 id="해결-6">해결</h3>
<p>비교 대상을 입력 전용 태그로 교체: <code>InputTag == NSGameplayTags::Input_Ability_Dash</code> (<code>NSGameplayTags_Input.h</code>에 선언된 <code>&quot;Input.Ability.Dash&quot;</code>).</p>
<h3 id="교훈">교훈</h3>
<p>GAS에서 &quot;어빌리티 자체 태그&quot;(<code>AssetTags</code>, <code>TryActivateAbilitiesByTag</code>용)와 &quot;입력 바인딩 태그&quot;(<code>InputTag</code>, <code>AbilityInputTagPressed</code>용)는 이름이 비슷해 보여도 완전히 다른 값일 수 있다. 어빌리티 활성화 방식(태그 기반 vs 입력태그 기반)에 따라 어느 쪽 태그를 비교해야 하는지 먼저 확인할 것.</p>
<h3 id="관련-파일">관련 파일</h3>
<ul>
<li><code>Source/NeoSanctum/Character/Component/NSInputBinderComponent.cpp</code> (<code>Input_AbilityPressed</code>)</li>
<li><code>Source/NeoSanctum/Tag/NSGameplayTags_Input.h</code>, <code>NSGameplayTags_Ability.h</code></li>
</ul>
<hr>
<h2 id="8-아웃런-조작법-가이드--완료-조건-설계를-두-번-뒤집음">8. 아웃런 조작법 가이드 — 완료 조건 설계를 두 번 뒤집음</h2>
<h3 id="배경-3">배경</h3>
<p>아웃런 진입 시 이동→점프→대시 순으로 조작법을 안내하는 기능을 추가하면서, &quot;언제 다음 단계로 넘어가는가&quot; 기준이 논의 중 두 번 바뀜.</p>
<p>1차: 입력 1회 발동 <strong>또는</strong> DT의 <code>AutoAdvanceSeconds</code> 경과(둘 중 먼저 오는 쪽) → 입력이 들어오면 즉시 완료.
2차: 텍스트가 뜨자마자 실제 이동 여부와 상관없이 몇 초 뒤 사라지는 게 이상하다는 피드백으로, &quot;키 입력은 전부 받되, 입력이 들어온 시점부터 <code>AutoAdvanceSeconds</code>만큼 대기 후 완료&quot;로 변경(최종). 즉 입력이 없으면 영원히 안 넘어가고, 입력 후에도 즉시 사라지지 않고 지정된 초만큼 텍스트가 더 보임</p>
<h3 id="최종-구조">최종 구조</h3>
<ul>
<li><code>NotifyMoveInput/NotifyJumpInput/NotifyDashInput</code>: 입력이 들어오면 (그리고 이전 단계가 끝났으면) <code>StartStageTimeout()</code> 호출</li>
<li><code>StartStageTimeout(RowName, CompleteFunc)</code>: 이미 타이머가 도는 중이면 무시(반복 입력에 재시작 안 됨). <code>DT_GuideText</code>에서 <code>AutoAdvanceSeconds</code>를 찾아 그 시간 뒤 완료 함수 호출. 값이 0 이하면 즉시 완료</li>
<li><code>UpdateGuideText()</code>는 텍스트 표시만 담당, 타이머는 전혀 건드리지 않음(표시만으로는 절대 자동 진행 안 됨)</li>
</ul>
<h3 id="교훈-1">교훈</h3>
<p>&quot;타임아웃 자동 진행&quot; 기능을 넣을 때는 &quot;입력 없이도 자동으로 넘어가야 하는가&quot;와 &quot;입력 후 텍스트를 얼마나 더 보여줄 것인가&quot;를 처음부터 구분해서 확인받을 것. 이번엔 이 둘을 섞어서 설계했다가 두 번 되돌렸음</p>
<hr>
<h2 id="9-캐릭터-선택-콘솔--같은-bp를-두-곳에-배치했는데-마커가-둘-다-뜸">9. 캐릭터 선택 콘솔 — 같은 BP를 두 곳에 배치했는데 마커가 둘 다 뜸</h2>
<h3 id="증상-3">증상</h3>
<p>캐릭터 선택 콘솔(<code>ANSCharacterSelectNPC</code>)이 레벨에 두 인스턴스 배치되어 있음(게임 시작 옆 / <code>CommonUpgradeNPC</code> 구출 후 갈 수 있는 NPC방). 튜토리얼 단계에서 게임 시작 옆 콘솔에만 마커가 떠야 하는데 둘 다 뜸.</p>
<h3 id="잘못된-접근-1차">잘못된 접근 (1차)</h3>
<p><code>ANSInteractableNPCBase</code>의 <code>NPCId</code> 필드가 이미 있길래 여기에 구분값을 넣고 <code>UnlockedNPCIds</code>로 해금 여부를 체크하는 코드를 넣었음. 하지만 캐릭터 선택 콘솔은 애초에 <code>NPCId</code>를 쓰는 액터가 아니었고(항상 비어있음), 사용자가 원한 것도 &quot;해금 여부에 따라 마커 표시&quot;가 아니라 &quot;같은 BP 중 특정 배치 인스턴스 하나만 항상 마커 대상&quot;이었음. 완전히 다른 요구사항에 엉뚱한 필드를 재활용한 것.</p>
<h3 id="해결-7">해결</h3>
<p>같은 블루프린트의 특정 배치 인스턴스만 구분하는 표준 방법인 <strong>액터 태그(Actor Tags)</strong> 사용. 게임 시작 옆 인스턴스에만 에디터에서 <code>GuideTutorial</code> 태그를 붙이고, 코드는 <code>ActorHasTag(TEXT(&quot;GuideTutorial&quot;))</code>로 체크.</p>
<pre><code class="language-cpp">// 캐릭터 선택 콘솔 마커 —&gt; 같은 BP가 여러 곳에 배치되므로, 레벨에서 &quot;GuideTutorial&quot; 태그를 붙인 인스턴스(게임 시작 옆)만 튜토리얼 대상
for (TActorIterator&lt;ANSCharacterSelectNPC&gt; It(GetWorld()); It; ++It)
{
    const bool bIsTutorialConsole = It-&gt;ActorHasTag(TEXT(&quot;GuideTutorial&quot;));
    SetActorMarkerLocal(*It, bNeedCharacterGuide &amp;&amp; bIsTutorialConsole);
}</code></pre>
<h3 id="교훈-2">교훈</h3>
<p>&quot;같은 클래스의 여러 배치 인스턴스 중 하나만 구분&quot;하고 싶을 때는 그 클래스에 이미 있는 필드를 억지로 재해석하지 말고 액터 태그부터 검토할 것. 필드를 재활용하면 그 필드의 원래 의미(여기선 &quot;해금 조건&quot;)가 오염돼서 나중에 헷갈림.</p>
<h3 id="관련-파일-1">관련 파일</h3>
<ul>
<li><code>Source/NeoSanctum/Core/Waypoint/NSOutRunGuideSubsystem.cpp</code> (<code>RefreshGuide</code>)</li>
</ul>
<h3 id="에디터-작업-필요-3">에디터 작업 필요</h3>
<p>게임 시작 옆 <code>ANSCharacterSelectNPC</code> 인스턴스의 Details → Actor → Tags에 <code>GuideTutorial</code> 추가. NPC방 쪽 인스턴스는 태그 없이 그대로 둠</p>
<hr>
<h2 id="10-가이드-텍스트-위젯--border-안-textblock이-과도하게-줄바꿈됨">10. 가이드 텍스트 위젯 — Border 안 TextBlock이 과도하게 줄바꿈됨</h2>
<h3 id="증상-4">증상</h3>
<p><code>Border &gt; TextBlock</code> 구조로 가이드 텍스트 배경을 만들었는데, 배경 폭을 다 채우기 전에 몇 글자마다 줄바꿈이 일어남.</p>
<h3 id="원인-3">원인</h3>
<p>TextBlock의 Border 슬롯 Horizontal Alignment가 <code>Center</code>였음. Auto Wrap은 &quot;할당된 폭&quot; 기준으로 줄바꿈하는데, HAlign=Center인 위젯은 부모의 전체 폭이 아니라 자기 컨텐츠의 최소 폭만 할당받아서, 그 좁은 폭 기준으로 계속 줄바꿈이 발생함.</p>
<h3 id="해결-8">해결</h3>
<ul>
<li>TextBlock의 Border 슬롯 <strong>Horizontal Alignment → Fill</strong> (전체 폭을 할당받게 함 → 줄바꿈이 배경 폭 기준으로 일어남)</li>
<li>텍스트 자체는 <strong>Justification → Center</strong>로 가운데 정렬 (HAlign=Fill과 별개 속성 — Fill은 &quot;얼마나 넓게 차지할지&quot;, Justification은 &quot;그 폭 안에서 글자를 어디에 놓을지&quot;)</li>
<li><code>Auto Wrap Text</code> 체크 필요</li>
</ul>
<p>이 두 속성을 같이 설정하면 짧은 텍스트(&quot;대시&quot;)는 완전 정중앙에, 긴 문장은 배경 폭을 다 채우고 줄바꿈되면서 각 줄이 가운데 정렬됨.</p>
<h3 id="참고">참고</h3>
<p>Border가 내용물 크기에 따라 늘어나는 구조(부모가 HorizontalBox/VerticalBox + Size To Content)라면 Auto Wrap의 기준 폭이 안 잡힐 수 있음 — 그럴 땐 <code>Wrap Text At</code>에 픽셀값을 직접 지정하거나 Border를 Size Box로 감싸 폭을 고정.</p>
<h3 id="에디터-작업-필요-4">에디터 작업 필요</h3>
<p>가이드 텍스트 WBP에서 TextBlock 선택 → Slot(Border Slot)의 Horizontal Alignment를 Fill로, Details의 Justification을 Center로 변경.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - 파츠 구조 변경 및 트러블 슈팅]]></title>
            <link>https://velog.io/@kyu_/TIL-%ED%8C%8C%EC%B8%A0-%EA%B5%AC%EC%A1%B0-%EB%B3%80%EA%B2%BD-%EB%B0%8F-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@kyu_/TIL-%ED%8C%8C%EC%B8%A0-%EA%B5%AC%EC%A1%B0-%EB%B3%80%EA%B2%BD-%EB%B0%8F-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Thu, 16 Jul 2026 09:17:10 GMT</pubDate>
            <description><![CDATA[<h1 id="til">TIL</h1>
<h2 id="파츠-데이터-테이블-구조변경">파츠 데이터 테이블 구조변경</h2>
<p>StatTag 후보 배열 + 인스턴스구조로 확정
Stat배열이란 이 파츠가 가질 수 있는 스텟 후보 배열 (치명타, 공격력, 치명타 데미지 증가 등)</p>
<p>이로인해 FNSPartData에 FGameplayTag StatTag필드 추가</p>
<p>지금 파츠 GE에 들어있는게</p>
<ul>
<li>공격력 Damage</li>
<li>최대체력</li>
<li>최대쉴드</li>
<li>방어력</li>
<li>이동속도</li>
<li>크리티컬확률</li>
<li>크리티컬 데미지</li>
<li>쉴드RechargeRate</li>
<li>쉴드RechargeCooldown</li>
<li>최대탄환수</li>
<li>DashRegenRate</li>
<li>MaxDashCount</li>
</ul>
<h2 id="몬스터-드롭-파츠-생성-함수-호출-순서">몬스터 드롭 파츠 생성 함수 호출 순서</h2>
<p>UNSRewardHandler::HandleRewardTrigger -&gt; UNSRewardDropResolver::ResolveDropResultsFromTable (DropTable-&gt;DropResults) -&gt; UNSRewardDropResolver::SelectDropRow -&gt; UNSRewardDropResolver::ApplyDropRowToResult</p>
<p>UNSRewardHandler::HandleDropResults -&gt; (RewardTypeTag == Part) -&gt; UNSRewardHandler::HandlePartDropResult -&gt; UNSRewardHandler::MakePartDataFromDropResult
이 과정을 통하고 있다.</p>
<ol>
<li>등급이 가장 먼저 결정 파츠 Row에 박혀있는대로</li>
<li>그 다음 어떤 파츠인지가 결정 UNSRewardHandler::MakePartDataFromDropResult에서 가장먼저 파츠가 정해지고, 등급/슬롯 필터링없이 전체 파츠 풀에서 완전 균등 랜덤</li>
<li>파츠 다음이 이제 StatTag 순회, 균등확률로 어떤 StatTag를 고를것인지</li>
<li>ValueRange는 그 후에 1번에서 정해진 그 등급대로 조회를 한다.</li>
<li>최종으로 ValueRange안에서 랜덤값을 뽑고 그것을 StatMaxValue와 곱해서 최종값</li>
</ol>
<h1 id="2차-구조-변경">2차 구조 변경</h1>
<h2 id="목표">목표</h2>
<ol>
<li>같은 파츠 종류(예: 암)가 여러 스탯 변형(치명타/공격력 등)을 가질 수 있게 — Definition 애셋/Row 복제 없이</li>
<li>스탯마다 등급별 수치 범위를 독립적으로 튜닝할 수 있게 — &quot;공격력 30<del>40&quot;과 &quot;치명타 30</del>40&quot;이 같은 범위를 공유하던 밸런스 문제 해소</li>
<li>특정 스탯(점프/대시 등 카운트 계열)을 특정 등급 전용으로 만들 수 있게 — 코드 하드코딩이 아니라 데이터로</li>
</ol>
<h2 id="데이터-모델-변화">데이터 모델 변화</h2>
<h3 id="1단계--stattag-단일-→-배열-랜덤-변형">1단계 — StatTag 단일 → 배열 (랜덤 변형)</h3>
<table>
<thead>
<tr>
<th></th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td><code>FNSPartDefinitionRow</code></td>
<td><code>StatTag</code>(단일)</td>
<td><code>StatTags</code>(배열, 후보 목록)</td>
</tr>
<tr>
<td><code>FNSPartData</code></td>
<td>스탯 없음 (Row에서 파생)</td>
<td><code>StatTag</code>(신규, 인스턴스 확정 스탯)</td>
</tr>
</tbody></table>
<p>드롭/상점 생성 시 <code>StatTags</code> 후보 중 하나가 균등 확률로 뽑혀 인스턴스에 저장된다. 같은 &quot;암&quot; Definition 하나로 치명타 변형/공격력 변형을 동시에 표현할 수 있게 됐다.</p>
<h3 id="2단계--품질-롤-→-스탯별-등급-범위-직접-지정">2단계 — 품질 롤 → 스탯별 등급 범위 직접 지정</h3>
<table>
<thead>
<tr>
<th></th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td><code>FNSStatDisplayInfoRow</code></td>
<td><code>MaxStatValue</code>(스탯 만점 1개)</td>
<td><code>ValueRangesByRarity</code>(<code>TMap&lt;ENSPartRarity, FNSPartValueRange&gt;</code>)</td>
</tr>
<tr>
<td><code>FNSPartUpgradeRow</code></td>
<td><code>ValueRange</code>(품질 0~1) + 리롤/상점 필드</td>
<td><strong><code>FNSPartShopRerollRow</code>로 개명</strong>, 리롤/상점 필드만 (수치 필드 없음)</td>
</tr>
<tr>
<td>최종 수치 계산</td>
<td><code>품질 롤(0~1) × MaxStatValue</code></td>
<td><strong>등급 범위에서 직접 롤</strong></td>
</tr>
</tbody></table>
<p>&quot;공격력: 커먼 0<del>10 / 레어 10</del>20 / 에픽 20<del>30 / 레전더리 30</del>40&quot; 처럼 스탯마다 등급 곡선을 독립적으로 그릴 수 있게 됐다. 카운트 계열(점프/대시)은 등급 범위에 정수를 직접 넣으면 되어, 이전에 floor 오차를 피하려고 쓰던 안전마진 역산(1.5, 2 같은 값)이 더 이상 필요 없다.</p>
<h2 id="아키텍처--값이-결정되는-흐름">아키텍처 — 값이 결정되는 흐름</h2>
<pre><code>파츠 인스턴스 생성 (드롭 / 인런 상점 / 아웃런 구매)
  1. 파츠 종류 확정 (전체 후보 균등 랜덤)
  2. 등급 확정 (드롭테이블 가중치 / 상점 가중치)
  3. StatTags 후보를 현재 등급 기준으로 필터링
     └ NSPartUtils::FilterStatTagsByRarity
        └ 후보마다 GetStatValueRange(스탯, 등급) 조회, 실패하면 후보에서 제외
  4. 남은 후보 중 스탯 하나 확정 (균등 랜덤) → FNSPartData::StatTag
  5. 그 스탯의 등급별 범위에서 최종 수치 롤 → FNSPartData::CurrentValue
     └ NSPartUtils::GetStatValueRange(확정 스탯, 등급) → FMath::RandRange

리롤 / 등급업
  - 스탯(StatTag)은 유지, 3~5단계만 재실행 (RollValueForPart 공용 함수)

공용 GE 적용 (장착 시)
  - GetPartStatTag(인스턴스 우선, 구세이브는 후보 첫 번째 폴백)
  - StatTag → SetByCaller 태그 매핑(NSCombatStatAttributeMapping) → GE Modifier 적용</code></pre><h1 id="pr">PR</h1>
<h2 id="변경-사항">변경 사항</h2>
<ul>
<li>파츠 GE 부여</li>
<li>DT로 스텟별 수치 조정 가능</li>
<li>기존 CombatTag사용</li>
</ul>
<h2 id="참고-사항">참고 사항</h2>
<!-- 리뷰어가 알면 좋은 내용 있으면 작성 -->

<img width="1918" height="850" alt="DT_PartDefinition" src="https://github.com/user-attachments/assets/ace3fe55-ce52-4f17-9edb-0665b621c135" />

<ul>
<li>Stat Tags에는 이 파츠에서 나오는 태그 지정</li>
<li>가능한 태그 목록은 DT_PartStatDisplay에 지정되어 있음</li>
<li>리롤 가능한지, 아웃런 해금가격 들어있음</li>
</ul>
<img width="1576" height="846" alt="DT_PartStatDisplay" src="https://github.com/user-attachments/assets/94715bd3-5229-464a-9ba1-eb12d2cd69a0" />

<ul>
<li>태그별 어떤 파츠 상호작용에 나오는 텍스트 지정</li>
<li>레어도별 스텟 설정 가능</li>
</ul>
<img width="1158" height="117" alt="DT_PartShopReroll" src="https://github.com/user-attachments/assets/0c814eb3-00c6-427c-b8d8-6b597f64eb05" />

<ul>
<li>상점과 리롤에 사용되는 DT</li>
</ul>
<img width="855" height="106" alt="DT_PartSlot" src="https://github.com/user-attachments/assets/4442fd8e-99f6-4e63-aee7-d6c3e5834e1a" />

<ul>
<li>슬롯 지정하는 DT</li>
<li>슬롯 추가 or 아웃런 슬롯해금 가격 변경등에 사용</li>
</ul>
<h1 id="트러블슈팅">트러블슈팅</h1>
<h2 id="1-maxjumpcount-파츠를-꼈는데-기본-점프-횟수가-0으로-보임">1. MaxJumpCount 파츠를 꼈는데 기본 점프 횟수가 0으로 보임</h2>
<p><strong>증상:</strong> <code>MaxJumpCount</code> 스탯을 파츠 시스템에 추가하고 캐릭터 스폰 시 기본값(1)이 적용되지 않고 0으로 보임.</p>
<p><strong>원인:</strong> <code>FGameplayAttributeData</code>는 별도 초기값을 안 주면 기본이 0이다. 캐릭터 스폰 시 실제 시작값은 <code>ApplyInitialAttributeEffect</code>가 별도의 <strong>Init GE</strong>(<code>CharacterBaseStatInitEffectClass</code>)를 적용해서 DT 기본값을 SetByCaller로 박아 넣는 구조인데, 이 Init GE 애셋은 기존에 만들어진 것이라 신규 추가한 <code>MaxJumpCount</code> 스탯용 Modifier가 빠져 있었다. <code>Internal_ApplySharedGE</code>가 쓰는 공용 파츠 GE(<code>GE_SharedPartEffectClass</code>)와는 별개 애셋이라, 파츠 GE에만 Modifier를 추가하고 Init GE 쪽을 빠뜨리기 쉽다.</p>
<p><strong>해결:</strong> Init GE에 <code>Attribute=MaxJumpCount, Op=Override, SetByCaller=Effect.SetByCaller.Init.MaxJumpCount</code> Modifier 추가. 기존 <code>MaxHealth</code>, <code>MaxDashCount</code> 등의 Modifier 구성을 그대로 참고.</p>
<p><strong>교훈:</strong> 새 Attribute를 추가하면 GE 두 개(공용 파츠 GE + 캐릭터 Init GE)에 각각 Modifier를 넣어야 한다. 하나만 잊어도 컴파일 에러 없이 조용히 0으로 남아서 발견이 늦어진다.</p>
<hr>
<h2 id="2-이중-점프-파츠-장착-후-스페이스를-계속-누르면-공중에서-점프가-계속-나감">2. 이중 점프 파츠 장착 후 스페이스를 계속 누르면 공중에서 점프가 계속 나감</h2>
<p><strong>증상:</strong> <code>JumpMaxCount</code>가 2 이상이 된 상태에서 스페이스를 떼지 않고 있으면, 착지하지 않았는데도 다음 점프가 즉시 또 나가버림. 아웃런에서는 파츠 없이도 항상 발생, 인런에서는 다단 점프 파츠를 얻은 순간부터 발생.</p>
<p><strong>원인:</strong> 언리얼 점프 입력은 엣지(눌리는 순간) 감지가 아니라 홀드 방식이다. <code>bPressedJump</code>가 true인 동안 매 틱 <code>CanJumpInternal()</code>을 재검사하는데, <code>JumpMaxCount=1</code>일 때는 공중에서 <code>JumpCurrentCount &gt;= JumpMaxCount</code>라 자연히 막혔던 것뿐이었다. <code>JumpMaxCount</code>를 2 이상으로 올리는 순간, 홀드 중에도 남은 충전량만큼 계속 재판정되어 발동되는 엔진 기본 동작이 그대로 드러났다. 아웃런은 <code>ApplyInitialAttributeEffect</code>가 실행되지 않아 네이티브 <code>ACharacter::JumpMaxCount</code> 엔진 기본값(2)이 그대로 남아있던 것으로 추정.</p>
<p><strong>해결:</strong> <code>ANSPlayerCharacterBase</code>에 &quot;마지막 점프 이후 키를 뗀 적이 있어야만 다음 점프 허용&quot; 게이트 추가.</p>
<ul>
<li><code>CanJumpInternal_Implementation()</code>: <code>JumpCurrentCount &gt; 0 &amp;&amp; !bHasReleasedJumpKeySinceLastJump</code>면 거부</li>
<li><code>OnJumped_Implementation()</code>: 점프 성공 시 플래그를 false로</li>
<li><code>ClearJumpInput(float DeltaTime)</code>: <code>bPressedJump</code>가 false인 프레임에 플래그를 true로 복귀</li>
</ul>
<p><strong>1차 구현의 버그 — 서버 러버밴딩:</strong> 처음엔 릴리즈 감지를 <code>StopJumping()</code>(입력 바인딩 함수)에서 했는데, 이 함수는 <strong>조종하는 클라이언트에서만</strong> 호출된다. 접속자 클라가 2단 점프에 성공해도, 호스트의 서버 파트는 <code>StopJumping()</code>이 호출되지 않아 플래그가 영영 false로 남아 서버가 그 점프를 거부 → 서버 보정으로 캐릭터가 끌어내려짐(러버밴딩). 호스트 본인은 서버 파트가 직접 도니까 이 버그가 재현되지 않아서, 호스트 단독 테스트로는 절대 못 잡는다.</p>
<p><strong>교훈:</strong></p>
<ul>
<li>캐릭터 이동 관련 로직은 &quot;이 함수가 조종 클라에서만 호출되는지, 서버(리슨서버의 서버 파트)에서도 호출되는지&quot;를 항상 구분해야 한다. <code>StopJumping()</code> 같은 입력 함수는 조종 클라 전용, <code>ClearJumpInput</code>/<code>CanJumpInternal</code>은 이동 검증 경로라 양쪽에서 실행된다.</li>
<li>리슨서버 구조에서 호스트는 자기 캐릭터의 서버 로직을 직접 실행하기 때문에, <strong>접속자 클라 관점의 버그는 호스트 혼자 테스트해서는 발견되지 않는다.</strong> 네트워크 관련 수정은 접속자 클라 시점으로 반드시 검증.</li>
</ul>
<hr>
<h2 id="3-인런-파츠-강화-ui에-스텟-변동폭--065--08-처럼-의미-없는-숫자가-표시됨">3. 인런 파츠 강화 UI에 &quot;스텟 변동폭 : 0.65 ~ 0.8&quot; 처럼 의미 없는 숫자가 표시됨</h2>
<p><strong>증상:</strong> 파츠 리롤/등급업 프리뷰 UI의 수치 범위 표시가 플레이어에게 의미 없는 소수(0.5~1.0 사이)로 보임.</p>
<p><strong>원인:</strong> 파츠 품질 롤 시스템으로 전환하면서 <code>DT_PartUpgrade::ValueRange</code>의 의미가 &quot;실제 수치 범위&quot;에서 &quot;품질 배율(0~1)&quot;로 바뀌었는데, <code>NSPartUpgradeWidget</code>의 UI 코드는 이 값을 그대로 찍고 있었다(전환 당시 놓친 부분).</p>
<p><strong>해결:</strong> UI에서 표시 직전에 <code>ValueRange × 해당 파츠 StatTag의 MaxStatValue</code>로 환산해서 실제 수치로 변환 후 표시.</p>
<p><strong>교훈:</strong> DataTable 필드의 &quot;의미&quot;를 바꾸는 리팩터링(절대값 → 배율 등)을 하면, 그 필드를 직접 읽어 화면에 찍는 모든 지점을 찾아서 같이 고쳐야 한다. 게임 로직(값 계산)과 UI 표시 코드가 같은 원본 필드를 각자 따로 읽는 구조라 한쪽만 고치면 조용히 어긋난다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - 버그 수정 및 상호작용 수정 및 파츠 GE달기]]></title>
            <link>https://velog.io/@kyu_/TIL-%EB%B2%84%EA%B7%B8-%EC%88%98%EC%A0%95-%EB%B0%8F-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9-%EC%88%98%EC%A0%95-%EB%B0%8F-%ED%8C%8C%EC%B8%A0-GE%EB%8B%AC%EA%B8%B0</link>
            <guid>https://velog.io/@kyu_/TIL-%EB%B2%84%EA%B7%B8-%EC%88%98%EC%A0%95-%EB%B0%8F-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9-%EC%88%98%EC%A0%95-%EB%B0%8F-%ED%8C%8C%EC%B8%A0-GE%EB%8B%AC%EA%B8%B0</guid>
            <pubDate>Wed, 15 Jul 2026 12:04:42 GMT</pubDate>
            <description><![CDATA[<h1 id="til">TIL</h1>
<ol>
<li>재화, 힐, 드롭 아이템이 몬스터를 막는 문제 해결</li>
<li>재화, 힐 바운싱 넣기</li>
</ol>
<h2 id="재화-힐-드롭-아이템이-몬스터를-막는-문제-해결">재화, 힐, 드롭 아이템이 몬스터를 막는 문제 해결</h2>
<p>몬스터 드롭으로 나온 아이템이 몬스터를 막는 문제, 몬스터가 이걸 못뚫고 지나감
콜리전쪽을 먼저확인해보니 콜리전은 Custom으로 플레이어한테 오버랩만 가능하게 되어있었음
문제는 네비메시를 드롭쪽에서 건드렸기 때문인데 각각 SetCanEverAffectNavigation설정을 false로 해주어서 해결하였음</p>
<h2 id="재화-힐-바운스-넣기">재화, 힐 바운스 넣기</h2>
<p>파츠쪽에는 현재 사인파를 이용한 바운싱 로직이 들어가 있다.
UpdateBobAnimation(float DeltaSeconds) 함수를 통해서
월드시간*스피드를 사인화 시키고 여기에 진폭을 곱한 후 틱을 돌면서 UpdateBobAnimation을 호출해서 바운싱가능하게 설정하는것을 재화와 힐에도 적용해주었다.</p>
<h2 id="파츠-겹침-방지-로직-및-언덕에-파묻히는-증상-해결">파츠 겹침 방지 로직 및 언덕에 파묻히는 증상 해결</h2>
<p>우선 파츠 겹침의 경우 몬스터가 드롭할때 파츠 레지스트리에 파츠를 등록해서 파츠끼리 일정량 겹치지 않게 하는 로직이 들어갔었음
근데 이게 교체를 선택했을때도 레지스트리에 파츠가 등록되어 있어서 파츠를 스폰하는 함수에서 랜덤 위치에 드롭되는 것을 타고 있었음 근데 이는 드롭할때 드롭위치를 확실하게 확인하는 로직이 없으므로 벽에 겹치거나 지형에 파묻히는 증상이 발생 (SpawnInWorld함수에서는 파츠끼리 겹치는지만 판단함), 교체시에는 드롭 파츠 레지스트리에서 해제해주는 식으로 해결하였음
그리고 파츠 상호작용 범위를 조금 늘려줌</p>
<h2 id="상호작용을-정면에-있는것만-가능하게">상호작용을 정면에 있는것만 가능하게</h2>
<p>ToCandidate와 ViewForward는 둘다 정규화된 (길이1)벡터라서, 내적(DotProduct)이 바로 두 벡터 사이 각도의 코사인 값이 됨
ViewForward : 카메라가 보는 방향
ToCandidate : 카메라 -&gt; 후보 액터 방향
이 둘의 내적 = 두 방향 사이 각도0의 코사인</p>
<p>코사인 함수는 0 ~ 180도 구간에서 각도가 커질수록 값이 감소(0 -&gt;1, 90 -&gt; 0, 180 -&gt; -1) 그래서 후보가 정면이면 (0도) 내적 = 1, 후보가 측면이면(90도) 내적 = 0, 후보가 뒤면 (180도) 내적 = -1
ViewCosThreshold를 미리 구해두고, DotProduct &lt; ViewCosThreshold면 0 &gt; HalfAngleDeg라는 뜻이라 시야각 밖이라고 판정해 continue로 후보에서 제외</p>
<h2 id="파츠-ge만들기">파츠 GE만들기</h2>
<p>파츠에 실제로 스텟 표기 뿐 아니라 GE로 적용을 해주어야 한다.
이미 파츠쪽에 Apply하는 구간이 있어서 GE만 만들면 됨
우리 프로젝트는 지금 현재 NSCombatStatAttributeMapping을 쓰고 있는데 CommonUpgrade/증강이 모든 스탯의 Modifier가 다 들어있는 공유 GE1개를 쓰고 있는 구조라서 스탯이 15개라고 SetbyCaller 태그도 15쌍 필요하고, 안쓰는 태그는 중립값으로 채우고 그래야 하기때문에 조회/중립화 담당 테이블이 있다.</p>
<p>그냥 쉽게 말해서 CombatStat 태그 -&gt; 공용 GE의 SetByCaller 태그 번역기, 공용 GE에 Modifier가 다 들어가 있음</p>
<h3 id="구현계획-claude">구현계획 Claude</h3>
<ol>
<li>NSPartTypes.h — FNSPartDefinitionRow에 Operation 필드 추가 (증강 Row와 동일한 ENSCombatStatModifierOperation 재사용, 기본 Add)</li>
<li>NSPartEquipComponent.h — SharedPartEffectClass 프로퍼티(증강의 SharedAttributeStackEffectClass와 동일 패턴) + Internal_ApplySharedGE
선언</li>
<li>NSPartEquipComponent.cpp — ApplyPartEffect에 분기 추가(EffectClass 비어있으면 공용 경로) + Internal_ApplySharedGE 구현. 매핑 조회 →<br>중립화 → SetByCaller 주입 → 슬롯별 핸들 저장. 전체 코드 문서에 포함</li>
<li>빌드 — 사용자</li>
<li>에디터 — 공용 GE BP(증강 공용 GE 복제 추천, Stacking None 필수), PlayerState BP에 할당, DT Row의 StatTag/Operation 설정</li>
<li>검증 8항목 — 장착/리롤/등급업/교체/2슬롯 독립성/Seamless Travel/접속자 클라/예외 로그</li>
</ol>
<ul>
<li>하이브리드 유지 — 기존 Internal_ApplyGE/OnEffectLoaded 경로는 한 줄도 안 건드림, EffectClass가 지정된 파츠는 예전처럼 개별 GE를 타서, 나중에 MMC형 특수 파츠가 와도 수용</li>
<li>초기엔 전 파츠 Add 통일 권장 — 등급별 ValueRange(FNSPartUpgradeRow)가 모든 스탯 공용이라, Add 파츠(+50)와 Multiply 파츠(×1.2)가 같은 범위를 공유할 수 없음</li>
<li>Multiply가 필요해지면 스탯별 ValueRange 분리를 별도 작업으로 논의하는 게 안전합니다.</li>
</ul>
<h2 id="게임수학">게임수학</h2>
<h3 id="라디안">라디안</h3>
<p>Degree(도) : 우리가 일상에서 쓰는 단위, 원 한바퀴 = 360도
Radian(라디언) : 수학/프로그래밍에서 쓰는 단위, 원 한바퀴 = 2파이</p>
<p>왜 두개나 있냐면 : Cos, Sin 같은 삼각함수는 라디안을 입력받도록 정의되어 있음, 근데 사람이 코드에서 각도를 다룰땐 도 단위가 직관적
그래서 FMath::Cos(FMath::DegreesToRadians(InteractViewHalfAngleDeg))
InteractViewHalfAngleDeg = 60.f(사람이 이해하기 쉬운 60도)를 Cos 함수가 요구하는 라디안으로 변환한 뒤 넘김
언제쓰는가 : 사람이 지정한 각도를 삼각함수 (Sin, Cos, Tan등 라디안 요구하는 함수)에 넣어야 할때마다 이 변환이 필요함</p>
<h3 id="내적">내적</h3>
<p>벡터 두개를 넣으면 숫자(스칼라) 하나가 나오는 연산. 3D 벡터 A=(x1, y1, z1), B=(x2, y2, z2)면 A내적B는 = x1<em>x2 + y1</em>y2 + z1*z2
언제쓰냐: 두 벡터가 얼마나 같은 방향을 보고있는지 알고 싶을 때, 정규화된(길이1) 벡터끼리 내적하면 그 결과가 정확히 두 방향 사이 각도의 코사인 값이 됨</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL]]></title>
            <link>https://velog.io/@kyu_/TIL-ekrjz3ub</link>
            <guid>https://velog.io/@kyu_/TIL-ekrjz3ub</guid>
            <pubDate>Tue, 14 Jul 2026 12:33:41 GMT</pubDate>
            <description><![CDATA[<p>#</p>
<h2 id="웨이포인트-위젯-컨테이너가-있는-이유">웨이포인트 위젯 컨테이너가 있는 이유</h2>
<p>마커는 여러개가 동시에 뜰 수 있고 개수가 런타임에 계속 바뀌는데, 그 생명주기 관리와 매 틱 화면 계산을 한 곳에서 하기 위한 관리자 레이어</p>
<ol>
<li><p>생성/제거 동기화 : NSWaypointSubsystem의 OnWaypointLIstChanged 델리게이트를 구독해서, 마커 컴포넌트가 등록/해제될 때마다 마커 위젯을 만들거나 지움(RegreshMarkerWidgets), 마커 위젯 개별로는 자기가 언제 생기고 사라져야 하는지 알 수 없음</p>
</li>
<li><p>매 틱 공통 계산 : 월드 좌표 -&gt; 스크린 투영, 카메라 뒤 판정, 화면 밖일때 가장자리 클램프, 거리 텍스트/스케일 갱신을 컨테이너의 NativeTick 한곳에서 전부 처리. 뷰포트 크기, DPI, 카메라 시점 같은 공통 값을 한 번만 구해서 모든 마커에 재사용할 수 있고, 마커 위젯 각각이 틱을 도는 것보다 효율적</p>
</li>
<li><p>배치 공간 제공 : 아이콘 + 거리 텍스트 비주얼만 담당하고,</p>
</li>
</ol>
<h2 id="nsstagemanagerh">NSStageManager.h</h2>
<h3 id="isobjectiveinitialized의-주석에서-rescurenpc-스폰-순서-역방향-확인용이라는데-이게-무슨-말인지">IsObjectiveInitialized()의 주석에서 RescureNPC 스폰 순서 역방향 확인용이라는데 이게 무슨 말인지?</h3>
<p>인런 실행하면 두가지가 발생함</p>
<ol>
<li>ANSRunGameMode::InitializeObjectiveInternal 에서 게임 목표를 랜덤 선택해서 NSStageManager에 세팅하는데 (인런 데이터 준비 완료 후 실행)</li>
<li>ANSInRunNPCSpawner::BeginPlay() -&gt; NPC가 스폰되는 시점에 이미 목표가 구출형으로 초기화 되어있는지 거꾸로 확인해서 켬</li>
</ol>
<h3 id="gettargetnpcid는-정확히-뭔지">GetTargetNPCId는 정확히 뭔지?</h3>
<p>그냥 구출 목표 id반환하는 함수</p>
<h2 id="playercontrollercpp">PlayerController.cpp</h2>
<ol>
<li>인터렉션 위젯을 열때 즉, 인터렉션을 할때 가이드 서브시스템에서는 NotifyNPCInteracted(NPCId)로 처리를 해준다 (다음부터 안띄우게)</li>
<li>BeginPlay에서 가이드서브시스템의 StartGuide호출, 실제로 블락은 가이드 서브시스템에서 처리</li>
</ol>
<h2 id="rungamemode">RunGameMode</h2>
<ol>
<li>목표 설정하는 ANSRunGameMode::InitializeObjectiveInternal에서 NPC마커 활성화 함수(ActivateRescueMarkersIfNeeded) 설정</li>
<li>ActivateRescueMarkersIfNeeded에서는 월드에 이미 스폰된 구출NPC를 순회해서 그중 조건에 맞는것만 마커를 켠다.</li>
<li>이때 ShouldShowRescueMarker 함수를 보는데 목표가 아직 초기화되지 않았거나, 목표가 NPC구출이 아니면 마커 대상이 아니고, 지정대상이 있으면 일치할때만 마킹해준다.</li>
</ol>
<h2 id="서브시스템">서브시스템</h2>
<p>// 캐릭터 선택 콘솔과 첫 상호작용 (NSCharacterSelectNPC::OnInteract에서 호출)
void NotifyCharacterConsoleUsed();</p>
<pre><code>// 게임시작 콘솔과 첫 상호작용 (NSReadyStartActor::OnInteract에서 호출)
void NotifyReadyConsoleUsed();

// 허브 NPC와 첫 상호작용 (NSPlayerController::OpenInteractionWidget에서 호출)
void NotifyNPCInteracted(FName NPCId);</code></pre><h2 id="마커어떻게-뜨는지">마커어떻게 뜨는지</h2>
<p>우선 틱 에서 MarkerWidget은 많은 효과가 있다.
1단계 화면 투영 (마커가 화면에 있을때) -&gt; ProjectWorldLocationToScreen이 카메라 투명 행렬을 이용해서 월드 좌표 -&gt; 스크린 픽셀 좌표로 변환해줌, 대상이 카메라 시야 안(정확히는 투명 평만 앞)에 있으면 성공(true)하고 픽셀 좌표를 채워줌
2단계 화면 안인지 판정(카메라 뒤 케이스가 핵심), ProjectWorldLocationToScreen은 대상이 카메라 뒤에 있으면 false를 반환하긴 하는데 그 전에 이미 계산된 스크리 좌표값이 남아있어서 신뢰할 수 없다. 그래서 내적(DotProduct)으로 직접 한번 더 확인함
WorldLocation -&gt; CameraLocation(카메라-&gt;대상 액터) 과 CameraForward(카메라가 보는 방향)의 내적이 음수면 대상이 카메라보다 뒤에 있다는 뜻</p>
<p>투영 성공 + 카메라 앞 + 여백(EdgePadding) 안쪽까지 다 만족해야 &#39;화면 안&#39;으로 인정합니다. 여백을 두는 이유는 마커가 화면 가장자리에 딱 붙어서 잘려 보이지 않게 하기 위해서입니다.</p>
<p>3단계 가장자리 글램핑 (화면 밖일때 방향만 표시) - </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - 드롭파츠 디스폰 및 위젯 수정]]></title>
            <link>https://velog.io/@kyu_/TIL-%EB%93%9C%EB%A1%AD%ED%8C%8C%EC%B8%A0-%EB%94%94%EC%8A%A4%ED%8F%B0-%EB%B0%8F-%EC%9C%84%EC%A0%AF-%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@kyu_/TIL-%EB%93%9C%EB%A1%AD%ED%8C%8C%EC%B8%A0-%EB%94%94%EC%8A%A4%ED%8F%B0-%EB%B0%8F-%EC%9C%84%EC%A0%AF-%EC%88%98%EC%A0%95</guid>
            <pubDate>Mon, 13 Jul 2026 12:14:41 GMT</pubDate>
            <description><![CDATA[<h1 id="til">TIL</h1>
<p>드랍 파츠(전투 중 필드에 떨어지는 파츠 액터) 관련 작업을 한 브랜치에서 진행하고 머지함. 범위: 겹침 방지, 디스폰/바운싱 연출, 상호작용 프롬프트의 스탯 비교 UI, 관련 데이터 캐시.</p>
<h3 id="1-드랍-파츠-겹침-방지--unsdroppedpartregistrysubsystem">1. 드랍 파츠 겹침 방지 — <code>UNSDroppedPartRegistrySubsystem</code></h3>
<p>여러 파츠가 동시에 드랍될 때 같은 자리에 겹쳐 스폰되는 문제를 막기 위해 서버 전용 <code>UWorldSubsystem</code>을 새로 추가.</p>
<ul>
<li><code>ANSDroppedPart::BeginPlay</code>/<code>EndPlay</code>에서 자기 자신을 등록/해제 (<code>TWeakObjectPtr</code> 배열로 보관)</li>
<li><code>IsLocationOccupied(Location, AvoidRadius)</code>로 반경 내 기존 드랍 존재 여부를 <code>DistSquared</code> 비교로 체크 (제곱근 계산 회피)</li>
<li><strong>중요</strong>: 이 레지스트리는 리플리케이션/가시성과 완전히 무관한 &quot;서버 내부 참고용 북키핑&quot;일 뿐. 파츠 액터 자체는 기존처럼 모든 클라에 복제됨. 서브시스템은 스폰 위치 결정 로직에서만 쓰임</li>
</ul>
<p>사용처 두 곳에서 동일한 패턴(최대 6회 재시도, 실패 시 그냥 진행) 반복:</p>
<ul>
<li><code>UNSPartEquipComponent::SpawnDroppedPart</code> — 교체 드랍 시 원래 위치 기준 반경 내에서 각도/거리를 랜덤 지터링해 새 위치 탐색</li>
<li><code>UNSRewardHandler::HandlePartDropResult</code> — 보상 드랍 시 <code>MakeDropLaunchData</code>를 겹치지 않을 때까지 재호출(발사 궤적 자체를 다시 뽑는 방식이라 좌표만 지터링하는 것과 접근이 다름)</li>
</ul>
<p>두 곳 모두 무한루프 방지용으로 시도 횟수를 하드코딩(<code>MaxAttempts = 6</code>)해뒀음 — 나중에 파츠 동시 드랍 개수가 늘어나면 이 상수가 병목이 될 수 있음.</p>
<h3 id="2-드랍-파츠-디스폰바운싱-연출">2. 드랍 파츠 디스폰/바운싱 연출</h3>
<p><code>ANSDroppedPart</code>에 시간 기반 자동 정리 + 시각적 생동감 추가:</p>
<ul>
<li><code>DespawnDuration</code> 경과 시 <code>FTimerHandle</code>로 자동 <code>Destroy</code> (<code>HandleDespawnTimerExpired</code>)</li>
<li>착지 후 <code>Tick</code>에서 <code>UpdateBobAnimation</code>이 사인파로 메시를 위아래로 흔듦 (<code>BobAmplitude</code>/<code>BobSpeed</code>)<ul>
<li><code>SetupVisual</code>에서 계산한 피벗 보정 Z값(<code>MeshBaseRelativeZ</code>)을 기준선으로 삼고 그 위에 오프셋을 더하는 방식 — 그냥 RelativeLocation.Z를 매 틱 덮어쓰면 피벗 보정이 날아가서 이렇게 분리함</li>
</ul>
</li>
<li>파츠를 감싸는 링 VFX(<code>RingVFXID</code>, <code>DT_VFXDataTable</code> 참조)는 <code>bRingVFXPlayed</code> 플래그로 중복 재생 방지</li>
<li>위 세 수치(<code>DespawnDuration</code>, <code>BobAmplitude</code>, <code>BobSpeed</code>, <code>RingVFXID</code>)는 코드 기본값이 있지만 <code>FNSDroppedPartConfigRow</code>(DT에 <code>&quot;Default&quot;</code> Row 하나)로 덮어씀 — 밸런스 조정은 DataTable에서, 구조는 C++에서라는 기존 규약 그대로 따름</li>
</ul>
<h3 id="3-상호작용-프롬프트-스탯-비교-ui">3. 상호작용 프롬프트 스탯 비교 UI</h3>
<p>드랍된 파츠를 주우려 할 때, 현재 장착 중인 파츠(없으면 캐릭터 기본 스탯)와 비교해 스탯 변화를 프롬프트에 보여주는 기능 추가.</p>
<ul>
<li><code>NSInteractionComponent::UpdateStatComparisonFor(DroppedPart, Widget)</code>가 비교 트리거</li>
<li><code>FNSPartDefinitionRow::StatTag</code> (파츠가 영향 주는 <code>CombatStat.*</code> 태그)와 <code>FNSStatDisplayInfoRow</code>(표시 이름 + <code>bHigherIsBetter</code>)를 매칭해서 값이 오르면 좋은지/내리면 좋은지 판단 후 화살표 색 결정</li>
<li><code>FNSStatDisplayInfoRow</code>는 <strong>RowName이 StatTag와 동일해야</strong> 조회가 맞게 동작하는 규약 — <code>NSDataSubsystem::FindStatDisplayInfoRow</code>가 이 전제로 캐시 조회</li>
<li><code>NSDataSubsystem</code>에 이 테이블용 캐시(<code>GetCommonStatDisplayInfoTable</code>, <code>FindStatDisplayInfoRow</code>)와 드랍 파츠 설정 캐시(<code>GetDroppedPartConfigRow</code>, <code>BuildDroppedPartConfigCache</code>)를 함께 추가 — 파츠 관련 캐시들을 한 서브시스템에 계속 누적하는 기존 패턴 유지</li>
</ul>
<h3 id="4-wbp-텍스트-크기-버그-뒤늦게-수정된-커밋-2개">4. WBP 텍스트 크기 버그 (뒤늦게 수정된 커밋 2개)</h3>
<p>브랜치 뒷부분 커밋(<code>85e8ceba5</code>, <code>6f90a564c</code>)은 위 기능과 별개로, 상호작용 프롬프트 위젯의 텍스트가 박스 밖으로 삐져나오던 문제를 고정 크기 박스 대신 텍스트 크기에 맞춰 늘어나도록 수정한 것. 스탯 비교 UI 추가로 프롬프트에 표시할 내용이 늘어나면서 드러난 문제로 보임.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - 몬스터 파츠 드랍 구조 변경]]></title>
            <link>https://velog.io/@kyu_/TIL-%EB%AA%AC%EC%8A%A4%ED%84%B0-%ED%8C%8C%EC%B8%A0-%EB%93%9C%EB%9E%8D-%EA%B5%AC%EC%A1%B0-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@kyu_/TIL-%EB%AA%AC%EC%8A%A4%ED%84%B0-%ED%8C%8C%EC%B8%A0-%EB%93%9C%EB%9E%8D-%EA%B5%AC%EC%A1%B0-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Thu, 09 Jul 2026 12:09:47 GMT</pubDate>
            <description><![CDATA[<h1 id="몬스터-파츠-드랍-레어도-태그--랜덤-파츠-풀">몬스터 파츠 드랍: 레어도 태그 + 랜덤 파츠 풀</h1>
<h2 id="목표">목표</h2>
<p>몬스터 드랍테이블에서 파츠를 &quot;특정 <code>PartDefinition</code> 지정&quot; 방식이 아니라 &quot;레어도만 지정 → 실행 시점에 전체 파츠 풀에서 랜덤 1개 선택 + 해당 레어도 부여&quot; 방식으로 뽑는다.</p>
<h2 id="확정된-최종-구조">확정된 최종 구조</h2>
<ul>
<li><code>FNSRewardDropRow</code>/<code>FNSRewardDropResult</code>에서 <code>PartDefinition</code>(<code>TSoftObjectPtr&lt;UNSPartDefinition&gt;</code>) 필드는 완전히 제거. Part 타입 행은 항상 &quot;전체 파츠 풀에서 랜덤 선택&quot; 방식만 사용 (몬스터별로 드랍 파츠 종류를 다르게 할 계획이 없다고 기획 확정 → 특정 파츠 못박는 경로 자체를 없앰).</li>
<li>레어도는 <strong>ENUM이 아니라 태그</strong>로 받는다: <code>FGameplayTag RarityTag</code> (<code>Part.Rarity.Common/Rare/Epic/Legendary</code>). 이유:<ul>
<li>같은 행의 다른 필드들(<code>DropGroupTag</code>, <code>RewardTypeTag</code>, <code>CurrencyTag</code>, <code>AugmentPoolTag</code>, <code>HealPotionTag</code>)이 전부 태그라서, <code>Rarity</code>만 한글 UENUM이면 이질적임.</li>
<li><code>RewardTypeTag</code>(보상 종류 축)와 <code>Rarity</code>(등급 축)는 서로 다른 축이라 하나의 태그 계층으로 합치지 않음 — <code>Reward.Type.Part.Common</code> 식으로 만들면 <code>RewardTypeTag == ...</code> 정확 일치 비교 5곳(<code>NSRewardHandler.cpp</code>, <code>NSRewardDropResolver.cpp</code>)이 전부 깨짐.</li>
<li>&quot;미설정 시 기본값&quot; 문제: <code>ENSPartRarity</code>에 <code>None</code>을 추가하는 것도 검토했으나 기각. <code>None</code>을 추가하면 그 값이 실수로 <code>PartData.CurrentRarity</code>까지 흘러갔을 때 <code>NSPartEquipComponent.cpp</code>의 등급업 로직(<code>static_cast&lt;ENSPartRarity&gt;(uint8(Rarity)+1)</code>)이 enum 범위 밖 값을 만들 수 있어 UI switch문에서 미정의 동작 위험이 있음. 태그는 미설정 시 자연히 &quot;invalid 태그&quot;가 되므로 별도 sentinel 값이 필요 없음.</li>
</ul>
</li>
<li>태그 ↔ enum 변환은 <code>NSPartUtils::ResolveRarityFromTag(RarityTag, OutRarity)</code>가 전담 (변환 실패 시 <code>false</code> 반환 → 호출부가 드랍을 안전하게 스킵).</li>
<li><code>ENSPartRarity</code> enum 자체는 원래대로 유지 (<code>Common/Rare/Epic/Legendary</code>, <code>None</code> 없음). 파츠 실제 로직(업그레이드/상점/세이브)은 계속 이 enum을 그대로 사용, 영향 없음.</li>
<li>랜덤 풀 선택은 기존 패턴 재사용: <code>NSPartEquipComponent.cpp:631~647</code>(상점 재고 생성)와 동일하게 <code>DataSS-&gt;GetAllPartRows()</code> 순회 + 인덱스 랜덤 선택.</li>
</ul>
<h2 id="변경-파일-전부-완료">변경 파일 (전부 완료)</h2>
<ol>
<li><p><strong><code>Source/NeoSanctum/Data/Part/NSPartTypes.h</code></strong></p>
<ul>
<li>변경 없음 (<code>None</code> 추가했다가 되돌림. 최종적으로 원본 그대로: Common/Rare/Epic/Legendary만).</li>
</ul>
</li>
<li><p><strong><code>Source/NeoSanctum/Tag/NSGameplayTags_Part.h</code> / <code>.cpp</code></strong></p>
<ul>
<li><code>Part.Rarity.Common</code> / <code>Rare</code> / <code>Epic</code> / <code>Legendary</code> 4개 태그 신규 추가.</li>
</ul>
</li>
<li><p><strong><code>Source/NeoSanctum/Progression/Part/NSPartUtils.h</code> / <code>.cpp</code></strong></p>
<ul>
<li><code>ResolveRarityFromTag(const FGameplayTag&amp;, ENSPartRarity&amp;) -&gt; bool</code> 추가. 태그 4개를 순서대로 비교해 매칭되는 enum 값을 반환, 매칭 안 되면 <code>false</code>.</li>
<li><code>.h</code>에 <code>#include &quot;GameplayTagContainer.h&quot;</code> 추가, <code>.cpp</code>에 <code>#include &quot;NeoSanctum/Tag/NSGameplayTags_Part.h&quot;</code> 추가.</li>
</ul>
</li>
<li><p><strong><code>Source/NeoSanctum/Data/Reward/NSRewardTypes.h</code></strong></p>
<ul>
<li><code>FNSRewardDropRow</code>/<code>FNSRewardDropResult</code> 둘 다: <code>PartDefinition</code> 필드 삭제, <code>RarityTag</code>(<code>FGameplayTag</code>, <code>meta=(Categories=&quot;Part.Rarity&quot;)</code>) 추가.</li>
<li>이제 이 파일에서 <code>ENSPartRarity</code>/<code>UNSPartDefinition</code>을 안 쓰므로 관련 include·forward-declare 제거.</li>
</ul>
</li>
<li><p><strong><code>Source/NeoSanctum/Data/Reward/NSRewardDropResolver.cpp</code></strong></p>
<ul>
<li><code>ApplyDropRowToResult</code>: <code>OutResult.PartDefinition = ...</code> 삭제, <code>OutResult.RarityTag = Row.RarityTag;</code> 추가. 리졸버는 태그를 그대로 전달만 함(파츠 세부 로직에는 관여하지 않음 — 책임 분리 유지).</li>
</ul>
</li>
<li><p><strong><code>Source/NeoSanctum/Progression/Reward/NSRewardHandler.cpp</code></strong></p>
<ul>
<li>상단 <code>#include &quot;Engine/AssetManager.h&quot;</code> 제거 (이 파일에서 <code>UAssetManager</code> 쓰는 곳이 없어짐).</li>
<li><code>HandlePartDropResult</code>: <code>DropResult.PartDefinition</code> 참조하던 가드/로그 전부 제거. <code>PartData.IsValid()</code> 결과로만 판별.</li>
<li><code>MakePartDataFromDropResult</code>: <code>UNSDataSubsystem::Get(World)-&gt;GetAllPartRows()</code>에서 랜덤 픽 → <code>NSPartUtils::ResolveRarityFromTag</code>로 <code>DropResult.RarityTag</code> 변환(실패 시 무효 <code>PartData</code> 반환) → <code>PartData.CurrentRarity</code>에 반영.</li>
<li><strong>주의(수정 완료)</strong>: <code>ValueRange</code> 롤링은 <code>RandomStream.FRandRange(MinValue, MaxValue)</code>를 써야 함. 중간에 <code>RandRange</code>(int32 전용 오버로드)로 잘못 타이핑되어 float가 int로 잘리는 버그가 있었으나 <code>FRandRange</code>로 수정함.</li>
</ul>
</li>
<li><p><strong>DataTable(디자이너 작업, 코드 범위 밖)</strong></p>
<ul>
<li>드랍테이블 Part 타입 행에는 이제 <code>RarityTag</code>만 채움 (<code>Part.Rarity.Common</code> 등). <code>PartDefinition</code> 열 자체가 없어졌으므로 입력 불필요.</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - 상호작용 윤곽선 만들기]]></title>
            <link>https://velog.io/@kyu_/TIL-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9-%EC%9C%A4%EA%B3%BD%EC%84%A0-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@kyu_/TIL-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9-%EC%9C%A4%EA%B3%BD%EC%84%A0-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 08 Jul 2026 13:55:09 GMT</pubDate>
            <description><![CDATA[<h1 id="상호작용-윤곽선-만들기">상호작용 윤곽선 만들기</h1>
<p>오늘 할작업은 상호작용 가능한 요소들에게 다가갔을때 실제로 상호작용이 가능하다는 느낌을 유저들에게 주기위해 상호작용 요소들에게 흰색 윤곽선을 만들 계획이다.</p>
<h2 id="아키텍처">아키텍처</h2>
<p>대상 메시를 CustomDepth버퍼에 스텐실 값과 함께 렌더링하고, 플레이어 카메라에 등록된 포스트 프로세스 머티리얼이 스텐실 경계를 감지해 테두리를 그림
On/Off제어는 기존 프롬프트 표시 지점(<code>ShowPromptFor</code>, <code>HidePrompt</code>)에 두고, PP머티리얼은 스텐실 값 -&gt; 색상 매핑구조로 만들어 향후 색상 추가 시 머티리얼 재작업이 없게 한다.</p>
<h2 id="왜-선택">왜 선택?</h2>
<ul>
<li>현재 상호작용 대상 표시는 프롬프트 위젯뿐임, 실제로 윤곽선을 두면 UX가 더 좋아질것이라고 기대하고 만듬</li>
<li>특히 파츠의 경우 그냥 필드에 떨어지는 경우 윤곽선이 없으면 파츠가 어떻게 생겼는지 제대로 안보임</li>
<li>CustomDepth+Stencil 방식을 선택한 이유는 선 굵기가 화면 픽셀 기준이라 균일해 품질이 좋을것을 기대했고, 스텐실 값-&gt; 색상 매핑 구조로 만들어두면 향후 적은 빨강, 등급별 색상 같은 확장 시 C++에서 스텐실 값만 바꾸면 되어 재작업이 없다.</li>
<li>오버레이 머티리얼방식과 고민하다 이걸로 결정</li>
</ul>
<h2 id="어떤것을-바꾸었나">어떤것을 바꾸었나?</h2>
<ul>
<li>프로젝트 세팅에 r.CustomDepth=3 추가</li>
<li>아웃라인용 PP 머티리얼 소프트 참조 프로퍼티 + 비동기 로드 후 소유자 카메라에 블렌더블 등록</li>
<li>대상 액터의 모든 <code>UMeshComponent</code>에 <code>SetRenderCustomDepth</code> + <code>SetCustomDepthStencilValue</code> 적용</li>
</ul>
<h2 id="어떻게-동작하는가">어떻게 동작하는가?</h2>
<p>상호작용 가능한 액터랑 상호작용을 감지할때 기존 구조는 캐릭터에 상호작용 컴포넌트를 달고 상호작용 인터페이스를 구현하는 액터들이 BeginPlay에서 EnableLocalInteraction() 함수를 통해 실제 콜리전을 가지게 된다.
이때 상호작용 아웃라인을 비동기로드하는 함수를 달아놓는다. 로드 완료되면 소유자 폰의 UCameraComponent에 포스트 프로세스 세팅에 카메라 블렌더블로 머티리얼을 담, 모든 레벨에서 동작 -&gt; 레벨별로 PostProcessVolume처리가 필요없음
매틱 기존처럼 최단거리 대상을 결정함 프롬프트와 아웃라인 거리가 항상 일정함 같은 파이프라인을 타서
대상이 바뀔때만 Update함수가 호출된다(같은 대상이면 얼리리턴) -&gt; 다른 대상으로 Update될때 기존 CustomDepth Off해주는 작업까지 빼먹지 않고 설정
PP 머티리얼은 화면의 각 픽셀에서 CustomStencil 값을 주변과 비교해 경계를 찾고, 경계 픽셀을 스텐실 값에 매핑된 색(1=현재는 흰색)으로 칠함</p>
<h2 id="관련-내용">관련 내용</h2>
<p>언리얼에서 관리하는 depth버퍼라는게 있다. 특정 액터만 골라서 렌더링 한 후에 포스트프로세스에서 참조할 수 있도록 하는데</p>
<p>depth(Z-버퍼)란 무엇이냐면 화면에 그려지는 각 픽셀마다 카메라로부터 얼마나 멀리있는 물체인지를 저장해두는 별도의 버퍼(텍스처)</p>
<p>3D 장면을 렌더링할 때 여러 물체가 서로 겹칠 수 있는데, 캐릭터 뒤에 벽이 있으면 벽이 캐릭터를 가려야 상. GPU는 각 픽셀을 그릴때마다 지금 그리려는 픽셀이 이미 저장된 depth값 보다 카메라에 더 가까운가를 비교해서 가까운 것만 최종화면에 반영하고 먼것은 버림, 이 비교용으로 쓰이는게 Depth버퍼</p>
<p>CustomDepth는 뭐가 다른가
일반 depth버퍼는 항상 모든 오브젝트가 자동으로 기록됨(렌더링 자체의 필수 요소)
CustomDepth : 기본적으로 아무것도 안담기고, 개발자가 SetRenderCustomDepth(true)로 선택한 액터만 골라서 여기에 depth를 추가로 기록함</p>
<p>포스트프로세스 머티리얼에서 이 특정 오브젝트가 화면 어디쯤에 있는지 알아내서 그 오브젝트에만 특수 효과를 입힐 수 있음</p>
<p>Stencil은 CustomDepth 버퍼가 이 픽셀이 특정 대상에 속하는가(depth 유무)만 저장한다면, Stencil은 거기에 8비트 정수 값을 하나 더 붙일 수 있게 해주는 부가 채널이다.
CustomDepth로 이 픽셀은 마스킹 대상이다 아니다 (0/1)만 판단 한다면 Stencil이 있으면 이 픽셀이 몇번 그룹에 속한 대상이다 (0~256)식으로 여러 그룹을 숫자로 구분할 수 있게 됨
코드에서는 SetActorOutlineEnabled함수를 호출하면서</p>
<pre><code class="language-c++">Mesh-&gt;SetRenderCustomDepth(bEnabled);
// 끌 때는 0으로 되돌려 잔여 스텐실을 남기지 않음
Mesh-&gt;SetCustomDepthStencilValue(bEnabled ? OutlineStencilValue : 0);</code></pre>
<p>이쪽에서 OutlineStencilValue를 지정해주고 있음 -&gt; 현재 인터렉션 컴포넌트에서는 1로 하드코딩되어있음 에디터에서 수정은 가능한 상태
머티리얼에서 1에 대한 색은 흰색으로 지정한 상태이다.</p>
<h2 id="머티리얼-만들기">머티리얼 만들기</h2>
<h3 id="linewidth--viewsize">LineWidth + ViewSize</h3>
<p>LineWidth는 몇 픽셀만큼 옆을 볼지를 픽셀당위로 정한 값(우리는 2), 근데 머티리얼에서는 텍스처를 읽을 때 좌표(UV)는 픽셀이 아니라 0~1 사이의 비율로 쓴다. 화면이 1920px인데 2픽셀 옆을 UV로 표현하려면 2/1920처럼 아주 작은 비율이 되므로 픽셀단위 -&gt; UV비율로 나누기 위해서 전체 해상도로 나눔 ViewSize는 그 화면 전체 해상도를 주는 노드</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/8f3083d0-0744-411a-a567-07bded69fde7/image.png" alt=""></p>
<h3 id="maskr-g의-역할">Mask(R, G)의 역할</h3>
<p>방향을 4개(상하좌우)를 만들어야 하는데, 오프셋은 원래 벡터(가로 성분, 세로 성분)하나. Mask는 이 벡터에서 한쪽 축만 살리고 나머지는 0으로 지우는 역할을 함
R선택 : 가로(X)성분만 사용 -&gt; 좌우 방향만들때
G선택 : 세로(Y)성분만 사용 -&gt; 상하 방향 만들때</p>
<p>R로 둔이유는 이 방향은 옆으로만 움직이고 상하로는 안 움직인다를 보장하는 순수 방향 벡터를 만드는 역할</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/5f0071af-49c6-4a1f-983c-be1d18c0a50a/image.png" alt=""></p>
<h3 id="mask-x--1---4방향-offset">Mask x -1 -&gt; 4방향 Offset</h3>
<p>Offset이란 지금 픽셀 기준으로 얼마나 떨어진 이웃 픽셀을 볼지를 나타내는 좌표 이동량
R로 가로 성분만 살린 값 = 오른쪽으로 이동 Offset -&gt; 여기다가 -1 곱하면 왼쪽, G도 마찬가지</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/ea67d148-6ea8-48fc-9228-78a6c199e53d/image.png" alt=""></p>
<h3 id="screenposition--offsetadd">ScreenPosition + Offset(Add)</h3>
<p>ScreenPosition은 지금 자신이 그리고 있는 픽셀, 자신의 화면 좌표(0~1 UV)임. 포스트 프로세스는 화면 전체를 한장의 사각형처럼 훑으면서 &#39;지금 픽셀&#39;만 아는 상태로 계산하는데, 아무것도 안하면 항상 내위치만 읽게됨
여기에 Offset을 더하면 내위치가 아니라 그 옆(이웃) 픽셀 위치를 가리키는 좌표가 나옴.
Append노드는 스칼라 두개를 붙여서 벡터 하나로 합치는 노드 이므로 0을 더해서 순수 방향 벡터로 만듬</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/b3547dbd-c9df-463c-890c-6876305c7067/image.png" alt=""></p>
<h3 id="scenetexture">SceneTexture</h3>
<p>SceneTexture 노드는 언리얼 렌더러가 이미 계산해둔 다양한 화면 버퍼(원본 색상, 뎁스, 노멀 등)를 포스트 프로세스 머티리얼 안에서 읽어오는 창구
여기서 Scene Texture Id를 CustomStencil로 설정해주어 C++에서 SetCustomStencilValue()로 찍어둔 그 숫자값을 화면픽셀별로 읽어옴
즉, 이 화면 좌표(UV)의 스텐실 값이 얼마인가를 알려주는 노드.
UV를 안넣으면 내 픽셀값을 주고, 4번에서 만든 이웃좌표를 넣으면 이웃 픽셀값을 준다.</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/90b8601d-0b4d-4fa9-9cb9-394847353362/image.png" alt=""></p>
<h3 id="4방향-값의-max를-찾는이유">4방향 값의 Max를 찾는이유</h3>
<p>목적은 이웃 4곳 중 아무 한곳이라도 대상(스텐실 &gt; 0) 안쪽인지를 확인하고 싶기때문. 4개 값중 가장 큰 값을 뽑으면 하나라도 스텐실이 있으면 그 값이 최대값으로 잡힘 -&gt; OR조건을 Max하나로 구현한것</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/d421a05d-d1d3-47b7-b2b6-eb01cde7166a/image.png" alt=""></p>
<h3 id="s0과-s_4방향max의-차이">S0과 S_4방향(Max)의 차이</h3>
<p>S0은 지금 그리고 있는 바로 그 픽셀 자신의 스텐실 값
S_4방향 : 그 픽셀을 둘러싼 상하좌우 이웃들 중 가장 큰 스텐실 값</p>
<p>이 둘을 비교하면 나는 대상 밖인데(S0=0), 내 이웃중 하나는 대상 안이다(S_max &gt; 0)라는 경계 상황을 잡아낼 수 있음 -&gt; 이게 바로 윤곽선이 그려질 자리</p>
<h3 id="smoothstep-minmax가-왜-05인지">SmoothStep, Min/Max가 왜 0.5인지</h3>
<p>SmoothStep(Min, Max, Value)는 Value가 Min보다 작으면 0, Max보다 크면 1, 그 사이면 부드럽게 0-&gt;1로 변하는것을 만드는 노드
스텐실값은 0, 1 처럼 딱 떨어지는 정수인데 0.5로 기준을 잡아서 0인지 1인지 구분하기 위함</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/79f0ef8d-8f4b-4dfc-8314-c2ea326dc455/image.png" alt=""></p>
<h3 id="multiply를-하는-이유">Multiply를 하는 이유</h3>
<p>경계조건은 두가지가 만족해야 한다.</p>
<ol>
<li>내가 대상 밖이다 (S0 기준 판정 결과)</li>
<li>그리고 이웃 중 하나는 대상 안이다 (S_max 기준 판정 결과)</li>
</ol>
<p>두 판정 결과가 각각 0이나 1이면 이걸 곱했을때 둘다 1이여야만 참이된다. AND연산을 위함
결과물이 바로 여기가 윤곽선 픽셀이다 를 나타내기 위해 명칭은 EdgeMask</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/2b94689e-4a55-485b-9af9-47809b81fa7e/image.png" alt=""></p>
<h3 id="postprocessinput0은-어디서-지정하나">PostProcessInput0은 어디서 지정하나</h3>
<p>PostProcessInput0도 SceneTexture노드의 Scene Texture Id로 고르는 특수한 값이다.
이 머티리얼이 포스트 프로세스 카메라에 등록되는 순간 언리얼 렌더러가 지금까지 그려진 화면 원본을 자동으로 이 이름표에 채워넣음</p>
<h3 id="lerp를-왜하는가">Lerp를 왜하는가</h3>
<p>Lerp(A, B, Alpha)는 그냥 선형 보간
A = 원본화면
B = 흰색(OutlineColor)
Alpha = EdgeMask(경계면 0또는 1)</p>
<p>EdgeMask가 0인 대부분 픽셀은 A 그대로 나오고, EdgeMaks가 1인 딱 그 경계 픽셀들만 흰색이 나오게끔 하기 위함</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/71a70d63-54fa-4496-81d8-c1eb01ea728d/image.png" alt=""></p>
<h3 id="전체-흐름">전체 흐름</h3>
<p>내 픽셀 스텐실 확인 -&gt; 옆 픽셀들 스텐실 확인 -&gt; 나는 밖, 옆은 안인 경계만 찾기(AND) -&gt; 그 경계만 흰색으로 덮어씌우기 (Lerp) 이 네단계라고 보면 된다.</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/fa88815c-ac3a-439b-8060-6151931dbd0d/image.png" alt=""></p>
<h1 id="트러블-슈팅">트러블 슈팅</h1>
<h2 id="1-실루엣-전체가-통짜로-채워짐-테두리가-아니라-면-전체">1. 실루엣 전체가 통짜로 채워짐 (테두리가 아니라 면 전체)</h2>
<p><strong>증상</strong>: 대상에게 다가가면 테두리가 아니라 캐릭터 실루엣 전체가 흰색으로 칠해짐.</p>
<p><strong>원인</strong>: <code>IsOutside</code> If 노드(<code>S0 &lt; 0.5</code>를 판별해야 함)의 <code>A &gt; B</code> / <code>A &lt; B</code> 분기에 연결된 상수가 뒤바뀌어 있었음. 결과적으로 <code>S0 &lt; 0.5</code>(대상 밖)가 아니라 <code>S0 &gt; 0.5</code>(대상 안)일 때 1이 나오는 <code>IsInside</code> 로직이 되어버림. <code>LineWidth</code>가 작아(2px) 대부분의 내부 픽셀은 이웃도 전부 대상 안이라 <code>NeighborInside</code>도 참이 되므로, <code>IsInside × NeighborInside</code> = 거의 실루엣 전체.</p>
<p><strong>해결</strong>: <code>IsOutside</code>의 <code>A &gt; B = 0</code>, <code>A &lt; B = 1</code>로 수정 (<code>NeighborInside</code>는 반대로 <code>A &gt; B = 1</code>, <code>A &lt; B = 0</code>이 맞음 — 두 If 노드는 서로 정반대 패턴이어야 함).</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/ca87821e-6884-4682-bfa9-49708de0f471/image.png" alt=""></p>
<h2 id="2-게임-시작하자마자-화면-전체가-하얗게-나옴">2. 게임 시작하자마자 화면 전체가 하얗게 나옴</h2>
<p><strong>증상</strong>: 상호작용 대상에 다가가지 않아도, PIE 시작 직후부터 화면 전체가 하얀색.</p>
<p><strong>원인</strong>: If 노드 로직 문제가 아니라 <strong>최종 <code>Lerp</code> 노드의 A/B가 서로 바뀜</strong>. <code>Lerp(A, B, Alpha)</code>는 Alpha=0일 때 A를 출력하는데, A 핀에 <code>OutlineColor_1</code>(흰색)이, B 핀에 원본 화면(<code>SceneTexture:PostProcessInput0</code>)이 연결되어 있었음. <code>EdgeMask</code>(Alpha)가 0인 곳(화면 대부분, 스텐실이 하나도 없을 때는 전체 화면)에서 흰색이 나오는 게 정상 동작이 되어버린 것.</p>
<p><strong>진단 방법</strong>: 스크린샷만으로는 어느 지점이 문제인지 특정이 안 돼서, 머티리얼 그래프를 텍스트로 복사(<code>Ctrl+A</code> → <code>Ctrl+C</code> → 텍스트 파일에 붙여넣기)해서 각 노드의 실제 핀 연결과 상수값을 정확히 대조함. 이렇게 하면 <code>MaterialExpressionIf</code>의 <code>AGreaterThanB</code>/<code>AEqualsB</code>/<code>ALessThanB</code>가 어느 <code>MaterialExpressionConstant</code>를 참조하는지, <code>Lerp</code>의 A/B가 정확히 어디 연결됐는지 텍스트로 명확히 확인 가능.</p>
<p><strong>해결</strong>: <code>Lerp</code>의 A ↔ B 연결을 서로 맞바꿈 — A = 원본 화면(SceneColor), B = <code>OutlineColor_1</code>.</p>
<p><img src="https://velog.velcdn.com/images/kyu_/post/75b0ea56-a4c6-4bdb-b9f4-1e1811eddfc3/image.png" alt=""></p>
<h2 id="3-아웃라인이-자글자글-떨림-지터깜빡임">3. 아웃라인이 자글자글 떨림 (지터/깜빡임)</h2>
<p><strong>증상</strong>: 아웃라인 자체는 정상 표시되지만 선이 미세하게 떨리면서 지글거림. <code>LineWidth</code>를 올려도 개선 안 되고 오히려 더 티가 남.</p>
<p><strong>원인</strong>: TSR/TAA는 매 프레임 카메라를 서브픽셀 단위로 지터시켜 여러 프레임을 누적해 안정화하는데, <strong>CustomStencil 버퍼는 이 시간 누적을 거치지 않고 매 프레임 지터된 원본 그대로</strong> 읽힘. 머티리얼의 <code>Blendable Location</code>이 <code>Scene Color After Tonemapping</code>(TSR 처리 완료 이후)으로 설정되어 있어서, 이미 안정화된 화면 위에 &quot;아직 흔들리는 스텐실 기반 선&quot;을 얹는 상태가 됨 → 선만 유독 떨림. <code>LineWidth</code>를 키우면 떨리는 영역도 같이 커져서 역효과.</p>
<p><strong>해결</strong>: <code>Blendable Location</code>을 <code>Scene Color After Tonemapping</code> → <strong><code>Scene Color After DOF</code></strong>로 변경. 이러면 아웃라인이 TSR 처리 <em>이전</em>에 합성되어, TSR이 아웃라인까지 포함해 시간 누적·안티에일리어싱을 해주므로 다른 픽셀과 동일하게 안정화됨.</p>
<p><strong>부작용</strong>: After DOF는 톤매핑 전(HDR 리니어 색공간)이라 <code>OutlineColor_1=(1,1,1)</code>이 톤매핑 후 순백이 아니라 약간 칙칙하게 보일 수 있음. 필요시 <code>(3,3,3)~(10,10,10)</code> 정도로 값을 올려서 톤매핑 후 흰색에 가깝게 보정 (과하면 블룸 번짐 발생하므로 적당한 값 탐색 필요).</p>
<p>!youtube[IBVS48oC-zo]</p>
<h2 id="최종-정상-동작-구성-요약">최종 정상 동작 구성 요약</h2>
<ul>
<li><code>IsOutside</code>: A&gt;B=0, A==B=0, A&lt;B=1 (S0 &lt; 0.5 → 1)</li>
<li><code>NeighborInside</code>: A&gt;B=1, A==B=0, A&lt;B=0 (SMax &gt; 0.5 → 1)</li>
<li><code>EdgeMask</code> = Multiply(IsOutside, NeighborInside)</li>
<li><code>Lerp</code>: A=SceneColor(원본 화면), B=OutlineColor_1(흰색), Alpha=EdgeMask</li>
<li><code>Blendable Location = Scene Color After DOF</code> (Tonemapping 이후 아님)</li>
<li><code>LineWidth = 2.0</code> (지터 해결 후에는 굳이 키울 필요 없음)</li>
</ul>
<p>!youtube[h-zTfNy_i8s]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - 회복 아이템 만들기]]></title>
            <link>https://velog.io/@kyu_/TIL-%ED%9A%8C%EB%B3%B5-%EC%95%84%EC%9D%B4%ED%85%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-pmeujz58</link>
            <guid>https://velog.io/@kyu_/TIL-%ED%9A%8C%EB%B3%B5-%EC%95%84%EC%9D%B4%ED%85%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-pmeujz58</guid>
            <pubDate>Tue, 07 Jul 2026 12:25:30 GMT</pubDate>
            <description><![CDATA[<h1 id="힐-드롭-시스템-만들기">힐 드롭 시스템 만들기</h1>
<p>회복 아이템을 만든다.</p>
<p>재화시스템의 ANSCurrencyReplicationProxy를 미러링해서 회복아이템 전용 프록시 만들기</p>
<p>서버가 특정 플레이어에게만 이 위치에 이런 회복아이템이 생겼다를 알리기 위함</p>
<p>Owner-Only인 이유도 해당 플레이어의 화면에만 픽업아이템이 보이도록</p>
<hr>
<h1 id="회복-아이템hp포션-드랍-시스템--작업-기록">회복 아이템(HP포션) 드랍 시스템 — 작업 기록</h1>
<p>몬스터 드랍으로 HP포션(50%/30%/10% 회복)을 추가하는 작업. 재화 드랍 시스템(서버 레지스트리 + Owner-only 프록시 + 클라 로컬 비주얼)을 그대로 미러링해서 구현.</p>
<h2 id="1-회복-비주얼-데이터를-어디-둘지--세-번-갈아엎음">1. 회복 비주얼 데이터를 어디 둘지 — 세 번 갈아엎음</h2>
<p><strong>1차 시도</strong>: 픽업 BP에 <code>TMap&lt;int32, TSoftObjectPtr&lt;UStaticMesh&gt;&gt; TierMeshMap</code>을 직접 박아둠.</p>
<ul>
<li><strong>문제</strong>: 이 프로젝트 원칙(&quot;수치/밸런스는 DataTable 우선&quot;)과 안 맞음. 새 포션 추가할 때마다 BP를 열어야 함.</li>
</ul>
<p><strong>2차 시도</strong>: <code>DT_HealVisual</code>(RowStruct=<code>FNSHealVisualRow</code>, Mesh+Scale만) 신규 생성, RowName을 회복 퍼센트 문자열(&quot;50&quot;/&quot;30&quot;/&quot;10&quot;)로 매핑. 드랍테이블의 범용 <code>Quantity</code> 필드를 회복 퍼센트로 재사용(<code>MinQuantity=MaxQuantity=50/30/10</code>).</p>
<ul>
<li><strong>문제</strong>: <code>Quantity</code>가 원래 &quot;몇 개&quot;를 의미하는 필드인데 회복%로 몰래 재사용해서, 드랍테이블만 보면 그 50이 개수인지 퍼센트인지 알 수 없음. &quot;1로 두면 안 되나?&quot;라는 질문에 답하다가 이 설계의 취약함이 드러남 — <code>Quantity</code> 필드 하나에 &quot;몇 티어가 뽑히는지(Weight)&quot;와 &quot;그 티어가 실제로 몇 %인지(Quantity)&quot;라는 두 가지 역할이 겹쳐 있었음.</li>
</ul>
<p><strong>3차(최종) 설계</strong>: 재화의 <code>CurrencyTag</code>와 동일한 패턴으로 <strong><code>FGameplayTag HealPotionTag</code></strong>를 <code>FNSRewardDropRow</code>/<code>FNSRewardDropResult</code>에 추가. 드랍테이블은 &quot;어떤 포션인지&quot;만 태그로 식별(<code>Reward.Potion.Heal.Large/Mid/Small</code> — 태그명에 퍼센트를 안 넣음, DT에서 밸런스를 바꿔도 태그명이 거짓말하지 않도록). 회복%·메시·스케일은 <code>DT_HealPotion</code>(RowStruct=<code>FNSHealPotionRow</code>) 한 곳에 통합해서, RowName=태그 전체 이름으로 조회.</p>
<ul>
<li><strong>왜</strong>: 보상 타입마다 자기 식별 필드를 하나씩 갖는 기존 패턴(<code>CurrencyTag</code>/<code>PartDefinition</code>/<code>AugmentPoolTag</code>)에 자연스럽게 얹혔고, &quot;새 포션 추가 = DT 행 하나 + 드랍행 하나, 코드 수정 0&quot;이 됨. 서버(<code>UNSHealDropSubsystem::ApplyHealEffect</code>)와 클라(<code>ANSLocalHealPickup::StartMeshLoad</code>) 둘 다 같은 DT를 태그로 조회하므로 회복%와 비주얼이 항상 같은 소스에서 나옴(불일치 불가능).</li>
</ul>
<h2 id="2-풀피-상태-수집-처리">2. 풀피 상태 수집 처리</h2>
<ul>
<li><strong>초기 설계</strong>: &quot;풀피여도 그냥 소모됨(단순화 우선)&quot;으로 미결 항목으로 남겨둠.</li>
<li><strong>변경 결정</strong>: 최대체력 증강이 있는 게임이라 %회복으로 설계했는데, 풀피에서도 소모되면 아이템 낭비가 실사용에서 자주 발생할 문제라 판단해 스킵 로직 추가.</li>
<li><strong>구현</strong>: <code>ApplyHealEffect</code>를 <code>void</code> → <code>bool</code>로 변경. 맨 앞에서 <code>Health &gt;= MaxHealth</code>면 GE를 적용하지 않고 <code>false</code> 반환. <code>TryCollect</code>는 <code>false</code>를 받으면 <code>CollectedPlayer</code>에 기록하지 않고(소모 안 됨) <code>SendRestoreEvent(DropId)</code>로 그 플레이어 화면의 낙관적 숨김을 되돌림 — 나중에 체력이 줄면 같은 드랍을 다시 시도 가능.</li>
<li><strong>부수 발견</strong>: 이 작업을 하다가 재화 시스템(<code>UNSCurrencyDropSubsystem</code>)에는 애초에 <code>SendRestoreEvent</code>를 호출하는 코드가 어디에도 없다는 기존 갭을 발견함. 이번엔 Heal에만 적용, 재화 쪽은 범위 밖으로 남겨둠.</li>
</ul>
<hr>
<h1 id="pr-작성">PR 작성</h1>
<ul>
<li><p>우선 Normal 몬스터 DT에만 추가</p>
<img width="3839" height="634" alt="스크린샷 2026-07-07 202903" src="https://github.com/user-attachments/assets/f48e35bb-7c93-495f-9561-fc57f3bf66b0" />
회복 Group을 만들고, Heal Potion Tag로 회복 아이템 식별
</li>
<li><p>DT_HealPotion에서 확률, 메시, 스케일 조정, RowName은 태그로</p>
<img width="3839" height="1421" alt="스크린샷 2026-07-07 202926" src="https://github.com/user-attachments/assets/0ebf00a6-409d-49e7-97c4-139146f73cd9" />
</li>
<li><p>RunConfig에 등록</p>
<img width="1917" height="584" alt="스크린샷 2026-07-07 210848" src="https://github.com/user-attachments/assets/10c09157-33d8-476d-b1ba-fb5476e33a9e" />
</li>
<li><p>BP_RunGameMode에 Heal프록시 등록</p>
<img width="3833" height="514" alt="스크린샷 2026-07-07 174859" src="https://github.com/user-attachments/assets/ee99f478-ddfe-49ae-8643-b82e468d9141" />
</li>
<li><p>Heal프록시에 DT, 픽업 클래스 등록</p>
</li>
</ul>
<img width="3834" height="405" alt="스크린샷 2026-07-07 190936" src="https://github.com/user-attachments/assets/12bb8f62-b369-4f29-88da-9b2ec7ecbe06" />

<img width="986" height="482" alt="스크린샷 2026-07-07 174730" src="https://github.com/user-attachments/assets/9ae565c3-4ed3-41b7-99fc-5cbc6eb208b4" />

<hr>
<h1 id="rpc">RPC</h1>
<p>클라이언트에서 발생한 이벤트를 서버로 전달하여 서버에서 로직을 실행하려면 <code>Server RPC</code>를 사용해야 한다. UFUNCTION(Server, Reliable, WithValidation) : 즉 통신의 신뢰성 보장을 위한 Reliable, 클라이언트의 요청을 서버가 검증할 수 있도록 WithValidation 지정자를 함께 사용하는것이 적절</p>
<pre><code class="language-c++">UFUNCTION(Server, Reliable)
void Server_SendChatMessage(FString&amp; Message);</code></pre>
<p>컴파일 했을때 결과 : RPC는 비동기 단방향 통신으로 값을 되돌려받기 위한 비상수 참조(Non-const Reference)를 파라미터로 사용할 수 없어 언리얼 헤더 툴(UHT)에러가 발생한다.</p>
<p>언리얼 C++에서 RPC를 통해 TArray와 같은 배열이나 구조체를 인자로 전달할 때는 값 복사에 의한 성능 저하를 막기 위해 const 참조(const &amp;)를 사용하는 것이 가장 효율적</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - 인런 상점 구현 및 트러블슈팅]]></title>
            <link>https://velog.io/@kyu_/TIL-%EC%9D%B8%EB%9F%B0-%EC%83%81%EC%A0%90-%EA%B5%AC%ED%98%84-%EB%B0%8F-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@kyu_/TIL-%EC%9D%B8%EB%9F%B0-%EC%83%81%EC%A0%90-%EA%B5%AC%ED%98%84-%EB%B0%8F-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</guid>
            <pubDate>Mon, 06 Jul 2026 12:02:40 GMT</pubDate>
            <description><![CDATA[<h1 id="인런-상점-구현">인런 상점 구현</h1>
<h2 id="변경-사항">변경 사항</h2>
<ul>
<li>인런에서 파츠 구매/리롤/업그레이드 구현</li>
<li>Upgrade DT 추가</li>
<li>파츠 Definition에서 파츠 정보를 사용, UpgradeDT에서는 레어도, 레어도별 가격, Reroll 가격등 사용</li>
</ul>
<h2 id="참고-사항">참고 사항</h2>
<ul>
<li><p>CommonDataConfig에 Upgrade추가</p>
<img width="1036" height="205" alt="스크린샷 2026-07-06 130238" src="https://github.com/user-attachments/assets/80bc5fcb-9b27-4417-b68f-e11d91ebc085" />
</li>
<li><p>Data &gt; Reward &gt; DT_PartUpgrade</p>
<img width="911" height="122" alt="스크린샷 2026-07-06 130252" src="https://github.com/user-attachments/assets/2f0c8340-9209-458d-9a3f-deaf1a9d2928" />

</li>
</ul>
<p>Rarity : 레어도
Value Range : 스텟 증감폭
Reroll Base Cost : 초기 리롤 비용
Reroll Cost Increment : 리롤 횟수마다 증가비용
Upgrade Success Chance : 다음 레어도로 업글될 확률(0~1)
Shop Weight : 각 레어도별 인런 상점 등장 확률
Shop Price : 각 레어도별 상점 가격
임의로 배치, 추후 변경 가능</p>
<ul>
<li>인런 파츠 NPC위치 Interaction &gt; NPC &gt; PartUpgradeNPC<img width="927" height="289" alt="스크린샷 2026-07-06 130226" src="https://github.com/user-attachments/assets/f17bda9a-0d21-4187-98dc-4d9ebc2d84b1" />

</li>
</ul>
<hr>
<h1 id="트러블-슈팅">트러블 슈팅</h1>
<h2 id="인런-상점-클라-문제">인런 상점 클라 문제</h2>
<p>클라에서는 인런 상점 재화가 즉시 줄어들지 않음</p>
<h2 id="리플레케이션-확인">리플레케이션 확인</h2>
<p>보통 클라에서 이런문제들은 리플레케이션 문제가 많기때문에 리플리케이션 경로를 확인하기로 하였다.
<code>UNSCurrencyComponent</code>의 재화 저장 구조를 추적했는데 모두 정상이였다.</p>
<h2 id="fastarrayserializer-특수화-누락">FastArraySerializer 특수화 누락</h2>
<p>현재 <code>FNSCurrencyWallet</code>이 <code>FFastArraySerializer</code>를 상속받고 <code>NetDeltaSerialize()</code>도 직접 구현해뒀지만, <code>TStructOpsTypeTraits&lt;FNSCurrencyWallet&gt;</code> 특수화가 누락되어있었다. 이게 없으면 언리얼 리플리케이션 시스템이 이 구조체에 커스텀 NetDeltaSerialize를 아예 호출하지않고 일반 구조체 복사로 처리한다고 한다.
하지만 FastArray 전용 콜백인 PostReplicateAdd, PostReplicatedChange는 FastArray 델타 경로에서만 호출 -&gt; 클라에서 발동하지 않음 OnTempChanged가 안터짐. 즉, 위젯이 갱신될 여지가 없음
호스트는 멀쩡</p>
<h2 id="결론">결론</h2>
<p>결과 통지 RPC에 잔액을 실어보내기로 하였다.
이미 존재하던 reliable Client RPC에 서버가 계산한 차감 후 잔액을 함께 실어 보내는 방식으로 전환을 하기로 마음먹었다.
구매/리롤/업그레이드가 끝나면 서버는 어차피 <code>Client_NotifyUpgradeResult(Slot, Result)</code>를 호출해 클라에 성공/실패/재화부족 등을 알려주고 있었다. 여기에 파라미터 하나만 추가하면 별도 인프라 없이 &quot;결과와 동시에 정확한 잔액&quot;을 보장할 수 있었다.</p>
<pre><code class="language-cpp">// NSPartEquipComponent.h
UFUNCTION(Client, Reliable)
void Client_NotifyUpgradeResult(FGameplayTag Slot, ENSPartUpgradeResult Result, int64 NewTempBalance);</code></pre>
<p><code>RerollStat</code> / <code>UpgradeRarity</code> / <code>Server_RequestPurchase</code>의 모든 성공/실패 분기에서 호출 시점의 <code>Currency-&gt;GetTemp()</code>를 함께 전달하도록 수정. 위젯 쪽은 <code>HandleUpgradeResult</code>가 이 값을 받아 <code>Wallet</code> 리플리케이션 도착을 기다리지 않고 즉시 <code>TempBalanceText</code>에 반영한다.</p>
<pre><code class="language-cpp">// NSPartUpgradeWidget.cpp
void UNSPartUpgradeWidget::HandleUpgradeResult(FGameplayTag PartSlot, ENSPartUpgradeResult Result, int64 NewTempBalance)
{
    SetBalanceText(NewTempBalance);  // Wallet 프로퍼티 복제와 무관하게 즉시 갱신
    RefreshBuyBox();
    RefreshUpgradePanels();

    OnUpgradeResultReceived(PartSlot, Result);  // WBP 이펙트/토스트용, 시그니처 유지
}</code></pre>
<p>HandleUpgradeResult 함수로 결과를 처리하는데 여기서 텍스트를 바로 갱신해주는 식으로
OnUpgradeResultReceived는 토스트용인데 아직 사용안하고 있음</p>
<h3 id="왜-이-방식을-선택했는가">왜 이 방식을 선택했는가</h3>
<ul>
<li><strong>타이밍 의존성 제거</strong>: 프로퍼티 리플리케이션은 <code>NetUpdateFrequency</code>, 우선순위, 대역폭 등 여러 변수에 걸쳐 있어 &quot;언제 도착하는지&quot;를 코드에서 직접 보장하기 어렵다. 반면 <code>Client, Reliable</code> RPC는 이미 결과 통지용으로 호출되고 있었으므로, 여기에 값 하나를 얹는 것만으로 반영 시점을 결과 통지 시점과 완전히 동일하게 고정할 수 있었다.</li>
<li><strong>최소 변경</strong>: 새 리플리케이션 채널이나 별도 동기화 인프라를 추가하지 않고, 기존 RPC 파라미터 확장 + 델리게이트(<code>FNSOnUpgradeResult</code>) 파라미터 확장만으로 해결됨. 둘 다 C++ 내부에서만 쓰이는 델리게이트/RPC라 블루프린트 쪽 영향이 없었다 (<code>OnUpgradeResultReceived</code> BlueprintImplementableEvent는 시그니처를 그대로 둬서 WBP 재작업 불필요).</li>
<li><strong>트레잇 수정은 유지</strong>: <code>TStructOpsTypeTraits</code> 특수화 자체는 틀린 코드가 아니고 여전히 필요하므로 되돌리지 않았다. 다만 그것만으로 실시간성을 &quot;보장&quot;하기엔 리플리케이션 타이밍이라는 외부 변수가 남아있어, RPC 기반의 확정적인 경로를 추가로 얹어 이중 안전판을 둔 구조가 되었다.</li>
</ul>
<hr>
<h2 id="파츠-3d-프리뷰-이슈">파츠 3D 프리뷰 이슈</h2>
<p>클라이언트에서 아웃런NPC의 3D프리뷰가 이상하게 나오던 문제 수정 (모든 파츠가 다 나옴)</p>
<img width="3833" height="2038" alt="스크린샷 2026-07-06 122633" src="https://github.com/user-attachments/assets/387c0bf9-ef56-4e73-925e-f61dbde89bb2" />

<h2 id="원인-조사">원인 조사</h2>
<p><code>ANSPartPreviewStage</code> 생성자를 보면 <code>CaptureComponent</code>에 <code>ShowOnlyActors.Add(this)</code>를 걸어뒀다 — 이 캡처가 &quot;자기 자신(스테이지 액터) 소속 컴포넌트만&quot; 찍도록 의도한 설계였다.</p>
<p>그런데 실제로는 <code>PrimitiveRenderMode</code>가 기본값(<code>PRM_RenderScenePrimitives</code>)으로 남아있었다. <strong><code>ShowOnlyActors</code> 목록은 <code>PrimitiveRenderMode</code>가 <code>PRM_UseShowOnlyList</code>일 때만 적용되고, 기본 모드에서는 통째로 무시되어 씬 전체가 캡처된다.</strong></p>
<p>그동안 문제가 안 보였던 이유는 스테이지의 배경판(<code>BackdropComponent</code>)이 뒤를 가려줬기 때문. 하지만 <code>UNSDataSubsystem::OnOutGameReferenceAssetsLoaded()</code>에서 호출하는 <code>ANSPartPreviewStage::WarmupAllPartMeshes()</code>가 <strong>3D 프리뷰 텍스처 밉을 미리 로드해두려고 모든 파츠 메시를 상주 컴포넌트로 붙인 별도의 &quot;워밍업 스테이지&quot;를 프리뷰 스테이지와 같은 위치 <code>(0, 0, -1000)</code>에 스폰</strong>한다. 캡처가 ShowOnly 필터 없이 씬 전체를 찍으니, 같은 좌표에 겹쳐 있는 워밍업 스테이지의 파츠 메시들까지 전부 프레임에 들어와 &quot;여러 파츠가 한 스켈레탈에 붙은 것처럼&quot; 보인 것</p>
<hr>
<h2 id="1차-수정-primitiverendermode를-useshowonlylist로-전환">1차 수정: PrimitiveRenderMode를 UseShowOnlyList로 전환</h2>
<pre><code class="language-cpp">// ANSPartPreviewStage 생성자
CaptureComponent-&gt;PrimitiveRenderMode = ESceneCapturePrimitiveRenderMode::PRM_UseShowOnlyList;
CaptureComponent-&gt;ShowOnlyActors.Add(this);</code></pre>
<p>이제 캡처가 <code>ShowOnlyActors</code> 목록에 있는 액터(자기 자신)의 컴포넌트만 렌더링하도록 명시</p>
<hr>
<h2 id="회귀-클라서버-가릴-것-없이-검은-화면만-나옴">회귀: 클라/서버 가릴 것 없이 검은 화면만 나옴</h2>
<p>위 수정 직후 재빌드해서 테스트하니, 이번엔 <strong>호스트든 클라든 프리뷰가 아예 검은 화면</strong>만 나왔다.</p>
<h3 id="원인">원인</h3>
<p><code>ShowOnlyActors.Add(this)</code>를 <strong>생성자</strong>에서 호출한 게 문제였다. 언리얼 오브젝트 생성 순서상, 생성자 실행 후 <strong>CDO(클래스 기본 오브젝트)로부터 프로퍼티가 복사/적용</strong>되는 단계가 있는데, CDO의 <code>ShowOnlyActors</code> 배열에는 (CDO 스스로가 생성자에서 <code>Add(this)</code>를 호출했을 때의) <strong>CDO 자기 자신에 대한 포인터</strong>가 들어 있다. 그 값이 실제 스폰된 인스턴스에 그대로 복사되면서, 정작 스폰된 인스턴스의 <code>ShowOnlyActors</code> 목록은 자기 자신이 아니라 CDO를 가리키게 된다.</p>
<p><code>PRM_UseShowOnlyList</code>를 켜기 전에는 이 목록 자체가 무시됐으니 문제가 드러나지 않았지만, 목록을 실제로 사용하는 순간 &quot;목록에 유효한(현재 인스턴스에 해당하는) 액터가 없음 → 아무것도 렌더링 안 됨 → 검은 화면&quot;이 된 것이다.</p>
<hr>
<h3 id="2차-수정-showonlyactors-등록을-beginplay로-이동">2차 수정: ShowOnlyActors 등록을 BeginPlay로 이동</h3>
<pre><code class="language-cpp">// 생성자 — 렌더 모드만 설정
CaptureComponent-&gt;PrimitiveRenderMode = ESceneCapturePrimitiveRenderMode::PRM_UseShowOnlyList;

// BeginPlay — 인스턴스가 확정된 시점에 등록
void ANSPartPreviewStage::BeginPlay()
{
    Super::BeginPlay();

    CaptureComponent-&gt;ShowOnlyActors.Reset();
    CaptureComponent-&gt;ShowOnlyActors.Add(this);
    ...
}</code></pre>
<p><code>BeginPlay</code>는 실제로 스폰되어 월드에 존재하는 인스턴스에서 호출되므로, 이 시점에 등록하면 CDO 복사 문제 없이 항상 올바른 <code>this</code>가 들어간다.</p>
<hr>
<h2 id="왜-이-방식을-선택했는가-1">왜 이 방식을 선택했는가</h2>
<ul>
<li><strong>원인에 정확히 대응하는 최소 수정</strong>: &quot;왜 씬 전체가 찍히는가&quot;(렌더 모드 누락)와 &quot;왜 검은 화면이 되는가&quot;(생성자에서의 self-reference가 CDO 복사에 덮어써짐)는 서로 다른 원인이었고, 각각을 정확히 짚어 한 줄씩만 손댔다. 렌더 모드는 생성자에 남겨도 무방한 정적 설정이라 그대로 두고, 인스턴스별로 달라야 하는 <code>this</code> 참조만 <code>BeginPlay</code>로 옮겼다.</li>
<li><strong>다른 스테이지 인스턴스와 충돌 없음</strong>: 프리뷰 스테이지(카탈로그/인런 상점)와 워밍업 스테이지가 같은 클래스를 같은 좌표에 여러 개 띄우는 구조이기 때문에, &quot;자기 자신만 보이게&quot;라는 원래 설계 의도(ShowOnlyActors)를 실제로 동작하게 만드는 것이 유일한 근본 해결책이었다 — 배경판으로 가리거나 좌표를 분리하는 식의 우회는 워밍업 메시 개수가 늘어날 때마다 다시 깨질 수 있는 임시방편이라 채택하지 않았다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL -  파츠 시스템 및 아웃런파츠 NPC 끝]]></title>
            <link>https://velog.io/@kyu_/TIL-%ED%8C%8C%EC%B8%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%B0%8F-%EC%95%84%EC%9B%83%EB%9F%B0%ED%8C%8C%EC%B8%A0-NPC-%EB%81%9D</link>
            <guid>https://velog.io/@kyu_/TIL-%ED%8C%8C%EC%B8%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%B0%8F-%EC%95%84%EC%9B%83%EB%9F%B0%ED%8C%8C%EC%B8%A0-NPC-%EB%81%9D</guid>
            <pubDate>Fri, 03 Jul 2026 11:47:01 GMT</pubDate>
            <description><![CDATA[<h2 id="1-파츠-데이터-구조-정의">1. 파츠 데이터 구조 정의</h2>
<h3 id="sourceneosanctumdatapartnsparttypesh"><code>Source/NeoSanctum/Data/Part/NSPartTypes.h</code></h3>
<p>파츠 관련 핵심 구조체가 모여있는 파일. 세 단계로 진화함.</p>
<ol>
<li><strong>최초</strong>: <code>FNSPartDefinitionRow</code>(DataTable row 구조체) 신설 — <code>Definition</code>(DA 소프트 참조), <code>PartSlot</code>, <code>bCanReroll</code>, <code>UnlockCost</code>, <code>ValueRange</code>, <code>bEnabled</code>. 파츠 수치를 DA가 아니라 DT에 두기로 한 결정의 시작점.</li>
<li><strong><code>FNSPartSlotRow</code> 추가 + <code>ENSPartSlot</code> enum 폐기 → <code>FGameplayTag PartSlot</code></strong>: 슬롯을 고정 enum(Body/Arm/Leg 3개)이 아니라 GameplayTag로 바꿔서, 슬롯 자체도 DT(<code>FNSPartSlotRow</code>: <code>SlotTag</code>, <code>UnlockCost</code>, <code>bUnlockedByDefault</code>, <code>bEnabled</code>)로 데이터화. 슬롯 언락 개념(비용, 기본 해금 여부)이 이때 처음 생김. <code>FNSPartData::Slot</code>도 enum → tag로 변경.</li>
</ol>
<h3 id="sourceneosanctumdatapartnspartdefinitionh"><code>Source/NeoSanctum/Data/Part/NSPartDefinition.h</code></h3>
<p>DA(DataAsset) 쪽에 있던 <code>PartSlot</code>/<code>bCanReroll</code>/<code>ValueRange</code>를 삭제. 위 DT 구조체로 옮기면서, DA는 <code>PartName</code>/<code>EffectClass</code>/<code>PartMesh</code>/<code>Icon</code> 같은 &quot;정체성·에셋 참조&quot;만 남기고 수치는 전부 DT로 위임. (계기: &quot;수치는 DataTable, DataAsset은 구조/참조만&quot;이라는 프로젝트 규칙)</p>
<hr>
<h2 id="2-데이터-로드-파이프라인--nsdatasubsystem">2. 데이터 로드 파이프라인 — <code>NSDataSubsystem</code></h2>
<h3 id="sourceneosanctumcoregameinstancesubsystemnsdatasubsystemhcpp"><code>Source/NeoSanctum/Core/GameInstance/Subsystem/NSDataSubsystem.h/.cpp</code></h3>
<p>세 단계로 발전:</p>
<ol>
<li><code>BuildPartRowCache()</code> 신설 — DT를 읽어 <code>TMap&lt;FPrimaryAssetId, FNSPartDefinitionRow&gt; CachedPartRowsByDefId</code>에 캐싱, <code>GetPartRow()</code>/<code>GetAllPartRows()</code>로 조회 제공. <code>OnOutGameReferenceAssetsLoaded()</code>에서 호출.</li>
<li>DT 에셋 참조를 처음엔 <code>NSDataSubsystem</code> 안에 직접(<code>TObjectPtr&lt;UDataTable&gt; PartDefinitionTable</code>) 뒀다가, 곧바로 <code>UNSDataSettings</code>(신설한 <code>UDeveloperSettings</code>)로 이관 — <code>OnOutGamePrimaryAssetsLoaded()</code>를 새로 만들어 Settings의 소프트 참조를 비동기 로드 후 캐시 빌드.</li>
<li><strong>최종적으로 <code>UNSDataSettings</code> 자체를 폐기</strong>하고 <code>NSCommonDataConfig</code>(기존에 있던 공용 데이터 설정 DA)로 재이관. 이유: 다른 모든 데이터가 &quot;페이즈 전용 PrimaryDataAsset + AssetBundle&quot; 패턴을 쓰는데 파츠만 <code>UDeveloperSettings</code>(프로젝트 세팅, 페이즈 개념 없음)를 써서 구조가 어긋나 있었음. <code>OnCommonAssetsLoaded()</code>에서 <code>BuildPartRowCache()</code>/<code>BuildSlotRowCache()</code>(슬롯 DT용으로 같이 신설) 호출하도록 정리. <code>GetSlotRow()</code>/<code>GetAllSlotRows()</code>도 이때 추가.</li>
</ol>
<h3 id="sourceneosanctumdataconfignscommondataconfigh"><code>Source/NeoSanctum/Data/Config/NSCommonDataConfig.h</code></h3>
<p><code>PartsBaseStatTable</code>, <code>PartsSlotBaseStatTable</code> 필드 추가 (<code>AssetBundles=&quot;CommonData&quot;</code>) — 파츠 DT 2개를 여기로 옮겨받는 자리.</p>
<h3 id="sourceneosanctumdatautilnsdatasettingshcpp-삭제"><code>Source/NeoSanctum/Data/Util/NSDataSettings.h/.cpp</code> (삭제)</h3>
<p>잠깐 만들었다가 위 이유로 완전히 삭제. <code>Config/DefaultGame.ini</code>의 관련 섹션도 같이 제거.</p>
<h3 id="sourceneosanctumneosanctumbuildcs"><code>Source/NeoSanctum/NeoSanctum.Build.cs</code></h3>
<p><code>NSDataSettings</code>(<code>UDeveloperSettings</code> 상속) 만들 때 <code>&quot;DeveloperSettings&quot;</code> 모듈 의존성 추가. (참고: <code>NSDataSettings</code> 자체는 나중에 삭제됐지만 이 모듈 의존성 라인은 diff상 그대로 남아있음 — 다른 데서 안 쓰면 정리 대상)</p>
<hr>
<h2 id="3-파츠-조회-유틸--nspartutils">3. 파츠 조회 유틸 — <code>NSPartUtils</code></h2>
<h3 id="sourceneosanctumprogressionpartnspartutilshcpp"><code>Source/NeoSanctum/Progression/Part/NSPartUtils.h/.cpp</code></h3>
<ul>
<li><code>ResolvePartRow(WorldContextObject, DefId)</code> 신설 — <code>NSDataSubsystem::GetPartRow()</code>를 경유하는 조회 헬퍼. <code>PurchasePart</code>/<code>EquipPart</code>/<code>RerollStat</code>/<code>RollValueForRarity</code> 등 여러 곳에서 DA를 직접 로드하는 대신 이걸 거치게 됨.</li>
<li>후속으로 <code>ResolvePartDefinition(WorldContextObject, TSoftObjectPtr&lt;UNSPartDefinition&gt;)</code> 오버로드 추가 — 기존엔 <code>FNSPartData</code>를 받는 버전만 있었는데, 카탈로그 선택(<code>FNSPartDefinitionRow::Definition</code>)과 장착중 조회(<code>FNSPartSaveData::Definition</code>) 양쪽에서 &quot;소프트포인터 → 캐시 경유 Definition&quot;이 필요해져서 공용화. 기존 <code>FNSPartData</code> 버전은 이 오버로드에 위임하도록 리팩터.</li>
</ul>
<hr>
<h2 id="4-서버-진행-로직--nsprogressionsubsystem">4. 서버 진행 로직 — <code>NSProgressionSubsystem</code></h2>
<h3 id="sourceneosanctumcoregameinstancesubsystemnsprogressionsubsystemhcpp"><code>Source/NeoSanctum/Core/GameInstance/Subsystem/NSProgressionSubsystem.h/.cpp</code></h3>
<p>파츠 구매/장착/슬롯 언락의 실질적인 백엔드. 여러 단계로 완성됨:</p>
<ul>
<li><code>PurchasePart(Definition, Rarity)</code> — 처음엔 <code>Cost</code>를 파라미터로 직접 받았으나, DT에서 <code>Row-&gt;UnlockCost</code>를 읽어오는 구조로 변경(<code>LoadSynchronous</code> 제거가 핵심 동기). 최종 버전엔 슬롯 언락 여부 체크(<code>IsSlotUnlocked</code>)도 추가되어, 슬롯이 안 열려있으면 구매 자체가 실패하도록 함.</li>
<li><code>SetEquippedPart(CharacterId, Definition, Rarity)</code> / <code>GetEquippedPart(CharacterId)</code> — 장착 저장/조회. <code>Definition</code>이 null이면 해제로 처리(장착 해제 기능이 이 널 체크 하나로 동작).</li>
<li><code>IsPartOwned</code>, <code>GetCommonCurrency</code>, <code>GetLastSelectedCharacterId</code> — 조회용.</li>
<li><code>UnlockSlot(CharacterId, Slot)</code> / <code>IsSlotUnlocked</code> / <code>GetSlotUnlockCost</code> — 슬롯 언락 로직. 재화 체크 실패, Row 없음, 이미 해금됨 등 각 실패 분기에 진단 로그 추가(원래 로그가 하나도 없어서 재화 부족 때문에 언락이 안 되는 걸 못 찾고 있었음).</li>
</ul>
<h3 id="sourceneosanctumprogressionsavenspermanentsavegameh"><code>Source/NeoSanctum/Progression/Save/NSPermanentSaveGame.h</code></h3>
<ul>
<li><code>FNSCharacterSaveData::UnlockedSlots</code>(<code>TSet&lt;FGameplayTag&gt;</code>) 추가 — 슬롯 언락 상태 저장.</li>
<li><code>OwnedParts</code>(계정 공유 파츠 인벤토리) 필드 위치 정리.</li>
</ul>
<hr>
<h2 id="5-드랍장착-실행-로직">5. 드랍/장착 실행 로직</h2>
<h3 id="sourceneosanctumprogressionrewardnsrewardhandlerhcpp"><code>Source/NeoSanctum/Progression/Reward/NSRewardHandler.h/.cpp</code></h3>
<p><code>MakePartDataFromDropResult()</code> — 드랍 시 파츠 수치를 만들 때 <code>LoadSynchronous()</code>로 DA를 강제 동기 로드하던 걸 제거하고, <code>NSPartUtils::ResolvePartRow()</code>(캐시 경유)로 대체. 이 함수가 <code>UWorld*</code> 인자를 새로 받도록 시그니처 변경(서브시스템 접근에 필요).</p>
<h3 id="sourceneosanctumprogressionpartnspartequipcomponenthcpp"><code>Source/NeoSanctum/Progression/Part/NSPartEquipComponent.h/.cpp</code></h3>
<p>실제로 GAS 이펙트를 붙이고 캐릭터에 파츠를 장착하는 컴포넌트.</p>
<ul>
<li><code>EquipPart</code>/<code>RerollStat</code>/<code>RollValueForRarity</code>가 <code>Def-&gt;PartSlot</code>/<code>Def-&gt;ValueRange</code>를 직접 읽던 걸, DT 이관 이후 <code>NSPartUtils::ResolvePartRow()</code>로 대체.</li>
<li>슬롯 타입을 <code>ENSPartSlot</code> enum에서 <code>FGameplayTag</code>로 전면 전환 (delegate 시그니처 <code>FNSOnPartChanged</code>, <code>HasEquippedPart</code>, <code>GetEquippedPart</code>, <code>FindPart</code>, <code>DropPartInSlot</code>, <code>RemovePartEffects</code>, <code>RemoveGEForSlot</code>, <code>RemoveAbilitiesForSlot</code>, <code>ApplyPartEffect</code>, <code>Internal_ApplyGE</code>, <code>OnEffectLoaded</code>, <code>GrantAbilities</code>, <code>OnAbilitiesLoaded</code>, <code>RerollStat</code>, <code>UpgradeRarity</code>, 관련 <code>TMap</code> 전부 포함).</li>
</ul>
<h3 id="sourceneosanctumcharactercomponentnspartvisualcomponenthcpp"><code>Source/NeoSanctum/Character/Component/NSPartVisualComponent.h/.cpp</code></h3>
<p>캐릭터 몸에 파츠 메시를 실제로 붙이는 컴포넌트(리더포즈 방식). 위와 같은 이유로 슬롯을 enum → tag로 전환. 이 과정에서 하드코딩되어 있던 <code>{ ENSPartSlot::Body, ENSPartSlot::Arm, ENSPartSlot::Leg }</code> 순회를 <code>DataSS-&gt;GetAllSlotRows()</code> 기반 순회로 바꿔서, 슬롯이 DT에 추가/삭제되어도 코드 수정 없이 대응 가능하게 함 (<code>BindToEquipComponent</code>, <code>EnsureSlotComponents</code>, <code>GetSlotMeshComp</code>, <code>HandlePartChanged</code>, <code>UpdateSlotVisual</code>, <code>ClearSlotVisual</code>).</p>
<hr>
<h2 id="6-인런-hud-파츠-패널">6. 인런 HUD 파츠 패널</h2>
<h3 id="sourceneosanctumuipartnspartpanelwidgethcpp"><code>Source/NeoSanctum/UI/Part/NSPartPanelWidget.h/.cpp</code></h3>
<p>원래 <code>BodySlotButton</code>/<code>ArmSlotButton</code>/<code>LegSlotButton</code> 3개를 WBP에 고정 배치해서 쓰던 걸, <code>SlotButtonContainer</code>(빈 패널) + <code>SlotButtonTemplate</code>(버튼 1개짜리 템플릿)로 바꿔서 <code>BuildSlotButtons()</code>가 슬롯 DT를 읽어 런타임에 동적 생성하도록 변경. <code>ApplySlot</code>/<code>HandlePartChanged</code>도 슬롯 태그 기반으로 전환. <code>OnOutGameDataReady()</code>를 새로 붙여서 DT 로드 완료 시점에 맞춰 버튼을 빌드.</p>
<hr>
<h2 id="7-파츠-3d-프리뷰">7. 파츠 3D 프리뷰</h2>
<h3 id="sourceneosanctumprogressionpartnspartpreviewstagehcpp-신규"><code>Source/NeoSanctum/Progression/Part/NSPartPreviewStage.h/.cpp</code> (신규)</h3>
<p>파츠 상세 패널의 실시간 3D 프리뷰 전용 액터. 월드 밖 먼 위치에 스폰되어 자기 자신만 캡처하는 <code>SceneCaptureComponent2D</code> + 조명 2개(Key/Rim) + 배경판(<code>BackdropComponent</code>)으로 구성. <code>SetPreviewMesh()</code>로 메시 교체, 드래그로 <code>AddManualYaw()</code>, 휠로 <code>AddZoom()</code>. <code>WarmupAllPartMeshes()</code>로 파츠샵 진입 전 모든 파츠 메시를 미리 로드해 첫 프레임에 저해상도 텍스처가 안 뜨게 함.</p>
<h3 id="관련-콘텐츠">관련 콘텐츠</h3>
<ul>
<li><code>BP_PartPreviewStage.uasset</code> — 위 액터의 BP</li>
<li><code>M_PartPreview.uasset</code> — RenderTarget을 UI Image에 뿌리는 머티리얼</li>
<li><code>M_PartPreviewBackDrop.uasset</code>, <code>T_PreviewBackDrop.uasset</code> — 배경을 거점 사진으로 교체 (Unlit 머티리얼 + 엔진 Plane UV 반전 보정)</li>
</ul>
<hr>
<h2 id="8-파츠샵-ui--카탈로그상세-패널">8. 파츠샵 UI — 카탈로그/상세 패널</h2>
<h3 id="sourceneosanctumuipartnspartdetailwidgethcpp-신규"><code>Source/NeoSanctum/UI/Part/NSPartDetailWidget.h/.cpp</code> (신규)</h3>
<p>카탈로그 후보와 장착중인 파츠 양쪽에서 공용으로 쓰는 상시 설명 패널.</p>
<ul>
<li><code>SetupFromDefinition(Row, Def)</code> — 카탈로그 후보(이름/슬롯/비용/리롤가능)</li>
<li><code>SetupFromEquipped(SaveData, Def)</code> — 장착중 파츠(이름/등급/효과값)</li>
<li><code>SetupFromSlotLock(SlotRow)</code> — 잠긴 슬롯 정보(슬롯명/언락비용), 3D 프리뷰 없음</li>
<li><code>ClearDetail()</code>, <code>SetPreviewTarget(Stage)</code>, <code>ClearPreview()</code> — 프리뷰 연결/해제</li>
<li><code>NativeConstruct()</code>에서 <code>ClearPreview()</code> 호출 — 초기 상태에 빈 흰색 프리뷰 이미지가 남아있던 버그 수정</li>
<li>마우스 드래그/휠 이벤트를 받아 <code>NSPartPreviewStage</code>의 <code>AddManualYaw</code>/<code>AddZoom</code> 호출</li>
<li>모든 텍스트에 &quot;이름 :&quot;, &quot;슬롯 :&quot;, &quot;효과 :&quot; 등 라벨 포맷 적용, 슬롯 태그를 leaf 이름으로 축약해서 표시(<code>GetSlotLeafName</code>), 효과 수치는 소수점 1자리로 포맷(<code>FormatEffectValue</code>)</li>
</ul>
<h3 id="sourceneosanctumuipartnspartcatalogentrywidgethcpp-신규"><code>Source/NeoSanctum/UI/Part/NSPartCatalogEntryWidget.h/.cpp</code> (신규)</h3>
<p>카탈로그 개별 항목. 클릭 가능한 <code>SelectButton</code> + 아이콘 비동기 로드(<code>OnDefinitionLoaded</code>) + <code>SelectedIndicator</code>(선택 시에만 보이는 반투명 오버레이, <code>SetIsSelected(bool)</code>)로 구성. 클릭하면 <code>Owner-&gt;SelectCatalogPart(StoredRow, this)</code> 호출.</p>
<h3 id="sourceneosanctumuipartbuttonnspartslotbuttonhcpp"><code>Source/NeoSanctum/UI/Part/Button/NSPartSlotButton.h/.cpp</code></h3>
<p>원래 HUD 파츠 패널(<code>WBP_PartsButton</code>) 전용이던 걸 파츠샵의 &quot;장착중&quot; 3박스에도 재사용. <code>SelectedHighlight</code>(선택 표시 오버레이) + <code>SetHighlighted(bool)</code> 추가 — CommonUI 자체 Selected 스타일 기능을 시도했다가 적용이 안 되는 문제가 계속돼서, 카탈로그와 동일한 수동 오버레이 방식으로 통일.</p>
<hr>
<h2 id="9-파츠샵-메인-위젯--nspartequipwidget">9. 파츠샵 메인 위젯 — <code>NSPartEquipWidget</code></h2>
<h3 id="sourceneosanctumuiinteractionnspartequipwidgethcpp"><code>Source/NeoSanctum/UI/Interaction/NSPartEquipWidget.h/.cpp</code></h3>
<p>이 브랜치에서 가장 많이 바뀐 파일. 세 번의 큰 리팩터를 거침.</p>
<p><strong>1단계 — 컴포넌트 직접 호출 → ProgressionSubsystem 경유</strong>
원래 <code>EquipSelectedPart(int32 SelectableIndex)</code>가 배열 인덱스로 파츠를 골라 <code>UNSPartEquipComponent::Server_RequestEquip</code>을 직접 호출하던 구조를, <code>RequestUnlockPart(Definition)</code>/<code>RequestEquipPart(Definition)</code>가 <code>NSProgressionSubsystem</code>(세이브 기반 구매/장착)을 경유하도록 전면 교체.</p>
<p><strong>2단계 — 선택 모델 도입 (카탈로그 클릭 → 중앙 패널 → 장착 버튼)</strong>
항목별 개별 버튼+hover 방식을 버리고, <code>BuildPartEntries()</code>로 <code>BodyListContainer</code>/<code>ArmListContainer</code>/<code>LegListContainer</code> 3분할 카탈로그 구성, <code>SelectCatalogPart(Row)</code>로 선택 시 중앙 패널(<code>SelectedDetailWidget</code>) 갱신, <code>OnEquipButtonClicked()</code>로 구매/장착 확정. 왼쪽 &quot;장착중&quot; 3박스는 <code>SelectEquippedSlot(SlotTag)</code>로 클릭한 슬롯에 장착된 것만 표시.</p>
<p><strong>3단계 — 슬롯 언락/해제를 같은 선택 흐름에 통합 (이번 세션 핵심 작업)</strong></p>
<ul>
<li><code>ENSPartSelectionMode</code>(<code>None</code>/<code>Part</code>/<code>SlotUnlock</code>/<code>UnequipPart</code>) 신설 — 중앙 패널/버튼이 가리키는 대상을 하나의 상태로 관리</li>
<li><code>RefreshEquippedDisplay()</code> — 왼쪽 패널을 슬롯버튼 클릭과 무관하게 &quot;현재 장착 파츠&quot;만 상시 표시하도록 단순화 (기존 <code>SelectEquippedSlot</code>을 대체)</li>
<li><code>RefreshEquippedSlotButton()</code> — 왼쪽 3개 슬롯버튼(<code>NSPartSlotButton</code>)에 실제 장착 파츠의 아이콘/이름/수치를 표시</li>
<li><code>OnSlotButtonClicked(SlotTag)</code> — 슬롯버튼 3개 공용 처리: 잠긴 슬롯 클릭 시 언락 정보 표시, 해금+장착된 슬롯 클릭 시 해제 정보 표시, 해금+빈 슬롯은 무반응</li>
<li><code>RefreshEquipButton()</code> — 슬롯해금/해제/슬롯해금필요/구매/장착 5가지 상태로 확장</li>
<li><code>RequestUnequipPart()</code> 신설 — null Definition으로 <code>SetEquippedPart</code> 호출해 해제</li>
<li><code>IsSlotEquipped(SlotTag)</code> — 이 슬롯에 지금 뭐가 장착돼있는지 BP에서도 조회 가능하게 노출 (슬롯버튼 활성 표시용)</li>
<li><code>RefreshSelectionHighlights()</code> — 카탈로그 선택/슬롯버튼 선택 하이라이트가 서로 배타적으로 켜지고 꺼지도록 정리</li>
<li>죽어있던 <code>NSPartSlotEntryWidget</code>/<code>SlotListContainer</code>(별도 슬롯 언락 리스트, WBP에 배치가 안 돼서 한 번도 작동한 적 없던 코드) 완전 삭제</li>
</ul>
<hr>
<h2 id="10-상호작용컨트롤러-자잘한-수정">10. 상호작용/컨트롤러 자잘한 수정</h2>
<h3 id="sourceneosanctuminteractioncomponentnsinteractioncomponentcpp"><code>Source/NeoSanctum/Interaction/Component/NSInteractionComponent.cpp</code></h3>
<p><code>Client_OnInteractApproved_Implementation</code>이 <code>ANSInteractableNPCBase</code>면 무조건 PC의 <code>OpenInteractionWidget</code>을 호출하던 걸, <code>NPC-&gt;GetInteractionWidgetClass()</code>가 있을 때만 그렇게 하도록 조건 추가. 위젯 클래스가 없는 NPC(예: 직접 <code>OnInteract</code>를 처리하는 캐릭터 선택 NPC)는 기존 <code>Execute_OnInteract</code> 폴백 경로를 타게 함. (원인: 파츠샵/펫 NPC는 위젯 클래스 방식, 캐릭터 선택 NPC는 직접 호출 방식으로 서로 다른데 공용 컴포넌트가 한쪽만 가정하고 있었음)</p>
<h3 id="sourceneosanctumcoreplayercontrollernsplayercontrollercpp"><code>Source/NeoSanctum/Core/PlayerController/NSPlayerController.cpp</code></h3>
<p><code>TogglePauseMenu()</code>에 <code>if (ActiveInteractionWidget) { CloseInteractionWidget(); return; }</code> 추가 — ESC를 눌렀을 때 상호작용 위젯(파츠샵 등)이 열려있으면 일시정지 메뉴보다 그것부터 닫히게 함.</p>
<hr>
<h2 id="11-개발용-치트">11. 개발용 치트</h2>
<h3 id="sourceneosanctumcorecheatnscheatmanagerhcpp"><code>Source/NeoSanctum/Core/Cheat/NSCheatManager.h/.cpp</code></h3>
<p><code>Debug_SetCommonCurrency()</code> 신규 — 슬롯 언락/파츠 구매 테스트용으로 공통 재화를 즉시 10000으로. <code>NSProgressionSubsystem</code>이 참조하는 것과 동일한 캐시된 세이브 객체 값만 메모리에서 직접 바꾸는 방식(디스크 저장·서버 동기화 없음 — 테스트 목적상 그럴 필요가 없어서 최대한 단순하게 처리).</p>
<hr>
<h2 id="12-데이터-콘텐츠-content">12. 데이터 콘텐츠 (Content/)</h2>
<ul>
<li><code>DT_PartDefinition.uasset</code>, <code>DT_PartSlot.uasset</code> — 위 DT 구조체들의 실제 데이터</li>
<li><code>Data/Reward/Part/DataAsset/</code> — 파츠별 DA(Arm 7종, Body 2종, Leg 3종)</li>
<li><code>Data/Reward/Part/Icon/</code> — 파츠별 아이콘 텍스처</li>
<li><code>DA_CommonDataConfig.uasset</code> — <code>PartsBaseStatTable</code>/<code>PartsSlotBaseStatTable</code>에 위 DT 연결</li>
<li><code>WBP_PartCatalogEntry.uasset</code>, <code>WBP_PartDetail.uasset</code>, <code>WBP_PartSlotButton.uasset</code>, <code>WBP_PartEquip.uasset</code>(<code>UI/Interaction/Parts/</code>로 위치 이동) — 파츠샵 UI 에셋 일체</li>
<li><code>WBP_PartsButton.uasset</code>, <code>WBP_PartPanel.uasset</code> — HUD 파츠 패널 쪽 에셋 (슬롯 동적 생성 대응)</li>
<li><code>BP_Style_PartSlotButton.uasset</code> — CommonButtonStyle 시도했다가 실제로는 안 쓰게 된 에셋 (수동 하이라이트 방식으로 대체됨, 정리 대상인지 확인 필요)</li>
<li><code>WBP_PartSlotEntry.uasset</code> — 삭제된 <code>NSPartSlotEntryWidget</code>이 쓰던 에셋 (C++ 클래스가 없어졌으므로 같이 정리 대상)</li>
</ul>
<hr>
<h2 id="확인이-필요한-것들">확인이 필요한 것들</h2>
<ul>
<li><code>Content/NeoSanctum/Interaction/NPC/BP_PartsNPC.uasset</code>, <code>Content/NeoSanctum/UI/HUD/WBP_HUD.uasset</code>(develop과 충돌 후 develop 버전 채택) — 이 브랜치에서 실제로 뭘 바꾸려 했는지 파악 못 함</li>
<li><code>BP_Style_PartSlotButton.uasset</code>, <code>WBP_PartSlotEntry.uasset</code> — 안 쓰는 게 확실하면 삭제 권장</li>
</ul>
<h2 id="파츠-프리뷰-배경-변경하기">파츠 프리뷰 배경 변경하기</h2>
<p>머티리얼을 만들고 파츠 프리뷰 화면으로 적절한곳에서 언리얼 스크린샷을 찍어서 머티리얼에 넣고 실제로 3D에셋이 보일때 뒤 배경으로 설정하였다.</p>
<h2 id="파츠-슬롯-해금">파츠 슬롯 해금</h2>
<p>파츠 슬롯이 해금이 안되는문제가 있어서 로그로 우선 문제파악을 들어갔다. -&gt; 로직은 정상인데 재화가 없어서 언락이 안되고 있었음
Tab눌렀을때 파츠가 출력되는것처럼 파츠장착 위젯에서도 해당 파츠 슬롯을 재사용하는식으로 변경했음 -&gt; 다 임시라서 우선 이름 :, 효과 : 이런식으로 표현</p>
<h3 id="ai-한테-질문">AI 한테 질문</h3>
<p>문제1. 파츠 장착을 했을때 정보가 이상함 (2.779가 뭘말하는건지 모르겠음), 파츠 이름 또 다른 정보들을 출력해야 될거같음 그리고 무슨 정보인지 알 수 있게 이름 : , 효과 : 이런식으로 들어가야 할거같음(가운데 파츠 디테일도 똑같이)
문제2. 슬롯쪽에 해금된 슬롯의 경우 슬롯색이 흰색으로 변해서 활성화된 느낌을 주어야할거같음
문제3. 장착한 슬롯이 슬롯버튼에 갱신이 안됨</p>
<h2 id="선택한-느낌이-안듬">선택한 느낌이 안듬</h2>
<p>파츠나 슬롯을 클릭했을때 선택되어있다는 느낌이 안듬</p>
]]></description>
        </item>
    </channel>
</rss>