<?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>Sun, 20 Sep 2026 13:23:44 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[게임 수학 3주차]]></title>
            <link>https://velog.io/@kyu_/%EA%B2%8C%EC%9E%84-%EC%88%98%ED%95%99-3%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@kyu_/%EA%B2%8C%EC%9E%84-%EC%88%98%ED%95%99-3%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 20 Sep 2026 13:23:44 GMT</pubDate>
            <description><![CDATA[<h1 id="게임-수학-3주차">게임 수학 3주차</h1>
<p>이번에는 앞에서 배운 외적부터 Basis, Matrix까지 한 번에 연결해서 정리해본다.</p>
<p>각 개념을 따로 보면 서로 관련이 없어 보일 수 있지만, 게임에서는 다음과 같은 흐름으로 이어진다.</p>
<pre><code class="language-text">방향을 표현한다
    ↓
Vector

두 방향에 수직인 새로운 방향이 필요하다
    ↓
Cross Product

Forward / Right / Up 같은 기준축이 필요하다
    ↓
Basis

Basis를 기준으로 Local 방향과 좌표를 표현한다
    ↓
Coordinate System

Basis와 여러 변환을 한 번에 계산하고 싶다
    ↓
Matrix</code></pre>
<hr>
<h1 id="1-cross-product">1. Cross Product</h1>
<p>외적(Cross Product)은 두 벡터로부터 <strong>두 벡터 모두에 수직인 새로운 벡터</strong>를 만드는 연산이다.</p>
<p>게임에서는 주로 다음과 같은 곳에서 사용한다.</p>
<ul>
<li>표면의 Normal 계산</li>
<li>좌우 방향 판정</li>
<li>Forward / Right / Up 축 생성</li>
<li>삼각형 넓이 계산</li>
</ul>
<p>예를 들어</p>
<pre><code class="language-text">X = (1, 0, 0)
Y = (0, 1, 0)</code></pre>
<p>라면</p>
<pre><code class="language-text">X × Y = (0, 0, 1)</code></pre>
<p>이 된다.</p>
<p>결과는 X와 Y 모두에 수직인 Z 방향이다.</p>
<p>외적에서 중요한 성질이 하나 있다.</p>
<pre><code class="language-text">A × B = -(B × A)</code></pre>
<p>즉, 순서를 바꾸면 결과 방향이 반대가 된다.</p>
<hr>
<h1 id="2-triangle-normal">2. Triangle Normal</h1>
<p>게임의 3D 모델은 대부분 삼각형으로 이루어져 있다.</p>
<p>삼각형의 세 점이 다음과 같다고 하자.</p>
<pre><code class="language-text">P0 = (0, 0, 0)
P1 = (0, 3, 0)
P2 = (0, 0, 2)</code></pre>
<p>우리가 알고 싶은 것은</p>
<pre><code class="language-text">이 삼각형 표면이 어느 방향을 보고 있는가?</code></pre>
<p>이다.</p>
<p>점 하나만 가지고는 표면의 방향을 알 수 없기 때문에 먼저 삼각형 위의 두 변을 만든다.</p>
<pre><code class="language-text">E1 = P1 - P0
E2 = P2 - P0</code></pre>
<p>계산하면</p>
<pre><code class="language-text">E1 = (0, 3, 0)
E2 = (0, 0, 2)</code></pre>
<p>두 벡터 모두 삼각형 표면 위에 놓여 있다.</p>
<p>이제 두 벡터를 외적한다.</p>
<pre><code class="language-text">N = E1 × E2</code></pre>
<p>결과는</p>
<pre><code class="language-text">N = (6, 0, 0)</code></pre>
<p>이다.</p>
<p>방향만 필요하다면 Normalize한다.</p>
<pre><code class="language-text">N = (1, 0, 0)</code></pre>
<p>따라서 이 정점 순서에서 계산한 삼각형의 Normal은 +X 방향이다.</p>
<p>Normal은 게임에서 굉장히 자주 사용된다.</p>
<pre><code class="language-text">빛이 표면에 얼마나 정면으로 들어오는가?

충돌한 물체를 어느 방향으로 밀어낼 것인가?

벽을 따라 어느 방향으로 이동시킬 것인가?

총알이 어느 방향으로 반사될 것인가?</code></pre>
<p>같은 계산의 기준이 된다.</p>
<hr>
<h2 id="cross-순서를-바꾸면">Cross 순서를 바꾸면?</h2>
<p>이번에는</p>
<pre><code class="language-text">E2 × E1</code></pre>
<p>을 계산하면 결과가 반대가 된다.</p>
<pre><code class="language-text">(-6, 0, 0)</code></pre>
<p>Normalize하면</p>
<pre><code class="language-text">(-1, 0, 0)</code></pre>
<p>이다.</p>
<p>따라서 삼각형에서 정점 순서는 실제 의미를 가진다.</p>
<p>정점의 순서에 따라 Normal 방향이 달라지고, 렌더링에서도 앞면과 뒷면을 판정할 때 정점의 회전 방향인 Winding Order를 사용한다.</p>
<p>단, 시계 방향과 반시계 방향 중 무엇을 앞면으로 사용할지는 그래픽 API나 엔진 설정에 따라 달라질 수 있다.</p>
<hr>
<h1 id="3-cross-product의-크기">3. Cross Product의 크기</h1>
<p>외적은 방향만 만들어내는 것이 아니다.</p>
<pre><code class="language-text">|A × B|</code></pre>
<p>의 크기는 A와 B가 만드는 <strong>평행사변형의 넓이</strong>다.</p>
<p>따라서 삼각형의 넓이는</p>
<pre><code class="language-text">TriangleArea = |A × B| / 2</code></pre>
<p>로 구할 수 있다.</p>
<p>외적 결과의 크기가 거의 0이라면 두 변이 거의 같은 방향이라는 의미이므로, 삼각형의 넓이 역시 거의 0이다.</p>
<p>이런 삼각형을 Degenerate Triangle, 즉 퇴화 삼각형이라고 볼 수 있다.</p>
<hr>
<h1 id="4-linear-combination">4. Linear Combination</h1>
<p>Forward와 Right가 있다고 하자.</p>
<pre><code class="language-text">Forward = (1, 0, 0)
Right   = (0, 1, 0)</code></pre>
<p>W를 누르면 앞으로 이동하고 D를 누르면 오른쪽으로 이동한다고 하면</p>
<pre><code class="language-cpp">Move += Forward * InputY;
Move += Right * InputX;</code></pre>
<p>처럼 표현할 수 있다.</p>
<p>W와 D를 동시에 눌렀다면</p>
<pre><code class="language-text">Move = Forward + Right</code></pre>
<p>이므로</p>
<pre><code class="language-text">(1, 0, 0) + (0, 1, 0)
= (1, 1, 0)</code></pre>
<p>이 된다.</p>
<p>이것이 선형 결합(Linear Combination)이다.</p>
<p>일반적으로</p>
<pre><code class="language-text">aA + bB</code></pre>
<p>처럼 여러 벡터에 숫자를 곱해서 더하는 것을 선형 결합이라고 한다.</p>
<p>실제 캐릭터 이동에서는 <code>(1,1,0)</code>의 길이가 <code>√2</code>이기 때문에 대각선 이동이 더 빨라지지 않도록 입력 벡터를 Normalize하거나 길이를 제한하는 처리를 함께 사용하는 경우가 많다.</p>
<hr>
<h1 id="5-span">5. Span</h1>
<p>Forward와 Right만 가지고</p>
<pre><code class="language-text">aForward + bRight</code></pre>
<p>를 만든다고 생각해보자.</p>
<pre><code class="language-text">Forward = (1, 0, 0)
Right   = (0, 1, 0)</code></pre>
<p>a와 b에 어떤 숫자를 넣더라도 결과의 Z값은 항상 0이다.</p>
<p>따라서 Forward와 Right만으로는 바닥 평면의 모든 방향은 만들 수 있지만 Up이나 Down 방향은 만들 수 없다.</p>
<p>이렇게 주어진 벡터들의 선형 결합으로 만들 수 있는 모든 범위를 <strong>Span</strong>이라고 한다.</p>
<p>게임 개발에서는 다음처럼 생각할 수 있다.</p>
<blockquote>
<p>현재 가지고 있는 방향축들로 어느 공간까지 표현할 수 있는가?</p>
</blockquote>
<p>Forward와 Right의 Span은 2차원 평면이고,</p>
<p>Forward, Right, Up 세 축이 서로 독립이라면 3차원 공간 전체를 표현할 수 있다.</p>
<hr>
<h1 id="6-linear-independence">6. Linear Independence</h1>
<p>다음과 같은 두 벡터가 있다고 하자.</p>
<pre><code class="language-text">Forward = (1, 0, 0)
Right   = (2, 0, 0)</code></pre>
<p>Right라는 이름을 붙였지만 실제로는 Forward의 두 배일 뿐이다.</p>
<p>따라서</p>
<pre><code class="language-text">aForward + bRight</code></pre>
<p>를 아무리 계산해도 X축 밖으로 나갈 수 없다.</p>
<p>Right가 새로운 방향 정보를 전혀 제공하지 않는 것이다.</p>
<p>이런 상태를 선형 종속(Linear Dependence)이라고 한다.</p>
<p>반대로</p>
<pre><code class="language-text">Forward = (1, 0, 0)
Right   = (0, 1, 0)</code></pre>
<p>이라면 두 벡터는 서로 다른 방향을 제공하고, 한 벡터를 다른 벡터의 배수로 만들 수 없다.</p>
<p>이 둘은 선형 독립(Linear Independence)이다.</p>
<hr>
<h1 id="7-basis">7. Basis</h1>
<p>Basis는 공간 전체를 표현할 수 있는 <strong>선형 독립인 벡터들의 집합</strong>이다.</p>
<p>3D 게임에서 가장 익숙한 예는</p>
<pre><code class="language-text">Forward
Right
Up</code></pre>
<p>이다.</p>
<p>세 벡터가 서로 독립이고 3차원 공간 전체를 표현할 수 있다면 하나의 Basis를 구성한다.</p>
<hr>
<h2 id="world-basis와-character-basis">World Basis와 Character Basis</h2>
<p>World의 기준축이 다음과 같다고 해보자.</p>
<pre><code class="language-text">World X = 동쪽
World Y = 북쪽
World Z = 위</code></pre>
<p>캐릭터가 동쪽을 보고 있다면 Character Forward는 World X와 같다.</p>
<p>하지만 캐릭터가 90도 회전해서 북쪽을 바라보게 되면</p>
<pre><code class="language-text">Character Forward = World Y</code></pre>
<p>가 된다.</p>
<p>여기서 중요한 점은 같은 숫자라도 <strong>어느 Basis를 기준으로 표현했는가</strong>에 따라 의미가 달라진다는 것이다.</p>
<p>캐릭터 Local 좌표에서</p>
<pre><code class="language-text">(1, 0, 0)</code></pre>
<p>은</p>
<pre><code class="language-text">캐릭터 기준 앞으로 1</code></pre>
<p>이라는 뜻이다.</p>
<p>캐릭터가 북쪽을 바라보고 있다면 이 Local 벡터는 World 좌표에서</p>
<pre><code class="language-text">(0, 1, 0)</code></pre>
<p>이 될 수 있다.</p>
<p>이것이 Local Space와 World Space를 구분해야 하는 이유다.</p>
<hr>
<h1 id="8-orthogonal-basis와-orthonormal-basis">8. Orthogonal Basis와 Orthonormal Basis</h1>
<p>Basis의 각 축이 서로 90도를 이루고 있다면 Orthogonal Basis라고 한다.</p>
<p>여기에 각 벡터의 길이까지 모두 1이라면 Orthonormal Basis가 된다.</p>
<p>캐릭터의</p>
<pre><code class="language-text">Forward
Right
Up</code></pre>
<p>을 Orthonormal 상태로 유지하면 여러 계산이 간단해진다.</p>
<p>예를 들어 Velocity가 있고 이 속도 중 캐릭터 Forward 방향 성분만 알고 싶다고 하자.</p>
<pre><code class="language-cpp">float ForwardSpeed =
    FVector::DotProduct(Velocity, Forward);</code></pre>
<p>Forward가 단위 벡터라면 이 값은 Velocity가 Forward 방향으로 얼마나 향하고 있는지를 나타내는 <strong>부호 있는 속도 성분</strong>이 된다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Velocity = 5Forward + 2Right</code></pre>
<p>라면</p>
<pre><code class="language-text">Velocity · Forward = 5</code></pre>
<p>이다.</p>
<p>따라서 캐릭터 기준 Forward 방향 속도 성분은 5다.</p>
<p>반대로 값이 -5라면 Forward 반대 방향, 즉 뒤쪽으로 5만큼 움직이고 있다는 뜻이다.</p>
<p>Orthonormal Basis에서는 이런 식으로 내적만 사용해서 특정 축 방향 성분을 쉽게 분리할 수 있다.</p>
<hr>
<h1 id="9-gram-schmidt">9. Gram-Schmidt</h1>
<p>계산을 반복하다 보면 Basis가 조금씩 틀어질 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Forward = (1, 0, 0)
Right   = (1, 1, 0)</code></pre>
<p>이라고 하자.</p>
<p>Right가 Forward 방향으로 기울어져 있기 때문에 두 축은 서로 수직이 아니다.</p>
<p>Right에서 Forward 방향 성분을 제거하면 된다.</p>
<pre><code class="language-text">Right&#39; = Right - projForward(Right)</code></pre>
<p>Forward가 단위 벡터라면</p>
<pre><code class="language-text">projForward(Right)
= (Right · Forward)Forward</code></pre>
<p>이다.</p>
<p>현재 값에서는</p>
<pre><code class="language-text">Right · Forward = 1</code></pre>
<p>이므로</p>
<pre><code class="language-text">Right&#39;
= (1,1,0) - (1,0,0)
= (0,1,0)</code></pre>
<p>이 된다.</p>
<p>필요하다면 마지막에 Right&#39;도 Normalize한다.</p>
<p>이런 식으로 여러 벡터를 서로 수직인 축으로 만드는 과정을 Gram-Schmidt Orthogonalization이라고 한다.</p>
<p>카메라나 좌표축을 다시 정리하거나, 특정 표면을 기준으로 새로운 Basis를 구성할 때 사용할 수 있다.</p>
<hr>
<h1 id="10-matrix">10. Matrix</h1>
<p>게임 수학에서 Matrix는 회전, 스케일, 좌표 변환 같은 <strong>선형 변환을 표현하는 구조</strong>로 볼 수 있다.</p>
<p>열벡터(Column Vector)를 사용하는 방식으로 다음 행렬을 생각해보자.</p>
<pre><code class="language-text">    [2 0]
M = [0 3]</code></pre>
<p>그리고</p>
<pre><code class="language-text">    [4]
V = [5]</code></pre>
<p>가 있다고 하자.</p>
<p>행렬과 벡터를 곱하면</p>
<pre><code class="language-text">     [2 0][4]
MV = [0 3][5]</code></pre>
<p>각 결과는 행과 열의 내적으로 계산한다.</p>
<pre><code class="language-text">2×4 + 0×5 = 8
0×4 + 3×5 = 15</code></pre>
<p>따라서</p>
<pre><code class="language-text">     [ 8]
MV = [15]</code></pre>
<p>이다.</p>
<hr>
<h1 id="11-matrix-×-vector를-basis-관점에서-보기">11. Matrix × Vector를 Basis 관점에서 보기</h1>
<p>위 행렬의 열벡터를 보면</p>
<pre><code class="language-text">E1 = (2, 0)
E2 = (0, 3)</code></pre>
<p>이다.</p>
<p>입력 벡터가</p>
<pre><code class="language-text">V = (4, 5)</code></pre>
<p>였으므로</p>
<pre><code class="language-text">MV = 4E1 + 5E2</code></pre>
<p>라고 볼 수 있다.</p>
<p>실제로 계산하면</p>
<pre><code class="language-text">4(2,0) + 5(0,3)
= (8,0) + (0,15)
= (8,15)</code></pre>
<p>이다.</p>
<p>즉 행렬 × 벡터 연산은 행렬의 열벡터들을 입력 벡터의 좌표값만큼 선형 결합하는 것으로 이해할 수 있다.</p>
<p>조금 더 정확하게 말하면, 행렬의 각 열은 <strong>표준 Basis 벡터가 이 변환을 거친 뒤 어디로 이동하는지</strong>를 나타낸다.</p>
<hr>
<h1 id="12-identity-matrix">12. Identity Matrix</h1>
<p>2차원 Identity Matrix는 다음과 같다.</p>
<pre><code class="language-text">    [1 0]
I = [0 1]</code></pre>
<p>열벡터를 보면</p>
<pre><code class="language-text">X = (1,0)
Y = (0,1)</code></pre>
<p>이다.</p>
<p>벡터</p>
<pre><code class="language-text">V = (3,2)</code></pre>
<p>를 곱하면</p>
<pre><code class="language-text">IV = 3X + 2Y</code></pre>
<p>이므로</p>
<pre><code class="language-text">IV = (3,2)</code></pre>
<p>가 된다.</p>
<p>입력 벡터가 전혀 변하지 않는다.</p>
<p>그래서 Identity Matrix는 곱해도 아무 변화가 없는 행렬이다.</p>
<hr>
<h1 id="13-rotation-matrix">13. Rotation Matrix</h1>
<p>다음 행렬을 보자.</p>
<pre><code class="language-text">    [ 0 -1]
R = [ 1  0]</code></pre>
<p>X축 벡터를 넣어보면</p>
<pre><code class="language-text">    [1]
X = [0]</code></pre>
<pre><code class="language-text">RX = (0,1)</code></pre>
<p>이 된다.</p>
<p>즉 X축이 Y축으로 이동했다.</p>
<p>이번에는 Y축을 넣으면</p>
<pre><code class="language-text">RY = (-1,0)</code></pre>
<p>이 된다.</p>
<p>따라서 이 행렬은 일반적인 2D Cartesian 좌표계에서 벡터를 반시계 방향으로 90도 회전시키는 행렬이다.</p>
<p>행렬의 열을 보면</p>
<pre><code class="language-text">첫 번째 열 = (0,1)
두 번째 열 = (-1,0)</code></pre>
<p>인데,</p>
<p>이는 원래의 X Basis와 Y Basis가 변환 후 어디를 향하는지를 그대로 보여준다.</p>
<hr>
<h1 id="14-matrix-×-matrix">14. Matrix × Matrix</h1>
<p>두 행렬이 있다고 하자.</p>
<pre><code class="language-text">    [1 2]
A = [3 4]

    [5 6]
B = [7 8]</code></pre>
<p>AB를 계산한다.</p>
<p>첫 번째 원소는 A의 첫 번째 행과 B의 첫 번째 열의 내적이다.</p>
<pre><code class="language-text">1×5 + 2×7 = 19</code></pre>
<p>두 번째 원소는</p>
<pre><code class="language-text">1×6 + 2×8 = 22</code></pre>
<p>두 번째 행도 같은 방법으로 계산하면</p>
<pre><code class="language-text">3×5 + 4×7 = 43
3×6 + 4×8 = 50</code></pre>
<p>따라서</p>
<pre><code class="language-text">     [19 22]
AB = [43 50]</code></pre>
<p>이다.</p>
<p>행렬곱이 가능하려면</p>
<pre><code class="language-text">A의 열 개수 = B의 행 개수</code></pre>
<p>여야 한다.</p>
<hr>
<h1 id="15-matrix-곱에서-순서가-중요한-이유">15. Matrix 곱에서 순서가 중요한 이유</h1>
<p>행렬곱은 일반적으로</p>
<pre><code class="language-text">AB ≠ BA</code></pre>
<p>이다.</p>
<p>따라서 변환을 합성할 때 순서가 중요하다.</p>
<p>현재처럼 <strong>열벡터 방식</strong>을 사용한다고 하면</p>
<pre><code class="language-text">ABv</code></pre>
<p>는</p>
<pre><code class="language-text">v
↓
B
↓
A</code></pre>
<p>순서로 적용된다.</p>
<p>즉,</p>
<pre><code class="language-text">ABv = A(Bv)</code></pre>
<p>이므로 <strong>오른쪽 행렬부터 적용된다.</strong></p>
<p>예를 들어</p>
<pre><code class="language-text">WorldPosition = Translation × Rotation × LocalPosition</code></pre>
<p>형태라면 LocalPosition에 Rotation이 먼저 적용되고 그 결과에 Translation이 적용되는 식이다.</p>
<p>실제 엔진에서는 행벡터/열벡터 방식이나 행렬 저장 방식이 다를 수 있기 때문에 API의 convention을 확인해야 한다.</p>
<hr>
<h1 id="16-matrix와-local-→-world">16. Matrix와 Local → World</h1>
<p>캐릭터의 Basis가</p>
<pre><code class="language-text">Forward
Right
Up</code></pre>
<p>이라고 하자.</p>
<p>열벡터 방식에서는 이 세 벡터를 행렬의 열로 배치해</p>
<pre><code class="language-text">M = [Forward Right Up]</code></pre>
<p>처럼 생각할 수 있다.</p>
<p>Local 방향</p>
<pre><code class="language-text">Vlocal = (x, y, z)</code></pre>
<p>에 M을 곱하면</p>
<pre><code class="language-text">Vworld
= M Vlocal

= xForward
+ yRight
+ zUp</code></pre>
<p>이 된다.</p>
<p>즉 Local 좌표에 적힌 숫자를 Character Basis를 기준으로 다시 조합해서 World 방향을 얻는 것이다.</p>
<p>여기서 중요한 점이 하나 있다.</p>
<p><strong>방향 벡터와 위치는 다르다.</strong></p>
<p>회전만 생각한다면 3×3 Matrix로 방향을 변환할 수 있지만, 오브젝트의 실제 위치를 Local에서 World로 변환하려면 Translation도 필요하다.</p>
<p>개념적으로는</p>
<pre><code class="language-text">Pworld = R Plocal + T</code></pre>
<p>형태가 된다.</p>
<p>게임 그래픽스에서는 Rotation, Scale, Translation을 한 번에 다루기 위해 4×4 Homogeneous Matrix를 많이 사용한다.</p>
<hr>
<h1 id="17-transpose">17. Transpose</h1>
<p>Transpose는 행렬의 행과 열을 서로 바꾸는 연산이다.</p>
<pre><code class="language-text">    [1 2 3]
A = [4 5 6]</code></pre>
<p>이라면</p>
<pre><code class="language-text">      [1 4]
A^T = [2 5]
      [3 6]</code></pre>
<p>이 된다.</p>
<p>특히 Orthonormal Rotation Matrix에서는 중요한 성질이 있다.</p>
<pre><code class="language-text">R^-1 = R^T</code></pre>
<p>즉 회전 행렬이 Orthonormal하다면 비싼 일반 역행렬 계산 대신 Transpose로 역회전을 구할 수 있다.</p>
<p>Normal 변환에서도 Transpose가 등장하지만, 일반적인 변환에서 Normal에 단순히 Transpose만 적용하는 것은 아니다.</p>
<p>특히 Non-uniform Scale이 포함되어 있다면 Normal은 보통 변환 행렬의 <strong>Inverse Transpose</strong>를 사용해 변환한다.</p>
<pre><code class="language-text">N&#39; = (M^-1)^T N</code></pre>
<hr>
<h1 id="정리">정리</h1>
<p>지금까지 배운 내용을 게임에서의 역할로 연결하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>개념</th>
<th>게임에서의 의미</th>
</tr>
</thead>
<tbody><tr>
<td>Cross Product</td>
<td>표면 Normal 생성, 좌우 방향 판정</td>
</tr>
<tr>
<td>Cross의 크기</td>
<td>평행사변형 넓이, 삼각형 넓이</td>
</tr>
<tr>
<td>Linear Combination</td>
<td>여러 방향을 조합해서 이동 방향 생성</td>
</tr>
<tr>
<td>Span</td>
<td>현재 축들로 표현 가능한 공간</td>
</tr>
<tr>
<td>Linear Independence</td>
<td>축들이 서로 중복된 정보를 가지지 않는지 확인</td>
</tr>
<tr>
<td>Basis</td>
<td>좌표계를 구성하는 기준축</td>
</tr>
<tr>
<td>Orthogonal Basis</td>
<td>기준축들이 서로 수직</td>
</tr>
<tr>
<td>Orthonormal Basis</td>
<td>서로 수직이고 길이도 1인 Basis</td>
</tr>
<tr>
<td>Gram-Schmidt</td>
<td>틀어진 Basis를 다시 수직으로 정리</td>
</tr>
<tr>
<td>Matrix</td>
<td>선형 변환을 표현</td>
</tr>
<tr>
<td>Matrix × Vector</td>
<td>벡터에 변환 적용</td>
</tr>
<tr>
<td>Matrix × Matrix</td>
<td>여러 변환을 하나의 변환으로 합성</td>
</tr>
<tr>
<td>Identity Matrix</td>
<td>아무 변화도 주지 않는 변환</td>
</tr>
<tr>
<td>Transpose</td>
<td>행과 열 교환, Orthonormal 행렬의 역행렬 계산 등에 사용</td>
</tr>
</tbody></table>
<p>처음에는 Cross, Basis, Matrix가 서로 다른 개념처럼 보였다.</p>
<p>하지만 게임에서는 결국 다음과 같이 연결된다.</p>
<pre><code class="language-text">Vector
↓
방향을 표현한다

Cross Product
↓
새로운 수직 방향을 만든다

Basis
↓
Forward / Right / Up 같은 좌표계의 기준축을 만든다

Matrix
↓
이 Basis와 변환을 이용해서 벡터와 좌표를 변환한다</code></pre>
<p>특히 Local Space와 World Space를 이해할 때 Basis와 Matrix가 연결된다.</p>
<p>캐릭터 Local에서 <code>(1,0,0)</code>이라는 값은 단순히 World X축을 의미하는 것이 아니라</p>
<pre><code class="language-text">Character Forward 방향으로 1</code></pre>
<p>이라는 뜻이다.</p>
<p>Matrix는 이 Local 좌표를 Character Basis를 이용해 선형 결합해서 실제 World 방향으로 변환한다.</p>
<p>결국 Basis는 <strong>좌표를 해석하기 위한 기준축</strong>이고, Matrix는 <strong>그 기준축과 변환을 계산으로 표현하는 방법</strong>이라고 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[게임 프로그래밍 개념 정리]]></title>
            <link>https://velog.io/@kyu_/%EA%B2%8C%EC%9E%84-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@kyu_/%EA%B2%8C%EC%9E%84-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 17 Sep 2026 16:34:12 GMT</pubDate>
            <description><![CDATA[<h1 id="게임-프로그래밍-개념-정리">게임 프로그래밍 개념 정리</h1>
<p>성능 병목 분석, 멀티스레딩, 네트워크 위치 동기화, 객체 설계, <code>std::vector</code> 삭제 전략, 메모리 할당과 캐시 지역성을 한 번에 정리한 글이다.</p>
<h2 id="cpu--gpu-병목-분석">CPU / GPU 병목 분석</h2>
<h3 id="목표">목표</h3>
<blockquote>
<p>Frame Time 확인 -&gt; Game / Draw / GPU 중 무엇이 프레임을 제한하는지 확인 -&gt; 해당 영역을 더 깊게 프로파일링 -&gt; 가설을 실험으로 검증</p>
</blockquote>
<h3 id="질문">질문</h3>
<blockquote>
<p>프레임 드랍이 발생했을 때 CPU 병목인지 GPU 병목인지 어떻게 판단할 것인가?</p>
</blockquote>
<p>처음에는 CPU/GPU 전체 사용률이나 Draw Call 수부터 보게 되기 쉽지만, 이것만으로 병목을 판정하면 오해하기 쉽다. 먼저 <code>stat unit</code> 등으로 <strong>Frame / Game / Draw / GPU 시간</strong>을 비교하고, 프레임을 제한하는 영역을 특정한 뒤 세부 프로파일링으로 내려가는 편이 안전하다.</p>
<h3 id="주의할-점">주의할 점</h3>
<p>8코어 16스레드 CPU에서</p>
<p>Game Thread의 하나만 완전히 바빠서</p>
<pre><code>Game Thread : 100%
나머지 스레드 : 대부분 놀고 있음</code></pre><p>이라고 하면 작업 관리자에서 CPU 전체 사용률은 낮게 보일 수 있다.</p>
<p>그런데 한 프레임이 Game Thread 완료를 기다린다면 게임은 명백히 CPU 병목이다.</p>
<p>즉,</p>
<pre><code>CPU 전체 사용률 30%
    ↓
CPU 여유 있음</code></pre><p>이라고 판단하면 안 된다.</p>
<hr>
<p>대부분 CPU 병목도 일반화하면 안 되는 이유는 게임과 장면에 따라 완전히 달라진다.</p>
<p>오브젝트/AI/물리가 많으면 CPU 쪽일 가능성이 있고, 높은 해상도/복잡한 셰이더/Lumen/그림자/후처리/오버드로우 등이 크면 GPU 쪽일 가능성이 있다.</p>
<h3 id="fps보다-먼저-frame-time을-본다">FPS보다 먼저 Frame Time을 본다.</h3>
<p>60FPS를 목표로 한다 했을 때</p>
<p>1초 = 1000ms 이므로 한 프레임을 16.67ms안에 끝내야 60FPS가 가능하다.</p>
<p>즉, 어떤 단계가 16.67ms를 넘기고 있지? 라고 생각하는게 프로파일링에 더 적합하다.</p>
<h3 id="cpu-time--gpu-time을-더하면-frame-time인가">CPU Time + GPU Time을 더하면 Frame Time인가?</h3>
<p>CPU와 GPU는 상당 부분 병렬로 작업한다.</p>
<pre><code>시간 ────────────────────────────────&gt;
CPU
Frame N+1 준비
████████████████          10ms
GPU
Frame N 렌더링
████████████████████████  15ms</code></pre><p>요런 느낌이라서 대체로 느린쪽이 throughput을 제한한다.</p>
<p>CPU와 GPU가 파이프라인으로 겹쳐 실행되는 정상적인 steady-state 상황에서는 <strong>프레임 처리량이 대체로 가장 느린 단계에 의해 제한된다</strong>고 이해하면 좋다. 다만 동기화, Present/VSync, RHI 작업, 프레임 큐잉과 같은 요소가 있으므로 <code>FrameTime = max(CPU, GPU)</code>를 항상 성립하는 수학적 등식처럼 사용하면 안 된다.</p>
<h4 id="예시">예시</h4>
<p>몬스터가 500마리가 있는 전투에서 언리얼의 stat unit을 실행했더니</p>
<pre><code>Frame : 24 ms
Game  : 23 ms
Draw  : 7 ms
GPU   : 10 ms</code></pre><p>라고 나와있다고 치면 게임 스레드 병목일 가능성이 크다.</p>
<p>따라서 이 상황에 <code>그림자 옵션을 낮춘다</code> 라고 하면 해결이 안 될 가능성이 크다.</p>
<p>이때는 Unreal Insights 등을 열어서 게임 스레드에서 무엇이 시간을 먹는지 내려가야 한다.</p>
<blockquote>
<p>CPU 사용률이 낮다고 해서 무조건 CPU 병목이 없다고는 할 수 없다</p>
</blockquote>
<blockquote>
<p>전체 CPU 사용률이 낮아도 Game Thread나 Render Thread 하나가 프레임 예산을 초과하면 CPU 병목이 될 수 있다.</p>
</blockquote>
<p><code>stat unit</code>의 <strong>Draw는 CPU의 Render Thread 시간</strong>을 의미한다. GPU 시간과는 별개의 값이다.</p>
<h3 id="gpu-병목-확인에-좋은-실험---해상도-낮추기">GPU 병목 확인에 좋은 실험 - 해상도 낮추기</h3>
<p>해상도를 낮춰서 GPU가 그릴 픽셀 수를 줄였을 때</p>
<pre><code>2560 × 1440
Frame : 24ms
Game  : 8ms
Draw  : 7ms
GPU   : 23ms
-&gt; 해상도 낮춘 후
Frame : 14ms
Game  : 8ms
Draw  : 7ms
GPU   : 13ms</code></pre><p>GPU Time이 현저히 줄어서 FrameTime이 줄었다면 GPU 쪽 작업이 병목임을 확인할 수 있다.</p>
<p>차이가 거의 없다면 CPU 병목을 의심할 수 있다. 다만 GPU 병목이라도 지오메트리 처리, 레이 트레이싱, 고정 비용이 큰 패스처럼 해상도 변화에 덜 민감한 원인이 있을 수 있으므로, 해상도 실험만으로 CPU 병목이라고 확정하지는 않는다.</p>
<p>더 심화로 가면 GPU프로파일러로  어떤 pass가 비싼지 확인하고 원인에 따라 Shadow, Material, Overdraw, Resolution등을 조사한다.</p>
<h2 id="멀티스레딩">멀티스레딩</h2>
<h3 id="질문-1">질문</h3>
<blockquote>
<p>입력, 물리, 렌더링 등이 서로 다른 스레드에서 돌아가며 같은 게임 상태에 접근하면 어떤 문제가 생기는가?</p>
</blockquote>
<p>여러 스레드가 같은 메모리 위치에 충돌하는 접근을 하고, 그중 하나 이상이 쓰기이며, 두 접근 사이에 적절한 <strong>happens-before 관계</strong>가 없고 원자적 접근도 아니라면 <strong>Data Race(데이터 레이스/데이터 경쟁)</strong>가 발생한다.</p>
<p>C++에서 Data Race는 <strong>Undefined Behavior(정의되지 않은 동작)</strong>이다.</p>
<p>게임에서는 이런 문제를 줄이기 위해 <strong>공유 게임 상태의 실제 변경은 Game Thread가 담당하고, Worker Thread는 무거운 계산만 수행하도록 역할을 분리하는 구조</strong>를 사용할 수 있다. 다만 이것은 한 가지 설계 방식이며, 데이터 소유권을 분리할 수 있다면 Worker가 자신이 소유한 파티션을 직접 갱신하는 구조도 가능하다.</p>
<hr>
<h3 id="기본-구조">기본 구조</h3>
<p>예를 들어 몬스터 1000마리의 AI를 모두 Game Thread에서 계산하면</p>
<pre><code class="language-text">Game Thread
Enemy 1 AI 계산
Enemy 2 AI 계산
Enemy 3 AI 계산
...
Enemy 1000 AI 계산</code></pre>
<p>모든 계산이 직렬로 실행되기 때문에 Game Thread 병목이 발생할 수 있다.</p>
<p>그래서 계산 작업을 Worker Thread에 분배할 수 있다.</p>
<pre><code class="language-text">Worker 1 → Enemy 1~250
Worker 2 → Enemy 251~500
Worker 3 → Enemy 501~750
Worker 4 → Enemy 751~1000</code></pre>
<p>하지만 Worker Thread가 직접 World State를 수정하면 여러 스레드가 같은 상태를 건드리면서 동기화가 복잡해진다.</p>
<p>한 가지 흔한 설계는 Worker에서는 가능한 한 계산만 수행하고, 공유 World State의 최종 변경은 소유권이 명확한 스레드에서 처리하는 것이다.</p>
<pre><code class="language-text">Worker Thread
Enemy 27의 AI 계산
        ↓
결과 생성
EnemyId = 27
Target = B
DesiredVelocity = ...</code></pre>
<p>계산 결과를 모아두고</p>
<pre><code class="language-text">Worker Threads
      ↓
Result Buffer
      ↓
Game Thread
      ↓
실제 게임 상태 변경</code></pre>
<p>Game Thread가 실제 상태를 적용한다.</p>
<hr>
<h3 id="result-buffer--command-queue">Result Buffer / Command Queue</h3>
<p>둘 다 <strong>Worker가 만든 내용을 Game Thread에 전달하기 위한 방법</strong>이다.</p>
<h4 id="result-buffer">Result Buffer</h4>
<p>Worker가 <strong>계산 결과 데이터</strong>를 저장한다.</p>
<pre><code class="language-cpp">struct FAIResult
{
    int EnemyId;
    int TargetId;
    FVector DesiredVelocity;
};</code></pre>
<p>예:</p>
<pre><code class="language-text">[Enemy 1의 AI 결과]
[Enemy 2의 AI 결과]
[Enemy 3의 AI 결과]
...</code></pre>
<p>Game Thread가 나중에 이 결과를 읽어서 실제 상태를 변경한다.</p>
<h4 id="command-queue">Command Queue</h4>
<p>결과 데이터 대신 <strong>실행해야 할 명령</strong>을 저장하는 방식이다.</p>
<pre><code class="language-text">Move Enemy 1
Change Target Enemy 2
Attack Enemy 3</code></pre>
<p>즉:</p>
<pre><code class="language-text">Result Buffer
= &quot;계산 결과가 이것이다.&quot;
Command Queue
= &quot;이 작업을 실행해라.&quot;</code></pre>
<hr>
<h3 id="그러면-왜-game-thread가-다시-병목이-될-수-있는가">그러면 왜 Game Thread가 다시 병목이 될 수 있는가?</h3>
<p>Worker Thread를 사용했다고 해서 Game Thread의 일이 없어지는 것은 아니다.</p>
<p>예를 들어 Worker가 병렬로 AI를 계산해서:</p>
<pre><code class="language-text">AI 계산 전체 = 4ms</code></pre>
<p>밖에 걸리지 않았다고 하자.</p>
<p>하지만 결과 100,000개를 Game Thread가 하나씩 적용한다면:</p>
<pre><code class="language-text">Game Thread
결과 1 적용
결과 2 적용
결과 3 적용
...
결과 100,000 적용</code></pre>
<p>이 작업에 20ms가 걸릴 수도 있다.</p>
<p>그러면:</p>
<pre><code class="language-text">Worker 계산       4ms
Game Thread 적용 20ms</code></pre>
<p>결국 Game Thread의 직렬 구간이 새로운 병목이 된다.</p>
<blockquote>
<p>병렬 계산을 빠르게 만들더라도 최종 상태 변경을 하나의 스레드에서 대량으로 처리하면 그 Commit 단계가 병목이 될 수 있다.</p>
</blockquote>
<hr>
<h3 id="batch-commit">Batch Commit</h3>
<p>이때 고려할 수 있는 것이 <strong>Batch Commit</strong>이다.</p>
<p>결과를 받을 때마다 하나씩 바로 반영하는 대신:</p>
<pre><code class="language-text">Worker 결과
Worker 결과
Worker 결과
Worker 결과
      ↓
결과들을 모음
      ↓
Game Thread에서 묶어서 처리</code></pre>
<p>하는 방식이다.</p>
<p>예를 들어 여러 변경을 개별 함수 호출, 이벤트 전달, 동기화와 함께 처리하는 대신 시스템이 지원한다면 여러 변경을 모아서 한 번에 처리하여 <strong>반복되는 부가 비용을 줄일 수 있다.</strong></p>
<p>중요한 점은</p>
<blockquote>
<p>Batch Commit이 100,000개의 실제 작업 자체를 없애주는 것은 아니다.</p>
</blockquote>
<p>실제로 반드시 해야 하는 상태 변경 자체가 20ms 걸린다면 Batch 처리만으로 해결되지 않을 수도 있다.</p>
<p>그 경우에는 Game Thread에서 수행해야 하는 작업 자체를 줄이거나 다른 구조를 고려해야 한다.</p>
<hr>
<h3 id="흐름">흐름</h3>
<p>전체 흐름은 이것만 먼저 기억하면 된다.</p>
<pre><code class="language-text">Game State
    ↓ 읽기
Worker Threads
    ↓
병렬 계산
    ↓
Result Buffer
    ↓
Game Thread
    ↓
Batch Commit
    ↓
실제 상태 변경</code></pre>
<hr>
<h3 id="추가적인-개념">추가적인 개념</h3>
<h4 id="snapshot">Snapshot</h4>
<blockquote>
<p>Worker가 AI를 계산하는 동안 Game Thread가 Worker가 읽는 데이터를 변경하면 어떻게 하는가?</p>
</blockquote>
<p>예를 들어</p>
<pre><code class="language-text">Worker
Player Position 읽음
동시에
Game Thread
Player Position 변경</code></pre>
<p>이런 공유 접근을 피하기 위해 Worker에게 <strong>계산 시점의 읽기 전용 상태 복사본</strong>을 제공할 수 있다.</p>
<pre><code class="language-text">Game State
    ↓
Snapshot
    ↓
Workers가 읽기만 함</code></pre>
<blockquote>
<p>Snapshot은 <strong>Worker에게 계산 동안 변경되지 않는 입력 뷰를 제공하기 위한 방법</strong>이다.</p>
</blockquote>
<p>복사본의 수명과 불변성이 보장되어야 하며, Snapshot 생성 비용과 한 프레임 정도 오래된 데이터를 사용할 수 있다는 점도 고려해야 한다.</p>
<p>Batch Commit과 목적이 다르다.</p>
<pre><code class="language-text">Snapshot
= 계산 전에 사용할 입력 데이터 관리
Batch Commit
= 계산이 끝난 후 결과 적용 관리</code></pre>
<hr>
<h4 id="double-buffering">Double Buffering</h4>
<p>Snapshot을 운용하는 방법 중 하나다.</p>
<p>두 개의 버퍼를 두고</p>
<pre><code class="language-text">Buffer A → 현재 쓰기
Buffer B → Worker가 읽기</code></pre>
<p>다음 시점에 역할을 바꾼다.</p>
<pre><code class="language-text">A ↔ B</code></pre>
<p>그러면 읽는 데이터와 쓰는 데이터를 분리할 수 있다. 단, 버퍼 역할을 교체할 때는 이전 Reader가 모두 사용을 끝냈는지 확인하는 동기화가 필요하다.</p>
<blockquote>
<p><strong>Double Buffering = 읽기용/쓰기용 데이터를 두 벌 두고 안전한 시점에 역할을 교체하는 방식</strong></p>
</blockquote>
<hr>
<h4 id="synchronization-point--barrier">Synchronization Point / Barrier</h4>
<blockquote>
<p>Game Thread는 Worker 계산이 끝났다는 것을 어떻게 알고 결과를 적용하는가?</p>
</blockquote>
<p>예를 들어</p>
<pre><code class="language-text">Worker 1 ── 완료
Worker 2 ───── 완료
Worker 3 ───────── 완료
                  ↓
             모두 완료 확인
                  ↓
              Commit</code></pre>
<p>Game Thread가 Worker들의 작업 완료를 기다리는 지점이 <strong>Synchronization Point</strong>다.</p>
<p>Barrier는 여러 작업이 특정 지점까지 모두 도착해야 다음 단계로 넘어가도록 만드는 동기화 방식이다.</p>
<p>단점도 있다.</p>
<pre><code class="language-text">Worker 1 = 2ms
Worker 2 = 2ms
Worker 3 = 8ms</code></pre>
<p>이면 앞의 Worker들도 결국 가장 느린 Worker 3이 끝날 때까지 기다려야 한다.</p>
<hr>
<h4 id="state-partitioning">State Partitioning</h4>
<blockquote>
<p>Game Thread에서 결과를 적용하는 구조 자체가 계속 병목이면 어떻게 할 것인가?</p>
</blockquote>
<p>한 가지 더 근본적인 방법은 <strong>공유 상태 자체를 줄이는 것</strong>이다.</p>
<p>예</p>
<pre><code class="language-text">Worker A → Enemy 1~100 상태 담당
Worker B → Enemy 101~200 상태 담당</code></pre>
<p>서로 같은 상태를 수정하지 않도록 데이터 소유권을 나누는 것이다.</p>
<p>엔진 구조가 허용한다면 각 Worker가 자신이 소유한 파티션을 직접 갱신하게 하여 하나의 중앙 Commit 단계에 몰리는 작업을 줄일 수 있다. 다만 Unreal Engine의 <code>UObject</code>/<code>Actor</code> 관련 작업처럼 Game Thread 제약이 있는 API는 단순히 파티션만 나눈다고 Worker에서 안전하게 수정할 수 있는 것은 아니다.</p>
<blockquote>
<p>State Partitioning은 <strong>Game Thread 병목이나 Lock 경쟁을 줄이기 위해 상태의 소유권 자체를 나누는 구조적 해결책</strong>이다.</p>
</blockquote>
<h2 id="네트워크-위치-동기화">네트워크 위치 동기화</h2>
<p>Server Authority / Client Prediction / Reconciliation</p>
<p>왜 이런 구조가 필요한지 -&gt; 각 단계가 무슨 역할인지 -&gt; 서로 어떻게 연결되는지</p>
<h3 id="질문-2">질문</h3>
<p>서버와 클라이언트의 플레이어 위치가 자주 어긋난다면 어떻게 처리할 것인가?</p>
<p>-&gt; 클라이언트는 자신의 입력을 로컬에서 즉시 시뮬레이션하고, 서버도 같은 이동 정보를 권위 있게 시뮬레이션한다. 서버 결과와 차이가 생기면 클라이언트는 서버 상태를 기준으로 보정하고 아직 확인되지 않은 이동을 다시 적용한다.</p>
<p>클라이언트 예측을 사용하면서도 서버 권한을 유지하고 플레이 감각을 해치지 않으려면?</p>
<p>-&gt; 서버는 이동 입력과 결과를 검증해 권위를 유지하고, 클라이언트는 Prediction과 Reconciliation을 사용한다. 화면에서는 Smoothing을 적용해 작은 보정이 순간이동처럼 보이지 않도록 한다.</p>
<h4 id="중요점">중요점</h4>
<ol>
<li><p>클라이언트가 서버에 무엇을 보내는지</p>
</li>
<li><p>서버가 무엇을 돌려주는지</p>
</li>
<li><p>클라이언트가 과거 입력을 어떻게 다시 적용하는지</p>
</li>
</ol>
<h3 id="개념">개념</h3>
<h4 id="왜-서버-위치만-따라가면-안-되나">왜 서버 위치만 따라가면 안 되나?</h4>
<p>네트워크 지연때문이다.</p>
<p>클라이언트 입력 → 서버에서 이동 계산 → 권위 상태가 다시 클라이언트에 도착 → 화면 반영</p>
<p>이 구조만 사용하면 입력 반응이 네트워크 왕복 지연과 서버/전송 주기의 영향을 받는다. 예를 들어 RTT가 약 100ms라면 체감 입력 지연이 매우 커질 수 있다.</p>
<p>그래서 자기 캐릭터는 서버 응답을 기다리지 않고 즉시 움직인다. 이게 Client-Side Prediction 클라이언트 예측이다.</p>
<h4 id="client-side-prediction">Client-side Prediction</h4>
<p>클라이언트에서</p>
<pre><code>W 입력
↓
&quot;앞으로 이동할 거라고 예상&quot;
↓
화면에서 즉시 이동</code></pre><p>동시에 서버에도 이동에 필요한 데이터를 보낸다. 개념적으로는 서버가 클라이언트의 최종 위치를 그대로 신뢰하는 것이 아니라 <strong>입력/가속도/타임스탬프 등의 이동 정보로 서버에서 다시 시뮬레이션하고 검증하는 구조</strong>라고 이해하는 것이 중요하다. Unreal의 <code>CharacterMovementComponent</code>는 이동 정보와 함께 클라이언트의 결과 위치도 전송해 서버 계산 결과와 비교한다.</p>
<h4 id="server-authority">Server Authority</h4>
<p>클라이언트가 <code>제 위치는 X=5000입니다</code> 라고 보냈다고 해서 서버가 그냥 믿으면 치트를 막을 수가 없다.</p>
<p>일반적으로 서버가 최종 정답을 가진다.</p>
<p>이게 Server Authority의 핵심</p>
<h4 id="클라이언트와-서버의-차이가-생기는-이유">클라이언트와 서버의 차이가 생기는 이유</h4>
<p>클라이언트는 플레이어 위치를 100이라고 예상했는데 서버에서는 95라고 계산을 했다 왜그럴까?</p>
<p>패킷 지연/손실, 충돌 판정 차이, 서버에서만 알고있는 상태, 이동 속도 변경, 다른 플레이어와 충돌, 서버 검증 결과 등이 있다.</p>
<p>서버상태가 오면 클라이언트는 맞춰야 한다 이를 <code>Reconciliation</code></p>
<p>이때 Rubber Banding 같은 튀는 현상이 발생한다.</p>
<h4 id="input-sequence-number">Input Sequence Number</h4>
<p>여기서 Sequence Number가 나온다.</p>
<p>클라이언트는 입력마다 번호를 붙인다.</p>
<pre><code>101 : 앞으로
102 : 앞으로
103 : 오른쪽
104 : 앞으로</code></pre><p>그리고 서버에 전송</p>
<p>서버는 타임스탬프나 시퀀스 정보로 이동의 순서와 유효성을 판단하고 처리한다. 패킷은 지연·중복·유실될 수 있으므로 단순히 &quot;도착한 순서&quot;를 신뢰한다는 뜻은 아니다.</p>
<pre><code>101 처리
102 처리
103 처리
...</code></pre><p>그리고 응답할때</p>
<pre><code>현재 위치 = 95
현재 Input #102까지 처리했다.</code></pre><p>라고 알려준다. 이를 Acknowledgement, 줄여서 ACK라고 한다.</p>
<h4 id="ack의-필요성">ACK의 필요성</h4>
<p>클라이언트에서는 104까지 전부 예측해서 화면에서 반영했는데 서버에서 ACK가 102로 왔다. 그러면 101, 102는 끝난 입력이라 버려도 되지만 103, 104는 아직 서버가 처리하지 않은 입력이다 이를 Unacknowledged Input이라고 한다.</p>
<h4 id="reconciliation의-핵심">Reconciliation의 핵심</h4>
<p>클라이언트는 ACK을 받으면</p>
<pre><code>1. 위치를 서버 위치 95로 되돌림
2. ACK된 입력 제거 (101, 102 제거)
3. 아직 ACK 안 된 입력 재적용 (103, 104)
즉,
Client Prediction
        ↓
Server Position으로 rewind
        ↓
미확인 입력 replay
        ↓
최신 Client Position 구성</code></pre><p>즉 서버 결과를 받아도 그 순간 이후에 클라이언트가 입력한 것 까지 버리지는 않는다.</p>
<h4 id="smoothing">Smoothing</h4>
<p>현재 위치 100 -&gt; 서버 보정 결과 95로 한번에 이동하면 순간이동처럼 보인다.,</p>
<p>그래서 작은 차이는 보통 여러 프레임에 걸쳐 천천히 보정한다.</p>
<p>이를 Smoothing이라고 한다.</p>
<blockquote>
<p>Reconciliation은 논리적인 상태를 서버와 맞추는 것</p>
</blockquote>
<blockquote>
<p>Smoothing은 그 보정을 화면에서 덜 거슬리게 보여주는 것</p>
</blockquote>
<h4 id="interpolation">Interpolation</h4>
<p>다른 플레이어를 보여줄 때 중요하다.</p>
<p>내 캐릭터는 입력을 내가 알고있으니까 Prediction이 가능하다.</p>
<p>하지만 다른 플레이어가 다음에 어떤 입력을 할지는 모른다.</p>
<p>그래서 보통 서버에서 위치 Snapshot이 주기적으로 오면</p>
<pre><code>t1 : 위치 10
t2 : 위치 20
이렇게 오면 렌더링 시점을 약간 과거로 지연시켜 두 Snapshot 사이를
10 → 12 → 14 → 16 → 18 → 20
처럼 보간한다.
이런 식으로 중간을 보간한다.</code></pre><p>이게 Interpolation이다. 실제 네트워크 게임에서는 다음 Snapshot을 미리 알 수 있어야 하므로, 보통 렌더링 시점을 약간 과거로 늦춘 interpolation buffer를 둔다.</p>
<h4 id="내-캐릭터-vs-다른-캐릭터">내 캐릭터 vs 다른 캐릭터</h4>
<p>내 캐릭터</p>
<pre><code>Prediction
+
Reconciliation
+
Smoothing</code></pre><p>다른 플레이어</p>
<pre><code>Server Snapshot
+
Interpolation</code></pre><p>필요에 따라 Extrapolation을 쓸 수도 있다.</p>
<h4 id="extrapolation">Extrapolation</h4>
<p>예를들어 서버 패킷이 잠깐 늦었다.</p>
<p>마지막으로 알고 있는 정보가</p>
<pre><code>위치 = 100
속도 = +10</code></pre><p>이라면</p>
<blockquote>
<p>아마 계속 같은 방향으로 가고 있겠지</p>
</blockquote>
<p>라고 잠깐 미래 위치를 예측할 수 있다.</p>
<p>이게 Extrapolation이다.</p>
<p>잘못 예측하면 나중에 보정이 더 커질 수 있다.</p>
<h4 id="rubber-banding">Rubber Banding</h4>
<p>서버와 클라이언트 차이가 크면</p>
<pre><code>클라이언트
앞으로 감
↓ 서버 보정
갑자기 뒤로 끌려감</code></pre><p>처럼 보일 수 있다.</p>
<p>이걸 흔히 Rubber Banding이라고 한다.</p>
<p>주 원인은</p>
<ul>
<li><p>높은 Ping</p>
</li>
<li><p>Packet Loss</p>
</li>
<li><p>큰 Prediction Error</p>
</li>
<li><p>서버에서 이동 거부</p>
</li>
<li><p>서버 부하</p>
</li>
</ul>
<h4 id="서버-이동-검증">서버 이동 검증</h4>
<p>서버 권한을 유지하려면 클라이언트 입력을 무조건 믿으면 안 된다.</p>
<p>예를들어</p>
<p>예를 들어 서버가 재시뮬레이션한 결과와 클라이언트가 보고한 결과가 크게 다르거나, 현재 이동 모드에서 허용될 수 없는 변화가 발생했다면 서버가 검증하고 보정할 수 있다.</p>
<pre><code>이동 속도가 허용 범위인가?
충돌을 뚫었는가?
현재 상태에서 이동 가능한가?
텔레포트 권한이 있었는가?</code></pre><p>이상하면 서버가 거부하고 서버 위치로 보정한다.</p>
<h2 id="상속-조합-전략-패턴">상속, 조합, 전략 패턴</h2>
<p>Inheritance, Composition, Strategy Pattern</p>
<h3 id="질문-3">질문</h3>
<p>여러 캐릭터가 서로 다른 공격 방식을 가지고 있고 공격 방식이 계속 추가된다면 어떻게 설계할 것인가?</p>
<p>-&gt; 공통 인터페이스를 만들고 각 캐릭터가 공격 방식을 구현하도록 한다.</p>
<p>무기 교체나 버프에 따라 런타임 중 공격 방식이 변경되어야 한다.</p>
<p>-&gt; 분기를 사용하거나, 공격 방식 자체를 별도의 상위 클래스/인터페이스로 만들어 캐릭터에게 붙인다.</p>
<p>답변하다보니 두번째쪽 방향이 더 맞는거 같았다.</p>
<p>캐릭터 자체의 상속을 계속 늘리지 말고, 공격 방식만 별도 객체로 분리해서 캐릭터가 그 객체를 사용하게 한다.</p>
<p>이게 Composition(조합)쪽 사고고, 그 중 대표적인 형태가 전략패턴(Strategy Pattern)이다.</p>
<h3 id="개념-1">개념</h3>
<h4 id="inheritance">Inheritance</h4>
<blockquote>
<p>A는 B의 한 종류다</p>
</blockquote>
<p>is-a관계라고 한다.</p>
<pre><code class="language-c++">class Character
{
public:
    virtual ~Character() = default;
    virtual void Attack() = 0;
};
class Warrior : public Character
{
public:
    void Attack() override
    {
        // 검 공격
    }
};</code></pre>
<p><strong>장점</strong></p>
<ul>
<li><p>공통 인터페이스를 강제할 수 있음</p>
</li>
<li><p>다형성(Polymorphism, 다형성) 사용 가능</p>
</li>
<li><p>구조가 단순할 때 이해하기 쉬움</p>
</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li><p>행동 변화가 타입과 강하게 결합됨</p>
</li>
<li><p>런타임 교체가 불편함</p>
</li>
<li><p>조합 경우의 수가 많아지면 클래스가 급증함</p>
</li>
</ul>
<h4 id="composition">Composition</h4>
<p>조합은</p>
<blockquote>
<p>객체가 다른 객체와의 관계를 이용해 기능을 구성하는 방식</p>
</blockquote>
<p>실무에서는 넓은 의미로 <code>has-a</code> 관계를 조합이라고 표현하는 경우가 많다. 다만 UML의 엄밀한 용어에서는 <strong>Composition은 강한 소유권과 수명 결합</strong>을 의미하고, 단순 참조는 Association/Aggregation에 더 가깝다.</p>
<pre><code class="language-c++">class Character
{
private:
    std::unique_ptr&lt;IAttackBehavior&gt; AttackBehavior;
};</code></pre>
<p>캐릭터가 공격 기능을 직접 구현하지 않고, 공격 동작을 별도 객체에게 맡긴다.</p>
<pre><code>Character
   ↓
AttackBehavior
   ↓
SwordAttack
BowAttack
MagicAttack</code></pre><h4 id="interface는-composition인가">Interface는 Composition인가?</h4>
<p>Interface자체는 조합이 아니다.</p>
<p>인터페이스는</p>
<blockquote>
<p>어떤 기능을 제공해야 하는지 약속하는 추상 계약</p>
</blockquote>
<pre><code class="language-c++">class IAttackBehavior
{
public:
    virtual ~IAttackBehavior() = default;
    virtual void Attack() = 0;
};</code></pre>
<p>이 기능을 쓰려면 Attack()을 구현해야 한다.</p>
<p><code>Character</code>가 인터페이스 구현 객체를 <strong>소유</strong>하고 그 수명을 함께 관리한다면 엄밀한 의미의 Composition에 가깝다. 단순 포인터/레퍼런스로 외부 객체를 참조하기만 한다면 넓은 의미의 조합 설계라고 부를 수는 있지만 UML의 Composition과는 구분하는 편이 정확하다.</p>
<pre><code class="language-c++">class Character
{
private:
    std::unique_ptr&lt;IAttackBehavior&gt; AttackBehavior;
};</code></pre>
<pre><code>Interface
= 기능의 계약
Composition
= 객체가 다른 객체를 포함/참조해서 기능을 구성
Strategy Pattern
= 교체 가능한 행동을 인터페이스 뒤에 분리한 조합 설계</code></pre><h4 id="strategy-pattern">Strategy Pattern</h4>
<p>Strategy Pattern은</p>
<blockquote>
<p>알고리즘이나 행동의 집합을 각각 캡슐화하고, 사용하는 쪽에서 구체 구현에 덜 의존하도록 만드는 패턴</p>
</blockquote>
<p>런타임 교체가 Strategy Pattern의 대표적인 장점 중 하나다.</p>
<p>공격 시스템에 적용하면</p>
<pre><code class="language-c++">class IAttackBehavior
{
public:
    virtual ~IAttackBehavior() = default;
    virtual void Attack() = 0;
};</code></pre>
<p>공격 방식별 구현</p>
<pre><code class="language-c++">class SwordAttack : public IAttackBehavior
{
public:
    void Attack() override
    {
        // 근접 공격
    }
};
class BowAttack : public IAttackBehavior
{
public:
    void Attack() override
    {
        // 화살 발사
    }
};</code></pre>
<p>캐릭터는</p>
<pre><code class="language-c++">class Character
{
private:
    std::unique_ptr&lt;IAttackBehavior&gt; AttackBehavior;
public:
    void Attack()
    {
        if (AttackBehavior)
        {
            AttackBehavior-&gt;Attack();
        }
    }
    void SetAttackBehavior(std::unique_ptr&lt;IAttackBehavior&gt; NewBehavior)
    {
        AttackBehavior = std::move(NewBehavior);
    }
};</code></pre>
<h4 id="왜-이-상황에서-상속보다-나은가">왜 이 상황에서 상속보다 나은가?</h4>
<p>상속 기반이면</p>
<pre><code>SwordCharacter
BowCharacter
MagicCharacter</code></pre><p>처럼 캐릭터 타입 자체가 공격 방식과 묶인다.</p>
<p>조합기반이면 AttackBehavior만 교체하면 된다.</p>
<h4 id="virtual-dispatch">Virtual Dispatch</h4>
<pre><code class="language-c++">Character.SetAttackBehavior(
    std::make_unique&lt;BowAttack&gt;()
);
AttackBehavior-&gt;Attack()</code></pre>
<p>가상 함수 디스패치는 <code>AttackBehavior-&gt;Attack()</code>을 호출할 때</p>
<pre><code class="language-c++">BowAttack Bow;
IAttackBehavior* Behavior = &amp;Bow;
Behavior-&gt;Attack();</code></pre>
<p>실제 실행되는 것은 <code>BowAttack::Attack()</code>이다.</p>
<p>이게 Virtual Dispatch(가상 함수 디스패치 / 동적 디스패치)다. 개념적으로 런타임의 동적 타입에 맞는 override 함수가 선택된다. 흔히 vtable/vptr로 구현되지만 C++ 표준이 특정 구현 방식을 강제하는 것은 아니다.</p>
<h4 id="ownership--lifetime">Ownership / Lifetime</h4>
<p>Character가 AttackBehavior를 가지고 있다면</p>
<blockquote>
<p>누가 이 객체를 소유하고 언제 파괴할 것인가?</p>
</blockquote>
<p>Character만 그 공격 방식을 소유한다면</p>
<pre><code>std::unique_ptr&lt;IAttackBehavior&gt;</code></pre><p>가 자연스럽다</p>
<h4 id="composition의-단점">Composition의 단점</h4>
<p>행동을 전부 분리하면 클래스가 너무 많이 생길 수 있다.</p>
<pre><code>SwordAttack
BowAttack
FireAttack
IceAttack
ChargeAttack
SpreadAttack
PoisonAttack
...</code></pre><p>또 너무 잘게 쪼개면 오히려 구조 파악이 어려워진다는 단점이 있다.</p>
<blockquote>
<p>변경 가능성이 실제로 있는 행동만 분리하는 게 중요하다</p>
</blockquote>
<h4 id="data-driven-design과-차이">Data Driven Design과 차이</h4>
<p>Strategy는 <strong>교체 가능한 행동/알고리즘의 구현이 다를 때</strong> 유용하다.</p>
<pre><code>SwordAttack
→ 근접 충돌 검사
BowAttack
→ Projectile 생성
MagicAttack
→ 범위 탐색 후 데미지</code></pre><pre><code>SwordDamage = 50
BowDamage = 30
AttackSpeed = 1.2
Range = 500</code></pre><p>로직은 같고 데미지, 속도, 사거리, 사용할 에셋, 태그 같은 설정 데이터만 다르다면 굳이 클래스를 늘릴 필요가 없다. 이런 값들을 코드 밖의 데이터로 분리하면 Data-Driven Design으로 관리하기 좋다.</p>
<pre><code>로직이 다름
-&gt; Strategy / 다른 클래스
설정 데이터만 다름
-&gt; Data-Driven</code></pre><h3 id="정리">정리</h3>
<pre><code>Inheritance(상속)
= 올바른 subtype 관계를 표현할 때 사용
= is-a
Composition(조합)
= 기능 객체를 붙임
= has-a
Interface(인터페이스)
= 행동 계약
Strategy Pattern(전략 패턴)
= 교체 가능한 행동을 별도 객체로 분리
Data-Driven(데이터 주도)
= 로직은 같고 설정 데이터만 다를 때</code></pre><h2 id="vector">vector</h2>
<h3 id="먼저-바로잡을-점">먼저 바로잡을 점</h3>
<p>비활성 객체가 많이 남아 있다고 해서 그 자체를 <strong>메모리 내부 단편화</strong>라고 부르지는 않는다. 슬롯의 Alive/Dead 상태는 논리적인 사용 상태이고, allocator의 내부 단편화는 할당된 블록 내부에 실제로 쓰이지 않는 공간이 생기는 현상이다.</p>
<h3 id="게임에서-어떻게-삭제하는가">게임에서 어떻게 삭제하는가?</h3>
<p>벡터는 erase하면 원소를 앞으로 당겨오는 비용 O(N)이 발생한다.</p>
<p>그렇기 때문에 삭제 순서가 중요하지 않다면 가장 대표적인 방법은</p>
<p>Swap-and-Pop(스왑 후 제거)이다.</p>
<pre><code class="language-c++">Enemies[Index] = std::move(Enemies.back());
Enemies.pop_back();</code></pre>
<p>단점은</p>
<pre><code>A B C D E
Swap and Pop후
A B E D</code></pre><p>마지막 원소를 삭제되는 자리에 이동 대입한 뒤 마지막 원소를 제거한다. 따라서 원소 순서가 중요하지 않을 때 유용하다.</p>
<p>주의할 점도 있다.</p>
<ul>
<li>삭제된 위치를 가리키던 iterator/reference는 더 이상 유효한 대상으로 취급하면 안 된다.</li>
<li>마지막 원소가 <code>Index</code> 위치로 이동했으므로, 외부에서 &quot;원소 → Index&quot; 매핑을 들고 있다면 그 원소의 Index를 갱신해야 한다.</li>
<li><code>Index == Enemies.size() - 1</code>이라면 굳이 self-move를 하지 않고 <code>pop_back()</code>만 하는 분기를 둘 수 있다.</li>
</ul>
<h3 id="tombstone-방식">Tombstone 방식</h3>
<blockquote>
<p>죽은 몬스터를 지우지 않고 비활성화 -&gt; Tombstone(툼스톤 / 삭제 표시) 방식</p>
</blockquote>
<p>삭제하지 않고 비활성화 상태로 남겨둔다. 객체가 큰 외부 자원을 소유하고 있다면 단순히 <code>bAlive = false</code>만 해서는 그 자원도 계속 유지될 수 있으므로, 필요한 경우 비활성화 시 자원을 별도로 정리해야 한다.</p>
<p><strong>장점</strong></p>
<p>삭제가 매우 싸다.</p>
<pre><code>erase
→ 뒤 원소 이동 필요
tombstone
→ bool 하나 변경</code></pre><h4 id="dead가-계속-쌓이면-실제-문제는-뭘까">Dead가 계속 쌓이면 실제 문제는 뭘까?</h4>
<ol>
<li><p>쓸데 없는 메모리 사용</p>
<p>1000개 중 100개만 살아 있어도 900개의 죽은 Enemy공간을 유지해야 한다</p>
</li>
<li><p>Working Set(작업 집합) 증가</p>
<p>운영체제 용어의 Working Set은 보통 <strong>현재 물리 메모리에 상주하는 프로세스의 페이지 집합</strong>을 뜻한다. 여기서 말하고 싶은 성능 문제는 엄밀히는 &quot;순회해야 하는 데이터 footprint와 cache working set이 커진다&quot;는 쪽에 가깝다.</p>
<p>100개의 Enemy만 실제 처리 대상인데 <code>vector</code>의 10,000개 슬롯을 매 프레임 검사한다면 CPU는 훨씬 큰 데이터 범위를 순회하게 된다.</p>
</li>
<li><p>Cache Locality(캐시 지역성)약화</p>
<pre><code>[Alive][Dead][Dead][Dead][Alive][Dead][Dead][Alive]</code></pre><p>CPU가 Cache Line하나를 가져왔는데 실제 필요한 데이터가 하나밖에 없을 수 있다.</p>
<p>즉, 캐시 안에 필요한 데이터1개, Dead 데이터 여러개가 들어간다</p>
<p>결국 메모리가 연속이라는 vector의 장점이 약해진다.</p>
</li>
<li><p>매 프레임 검사 비용</p>
<pre><code class="language-c++">if (!E.bAlive)
    continue;</code></pre>
<p>자체는 싸지만 객체가 수십만개 라면 쓸데없는 순회 자체가 비용이다.</p>
</li>
</ol>
<h3 id="free-list">Free List</h3>
<p>Tombstone의 단점을 보완하는 방법</p>
<p>죽은 자리의 인덱스를 따로 저장해두고</p>
<pre><code>Enemies
0 Alive
1 Dead
2 Alive
3 Dead
4 Alive
FreeList = [1, 3]</code></pre><p>다음 몬스터가 생성되면 <code>push_back()</code> 대신 빈 슬롯을 재사용할 수 있다. 다만 이 방식은 <strong>슬롯 Index의 안정성</strong>을 위한 것이지 <code>std::vector</code> 요소의 <strong>메모리 주소 안정성</strong>을 자동으로 보장하는 것은 아니다. <code>vector</code>가 capacity를 넘어서 재할당되면 모든 요소의 주소가 바뀔 수 있다.</p>
<pre><code>새 Enemy 생성
↓
FreeList에서 3 꺼냄
↓
Enemies[3]에 재사용</code></pre><h4 id="tombstone과-free-list를-함께-쓸-때">Tombstone과 Free List를 함께 쓸 때</h4>
<pre><code>Tombstone
→ 이 슬롯 살아있음?
→ lookup / iteration / validity check
Free List
→ 비어 있는 슬롯 어디 있음?
→ allocation / reuse
Generation Counter
→ 같은 Index가 다른 객체로 재사용되었는지 구분
→ stale handle 방지</code></pre><h3 id="periodic-compaction주기적-압축">Periodic Compaction(주기적 압축)</h3>
<p>Free List로 빈 슬롯을 재사용해도 Alive/Dead가 섞여 순회 효율이 떨어질 수 있다.</p>
<p>그러면 특정 시점에 살아있는 객체만 앞으로 모은다.</p>
<pre><code>[A][X][B][X][X][C][X][D]
압축 후
[A][B][C][D]</code></pre><p>이게 Compaction(압축/밀집화)이다. 다만 압축하면 요소의 Index와 주소가 바뀔 수 있으므로 외부 핸들/참조를 갱신할 수 있는 구조에서만 사용해야 한다. 또한 <code>vector</code>의 논리적 크기를 줄여도 capacity가 자동으로 줄어드는 것은 아니다.</p>
<h3 id="어떤-방식을-언제-쓸까">어떤 방식을 언제 쓸까?</h3>
<p><strong>순서가 중요하지 않음 + 삭제 빈번</strong></p>
<p>Swap-and-Pop</p>
<p><strong>Index 안정성이 중요하고 슬롯 재사용이 필요함</strong></p>
<p>Tombstone + Free List + Generation Handle</p>
<p><strong>실제 메모리 주소 안정성이 중요함</strong></p>
<p><code>std::vector</code>는 재할당 시 모든 요소의 주소가 바뀔 수 있으므로 Tombstone만으로는 부족하다. 최대 크기를 확실히 알고 <code>reserve()</code>로 재할당을 막거나, 주소 안정성을 제공하는 별도 저장소/청크 기반 풀을 고려해야 한다.</p>
<p><strong>Tombstone이 너무 많이 쌓임</strong></p>
<p>Periodic Compaction 으로 다시 밀집</p>
<h3 id="정리-1">정리</h3>
<pre><code>vector
= 순차 접근 좋음
= Cache Locality 좋음
erase
= 뒤 원소 이동
= O(n)
Swap-and-Pop
= vector 길이 N에 대해 O(1) 삭제 (단, 원소 타입의 이동 대입 비용은 별도)
= 순서 변경됨
Tombstone
= 삭제 대신 Dead 표시
= 삭제 빠름
= Dead가 쌓이면 순회/캐시 낭비
Free List
= Dead 슬롯 재사용
Compaction
= Dead가 많이 쌓였을 때 살아있는 것만 다시 밀집
Dead가 많은 문제
≠ 메모리 단편화
실제 문제
= 메모리 footprint 증가 + CPU cache working set 증가 + Cache Locality 저하 + 불필요한 순회</code></pre><h2 id="object-pool--allocator">Object Pool / Allocator</h2>
<h3 id="allocator">Allocator</h3>
<p>Allocator(할당자)는 <strong>메모리 저장소에서 공간을 요청하고 반환하며, 어떤 블록을 어떻게 배정할지 결정하는 정책/구조</strong>다. 반드시 운영체제의 일반 힙만 관리하는 것은 아니며, 별도의 Arena나 Pool 같은 메모리 영역을 대상으로 할 수도 있다.</p>
<p>예를들어 힙이 이렇게 있다고 했을 때</p>
<pre><code>[사용][빈 공간][사용][빈 공간][사용]</code></pre><p>새로운 32바이트 할당 요청이 들어오면 Allocator가 현재 관리 중인 빈 공간 중에 이 요청을 처리할 수 있는 블록을 찾아서 넘겨준다.</p>
<p><code>중요한 건 힙 전체를 매번 처음부터 끝까지 선형 탐색하는건 아니다.</code></p>
<p>일반적으로 Allocator는 빈 공간을 빠르게 찾기 위한 별도의 자료구조를 쓴다</p>
<p>대표적으로 <code>Free List</code>와 <code>Size Class</code>다</p>
<h3 id="freelist">FreeList</h3>
<p>프리리스트란 빈 블록 목록이다. vector에 있던것과 아이디어가 같다</p>
<blockquote>
<p>빈곳을 매번 전체 탐색하지 않고, 어디가 비었는지 따로 관리한다</p>
</blockquote>
<h3 id="size-class">Size Class</h3>
<p>크기 클래스</p>
<p>만약 메모리 요청이 제각각 들어올때</p>
<pre><code>13 bytes
21 bytes
30 bytes
57 bytes
...</code></pre><p>이걸 정확히 크기마다 따로 관리하면 복잡하다. 그래서 Allocator가 비슷한 크기를 묶는다</p>
<p>예시</p>
<pre><code>16B Class
32B Class
64B Class
128B Class</code></pre><p>21바이트가 필요하다? -&gt; 32사이즈 클래스에서 할당</p>
<p><strong>왜 이렇게 하는가?</strong></p>
<p>비슷한 크기의 블록을 묶으면</p>
<ul>
<li><p>빈 블록을 찾기 쉬움</p>
</li>
<li><p>할당/반환 관리가 단순해짐</p>
</li>
<li><p>같은 크기의 블록을 재사용하기 쉬움</p>
</li>
</ul>
<p>대신 21바이트가 필요한데 32바이트를 받으면 11바이트가 남는다 이를 <code>내부 단편화</code> 라고 한다</p>
<h3 id="메모리-단편화">메모리 단편화</h3>
<p>외부/내부 단편화 두개로 나뉜다</p>
<h4 id="외부-단편화">외부 단편화</h4>
<p>메모리가</p>
<pre><code>[사용][빈 20][사용][빈 30][사용][빈 10]</code></pre><p>이라고 했을 때 전체 빈 공간은 20 + 30 + 10 = 60</p>
<p>그런데 50짜리 연속 공간이 필요하다면??? -&gt; 없다</p>
<p>빈 공간은 충분한데 조각조각 흩어져 있어서 큰 연속 공간을 만들 수 없는 것이 외부 단편화다.</p>
<h4 id="내부-단편화">내부 단편화</h4>
<p>아까 예시에서 11바이트가 남는 그 상황</p>
<p>블록 내부에서 남는 공간이므로 내부 단편화라고 한다.</p>
<h3 id="그래서-objectpool이-왜좋은가">그래서 ObjectPool이 왜좋은가?</h3>
<p>총알처럼 같은 타입의 객체를 계속 생성/삭제 한다고 하자</p>
<p>일반 할당을 반복하면 할당/해제/할당/해제 반복을 계속 Allocator가 관리해야 한다.</p>
<p>ObjectPool은 아예 한번에 총알 여러개를 준비한다.</p>
<pre><code>사용할때
Free List에서 빈 Bullet 슬롯 꺼냄
사용끝나면?
Reset
-&gt; Free List에 반환</code></pre><blockquote>
<p>일반 Heap Allocator를 반복적으로 거치는 횟수를 줄인다.</p>
</blockquote>
<p>동일한 크기의 객체 공간을 반복해서 사용하기 때문에 메모리 상태도 비교적 예측 가능해진다.</p>
<h3 id="object-pool의-장점">Object Pool의 장점</h3>
<h4 id="할당해제-비용-감소">할당/해제 비용 감소</h4>
<p>매번 일반 Allocator에게 새로운 공간을 요청하지 않고 기존 슬롯을 재사용한다.</p>
<h4 id="fragmentation-감소">Fragmentation 감소</h4>
<p>동일한 객체 공간을 반복해서 재사용하기 때문에 빈번한 다양한 할당/해제로 인한 메모리 조각화를 줄일 수 있다.</p>
<h4 id="cache-locality">Cache Locality</h4>
<p>Pool이 객체들을 가까운 메모리에 배치한다면 순회 시 캐시 효율도 좋아질 수 있다.</p>
<p>단, Object Pool을 썼다고 무조건 캐시 효율이 좋아지는 것은 아니다. -&gt; 메모리 배치 방식에 따라 달라진다</p>
<h5 id="예시-1">예시</h5>
<p>이런 Pool은 캐시 지역성이 좋을 수 있다.</p>
<pre><code class="language-c++">std::vector&lt;Bullet&gt; Pool;</code></pre>
<p>객체들이 연속해서 붙어 있으니까 매 프레임 순회할때</p>
<pre><code class="language-c++">for (Bullet&amp; B : Pool)
{
    B.Update();
}</code></pre>
<p>CPU가 한 Bullet을 읽으면서 주변 Bullet 데이터도 같은 Cache Line(캐시 라인)에 같이 가져올 가능성이 높다.</p>
<p>이런 배치는 캐시 친화적임</p>
<p>반대로 이런 식이면 다르다</p>
<pre><code class="language-c++">std::vector&lt;Bullet*&gt; Pool;</code></pre>
<p>각 Bullet을 개별적으로 new해서 저장했다고 치자</p>
<pre><code>Pool
[ptr][ptr][ptr][ptr]
  │    │    │    │
  ↓    ↓    ↓    ↓
0x1000   Bullet
0xA800   Bullet
0x3500   Bullet
0xF200   Bullet</code></pre><p>포인터 배열 자체는 연속이지만, 실제 Bullet 객체들은 Heap 여기저기에 흩어져 있을 수 있다.</p>
<p>순회하면</p>
<pre><code>Bullet A 읽음
↓
멀리 떨어진 Bullet B
↓
또 다른 주소의 Bullet C</code></pre><p>이런 식이 되기 때문에 Cache Miss(캐시 미스)가 많이 날 수 있다.</p>
<p><strong>또 중요한게 활성 객체 배치</strong></p>
<pre><code>[Active][Inactive][Inactive][Active][Inactive][Active]</code></pre><p>Pool이 연속 메모리여도 위처럼 활성 객체가 듬성듬성 섞여있으면 매 프레임 Active만 처리하고 싶어도 불필요한 데이터까지 캐시에 들어올 수 있다.</p>
<p>반대로</p>
<pre><code>[Active][Active][Active][Active][Inactive][Inactive]</code></pre><p>위처럼 활성 객체를 앞쪽으로 밀집시키면 실제로 순회하는 데이터가 서로 붙어있어서 캐시 효율이 더 좋다.</p>
<h4 id="결론">결론</h4>
<p>배치방식은 크게 두가지를 보면 된다.</p>
<ul>
<li><p>객체 자체가 연속 메모리에 있느냐</p>
</li>
<li><p>자주 같이 접근하는 활성 객체들이 서로 가까이 있느냐</p>
</li>
</ul>
<h3 id="object-pool의-단점">Object Pool의 단점</h3>
<h4 id="pool-크기-문제">Pool 크기 문제</h4>
<p>1000개를 미리 준비했는데 실제 최대 동시 사용량이 100개라면 많은 메모리를 미리 잡아둘 수 있다.</p>
<p>반대로 준비한 용량보다 더 많이 필요해지면 실패 처리, 추가 Chunk 할당, Pool 확장 같은 정책이 필요하다. Object Pool이 반드시 고정 크기일 필요는 없다.</p>
<blockquote>
<p>최대 동시 사용량을 예측하고 실제 프로파일링 결과로 크기를 조정해야 한다</p>
</blockquote>
<h4 id="reset초기화-문제">Reset(초기화) 문제</h4>
<p>이게 실제 버그로 이어지기 쉽다</p>
<p>총알을 반환하기 전에</p>
<pre><code>Damage = 100
Target = EnemyA
Homing = true</code></pre><p>가 남아있다고 하자</p>
<p>다음에 같은 객체를 재사용했는데 초기화를 안하면 이전 총알 상태가 새 총알에 남는다.</p>
<p>그래서 Pool에는</p>
<pre><code>Acquire
↓
초기화
사용
Release
↓
상태 정리</code></pre><p>가 중요하다</p>
<h3 id="object-pool--arena--slab-차이">Object Pool / Arena / Slab 차이</h3>
<p>Object Pool -&gt; 특정 종류의 슬롯/객체를 재사용해 반복적인 일반 할당·해제 비용을 줄이는 것이 핵심. 구현에 따라 객체의 생성자/소멸자를 매번 호출할 수도 있고, 상태를 Reset해 객체 자체를 재사용할 수도 있다.</p>
<p>Arena Allocator -&gt; 큰 메모리 덩어리 하나를 잡아두고 앞에서부터 빠르게 할당한다, 개별 해제를 거의 안하고 전체 Reset한다. 예) 한 프레임 동안만 사용하는 임시 데이터</p>
<p>Slab Allocator -&gt; 같은 크기의 메모리 블록을 대량으로 관리한다, Object Pool과 비슷하지만 Object Pool은 게임 객체 재사용 관점, Slab은 메모리 할당 시스템 관점</p>
<h2 id="캐시-지역성">캐시 지역성</h2>
<p>CPU 캐시는 일반적으로 데이터를 Cache Line(캐시 라인) 단위로 가져오고 관리한다. 요청한 데이터가 상위 캐시에 없다면 더 낮은 캐시나 메모리 계층에서 해당 캐시 라인을 가져온다.</p>
<blockquote>
<p>일반적인 현대 CPU에서 캐시 라인은 보통 64바이트이다.</p>
</blockquote>
<p>예를 들어 캐시 라인이 64바이트이고 Bullet 하나가 16바이트라면</p>
<pre><code>[Bullet0][Bullet1][Bullet2][Bullet3]
|----------- 64B -----------|</code></pre><p>예시처럼 <code>Bullet</code> 4개가 하나의 64바이트 캐시 라인 경계 안에 배치되어 있다면, <code>Bullet0</code>을 읽을 때 같은 라인의 <code>Bullet1~3</code>도 함께 캐시에 들어온다. 실제 배치는 정렬과 객체 크기에 따라 달라질 수 있다.</p>
<p>그 다음 바로 Bullet1을 읽으면</p>
<pre><code>더 낮은 메모리 계층까지 내려갈 필요가 줄어듦
→ 이미 Cache에 있음
→ Cache Hit(캐시 히트)</code></pre><p>이것이 공간 지역성(Spatial Locality)</p>
<blockquote>
<p>지금 사용한 데이터 근처의 데이터를 곧 다시 사용할 가능성이 높다</p>
</blockquote>
<p>시간 지역성 (Temporal Locality)는</p>
<blockquote>
<p>한 데이터를 사용하면 가까운 시간 안에 같은 데이터를 다시 사용할 가능성이 높다.</p>
</blockquote>
<p>한 번 가져온 캐시 라인은 당장 사라지는 것이 아니라 캐시에 머무를 수 있으므로, 가까운 시간 안에 같은 데이터를 다시 사용하면 Cache Hit가 날 가능성이 높다.</p>
<p>캐시 공간이 필요하면 기존 Cache Line 일부가 Eviction(축출)된다. 어떤 라인을 교체할지는 CPU 마이크로아키텍처의 Replacement Policy(교체 정책)에 따라 달라지며, 실제 하드웨어는 정확한 LRU보다는 pseudo-LRU나 적응형 정책 등 구현별 정책을 사용하는 경우가 많다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Copy Semantics / OwnerShip / Copy Constructor / Copy Assignment / Value Category]]></title>
            <link>https://velog.io/@kyu_/Copy-Semantics-OwnerShip-Copy-Constructor-Copy-Assignment-Value-Category</link>
            <guid>https://velog.io/@kyu_/Copy-Semantics-OwnerShip-Copy-Constructor-Copy-Assignment-Value-Category</guid>
            <pubDate>Mon, 14 Sep 2026 17:44:11 GMT</pubDate>
            <description><![CDATA[<h1 id="copy-semantics--ownership-정리">Copy Semantics / Ownership 정리</h1>
<p>Copy Semantics는 이 객체를 복사한다는게 프로그램에서 무슨 의미인가? 를정하는것</p>
<p>예를들어</p>
<pre><code class="language-c++">class Inventory
{
    std::vector&lt;Item&gt; Items;
};</code></pre>
<p>Inventory를 복사한다면 보통 아이템 목록도 독립적으로 복사된 새로운 Inventory가 자연스럽다</p>
<p>반대로</p>
<pre><code class="language-c++">class Enemy
{
    AIController Controller;
    PhysicsBody Physics;
    ...
};</code></pre>
<p>Enemy를 복사한다는게 애매할 수 있다.</p>
<ul>
<li>같은 AI 상태까지 복사?</li>
<li>같은 월드 위치?</li>
<li>네트워크 ID?</li>
<li>물리 객체?</li>
<li>등록된 이벤트?</li>
</ul>
<p>이런 경우는 애초에 Enemy는 복사 불가능하게 만드는게 더 올바른 설계
즉, 모든 객체가 복사 가능해야 하는게 아니다.</p>
<hr>
<p>Ownership 판단</p>
<p>게임코드에서</p>
<p><code>값으로 직접 가지고 있음</code></p>
<pre><code>std::vector&lt;Item&gt; Items;</code></pre><p>객체가 그 데이터를 직접 소유한다.
부모객체가 죽으면 같이 정리된다.</p>
<p><code>unique_ptr</code></p>
<pre><code class="language-c++">std::unique_ptr&lt;Weapon&gt; Weapon;</code></pre>
<p>이 객체가 Weapon을 단독 소유한다.</p>
<p>그래서 기본적으로 복사가 안된다.
오히려 이게 소유권이 두 군데 생기는것을 막아줘서 좋은것</p>
<p><code>shared_ptr</code></p>
<pre><code class="language-c++">std::shared_ptr&lt;Resource&gt; Resource;</code></pre>
<p>복사하면 Resource자체가 복사되는게 아니라 같은 객체를 공동 소유한다.
그래서 정말 공동 소유가 필요한 경우에만 사용한다.</p>
<p><code>Raw pointer / Reference</code></p>
<pre><code class="language-c++">Enemy* Target;
Enemy&amp; Owner;</code></pre>
<p>내가 소유하는게 아니라 다른 곳의 객체를 참조한다.
는 의미로 사용할 수 있다.
따라서 복사해도 같은 Enemy를 가리키는게 자연스러울 순 있는데 Dangling을 조심해야함</p>
<h2 id="결론">결론</h2>
<p>클래스를 만들때</p>
<ol>
<li>이 객체는 복사되는게 자연스러운가?</li>
<li>멤버 중 누가 무엇을 소유했는가?</li>
<li>복사한다면 Resource도 복제할 것인가, 공유할 것인가?</li>
<li>복사의 의미가 애매하면 차라리 복사를 금지할 것인가?</li>
</ol>
<p>판단이 중요하다.</p>
<hr>
<h1 id="copy-constructor--copy-assignment">Copy Constructor / Copy Assignment</h1>
<h2 id="목표">목표</h2>
<blockquote>
<p>Copy Constructor / Copy Assignment의 실전 의미
어떤 클래스를 복사 가능하게 만들고, 어떤 클래스는 복사를 막아야 하는지 판단한다.</p>
</blockquote>
<p>Copy Constructor와 Copy Assignment가 언제 필요한 연산인지 이해한다.</p>
<hr>
<h2 id="둘의-차이">둘의 차이</h2>
<p>Copy Constructor(복사 생성자)</p>
<pre><code class="language-c++">Player b = a;</code></pre>
<p>b가 아직 없었다.
-&gt; a를 이용해서 새로운 b를 만든다.</p>
<p>Copy Assignment(복사 대입 연산자)</p>
<pre><code class="language-c++">Player a;
Player b;

b = a;</code></pre>
<h2 id="직접-만들어-사용">직접 만들어 사용??</h2>
<pre><code class="language-C++">class PlayerConfig
{
public:
    std::string Name;
    std::vector&lt;int&gt; Stats;
    int Level;
};</code></pre>
<p>이 클래스는 멤버들이 알아서 올바르게 복사된다. (string, vector, int모두 자체적으로 깊은복사를 지원)
그래서 직접</p>
<pre><code class="language-c++">PlayerConfig(const PlayerConfig&amp; other)</code></pre>
<p>같은걸 구현할 이유가 거의 없다. 차라리 컴파일러에게 맡기는게 낫다</p>
<pre><code class="language-c++">PlayerConfig A;
A.Name = &quot;Hero&quot;;
A.Stats = {10, 20, 30};
A.Level = 5;

PlayerConfig B = A; // 복사 생성자 자동 호출</code></pre>
<p>이때 컴파일러는 Name -&gt; 문자열 메모리를 새로 할당받아 내부 텍스트(&quot;Hero&quot;)를 안전하게 깊은 복사함
Stats -&gt; 동적 배열 메모리를 새로 할당받아 원소들을 깊은 복사함
Level -&gt; 기본 타입이므로 값(5)을 단순 복사</p>
<p>이를
Rule of Zero</p>
<blockquote>
<p>자원 관리를 이미 잘하는 타입들을 멤버로 사용하면 내 클래스가 직접 복사/소멸 코드를 작성할 일이 줄어든다.</p>
</blockquote>
<p>만약 클래스가 원시포인터를 직접 동적할당하여 관리하고 있다면</p>
<pre><code class="language-c++">class DangerousPlayer
{
public:
    char* Name; // 원시 포인터
    int Level;
};</code></pre>
<p>이 경우 컴파일러가 자동 생성한 복사는 포인터 주소만 그대로 복사하는 얕은복사를 수행한다.</p>
<h2 id="복사가-자연스러운-예시">복사가 자연스러운 예시</h2>
<pre><code>struct WeaponStats
{
    float Damage;
    float FireRate;
    float Range;
};

struct CharacterConfig
{
    std::string Name;
    std::vector&lt;float&gt; Stats;
};</code></pre><p>이런 데이터들은 복사가 자연스럽다. 값 자체가 의미인 객체라서
하나 복사해서 수정해도 원본과 별개인게 자연스럽다.</p>
<h2 id="복사가-이상한-객체">복사가 이상한 객체</h2>
<pre><code class="language-c++">class NetworkConnection
{
};

class ThreadPool
{
};

class GameServer
{
};</code></pre>
<p>이런 객체는 <code>복사본을 하나 만든다</code>라는 의미 자체가 이상할 수 있다.</p>
<p>ThreadPool을 복사하면</p>
<ul>
<li>Worker Thread들도 복사?</li>
<li>Queue도 복사?</li>
<li>진행중 Task도 복사?</li>
</ul>
<p>이런 객체들은 복사를 막는게 자연스러움</p>
<pre><code class="language-c++">class ThreadPool
{
public:
    ThreadPool(const ThreadPool&amp;) = delete;
    ThreadPool&amp; operator=(const ThreadPool&amp;) = delete;
};</code></pre>
<blockquote>
<p>복사가 의미없는 타입이면 API수준에서 복사를 금지한다.</p>
</blockquote>
<h2 id="unique_ptr이-있다면">unique_ptr이 있다면?</h2>
<pre><code class="language-c++">class Character
{
    std::unique_ptr&lt;Weapon&gt; Weapon;
};</code></pre>
<p>기본적으로 Character도 자동 복사가 안된다. Weapon을 누가 소유해야 하는지 C++이 마음대로 정할 수 없기에</p>
<p><strong>선택 1</strong></p>
<p>Character 자체를 복사하지 않는다.
게임 Entity라면 이쪽이 자연스러울 수 있다.</p>
<p><strong>선택 2</strong></p>
<p>Character를 복사하면 Weapon도 새로 복제한다.
이 경우에만 직접 Copy Semantics를 정의한다.</p>
<p>직접 Copy Constructor를 구현하는건 실제 설계 요구가 있는경우만 하는게 맞다</p>
<h2 id="copy-assignment가-더-까다로운-이유">Copy Assignment가 더 까다로운 이유</h2>
<pre><code class="language-c++">Player a;
Player b;

...
b = a;</code></pre>
<p>이 경우에 b가 이미 무언가를 가지고 있을 수 있다.
예를들어 a -&gt; Weapon A소유, b -&gt; Weapon B소유
b = a했을때 기존 Weapon B는 어떻게 할것인가?
RAII타입을 사용하면 이런 정리를 멤버 타입이 알아서 해줌
그래서 결론적으로 RAII타입들을 이용해서 직접 자원 관리 코드를 줄이는게 중요하다.</p>
<h2 id="게임-개발에서의-기준">게임 개발에서의 기준</h2>
<pre><code class="language-c++">class Character
{
public:
    CharacterStats Stats;
    std::vector&lt;Item&gt; Inventory;
    std::unique_ptr&lt;Weapon&gt; Weapon;
    Enemy* Target;
};</code></pre>
<p>복사 요구사항을 판단할때</p>
<ul>
<li>Stats -&gt; 값이므로 복사가 자연스러움</li>
<li>Inventory -&gt; 독립적인 Inventory복사가 자연스러울 수 있음</li>
<li>Weapon -&gt; 독점 소유, 복사 할지 새 Weapon을 만들지 결정 필요</li>
<li>Target -&gt; non-owning이라면 같은 Enemy를 가리켜도 될 수 있음</li>
</ul>
<p>그래서 결론적으로 <code>Character자체를 복사하는게 게임 설계상 필요한가?</code>를 고민</p>
<hr>
<h1 id="value-category">Value Category</h1>
<h2 id="목표-1">목표</h2>
<pre><code class="language-c++">std::string a = &quot;hello&quot;;
std::string b = a;
std::string c = std::string(&quot;hello&quot;);</code></pre>
<p>여기서 어떤 경우는 복사, 어떤경우는 이동 가능한 후보가 되는지 설명할 수 있는가</p>
<h2 id="왜-value-category가-필요한가">왜 Value Category가 필요한가?</h2>
<p>C++에서는 어떤 표현식이</p>
<ul>
<li>이미 존재하는 특정 객체를 나타내는지</li>
<li>곧 사라질 임시 값인지</li>
<li>존재하는 객체지만 이제 자원을 가져가도 되는 상태인지</li>
</ul>
<p>에 따라 다른 함수를 선택할 수 있다.</p>
<pre><code class="language-c++">void SetName(const std::string&amp; name); // 읽기/복사 용도
void SetName(std::string&amp;&amp; name);      // 이동 가능한 값</code></pre>
<p>호출</p>
<pre><code class="language-c++">std::string name = &quot;Kim&quot;;

SetName(name);
SetName(std::string(&quot;Kim&quot;));</code></pre>
<p>두 호출의 name 표현식과 임시 std::string(&quot;Kim&quot;)의 성질이 다르기 때문에 서로 다른 overload가 선택될 수 있다.</p>
<h2 id="큰-구조">큰 구조</h2>
<p>C++의 분류를 보자</p>
<pre><code>Expression
│
├─ glvalue
│   ├─ lvalue
│   └─ xvalue
│
└─ prvalue


rvalue
├─ prvalue
└─ xvalue</code></pre><p>즉</p>
<pre><code>glvalue = lvalue + xvalue
rvalue = prvalue + xvalue</code></pre><p><code>xvalue는 둘에 동시에 포함된다.</code></p>
<h2 id="lvalue">lvalue</h2>
<pre><code class="language-c++">std::string name = &quot;Player&quot;;</code></pre>
<p>여기서 표현식 name은 lvalue다</p>
<p>특징은</p>
<blockquote>
<p>현재 존재하는 특정 객체를 나타내며, 그 객체의 정체성을 계속 추적할 수 있다.</p>
</blockquote>
<p>쉽게말해 name이라는 그 객체가 실제로 계속 존재한다.</p>
<pre><code class="language-c++">name = &quot;Enemy&quot;;
name.size();
&amp;name;</code></pre>
<p>이것처럼 계속 사용할 수 있다.</p>
<p>여기서 주의할건 lvalue는 왼쪽에 올 수 있는 값이 아니다.</p>
<pre><code class="language-c++">const int x = 10;
x = 20;</code></pre>
<p>const라는 예외가 있다.
lvalue = 대입 가능이 아니다.</p>
<h2 id="prvalue">prvalue</h2>
<pre><code class="language-c++">10
또는
std::string(&quot;Player&quot;);</code></pre>
<p>같은 표현식은 대표적인 prvalue이다.</p>
<pre><code class="language-c++">std::string name = std::string(&quot;Player&quot;);</code></pre>
<p>여기서도 오른쪽 std::string(&quot;Player&quot;)는 prvalue이다.</p>
<blockquote>
<p>특정 장기 생존 객체의 이름을 나타내기보다는 새로운 값을 계산하거나 생성하는 표현식</p>
</blockquote>
<p>이라고 이해하면 좋다.</p>
<pre><code class="language-c++">FVector GetSpawnPosition();

FVector pos = GetSpawnPosition();</code></pre>
<p>GetSpawnPosition()이 값으로 FVector를 반환한다면 그 함수 호출 표현식도 보통 prvalue다.</p>
<h2 id="xvalue">xvalue</h2>
<pre><code class="language-c++">std::string name = &quot;Player&quot;;
std::move(name);</code></pre>
<p>std::move(name) 표현식은 xvalue다.
name객체가 사라진건 아니다, 여전히 name은 존재 하지만 std::move(name)이라는 표현식은</p>
<blockquote>
<p>이 객체는 이제 자원을 가져가도 되는 대상으로 취급해도 된다.</p>
</blockquote>
<p>라는 형태의 표현식</p>
<p>그래서 xvalue는</p>
<ul>
<li>실제 특정 객체를 나타냄 -&gt; glvalue</li>
<li>이동 가능한 대상으로 취급됨 -&gt; rvalue</li>
</ul>
<p>둘다 해당한다.</p>
<h2 id="가장-실용적인-세분류">가장 실용적인 세분류</h2>
<p><strong>lvalue</strong></p>
<pre><code class="language-c++">std::string a = &quot;hello&quot;;
a</code></pre>
<p>-&gt; 살아있는 기존 객체</p>
<p><strong>prvalue</strong></p>
<pre><code class="language-c++">std::string(&quot;hello&quot;);</code></pre>
<p>-&gt; 새 값을 만드는 표현식/임시 값</p>
<p><strong>xvalue</strong></p>
<pre><code class="language-c++">std::move(a);</code></pre>
<p>-&gt; 기존 객체인데 이동 가능한 대상으로 표시된 표현식</p>
<p>이 세개를 구분하면 Move Semantics의 대부분이 이해된다!</p>
<h2 id="중요점">중요점</h2>
<p>객체와 표현식을 구분해야 한다.</p>
<pre><code class="language-c++">std::string a = &quot;hello&quot;;</code></pre>
<p>여기서 a객체가 lvalue객체인게 아니다.
a라는 표현식의 Value Category가 lvalue다.</p>
<p>왜냐하면 같은객체를 a로 쓰면 lvalue인데 std::move(a)로 쓰면 xvalue이기 때문이다. (둘다 같은 a객체를 나타낸다)
객체가 변신한게 아니라 그 객체를 나타내는 표현식의 분류가 달라진 것</p>
<pre><code>a
→ lvalue expression
        │
        ▼
   같은 string 객체

std::move(a)
→ xvalue expression
        │
        ▼
   같은 string 객체</code></pre><h2 id="이게-함수선택에-영향을-줌">이게 함수선택에 영향을 줌</h2>
<pre><code class="language-c++">void Func(const std::string&amp; value)
{
    std::cout &lt;&lt; &quot;const&amp;&quot;;
}

void Func(std::string&amp;&amp; value)
{
    std::cout &lt;&lt; &quot;&amp;&amp;&quot;;
}

std::string str = &quot;hello&quot;;
Func(str);</code></pre>
<p>str은 lvalue 표현식이므로 보통 Func(const std::string&amp;)쪽이 선택된다.
반면 Func(std::string(&quot;hello&quot;))로 호출한다면 prvalue이므로 Func(std::string&amp;&amp;)에 바인딩 될 수 있다.</p>
<h2 id="rvalue는">rvalue는?</h2>
<p>rvalue = prvalue + xvalue</p>
<p>prvalue = 새로 만들어진 임시 값
xvalue = 기존 객체를 이동 가능한 대상으로 나타냄
둘다 rvalue reference <code>T&amp;&amp;</code>에 연결될 수 있는 쪽이라는 공통점이 있다.</p>
<h2 id="glvalue는">glvalue는?</h2>
<p>glvalue = lvalue + xvalue</p>
<p>둘의 공통점은, <code>특정 객체의 정체성을 나타낸다.</code>
a(특정 a객체), std::move(a) (여전히 특정 a객체)</p>
<p>10같은 prvalue는 특정 기존 객체를 찾아가는 표현식이 아니다.
xvalue가 왜 lvalue와 rvalue의 성질을 일부 동시에 갖는지 이렇게 확인가능</p>
<h2 id="헷갈리는것-이름있는-t는-lvalue다">헷갈리는것 이름있는 T&amp;&amp;는 lvalue다.</h2>
<pre><code class="language-c++">void Func(std::string&amp;&amp; value)
{
}</code></pre>
<p>value의 타입은 std::string&amp;&amp;이다.
그런데 함수 안에서 value라는 표현식은 lvalue다.
왜?</p>
<p>value라는 이름이 있고, 함수 안에서 같은 객체를 계속 특정해서 사용할 수 있기때문에</p>
<pre><code class="language-c++">void Func(std::string&amp;&amp; value)
{
    OtherFunc(value);
}</code></pre>
<p>그래서 위와같이 value를 그냥 넘기면 lvalue 판정이 된다.
다시 이동 가능한 값으로 넘기려면 std::move(value)처럼 해야한다.</p>
<pre><code class="language-c++">void Func(std::string&amp;&amp; value)
{
    OtherFunc(std::move(value));
}</code></pre>
<h2 id="결론-1">결론</h2>
<pre><code>이 표현식은 기존 객체인가?
        │
        ├─ 그렇다
        │    ↓
        │  이동 가능하다고 표시됐나?
        │    ├─ X → lvalue
        │    └─ O → xvalue
        │
        └─ 새 값을 만들어내는 표현식인가?
             ↓
           prvalue</code></pre><h2 id="문제">문제</h2>
<h3 id="1">1</h3>
<pre><code class="language-c++">void Submit(std::vector&lt;Enemy&gt; enemies)
{
    EnemyManager.SetEnemies(enemies);
}</code></pre>
<p><code>enemies</code>는 함수에 값으로 들어왔고 더 이상 <code>Submit()</code>에서 사용할 필요가 없다고 하자.</p>
<p><code>SetEnemies()</code>로 넘길 때 불필요한 대형 vector 복사를 줄이려면 왜 그냥 <code>enemies</code>를 넘기는 것보다 다음 형태가 의미가 있을지 설명해봐.</p>
<pre><code class="language-c++">EnemyManager.SetEnemies(std::move(enemies));</code></pre>
<p>-&gt; lvalue상태로 넘어가면 <code>enemies</code>라는 vector를 전부 다 복사해야한다. 즉, <code>SetEnemies</code>의 매개변수 <code>enemies</code>를 만들기 위해서 큰 복사비용이 발생한다. 하지만 std::move를 통해 xvalue로 넘기게 되면 vector의 move constructor를 이용할 수 있다. 벡터는 보통 이동비용이 싸다. 기존 벡터가 가지고 있던 내부 버퍼의 소유권을 새 vector가 가져간다. 즉, 데이터가 만개라면 만개를 하나씩 복사하는게 아니라, 대략 포인터/size/capacity같은 내부 상태만 넘기는 것.</p>
<p>정리하자면</p>
<p>std::move는 enemies를 xvalue로 변환할 뿐이며, 실제 이동 여부는 받는 함수의 매개변수 형태에 따라 결정됨. SetEnemies가 값으로 받거나 std::vector<Enemy>&amp;&amp;로 받는 등 이동을 활용할 수 있는 형태라면 vector의 이동 연산이 선택되어 복사비용을 줄일 수 있다. const std::vector<Enemy>&amp;로 받는다면 std::move를 붙여도 이동은 일어나지 않는다.</p>
<h3 id="2">2</h3>
<pre><code class="language-c++">void Process(std::string&amp;&amp; name)
{
    Use(name);
}</code></pre>
<p><code>name</code>의 타입이 <code>std::string&amp;&amp;</code>인데도 <code>Use(name)</code>에서 <code>name</code>이 자동으로 rvalue 취급되지 않는 이유를 네 말로 설명해봐.</p>
<p>-&gt; 매개변수 name은 Process의 함수블록안에서 name이라는 lvalue로써 존재하기 떄문, name이라는 특정 객체로 존재해서 name.size() 같은 작업들이 가능하다 그렇기 때문에 rvalue취급이 안되고 std::move(name)을 사용해서 xvalue로 만들어주어야 rvalue취급을 받을 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[게임 수학 2주차]]></title>
            <link>https://velog.io/@kyu_/%EA%B2%8C%EC%9E%84-%EC%88%98%ED%95%99-2%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@kyu_/%EA%B2%8C%EC%9E%84-%EC%88%98%ED%95%99-2%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 13 Sep 2026 10:13:18 GMT</pubDate>
            <description><![CDATA[<h1 id="7일차--선형-결합-span-선형-독립">7일차 — 선형 결합, Span, 선형 독립</h1>
<h2 id="목표">목표</h2>
<blockquote>
<p>여러 벡터를 재료로 사용했을 때 어떤 방향들을 만들어낼 수 있는지 이해한다.</p>
</blockquote>
<hr>
<h2 id="선형-결합">선형 결합</h2>
<p>플레이어에게 다음 두 방향 벡터가 있다고 하자.</p>
<pre><code class="language-text">Forward = (1, 0, 0)
Right   = (0, 1, 0)</code></pre>
<p>플레이어가 앞으로 3만큼, 오른쪽으로 2만큼 이동한다면 이동 벡터는 다음과 같다.</p>
<pre><code class="language-text">3Forward + 2Right

= 3(1, 0, 0) + 2(0, 1, 0)
= (3, 2, 0)</code></pre>
<p>이처럼 여러 벡터에 각각 숫자를 곱한 뒤 더하는 것을 <strong>선형 결합(Linear Combination)</strong>이라고 한다.</p>
<p>일반적으로 벡터 <code>A</code>, <code>B</code>에 대해</p>
<pre><code class="language-text">aA + bB</code></pre>
<p>형태로 표현할 수 있다.</p>
<p>여기서 <code>a</code>, <code>b</code>는 스칼라다.</p>
<hr>
<h2 id="span">Span</h2>
<p><strong>Span</strong>은 주어진 벡터들을 선형 결합해서 만들 수 있는 <strong>모든 벡터의 집합</strong>이다.</p>
<p>예를 들어</p>
<pre><code class="language-text">A = (1, 0, 0)</code></pre>
<p>하나만 있다고 하자.</p>
<pre><code class="language-text">2A  = (2, 0, 0)
-5A = (-5, 0, 0)</code></pre>
<p>처럼 X축 방향의 벡터는 얼마든지 만들 수 있다.</p>
<p>하지만</p>
<pre><code class="language-text">(0, 1, 0)</code></pre>
<p>같은 Y축 방향 벡터는 만들 수 없다.</p>
<p>따라서 <code>A</code> 하나가 만드는 Span은 <strong>X축 전체</strong>, 즉 하나의 직선이다.</p>
<hr>
<h2 id="벡터가-두-개라면">벡터가 두 개라면?</h2>
<pre><code class="language-text">A = (1, 0, 0)
B = (0, 1, 0)</code></pre>
<p>두 벡터를 선형 결합하면</p>
<pre><code class="language-text">aA + bB = (a, b, 0)</code></pre>
<p>가 된다.</p>
<p><code>a</code>, <code>b</code>에 어떤 실수를 넣어도 되므로 XY 평면 위의 모든 벡터를 만들 수 있다.</p>
<p>따라서</p>
<pre><code class="language-text">Span(A, B) = XY 평면 전체</code></pre>
<p>가 된다.</p>
<hr>
<h2 id="3d-공간-전체를-표현하려면">3D 공간 전체를 표현하려면</h2>
<p>다음 세 벡터를 생각해보자.</p>
<pre><code class="language-text">A = (1, 0, 0)
B = (0, 1, 0)
C = (1, 1, 0)</code></pre>
<p>벡터가 세 개이기는 하지만 모두 XY 평면에 있다.</p>
<p>특히</p>
<pre><code class="language-text">C = A + B</code></pre>
<p>이므로 <code>C</code>가 새로운 방향을 추가해주지도 않는다.</p>
<p>따라서 이 세 벡터만으로는 Z축 방향을 만들 수 없으며 3D 공간 전체를 표현할 수 없다.</p>
<p>반면</p>
<pre><code class="language-text">A = (1, 0, 0)
B = (0, 1, 0)
C = (0, 0, 1)</code></pre>
<p>이라면</p>
<pre><code class="language-text">aA + bB + cC = (a, b, c)</code></pre>
<p>가 되므로 3D 공간의 모든 벡터를 만들 수 있다.</p>
<hr>
<h2 id="선형-독립과-선형-종속">선형 독립과 선형 종속</h2>
<p>벡터들이 다음 식을 만족한다고 하자.</p>
<pre><code class="language-text">aA + bB + cC = 0</code></pre>
<p>이 식이</p>
<pre><code class="language-text">a = b = c = 0</code></pre>
<p>일 때만 성립한다면 <code>A</code>, <code>B</code>, <code>C</code>는 <strong>선형 독립(Linear Independent)</strong>이다.</p>
<p>반대로 0이 아닌 계수를 사용해도 위 식을 만족시킬 수 있다면 <strong>선형 종속(Linear Dependent)</strong>이다.</p>
<p>쉽게 말하면 선형 독립은</p>
<blockquote>
<p>어떤 벡터도 나머지 벡터들의 선형 결합으로 만들어낼 수 없는 상태</p>
</blockquote>
<p>라고 볼 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">A = (1, 0, 0)
B = (2, 0, 0)</code></pre>
<p>에서는</p>
<pre><code class="language-text">B = 2A</code></pre>
<p>이므로 두 벡터는 선형 종속이다.</p>
<p>반면</p>
<pre><code class="language-text">A = (1, 0, 0)
B = (0, 1, 0)</code></pre>
<p>에서는 한 벡터를 다른 벡터의 배수로 만들 수 없으므로 선형 독립이다.</p>
<p>두 개의 <strong>0이 아닌 벡터만 비교할 때는</strong> 서로 평행하면 종속이고, 평행하지 않으면 독립이라고 볼 수 있다.</p>
<p>하지만 벡터가 세 개 이상이라면 단순히 서로 평행한지만 봐서는 안 된다.</p>
<p>예를 들어</p>
<pre><code class="language-text">A = (1, 0, 0)
B = (0, 1, 0)
C = (1, 1, 0)</code></pre>
<p>에서는 어느 두 벡터도 서로 평행하지 않지만</p>
<pre><code class="language-text">C = A + B</code></pre>
<p>이므로 세 벡터 전체는 선형 종속이다.</p>
<hr>
<h2 id="외적과-선형-독립">외적과 선형 독립</h2>
<p>두 3차원 벡터가 평행하다면</p>
<pre><code class="language-text">A × B = 0</code></pre>
<p>이다.</p>
<p>따라서 두 벡터의 외적이 0이라면 두 벡터가 같은 직선 방향에 놓여 있다는 뜻이고, 두 벡터는 선형 종속이다.</p>
<p>반대로 외적이 0이 아니라면 두 벡터는 평행하지 않으므로 선형 독립이다.</p>
<hr>
<h2 id="7일차-정리">7일차 정리</h2>
<h3 id="선형-결합-1">선형 결합</h3>
<blockquote>
<p>여러 벡터에 각각 스칼라를 곱한 뒤 더하는 것</p>
</blockquote>
<pre><code class="language-text">aA + bB + cC</code></pre>
<h3 id="span-1">Span</h3>
<blockquote>
<p>주어진 벡터들을 선형 결합해서 만들 수 있는 모든 벡터의 집합</p>
</blockquote>
<h3 id="선형-독립">선형 독립</h3>
<blockquote>
<p>어떤 벡터도 나머지 벡터들의 선형 결합으로 만들어낼 수 없는 상태</p>
</blockquote>
<p>3차원 공간에서 서로 선형 독립인 세 벡터가 있다면 그 세 벡터를 이용해 3D 공간 전체를 표현할 수 있다.</p>
<p>반대로 세 벡터 중 하나가</p>
<pre><code class="language-text">C = A + B</code></pre>
<p>처럼 나머지 벡터로 만들어진다면 <code>C</code>는 새로운 방향을 추가하지 않는다.</p>
<hr>
<h1 id="8일차--basis와-좌표계">8일차 — Basis와 좌표계</h1>
<h2 id="basis란">Basis란?</h2>
<p>다음 세 벡터를 생각해보자.</p>
<pre><code class="language-text">X = (1, 0, 0)
Y = (0, 1, 0)
Z = (0, 0, 1)</code></pre>
<p>어떤 벡터</p>
<pre><code class="language-text">P = (3, 2, -1)</code></pre>
<p>는</p>
<pre><code class="language-text">P = 3X + 2Y - Z</code></pre>
<p>로 표현할 수 있다.</p>
<p>그리고 X, Y, Z를 이용하면 3D 공간의 모든 벡터를 표현할 수 있다.</p>
<p>이처럼 공간의 모든 벡터를 표현할 수 있는 <strong>선형 독립 벡터 집합</strong>을 <strong>Basis(기저)</strong>라고 한다.</p>
<hr>
<h2 id="basis의-조건">Basis의 조건</h2>
<p>3D 공간의 Basis라면 다음 조건을 만족해야 한다.</p>
<ol>
<li>벡터들이 3D 공간 전체를 Span한다.</li>
<li>벡터들이 서로 선형 독립이다.</li>
</ol>
<p>즉,</p>
<blockquote>
<p>불필요하게 겹치는 방향 없이 공간 전체를 표현할 수 있어야 한다.</p>
</blockquote>
<p>표준적인</p>
<pre><code class="language-text">X = (1, 0, 0)
Y = (0, 1, 0)
Z = (0, 0, 1)</code></pre>
<p>만 Basis가 되는 것은 아니다.</p>
<p>예를 들어</p>
<pre><code class="language-text">E1 = (1, 0, 0)
E2 = (1, 1, 0)
E3 = (0, 0, 1)</code></pre>
<p>도 Basis가 될 수 있다.</p>
<p>세 벡터는 서로 90도가 아니지만 선형 독립이며 3D 공간 전체를 만들 수 있기 때문이다.</p>
<hr>
<h1 id="basis가-바뀌면-좌표도-바뀐다">Basis가 바뀌면 좌표도 바뀐다</h1>
<p>중요한 점은 <strong>벡터와 그 벡터를 표현하는 좌표는 같은 것이 아니라는 것</strong>이다.</p>
<p>World 기준으로</p>
<pre><code class="language-text">P = (3, 2, 1)</code></pre>
<p>이라는 벡터가 있다고 하자.</p>
<p>표준 Basis에서는</p>
<pre><code class="language-text">P = 3X + 2Y + Z</code></pre>
<p>이므로 좌표가</p>
<pre><code class="language-text">(3, 2, 1)</code></pre>
<p>이다.</p>
<p>이번에는 Basis가 다음과 같다고 하자.</p>
<pre><code class="language-text">E1 = (1, 0, 0)
E2 = (1, 1, 0)
E3 = (0, 0, 1)</code></pre>
<p>P를 새로운 Basis로 표현하면</p>
<pre><code class="language-text">P = aE1 + bE2 + cE3</code></pre>
<p>이고,</p>
<pre><code class="language-text">a(1,0,0) + b(1,1,0) + c(0,0,1)
= (a+b, b, c)</code></pre>
<p>이므로</p>
<pre><code class="language-text">(a+b, b, c) = (3, 2, 1)</code></pre>
<p>이다.</p>
<p>따라서</p>
<pre><code class="language-text">a = 1
b = 2
c = 1</code></pre>
<p>이고 새로운 Basis에서 P의 좌표는</p>
<pre><code class="language-text">(1, 2, 1)</code></pre>
<p>이다.</p>
<p>실제 벡터 P가 달라진 것은 아니다.</p>
<blockquote>
<p>같은 벡터를 표현하는 기준축이 달라졌기 때문에 좌표값이 달라진 것이다.</p>
</blockquote>
<p>이 개념이 이후 Local Space와 World Space를 이해하는 핵심이 된다.</p>
<hr>
<h1 id="local-space와-world-space">Local Space와 World Space</h1>
<p>어떤 캐릭터가 회전해 있다면 캐릭터가 바라보는</p>
<pre><code class="language-text">Forward
Right
Up</code></pre>
<p>방향은 World의 X/Y/Z축과 다를 수 있다.</p>
<p>따라서 똑같은 실제 방향도</p>
<pre><code class="language-text">World 기준 좌표
Local 기준 좌표</code></pre>
<p>가 서로 다를 수 있다.</p>
<p>여기서 기억해야 할 것은</p>
<blockquote>
<p>벡터 자체가 바뀐 것이 아니라 벡터를 표현하는 Basis가 바뀐 것이다.</p>
</blockquote>
<hr>
<h1 id="orthogonal-basis--직교-기저">Orthogonal Basis — 직교 기저</h1>
<p>Basis를 이루는 축들이 서로 모두 90도라면 <strong>직교 기저(Orthogonal Basis)</strong>라고 한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">E1 = (2, 0, 0)
E2 = (0, 3, 0)
E3 = (0, 0, 5)</code></pre>
<p>에서</p>
<pre><code class="language-text">E1 · E2 = 0
E1 · E3 = 0
E2 · E3 = 0</code></pre>
<p>이므로 세 벡터는 서로 수직이다.</p>
<p>따라서 직교 기저다.</p>
<p>벡터의 길이는 1일 필요가 없다.</p>
<hr>
<h1 id="orthonormal-basis--정규직교-기저">Orthonormal Basis — 정규직교 기저</h1>
<p>다음 Basis는</p>
<pre><code class="language-text">X = (1, 0, 0)
Y = (0, 1, 0)
Z = (0, 0, 1)</code></pre>
<p>서로 수직이고</p>
<pre><code class="language-text">|X| = |Y| = |Z| = 1</code></pre>
<p>이다.</p>
<p>이처럼</p>
<blockquote>
<p>서로 직교하면서 각 벡터의 길이까지 1인 Basis</p>
</blockquote>
<p>를 <strong>정규직교 기저(Orthonormal Basis)</strong>라고 한다.</p>
<p>게임과 그래픽스에서 매우 자주 사용하는 형태다.</p>
<hr>
<h1 id="정규직교-basis가-편리한-이유">정규직교 Basis가 편리한 이유</h1>
<p>어떤 벡터가</p>
<pre><code class="language-text">V = 3Forward + 2Right + 5Up</code></pre>
<p>이라고 하자.</p>
<p>Forward 방향 성분을 알고 싶다면</p>
<pre><code class="language-text">V · Forward</code></pre>
<p>를 계산하면 된다.</p>
<p>전개하면</p>
<pre><code class="language-text">3(Forward · Forward)
+ 2(Right · Forward)
+ 5(Up · Forward)</code></pre>
<p>정규직교 Basis에서는</p>
<pre><code class="language-text">Forward · Forward = 1
Right · Forward   = 0
Up · Forward      = 0</code></pre>
<p>이므로</p>
<pre><code class="language-text">V · Forward = 3</code></pre>
<p>이다.</p>
<p>따라서 정규직교 Basis에서는 각 축에 대한 성분을 단순한 내적으로 얻을 수 있다.</p>
<pre><code class="language-text">Forward 성분 = V · Forward
Right 성분   = V · Right
Up 성분      = V · Up</code></pre>
<p>이것이 정규직교 Basis가 계산하기 편한 이유 중 하나다.</p>
<hr>
<h1 id="basis가-기울어져-있다면">Basis가 기울어져 있다면?</h1>
<p>다음 두 벡터를 보자.</p>
<pre><code class="language-text">A = (1, 0, 0)
B = (1, 1, 0)</code></pre>
<p>두 벡터는 선형 독립이므로 XY 평면의 Basis가 될 수 있다.</p>
<p>하지만</p>
<pre><code class="language-text">A · B = 1</code></pre>
<p>이므로 서로 수직은 아니다.</p>
<p>이런 Basis에서는 각 축 방향 성분을 단순한 내적만으로 바로 얻을 수 없다.</p>
<p>그래서 그래픽스에서는 필요에 따라 Basis를</p>
<pre><code class="language-text">서로 수직
+
각각 길이 1</code></pre>
<p>인 형태로 만드는 경우가 많다.</p>
<hr>
<h1 id="gram-schmidt-직교화">Gram-Schmidt 직교화</h1>
<p>서로 수직이 아닌 두 벡터</p>
<pre><code class="language-text">A = (1, 0, 0)
B = (1, 1, 0)</code></pre>
<p>가 있다고 하자.</p>
<p>A의 방향은 유지하면서 B를 A와 수직으로 만들고 싶다면 B에서 <strong>A 방향 성분을 제거</strong>하면 된다.</p>
<p>일반적인 투영 공식은</p>
<pre><code class="language-text">proj_A(B) = ((B · A) / (A · A)) A</code></pre>
<p>이다.</p>
<p>A가 이미 단위벡터라면 <code>A · A = 1</code>이므로</p>
<pre><code class="language-text">proj_A(B) = (B · A)A</code></pre>
<p>로 간단해진다.</p>
<p>현재</p>
<pre><code class="language-text">A = (1, 0, 0)
B = (1, 1, 0)</code></pre>
<p>이고 A는 단위벡터이므로</p>
<pre><code class="language-text">B · A = 1</code></pre>
<p>이다.</p>
<p>따라서</p>
<pre><code class="language-text">proj_A(B) = (1, 0, 0)</code></pre>
<p>이고 이를 B에서 제거한다.</p>
<pre><code class="language-text">B&#39; = B - proj_A(B)

   = (1, 1, 0) - (1, 0, 0)
   = (0, 1, 0)</code></pre>
<p>결과적으로</p>
<pre><code class="language-text">A  = (1, 0, 0)
B&#39; = (0, 1, 0)</code></pre>
<p>가 되어</p>
<pre><code class="language-text">A · B&#39; = 0</code></pre>
<p>즉 서로 수직이 된다.</p>
<p>이 과정을 여러 벡터에 반복하는 것이 <strong>Gram-Schmidt 직교화</strong>의 핵심 아이디어다.</p>
<hr>
<h2 id="세-벡터의-경우">세 벡터의 경우</h2>
<pre><code class="language-text">V1 = (1, 0, 0)
V2 = (1, 1, 0)
V3 = (1, 1, 1)</code></pre>
<p>먼저</p>
<pre><code class="language-text">E1 = V1 = (1, 0, 0)</code></pre>
<p>로 잡는다.</p>
<p>V2에서 E1 방향 성분을 제거하면</p>
<pre><code class="language-text">E2 = (0, 1, 0)</code></pre>
<p>을 얻을 수 있다.</p>
<p>그 다음 V3에서 E1과 E2 방향 성분을 모두 제거한다.</p>
<pre><code class="language-text">E3 = (0, 0, 1)</code></pre>
<p>결과적으로</p>
<pre><code class="language-text">E1 = (1, 0, 0)
E2 = (0, 1, 0)
E3 = (0, 0, 1)</code></pre>
<p>이라는 서로 수직인 Basis를 얻는다.</p>
<p>필요하다면 마지막에 각각 정규화하여 정규직교 Basis로 만들 수 있다.</p>
<hr>
<h1 id="정규화와-직교화">정규화와 직교화</h1>
<p>두 개념은 서로 다르다.</p>
<h3 id="정규화normalization">정규화(Normalization)</h3>
<blockquote>
<p>벡터의 방향은 유지하면서 길이를 1로 만든다.</p>
</blockquote>
<h3 id="직교화orthogonalization">직교화(Orthogonalization)</h3>
<blockquote>
<p>여러 벡터가 서로 90도를 이루도록 만든다.</p>
</blockquote>
<p>따라서 Gram-Schmidt를 통해 직교화한 뒤 각 벡터를 정규화하면 정규직교 Basis를 만들 수 있다.</p>
<hr>
<h1 id="외적으로-basis-만들기">외적으로 Basis 만들기</h1>
<p>3D 게임에서는 두 방향을 알고 있을 때 외적을 이용해 세 번째 수직 방향을 만드는 경우도 많다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Forward
Up</code></pre>
<p>을 알고 있다면 두 벡터를 외적하여 <code>Right</code> 방향을 만들 수 있다.</p>
<p>이후 필요하다면 새롭게 만들어진 Right와 Forward를 이용해 Up을 다시 계산하여 축들을 서로 정확히 수직으로 맞출 수도 있다.</p>
<pre><code class="language-text">Forward
   ↓
Forward와 Up으로 Right 계산
   ↓
Right와 Forward로 Up 재계산</code></pre>
<p>단, 외적의 순서를 바꾸면 결과 방향의 부호가 반대가 된다.</p>
<p>또한 엔진의 좌표계와 handedness에 따라 사용하는 순서가 달라질 수 있으므로 특정 공식을 무조건 외우기보다는 <strong>외적의 순서에 따라 방향이 뒤집힌다는 원리</strong>를 이해하는 것이 중요하다.</p>
<hr>
<h1 id="8일차-정리">8일차 정리</h1>
<h3 id="basis">Basis</h3>
<blockquote>
<p>공간 전체를 표현할 수 있는 선형 독립 벡터 집합</p>
</blockquote>
<p>3차원 공간이라면 일반적으로 독립인 세 벡터로 Basis를 구성한다.</p>
<h3 id="orthogonal-basis">Orthogonal Basis</h3>
<blockquote>
<p>Basis 벡터들이 서로 수직인 기저</p>
</blockquote>
<h3 id="orthonormal-basis">Orthonormal Basis</h3>
<blockquote>
<p>Basis 벡터들이 서로 수직이고 각각의 길이도 1인 기저</p>
</blockquote>
<h3 id="gram-schmidt">Gram-Schmidt</h3>
<blockquote>
<p>기존 벡터에서 이전 축 방향으로 투영된 성분을 제거해 서로 수직인 축을 만드는 방법</p>
</blockquote>
<hr>
<h1 id="9일차--행렬을-basis-관점에서-보기">9일차 — 행렬을 Basis 관점에서 보기</h1>
<h2 id="목표-1">목표</h2>
<blockquote>
<p>행렬이 단순히 숫자를 사각형으로 배치한 것이 아니라 무엇을 표현할 수 있는 구조인지 이해한다.</p>
</blockquote>
<hr>
<h1 id="행렬">행렬</h1>
<p>다음과 같은 숫자 배열을 행렬이라고 한다.</p>
<pre><code class="language-text">[ 1 2 3 ]
[ 4 5 6 ]</code></pre>
<p>이 행렬에는</p>
<pre><code class="language-text">2개의 행
3개의 열</code></pre>
<p>이 있으므로 <code>2 × 3</code> 행렬이다.</p>
<hr>
<h1 id="정방행렬">정방행렬</h1>
<p>행과 열의 개수가 같은 행렬을 <strong>정방행렬(Square Matrix)</strong>이라고 한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">[1 2]
[3 4]</code></pre>
<p>는 <code>2 × 2</code> 정방행렬이다.</p>
<hr>
<h1 id="주대각선과-대각행렬">주대각선과 대각행렬</h1>
<p>정방행렬에서 왼쪽 위부터 오른쪽 아래로 이어지는 원소들을 <strong>주대각선(Main Diagonal)</strong>이라고 한다.</p>
<pre><code class="language-text">[2 0 0]
[0 3 0]
[0 0 4]</code></pre>
<p>여기서</p>
<pre><code class="language-text">2, 3, 4</code></pre>
<p>가 주대각선 원소다.</p>
<p>주대각선을 제외한 모든 값이 0인 행렬을 <strong>대각행렬(Diagonal Matrix)</strong>이라고 한다.</p>
<hr>
<h1 id="전치행렬">전치행렬</h1>
<p>행과 열을 서로 바꾸는 것을 <strong>전치(Transpose)</strong>라고 한다.</p>
<pre><code class="language-text">[1 2 3]
[4 5 6]</code></pre>
<p>을 전치하면</p>
<pre><code class="language-text">[1 4]
[2 5]
[3 6]</code></pre>
<p>이 된다.</p>
<p>보통</p>
<pre><code class="language-text">M^T</code></pre>
<p>로 표기한다.</p>
<hr>
<h1 id="벡터도-행렬로-볼-수-있다">벡터도 행렬로 볼 수 있다</h1>
<p>벡터</p>
<pre><code class="language-text">V = (1, 2, 3)</code></pre>
<p>를 열벡터로 표현하면</p>
<pre><code class="language-text">[1]
[2]
[3]</code></pre>
<p>이다.</p>
<p>이는 <code>3 × 1</code> 행렬이다.</p>
<p>이를 전치하면</p>
<pre><code class="language-text">[1 2 3]</code></pre>
<p>이라는 <code>1 × 3</code> 행벡터가 된다.</p>
<hr>
<h1 id="단위행렬">단위행렬</h1>
<p>3차원 단위행렬은</p>
<pre><code class="language-text">[1 0 0]
[0 1 0]
[0 0 1]</code></pre>
<p>이다.</p>
<p>4차원이라면</p>
<pre><code class="language-text">[1 0 0 0]
[0 1 0 0]
[0 0 1 0]
[0 0 0 1]</code></pre>
<p>이다.</p>
<p>단위행렬은 보통 <code>I</code>로 표현한다.</p>
<p>적절한 크기의 행렬 M에 대해</p>
<pre><code class="language-text">IM = M
MI = M</code></pre>
<p>이므로 숫자에서 <code>1</code>과 비슷한 역할을 한다.</p>
<hr>
<h1 id="단위행렬을-basis-관점에서-보기">단위행렬을 Basis 관점에서 보기</h1>
<p>3 × 3 단위행렬을 다시 보자.</p>
<pre><code class="language-text">[1 0 0]
[0 1 0]
[0 0 1]</code></pre>
<p>열을 하나씩 떼어보면</p>
<pre><code class="language-text">(1, 0, 0)
(0, 1, 0)
(0, 0, 1)</code></pre>
<p>이다.</p>
<p>즉 표준 X, Y, Z Basis가 들어 있다.</p>
<p>따라서 행렬은</p>
<blockquote>
<p>여러 Basis Vector를 하나의 구조 안에 묶어 표현하는 방법</p>
</blockquote>
<p>으로도 볼 수 있다.</p>
<hr>
<h1 id="basis가-회전하면-행렬도-달라진다">Basis가 회전하면 행렬도 달라진다</h1>
<p>2D의 기본 Basis를 생각해보자.</p>
<pre><code class="language-text">X = (1, 0)
Y = (0, 1)</code></pre>
<p>이 축들을 90도 회전시켜</p>
<pre><code class="language-text">X&#39; = (0, 1)
Y&#39; = (-1, 0)</code></pre>
<p>을 얻었다고 하자.</p>
<p>두 벡터를 <strong>열에 넣는 convention</strong>을 사용한다면 행렬은</p>
<pre><code class="language-text">[ 0 -1 ]
[ 1  0 ]</code></pre>
<p>이 된다.</p>
<p>첫 번째 열은 새로운 X축,</p>
<p>두 번째 열은 새로운 Y축을 나타낸다.</p>
<p>즉 행렬을 통해</p>
<blockquote>
<p>새로운 좌표계의 각 축이 기존 좌표계에서 어느 방향을 향하는지</p>
</blockquote>
<p>표현할 수 있다.</p>
<hr>
<h1 id="행렬이-벡터를-변환하는-이유">행렬이 벡터를 변환하는 이유</h1>
<p>벡터</p>
<pre><code class="language-text">V = (3, 2)</code></pre>
<p>가 있다고 하자.</p>
<p>좌표 <code>(3, 2)</code>는 사실</p>
<pre><code class="language-text">3X + 2Y</code></pre>
<p>라는 의미다.</p>
<p>이번에는 행렬 안에 새로운 Basis가 들어 있다고 해보자.</p>
<pre><code class="language-text">M = [ E1 E2 ]</code></pre>
<p>열벡터 convention에서</p>
<pre><code class="language-text">MV</code></pre>
<p>를 계산하면 행렬은 V의 좌표값을 이용해</p>
<pre><code class="language-text">3E1 + 2E2</code></pre>
<p>를 계산한다.</p>
<p>즉,</p>
<blockquote>
<p>행렬과 벡터의 곱은 행렬 안의 열벡터들을 입력 벡터의 좌표값으로 선형 결합하는 연산</p>
</blockquote>
<p>으로 이해할 수 있다.</p>
<hr>
<h1 id="스케일-행렬-예제">스케일 행렬 예제</h1>
<p>다음 Basis가 있다고 하자.</p>
<pre><code class="language-text">E1 = (2, 0)
E2 = (0, 3)</code></pre>
<p>이를 열로 넣으면</p>
<pre><code class="language-text">M =
[2 0]
[0 3]</code></pre>
<p>이다.</p>
<p>그리고</p>
<pre><code class="language-text">V = (4, 5)</code></pre>
<p>를 곱하면</p>
<pre><code class="language-text">MV = 4E1 + 5E2</code></pre>
<p>이므로</p>
<pre><code class="language-text">4(2,0) + 5(0,3)
= (8,15)</code></pre>
<p>가 된다.</p>
<p>원래 벡터</p>
<pre><code class="language-text">(4,5)</code></pre>
<p>가</p>
<pre><code class="language-text">(8,15)</code></pre>
<p>로 변했으므로 이 행렬은 결과적으로</p>
<pre><code class="language-text">X 방향 2배
Y 방향 3배</code></pre>
<p>의 스케일 변환을 수행한다.</p>
<p>여기서 중요한 점은</p>
<blockquote>
<p>Basis를 담은 행렬이라는 관점과 벡터를 변환하는 행렬이라는 관점은 서로 별개의 이야기가 아니다.</p>
</blockquote>
<p>행렬 안의 Basis들이 어디를 향하고 얼마나 긴지가 입력 벡터를 어떻게 변환할지를 결정한다.</p>
<hr>
<h1 id="행렬의-행과-열-convention">행렬의 행과 열 Convention</h1>
<p>지금까지 설명에서는 벡터를 <strong>열벡터</strong>로 두고</p>
<pre><code class="language-text">M × V</code></pre>
<p>형태로 계산한다고 가정했다.</p>
<p>이 경우 Basis Vector들이 행렬의 <strong>열</strong>에 들어간다.</p>
<p>하지만 엔진, API, 수학 라이브러리에 따라</p>
<pre><code class="language-text">행벡터
V × M</code></pre>
<p>방식을 사용하는 경우도 있다.</p>
<p>이때는 Basis의 배치나 곱셈 순서가 달라질 수 있다.</p>
<p>따라서 특정 행이나 열 위치만 외우기보다는</p>
<blockquote>
<p>어떤 convention을 사용하는지 먼저 확인하는 것이 중요하다.</p>
</blockquote>
<hr>
<h1 id="9일차-정리">9일차 정리</h1>
<p>행렬은 단순한 숫자 표가 아니다.</p>
<p>특히 게임 수학에서는 행렬을 다음과 같이 볼 수 있다.</p>
<blockquote>
<p>여러 Basis Vector를 하나의 구조에 저장한 것</p>
</blockquote>
<p>그리고 행렬과 벡터를 곱하면</p>
<blockquote>
<p>입력 벡터의 좌표값을 이용해 행렬 안의 Basis를 선형 결합한다.</p>
</blockquote>
<p>따라서 행렬은 좌표계를 표현할 수도 있고, 벡터를 회전·스케일 같은 방식으로 변환하는 도구로도 사용할 수 있다.</p>
<p>이후 배우게 될 회전 행렬, 스케일 행렬, 좌표계 변환도 결국 이 개념에서 이어진다.</p>
<hr>
<h1 id="2주차-전체흐름">2주차 전체흐름</h1>
<p>지금까지 배운 내용을 하나로 연결하면 다음과 같다.</p>
<pre><code class="language-text">선형 결합
↓
여러 벡터를 몇 배씩 해서 더한다.

Span
↓
그 선형 결합으로 만들 수 있는 모든 방향을 생각한다.

선형 독립
↓
각 벡터가 새로운 방향을 제공하는지 판단한다.

Basis
↓
공간 전체를 표현할 수 있는 최소한의 독립적인 방향들을 정한다.

좌표
↓
Basis를 각각 몇 배해야 원하는 벡터가 되는지를 숫자로 표현한다.

행렬
↓
여러 Basis Vector를 하나의 구조 안에 저장한다.

행렬 × 벡터
↓
벡터의 좌표값을 이용해 Basis를 선형 결합한다.

변환
↓
Basis의 방향이나 길이를 바꾸면 회전이나 스케일 같은 변환이 된다.</code></pre>
<p>결국 선형 결합, Basis, 좌표, 행렬은 따로 떨어진 개념이 아니라 하나의 흐름으로 연결되어 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[게임 수학 공부]]></title>
            <link>https://velog.io/@kyu_/%EA%B2%8C%EC%9E%84-%EC%88%98%ED%95%99-%EA%B3%B5%EB%B6%80</link>
            <guid>https://velog.io/@kyu_/%EA%B2%8C%EC%9E%84-%EC%88%98%ED%95%99-%EA%B3%B5%EB%B6%80</guid>
            <pubDate>Fri, 04 Sep 2026 09:09:20 GMT</pubDate>
            <description><![CDATA[<h1 id="1일차">1일차</h1>
<h2 id="목표">목표</h2>
<blockquote>
<p>3D 모델 하나가 GPU에 들어가서 모니터의 픽셀이 되기까지 무슨 일이 일어나는지 설명할 수 있다.</p>
</blockquote>
<h2 id="렌더링-파이프-라인">렌더링 파이프 라인</h2>
<pre><code>3D 모델의 정점
   ↓
정점 변환
   ↓
화면에 보이는 부분만 남김
   ↓
삼각형을 픽셀 후보들로 변환
   ↓
각 픽셀의 색 계산
   ↓
깊이/스텐실 등의 검사
   ↓
이미지 버퍼에 기록
   ↓
화면 출력</code></pre><p>게임에서 메시 하나를 그린다고 했을때 대략 이런 과정을 거침
이것을 크게 세부분으로 나눈다
그래픽 처리기 -&gt; 정점 변환 -&gt; 레스터화와 단편 연산</p>
<hr>
<h2 id="gpu">GPU</h2>
<blockquote>
<p>GPU는 무엇을 받는가??</p>
</blockquote>
<p>GPU입장에서는 기본적으로 정점(Vertex)과 정점들이 어떻게 연결되어 있는가가 중요</p>
<pre><code>       V0
       /\
      /  \
     /    \
   V1------V2</code></pre><p>삼각형 하나라면 정점 세개가 있고 이 세개를 연결해 삼각형을 만든다. 3D 모델은 이런 삼각형이 아주 많이 모여있는 형태라고 생각하면 된다.</p>
<p>수천~수만개가 보이면 메시가 되고, 이때 3차원 벡터가 등장한다.</p>
<hr>
<h2 id="cpu와-gpu는-왜-나누어져있나">CPU와 GPU는 왜 나누어져있나</h2>
<p>CPU는 GPU에게</p>
<pre><code>이 메시 그려
이 텍스처 사용해
이런 설정으로 렌더링해</code></pre><p>같은 렌더링 작업을 전달하면 GPU가 그 작업을 진행한다.
이는 비동기적으로 작동한다 (CPU와 GPU를 억지로 계속 동기화하면 성능이 떨어진다)</p>
<pre><code>CPU
 │
 │ &quot;이거 렌더링해&quot;
 ▼
GPU ───── 렌더링 중 ─────→
 │
 │
CPU ───── 다른 게임 로직 수행 ─────→</code></pre><hr>
<h2 id="vram">VRAM</h2>
<p>GPU에도 사용할 메모리가 필요하다.
VRAM에는 여러 그래픽 데이터가 들어가는데 대표적으로</p>
<pre><code>텍스처
이미지 버퍼
깊이 버퍼
스텐실 버퍼</code></pre><p>등이있다.</p>
<hr>
<h2 id="front-buffer와-back-buffer">Front Buffer와 Back Buffer</h2>
<p>GPU가 화면을 그린다고 해서 모니터에 직접 픽셀 하나씩 바로 그리는 방식은 아니다.</p>
<p>전면 이미지 버퍼(Front Buffer)와 후면 이미지 버퍼(Back Buffer)가 있다.</p>
<p>Front Buffer는</p>
<blockquote>
<p>현재 사용자에게 보여주고 있는 화면</p>
</blockquote>
<p>Back Buffer는</p>
<blockquote>
<p>GPU가 다음 화면을 그리고 있는 곳</p>
</blockquote>
<p>이라고 생각하면 됨
Back Buffer가 완성되면 두 버퍼를 교체하는데 이를 Buffer Swap이라고 한다.</p>
<hr>
<h2 id="depth-buffer">Depth Buffer</h2>
<p>GPU가 단순히 픽셀 색만 그리면</p>
<pre><code>먼 물체를 나중에 그렸다는 이유로
앞의 문제를 덮어버리는 문제</code></pre><p>가 생길 수 있음 그래서 픽셀마다 깊이값을 저장한다.
Color Buffer -&gt; (120, 50, 20) : 픽셀 색상
Depth Buffer -&gt; 0.37 : 픽셀 깊이</p>
<p>개념적으로 현재 저장된 깊이 = 0.7, 새로운 픽셀 깊이 = 0.3 이렇게 새로운 픽셀이 더 앞에 있다면 그 픽셀이 살아남는다.
그래서 뒤의 물체가 앞 물체를 뚫고 보이지 않도록 만들 수 있다.</p>
<p>이미지의 각 픽셀에 대한 깊이 값을 담고, 가려진 표면을 제거하는데 사용하는 버퍼</p>
<hr>
<h2 id="stencil-buffer">Stencil Buffer</h2>
<blockquote>
<p>각 픽셀에 대해서 여기를 그릴지 말지 등을 제어하기 위한 정수 마스크</p>
</blockquote>
<p>이미지 버퍼 각 픽셀에 대한 정수 마스크, 픽셀단위의 렌더링 활성화/비활성화에 사용</p>
<hr>
<h2 id="정점-변환">정점 변환</h2>
<p>캐릭터 머리의 어떤 정점 좌표가 (0, 0, 100)이라고 했을때 이 좌표가 무엇을 기준으로 한건가 이게 중요하다.
그래서 3D모델에는 정점 좌표가 있다.</p>
<pre><code>물체 공간
    ↓
세계 곤강
    ↓
카메라 공간
    ↓
동차절단공간
    ↓
정규화된 장치 좌표
    ↓
윈도우 공간</code></pre><hr>
<h2 id="물체-공간">물체 공간</h2>
<p>Object Space</p>
<p>캐릭터 모델을 만들었다고 했을때 모델 중앙이 (0, 0, 0)이라 했을때</p>
<pre><code>          머리
        (0,0,2)
           │
           │
         몸통
        (0,0,1)
           │
           │
        (0,0,0)</code></pre><p>이 좌표는 게임 세계 전체 좌표가 아님 이 캐릭터 모델 자체를 기준으로 하는 좌표이다.</p>
<blockquote>
<p>물체 공간은 특정 모형에 국한되어 그 모형에 쓰이는 좌표계</p>
</blockquote>
<hr>
<h2 id="세계-공간">세계 공간</h2>
<p>World Space
게임 월드 전체 기준</p>
<hr>
<h2 id="camera-space">Camera Space</h2>
<p>화면을 만드려면 중요한 문제가 있다. <code>카메라가 어디있는가??</code>
세계의 모든 물체를 카메라를 기준으로 다시 표현한다.
이를 Camera Space라고 한다.</p>
<p>카메라 공간을 x, y 축이 화면과 정렬되고 z축이 시선 방향과 평행한 좌표계라고 한다.</p>
<blockquote>
<p>카메라 입장에서 이 물체는 어디에 있는가?</p>
</blockquote>
<hr>
<h2 id="projection">Projection</h2>
<p>3D에서는 (x, y, z)인데
모니터는 (x, y)이다.</p>
<p>더 중요한거는 원근감 그래서 투영변환(Projection Transformation)을 한다.
투영 변환은 카메라에서 멀수록 물체가 작게 보이게 해 장면에 원근감을 추가하는 역할</p>
<hr>
<h2 id="clipping">Clipping</h2>
<p>카메라 뒤에 있는 물체나 화면 밖에 완전히 벗어난 삼각형까지 그릴필요는 없다.</p>
<pre><code>       Camera
         📷
        /   \
       /     \
      /       \
     / Visible \
    /           \</code></pre><p>그래서 보이는 영역 밖을 잘라냄</p>
<p>동차절단공간의 절단이 이 의미와 연결됨</p>
<hr>
<h2 id="정규화된-장치-좌표">정규화된 장치 좌표</h2>
<p>화면 해상도가 컴퓨터마다 다르기때문에 처음부터 특정 픽셀 좌표로 작업하는 것보다 일정한 범위의 좌표로 표현하는 단계가 있음</p>
<p>정규화된 장치 좌표의 x, y,z를 마지막에 실제 화면 크기로 바꿈</p>
<hr>
<h2 id="window-space">Window Space</h2>
<p>정규화된 좌표를 마지막 실제 화면 좌표로 변환 FHD라고 했을때 화면 중앙이면 대충 960, 540 같은 위치가 됨
여기까지 오면 정점이 화면의 어디에 위치할 것인가가 결정됨</p>
<p>이를 위해 행렬을 배움</p>
<blockquote>
<p>Object -&gt; World -&gt; Camera</p>
</blockquote>
<hr>
<h2 id="레스터-화">레스터 화</h2>
<p>정점 세개의 안쪽을 다채우는것을 레스터화 라고 함
기본 도형의 영역을 픽셀들로 채우는 과정</p>
<blockquote>
<p>삼각형 -&gt; 픽셀 후보들</p>
</blockquote>
<hr>
<h2 id="fragment">Fragment</h2>
<p>레스터화가 되면 바로 화면에 찍는게 아님
각 픽셀 위치마다 여러 정보가 생김 이를 묶어서 단편(Fragment)라고 부름</p>
<pre><code>Fragment

화면 위치
깊이값
색
텍스처 좌표
등등...</code></pre><p>대신 Fragment는 최종 픽셀이 아니다.
아직 최종적으로 화면에 기록될지 결정되지 않음
왜냐하면</p>
<pre><code>다른 물체 뒤에 가려졌을 수도
스텐실 조건에 실패할 수도
화면 제한 영역 밖일수도있다.</code></pre><hr>
<h2 id="face-culling">Face Culling</h2>
<p>레스터화 전에 필요없는 면을 제거할 수도 있다.
상자를 앞에서 보면 뒤쪽면은 어차피 보이지 않는다. 그래서 카메라 반대쪽을 향하는 면을 제거해서 불필요한 렌더링을 줄일 수 있다.</p>
<p>이를 면 선별(Face Culling)이라고 함</p>
<blockquote>
<p>안보이는 방향의 삼각형을 미리 버릴 수 있다.</p>
</blockquote>
<hr>
<h2 id="fragment-shading">Fragment Shading</h2>
<p>각 Fragment의 색을 계산</p>
<pre><code>텍스처 색 + 빛의 세기 + 정점에서 전달된 정보</code></pre><p>이런 정보를 이용해 Fragment의 최종 색상을 계산한다.</p>
<p>단편 셰이딩(Fragment Shading) 또는 픽셀 셰이딩이라고 표현한다.</p>
<hr>
<h2 id="depth-test">Depth Test</h2>
<p>계산된 Fragment는 여러 판정을 거친다.</p>
<pre><code>Fragment
   ↓
픽셀 소유권 판정
   ↓
가위 판정
   ↓
알파 판정
   ↓
스텐실 판정
   ↓
깊이 판정
   ↓
혼합
   ↓
Image Buffer</code></pre><p>여기서 Depth Test란 같은 화면 위치에 두 Fragment가 왔다고 했을때 Depth가 더 낮은 즉 카메라에 더 가까운 Fragment만 화면에 남는다.</p>
<blockquote>
<p>3D 공간에서 어떤 물체가 앞에 있고 뒤에 있는지를 최종 픽셀에서 해결함</p>
</blockquote>
<hr>
<h2 id="blending">Blending</h2>
<p>모든 테스트를 통과했다면 색을 이미지 버퍼에 반영
그냥 덮어 쓸수도 반투명 물체라면 기존 색과 섞을 수도 있다.
이를 Blending이라고 함</p>
<hr>
<h2 id="1일차-정리">1일차 정리</h2>
<pre><code>① Object Space의 정점
        ↓
② World Space로 변환
        ↓
③ Camera Space로 변환
        ↓
④ Projection
        ↓
⑤ Clip
        ↓
⑥ 화면 좌표 결정
        ↓
⑦ Triangle Rasterization
        ↓
⑧ Fragment 생성
        ↓
⑨ Fragment 색 계산
        ↓
⑩ Depth / Stencil 등의 판정
        ↓
⑪ Image Buffer 기록
        ↓
⑫ Back Buffer 완성
        ↓
⑬ Buffer Swap
        ↓
화면에 보임</code></pre><p>줄이자면</p>
<pre><code>Vertex
  ↓
Object
  ↓
World
  ↓
Camera
  ↓
Projection</code></pre><p>벡터와 행렬은 이를 이해하기 위한 도구</p>
<hr>
<h2 id="연습문제">연습문제</h2>
<ol>
<li>3D 모델 하나가 있다고 하자. GPU 입장에서 모델은 단순히 &quot;캐릭터&quot;라는 하나의 덩어리가 아니다. 모델의 표면은 일반적으로 어떤 기본도형들의 집합으로 표현되는가?</li>
</ol>
<p>삼각형의 집합</p>
<ol start="2">
<li>Front Buffer와 Back Buffer는 각각 어떤 역할을 하는가??</li>
</ol>
<p>Front Buffer는 현재 화면에 보이는 화면 Back Buffer는 다음 프레임 화면을 그리고 BackBuffer가 다 그려지면 Buffer Swap을 통해서 화면에 출력한다.</p>
<ol start="3">
<li>화면의 동일한 픽셀 위치에 몬스터 A와 몬스터 B의 Fragment가 들어왔다. A가 카메라에 더 가깝다. 어떤 버퍼와 어떤 판정을 이용해서 A만 화면에 남도록 만들 수 있는가?</li>
</ol>
<p>Depth Buffer에 들어있는 깊이 값으로 Depth Test를 해서 값이 더 작은 즉 카메라에 더 가까운 A만 남도록한다.</p>
<ol start="4">
<li>캐릭터 머리 정점의 좌표가 (0, 0, 100)이라고 하자. 이 값만 보고 &quot;게임 월드의 (0,0,100)에 머리가 있다&quot;고 단정할 수 없는 이유를 Object Space와 World Space를 사용해서 설명해봐.</li>
</ol>
<p>기준이 어떤 값이냐에 따라 다르기 때문 Object Space의 경우에는 물체 기준으로 좌표를 매김, World Space는 세계 좌표를 기준으로 하기때문</p>
<ol start="5">
<li>다음 순서를 올바르게 배열</li>
</ol>
<pre><code>Object Space
World Space
Camera Space
Projection / Clip
Window Space</code></pre><ol start="6">
<li>래스터화가 하는 일을 한 문장으로 설명해봐. 정점, 삼각형, 픽셀이라는 단어 중 적어도 두 개를 사용하면 된다.</li>
</ol>
<p>레스터화는 삼각형의 정점만 있을때 그 내부 픽셀을 채우는것을 말한다.</p>
<p>개선</p>
<blockquote>
<p>화면에 투영된 삼각형이 어떤 픽셀 위치들을 덮는지를 계산해서 Fragment들을 생성하는 과정</p>
</blockquote>
<ol start="7">
<li>Fragment와 최종 Pixel이 완전히 같은 것이라고 할 수 없는 이유는 무엇인가?</li>
</ol>
<p>Fragment를 검증하는 단계가 더 있기 때문이다 대표적으로 오늘배운 Depth Test가 있다.</p>
<ol start="8">
<li>게임에서 캐릭터가 벽 뒤에 완전히 가려져 있는데도 캐릭터 색이 벽 위에 그대로 그려진다고 하자. 오늘 배운 개념 중 가장 먼저 의심해볼 부분은 무엇인가?</li>
</ol>
<p>Projection이 의심? 혹은 Clipping이 의심된다.</p>
<p>오답</p>
<blockquote>
<p>벽 뒤에 캐릭터가 벽 위에 그대로 그려지고 있다면 의심할것은 Depth Test/Depth Buffer다. 왜냐면 이미 캐릭터가 화면에 투영되는것 자체는 성공한 상태이기 때문, Projection은 3D위치를 화면에 어떻게 투영할까? 이고 Clipping은 카메라가 보는 영역 밖의 물체를 어떻게 잘라낼까?</p>
</blockquote>
<ol start="9">
<li>가장 중요한 문제. 아래 빈칸을 네 말로 채워봐.</li>
</ol>
<p>Object Space, World Space, Camera Space, Window Space</p>
<ol start="10">
<li>마지막으로 책을 보지 말고 &quot;3D 캐릭터 하나가 화면에 나타나기까지의 과정&quot;을 5~10줄 정도로 설명해봐.</li>
</ol>
<blockquote>
<p>3D캐릭터는 여러 정점과 그 정점들로 이루어진 삼각형 메시로 구성된다. 먼저 캐릭터의 정점들은 캐릭터 자신을 기준으로 하는 Object Space에 존재한다. 이 정점들에 캐릭터의 위치, 회전, 크기 등의 변환을 적용해서 World Space좌표로 바꾼다. 그 후 카메라를 기준으로 Camera Space로 변환한다.
이후 Projection을 통해 3D좌표를 화면에 표시할 수 있는 형태로 변환하고, 카메라가 볼 수 있는 영역 밖의 부분은 Clipping한다. 변환된 삼각형은 Rasterization을 통해 화면에서 차지하는 Fragment들로 만들어진다. 각 Fragment에 대해 색과 텍스처등의 값을 계산하고 Depth Test나 Stencil Test같은 판정을 수행한다. 살아남은 Fragment의 색을 이미지 버퍼에 기록한다. GPU가 Back Buffer에 한 프레임을 모두 그리면 Buffer Swap을 통해 완성된 이미지가 화면에 출력된다.</p>
</blockquote>
<hr>
<h1 id="2일차">2일차</h1>
<h2 id="목표-1">목표</h2>
<blockquote>
<p>벡터가 무엇인지, 벡터끼리 더하고 빼는것이 무슨 의미인지, 벡터의 길이를 어떻게 구하는지, 정규화가 무엇이고 왜 하는지를 설명할 수 있다.</p>
</blockquote>
<hr>
<h2 id="벡터란-무엇인가">벡터란 무엇인가?</h2>
<p>벡터를 기본적으로 여러개의 실수를 하나로 묶은것을 정의한다.</p>
<pre><code class="language-c++">FVector PlayerPosition(100, 200 ,50);</code></pre>
<p>숫자 세개로 3D공간의 무언가를 표현할 수 있다.
반대로 방향으로 해석할 수도 있다.</p>
<pre><code class="language-c++">FVector Forward(1, 0, 0);</code></pre>
<p>벡터는 크게 두가지 정보가 있다고 생각하면 편하다 어느 방향인가 + 얼마나 긴가</p>
<hr>
<h2 id="벡터-덧셈">벡터 덧셈</h2>
<p>두 벡터를 더할때는 같은 성분끼리 그냥 더함
게임에서는 현재 위치에 이동량을 추가한다 라고 이해하면 된다.</p>
<hr>
<h2 id="벡터-뺄셈">벡터 뺄셈</h2>
<p>얘도 성분끼리 빼는건 같다.
플레이어 P = (2, 3, 0) 이라고하고
적이 E = (7, 5, 0) 이라고 했을때</p>
<p>E - P = (5, 2, 0)은 플레이어에서 적까지 얼마나, 어느 방향으로 이동해야 하는가를 뜻한다.
게임에서도 Target - Current는 결론적으로 Current에서 Target으로 향하는 벡터를 만드는구나라고 생각하면 된다.</p>
<hr>
<h2 id="스칼라란">스칼라란?</h2>
<p>벡터와 함께 등장하는 스칼라는 그냥 숫자 하나라고 생각
스칼라는 벡터에 곱해서 사용을 하는데 벡터의 방향이나 크기를 조절할때 사용한다.
Velocity = Direction * Speed;
만약에 Direction(1, 0, 0), Speed = 500이라고 했을때 Velocity = (500, 0, 0)이 된다.
Direction은 어느쪽으로 갈지, Speed는 얼마나 빠르게 갈지를 담당</p>
<hr>
<h2 id="벡터의-길이">벡터의 길이</h2>
<p>|V| = $\sqrt{x^{2} + y^{2} + z^{2}}$</p>
<p>벡터의 길이는 이렇게 계산함 2D개념의 피타고라스를 3D로 확장</p>
<p>당연히 위치 두개 사이의 거리도 이거를 착안해서 나옴</p>
<p>Player=(1,1,0)
Enemy=(4,5,0)</p>
<p>Enemy - Player = (3, 4, 0)</p>
<p>$\sqrt{3^{2} + 4^{2} + 0^{2}}$ = 5</p>
<p>따라서 플레이어와 적 사이의 거리는 5</p>
<pre><code>Distance = |EnemyPosition - PlayerPosition|</code></pre><hr>
<h2 id="단위벡터">단위벡터</h2>
<p>벡터의 길이가 정확히 1이면 단위벡터라고 한다
방향만 필요할때 편하게 사용</p>
<p>V=(3,4,0)
|V| = 5</p>
<p>정규화 = V / |V| -&gt; (0.6, 0.8, 0) 이런식으로 된다.</p>
<h3 id="정규화">정규화</h3>
<p>1이아닌 벡터를 단위벡터로 만드는것을 정규화라고 한다.
크기만 사라지고 방향만 남음</p>
<h3 id="정규화를-게임에서-왜-많이-쓰는가">정규화를 게임에서 왜 많이 쓰는가?</h3>
<p>예시로 플레이어 위치와 적 위치가 있는데</p>
<pre><code class="language-c++">FVector ToEnemy = EnemyPosition - PlayerPosition;</code></pre>
<p>이 상태의 ToEnemy에는 두 정보가 섞여있다..
적이 어느 방향인가? + 적이 얼마나 멀리 있는가?</p>
<p>그런데 AI에게 적 방향으로 총알을 발사해 라고 한다면 거리정보는 필요가 없다.</p>
<pre><code class="language-c++">FVector Direction = ToEnemy.GetSafeNormal();
velocity = Direction * BulletSpeed;</code></pre>
<p>방향만남기고 스피드를 곱하는 식으로
영벡터는 정규화할 수 없다 0으로 나눠야 하기때문 그래서 일반적으로 정규화하기전에 길이가 0인지 확인해야함</p>
<h3 id="헷갈리는-용어">헷갈리는 용어</h3>
<p>Normalize는 벡터의 길이를 1로 만드는 연산이고
Normal Vector는 어떤 표면에 수직인 벡터를 말함</p>
<hr>
<h2 id="연습문제-1">연습문제</h2>
<ol>
<li>벡터 V = (3, 4, 0)의 길이를 구하고, 이 벡터를 정규화한 결과도 구해봐.</li>
</ol>
<hr>
<p>5
(0.6, 0.8, 0)</p>
<ol start="2">
<li>P = (2, -1, 3), Q = (1, 4, -2)일 때 P + Q와 P - Q를 각각 계산해봐.</li>
</ol>
<hr>
<p>P+Q = (3, 3, 1)
P-Q = (1, -5, 5)</p>
<ol start="3">
<li>플레이어 위치가 (2, 3, 0), 적의 위치가 (8, 11, 0)이다.플레이어에서 적을 향하는 벡터를 구하고, 둘 사이의 거리도 구해봐.</li>
</ol>
<hr>
<p>E-P = (6, 8, 0), 거리 10</p>
<ol start="4">
<li>벡터 V=(2,0,0)에 각각 3, 0.5, -1을 곱하면 결과가 어떻게 되는가? 그리고 세 결과가 원래 V와 비교해서 방향과 길이가 어떻게 달라졌는지도 설명해봐.</li>
</ol>
<hr>
<p>(6, 0, 0)
(1, 0, 0)
(-2, 0, 0)
3배, 0.5배, -1배, -1의 경우 방향이 바뀌었음, 나머지는 길이가 늘거나 줄어듬</p>
<ol start="5">
<li>다음 두 벡터가 있다.</li>
</ol>
<pre><code>   A = (10, 0, 0)
   B = (1, 0, 0)</code></pre><p>둘은 값이 다른데도 &quot;같은 방향을 가리킨다&quot;고 말할 수 있는 이유는 무엇인가? 그리고 A를 정규화하면 무엇이 되는가?</p>
<hr>
<p>정규화하면 (1, 0, 0)이 되기때문에 둘의 방향은 같다 둘다 x에만 값이 있기도하고</p>
<ol start="6">
<li>다음 코드가 어떤 계산을 하는지 설명해봐.</li>
</ol>
<pre><code>FVector ToTarget = TargetPosition - PlayerPosition;
FVector Direction = ToTarget.GetSafeNormal();
FVector Velocity = Direction \* 500.0f;</code></pre><p>특히 ToTarget과 Direction이 가지고 있는 정보의 차이를 설명해봐.</p>
<hr>
<p>ToTarget이라는 플레이어 위치에서 타겟 위치를 향하는 벡터가 있다. 이는 방향과 얼마나 떨어져있는지가 벡터의 크기 형태로 함께들어있고, Direction은 이를 정규화해서 방향만 남기고 길이는 1로 만듬</p>
<ol start="7">
<li>(0,0,0)을 일반적인 방식으로 정규화하려 하면 왜 문제가 생기는가?</li>
</ol>
<hr>
<p>0으로 나눠야하기때문에 문제가 생긴다.</p>
<ol start="8">
<li>오늘의 핵심 문제. 총알을 플레이어 위치에서 적을 향해 속력 1000으로 발사한다고 하자. 필요한 계산 과정을 코드가 아니라 벡터 개념만 사용해서 순서대로 설명해봐.</li>
</ol>
<hr>
<ol>
<li><p>ToTarget = EnemyPosition - PlayerPosition
↓
적까지의 방향 + 거리</p>
</li>
<li><p>Normalize(ToTarget)
↓
적 방향만 남김, 길이 = 1</p>
</li>
<li><p>Direction × 1000
↓
적 방향으로 속력 1000을 가진 Velocity 생성</p>
</li>
</ol>
<hr>
<h1 id="3일차">3일차</h1>
<h2 id="목표-2">목표</h2>
<blockquote>
<ol>
<li>내적을 계산할 수 있다</li>
<li>내적의 결과가 왜 벡터가 아니라 숫자인지 안다.</li>
<li>내적의 부호로 두 방향의 관계를 판단할 수 있다.</li>
<li>게임에서 &quot;적이 내 앞에 있는가?&quot;를 내적으로 판단할 수 있다.</li>
</ol>
</blockquote>
<hr>
<h2 id="내적이란">내적이란?/</h2>
<p>P=(Px​,Py​,Pz​)
Q=(Qx​,Qy​,Qz​)</p>
<p>두 벡터의 내적은
P⋅Q=Px​Qx​+Py​Qy​+Pz​Qz​
이다.</p>
<p>P=(1,2,3)
Q=(4,5,6)</p>
<p>P⋅Q = 1×4 + 2×5 + 3×6 = 4 + 10 + 18 = 32</p>
<p>여기서 중요한것은 두 벡터를 내적하면 스칼라가 반환이 된다. 즉 숫자 하나가 반환된다.</p>
<h3 id="이-계산은-왜할까">이 계산은 왜할까?</h3>
<p>내적의 핵심 공식은</p>
<p>P⋅Q = ∥P∥∥Q∥cosθ
여기서 (\theta)는 두 벡터 사이의 각도</p>
<p>즉 내적은 두 벡터 사이의 각도와 연결이 되어있다.</p>
<h3 id="우선-단위벡터끼리-생각하자">우선 단위벡터끼리 생각하자</h3>
<p>정규화를 하면
∥P∥ = ∥Q∥ = 1</p>
<p>그러면 결국 P⋅Q = 1 x 1 x cosθ = cosθ</p>
<h3 id="같은-방향">같은 방향</h3>
<p>두 벡터가 정확히 같은 방향이면 cos 0 = 1
따라서 정규화된 두 벡터라면 내적이 1이다.
내적이 1에 가까울수록 같은 방향을 보고 있다.</p>
<h3 id="직각">직각</h3>
<p>cos90 = 0이므로 두벡터의 내적이 0이면 두 벡터는 서로 수직이다.</p>
<h3 id="반대-방향">반대 방향</h3>
<p>cos180 = −1 이므로 -1이면 완전히 반대방향</p>
<h3 id="왜-정규화를-하는가">왜 정규화를 하는가?</h3>
<pre><code>A = (1,0,0)
B = (1,0,0)
이면

A⋅B = 1

A = (100,0,0)
B = (100,0,0)
이면

A⋅B = 100×100 = 10000

벡터의 길이도 결과에 들어가버린다.
그래서 방향 관계만 보고 싶으면 정규화를 해서 확인해야 한다.</code></pre><h3 id="정리">정리</h3>
<table>
<thead>
<tr>
<th align="right">내적</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td align="right"><code>1</code></td>
<td>완전히 같은 방향</td>
</tr>
<tr>
<td align="right"><code>0보다 큼</code></td>
<td>대체로 같은 쪽</td>
</tr>
<tr>
<td align="right"><code>0</code></td>
<td>90도</td>
</tr>
<tr>
<td align="right"><code>0보다 작음</code></td>
<td>반대쪽</td>
</tr>
<tr>
<td align="right"><code>-1</code></td>
<td>완전히 반대 방향</td>
</tr>
</tbody></table>
<p>두 벡터를 정규화하면 -1 ~ 1사이로 나옴</p>
<hr>
<h2 id="예제---적이-내-앞에-있는가">예제 - 적이 내 앞에 있는가?</h2>
<p>플레이어가 오른쪽을 바라본다고 가정하고 적이 플레이어 오른쪽 앞에 있다고 했을때</p>
<pre><code>Player Forward = (1, 0)

ToEnemy = EnemyPosition - PlayerPosition;</code></pre><p>저번에 배운대로 적의 위치 벡터 - 플레이어 위치 벡터를해서 ToEnemy 벡터를 구함</p>
<pre><code>Direction = Normalize(ToEnemy)</code></pre><p>이를 정규화해서 방향만 남김</p>
<pre><code>Dot(PlayerForward, Direction)</code></pre><p>내적을해서 결과를 구함 예를들어 0.8이 나온다 하면
적이 플레이어가 바라보는쪽에 있다.
-0.7이라고 하면 적이 플레이어 뒤쪽에 있다는 뜻</p>
<h3 id="앞이라기에는-너무-넓지-않나">앞이라기에는 너무 넓지 않나?</h3>
<p>그래서 실제 게임에서는</p>
<pre><code class="language-c++">if (Dot &gt; 0.8f)
{
    // 꽤 정면에 있음
}</code></pre>
<p>이런식으로 0.8정도 값을 해서 일정 각도 안에 들어오면 시야에 들어왔다고 판단할 수도 있다.</p>
<hr>
<h2 id="결론">결론</h2>
<p>x끼리 곱하고 y끼리 곱하고 z끼리 곱해서 더한다. 는 단순 계산법</p>
<blockquote>
<p>두 벡터가 얼마나 같은 방향을 향하고 있는지를 숫자 하나로 알아내는 연산</p>
</blockquote>
<p>을 내적이라고 생각하는게 좋다.</p>
<hr>
<h1 id="4일차">4일차</h1>
<h2 id="목표-3">목표</h2>
<blockquote>
<p>내적으로 두 벡터 사이의 각도를 구할 수 있다.
벡터 투영이 무엇인지 설명할 수 있다.
어떤 이동 벡터에서 특정 방향 성분만 뽑아낼 수 있다.</p>
</blockquote>
<hr>
<h2 id="내적으로-각도-구하기">내적으로 각도 구하기</h2>
<p>A⋅B=∥A∥∥B∥cosθ
내적 식은 이랬다. 벡터 길이 x 길이 x 코사인 세타
이는 다시말하면</p>
<pre><code>cosθ = A⋅B​ / ∥A∥∥B∥</code></pre><p>가 될 수 있다.</p>
<p>여기서 세타만 남기면</p>
<pre><code>θ=cos−1(∥A∥∥B∥A⋅B​)</code></pre><p>가 될수있다. (-1승)</p>
<p>A=(1,0)
B=(0,1)
라고 했을때 내적은 0
즉, 코사인 세타는 0 / 1x1 = 0</p>
<p>θ=cos−1(0)=90∘</p>
<h3 id="dot값과-각도의-관계">Dot값과 각도의 관계</h3>
<p>정규화된 벡터 기준으로</p>
<table>
<thead>
<tr>
<th align="right">각도</th>
<th align="right">Dot</th>
</tr>
</thead>
<tbody><tr>
<td align="right">0°</td>
<td align="right">1</td>
</tr>
<tr>
<td align="right">60°</td>
<td align="right">0.5</td>
</tr>
<tr>
<td align="right">90°</td>
<td align="right">0</td>
</tr>
<tr>
<td align="right">120°</td>
<td align="right">-0.5</td>
</tr>
<tr>
<td align="right">180°</td>
<td align="right">-1</td>
</tr>
</tbody></table>
<hr>
<h2 id="벡터-투영">벡터 투영</h2>
<p>Projection, 투영이다.
벡터를 다른 벡터위에 투영</p>
<blockquote>
<p>어떤 벡터에서 원하는 방향으로 향하는 성분만 뽑아내는 것</p>
</blockquote>
<p>내적은 두벡터가 얼마나 같은 방향을 향하고 있는가를 알려준다. 그러니 P안에 Q방향이 얼마나 들어있는가? 를 계산할때 내적이 사용되는건 자연스럽다.</p>
<h3 id="예시">예시</h3>
<p>벡터 P가 벡터 Q방향으로 얼마나 들어있는지를 벡터 형태로 뽑아낸것?
번역이 조금 이상한데 예시를 보면</p>
<p>P = (3, 4)
Q = (1, 0)</p>
<p>이라고 했을때 x축방향 Q에 P를 투영하면 P에서 Q방향 성분은 당연히 (3, 0)이 된다.</p>
<hr>
<p>Q가 (0.8, 0.6)같은 대각이면 어떨까?
어떤 성분이 그 방향에 평행한지 눈으로 바로 구하기가 어렵다.
그래서 내적을 사용</p>
<p>벡터 P와 Q 사이 각도를 θ라고 하면</p>
<pre><code>P가 Q방향으로 가진 길이는
∥P∥cosθ이다.

그런데 내적 공식은 P⋅Q=∥P∥∥Q∥cosθ
양변을 |Q|로 나누면
P⋅Q / |Q| = ∥P∥cosθ가 된다.</code></pre><p>Q방향으로 투영된 P의 길이를 내적으로 구할 수 있다.</p>
<h2 id="궁금한거">궁금한거</h2>
<h3 id="p가-q방향으로-가진-길이가-왜-pcosθ-이거인가">P가 Q방향으로 가진 길이가 왜 |P|cosθ 이거인가</h3>
<pre><code>            P
           /|
          / |
         /  |
        /   |
       / θ  |
------●-----●------------→ Q
      &lt;----&gt;
      Q 방향 성분</code></pre><p>여기서 P를 Q방향으로 내려서 직각삼각형을 만듬
빗변 = P의 길이 = |P|
세타의 이웃변 = P가 Q방향으로 가지고 있는 길이
나머지변 = Q의 수직 방향의 길이</p>
<p>코사인세타는 빗변분의 이웃변, 이웃변/빗변
즉, 코사인 세타 = P의 Q방향 성분 길이 / |P|</p>
<h3 id="왜-q의-방향-단위벡터가-fracqq인가">왜 Q의 방향 단위벡터가 (\frac{Q}{|Q|})인가?</h3>
<p>Q가 (6, 8)이라고 했을때
Q의 길이는 10, 현재 길이가 10이니까 전체를 10으로 나누면</p>
<p>Q / 10 = (0.6, 0.8)
이 벡터의 길이는 $\sqrt{0.6^{2} + 0.8^{2}}$ = 1
그래서 Q / |Q|는 Q와 같은 방향을 바라보면서 길이만 1로만든 단위벡터</p>
<hr>
<h1 id="5일차">5일차</h1>
<h2 id="목표-4">목표</h2>
<blockquote>
<p>외적을 하면 무엇이 나오는가
외적을 어떻게 계산하는가
왜 결과가 두 벡터에 수직인가?
왜 A x B와 B x A의 방향이 반대인가?</p>
</blockquote>
<hr>
<h2 id="내적과-외적의-차이">내적과 외적의 차이</h2>
<p>내적은 숫자 하나가 반환된다.</p>
<pre><code>A · B  → 숫자 하나</code></pre><p>A · B = 0이면 두 벡터가 90도라는걸 알 수 있었다.</p>
<p>반면 외적은</p>
<pre><code>A x B -&gt; 새로운 벡터

즉,

내적은 두방향을 비교한다 -&gt; 스칼라
외적은 두 방향으로부터 새로운 방향을 만든다 -&gt; 벡터</code></pre><blockquote>
<p>외적의 핵심은 둘다에 수직인 방향을 구한다 이다.</p>
</blockquote>
<hr>
<h2 id="게임-예시">게임 예시</h2>
<pre><code>B
↑
│
│
A ──────→ C</code></pre><p>A에서 C로 향하는 벡터와, A에서 B로 향하는 벡터가 있다.
두 벡터 모두 바닥에 붙어있다.</p>
<p>그렇다면 바닥에서 수직으로 튀어나오는 방향이 하나 필요하다.</p>
<p>이 삼각형의 앞면이 어디인가?
빛을 어느 방향으로 반사해야 하는가?
이 표면의 Normal은 무엇인가?</p>
<p>이때 외적을 사용한다.
두벡터가 만드는 <code>평면</code>에 수직인 벡터를 만들어주는 것</p>
<hr>
<h2 id="외적-계산-공식">외적 계산 공식</h2>
<p>A×B=(Ay​Bz​−Az​By​,Az​Bx​−Ax​Bz​,Ax​By​−Ay​Bx​)</p>
<p>외적또한 방향뿐아니라 크기에도 의미가 있다.</p>
<p>외적의 크기 -&gt; ∣A×B∣=∣A∣∣B∣sinθ</p>
<pre><code>       B
      /|
     / |
    /  | height
   / θ |
  /____|
     A</code></pre><p>A의 길이 |A|, B에서 A에 수직인 높이는 |B|sinθ</p>
<p>외적의 방향은 두 벡터에 수직인 방향이고
외적의 크기는 두 벡터가 만드는 평행사변형의 넓이이다.</p>
<hr>
<h2 id="오른손-법칙">오른손 법칙</h2>
<p>X, Y축 모두에 수직인 방향은 생각해보면 +Z, -Z축 두개가 있다.
이때 오른손 법칙이 등장한다.
오른손으로 A에서 B쪽으로 손가락을 감았을때 엄지가 향하는쪽이 외적방향이다.</p>
<p>A x B = -(B x A)가 성립한다.</p>
<h2 id="평행한-벡터-외적">평행한 벡터 외적</h2>
<p>A = (1, 0, 0)
B = (3, 0, 0)</p>
<p>외적 크기는
|AxB| = |A||B|sin0∘</p>
<p>근데 sin0은 0이기때문에
두벡터가 평행하면 영벡터가 된다. A x B = (0, 0, 0)</p>
<pre><code>────────────→
───────────────→</code></pre><p>두벡터가 이렇게 평행하게 있을때 두 벡터가 만드는 평행사변형은 높이가 0이다 그래서 넓이, 크기 모두 0</p>
<hr>
<h2 id="unreal에서">Unreal에서</h2>
<pre><code class="language-c++">FVector A(2.f, 0.f, 0.f);
FVector B(0.f, 3.f, 0.f);

FVector Cross = FVector::CrossProduct(A, B);
FVector Normal = Cross.GetSafeNormal();</code></pre>
<p>같은 형태로 볼 수 있다.</p>
<hr>
<h2 id="결론-1">결론</h2>
<p>결과가 벡터다.
결과는 A, B 둘다에 수직
크기는 A벡터의 크기 곱하기 B벡터의 크기 곱하기 sin세타
외적은 순서가 중요하다
평행하면 0이다</p>
<pre><code>       C
      / \
     /   \
    A-----B</code></pre><p>B-A는 A에서 B로가는 방향과 길이 벡터
C-A는 A에서 C로가는 방향과 길이 벡터를 말한다.</p>
<hr>
<h1 id="6일차">6일차</h1>
<h2 id="목표-5">목표</h2>
<blockquote>
<p>삼각형에서 Normal을 만드는 방법
정점 순서를 바꾸면 왜 Normal이 뒤집히는지
외적으로 삼각형 넓이를 구하는 방법
플레이어 기준으로 대상이 왼쪽/오른쪽인지 판정하는 방법</p>
</blockquote>
<hr>
<h2 id="삼각형은-normal은-왜-필요한가">삼각형은 Normal은 왜 필요한가?</h2>
<p>3D모델은 결국 수많은 삼각형으로 이루어져 있다.</p>
<pre><code>       P2
       *
      / \
     /   \
    /     \
P0 *-------* P1</code></pre><p>삼각형이 공간에서 어느 방향을 보고 있는지 알고 싶다.
예를들어 빛이 들어왔을때</p>
<pre><code>Light
   ↓
   ↓
---------
Triangle
   ↑
 Normal</code></pre><p>빛과 표면의 방향 관계를 계산하려면 표면에 수직인 방향이 필요하다.
그게 <code>Normal</code>이다</p>
<hr>
<h2 id="점세개만-가지고-normal을-어떻게-만들까">점세개만 가지고 Normal을 어떻게 만들까??</h2>
<p>삼각형의 세점이 p0, p1, p2라고 했을때
점 자체끼리 바로 외적하는게 아님, 먼저 삼각형의 두 변을 만든다.
E1 = P1 - P0, E2 = P2 - P0
여기서 P1 - P0은 P0에서 P1로 가는 벡터</p>
<pre><code>        P2
        *
       ↗
      / E2
     /
P0 *────────→ * P1
         E1</code></pre><p>따라서 E1과 E2는 삼각형 표면위에 있다.
그러면 E1 x E2를 하면 둘 모두에 수직인 벡터가 나온다.</p>
<p>삼각형이 놓인 평면의 법선을 N = (P1 - P0) x (P2 - P0) 형태로 구함</p>
<hr>
<h2 id="예시-1">예시</h2>
<p>P0 = (0, 0, 0)
P1 = (4, 0, 0)
P2 = (0, 3, 0)</p>
<p>E1 = P1 - P0 = (4, 0, 0)
E2 = P2 - P0 = (0, 3, 0)</p>
<p>외적 E1 x E2 = (4, 0, 0) x (0, 3, 0) = (0, 0, 12)
즉, 표면에서 +Z 방향으로 튀어나온다.
길이가 12라서 조명등에 사용할 방향만 필요하면 정규화해서 N = (0, 0, 12) / 12 = (0, 0, 1)</p>
<hr>
<h2 id="raw-normal과-unit-normal을-구분하자">Raw Normal과 Unit Normal을 구분하자</h2>
<p>게임 코드에서는 보통 조명, 반사, 충돌 방향등에 사용할때 단위 Normal을 원한다.</p>
<hr>
<h2 id="정점-순서가-왜-중요할까">정점 순서가 왜 중요할까?</h2>
<p>(P1 - P0) x (P2 - P0) 는 +Z가 나왔었다.
그런데 두번째 세번째 정점을 바꾸면 (P2 - P0) x (P1 - P0) = (0, 3, 0) x (4, 0, 0) 이면 (0, 0, -12)가 된다.
왜냐면 외적은 A x B = -(B x A) 이기때문에</p>
<hr>
<h2 id="외적의-크기는-삼각형-상태도-알려준다">외적의 크기는 삼각형 상태도 알려준다.</h2>
<p>삼각형 세점이</p>
<pre><code>P0 ------ P1 ------ P2</code></pre><p>이렇게 한직선위에있다고 가정해보자</p>
<p>E1 x E2 = 0이고 Area = 0이기 때문에 이 삼각형은 실제 면을 만들지 못한다.
이를 Degenerate Triangle 즉, 퇴화 삼각형이라고 한다.
게임/그래픽 코드에서 외적 크기가 거의 0인지 검사하면</p>
<blockquote>
<p>이 세점이 제대로 된 삼각형을 만드는가?</p>
</blockquote>
<p>를 확인하는데 사용할 수 있다.</p>
<hr>
<h2 id="외적으로-왼쪽오른쪽을-판정할-수-있다">외적으로 왼쪽/오른쪽을 판정할 수 있다.</h2>
<p>플레이어가 +X방향을 보고있다고 가정</p>
<pre><code>Forward = +X
Right = +Y
Up = +Z</code></pre><p>라고 했을때 플레이어 Forward F = (1, 0, 0)
적이 플레이어의 오른쪽에 있다면 T = (0, 1, 0)</p>
<p>외적 F x T = (1, 0, 0) x (0, 1, 0) = (0, 0, 1) -&gt; 즉 +Z
반대로 적이 왼쪽이면 T = (0, -1, 0), F x T = (1, 0, 0) x (0, -1, 0) = (0, 0, -1) -&gt; -Z</p>
<p>따라서 이 상황에서는 Cross의 Z &gt; 0 오른쪽, Z &lt; 0 왼쪽 -&gt; 이렇게 판단할 수 있다.</p>
<hr>
<h2 id="정리-1">정리</h2>
<p>삼각형의 Normal</p>
<p>E1​=P1​−P0​
E2​=P2​−P0​
N=E1​×E2​</p>
<p>방향만 필요하면 Normalize</p>
<hr>
<p>정점 순서</p>
<p>E1 x E2 = -(E2 x E1)
정점 순서를 뒤집으면 Normal도 뒤집힌다.</p>
<hr>
<p>삼각형 넓이는</p>
<p>Area = 1/2 * |E1 x E2|</p>
<hr>
<p>퇴화 삼각형</p>
<p>|E1 x E2| = 0
0에 가까울수록, 삼각형의 면적이 거의 0이다</p>
<hr>
<p>좌우 판정</p>
<p>Side=(Forward×ToTarget)⋅Up</p>
<ol>
<li>(3, 0, 0), (0, 2, 0), (0, 0, 6), (0, 0, 1)</li>
<li>-Z를 바라본다. 외적은 A x B = -(B x A) 이기 때문에</li>
<li>10, 외적해서 (0, 0, 20) 의 길이 20 을 반으로 (삼각형이니까)</li>
<li>E1, E2벡터를 구함 P0에서 P1, P2로 향하는 벡터, 그것을 외적을 한다. 둘다에 수직인 벡터를 구함 이의 크기를 구하면 이것은 삼각형이 아니고 평행사변형이다. 이를 반으로 나누면 삼각형의 넓이</li>
<li>같은 방향에 있기때문에 사실상 변을 구할수가없다 외적해도 0이 나오므로 크기가 0</li>
<li>음수 -Z 즉 왼쪽이다.</li>
<li>같은 결과가 이니다 방향이다름 위에서 했던 외적의 성질때문</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[C++ 핵심 개념 복습: 메모리, 다형성, 동시성, 멀티스레드]]></title>
            <link>https://velog.io/@kyu_/C-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90-%EB%B3%B5%EC%8A%B5-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A4%ED%98%95%EC%84%B1-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%93%9C</link>
            <guid>https://velog.io/@kyu_/C-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90-%EB%B3%B5%EC%8A%B5-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%8B%A4%ED%98%95%EC%84%B1-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%93%9C</guid>
            <pubDate>Sun, 30 Aug 2026 15:03:41 GMT</pubDate>
            <description><![CDATA[<h1 id="cs">CS</h1>
<h2 id="문제-1">문제 1</h2>
<p>멀티코어 환경에서 두 스레드가 다음 코드를 실행한다고 해보겠습니다.</p>
<pre><code class="language-c++">int count = 0;

// Thread A
count++;

// Thread B
count++;</code></pre>
<p>CPU에는 캐시 일관성(Cache Coherence)을 유지하기 위한 기능이 있는데도 count의 최종 결과가 항상 2가 된다고 보장할 수 없습니다.</p>
<p>캐시 일관성이 있는데도 왜 이런 문제가 발생하는지 설명해주세요.</p>
<hr>
<p><code>count++</code>은 한 번의 원자적 연산이 아니고</p>
<ol>
<li>count 값을 메모리/캐시에서 읽음</li>
<li>값에 1을 더함</li>
<li>결과를 다시 씀</li>
</ol>
<p>캐시 일관성은 “같은 메모리 주소의 값이 각 CPU 캐시에서 서로 엉뚱하게 유지되지 않도록” 해주는 기능
<code>읽기 → 증가 → 쓰기</code>라는 여러 연산을 한 덩어리로 보호해주는 기능은 아님</p>
<p>캐시 일관성 : 최신 값을 서로 맞춰주는 것
원자성 : 여러 단계를 다른 스레드가 끼어들 수 없는 하나의 연산처럼 처리하는 것</p>
<h2 id="문제-2">문제 2</h2>
<p>이 count++ 문제를 std::atomic<int>로 바꾸면 왜 해결될까요?</p>
<pre><code class="language-c++">std::atomic&lt;int&gt; count = 0;

count++;</code></pre>
<p><code>atomic</code>이 일반 int와 무엇이 다른지 설명</p>
<hr>
<p>atomic은 읽기 -&gt; 증가 -&gt; 쓰기 전체를 하나의 원자적 연산으로 처리함
일반 int는 읽기 -&gt; 증가 -&gt; 쓰기 사이에 다른 스레드가 끼어들 수 있지만, std::atomic<int> count; 로 atomic 지정하면 증가 연산에 다른 스레드가 중간에 끼어들 수 없음</p>
<h2 id="문제-3">문제 3</h2>
<p>그럼 std::atomic을 사용하면 항상 lock 없이 동작한다고 볼 수 있을까요?</p>
<p>예를 들어:</p>
<pre><code class="language-c++">std::atomic&lt;int&gt; a;
std::atomic&lt;MyStruct&gt; b;</code></pre>
<p>둘 다 무조건 lock-free일까요?</p>
<hr>
<p>atomic은 연산의 원자성을 보장하지만, 반드시 lock-free로 구현된다는 의미는 아니다.
CPU와 플랫폼, 타입에 따라 CPU의 원자적 명령으로 lock-free하게 구현될 수도 있고, 내부적으로 lock을 사용할수도 있다.</p>
<p>mutex(lock)을 사용하면</p>
<pre><code class="language-c++">mutex.lock();
count++;
mutex.unlock();</code></pre>
<p>개념적으로는 ThreadA가 사용중일때 ThreadB가 접근하려고 할때 기다림
ThreadA는 count++하고 unlock 그때 ThreadB가 접근</p>
<p>atomic을 사용하면</p>
<pre><code class="language-c++">std::atomic&lt;int&gt; count = 0;
count++;</code></pre>
<p>여기서 보장되는거는 count++연산이 원자적으로 처리된다.
CPU가 이런 연산을 직접 지원하면 CPU의 원자적 명령을 사용할 수 있음</p>
<pre><code class="language-c++">std::atomic&lt;int&gt;
       ↓
CPU가 지원하는 atomic instruction
       ↓
lock 없이 처리 가능</code></pre>
<p>이런것을 lock-free라고 함</p>
<p>CPU가 해당 타입을 원자적으로 처리하지 못하면 (예를들어 아주 큰 구조체라고 한다면)
구현체가 내부적으로 lock을 사용할수도 있다.</p>
<pre><code class="language-c++">struct BigData
{
    long long a;
    long long b;
    long long c;
};

std::atomic&lt;BigData&gt; data;</code></pre>
<pre><code>std::atomic&lt;BigData&gt;
       ↓
CPU만으로 처리하기 어려움
       ↓
라이브러리 내부에서 lock 등을 이용
       ↓
그래도 사용자에게는 atomic으로 보장</code></pre><p>결론 :</p>
<p>atomic이 반드시 lock을 사용한다 -&gt; X
atomic은 절대 lock을 사용하지 않는다 -&gt; X</p>
<h2 id="문제-4">문제 4</h2>
<pre><code class="language-c++">class Resource
{
public:
    Resource()
    {
        data = new int[100];
    }

    ~Resource()
    {
        delete[] data;
    }

private:
    int* data;
};

Resource a;
Resource b = a;</code></pre>
<p>이 코드에서 어떤 문제가 생길 수 있는가??
왜 그런 문제가 생기는가??</p>
<hr>
<p>기본 복사 생성자가 호출되고, 멤버를 그대로 복사한다. 그때 얕은 복사가 발생되고 스코프가 끝날때
b가 소멸되면 delete[] data; 가 호출
a가 소멸될때 같은 data를 또 delete[]; 같은 메모리를 두번 해제하는 undefined behavior가 발생한다.
그래서 직접 깊은 복사를 구현하거나 복사를 금지해야한다.</p>
<pre><code class="language-c++">class Resource
{
public:
    Resource()
    {
        data = new int[100];
    }

    // 복사 생성자 구현
    Resource(const Resource&amp; other){
        data = new int[100];
        for (int i = 0; i &lt; 100; ++i){
            data[i] = other.data[i];
        }
    }

    ~Resource()
    {
        delete[] data;
    }

private:
    int* data;
};</code></pre>
<h2 id="문제-5">문제 5</h2>
<p>C++의 Rule of Five에 대해서 설명</p>
<hr>
<p>소멸자
복사 생성자 -&gt; Resource(const Resource&amp;)
복사 대입 연산자 -&gt; operator=(const Resource&amp;)
이동 생성자 -&gt; Resource(Resource&amp;&amp;)
이동 대입 연산자 -&gt; operator=(Resource&amp;&amp;)</p>
<p>복사 대입연산자를 만들어 본다면</p>
<pre><code class="language-c++">Resource&amp; operator=(const Resource&amp; other){
    if (this == &amp;other){
        return *this;
    }

    for (int i = 0; i &lt; 100; ++i){
        data[i] = other.data[i];
    }

    return *this;
}</code></pre>
<p>여기서 하나 헷갈리는게 있는데 &amp;&amp;의 경우 &amp;와 차이점이 있다.
T&amp;&amp;는 <code>이 객체는 이제 이동해도 되는 값으로 받는다</code>는 의미이다.</p>
<pre><code class="language-c++">Resource&amp; // 기존 객체를 계속 사용할 값으로 받음

Resource a;
Resource&amp; ref = a;

---

Resource&amp;&amp; // 이동해도 되는 값으로 받음
Resource&amp;&amp; ref = std::move(a);</code></pre>
<p>Resource&amp;&amp;가 rvalue reference, move는 a자체를 옮기는게 아니라 이동가능한 값처럼 취급해도 된다는 의미이고 이를 받는게 &amp;&amp;
이걸 쓰는 이유는 Resource가 엄청 큰값이라고 했을때 이를 굳이 복사해서 사용할 필요가없기때문 (만약에 기존 a를 앞으로 안쓴다면)</p>
<h2 id="문제-6">문제 6</h2>
<pre><code class="language-c++">class Resource
{
public:
    Resource() {}

    Resource(const Resource&amp; other)
    {
        cout &lt;&lt; &quot;Copy&quot;;
    }

    Resource(Resource&amp;&amp; other)
    {
        cout &lt;&lt; &quot;Move&quot;;
    }
};

Resource MakeResource()
{
    Resource r;
    return r;
}

int main()
{
    Resource a;
    Resource b = a;
    Resource c = std::move(a);
    Resource d = MakeResource();
}</code></pre>
<p>d는 어떻게 동작할까??</p>
<hr>
<p>복사생성자가 호출될거같지만 컴파일러가 NRVO(Named Return Value Optimization)를 적용하면 r을 따로 만들고 d로 옮기는게 아니고 d가 들어갈 메모리에 바로 Resource를 생성한다.
C++17부터 이런 경우는 guaranteed copy elision이라서 복사/이동 없이 결과 객체를 직접 만드는 것이 보장됨</p>
<h2 id="문제-7">문제 7</h2>
<pre><code class="language-c++">class Resource
{
public:
    Resource()
    {
        data = new int[100];
    }

    Resource(Resource&amp;&amp; other)
    {
        data = other.data;
        other.data = nullptr;
    }

    ~Resource()
    {
        delete[] data;
    }

private:
    int* data;
};

Resource a;
Resource b = std::move(a);</code></pre>
<p>other.data = nullptr; 하는 이유는??</p>
<hr>
<p>data = other.data에서 둘다 같은 메모리 주소를 가리키는 상태가 되기 때문에 nullptr로 초기화하지 않으면 나중에 둘다 소멸할때 또 double delete가 발생한다.</p>
<h2 id="문제-8">문제 8</h2>
<p>reserve와 resize의 차이</p>
<hr>
<p>reserve는 capacity만 미리 할당해놓는것이고, resize는 만약 타입이 int라면 0으로 미리 초기화까지 해놓음 그래서 reserve(100)과 resize(100)의 차이는 reserve한 후에 v[50] 은 접근 불가하지만 resize는 v[50]접근이 가능하다</p>
<h2 id="문제-9">문제 9</h2>
<pre><code class="language-c++">class Base
{
public:
    virtual void Print()
    {
        cout &lt;&lt; &quot;Base&quot;;
    }
};

class Derived : public Base
{
public:
    void Print() override
    {
        cout &lt;&lt; &quot;Derived&quot;;
    }
};

void Foo(Base obj)
{
    obj.Print();
}

int main()
{
    Derived d;
    Foo(d);
}</code></pre>
<p>출력이 어떤것이 될까?</p>
<hr>
<p>객체 슬라이싱을 통해서 Base가 출력된다. 매개변수가 Base&amp; obj라면 레퍼런스 접근이 되어서 Derived가 출력된다. 결론적으로 참조나 포인터로 받으면 객체슬라이싱이 발생하지 않아서 다형성이 유지된다.</p>
<h2 id="문제-10">문제 10</h2>
<pre><code class="language-c++">int value = 10;

std::thread t1([&amp;]()
{
    value = 20;
});

std::thread t2([&amp;]()
{
    cout &lt;&lt; value;
});

t1.join();
t2.join();</code></pre>
<p>t1에서 value = 20을 써놨으니까 t2에서 20이 출력된다고 보장할 수 있을까?
안된다면 왜 안되는지 그리고 join()이 여기서 무엇을 보장하고 무엇을 보장하지 않는지 설명</p>
<hr>
<p>data race가 발생할 수 있다.
여기서 중요한건 t1을 먼저 만들었다고 해서 t1의 코드가 먼저 끝난다는 보장이 없다.
둘사이에 mutex, atomic같은 동기화가 없기 때문에 data race가 발생하고 프로그램 동작 자체가 Undefined Behavior이다.
t1.join()의 의미는 호출한 현재 스레드가 t1이 끝날때까지 기다림을 뜻한다.</p>
<p>결론</p>
<p>20이 출력된다고 보장할 수 없다. t1을 먼저 생성했다고 해서 t1의 작업이 t2보다 먼저 실행되거나 완료되는것은 아니다. 또한 value에 대해 한 스레드는 쓰고 다른 스레드는 읽는데 동기화가 없으므로 data race가 발생하고 Undefined Behavior이다. join()은 호출한 스레드가 대상 스레드의 종료를 기다리게 할 뿐, t1과 t2사이의 실행 순서를 보장하지는 않는다.</p>
<h2 id="문제-11">문제 11</h2>
<pre><code class="language-c++">class Base
{
public:
    virtual void Foo() {}
};

class Derived : public Base
{
public:
    void Foo() override {}
    void Bar() {}
};

Base* p = new Base();

Derived* d1 = static_cast&lt;Derived*&gt;(p);
Derived* d2 = dynamic_cast&lt;Derived*&gt;(p);</code></pre>
<p>d1과 d2는 각각 어떻게 될까?
그리고 static_cast와 dynamic_cast의 차이를 런타임 타입 검사 관점에서 설명</p>
<hr>
<p>static_cast는 컴파일 타임에 타입검사, dynamic_cast는 런타임에 타입 검사
d1의 경우 static_cast이기 때문에 컴파일러는 Base와 Derived가 상속관계라는것만 보고 캐스팅을 허용한다. 하지만 런타임에 얘가 진짜 Derived 객체가 맞는지 검사는 하지 않음 그래서 d1은 보통 nullptr이 아닌 포인터가 만들어지지만, 실제 객체는 Base다.
그래서 이를 Derived라고 생각해서 사용하는건 위험하다. d1-&gt;Bar() // 잘못된 사용
즉, 캐스팅 문법 자체는 성공했지만 실제 타입이 맞게 변한게 아님</p>
<p>d2는 dynamic_cast이기 때문에 런타임에 실제 객체 타입을 확인함
p가 가리키는 실제 객체는? -&gt; Base -&gt; Derived인가? -&gt; X -&gt; nullptr 반환 그래서 d2 == nullptr이 됨
만약에 본문 코드에서 실제 캐스팅에 성공하려면 Base* p = new Derived(); 로 선언을해야 캐스팅에 성공함
그래서 p의 타입은 Base*고 실제 가리키는 객체는 Derived다</p>
<p>그러면 애초에 Derived* p = new Derived()로 하면 안되나? -&gt; 다형성을 사용할 수 없게 됨</p>
<h2 id="문제-12">문제 12</h2>
<p>순수 가상함수가 하나라도 있는 클래스는 추상클래스가 되기 때문에 객체를 만들 수 없다.</p>
<h2 id="문제-13">문제 13</h2>
<pre><code class="language-c++">struct A
{
    char a;
    int b;
    char c;
};

struct B
{
    int b;
    char a;
    char c;
};</code></pre>
<p>A와 B의 사이즈는 다를까?
다르다면 왜 멤버변수 순서만 바꿨는데 구조체 크기가 줄어드는가?
구조체 패딩은 왜 필요한가?</p>
<hr>
<p>A는 12, B는 8이됨 구조체 변수중 가장 큰 바이트를 가진 애들을 따라가므로, int b가 4바이트, a, c 그리고 패딩 2바이트 해서 4바이트 총 8바이트 됨
구조체 패딩을 사용하면 주소가 정렬되는데 CPU가 접근하기 더 효율적인 경우가 있음</p>
<h2 id="문제-14">문제 14</h2>
<pre><code class="language-c++">std::shared_ptr&lt;int&gt; a = std::make_shared&lt;int&gt;(10);
std::shared_ptr&lt;int&gt; b = a;

std::weak_ptr&lt;int&gt; w = a;</code></pre>
<p>a.reset()을 호출하면 int 객체는 바로 삭제될까?
b.reset()까지 호출하면 <code>weak_ptr w</code>는 어떻게 되는지 설명</p>
<hr>
<p>reset()은 스마트 포인터가 현재 소유하고 있는 객체와의 연결을 끊는 함수라고 보면 됨
본문에서 int 10은 레퍼런스 카운트가 2임 여기서 a.reset을 하면 a -&gt; nullptr, b-&gt; int(10) 이상태
여기서 b.reset()하면 레퍼런스 카운트가 0되면서 int(10)삭제
weak_ptr은 그냥 관찰만 하기때문에 a.reset이 되건 b.reset이 되건 존재는 계속 해있음 대신 a.reset()후에 w.expired()하면 true를 반환함
weak_ptr을 실제 쓸때</p>
<pre><code class="language-c++">if (auto ptr = w.lock()){
    cout &lt;&lt; *ptr;
}</code></pre>
<p>처럼 lock()으로 임시 shared_ptr을 얻어야 함</p>
<h2 id="문제-15">문제 15</h2>
<pre><code class="language-c++">class Base
{
public:
    virtual void Foo()
    {
        cout &lt;&lt; &quot;Base&quot;;
    }
};

class Derived : public Base
{
public:
    void Foo() override
    {
        cout &lt;&lt; &quot;Derived&quot;;
    }
};

void CallFoo(Base* p)
{
    p-&gt;Foo();
}

int main()
{
    Base* p = new Derived();
    delete p;

    CallFoo(p);
}</code></pre>
<p>delete p; 이후에도 p 변수 자체에는 주소값이 남아 있을 수 있어.</p>
<p>그렇다면 왜 CallFoo(p)를 하면 안 될까?</p>
<p>dangling pointer가 무엇인지, 그리고 delete와 포인터 변수 자체의 관계를 설명해봐.</p>
<hr>
<p>우선 소멸자가 없어서 컴파일러가 기본 소멸자를 만들어주는데 그거에는 virtual이 안붙어 있어서 1차문제
delete p를 하게되면 p= 0x1000 -&gt; [Derived객체] 이상태에서 p = 0x1000 -&gt; [이미 해제된 메모리] 이렇게 되기 때문에 포인터는 살아있는데 가리키던 객체가 죽은 상태가 됨 그래서 CallFoo에 접근하면 이미 해제된 객체에 접근하는 문제가 생김
delete p는 p가 가리키는 객체를 소멸시키고 그 메모리를 반환한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C++/CS : 메모리, 스마트 포인터, 다형성, 객체 슬라이싱]]></title>
            <link>https://velog.io/@kyu_/CCS-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%8A%A4%EB%A7%88%ED%8A%B8-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EB%8B%A4%ED%98%95%EC%84%B1-%EA%B0%9D%EC%B2%B4-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EC%8B%B1</link>
            <guid>https://velog.io/@kyu_/CCS-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%8A%A4%EB%A7%88%ED%8A%B8-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EB%8B%A4%ED%98%95%EC%84%B1-%EA%B0%9D%EC%B2%B4-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EC%8B%B1</guid>
            <pubDate>Thu, 27 Aug 2026 14:03:27 GMT</pubDate>
            <description><![CDATA[<h1 id="cs">CS</h1>
<h2 id="메모리-파편화">메모리 파편화</h2>
<ul>
<li><p>외부 파편화
빈 공간 자체는 충분하지만 여기저기 흩어져 있어서 큰 연속 메모리를 못잡는 상태</p>
</li>
<li><p>내부 파편화
할당받은 블록 내부에서 실제로 안 쓰는 공간이 남은 상태</p>
</li>
</ul>
<h2 id="noexcept">noexcept</h2>
<p>이 함수는 예외를 함수 밖으로 던지지 않겠다는 의미
내부에서 예외가 발생하는 것 자체는 가능하고, 내부에서 잡아도 됨
예외가 noexcept함수 밖으로 빠져나가면 std::terminate()가 호출돼 프로그램이 종료될 수 있음</p>
<pre><code class="language-c++">void Foo() noexcept
{
    try
    {
        throw 1;
    }
    catch (...)
    {
        // 여기서 처리하면 괜찮음
    }
}</code></pre>
<h2 id="캐시-일관성">캐시 일관성</h2>
<p>멀티코어 CPU에서 생기는 문제, 각 코어가 자기 캐시를 따로 가지고 있는데, 같은 메모리주소의 값을 여러 코어가 동시에 캐시에 들고 있을 수 있다는게 시작점
예를들어 Core1, Core2에서 같은 메모리의 x = 10을 캐싱해두고 있을때 Core1에서 x=20으로 변경하면 Core2의 캐시는 유효하지 않다. 이같은 처리를 하는게 캐시 일관성이다. 그래서 Core2는 x를 읽을때 10을 못쓰고 다시 최신데이터를 가져오게됨</p>
<h2 id="false-sharing-거짓-공유">False Sharing (거짓 공유)</h2>
<p>서로 다른 변수를 쓰고 있는데도, 그 변수들이 같은 캐시라인에 들어 있어서 서로 간섭하는 현상</p>
<pre><code class="language-c++">int a = 0;
int b = 0;

// Thread 1
a++;

// Thread 2
b++;</code></pre>
<p>a와 b는 다른 변수니까 충돌이 없어보이지만 CPU캐시는 보통 변수를 1바이트, 4바이트 단위로 따로 가져오는게 아니라 캐시 라인 단위로 가져와서 흔히 64바이트 정도다.
그래서 메모리에 아래처럼 붙어있으면</p>
<pre><code>[a][b][기타 데이터들....]</code></pre><p>Core1이 a를 수정하면 그 캐시라인전체가 수정된것으로 취급될 수 있다.
그러면 Core2가 가지고 있던 같은 캐시라인은 무효화된다.
당연히 이러면 성능저하가 발생한다.</p>
<p>예방법은 그저 여러스레드가 자주 수정하는 변수들을 같은 캐시라인에 두지 않는 방법이 있다.</p>
<h2 id="stdshared_ptr과-stdmake_shared">std::shared_ptr과 std::make_shared</h2>
<pre><code>A. make_shared는 객체와 shared_ptr을 Stack에 생성한다.
B. make_shared는 일반적으로 객체와 참조 카운트 관리용 control block을 한 번의 메모리 할당으로 처리할 수 있어 효율적이다.
C. make_shared로 만든 객체는 weak_ptr로 관찰할 수 없다.
D. make_shared를 사용하면 순환 참조가 자동으로 해결된다.</code></pre><p>정답이 A라고 생각했는데 정답은 B였다.
std::make_shared<T>()는 실제 T객체, shared_ptr의 참조 카운트 등을 담는 control block을 한번의 메모리 할당으로 묶어서 처리할 수 있어서 효율적이라고 한다.</p>
<p>A가 틀린 이유는</p>
<pre><code class="language-c++">void Foo()
{
    auto p = std::make_shared&lt;MyClass&gt;();
}</code></pre>
<p>make_shared에서 로컬변수p자체와 p가 관리하는 객체를 구분해야 한다.
로컬변수 p자체는 Stack에 있을 수 있다. 하지만 실제로 관리하는 MyClass객체와 control block은 동적으로 할당된 메모리에 존재한다 (Heap)</p>
<p>여기서 control block이란 shared_ptr이 소유권을 관리하기 위해 따로 가지고 있는 관리 정보 묶음을 뜻한다.</p>
<h2 id="weak_ptrlock">weak_ptr::lock()</h2>
<p>weak_ptr이 보고있던 객체가 살아있는지 확인하면서, 살아 있으면 잠깐 사용할 수 있는 shared_ptr을 얻는 함수</p>
<h2 id="다형성">다형성</h2>
<pre><code class="language-c++">class Base
{
public:
    virtual void Run() {}
};

class Derived : public Base
{
public:
    void Run() override {}
};

void Execute(Base obj)
{
    obj.Run();
}

int main()
{
    Derived d;
    Execute(d);
}</code></pre>
<p>A. Derived::Run()이 호출된다.
B. Base::Run()이 호출된다.
C. 컴파일 오류가 발생한다.
D. 호출 결과는 정의되지 않는다.</p>
<p>나는 당연히 A를 골랐는데 정답이 B였다. Execute(Base obj)가 값 전달이라서 Derived d가 Base obj로 복사되는 순간 객체 슬라이싱이 발생하기 때문이다. obj는 정말 실제로 Base객체가 되고 Derived부분은 잘려나간다고 한다.
만약에 Execute(Base&amp; obj)였으면 원래대로 Derived::Run()이 호출된다.</p>
<h2 id="unique_ptr문제">unique_ptr문제</h2>
<pre><code class="language-c++">void Foo()
{
    std::unique_ptr&lt;int&gt; p = std::make_unique&lt;int&gt;(10);

    if (SomeCondition())
        throw std::runtime_error(&quot;error&quot;);
}</code></pre>
<p>A. 예외가 발생하면 p의 소멸자가 호출되지 않아 메모리가 누수된다.
B. 예외 발생 여부와 관계없이 p가 관리하던 메모리는 함수 종료 과정에서 해제된다.
C. 예외가 발생하면 int 객체만 파괴되고 unique_ptr 객체는 남아 있게 된다.
D. unique_ptr은 정상적인 return으로 함수를 빠져나갈 때만 자원을 해제한다.</p>
<p>내정답은 A였는데 정답은 B였다.
p자체는 Foo()의 지역객체라서 보통 Stack에 존재하고 실제 int 10은 동적 메모리에 존재하고 p는 그 int를 유일하게 소유한다. 예외가 발생하면 stack unwinding과정이 일어나서 Foo의 지역객체들이 역순으로 소멸이되고 -&gt; p의 소멸자가 호출되고 -&gt; unique_ptr이 관리하던 int에 delete수행 -&gt; Foo 빠져나감 이런 순서로 된다.</p>
<h2 id="dynamic_cast">dynamic_cast</h2>
<p>dynamic_cast는 런타임 캐스팅이다. 이는 기준이 되는 부모가 다형성 클래스여야 한다 즉 virtual함수가 하나이상 있어야함</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C++·CS 복습: 다형성·Page Fault·std::move와 해시맵]]></title>
            <link>https://velog.io/@kyu_/TIL-ek6lk4i4</link>
            <guid>https://velog.io/@kyu_/TIL-ek6lk4i4</guid>
            <pubDate>Mon, 24 Aug 2026 12:57:29 GMT</pubDate>
            <description><![CDATA[<h1 id="복습">복습</h1>
<h2 id="다형성">다형성</h2>
<p>생성자/소멸자에서 virtual 호출은 현재 생성/소멸중인 클래스까지만 적용된다.
즉, 자식의 오버라이딩된 함수가 아닌 같은 레벨의 가상함수가 호출된다.</p>
<h2 id="pagefault">PageFault</h2>
<p>PageFault란 프로세스가 접근한 가상 메모리 페이지가 현재 물리 메모리에 없을때 발생할 수 있음</p>
<ol>
<li>CPU가 특정 가상 주소에 접근</li>
<li>페이지 테이블을 확인했는데 해당 페이지가 현재 물리 메모리에 없음</li>
<li>Page Fault발생 -&gt; OS가 처리</li>
<li>필요한 페이지를 디스크등에서 물리 메모리로 가져옴</li>
<li>페이지 테이블을 갱신</li>
<li>중단됐던 명령을 다시 실행</li>
</ol>
<h2 id="stdmove">std::move</h2>
<p>lvalue는 이름이 있고 메모리어딘가에 계속 존재해서 접근할 수 있는 값
rvalue는 이름이 없고, 잠깐 만들어졌다가 금세 사라질 임시 값</p>
<p>move는 객체가 가진 자원을 새 객체가 재사용해서 비싼 복사를 피하기 위해서 사용한다.
여기서 중요한게 move는 실제로 값을 옮기는건 아니고 std::move(a)하면 a를 rvalue처럼 취급하도록 변환한다는 의미이다(xvalue)
이동가능한 값으로 취급된 a는 대입했을때 move constructor가 선택되어서 이동연산이 선택되어 복사비용이 없다
string b = std::move(a)로 대입한 a는 여전히 존재하지만 내용은 보장되지 않음</p>
<h1 id="cs">CS</h1>
<h2 id="volatile">volatile</h2>
<p>C++에서 코드가 예상하지 못하는 외부 요인에 의해 바뀔 수 있으니, 컴파일러가 값을 마음대로 캐싱하거나 접근을 없애지 말라는 표시</p>
<pre><code class="language-c++">volatile int status;</code></pre>
<p>컴파일러는 보통 변수값을 한 번 읽고 레지스터에 저장한 뒤 재사용하는 최적화를 할 수 있음
volatile이면 status가 외부 장치 같은것에 의해 변경될 수 있으니, 컴파일러에게 실제 값을 계속 확인해야 하는 변수라고 알려주는 것
대표적인 용도는 하드웨어 레지스터처럼 프로그램 외부에서 값이 변할 수 있는 경우</p>
<h2 id="sizeof">sizeof</h2>
<ol>
<li>포인터에 sizeof를 쓰면 포인터 크기가 나옴</li>
<li>배열에 sizeof를 쓰면 배열 전체 바이트 크기가 나옴</li>
<li>sizeof는 대부분 컴파일 탕미에 결정됨</li>
<li>sizeof(char)는 항상 1이다.</li>
</ol>
<h2 id="reinterpret_cast">reinterpret_cast</h2>
<p>비트/주소를 저수준에서 다른 타입으로 재해석할 때쓰는 캐스팅</p>
<pre><code class="language-c++">int x = 10;
int* p = &amp;x;

uintptr_t addr = reinterpret_cast&lt;uintptr_t&gt;(p);</code></pre>
<p>포인터 주소값을 정수 형태로 바꾸는 식으로 쓸 수 있음
uintptr_t : unsigned integer ptr , 포인터 주소값을 정수로 담을 수 있도록 만든 타입</p>
<pre><code class="language-c++">int* p;

char* c= reinterpret_cast&lt;char*&gt;(p);</code></pre>
<p>서로 다른 포인터 타입으로 강제로 해석할 수도 있다.</p>
<h1 id="알고리즘">알고리즘</h1>
<ul>
<li><a href="https://school.programmers.co.kr/learn/courses/30/lessons/42578">https://school.programmers.co.kr/learn/courses/30/lessons/42578</a></li>
</ul>
<pre><code class="language-c++">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;unordered_map&gt;

using namespace std;

int solution(vector&lt;vector&lt;string&gt;&gt; clothes) {
    int answer = 1;

    unordered_map&lt;string, int&gt; umap;

    for (const vector&lt;string&gt; clothe : clothes){
        umap[clothe[1]]++;
    }

    for (const auto&amp; [key, value] : umap){
        answer *= (value + 1);
    }

    return answer - 1;
}</code></pre>
]]></description>
        </item>
        <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[UE5 핵심 개념 정리: UObject·GAS·렌더링·애니메이션]]></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[C++·UE5 렌더링 최적화와 동시성 정리]]></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[DirectX 입문: Windows API·C++ 컴파일·LRU 캐시]]></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[GAS 점프 파츠 효과가 남는 버그 해결]]></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>
    </channel>
</rss>