<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Code in Coffee</title>
    <link>https://code-in-coffee.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 19 Aug 2026 10:24:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>타자치는 문돌이</managingEditor>
    <image>
      <title>Code in Coffee</title>
      <url>https://tistory1.daumcdn.net/tistory/6162347/attach/2aff01ba0a194b4fa5028501dadf57d6</url>
      <link>https://code-in-coffee.tistory.com</link>
    </image>
    <item>
      <title>[CPP] 1. 객체와 메모리</title>
      <link>https://code-in-coffee.tistory.com/57</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;객체에 어떻게 접근할까? - 주소와 포인터&lt;/h2&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main()
{
    int x = 10;
    int* p = &amp;amp;x;
    int&amp;amp; r = x;

    std::cout &amp;lt;&amp;lt; &quot;x value: &quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; '\n'; 
    std::cout &amp;lt;&amp;lt; &quot;x address: &quot; &amp;lt;&amp;lt; &amp;amp;x &amp;lt;&amp;lt; '\n';

    std::cout &amp;lt;&amp;lt; &quot;p value: &quot; &amp;lt;&amp;lt; p &amp;lt;&amp;lt; '\n'; 
    std::cout &amp;lt;&amp;lt; &quot;p address: &quot; &amp;lt;&amp;lt; &amp;amp;p &amp;lt;&amp;lt; '\n'; 
    std::cout &amp;lt;&amp;lt; &quot;*p value: &quot; &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; '\n';

    std::cout &amp;lt;&amp;lt; &quot;r: &quot; &amp;lt;&amp;lt; r &amp;lt;&amp;lt; '\n'; 
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;r: &quot; &amp;lt;&amp;lt; &amp;amp;r &amp;lt;&amp;lt; '\n';
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;x&lt;/code&gt;는 객체다.&lt;br /&gt;메모리의 어딘가에 4 바이트의 메모리를 잡고 10을 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;amp;x&lt;/code&gt;는 &lt;code&gt;x&lt;/code&gt;가 10을 저장한 메모리의 주소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;int* p&lt;/code&gt;는 주소를 저장하는 변수로, 여기에 &lt;code&gt;x&lt;/code&gt;의 주소인 &lt;code&gt;&amp;amp;x&lt;/code&gt;를 저장할 수 있다.&lt;br /&gt;즉, &lt;code&gt;p&lt;/code&gt;는 &lt;code&gt;x&lt;/code&gt;의 주소 &lt;code&gt;&amp;amp;x&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;*p&lt;/code&gt;는 &lt;code&gt;p&lt;/code&gt;의 주소에 저장된 객체에 접근한다.&lt;br /&gt;&lt;code&gt;p&lt;/code&gt;는 &lt;code&gt;x&lt;/code&gt;의 주소가 있으므로 &lt;code&gt;x&lt;/code&gt;의 주소에 저장된 10에 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;int&amp;amp; r&lt;/code&gt;은 &lt;code&gt;x&lt;/code&gt;를 &lt;code&gt;x&lt;/code&gt;라는 이름 대신 &lt;code&gt;r&lt;/code&gt;이라는 이름으로 쓸 수 있게 해준다.&lt;br /&gt;따라서 &lt;code&gt;r&lt;/code&gt;의 주소도 &lt;code&gt;x&lt;/code&gt;와 같고, 값도 &lt;code&gt;x&lt;/code&gt;와 같으며, &lt;code&gt;x&lt;/code&gt;를 수정하면 값이 같이 달라진다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;x value: 10
x address: 0x7ffe0d6f03a4
p value:   0x7ffe0d6f03a4
p address: 0x7ffe0d6f03a8
*p value: 10
r: 10
&amp;amp;r:        0x7ffe0d6f03a4&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;int* p의 *와 *p의 *는 역할이 다르다.&lt;br /&gt;int*는 p가 포인터 변수라는 것을 알려주는 선언자(int, float 같은)의 한 부분이고,&lt;br /&gt;*p는 p의 주소에 저장된 객체에 접근하는 연산자이다.&lt;br /&gt;int&amp;amp;과 &amp;amp;도 마찬가지로 int&amp;amp;는 선언자, &amp;amp;는 주소를 반환하는 연산자이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;객체의 수명 - Stack, Heap, Static의 생명주기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제를 공부할 때, 프로세스의 지역 변수는 Stack, 전역 변수는 Data, 동적으로 할당된 데이터는 Heap에 저장된다고 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데이터는 언제까지 유효할까?&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

struct Tracer
{
    const char* Name;

    Tracer(const char* InName) : Name(InName)
    {
        std::cout &amp;lt;&amp;lt; &quot;Construct: &quot; &amp;lt;&amp;lt; Name &amp;lt;&amp;lt; '\n';
    }

    ~Tracer()
    {
        std::cout &amp;lt;&amp;lt; &quot;Destruct: &quot; &amp;lt;&amp;lt; Name &amp;lt;&amp;lt; '\n';
    }
};

Tracer GlobalTracer(&quot;Global&quot;);

void Foo()
{
    std::cout &amp;lt;&amp;lt; &quot;Enter Foo\n&quot;;
    Tracer LocalTracer(&quot;Local in Foo&quot;);
    Tracer* DynamicTracer = new Tracer(&quot;Dynamic in Foo&quot;);
    std::cout &amp;lt;&amp;lt; &quot;DynamicTracer : &quot;&amp;lt;&amp;lt; DynamicTracer &amp;lt;&amp;lt; '\n';
    delete DynamicTracer;
    std::cout &amp;lt;&amp;lt; &quot;Leave Foo\n&quot;;
}

int main()
{
    std::cout &amp;lt;&amp;lt; &quot;Enter main\n&quot;;
    std::cout &amp;lt;&amp;lt; &quot;GlobalTracer : &quot;&amp;lt;&amp;lt; &amp;amp;GlobalTracer &amp;lt;&amp;lt; '\n';
    Tracer LocalTracer(&quot;Local in main&quot;);
    std::cout &amp;lt;&amp;lt; &quot;LocalTracer : &quot;&amp;lt;&amp;lt; &amp;amp;LocalTracer &amp;lt;&amp;lt; '\n';
    Foo();
    std::cout &amp;lt;&amp;lt; &quot;Leave main\n&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Tracer&lt;/code&gt;는 생성과 소멸에 &lt;code&gt;Name&lt;/code&gt;을 출력한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Construct: Global
    Enter main
    GlobalTracer : 0x56343152d158
    Construct: Local in main
    LocalTracer : 0x7fffd3d7cd80
        Enter Foo
        Construct: Local in Foo
        Construct: Dynamic in Foo
        DynamicTracer : 0x56343c94c6c0
        Destruct: Dynamic in Foo
        Leave Foo
        Destruct: Local in Foo
    Leave main
    Destruct: Local in main
Destruct: Global&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 객체는 &lt;code&gt;main&lt;/code&gt;보다 먼저 생성되고, 프로그램 종료 시점에 사라진다.&lt;br /&gt;지역 객체는 Scope에 들어가면 생성되고, Scope 종료 시점에 사라진다.&lt;br /&gt;동적 객체는 &lt;code&gt;new&lt;/code&gt;에 생성되고, &lt;code&gt;delete&lt;/code&gt;에 소멸한다. &lt;code&gt;delete&lt;/code&gt;가 없으면 사라지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;객체는 어떻게 메모리에 배치되는가 - &lt;code&gt;sizeof&lt;/code&gt;, alignment, padding&lt;/h2&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

struct A
{
    char c1;
    int i;
    char c2;
};

struct B
{
    int i;
    char c1;
    char c2;
};

int main()
{
    std::cout &amp;lt;&amp;lt; &quot;sizeof(char): &quot; &amp;lt;&amp;lt; sizeof(char) &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;sizeof(int): &quot; &amp;lt;&amp;lt; sizeof(int) &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;sizeof(void*): &quot; &amp;lt;&amp;lt; sizeof(void*) &amp;lt;&amp;lt; '\n';

    std::cout &amp;lt;&amp;lt; &quot;sizeof(A): &quot; &amp;lt;&amp;lt; sizeof(A) &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;sizeof(B): &quot; &amp;lt;&amp;lt; sizeof(B) &amp;lt;&amp;lt; '\n';
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sizeof&lt;/code&gt;는 객체가 차지하는 메모리의 크기이다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;sizeof(char): 1
sizeof(int): 4
sizeof(void*): 8
sizeof(A): 12
sizeof(B): 8&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;void*&lt;/code&gt;는 포인터의 크기로, 64 bit 환경에서는 주로 8 바이트, 32 bit에서는 4 바이트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;A&lt;/code&gt;와 &lt;code&gt;B&lt;/code&gt;는 같은 내용을 담지만, 12와 8로 크기가 다르다. 이는 alignment에 따른 변수의 위치 조정 때문이다. &lt;code&gt;int&lt;/code&gt;의 alignment가 4라면, &lt;code&gt;int&lt;/code&gt; 객체는 메모리의 4의 배수 자리에 놓인다.&lt;br /&gt;&lt;code&gt;A&lt;/code&gt;의 경우 &lt;code&gt;char&lt;/code&gt;-&lt;code&gt;int&lt;/code&gt;-&lt;code&gt;char&lt;/code&gt; 순서로 놓는 동시에 &lt;code&gt;int&lt;/code&gt;를 4의 배수 자리에 놓기 위해 &lt;code&gt;char&lt;/code&gt; 뒤에 padding 3 바이트를 추가한다.&lt;br /&gt;&lt;code&gt;B&lt;/code&gt;는 같은 데이터지만, &lt;code&gt;int&lt;/code&gt;-&lt;code&gt;char&lt;/code&gt;-&lt;code&gt;char&lt;/code&gt;로 배치해 alignement가 1인 &lt;code&gt;char&lt;/code&gt;를 붙여 배치할 수 있으므로 8 바이트만 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;alignment에 따른 padding은 클래스에서도 마찬가지로 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배열, 포인터, 연속 메모리&lt;/h2&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main()
{
    int arr[3] = {10, 20, 30};
    int* p = arr;

    std::cout &amp;lt;&amp;lt; &quot;sizeof(int): &quot; &amp;lt;&amp;lt; sizeof(int) &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;sizeof(arr): &quot; &amp;lt;&amp;lt; sizeof(arr) &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; '\n';

    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;arr[0]: &quot; &amp;lt;&amp;lt; &amp;amp;arr[0] &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;arr[1]: &quot; &amp;lt;&amp;lt; &amp;amp;arr[1] &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;arr[2]: &quot; &amp;lt;&amp;lt; &amp;amp;arr[2] &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; '\n';

    std::cout &amp;lt;&amp;lt; &quot;arr: &quot; &amp;lt;&amp;lt; arr &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;arr[0]: &quot; &amp;lt;&amp;lt; &amp;amp;arr[0] &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; '\n';

    std::cout &amp;lt;&amp;lt; &quot;sizeof(arr) : &quot; &amp;lt;&amp;lt; sizeof(arr) &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;sizeof(&amp;amp;arr[0]) : &quot; &amp;lt;&amp;lt; sizeof(&amp;amp;arr[0]) &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;sizeof(p) : &quot; &amp;lt;&amp;lt; sizeof(p)   &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; '\n';

    std::cout &amp;lt;&amp;lt; &quot;p : &quot; &amp;lt;&amp;lt; p &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;p+1 : &quot; &amp;lt;&amp;lt; p + 1 &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;p+2 : &quot; &amp;lt;&amp;lt; p + 2 &amp;lt;&amp;lt; '\n';
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;amp;arr[1]: 0x7ffc8015d550
&amp;amp;arr[2]: 0x7ffc8015d554

arr: 0x7ffc8015d54c
&amp;amp;arr[0]: 0x7ffc8015d54c

sizeof(arr) : 12
sizeof(&amp;amp;arr[0]) : 8
sizeof(p) : 8

p : 0x7ffc8015d54c
p+1 : 0x7ffc8015d550
p+2 : 0x7ffc8015d554&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열은 같은 타입의 객체가 연속된 메모리에 저장된다.&lt;br /&gt;따라서 &lt;code&gt;int&lt;/code&gt; 배열 원소의 주소는 4 차이이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 이름은 포인터가 아니지만, 많은 표현식에서 배열의 첫 원소를 가리키는 포인터로 변환된다.&lt;br /&gt;따라서 &lt;code&gt;arr&lt;/code&gt;은 &lt;code&gt;&amp;amp;arr[0]&lt;/code&gt;으로 쓰기도 한다.&lt;br /&gt;둘은 같은 메모리 주소를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 &lt;code&gt;sizeof(arr)&lt;/code&gt;는 &lt;code&gt;arr&lt;/code&gt;의 크기 (4*3), &lt;code&gt;sizeof(&amp;amp;arr[0])&lt;/code&gt;과 &lt;code&gt;sizeof(p)&lt;/code&gt;는 &lt;code&gt;arr[0]&lt;/code&gt;와 &lt;code&gt;p&lt;/code&gt;의 포인터 객체 크기(8)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인터의 산술 연산은 타입 크기를 기준으로 한다.&lt;br /&gt;&lt;code&gt;int*&lt;/code&gt;의 연산은 4 바이트 기준이다.&lt;br /&gt;따라서 &lt;code&gt;p + 1&lt;/code&gt;은 &lt;code&gt;p + int (4 바이트)&lt;/code&gt;이다.&lt;br /&gt;내부적으로 &lt;code&gt;arr[i] == *(arr + i)&lt;/code&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

struct A
{
    char c1;
    int i;
    char c2;
};

int main()
{
    A arr[3];

    std::cout &amp;lt;&amp;lt; &quot;sizeof(A): &quot; &amp;lt;&amp;lt; sizeof(A) &amp;lt;&amp;lt; '\n';

    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;arr[0]: &quot; &amp;lt;&amp;lt; &amp;amp;arr[0] &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;arr[1]: &quot; &amp;lt;&amp;lt; &amp;amp;arr[1] &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;&amp;amp;arr[2]: &quot; &amp;lt;&amp;lt; &amp;amp;arr[2] &amp;lt;&amp;lt; '\n';
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sizeof(A): 12
&amp;amp;arr[0]: 0x7fff34d9d070
&amp;amp;arr[1]: 0x7fff34d9d07c
&amp;amp;arr[2]: 0x7fff34d9d088&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;A&lt;/code&gt;는 &lt;code&gt;char&lt;/code&gt;+&lt;code&gt;int&lt;/code&gt;+&lt;code&gt;char&lt;/code&gt;로 &lt;code&gt;char 1&lt;/code&gt; + &lt;code&gt;[padding 3]&lt;/code&gt; + &lt;code&gt;int 4&lt;/code&gt; + &lt;code&gt;char 1&lt;/code&gt; + &lt;code&gt;[padding 3]&lt;/code&gt;로 12 바이트를 차지한다. 이때 마지막 &lt;code&gt;char&lt;/code&gt;도 tail padding을 가져 &lt;code&gt;A&lt;/code&gt;의 배열에서도 그 크기만큼 간격이 생긴다. 따라서 &lt;code&gt;A&lt;/code&gt; 배열의 각 원소는 12 바이트 간격으로 배치된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사라진 객체 - Dangling Pointer와 Undefined Behavior&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dangling Pointer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dangling Pointer는 이미 수명이 끝난 객체를 가리키는 포인터이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int* MakePointer()
{
    int Local = 10;
    return &amp;amp;Local; // 위험
}

int main()
{
    int* p = MakePointer();

    std::cout &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; '\n'; // Undefined Behavior
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Local&lt;/code&gt;은 &lt;code&gt;MakePointer()&lt;/code&gt;가 종료되면 사라지지만, &lt;code&gt;p&lt;/code&gt;는 사라진 &lt;code&gt;Local&lt;/code&gt;의 주소를 들고 있다.&lt;br /&gt;이때 사라진 &lt;code&gt;Local&lt;/code&gt;의 주소를 가리키는 &lt;code&gt;p&lt;/code&gt;를 dangling pointer라고 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int* p = new int(10);

delete p;

std::cout &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; '\n'; // Undefined Behavior&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우도 &lt;code&gt;p&lt;/code&gt;는 &lt;code&gt;delete&lt;/code&gt;로 사라진 &lt;code&gt;new int(10)&lt;/code&gt;의 주소를 들고 있다.&lt;br /&gt;안전하게 처리하려면&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;delete p;
p = nullptr;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;nullptr&lt;/code&gt;과 dangling pointer는 다르다. &lt;code&gt;nullptr&lt;/code&gt;은 '아무 객체도 가리키지 않는다'는 명확한 값으로&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;if (p)
{
    std::cout &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; '\n';
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같이 사용할 수 있지만,&lt;br /&gt;dangling pointer의 값은 &lt;code&gt;nullptr&lt;/code&gt;이 아닐 수도, 유효하지 않은 예전 주소값을 그대로 가지고 있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dangling pointer와 마찬가지로 dangling reference도 존재한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int&amp;amp; MakeReference()
{
    int Local = 10;
    return Local; // 위험
}

int main()
{
    int&amp;amp; r = MakeReference();

    std::cout &amp;lt;&amp;lt; r &amp;lt;&amp;lt; '\n'; // Undefined Behavior
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 이미 소멸한 객체를 참조하고 있다면 위험하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Undefined Behaviour&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++이 프로그램이 어떻게 동작할지 정의하지 않는다는 뜻으로 어떤 결과나 나올지 모른다.&lt;br /&gt;정상처럼 보일 수도, 쓰레깃값이 나올 수도, 크래시가 날 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int* p = new int(10);

delete p;

std::cout &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; '\n';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드가 어떤 경우에는 10을 출력할 수 있지만, 그건 우연히 메모리에 아직 10이 남아 있던 것이고 항상 같은 결과를 보장하지도, 안전하지도 않다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main()
{
    int arr[3] = {10, 20, 30};

    std::cout &amp;lt;&amp;lt; arr[3] &amp;lt;&amp;lt; '\n'; // Undefined Behavior
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;arr[3]&lt;/code&gt;은 &lt;code&gt;*(arr + 3)&lt;/code&gt;으로 &lt;code&gt;arr[2]&lt;/code&gt; 다음 위치 주소를 역참조한다. 그러나 이 위치의 값은 무엇일지 알 수 없다. 따라서 역참조하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 반복문에 활용할 수는 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;int arr[3] = {10, 20, 30};

int* begin = arr;
int* end = arr + 3;

for (int* p = begin; p != end; ++p)
{
    std::cout &amp;lt;&amp;lt; *p &amp;lt;&amp;lt; '\n';
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문의 종료 지점으로 사용하지만, 절대 그 값을 역참조해서는 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;const와 메모리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;const&lt;/code&gt;는 이 이름을 통해서는 객체를 수정하지 않겠다는 뜻이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const int x = 10;

x = 20; // 컴파일 에러&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;const int*&lt;/code&gt;와 &lt;code&gt;int* const&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int a = 10;
int b = 20;

const int* p = &amp;amp;a;

*p = 30; // 불가능
p = &amp;amp;b;  // 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 &lt;code&gt;p&lt;/code&gt;가 가리키는 &lt;code&gt;int&lt;/code&gt;를 수정할 수 없다는 뜻이다.&lt;br /&gt;&lt;code&gt;p&lt;/code&gt;가 &lt;code&gt;b&lt;/code&gt;를 가리킬 수는 있다.&lt;br /&gt;그러나 &lt;code&gt;p&lt;/code&gt;가 가리키는 값을 수정할 수는 없다.&lt;br /&gt;즉, 대상은 수정 가능하지만, 내용은 수정할 수 없다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int* const p = &amp;amp;a;

*p = 30; // 가능
p = &amp;amp;b;  // 불가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 반대로 &lt;code&gt;p&lt;/code&gt; 자체가 &lt;code&gt;const&lt;/code&gt;이므로&lt;br /&gt;&lt;code&gt;p&lt;/code&gt;가 가리키는 &lt;code&gt;int&lt;/code&gt;를 바꿀 수는 없지만, &lt;code&gt;p&lt;/code&gt;가 가리키는 &lt;code&gt;int&lt;/code&gt;의 값을 수정할 수는 있다.&lt;br /&gt;즉, 대상은 수정할 수 없지만, 내용은 수정 가능하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const int* const p = &amp;amp;a;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 대상도 내용도 수정할 수 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;const &amp;amp;&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int a = 10;

const int&amp;amp; r = a;

r = 20; // 불가능 
a = 20; // 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;r&lt;/code&gt;은 &lt;code&gt;a&lt;/code&gt;의 별칭으로, &lt;code&gt;r&lt;/code&gt;의 값은 수정할 수 없다는 뜻이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 인자로의 &lt;code&gt;const&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++의 &lt;code&gt;const&lt;/code&gt;는 함수 인자를 받을 때의 약속이 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void PrintValue(const int&amp;amp; value)
{
    std::cout &amp;lt;&amp;lt; value &amp;lt;&amp;lt; '\n';
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 &lt;code&gt;value&lt;/code&gt;를 복사하지 않고 참조로 받지만, 함수 안에서 &lt;code&gt;value&lt;/code&gt;를 수정하지 않는다는 뜻이다. 즉, &lt;code&gt;value&lt;/code&gt;의 복사 비용이 들지 않고, 읽기 전용으로 전달할 수 있다.&lt;/p&gt;</description>
      <category>Programming Languange/C++</category>
      <category>C++</category>
      <category>cpp</category>
      <author>타자치는 문돌이</author>
      <guid isPermaLink="true">https://code-in-coffee.tistory.com/57</guid>
      <comments>https://code-in-coffee.tistory.com/57#entry57comment</comments>
      <pubDate>Tue, 14 Jul 2026 17:17:06 +0900</pubDate>
    </item>
    <item>
      <title>[softeon-theme] Stitch와 Codex로 Jekyll Blog Theme 만들기</title>
      <link>https://code-in-coffee.tistory.com/55</link>
      <description>&lt;figure id=&quot;og_1781684158349&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Home | softeon-theme&quot; data-og-description=&quot;This preview sentence appears on the Projects Sample Beta card. Sample Projects Order 70&quot; data-og-host=&quot;taeahnk.github.io&quot; data-og-source-url=&quot;https://taeahnk.github.io/softeon-theme/&quot; data-og-url=&quot;https://taeahnk.github.io/softeon-theme/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/YmaFI/dJMb9c9GOyU/NjgK09QXZhSX2Kfu4u9tOk/img.png?width=1672&amp;amp;height=941&amp;amp;face=0_0_1672_941,https://scrap.kakaocdn.net/dn/cwvfWJ/dJMb9aKN157/pJ9PTSfEKgyBclz1uNjJhK/img.png?width=1672&amp;amp;height=941&amp;amp;face=0_0_1672_941,https://scrap.kakaocdn.net/dn/AUvDr/dJMb9bwbdON/hYgZ3R8skjgHuKI9jDKtek/img.png?width=1672&amp;amp;height=941&amp;amp;face=0_0_1672_941&quot;&gt;&lt;a href=&quot;https://taeahnk.github.io/softeon-theme/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://taeahnk.github.io/softeon-theme/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/YmaFI/dJMb9c9GOyU/NjgK09QXZhSX2Kfu4u9tOk/img.png?width=1672&amp;amp;height=941&amp;amp;face=0_0_1672_941,https://scrap.kakaocdn.net/dn/cwvfWJ/dJMb9aKN157/pJ9PTSfEKgyBclz1uNjJhK/img.png?width=1672&amp;amp;height=941&amp;amp;face=0_0_1672_941,https://scrap.kakaocdn.net/dn/AUvDr/dJMb9bwbdON/hYgZ3R8skjgHuKI9jDKtek/img.png?width=1672&amp;amp;height=941&amp;amp;face=0_0_1672_941');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Home | softeon-theme&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This preview sentence appears on the Projects Sample Beta card. Sample Projects Order 70&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;taeahnk.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1781684191315&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - TaeAhnK/softeon-theme&quot; data-og-description=&quot;Contribute to TaeAhnK/softeon-theme development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/TaeAhnK/softeon-theme&quot; data-og-url=&quot;https://github.com/TaeAhnK/softeon-theme&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/VHirn/dJMb9cBQX40/7LFIhvC1GIREZnEL9KNiKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dsNKhL/dJMb9eTYuHk/Jl38CnY0EzT5KkpWY3MOx0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/TaeAhnK/softeon-theme&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/TaeAhnK/softeon-theme&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/VHirn/dJMb9cBQX40/7LFIhvC1GIREZnEL9KNiKK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/dsNKhL/dJMb9eTYuHk/Jl38CnY0EzT5KkpWY3MOx0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - TaeAhnK/softeon-theme&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to TaeAhnK/softeon-theme development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex가 프론트엔드는 매우 잘 만든다고 해서 기존의 포트폴리오 사이트를 새로 만들어보기로 했다. 웹개발 쪽은 잘 모르지만 정적 웹사이트 정도는 만들 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 Github Pages로 호스팅하는 Jekyll 블로그를 포폴로 쓰고 있다.&lt;br /&gt;사이트에서 다운 받은 템플릿을 조금 수정해 사용 중이다.&lt;br /&gt;현재 사이트도 나쁘지 않지만 만들어 보고 싶은 기능이 있어 새로 만들어 보기로 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzM2P9/dJMcajoZLaF/6467VGWW3kZkP0mtdHs6D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzM2P9/dJMcajoZLaF/6467VGWW3kZkP0mtdHs6D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzM2P9/dJMcajoZLaF/6467VGWW3kZkP0mtdHs6D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzM2P9%2FdJMcajoZLaF%2F6467VGWW3kZkP0mtdHs6D0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;492&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔씩 포트폴리오를 pdf로 요구하는 회사들이 있다. 포트폴리오에 프로젝트가 늘어나고 게시글이 많아질수록 이걸 워드나 파워포인트로 옮겨서 작성하기가 어렵다. 특히 코드 블록이 모두 어그러진다.&lt;br /&gt;그래서 포트폴리오에 프린트용 View를 따로 두고, 이걸 pdf로 저장해 제출하려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능 기획&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 구현해야 할 기본 기능은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Header, Hero, Profile, Projects, Other Projects, Current Project 섹션&lt;/li&gt;
&lt;li&gt;Markdown 프로젝트 포스트&lt;/li&gt;
&lt;li&gt;포스트에서 썸네일과 설명, 태그를 추출하고, 적절한 섹션에 적절한 순서로 배치&lt;/li&gt;
&lt;li&gt;썸네일로 이미지 또는 유튜브 영상 사용&lt;/li&gt;
&lt;li&gt;이후 Markdown 포스트 추가만으로 확장 가능&lt;/li&gt;
&lt;li&gt;반응형으로 브라우저 크기를 줄여도 어색하지 않은 화면&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능들은 이전 포트폴리오에서도 수정해서 적용했었다.&lt;br /&gt;추가로 구현해야 하는 기능은&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;print.html에서 print용 view 보여주기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;div class=&quot;print-hide&quot;&amp;gt;&lt;/code&gt;가 있으면 print view에서 숨기기&lt;/li&gt;
&lt;li&gt;인쇄 시 (pdf로 출력) 적절한 위치에서 페이지 나눔&lt;/li&gt;
&lt;li&gt;print.html은 A4에 적절한 css&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;div class=&quot;print-show&quot;&amp;gt;&lt;/code&gt;가 있으면 게시글에선 숨기고 print view에서 보여주기&lt;/li&gt;
&lt;li&gt;print view에서 보여줄 project 개수 설정&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex에게 다음 순서로 작업을 요청했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Init&lt;/li&gt;
&lt;li&gt;Index 페이지 구현&lt;/li&gt;
&lt;li&gt;Post 페이지 구현&lt;/li&gt;
&lt;li&gt;테스트 게시글 생성&lt;/li&gt;
&lt;li&gt;카드 제목/설명/썸네일 출력&lt;/li&gt;
&lt;li&gt;Youtube Embed 출력&lt;/li&gt;
&lt;li&gt;디자인 파일 적용&lt;/li&gt;
&lt;li&gt;다크모드 적용&lt;/li&gt;
&lt;li&gt;print.html 구현&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;div class=&quot;print-hide&quot;&amp;gt;&lt;/code&gt; 구현&lt;/li&gt;
&lt;li&gt;프린트모드 적용&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계별로 구현에 필요한 내용을 Feature.md에 적고, 해당 내용을 참고하며 작업을 요청했다.&lt;br /&gt;먼저 블로그의 구조를 정의했다. 기존 블로그와 비슷한 구조로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Header&lt;/li&gt;
&lt;li&gt;Hero&lt;/li&gt;
&lt;li&gt;About&lt;/li&gt;
&lt;li&gt;Projects&lt;/li&gt;
&lt;li&gt;Other Projects&lt;/li&gt;
&lt;li&gt;Current Projects&lt;/li&gt;
&lt;li&gt;Footer&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 구성했다. 프로젝트 섹션에서는 카드 형태로 게시물을 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;1410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9J0dP/dJMcabYWbd1/OOrLP60kLcJTSqP64pLSKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9J0dP/dJMcabYWbd1/OOrLP60kLcJTSqP64pLSKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9J0dP/dJMcabYWbd1/OOrLP60kLcJTSqP64pLSKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9J0dP%2FdJMcabYWbd1%2FOOrLP60kLcJTSqP64pLSKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;831&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;1410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스트는 간단하게 뒤로가기 버튼이 있는 헤더와 제목, 본문, Footer로 구성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260617161558.png&quot; data-origin-width=&quot;2820&quot; data-origin-height=&quot;1798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KXQQ1/dJMcaaeD64D/ZWC04V9kkId35Y7vcEclfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KXQQ1/dJMcaaeD64D/ZWC04V9kkId35Y7vcEclfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KXQQ1/dJMcaaeD64D/ZWC04V9kkId35Y7vcEclfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKXQQ1%2FdJMcaaeD64D%2FZWC04V9kkId35Y7vcEclfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;383&quot; data-filename=&quot;Pasted image 20260617161558.png&quot; data-origin-width=&quot;2820&quot; data-origin-height=&quot;1798&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마크다운만으로 새 프로젝트를 추가할 수 있도록 프로젝트 포스트의 Front Matter에 다음과 같은 내용을 정의했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;---
layout: post
title: Title
display_area: projects / other-projects / current-projects / hide
display_order: number (descending order)
thumbnail_type: img / vid
thumbnail_img: path
thumbnail_vid: &quot;{youtube link}&quot;
tags: tags for thumbnail
---&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Front Matter의 값에 따라 보일 섹션과 순서, 썸네일 타입과 썸네일을 정의했다. 썸네일에서 같이 보여줄 태그도 작성했다. display_order는 나중에 프로젝트를 추가할수록 앞에 오는 일이 많을 것 같아 내림차순으로 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 페이지의 프로젝트 소개글은 &lt;code&gt;excerpt_separator&lt;/code&gt;로 구분하고, 이 내용은 포스트를 볼 때는 보이지 않게 했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;---
(front matter)
---

Preview Description

&amp;lt;!-- preview --&amp;gt;

(Main Post)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 이전 포트폴리오에서 쓰던 방식으로 preview용 마크다운을 따로 두거나 하는 다른 방식보다 편했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 디자인을 못 한다. 내가 가장 취약한 부분이다.&lt;br /&gt;일단은 Google Stitchs가 웹 디자인을 해줄 수 있다고 하니 한번 사용해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 개발자 포트폴리오를 만들 것이고, 위에서 만든 레이아웃을 사용할 것이라고 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vs7x1/dJMcah5VGTb/QO2zfkwaQk2gSCzOKe0r6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vs7x1/dJMcah5VGTb/QO2zfkwaQk2gSCzOKe0r6K/img.png&quot; data-origin-width=&quot;531&quot; data-origin-height=&quot;789&quot; data-is-animation=&quot;false&quot; data-filename=&quot;blob&quot; width=&quot;400&quot; data-widthpercent=&quot;48.98&quot; style=&quot;width: 48.407%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vs7x1/dJMcah5VGTb/QO2zfkwaQk2gSCzOKe0r6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvs7x1%2FdJMcah5VGTb%2FQO2zfkwaQk2gSCzOKe0r6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;531&quot; height=&quot;789&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPERHI/dJMcaa6NHqY/ILKQ6raFIdQRTxG674Flk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPERHI/dJMcaa6NHqY/ILKQ6raFIdQRTxG674Flk0/img.png&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;2031&quot; data-is-animation=&quot;false&quot; data-filename=&quot;blob&quot; width=&quot;400&quot; height=&quot;842&quot; data-widthpercent=&quot;51.02&quot; style=&quot;width: 50.4302%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPERHI/dJMcaa6NHqY/ILKQ6raFIdQRTxG674Flk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPERHI%2FdJMcaa6NHqY%2FILKQ6raFIdQRTxG674Flk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1424&quot; height=&quot;2031&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;괜찮은 디자인이 나왔다.&lt;br /&gt;프린트모드를 다크모드로 만들었다간 인쇄할 때 어떤 끔찍한 일이 생길지 모르므로 라이트모드도 만들어 달라고 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4pe1W/dJMcaasbj0Q/h7ktg8FF5Gklg7p2Xo2Rr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4pe1W/dJMcaasbj0Q/h7ktg8FF5Gklg7p2Xo2Rr1/img.png&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;791&quot; data-is-animation=&quot;false&quot; data-filename=&quot;blob&quot; data-widthpercent=&quot;49.04&quot; style=&quot;width: 48.4699%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4pe1W/dJMcaasbj0Q/h7ktg8FF5Gklg7p2Xo2Rr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4pe1W%2FdJMcaasbj0Q%2Fh7ktg8FF5Gklg7p2Xo2Rr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;532&quot; height=&quot;791&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ9F1h/dJMcahrjKA5/th8p3PvrXOMZz6xnt6E871/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ9F1h/dJMcahrjKA5/th8p3PvrXOMZz6xnt6E871/img.png&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;1086&quot; data-is-animation=&quot;false&quot; data-filename=&quot;blob&quot; data-widthpercent=&quot;50.96&quot; style=&quot;width: 50.3673%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ9F1h/dJMcahrjKA5/th8p3PvrXOMZz6xnt6E871/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ9F1h%2FdJMcahrjKA5%2Fth8p3PvrXOMZz6xnt6E871%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;759&quot; height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 유독 좋아하는 흰색+보라색 조합으로 만들어줬다. Neon 느낌이 약해졌지만, 마음에 들었다. 아쉬워도 뭐가 어색하니 고쳐달라 할 감각이 부족하다.&lt;br /&gt;이 6개의 이미지를 zip으로 내보냈다. 팔레트는 Markdown, 사이트는 이미지와 html로 제공한다.&lt;br /&gt;이 파일을 주고 이 디자인을 그대로 프로젝트에 적용해달라고 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;1394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Khwwu/dJMcacQZ5UX/KzFdfakieU1g2RyMW2tF80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Khwwu/dJMcacQZ5UX/KzFdfakieU1g2RyMW2tF80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Khwwu/dJMcacQZ5UX/KzFdfakieU1g2RyMW2tF80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKhwwu%2FdJMcacQZ5UX%2FKzFdfakieU1g2RyMW2tF80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;722&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;1394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거의 완성된 블로그가 만들어졌다. 텍스트를 &lt;code&gt;yml&lt;/code&gt; 파일로 수정할 수 있도록 연결하고, &lt;code&gt;home.yml&lt;/code&gt;에서 관리할 수 있게 했다. 마음에 안 드는 간격과 위치는 조금씩 조정했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;1343&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byZKAy/dJMcajbsUzl/r4K6A7QSL8U9QasxVnfkiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byZKAy/dJMcajbsUzl/r4K6A7QSL8U9QasxVnfkiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byZKAy/dJMcajbsUzl/r4K6A7QSL8U9QasxVnfkiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyZKAy%2FdJMcajbsUzl%2Fr4K6A7QSL8U9QasxVnfkiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;704&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;1343&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다크모드 버튼도 추가해달라 하고, 다크모드 디자인도 적용해달라고 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;print.html&lt;/code&gt;을 어떤 식으로 구현할지를 고민해야 했다. 우선은 &lt;code&gt;index.html&lt;/code&gt;을 구성하는 다양한 &lt;code&gt;include html&lt;/code&gt;을 인쇄용에 적합하게 배치하고, post &lt;code&gt;collections&lt;/code&gt;를 이어 붙이는 방식으로 구현해 보았다. 원하는 대로 index.html이 cover 역할을 하고, post가 내용 역할을 하는 pdf가 만들어졌다. 모양과 정렬 등은 나중에 css에서 처리하기로 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;363&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp1r3j/dJMcahELKp8/My92kZ5VjWQSIVf5GUYin1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp1r3j/dJMcahELKp8/My92kZ5VjWQSIVf5GUYin1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp1r3j/dJMcahELKp8/My92kZ5VjWQSIVf5GUYin1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp1r3j%2FdJMcahELKp8%2FMy92kZ5VjWQSIVf5GUYin1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;363&quot; height=&quot;468&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;363&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;print.html&lt;/code&gt;은 별도의 css를 만들어 달라고 했다. 폰트 크기부터 규칙까지 다른 부분이 많아 적용할 내용이 많았기 때문이다. 대표적으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;폰트 크기 10pt&lt;/li&gt;
&lt;li&gt;별도의 링크 소개&lt;/li&gt;
&lt;li&gt;항상 라이트모드&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;로 접힌 부분 모두 열어두기&lt;/li&gt;
&lt;li&gt;포스트 끝마다 페이지 넘기기 적용&lt;/li&gt;
&lt;li&gt;코드 블록이 길어지면 줄바꿈&lt;/li&gt;
&lt;li&gt;클릭해도 게시물로 넘어가지 않기&lt;br /&gt;등을 적용해야 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스트에서 프린트모드에서 숨길 부분은 &lt;code&gt;&amp;lt;div class=&quot;print-hide&amp;gt;&lt;/code&gt;로 감싸면 숨겨지게 했다. 웹에서는 코드를 모두 보여줘도 &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;로 숨겨 분량 걱정이 없지만, 이걸 그대로 출력하면 포폴이 너무 길어져 편집해야 한다. 그리고 Demo 영상도 숨겨야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 출력 설정으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 부분을 크롬 인쇄 설정으로 미표시하지 않기&lt;/li&gt;
&lt;li&gt;위아래 1 inch margin&lt;br /&gt;을 줘야 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 부분은 모두 codex에 맡기고, codex가 css를 수정해 적용했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;code&gt;config.yml&lt;/code&gt;에서 레이아웃 조정이 가능하게 했다. 각 섹션의 가로 프로젝트 개수, 보여줄 프로젝트 개수, 프린트모드에서 보여줄 프로젝트 개수를 설정하게 해 개수가 달라져도 모양이 어색하지 않게 했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;index:
  projects_count: 4
  other_projects_count: 3
  current_projects_count: 1

print:
  projects_count: 4
  other_projects_count: 3
  current_projects_count: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;괜찮은 Jekyll Theme이 만들어졌다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 기존 포트폴리오에 적용해 보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;1426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba9lTf/dJMcafNPaWh/85fmGEyLPMIdt416a3wXDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba9lTf/dJMcafNPaWh/85fmGEyLPMIdt416a3wXDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba9lTf/dJMcafNPaWh/85fmGEyLPMIdt416a3wXDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba9lTf%2FdJMcafNPaWh%2F85fmGEyLPMIdt416a3wXDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1211&quot; height=&quot;1426&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1211&quot; data-origin-height=&quot;1426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 때는 몰랐는데 폰트가 별로여서 수정했다. (한글 태그에 굴림체가 적용됐다.)&lt;br /&gt;그리고 아무리 노력해도 줄 끝 맞춤과 단어 잘림의 적절한 조합을 모르겠다.&lt;br /&gt;가끔 긴 단어 앞에서의 넓어지는 여백이나 어색하게 잘리는 단어, 안 맞는 줄 끝 중 하나를 택해야 한다. 이것 때문에 문장을 수정하는 경우도 종종 등장한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;느낀 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 말이 안 된다. html, css에 대한 매우 기초적인 지식은 있고, Jekyll로 다른 사람이 만든 Theme을 사용해 블로그를 만들어 봤지만 이 기술에 대한 이해는 분명 부족하다. 그럼에도 Codex에 해달라고 하면 어지간한 기능이 완성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 한계가 있다고 생각한다. 먼저 이건 정적 웹사이트다. css, html 잘하고 예뻐 보이면 땡이다. 이건 AI가 매우 잘하는 분야일 것이다. 서버가 있는 동적 웹사이트면 다양한 오류와 요구사항에 맞지 않는 구현이 자주 나올 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술에 대한 이해 없이 썼을 때의 문제도 있다. 먼저 토큰이다. 5시간 토큰 제한에 걸린 게 거의 처음이다. css를 잘 모르니 간단한 css 수정과 롤백도 다 Codex에 맡기게 된다. 그리고 수정을 위해 필요한 파일의 위치를 모르니 프로젝트를 다 검색하는 경우도 자주 있다. 로직을 설명하며 구현하라고 하면 더 정확하고 저렴하게 구현하지만 내가 원하는 기능을 묘사만 하고 만들어 달라고 하면 토큰이 녹는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 이유로 개발자가 필요하다고는 하지만... 결국 토큰 가성비가 좋은 모델이 나올수록 해결될 문제 아닐까? 수정해야 할 개발자가 필요하다는 것도 이전만큼 많은 개발자가 필요하지는 않다는 것이다. 기술을 아는 개발자가 필요는 하지만 많이 필요하지 않고, 기존의 개발자로 더 큰 효율을 낼 수 있는 시점이다.&lt;/p&gt;</description>
      <category>개발일지</category>
      <author>타자치는 문돌이</author>
      <guid isPermaLink="true">https://code-in-coffee.tistory.com/55</guid>
      <comments>https://code-in-coffee.tistory.com/55#entry55comment</comments>
      <pubDate>Wed, 17 Jun 2026 17:17:10 +0900</pubDate>
    </item>
    <item>
      <title>[ProjectRE] 개발 일지 1</title>
      <link>https://code-in-coffee.tistory.com/54</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byj5P7/dJMcadoIPMD/28TlEo6HPdEQCMMQ8w1Ao1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byj5P7/dJMcadoIPMD/28TlEo6HPdEQCMMQ8w1Ao1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byj5P7/dJMcadoIPMD/28TlEo6HPdEQCMMQ8w1Ao1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbyj5P7%2FdJMcadoIPMD%2F28TlEo6HPdEQCMMQ8w1Ao1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 개발 단계의 한 사이클을 돌았다고 할 정도가 진행되었다.&lt;br /&gt;이제 어떤 게임을 만드려고 하는지 보이는 프로토타입 정도가 되었다.&lt;br /&gt;며칠 정도 현재까지 개발한 내용을 검토하며 다져놔야 할 부분을 다질 타이밍인 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;복기&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 방식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LLM 도입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초창기에는 GPT를 참고용으로 활용하다가 Enhancement System 개발 즈음부터 본격적으로 Codex와 Gemini cli를 도입했다. 몇 달 사이에 너무 많이 발전했다. 몇 번의 시행착오와 조사 끝에 나만의 작업 흐름을 완성했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; 1. 기획 문서 작성&lt;/b&gt;&lt;br /&gt;구현하려는 기능의 기획을 작성한다. 어떤 기능이고, 어떤 식으로 쓰일 지, 규칙 등을 작성하고 GPT로 구체화해서 완성했다. 어떻게 구현할지 코드 관련 내용은 모두 빼고 기획 측면에서 원하는 내용을 적었다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2. 관련 자료 조사&lt;/b&gt;&lt;br /&gt;AI로 게임을 개발하며 있던 가장 큰 문제는(특히 언리얼), 이미 구현되어 있는 기능을 새로 구현하려 한다는 점이었다. 예를 들어 Ability System에는 이미 사용 가능한 Target Data와 활용 함수들이 정의되어 있지만, 이걸 활용하는 대신 새로운 Target Data를 정의하고 사용하려 한다. 이를 막기 위해 이 단계를 추가했다.&lt;br /&gt;Codex로 1의 기획을 구현하기 위해 알아두어야 할 프로젝트의 내용과 Unreal Engine의 내용, 보편적으로 구현하는 방식을 조사해 작성한다. AbilityBase를 구현하려 하면 현재 Player ASC, Gameplay Ability 등에 대해 조사해 작성한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;3. 구현 기획 문서 작성&lt;/b&gt;&lt;br /&gt;1과 2를 바탕으로 새 기능을 어떻게 구현할 지 작성한다. 클래스가 어떤 함수를 가지고, 어떤 로직으로 작동하며, 어떤 기능을 사용할지를 정리한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;4. 코드&lt;/b&gt;&lt;br /&gt;실제로 구현할 코드나 Blueprint를 작성한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계에서 마음에 안들면 직접 수정하거나 전 단계로 돌아가 내가 원하는 대로 기획이 코드로 구체화되도록 방향을 잡았다. 그리고 작성한 문서는 Gemini cli가 검토하고 피드백을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과물은 상당히 마음에 들고 원하는 방향으로 구현하지만 생각보다 검토에 시간이 오래 걸린다. 각 단계마다 300~500줄 정도의 문서가 생성되고 검토해야 한다. 쓸데없는 말을 줄이는 방향으로 promts를 설정할 수 있다면 찾아봐야겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Experience System&lt;/h3&gt;
&lt;figure id=&quot;og_1781108857577&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Lyra와 모듈형 게임플레이&quot; data-og-description=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/game-features-and-modular-gameplay-in-unreal-engineGame Feature와 ExperienceLyra는 여러가지 모드가 존재하고, 이를 Experience라는 단위로 나누어 구분한다.이때, 필요없&quot; data-og-host=&quot;code-in-coffee.tistory.com&quot; data-og-source-url=&quot;https://code-in-coffee.tistory.com/49&quot; data-og-url=&quot;https://code-in-coffee.tistory.com/49&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bZVlmo/dJMb9hC9mpA/Je9bkYBBZOBseSOD2SMBu1/img.png?width=800&amp;amp;height=283&amp;amp;face=0_0_800_283,https://scrap.kakaocdn.net/dn/GkR8z/dJMb9efmbrT/durK0GKydM5Oc2smyY7qt1/img.png?width=800&amp;amp;height=283&amp;amp;face=0_0_800_283,https://scrap.kakaocdn.net/dn/krPFX/dJMb896bUZY/OTfPUCcLA5mbO8sQgxlkFK/img.png?width=1061&amp;amp;height=1080&amp;amp;face=0_0_1061_1080&quot;&gt;&lt;a href=&quot;https://code-in-coffee.tistory.com/49&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://code-in-coffee.tistory.com/49&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bZVlmo/dJMb9hC9mpA/Je9bkYBBZOBseSOD2SMBu1/img.png?width=800&amp;amp;height=283&amp;amp;face=0_0_800_283,https://scrap.kakaocdn.net/dn/GkR8z/dJMb9efmbrT/durK0GKydM5Oc2smyY7qt1/img.png?width=800&amp;amp;height=283&amp;amp;face=0_0_800_283,https://scrap.kakaocdn.net/dn/krPFX/dJMb896bUZY/OTfPUCcLA5mbO8sQgxlkFK/img.png?width=1061&amp;amp;height=1080&amp;amp;face=0_0_1061_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Lyra와 모듈형 게임플레이&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/game-features-and-modular-gameplay-in-unreal-engineGame Feature와 ExperienceLyra는 여러가지 모드가 존재하고, 이를 Experience라는 단위로 나누어 구분한다.이때, 필요없&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;code-in-coffee.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lyra의 Experience System을 구현해 초기화 로직을 관리한다. 아직 간단한 역할만 하며 현재 주요 역할은 Player의 Pawn, Abilityset, InputConfig 등을 연결하고 초기화 타이밍을 관리하며 Experience를 로드하는 GameMode 역할이다. Main Menu -&amp;gt; Room -&amp;gt; Gameplay 부분과 Async Load가 완성되면 더 강조될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GAS 기반 전투 시스템&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byj5P7/dJMcadoIPMD/28TlEo6HPdEQCMMQ8w1Ao1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byj5P7/dJMcadoIPMD/28TlEo6HPdEQCMMQ8w1Ao1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byj5P7/dJMcadoIPMD/28TlEo6HPdEQCMMQ8w1Ao1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbyj5P7%2FdJMcadoIPMD%2F28TlEo6HPdEQCMMQ8w1Ao1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Attributeset을 기반으로 하는 능력치 시스템과, Gamplay Ability를 바탕으로 하는 AbilityBase, Gameplay Tag를 활용한 Input System과 전투 제어 로직을 구현했다. Blueprint에서 쉽게 확장 가능한 구조로 만들어 다양한 스킬을 쉽게 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Enhancement System&amp;nbsp;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RYVG0/dJMcageKTwe/6dpMutnz9STP0Ux7okyh0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RYVG0/dJMcageKTwe/6dpMutnz9STP0Ux7okyh0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RYVG0/dJMcageKTwe/6dpMutnz9STP0Ux7okyh0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRYVG0%2FdJMcageKTwe%2F6dpMutnz9STP0Ux7okyh0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로스트아크의 엘릭서 시스템을 가져온 강화 시스템을 구현했다. 강화 미리 보기의 구현이 특히 힘들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Widget Controller 기반 UI&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baAwYX/dJMcaaFESvO/cg2Jq4DVligDgH6Y8cLDe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baAwYX/dJMcaaFESvO/cg2Jq4DVligDgH6Y8cLDe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baAwYX/dJMcaaFESvO/cg2Jq4DVligDgH6Y8cLDe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaAwYX%2FdJMcaaFESvO%2Fcg2Jq4DVligDgH6Y8cLDe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Widget Controller를 기반으로 UI를 관리하는 HUD 시스템을 만들었다. WC에서 쉽게 필요한 값을 전달 받아 Widget이 보여준다. Widget 크기와 위치 관련해서는 공부가 더 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BT 기반 Enemy&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CBXm0/dJMcaftmM4e/biNwzfZnfCtdFFqhoVLEFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CBXm0/dJMcaftmM4e/biNwzfZnfCtdFFqhoVLEFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CBXm0/dJMcaftmM4e/biNwzfZnfCtdFFqhoVLEFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCBXm0%2FdJMcaftmM4e%2FbiNwzfZnfCtdFFqhoVLEFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 BT와 Blackboard를 활용한 Enemy 로직을 구현했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;막힌 부분&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기획과 에셋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 기획한 내용은 지금까지의 구현이 대부분이다. &quot;전투-&amp;gt;강화가 반복되는, 엘릭서 방식 강화를 사용하고 다양한 무기를 쓸 수 있는 쿼터뷰 액션 로그라이크&quot;. 여기서 더 나아가려면 기획을 확정해야 한다. 메인 컨셉을 어떻게 할지를 정해야 무기와 스킬, 적, 맵 스타일, 스토리 등이 정해진다. 이게 어려운 이유는 에셋이다. 메인 컨셉에 맞춰 에셋을 구하는 것보다 에셋에 맞춰 메인 컨셉이 강제로 정해질 확률이 높다. 3D 에셋을 거의 못 만지는 개발자의 한계다. 에셋을 찾으며 느낀 것은 생각보다 쿼터뷰에 맞는 애니메이션을 찾기 어렵다는 것(Paragon 에셋을 쓰니까 너무 붕쯔붕쯔 느낌이라 다른 애니메이션을 찾아야 했다)과 무료 VFX가 많이 없다는 것이다.&lt;br /&gt;돈을 쓰거나 AI를 써야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Map 구현 방식&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1153&quot; data-origin-height=&quot;711&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wf83S/dJMcahEGNkA/wpT6u7urRaPtwJtLSKdUp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wf83S/dJMcahEGNkA/wpT6u7urRaPtwJtLSKdUp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wf83S/dJMcahEGNkA/wpT6u7urRaPtwJtLSKdUp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwf83S%2FdJMcahEGNkA%2FwpT6u7urRaPtwJtLSKdUp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;493&quot; data-origin-width=&quot;1153&quot; data-origin-height=&quot;711&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 BP_Stage를 소환하는 방식으로 맵을 구현 했다. 그러나 배경을 더 신경 쓴다면 스테이지 여러 개가 채워진 맵을 만드는 것이 나을 것 같다. 아니면 PCG도 고려해 볼 만하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Enhancement System&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강화 시스템은 선택지에 따라 다양한 효과를 준다.&lt;br /&gt;카드 게임을 어떻게 구현할지 고민하며 생각해 본 것인데, 나라면 하스스톤 카드의 효과를 어떻게 구현할까?&lt;br /&gt;비슷한 기능으로 묶어서 대상-비용-규칙-효과 데이터를 저장하고 이걸 해석해서 발동하는 것이 정석적이고, Enhancement System도 이렇게 구현했지만, 사실 규칙이 복잡해지고 양이 많아지면 오히려 이걸 각각 구현하는 것이 더 낫지 않을까라는 생각도 한다. 특히 새 효과를 위해 규칙 전체나 효과 전체에 수정을 해야 하는데 이 수정이 다른 모든 카드에 영향이 없을지를 확신할 수 있을까? 하스스톤이라면 가능할 것 같다고 생각이 든다면 유희왕은 어떨까? 그 말도 안 되는 카드 효과를 컨텍스트 해석으로 풀어낼 수 있는 것일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Loading Screen&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bngJRX/dJMcahLuVBv/KLlKKN6dtuRKsF9JAwvLEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bngJRX/dJMcahLuVBv/KLlKKN6dtuRKsF9JAwvLEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bngJRX/dJMcahLuVBv/KLlKKN6dtuRKsF9JAwvLEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbngJRX%2FdJMcahLuVBv%2FKLlKKN6dtuRKsF9JAwvLEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵과 맵의 전환에서 동작하는 시스템이라 아직 버그가 많다. 맵 전환하면 아주 짧게 로딩 안된 상태의 맵이 보일 때가 있다.&lt;/p&gt;
&lt;figure id=&quot;og_1781108937432&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;IGameMoviePlayer | Unreal Engine 5.7 Documentation | Epic Developer Community&quot; data-og-description=&quot;An interface to the movie player we will use for loading screens and gameplay movies&quot; data-og-host=&quot;dev.epicgames.com&quot; data-og-source-url=&quot;https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/MoviePlayer/IGameMoviePlayer?application_version=4.27&quot; data-og-url=&quot;https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/MoviePlayer/IGameMoviePlayer&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2eEIm/dJMb9iaZirG/R7al2rU0Euu6CscTYmm5H0/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675,https://scrap.kakaocdn.net/dn/d9fKV8/dJMb9iaZirH/3K99wvVBZvMBdXE0wkhbBK/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/MoviePlayer/IGameMoviePlayer?application_version=4.27&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/MoviePlayer/IGameMoviePlayer?application_version=4.27&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2eEIm/dJMb9iaZirG/R7al2rU0Euu6CscTYmm5H0/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675,https://scrap.kakaocdn.net/dn/d9fKV8/dJMb9iaZirH/3K99wvVBZvMBdXE0wkhbBK/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;IGameMoviePlayer | Unreal Engine 5.7 Documentation | Epic Developer Community&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;An interface to the movie player we will use for loading screens and gameplay movies&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dev.epicgames.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 써보는 것도 계획 중이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;다음 개발 동안 생각해 볼 것&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 방식과 관련해 몇 가지 변화를 주려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LLM Wiki 도입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앤드류 카파시가 제안한 패턴으로, llm과의 대화를 일회성으로 끝내는 대신 llm wiki에 기록해 지식을 축적하고, 대화를 할 때 필요한 정보를 wiki에서 검색하는 방식이다.&lt;/p&gt;
&lt;figure id=&quot;og_1781109273849&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;llm-wiki&quot; data-og-description=&quot;llm-wiki. GitHub Gist: instantly share code, notes, and snippets.&quot; data-og-host=&quot;gist.github.com&quot; data-og-source-url=&quot;https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f&quot; data-og-url=&quot;https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/08wOk/dJMb8SXF7v0/k8KknDbfkRcYtfN12KALTK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/YmRa7/dJMb8UHXty6/rw6sqFXMoYzGkCskGZ97jk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640&quot;&gt;&lt;a href=&quot;https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/08wOk/dJMb8SXF7v0/k8KknDbfkRcYtfN12KALTK/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/YmRa7/dJMb8UHXty6/rw6sqFXMoYzGkCskGZ97jk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;llm-wiki&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;llm-wiki. GitHub Gist: instantly share code, notes, and snippets.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gist.github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재도 비슷한 방식을 쓰고는 있다. 기능을 구현한 다음, 방금 개발한 기능을 요약하라고 한 뒤, &lt;code&gt;IMPLEMENTATION_STATE.md&lt;/code&gt;에 기록하고 있다. 개발에서 맥락이 필요하면 이 문서를 참고해 정보를 긁어오지만, 이 문서는 벌써 1000줄을 넘어가 검색할 때 비효율적인 문서가 되었다. &lt;code&gt;IMPLEMENTATION_STATE.md&lt;/code&gt;를 대체하는 기능으로 프로젝트에 한정한 llm wiki를 구성할 계획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;은근히 거슬리는 폴더 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex는 &lt;code&gt;.gitignore&lt;/code&gt;를 반영하고, 이걸 수정할 방법이 아직 없는 것 같다. Gemini는 &lt;code&gt;.gitignore&lt;/code&gt; 무시하기 옵션이 있지만 Codex는 찾을 수 없다. llm 관련 파일이 많이 존재하는데 &lt;code&gt;.gitignore&lt;/code&gt;에 등록하면 무시되고, 안 하면 계속 unstaged change에 보인다. 생각해 낸 꼼수는 프로젝트 폴더를 감싸는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 프로젝트 root에 &lt;code&gt;.gemini&lt;/code&gt;, &lt;code&gt;.agent&lt;/code&gt;폴더를 두고 여기서 Codex나 Gemini를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;// 기존 구조
ProjectRE/  
├─ Source/  
├─ Content/
├─ ...
├─ .gemini/
├─ .agents/
│  ├─ commands/  
│  ├─ ...
│  └─ docs/
├─ .gitignore
├─ ...
└─ ProjectName.uproject
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 구상 중인 방법은 프로젝트 폴더 밖에 llm 관련 파일을 놓고 실행하는 방식이다. 이러면 거슬리는 llm 파일을 git의 범위에서 뺄 수 있다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;ProjectRE/
├─ ProjectRE/  
│  ├─ Source/  
│  ├─ Content/
│  ├─ .gitignore
│  ├─ ...
│  └─ ProjectName.uproject
├─ .gemini/
├─ .agents/
├─ feature/
└─ wiki/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;객체 지향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 지향적인 코드를 짜기 위해 노력했지만 ai를 활용한 개발을 하며 새로운 관점으로 생각해 보기 시작했다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, '돋움', sans-serif;&quot;&gt;게임의 객체들이 단독으로 존재하고, 다른 요소를 모르거나 없어도 작동해야 한다.&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 내부를 모르는 상태에서도 로직이 돌아가면 ai도 상대 객체를 파악하지 않아도 된다. 아직 ai는 다른 요소를 파악하고 그것까지 감안해 줄 정도로 똑똑하지는 않다. 상대 객체 파악은 많은 토큰을 쓰기도 한다. 차라리 단독으로도 잘 작동하고 상호작용 가능한 객체를 만드는 것이 낫다고 생각한다. Component도 더 적극적으로 활용해야겠다. 역할과 기능이 얽힐수록 ai가 멍청한 대답을 준다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도입해 볼 만한 기술&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PCG, Level Streaming&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맵을 PCG로 생성하는 것도 후보에 있다. 아니면 방마다 Level Streaming으로 로드/언로드 할 수도 있다. 맵은 어떻게 구현할지 더 고민해봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Motion Matching&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동 애니메이션을 아직은 크게 신경 안 쓰고 있지만 자연스러운 애니메이션 전환이 가능하다면 도입해보려고 한다. 다만 대부분 예제가 3인칭 백뷰 기준이라 그대로 적용하면 회전이 이상하게 동작한다. 이 문제를 고쳐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Async Load&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Gameplay Camera&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세히 조사는 안 한 기능이다. GAS와 조합이 좋아서 컷신 연출 등이 간편하면 사용해 볼 계획이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흠...&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 일주일 정도 쉬면서 프로젝트의 클래스 다이어그램을 그려봐야겠다. 프로토타입 정도까지 되니까 어떤 요소로 내가 원하는 것을 표현하면 될지 큰 그림이 잡혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqixYg/dJMcaiDDPVy/0FJ9Hh3JK2joF2eqkg69R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqixYg/dJMcaiDDPVy/0FJ9Hh3JK2joF2eqkg69R0/img.png&quot; data-alt=&quot;&amp;quot;대화를 바탕으로 이 게임이 완성됐을 때의 모습을 그려줘.&amp;quot;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqixYg/dJMcaiDDPVy/0FJ9Hh3JK2joF2eqkg69R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqixYg%2FdJMcaiDDPVy%2F0FJ9Hh3JK2joF2eqkg69R0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&quot;대화를 바탕으로 이 게임이 완성됐을 때의 모습을 그려줘.&quot;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림은 참 잘 그린단 말이지...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발일지</category>
      <category>devlog</category>
      <category>projectre</category>
      <category>개발일지</category>
      <author>타자치는 문돌이</author>
      <guid isPermaLink="true">https://code-in-coffee.tistory.com/54</guid>
      <comments>https://code-in-coffee.tistory.com/54#entry54comment</comments>
      <pubDate>Thu, 11 Jun 2026 01:49:30 +0900</pubDate>
    </item>
    <item>
      <title>[TinyRenderer] Lesson 1. Bresenham 직선 알고리즘</title>
      <link>https://code-in-coffee.tistory.com/53</link>
      <description>&lt;figure id=&quot;og_1780829558149&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Lesson 1: Bresenham&amp;rsquo;s line drawing algorithm &amp;middot; TaeAhnK/TinyRenderer@c308465&quot; data-og-description=&quot;+ void line(int x0, int y0, int x1, int y1, TGAImage&amp;amp; framebuffer, TGAColor color)&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/TaeAhnK/TinyRenderer/commit/c3084650f6269952a8887370558e1c022093552e&quot; data-og-url=&quot;https://github.com/TaeAhnK/TinyRenderer/commit/c3084650f6269952a8887370558e1c022093552e&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/6uAhh/dJMb9cBPL03/JjHgyMplv5KbeVhU9Y4Jc1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/h0YXT/dJMb87gekur/h9H2pqFkQ14iYOvJIcdyPK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/TaeAhnK/TinyRenderer/commit/c3084650f6269952a8887370558e1c022093552e&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/TaeAhnK/TinyRenderer/commit/c3084650f6269952a8887370558e1c022093552e&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/6uAhh/dJMb9cBPL03/JjHgyMplv5KbeVhU9Y4Jc1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/h0YXT/dJMb87gekur/h9H2pqFkQ14iYOvJIcdyPK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Lesson 1: Bresenham&amp;rsquo;s line drawing algorithm &amp;middot; TaeAhnK/TinyRenderer@c308465&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;+ void line(int x0, int y0, int x1, int y1, TGAImage&amp;amp; framebuffer, TGAColor color)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점을 그렸으니, 이제 선을 그려야 한다.&lt;br /&gt;두 점을 잇는 선을 어떻게 그릴까?&lt;br /&gt;픽셀 위에서는 결국 점을 찍어야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법은 보간을 사용하는 방식으로, 두 점 사이를 나타내는 식을 구하고, 시작점을 0, 끝점을 1로 둔 뒤, 0에서 1까지의 값을 함수에 넣으며 선 위의 좌표를 계산해 점을 찍는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 점 $(x_0, y_0)$, $(x_1, y_1)$, 보간 매개변수 $t (0 \le t \le1)$에 대해&lt;br /&gt;$$&lt;br /&gt;\begin{aligned}&lt;br /&gt;x &amp;amp;= x_0 + (x_1 - x_0) \times t \\&lt;br /&gt;y &amp;amp;= y_0 + (y_1 - y_0) \times t&lt;br /&gt;\end{aligned}&lt;br /&gt;$$&lt;br /&gt;은 두 점을 잇는 선 위에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 코드로 구현하면, 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;void line(int x0, int y0, int x1, int y1, TGAImage&amp;amp; framebuffer, TGAColor color)
{
    for (float t = 0.; t &amp;lt; 1.; t += 0.02)
    {
        int x = x0 + ((x1 - x0) * t);
        int y = y0 + ((y1 - y0) * t);

        framebuffer.set(x, y, color);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 이용해 선을 그려보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int main()
{
    constexpr int width = 64;
    constexpr int height = 64;

    TGAImage framebuffer(width, height, TGAImage::RGB);

    line(7, 3, 12, 37, framebuffer, white);
    line(12, 37, 62, 53, framebuffer, red);
    line(62, 53, 7, 3, framebuffer, blue);
    line(62, 53, 12, 37, framebuffer, green);

    framebuffer.write_tga_file(&quot;output/framebuffer.tga&quot;);

    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260603172843.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNWeMR/dJMcahrbNU5/e5VDkhNj2SnTMehr1QQIG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNWeMR/dJMcahrbNU5/e5VDkhNj2SnTMehr1QQIG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNWeMR/dJMcahrbNU5/e5VDkhNj2SnTMehr1QQIG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNWeMR%2FdJMcahrbNU5%2Fe5VDkhNj2SnTMehr1QQIG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;300&quot; data-filename=&quot;Pasted image 20260603172843.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 몇 가지 문제가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;초록과 빨강은 같은 선을 그려야 하지만, 실제로는 다른 값이 칠해져 빨강이 보인다.&lt;/li&gt;
&lt;li&gt;파랑 중간중간에 빈 공간이 있다. (7,3)과 (62, 53)을 잇기 위해선 최소한 62-7=55 픽셀이 필요하지만, t가 0.02씩 증가해 51개의 픽셀만 칠했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 더 나은 방법이 필요하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;t를 0.02같은 고정된 숫자만큼 늘리는 방식 대신 x를 1씩 증가시키고, 선에서의 진행도 t를 구해 y를 계산할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void line(int x0, int y0, int x1, int y1, TGAImage&amp;amp; framebuffer, TGAColor color)  
{  
    for (int x = x0; x &amp;lt;= x1; x++)  
    {  
        float t = static_cast&amp;lt;float&amp;gt;(x - x0) / static_cast&amp;lt;float&amp;gt;(x1 - x0);
        int y = static_cast&amp;lt;int&amp;gt;(y0 * (1.0f - t) + y1 * t);  

        framebuffer.set(x, y, color);  
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260603174323.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdTbNz/dJMcaijh648/Dzk4hhNrbmzSdc7TppE1z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdTbNz/dJMcaijh648/Dzk4hhNrbmzSdc7TppE1z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdTbNz/dJMcaijh648/Dzk4hhNrbmzSdc7TppE1z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdTbNz%2FdJMcaijh648%2FDzk4hhNrbmzSdc7TppE1z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;300&quot; data-filename=&quot;Pasted image 20260603174323.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 가지 문제가 생겼다. 파랑은 사라지고, 흰색은 간격이 매우 넓어졌다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파랑은 (62, 53) -&amp;gt; (7, 3)으로 x가 줄어들어 for문이 작동하지 않았다.&lt;/li&gt;
&lt;li&gt;흰색은 x만을 기준으로 해 간격이 매우 넓다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, sample이 이번에도 적절하지 않았다.&lt;br /&gt;우선 파랑 문제를 해결해 보자.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void line(int x0, int y0, int x1, int y1, TGAImage&amp;amp; framebuffer, TGAColor color)
{
    if (x0 &amp;gt; x1) { // make it left&amp;minus;to&amp;minus;right
        std::swap(x0, x1);
        std::swap(y0, y1);
    }

    for (int x = x0; x &amp;lt;= x1; x++)
    {
        float t = static_cast&amp;lt;float&amp;gt;(x - x0) / static_cast&amp;lt;float&amp;gt;(x1 - x0);
        int y = static_cast&amp;lt;int&amp;gt;(y0 * (1.0f - t) + y1 * t);

        framebuffer.set(x, y, color);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260603174842.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chIUD0/dJMcahxVW93/juG8Oop1EYjG4HB2maJrrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chIUD0/dJMcahxVW93/juG8Oop1EYjG4HB2maJrrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chIUD0/dJMcahxVW93/juG8Oop1EYjG4HB2maJrrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchIUD0%2FdJMcahxVW93%2FjuG8Oop1EYjG4HB2maJrrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;300&quot; data-filename=&quot;Pasted image 20260603174842.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선이 가파를 때 흰색과 같은 문제가 생긴다.&lt;br /&gt;그러면 가파를 때 x 대신 y를 쓰면 어떨까?&lt;br /&gt;x 차이보다 y 차이가 더 크면 y를 기준으로 선을 그려보자.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void line(int x0, int y0, int x1, int y1, TGAImage&amp;amp; framebuffer, TGAColor color)
{
    bool steep = std::abs(x0 - x1) &amp;lt; std::abs(y0 - y1);
    if (steep) { // if the line is steep, we transpose the image
        std::swap(x0, y0);
        std::swap(x1, y1);
    }

    if (x0 &amp;gt; x1) { // make it left&amp;minus;to&amp;minus;right
        std::swap(x0, x1);
        std::swap(y0, y1);
    }

    for (int x = x0; x &amp;lt;= x1; x++)
    {
        float t = static_cast&amp;lt;float&amp;gt;(x - x0) / static_cast&amp;lt;float&amp;gt;(x1 - x0);
        int y = static_cast&amp;lt;int&amp;gt;(y0 * (1.0f - t) + y1 * t);

        if (steep)
            framebuffer.set(y, x, color);
        else
            framebuffer.set(x, y, color);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260603175356.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lzggN/dJMb997Iu6f/lmSvUVDDSYReCq7ONH9ZL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lzggN/dJMb997Iu6f/lmSvUVDDSYReCq7ONH9ZL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lzggN/dJMb997Iu6f/lmSvUVDDSYReCq7ONH9ZL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlzggN%2FdJMb997Iu6f%2FlmSvUVDDSYReCq7ONH9ZL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;300&quot; data-filename=&quot;Pasted image 20260603175356.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 원하는 대로 선을 그리지만, 아직 비효율적인 부분이 많다.&lt;br /&gt;이를 최적화한 최종 버전을 만들어보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜토리얼에 따르면 이 부분에는 언급되지 않은 역사적/문화적 배경이 많고, 특히 CPU냐 GPU냐에 따라 다른 결과가 나올 것이라고 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 기존 방식에서 1,600만 회 정도 선을 그려보자.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;int main()
{
    constexpr int width = 64;
    constexpr int height = 64;

    TGAImage framebuffer(width, height, TGAImage::RGB);

    auto start = std::chrono::high_resolution_clock::now();

    std::srand(std::time({}));
    for (int i = 0; i &amp;lt; (1 &amp;lt;&amp;lt; 24); i++) {
        int ax = rand() % width, ay = rand() % height;
        int bx = rand() % width, by = rand() % height;

        line(ax, ay, bx, by, framebuffer, { static_cast&amp;lt;unsigned char&amp;gt;(rand() % 255), 
            static_cast&amp;lt;unsigned char&amp;gt;(rand() % 255), 
            static_cast&amp;lt;unsigned char&amp;gt;(rand() % 255), 
            static_cast&amp;lt;unsigned char&amp;gt;(rand() % 255) });
    }

    auto end = std::chrono::high_resolution_clock::now();

    double elapsedSec = std::chrono::duration&amp;lt;double&amp;gt;(end - start).count();

    std::cout &amp;lt;&amp;lt; &quot;Line rendering time: &quot; &amp;lt;&amp;lt; elapsedSec &amp;lt;&amp;lt; &quot; s\n&quot;;

    framebuffer.write_tga_file(&quot;output/framebuffer.tga&quot;);

    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260603180745.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bopvo1/dJMcafGT6Cf/7euv4FaV0q9sXKbz9qDknk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bopvo1/dJMcafGT6Cf/7euv4FaV0q9sXKbz9qDknk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bopvo1/dJMcafGT6Cf/7euv4FaV0q9sXKbz9qDknk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbopvo1%2FdJMcafGT6Cf%2F7euv4FaV0q9sXKbz9qDknk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;300&quot; data-filename=&quot;Pasted image 20260603180745.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;Line rendering time: 10.9131 s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 방식은 픽셀마다&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;float t = (x - x0) / (float)(x1 - x0);  
int y = y0 * (1.0f - t) + y1 * t;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 계산한다.&lt;br /&gt;이때 t에 대해 증가하는 y 비율은 항상 같다.&lt;br /&gt;t와 y를 계산하는 대신, 기울기만큼 y를 누적하는 방식으로 바꾸면 y를 매번 계산할 필요가 없다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void line(int x0, int y0, int x1, int y1, TGAImage&amp;amp; framebuffer, TGAColor color)
{
    bool steep = std::abs(x0 - x1) &amp;lt; std::abs(y0 - y1);
    if (steep) { // if the line is steep, we transpose the image
        std::swap(x0, y0);
        std::swap(x1, y1);
    }

    if (x0 &amp;gt; x1) { // make it left&amp;minus;to&amp;minus;right
        std::swap(x0, x1);
        std::swap(y0, y1);
    }

    float slope = (y1 - y0) / static_cast&amp;lt;float&amp;gt;(x1 - x0);
    float y = y0;

    for (int x = x0; x &amp;lt;= x1; x++) {
        if (steep) // if transposed, de&amp;minus;transpose
            framebuffer.set(y, x, color);
        else
            framebuffer.set(x, y, color);
        y += slope;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;Line rendering time: 10.0969 s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 우리는 기울기만큼 y를 올리고 있지만, y는 정수 좌표만 유효하다. 그러면 소수 부분을 누적했다가 정수 부분이 바뀌면 1씩 올리는 방식을 사용해 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void line(int x0, int y0, int x1, int y1, TGAImage&amp;amp; framebuffer, TGAColor color)
{
    bool steep = std::abs(x0 - x1) &amp;lt; std::abs(y0 - y1);
    if (steep) { // if the line is steep, we transpose the image
        std::swap(x0, y0);
        std::swap(x1, y1);
    }

    if (x0 &amp;gt; x1) { // make it left&amp;minus;to&amp;minus;right
        std::swap(x0, x1);
        std::swap(y0, y1);
    }

    float slope = abs(y1 - y0) / static_cast&amp;lt;float&amp;gt;(x1 - x0);
    int y = y0;
    float error = 0;

    for (int x = x0; x &amp;lt;= x1; x++) {
        if (steep) // if transposed, de&amp;minus;transpose
            framebuffer.set(y, x, color);
        else
            framebuffer.set(x, y, color);
        error += slope;
        if (error &amp;gt; 0.5) 
        {
            y += y1 &amp;gt; y0 ? 1 : -1;
            error -= 1.;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;Line rendering time: 11.8649 s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산이 늘어 시간이 늘어났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 소수점 연산을 없앨 수 있을까?&lt;br /&gt;오차를 적절히 scale해 &lt;code&gt;int&lt;/code&gt;로 만들어보자.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void line(int x0, int y0, int x1, int y1, TGAImage&amp;amp; framebuffer, TGAColor color)
{
    bool steep = std::abs(x0 - x1) &amp;lt; std::abs(y0 - y1);
    if (steep) { // if the line is steep, we transpose the image
        std::swap(x0, y0);
        std::swap(x1, y1);
    }

    if (x0 &amp;gt; x1) { // make it left&amp;minus;to&amp;minus;right
        std::swap(x0, x1);
        std::swap(y0, y1);
    }

    int dx = x1 - x0;
    int dy = y1 - y0;
    int absdy2 = 2 * std::abs(dy);
    int dx2 = 2 * dx;
    int ystep = dy &amp;gt; 0 ? 1 : -1;

    int y = y0;
    int ierror = 0;

    for (int x = x0; x &amp;lt;= x1; x++) {
        if (steep)
            framebuffer.set(y, x, color);
        else
            framebuffer.set(x, y, color);

        ierror += absdy2;

        if (ierror &amp;gt; dx) {
            y += ystep;
            ierror -= dx2;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 &lt;code&gt;x1 - x0&lt;/code&gt;으로 나누는 대신, 오차를 &lt;code&gt;2*(x1 - x0)&lt;/code&gt;배 scale해 정수로 관리한다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;Line rendering time: 11.8008 s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;float 연산이 있는 버전보다 빨라지긴 했지만, 오히려 처음 방식보다 느리다.&lt;br /&gt;CPU 관점에서 CPU는 명령어 가져오기, 해석하기, 계산, 메모리 읽기/쓰기 등의 작업을 나눠서 처리한다. 이걸 순차적으로 처리하면 느리므로 여러 명령어를 겹쳐서 처리하는 Pipelining을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;명령어 A: 실행 중  
명령어 B: 해석 중  
명령어 C: 가져오는 중&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &lt;code&gt;if&lt;/code&gt;와 같은 분기문은 흐름을 끊을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;if (condition) 
{  
    A();  
} 
else 
{  
    B();  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 입장에서는 다음에 실행해야 할 코드가 A인지 B인지 모른다. 따라서 condition 계산 전에 미리 사용할 명령어를 예측한다. 이때 예측이 틀리면 성능 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 여기서 병목이 생긴다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;    if (ierror &amp;gt; dx) {
        y += ystep;
        ierror -= dx2;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해소하기 위해 branchless form으로 다음과 같이 수정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void line(int x0, int y0, int x1, int y1, TGAImage&amp;amp; framebuffer, TGAColor color)
{
    bool steep = std::abs(x0 - x1) &amp;lt; std::abs(y0 - y1);
    if (steep) { // if the line is steep, we transpose the image
        std::swap(x0, y0);
        std::swap(x1, y1);
    }

    if (x0 &amp;gt; x1) { // make it left&amp;minus;to&amp;minus;right
        std::swap(x0, x1);
        std::swap(y0, y1);
    }

    int dx = x1 - x0;
    int dy = y1 - y0;
    int absdy2 = 2 * std::abs(dy);
    int dx2 = 2 * dx;
    int ystep = dy &amp;gt; 0 ? 1 : -1;

    int y = y0;
    int ierror = 0;

    for (int x = x0; x &amp;lt;= x1; x++) {
        if (steep)
            framebuffer.set(y, x, color);
        else
            framebuffer.set(x, y, color);

        ierror += absdy2;

        int over = ierror &amp;gt; dx;
        y += ystep * over;
        ierror -= dx2 * over;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;Line rendering time: 11.4141 s&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 선을 이용한 Wireframe Rendering이 가능하다.&lt;br /&gt;Obj 파일은 &lt;code&gt;v (x) (y) (z)&lt;/code&gt;, &lt;code&gt;vt (u) (v)&lt;/code&gt;, &lt;code&gt;vn (x) (y) (z)&lt;/code&gt;, &lt;code&gt;f (a/b/c) (ta/tb/tc) (na/nb/nc)&lt;/code&gt;가 나열되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;v (x) (y) (z)&lt;/code&gt;는 점의 3D 위치이다. 아직 TinyRender는 &lt;code&gt;x&lt;/code&gt;와 &lt;code&gt;y&lt;/code&gt;만 사용할 수 있다.&lt;br /&gt;&lt;code&gt;vt (u) (v)&lt;/code&gt;는 텍스처의 위치이다. 이번에는 무시할 것이다.&lt;br /&gt;&lt;code&gt;vn (x) (y) (z)&lt;/code&gt;는 Normal Vector로 표면이 향하는 방향을 의미한다. 빛 계산에 필요하고, 이번에는 무시한다.&lt;br /&gt;&lt;code&gt;f (a/ta/na) (b/tb/nb) (c/tc/nc)&lt;/code&gt;는 연결되어 Face를 만드는 세 좌표를 의미한다. &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;,는 좌표를 저장한 &lt;code&gt;v&lt;/code&gt; 인덱스, &lt;code&gt;ta&lt;/code&gt;, &lt;code&gt;tb&lt;/code&gt;, &lt;code&gt;tc&lt;/code&gt;는 &lt;code&gt;vt&lt;/code&gt;에서의 인덱스, &lt;code&gt;na&lt;/code&gt;, &lt;code&gt;nb&lt;/code&gt;, &lt;code&gt;nc&lt;/code&gt;는 &lt;code&gt;vn&lt;/code&gt;에서의 인덱스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 목표는 &lt;code&gt;z&lt;/code&gt;를 무시한 &lt;code&gt;v&lt;/code&gt;를, &lt;code&gt;f&lt;/code&gt;를 기준으로 연결해 렌더링 된 이미지를 생성하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;code&gt;v&lt;/code&gt;를 저장하려면 Vector3가 필요하나 Vector3가 없어 직접 구현한다.&lt;/p&gt;
&lt;pre class=&quot;thrift&quot;&gt;&lt;code&gt;typedef struct vec3
{
    double x;
    double y;
    double z;
} vec3;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;obj 파일을 담기 위한 클래스 &lt;code&gt;Model&lt;/code&gt;을 만들자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;model.h&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#pragma once

#include &amp;lt;vector&amp;gt;
#include &quot;geometry.h&quot;
#include &amp;lt;string&amp;gt;

class Model
{
    std::vector&amp;lt;vec3&amp;gt; verts = {};
    std::vector&amp;lt;int&amp;gt; facet_vrt = {};

public:
    Model(const std::string filename);
    int nverts() const;
    int nfaces() const;
    vec3 vert(const int i) const;
    vec3 vert(const int iface, const int nthvert) const;
};&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;model.cpp&lt;/p&gt;
&lt;pre id=&quot;code_1780829820961&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;fstream&amp;gt;
#include &amp;lt;sstream&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &quot;model.h&quot;

Model::Model(const std::string filename)
{
    std::ifstream in;

    in.open(filename, std::ifstream::in);

    if (in.fail())
    {
        std::cerr &amp;lt;&amp;lt; &quot;Failed to open OBJ file: &quot; &amp;lt;&amp;lt; filename &amp;lt;&amp;lt; '\n';
        return;
    }

    std::string line;

    while (std::getline(in, line))
    {
        std::istringstream iss(line);

        if (line.rfind(&quot;v&quot;, 0) == 0)
        {
            char prefix;
            vec3 vertex;

            iss &amp;gt;&amp;gt; prefix &amp;gt;&amp;gt; vertex.x &amp;gt;&amp;gt; vertex.y &amp;gt;&amp;gt; vertex.z;

            verts.push_back(vertex);
        }
        else if (line.rfind(&quot;f&quot;, 0) == 0)
        {
            char prefix;
            char slash;
            int a, b, c;
            int ta, tb, tc;
            int na, nb, nc;

            iss &amp;gt;&amp;gt; prefix;

            iss &amp;gt;&amp;gt; a &amp;gt;&amp;gt; slash &amp;gt;&amp;gt; ta &amp;gt;&amp;gt; slash &amp;gt;&amp;gt; na;
            facet_vrt.push_back(a - 1);

            iss &amp;gt;&amp;gt; b &amp;gt;&amp;gt; slash &amp;gt;&amp;gt; tb &amp;gt;&amp;gt; slash &amp;gt;&amp;gt; nb;
            facet_vrt.push_back(b - 1);

            iss &amp;gt;&amp;gt; c &amp;gt;&amp;gt; slash &amp;gt;&amp;gt; tc &amp;gt;&amp;gt; slash &amp;gt;&amp;gt; nc;
            facet_vrt.push_back(c - 1);
        }
    }

}

int Model::nverts() const
{
    return verts.size();
}

int Model::nfaces() const
{
    return facet_vrt.size()/3;
}

vec3 Model::vert(const int i) const
{
    return verts[i];
}

vec3 Model::vert(const int iface, const int nthvert) const
{
    return verts[facet_vrt[iface * 3 + nthvert]];
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;obj 파일을 읽을 때는 a/b/c의 값이 반드시 존재하고, 한 줄에 3번 나타나는 파일임을 가정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데이터를 바탕으로 &lt;code&gt;main&lt;/code&gt;에서 obj를 렌더링해 보자.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;int main()
{
    constexpr int width = 800;
    constexpr int height = 800;

    TGAImage framebuffer(width, height, TGAImage::RGB);

    Model model(&quot;obj/diablo3_pose.obj&quot;);

    for (int i = 0; i &amp;lt; model.nfaces(); i++)
    {
        vec3 a = model.vert(i, 0);
        int ax = static_cast&amp;lt;int&amp;gt;((a.x + 1.f) * width / 2.f);
        int ay = static_cast&amp;lt;int&amp;gt;((a.y + 1.f) * height / 2.f);

        vec3 b = model.vert(i, 1);
        int bx = static_cast&amp;lt;int&amp;gt;((b.x + 1.f) * width / 2.f);
        int by = static_cast&amp;lt;int&amp;gt;((b.y + 1.f) * height / 2.f);

        vec3 c = model.vert(i, 2);
        int cx = static_cast&amp;lt;int&amp;gt;((c.x + 1.f) * width / 2.f);
        int cy = static_cast&amp;lt;int&amp;gt;((c.y + 1.f) * height / 2.f);

        line(ax, ay, bx, by, framebuffer, red);
        line(bx, by, cx, cy, framebuffer, red);
        line(cx, cy, ax, ay, framebuffer, red);
    }


    framebuffer.write_tga_file(&quot;output/framebuffer.tga&quot;);

    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260607193855.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG1WTJ/dJMcahrbN1x/Y0IutScGGfdrxnkzzTvKW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG1WTJ/dJMcahrbN1x/Y0IutScGGfdrxnkzzTvKW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG1WTJ/dJMcahrbN1x/Y0IutScGGfdrxnkzzTvKW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG1WTJ%2FdJMcahrbN1x%2FY0IutScGGfdrxnkzzTvKW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;800&quot; data-filename=&quot;Pasted image 20260607193855.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>etc</category>
      <category>graphics</category>
      <category>tinyrenderer</category>
      <author>타자치는 문돌이</author>
      <guid isPermaLink="true">https://code-in-coffee.tistory.com/53</guid>
      <comments>https://code-in-coffee.tistory.com/53#entry53comment</comments>
      <pubDate>Sun, 7 Jun 2026 19:58:49 +0900</pubDate>
    </item>
    <item>
      <title>[TinyRenderer] Lesson 0. 시작하기</title>
      <link>https://code-in-coffee.tistory.com/52</link>
      <description>&lt;figure id=&quot;og_1780829286327&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Lesson 0: getting started &amp;middot; TaeAhnK/TinyRenderer@206b974&quot; data-og-description=&quot;Create a tga file and draw points&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/TaeAhnK/TinyRenderer/commit/206b97494661be4459a77976fe38e9220abd685e&quot; data-og-url=&quot;https://github.com/TaeAhnK/TinyRenderer/commit/206b97494661be4459a77976fe38e9220abd685e&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RQS2U/dJMb9fZC4PJ/PF3lGPG7hCpQEs0gS3D2Nk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cYZtkG/dJMb8Rj9Skw/Ul3JTmZAtpTOo50JCFfqhK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/TaeAhnK/TinyRenderer/commit/206b97494661be4459a77976fe38e9220abd685e&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/TaeAhnK/TinyRenderer/commit/206b97494661be4459a77976fe38e9220abd685e&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RQS2U/dJMb9fZC4PJ/PF3lGPG7hCpQEs0gS3D2Nk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cYZtkG/dJMb8Rj9Skw/Ul3JTmZAtpTOo50JCFfqhK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Lesson 0: getting started &amp;middot; TaeAhnK/TinyRenderer@206b974&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Create a tga file and draw points&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 &lt;a href=&quot;https://haqr.eu/tinyrenderer/&quot;&gt;ssloy/tinyrenderer&lt;/a&gt;를 보고 진행한 실습으로, OpenGL, DirectX, Vulkan과 같은 현대 3D 그래픽 API가 작동하는 원리를 재현한 렌더러 프로그램이다.&lt;br /&gt;그래픽 API는 사용하지 않았고, 그래픽 API가 동작하는 원리를 학습하기 위해 진행하였다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260603160829.png&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;1350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qRie1/dJMb99T8vBN/ErX4ldIrMmG1cbnwKz0DBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qRie1/dJMb99T8vBN/ErX4ldIrMmG1cbnwKz0DBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qRie1/dJMb99T8vBN/ErX4ldIrMmG1cbnwKz0DBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqRie1%2FdJMb99T8vBN%2FErX4ldIrMmG1cbnwKz0DBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;533&quot; data-filename=&quot;Pasted image 20260603160829.png&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;1350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Visual Studio 2022에서 빈 프로젝트를 만들고, &lt;code&gt;main.cpp&lt;/code&gt; 를 추가한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int main()
{
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 첫 목표는 이미지 파일 한 장을 직접 생성하고, 픽셀에 점을 찍어보는 것이다. &lt;br /&gt;TinyRenderer는 &lt;code&gt;.tga&lt;/code&gt;라는 형식의 이미지 파일로 이미지를 저장한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;code&gt;tga&lt;/code&gt; 이미지를 정의하는 클래스가 필요하다. &lt;code&gt;tinyrenderer&lt;/code&gt;의 &lt;code&gt;tgaimage.h&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tgaimage.h&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;#pragma once
#include &amp;lt;cstdint&amp;gt;
#include &amp;lt;fstream&amp;gt;
#include &amp;lt;vector&amp;gt;

#pragma pack(push,1)
struct TGAHeader {
    std::uint8_t  idlength = 0;
    std::uint8_t  colormaptype = 0;
    std::uint8_t  datatypecode = 0;
    std::uint16_t colormaporigin = 0;
    std::uint16_t colormaplength = 0;
    std::uint8_t  colormapdepth = 0;
    std::uint16_t x_origin = 0;
    std::uint16_t y_origin = 0;
    std::uint16_t width = 0;
    std::uint16_t height = 0;
    std::uint8_t  bitsperpixel = 0;
    std::uint8_t  imagedescriptor = 0;
};
#pragma pack(pop)

struct TGAColor {
    std::uint8_t bgra[4] = {0,0,0,0};
    std::uint8_t bytespp = 4;
    std::uint8_t&amp;amp; operator[](const int i) { return bgra[i]; }
};

struct TGAImage {
    enum Format { GRAYSCALE=1, RGB=3, RGBA=4 };
    TGAImage() = default;
    TGAImage(const int w, const int h, const int bpp);
    bool  read_tga_file(const std::string filename);
    bool write_tga_file(const std::string filename, const bool vflip=true, const bool rle=true) const;
    void flip_horizontally();
    void flip_vertically();
    TGAColor get(const int x, const int y) const;
    void set(const int x, const int y, const TGAColor &amp;amp;c);
    int width()  const;
    int height() const;
private:
    bool   load_rle_data(std::ifstream &amp;amp;in);
    bool unload_rle_data(std::ofstream &amp;amp;out) const;
    int w = 0, h = 0;
    std::uint8_t bpp = 0;
    std::vector&amp;lt;std::uint8_t&amp;gt; data = {};
};&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tgaimage.cpp&lt;/code&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1780499397258&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &quot;tgaimage.h&quot;

TGAImage::TGAImage(const int w, const int h, const int bpp) : w(w), h(h), bpp(bpp), data(w*h*bpp, 0) {}

bool TGAImage::read_tga_file(const std::string filename) {
    std::ifstream in;
    in.open(filename, std::ios::binary);
    if (!in.is_open()) {
        std::cerr &amp;lt;&amp;lt; &quot;can't open file &quot; &amp;lt;&amp;lt; filename &amp;lt;&amp;lt; &quot;\n&quot;;
        return false;
    }
    TGAHeader header;
    in.read(reinterpret_cast&amp;lt;char *&amp;gt;(&amp;amp;header), sizeof(header));
    if (!in.good()) {
        std::cerr &amp;lt;&amp;lt; &quot;an error occured while reading the header\n&quot;;
        return false;
    }
    w   = header.width;
    h   = header.height;
    bpp = header.bitsperpixel&amp;gt;&amp;gt;3;
    if (w&amp;lt;=0 || h&amp;lt;=0 || (bpp!=GRAYSCALE &amp;amp;&amp;amp; bpp!=RGB &amp;amp;&amp;amp; bpp!=RGBA)) {
        std::cerr &amp;lt;&amp;lt; &quot;bad bpp (or width/height) value\n&quot;;
        return false;
    }
    size_t nbytes = bpp*w*h;
    data = std::vector&amp;lt;std::uint8_t&amp;gt;(nbytes, 0);
    if (3==header.datatypecode || 2==header.datatypecode) {
        in.read(reinterpret_cast&amp;lt;char *&amp;gt;(data.data()), nbytes);
        if (!in.good()) {
            std::cerr &amp;lt;&amp;lt; &quot;an error occured while reading the data\n&quot;;
            return false;
        }
    } else if (10==header.datatypecode||11==header.datatypecode) {
        if (!load_rle_data(in)) {
            std::cerr &amp;lt;&amp;lt; &quot;an error occured while reading the data\n&quot;;
            return false;
        }
    } else {
        std::cerr &amp;lt;&amp;lt; &quot;unknown file format &quot; &amp;lt;&amp;lt; (int)header.datatypecode &amp;lt;&amp;lt; &quot;\n&quot;;
        return false;
    }
    if (!(header.imagedescriptor &amp;amp; 0x20))
        flip_vertically();
    if (header.imagedescriptor &amp;amp; 0x10)
        flip_horizontally();
    std::cerr &amp;lt;&amp;lt; w &amp;lt;&amp;lt; &quot;x&quot; &amp;lt;&amp;lt; h &amp;lt;&amp;lt; &quot;/&quot; &amp;lt;&amp;lt; bpp*8 &amp;lt;&amp;lt; &quot;\n&quot;;
    return true;
}

bool TGAImage::load_rle_data(std::ifstream &amp;amp;in) {
    size_t pixelcount = w*h;
    size_t currentpixel = 0;
    size_t currentbyte  = 0;
    TGAColor colorbuffer;
    do {
        std::uint8_t chunkheader = 0;
        chunkheader = in.get();
        if (!in.good()) {
            std::cerr &amp;lt;&amp;lt; &quot;an error occured while reading the data\n&quot;;
            return false;
        }
        if (chunkheader&amp;lt;128) {
            chunkheader++;
            for (int i=0; i&amp;lt;chunkheader; i++) {
                in.read(reinterpret_cast&amp;lt;char *&amp;gt;(colorbuffer.bgra), bpp);
                if (!in.good()) {
                    std::cerr &amp;lt;&amp;lt; &quot;an error occured while reading the header\n&quot;;
                    return false;
                }
                for (int t=0; t&amp;lt;bpp; t++)
                    data[currentbyte++] = colorbuffer.bgra[t];
                currentpixel++;
                if (currentpixel&amp;gt;pixelcount) {
                    std::cerr &amp;lt;&amp;lt; &quot;Too many pixels read\n&quot;;
                    return false;
                }
            }
        } else {
            chunkheader -= 127;
            in.read(reinterpret_cast&amp;lt;char *&amp;gt;(colorbuffer.bgra), bpp);
            if (!in.good()) {
                std::cerr &amp;lt;&amp;lt; &quot;an error occured while reading the header\n&quot;;
                return false;
            }
            for (int i=0; i&amp;lt;chunkheader; i++) {
                for (int t=0; t&amp;lt;bpp; t++)
                    data[currentbyte++] = colorbuffer.bgra[t];
                currentpixel++;
                if (currentpixel&amp;gt;pixelcount) {
                    std::cerr &amp;lt;&amp;lt; &quot;Too many pixels read\n&quot;;
                    return false;
                }
            }
        }
    } while (currentpixel &amp;lt; pixelcount);
    return true;
}

bool TGAImage::write_tga_file(const std::string filename, const bool vflip, const bool rle) const {
    constexpr std::uint8_t developer_area_ref[4] = {0, 0, 0, 0};
    constexpr std::uint8_t extension_area_ref[4] = {0, 0, 0, 0};
    constexpr std::uint8_t footer[18] = {'T','R','U','E','V','I','S','I','O','N','-','X','F','I','L','E','.','\0'};
    std::ofstream out;
    out.open(filename, std::ios::binary);
    if (!out.is_open()) {
        std::cerr &amp;lt;&amp;lt; &quot;can't open file &quot; &amp;lt;&amp;lt; filename &amp;lt;&amp;lt; &quot;\n&quot;;
        return false;
    }
    TGAHeader header = {};
    header.bitsperpixel = bpp&amp;lt;&amp;lt;3;
    header.width  = w;
    header.height = h;
    header.datatypecode = (bpp==GRAYSCALE ? (rle?11:3) : (rle?10:2));
    header.imagedescriptor = vflip ? 0x00 : 0x20; // top-left or bottom-left origin
    out.write(reinterpret_cast&amp;lt;const char *&amp;gt;(&amp;amp;header), sizeof(header));
    if (!out.good()) goto err;
    if (!rle) {
        out.write(reinterpret_cast&amp;lt;const char *&amp;gt;(data.data()), w*h*bpp);
        if (!out.good()) goto err;
    } else if (!unload_rle_data(out)) goto err;
    out.write(reinterpret_cast&amp;lt;const char *&amp;gt;(developer_area_ref), sizeof(developer_area_ref));
    if (!out.good()) goto err;
    out.write(reinterpret_cast&amp;lt;const char *&amp;gt;(extension_area_ref), sizeof(extension_area_ref));
    if (!out.good()) goto err;
    out.write(reinterpret_cast&amp;lt;const char *&amp;gt;(footer), sizeof(footer));
    if (!out.good()) goto err;
    return true;
err:
    std::cerr &amp;lt;&amp;lt; &quot;can't dump the tga file\n&quot;;
    return false;
}

bool TGAImage::unload_rle_data(std::ofstream &amp;amp;out) const {
    const std::uint8_t max_chunk_length = 128;
    size_t npixels = w*h;
    size_t curpix = 0;
    while (curpix&amp;lt;npixels) {
        size_t chunkstart = curpix*bpp;
        size_t curbyte = curpix*bpp;
        std::uint8_t run_length = 1;
        bool raw = true;
        while (curpix+run_length&amp;lt;npixels &amp;amp;&amp;amp; run_length&amp;lt;max_chunk_length) {
            bool succ_eq = true;
            for (int t=0; succ_eq &amp;amp;&amp;amp; t&amp;lt;bpp; t++)
                succ_eq = (data[curbyte+t]==data[curbyte+t+bpp]);
            curbyte += bpp;
            if (1==run_length)
                raw = !succ_eq;
            if (raw &amp;amp;&amp;amp; succ_eq) {
                run_length--;
                break;
            }
            if (!raw &amp;amp;&amp;amp; !succ_eq)
                break;
            run_length++;
        }
        curpix += run_length;
        out.put(raw ? run_length-1 : run_length+127);
        if (!out.good()) return false;
        out.write(reinterpret_cast&amp;lt;const char *&amp;gt;(data.data()+chunkstart), (raw?run_length*bpp:bpp));
        if (!out.good()) return false;
    }
    return true;
}

TGAColor TGAImage::get(const int x, const int y) const {
    if (!data.size() || x&amp;lt;0 || y&amp;lt;0 || x&amp;gt;=w || y&amp;gt;=h) return {};
    TGAColor ret = {0, 0, 0, 0, bpp};
    const std::uint8_t *p = data.data()+(x+y*w)*bpp;
    for (int i=bpp; i--; ret.bgra[i] = p[i]);
    return ret;
}

void TGAImage::set(int x, int y, const TGAColor &amp;amp;c) {
    if (!data.size() || x&amp;lt;0 || y&amp;lt;0 || x&amp;gt;=w || y&amp;gt;=h) return;
    memcpy(data.data()+(x+y*w)*bpp, c.bgra, bpp);
}

void TGAImage::flip_horizontally() {
    for (int i=0; i&amp;lt;w/2; i++)
        for (int j=0; j&amp;lt;h; j++)
            for (int b=0; b&amp;lt;bpp; b++)
                std::swap(data[(i+j*w)*bpp+b], data[(w-1-i+j*w)*bpp+b]);
}

void TGAImage::flip_vertically() {
    for (int i=0; i&amp;lt;w; i++)
        for (int j=0; j&amp;lt;h/2; j++)
            for (int b=0; b&amp;lt;bpp; b++)
                std::swap(data[(i+j*w)*bpp+b], data[(i+(h-1-j)*w)*bpp+b]);
}

int TGAImage::width() const {
    return w;
}

int TGAImage::height() const {
    return h;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TGAImage는 정보를 저장한 &lt;code&gt;uint8_t&lt;/code&gt;의 &lt;code&gt;vector&lt;/code&gt; 형식으로 이미지 데이터를 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TGAImage 파일을 생성해보자.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;#include &quot;tgaimage.h&quot;

int main()
{
    constexpr int width = 64;
    constexpr int height = 64;

    TGAImage framebuffer(width, height, TGAImage::RGB);

    framebuffer.write_tga_file(&quot;output/framebuffer.tga&quot;);

    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TGAImage&lt;/code&gt; 변수를 만들고, &lt;code&gt;write_tga_file&lt;/code&gt;로 파일로 작성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260603164256.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PUChL/dJMcahR8NO3/enoqa5HybbaD69Oh7VSNyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PUChL/dJMcahR8NO3/enoqa5HybbaD69Oh7VSNyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PUChL/dJMcahR8NO3/enoqa5HybbaD69Oh7VSNyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPUChL%2FdJMcahR8NO3%2Fenoqa5HybbaD69Oh7VSNyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;320&quot; data-filename=&quot;Pasted image 20260603164256.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;64*64의 작은 이미지가 생성되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 TGAImage의 픽셀을 변경해 점을 찍어보자.&lt;br /&gt;색을 미리 선언해 두면 편하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;constexpr TGAColor white =    { 255, 255, 255, 255 };
constexpr TGAColor red =    {    0,   0, 255, 255 };
constexpr TGAColor blue =    { 255,     0,   0, 255 };
constexpr TGAColor green =    {    0, 255,      0, 255 };&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;int main()
{
    constexpr int width = 64;
    constexpr int height = 64;

    TGAImage framebuffer(width, height, TGAImage::RGB);

    framebuffer.set(7, 3, white);
    framebuffer.set(12, 37, red);
    framebuffer.set(62, 53, blue);
    framebuffer.set(30, 34, green);

    framebuffer.write_tga_file(&quot;output/framebuffer.tga&quot;);

    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;set&lt;/code&gt;을 통해 특정 위치의 픽셀을 변경했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260603165024.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhAktY/dJMcaalbVzH/Trh4nJKTRWa9tTXWEK1Rtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhAktY/dJMcaalbVzH/Trh4nJKTRWa9tTXWEK1Rtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhAktY/dJMcaalbVzH/Trh4nJKTRWa9tTXWEK1Rtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhAktY%2FdJMcaalbVzH%2FTrh4nJKTRWa9tTXWEK1Rtk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;320&quot; data-filename=&quot;Pasted image 20260603165024.png&quot; data-origin-width=&quot;64&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>etc</category>
      <category>graphics</category>
      <category>tinyrenderer</category>
      <author>타자치는 문돌이</author>
      <guid isPermaLink="true">https://code-in-coffee.tistory.com/52</guid>
      <comments>https://code-in-coffee.tistory.com/52#entry52comment</comments>
      <pubDate>Thu, 4 Jun 2026 00:10:59 +0900</pubDate>
    </item>
    <item>
      <title>Heap, Hashmap, Set</title>
      <link>https://code-in-coffee.tistory.com/51</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Heap&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++에서는 priority_queue를 Heap으로 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;struct Node {
    int cost, x, y;
};

struct Cmp {
    bool operator()(const Node&amp;amp; a, const Node&amp;amp; b) const {
        return a.cost &amp;gt; b.cost;   // ex) 비용이 작은 게 top에 오도록
    }
};


int main() {
    // 선언
    priority_queue&amp;lt;int&amp;gt; pq; // 최대 힙

    // 추가
    pq.push(1);
    pq.push(2);
    pq.push(3);

    // 가장 큰 값
    pq.top();

    // 삭제
    pq.pop();

    // 작은 게 위로
    priority_queue&amp;lt;int, vector&amp;lt;int&amp;gt;, greater&amp;lt;int&amp;gt;&amp;gt; pq;

    // 내가 원하는 기준으로
    priority_queue&amp;lt;Node, vector&amp;lt;Node&amp;gt;, Cmp&amp;gt; pq;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삽입과 삭제에 O(log N), 최대/최소값 조회는 O(1)이 걸린다.&lt;br /&gt;가장 큰 원소 K개를 구하려면, 최소 힙 (greater&amp;lt;int&amp;gt;)를 만든 뒤, 크기가 K가 넘어가면 pop을 해준다.&lt;br /&gt;priority_queue는 queue처럼 직접 모든 원소를 빼는 것이 아니면 순회가 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hashmap&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++에서는 unordered_map으로 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;
int main()
{
    // 선언
    unordered_map&amp;lt;string, int&amp;gt; mp; // &amp;lt;key, value&amp;gt;

    // 추가
    mp[&quot;apple&quot;] = 3;
    mp[&quot;banana&quot;] = 5;

    // 값 증가
    mp[&quot;apple&quot;]++;

    // 키 확인
    if (mp.count(&quot;apple&quot;))
    {

    }

    // 조회
    auto it = mp.find(&quot;banana&quot;);
    if (it != mp.end()) {
        cout &amp;lt;&amp;lt; it-&amp;gt;second;
    }

    // 삭제
    mp.erase(&quot;apple&quot;);

    // 순회
    for (auto &amp;amp;p : mp) 
    {
        cout &amp;lt;&amp;lt; p.first &amp;lt;&amp;lt; &quot; : &quot; &amp;lt;&amp;lt; p.second &amp;lt;&amp;lt; &quot;\n&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순회는 정렬된 순서가 아니다.&lt;br /&gt;없는 원소에 대해 &lt;code&gt;mp[&quot;hello&quot;]&lt;/code&gt;를 조회하면 자동으로 추가되니 주의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Set&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unordered_set으로 중복 없는 원소와 빠른 접근(O(1))이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int main()
{
    // 선언
    unordered_set&amp;lt;int&amp;gt; s;

    // 삽입
    s.insert(10);

    // 확인
    if (s.count(10))
    {

    }

    // 삭제
    s.erase(10);

    // 순회
    for (auto x : s) 
    {
        cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &quot; &quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 순회는 랜덤이다.&lt;/p&gt;</description>
      <category>Computer Science Basics/알고리즘</category>
      <author>타자치는 문돌이</author>
      <guid isPermaLink="true">https://code-in-coffee.tistory.com/51</guid>
      <comments>https://code-in-coffee.tistory.com/51#entry51comment</comments>
      <pubDate>Fri, 21 Nov 2025 20:32:17 +0900</pubDate>
    </item>
    <item>
      <title>순열과 조합, 비트마스크</title>
      <link>https://code-in-coffee.tistory.com/50</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;순열&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;permutation 함수를 통해 벡터를 순열 순으로 정렬할 수 있다.&lt;br /&gt;이때, a는 정렬되어 있어야 한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;using namespace std;

int main() {
    vector&amp;lt;int&amp;gt; a = {1, 2, 3};
    sort(a.begin(), a.end());

    do {
        // 이제 a는 while을 돌면서
        // {1, 2, 3}, {1, 3, 2}...로 정렬됨
        for (int x : a) cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &quot; &quot;;
        cout &amp;lt;&amp;lt; &quot;\n&quot;;
    } while (next_permutation(a.begin(), a.end()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;조합&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5개 중 3개를 선택한다고 하자.&lt;br /&gt;선택한 원소를 1, 아닌 원소를 0으로 00111과 같이 표현할 수 있다.&lt;br /&gt;00111에 대해 permutation을 통해 순열을 구하면 포함 정보를 구할 수 있다.&lt;br /&gt;(00111, 01011, 01101, 01110...)&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;using namespace std;

int main()
{
    vector&amp;lt;int&amp;gt; idx(n, 0);
    fill(idx.end()-k, idx.end(), 1); // 1이 선택

    do {
        if (idx[i] == 1) // i가 선택된 원소
        {
            ...
        }
    } while (next_permutation(idx.begin(), idx.end()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비트마스크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3자리 vector에 대해, 3자리 이진수를 생각해보면&lt;br /&gt;000, 001, 010, 100, 101, 110, 111으로 0을 넣음, 1을 안넣음으로 생각할 수 있다.&lt;br /&gt;원소의 포함/미포함 조합에 대해 특정 로직을 적용하거나, 팀을 나누는 등의 경우에 사용할 수 있다.&lt;br /&gt;이 이진수는 Bitmask를 통해 구한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;using namespace std;

int main()
{
    vector&amp;lt;int&amp;gt; a = {1, 2, 3};

    for (int mask = 0; mask &amp;lt; (1 &amp;lt;&amp;lt; a.size()); i++) // 0(000), 1(001), 2(010)...
    {
        for (int i = 0; i &amp;lt; a.size(); i++)
        {
            if (mask &amp;amp; (1 &amp;lt;&amp;lt; i)) // i가 1이면
            {
                a[i] // 포함된 a의 원소
                ...
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Computer Science Basics/알고리즘</category>
      <author>타자치는 문돌이</author>
      <guid isPermaLink="true">https://code-in-coffee.tistory.com/50</guid>
      <comments>https://code-in-coffee.tistory.com/50#entry50comment</comments>
      <pubDate>Fri, 21 Nov 2025 20:07:35 +0900</pubDate>
    </item>
    <item>
      <title>Lyra와 모듈형 게임플레이</title>
      <link>https://code-in-coffee.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/game-features-and-modular-gameplay-in-unreal-engine&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.epicgames.com/documentation/ko-kr/unreal-engine/game-features-and-modular-gameplay-in-unreal-engine&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Game Feature와 Experience&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lyra는 여러가지 모드가 존재하고, 이를 Experience라는 단위로 나누어 구분한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20251103100958.png&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;623&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xTrIX/dJMcagjvhgL/Y6iBNJpHVWVlDS9NUFYxik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xTrIX/dJMcagjvhgL/Y6iBNJpHVWVlDS9NUFYxik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xTrIX/dJMcagjvhgL/Y6iBNJpHVWVlDS9NUFYxik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxTrIX%2FdJMcagjvhgL%2FY6iBNJpHVWVlDS9NUFYxik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1760&quot; height=&quot;623&quot; data-filename=&quot;Pasted image 20251103100958.png&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 필요없는 종속성과 인터랙션을 방지하기 위해 Game Feature로 여러 기능을 Plugin화 한 뒤, Experience를 로드할 때 필요한 Game Feature만 동적으로 추가하는 방식으로 작동한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20251103101435.png&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GxmIO/dJMcaiuQ4OI/4K5XhwyxqV8YREWhxHkYEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GxmIO/dJMcaiuQ4OI/4K5XhwyxqV8YREWhxHkYEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GxmIO/dJMcaiuQ4OI/4K5XhwyxqV8YREWhxHkYEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGxmIO%2FdJMcaiuQ4OI%2F4K5XhwyxqV8YREWhxHkYEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;843&quot; height=&quot;342&quot; data-filename=&quot;Pasted image 20251103101435.png&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Experience는 &lt;code&gt;ULyraExperienceDefinition&lt;/code&gt;라는 데이터 에셋으로 저장되어 이 Experience를 로드할 때 수행해야할 Action, 사용할 DefaultPawn 등으로 구성되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20251103102629.png&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;1084&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAkR08/dJMcaiuQ4OW/0I7gMc5Mv4SV42oeKSWCfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAkR08/dJMcaiuQ4OW/0I7gMc5Mv4SV42oeKSWCfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAkR08/dJMcaiuQ4OW/0I7gMc5Mv4SV42oeKSWCfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAkR08%2FdJMcaiuQ4OW%2F0I7gMc5Mv4SV42oeKSWCfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;805&quot; data-filename=&quot;Pasted image 20251103102629.png&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;1084&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 수행하는 액션으로는 Ability 부착, InputBinding 추가, Widget 추가, 컴포넌트 추가 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 게임 플레이 맵으로 이동할 때, 기존의 Gameplay Actor에 여러 컴포넌트를 붙여 무기 기능, 인벤토리 기능 등을 활성화 하는 것이다. 이를 위해 로딩 개념을 추가했다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ALyraGameMode::OnMatchAssignmentGiven(FPrimaryAssetId ExperienceId, const FString&amp;amp; ExperienceIdSource)  
{  
    if (ExperienceId.IsValid())  
    {  
       UE_LOG(LogLyraExperience, Log, TEXT(&quot;Identified experience %s (Source: %s)&quot;), *ExperienceId.ToString(), *ExperienceIdSource);  

       ULyraExperienceManagerComponent* ExperienceComponent = GameState-&amp;gt;FindComponentByClass&amp;lt;ULyraExperienceManagerComponent&amp;gt;();  
       check(ExperienceComponent);  
       ExperienceComponent-&amp;gt;SetCurrentExperience(ExperienceId);  
    }  
    else  
    {  
       UE_LOG(LogLyraExperience, Error, TEXT(&quot;Failed to identify experience, loading screen will stay up forever&quot;));  
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GameMode에서 매칭을 시작하기 전에 ExperienceId를 받아 GameState에 부착된 &lt;code&gt;ULyraExperienceManagerComponent&lt;/code&gt;를 통해 Experience를 Set한다. 이 과정에서 &lt;code&gt;StartExperienceLoad&lt;/code&gt;가 호출되어 필요한 에셋을 비동기로 로드한다. 그 후, &lt;code&gt;OnExperienceLoadComplete&lt;/code&gt;에서 Game Feature를 로드한 뒤, &lt;code&gt;OnExperienceFullLoadCompleted&lt;/code&gt;에서 GameFeature Action을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Experience의 로드가 완료되면 &lt;code&gt;OnExperienceLoaded&lt;/code&gt;를 구독한 요소들에게 알림을 보낸다. 주요 구독자로는 &lt;code&gt;ALyraGameMode&lt;/code&gt;와 &lt;code&gt;ALyraPlayerState&lt;/code&gt;가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byM1mN/dJMcadf0Ki7/tCQrG7zl4DnpAdIE7ztrqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byM1mN/dJMcadf0Ki7/tCQrG7zl4DnpAdIE7ztrqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byM1mN/dJMcadf0Ki7/tCQrG7zl4DnpAdIE7ztrqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyM1mN%2FdJMcadf0Ki7%2FtCQrG7zl4DnpAdIE7ztrqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1061&quot; height=&quot;1080&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Modular Gameplay Actor와 Component&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 로직이 동적으로 Component로 붙기 때문에 Component 초기화 로직이 중요해진다. 이를 ModularGameplayActor를 통해 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lyra의 Gameplay Actor(PlayerController, PlayerState...)는 ModularGameplayActor와 CommonGame을 상속 받는다. ModularGameplayActor는 GameFeature를, CommonGame은 온라인 서브시스템을 위한 플러그인의 일부이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;mermaid-diagram-2025-10-31-123950 (2).png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Khyq/dJMcaboXIbw/CbYmH8rEwFWggJQvPXJRmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Khyq/dJMcaboXIbw/CbYmH8rEwFWggJQvPXJRmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Khyq/dJMcaboXIbw/CbYmH8rEwFWggJQvPXJRmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Khyq%2FdJMcaboXIbw%2FCbYmH8rEwFWggJQvPXJRmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;300&quot; data-filename=&quot;mermaid-diagram-2025-10-31-123950 (2).png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;mermaid-diagram-2025-10-31-123958 (2).png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QehHu/dJMcahJtXZu/VvsM8fE92ILUwa46fyZByK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QehHu/dJMcahJtXZu/VvsM8fE92ILUwa46fyZByK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QehHu/dJMcahJtXZu/VvsM8fE92ILUwa46fyZByK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQehHu%2FdJMcahJtXZu%2FVvsM8fE92ILUwa46fyZByK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;389&quot; data-filename=&quot;mermaid-diagram-2025-10-31-123958 (2).png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void AModularPlayerState::PreInitializeComponents()  
{  
    Super::PreInitializeComponents();  

    UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this);  
}  

void AModularPlayerState::BeginPlay()  
{  
    UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady);  

    Super::BeginPlay();  
}  

void AModularPlayerState::EndPlay(const EEndPlayReason::Type EndPlayReason)  
{  
    UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this);  

    Super::EndPlay(EndPlayReason);  
}  

void AModularPlayerState::Reset()  
{  
    Super::Reset();  

    TArray&amp;lt;UPlayerStateComponent*&amp;gt; ModularComponents;  
    GetComponents(ModularComponents);  
    for (UPlayerStateComponent* Component : ModularComponents)  
    {  
       Component-&amp;gt;Reset();  
    }  
}  

void AModularPlayerState::CopyProperties(APlayerState* PlayerState)  
{  
    Super::CopyProperties(PlayerState);  

    TInlineComponentArray&amp;lt;UPlayerStateComponent*&amp;gt; PlayerStateComponents;  
    GetComponents(PlayerStateComponents);  
    for (UPlayerStateComponent* SourcePSComp : PlayerStateComponents)  
    {  
       if (UPlayerStateComponent* TargetComp = Cast&amp;lt;UPlayerStateComponent&amp;gt;(static_cast&amp;lt;UObject*&amp;gt;(FindObjectWithOuter(PlayerState, SourcePSComp-&amp;gt;GetClass(), SourcePSComp-&amp;gt;GetFName()))))  
       {  
          SourcePSComp-&amp;gt;CopyProperties(TargetComp);  
       }  
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void AModularCharacter::PreInitializeComponents()  
{  
    Super::PreInitializeComponents();  

    UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this);  
}  

void AModularCharacter::BeginPlay()  
{  
    UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady);  

    Super::BeginPlay();  
}  

void AModularCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)  
{  
    UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this);  

    Super::EndPlay(EndPlayReason);  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Modular GameplayActor들의 역할은 단순히 UGameFrameworkComponentManager에 자신을 등록하고, 적절한 타이밍에 &lt;code&gt;GameActorReady&lt;/code&gt;를 보내는 것이다. &lt;code&gt;GameActorReady&lt;/code&gt;를 보내면 UGameFrameworkComponentManager는 알림을 구독한 Component에 State가 변경되었다는 알림을 보낸다. 구독자는 &lt;code&gt;IGameFrameworkInitStateInterface&lt;/code&gt;라는 Interface를 가지고 있고, 알림이 오면 자신의 &lt;code&gt;CanChangeInitState&lt;/code&gt;를 확인해 자신의 상태를 변환한다. 그 후, 변환된 상태에 맞게 &lt;code&gt;HandleChangeInitState&lt;/code&gt;를 실행해 특정 행동을 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;mermaid-diagram-2025-11-03-174703 (2).png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg6kc2/dJMcac9fbIq/J0zpypqyBkMw7faBuC7ik0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg6kc2/dJMcac9fbIq/J0zpypqyBkMw7faBuC7ik0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg6kc2/dJMcac9fbIq/J0zpypqyBkMw7faBuC7ik0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg6kc2%2FdJMcac9fbIq%2FJ0zpypqyBkMw7faBuC7ik0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;500&quot; data-filename=&quot;mermaid-diagram-2025-11-03-174703 (2).png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Game Programming/Unreal Engine</category>
      <category>LYRA</category>
      <category>Unreal 5</category>
      <author>타자치는 문돌이</author>
      <guid isPermaLink="true">https://code-in-coffee.tistory.com/49</guid>
      <comments>https://code-in-coffee.tistory.com/49#entry49comment</comments>
      <pubDate>Mon, 3 Nov 2025 17:58:40 +0900</pubDate>
    </item>
    <item>
      <title>Lyra의 스킬 발동</title>
      <link>https://code-in-coffee.tistory.com/48</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Lyra는 GameplayAbilitySystem를 기반으로 스킬을 처리한다.&lt;br /&gt;Move, Look, Crouch, AutoRun은 별도의 C++ 구현을 두고, Dash, Heal, Grenade, Emote 등 다른 스킬은 모두 Gameplay Ability로 구현되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, &lt;code&gt;LyraHeroComponent&lt;/code&gt;에서 InputAction을 바인드 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ULyraHeroComponent::InitializePlayerInput(UInputComponent* PlayerInputComponent)
{
    ...
    UEnhancedInputLocalPlayerSubsystem* Subsystem = LP-&amp;gt;GetSubsystem&amp;lt;UEnhancedInputLocalPlayerSubsystem&amp;gt;();
    check(Subsystem);

    Subsystem-&amp;gt;ClearAllMappings();

    if (const ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
    {
        if (const ULyraPawnData* PawnData = PawnExtComp-&amp;gt;GetPawnData&amp;lt;ULyraPawnData&amp;gt;())
        {
            if (const ULyraInputConfig* InputConfig = PawnData-&amp;gt;InputConfig)
            {
                for (const FInputMappingContextAndPriority&amp;amp; Mapping : DefaultInputMappings)
                {
                    ...
                    Subsystem-&amp;gt;AddMappingContext(IMC, Mapping.Priority, Options);
                }

                // The Lyra Input Component has some additional functions to map Gameplay Tags to an Input Action.
                // If you want this functionality but still want to change your input component class, make it a subclass
                // of the ULyraInputComponent or modify this component accordingly.
                ULyraInputComponent* LyraIC = Cast&amp;lt;ULyraInputComponent&amp;gt;(PlayerInputComponent);
                if (LyraIC)
                {
                    ...
                    // This is where we actually bind and input action to a gameplay tag, which means that Gameplay Ability Blueprints will
                    // be triggered directly by these input actions Triggered events. 
                    TArray&amp;lt;uint32&amp;gt; BindHandles;
                    LyraIC-&amp;gt;BindAbilityActions(InputConfig, this, &amp;amp;ThisClass::Input_AbilityInputTagPressed, &amp;amp;ThisClass::Input_AbilityInputTagReleased, /*out*/ BindHandles);

                    LyraIC-&amp;gt;BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Move, ETriggerEvent::Triggered, this, &amp;amp;ThisClass::Input_Move, /*bLogIfNotFound=*/ false);
                    LyraIC-&amp;gt;BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Look_Mouse, ETriggerEvent::Triggered, this, &amp;amp;ThisClass::Input_LookMouse, /*bLogIfNotFound=*/ false);
                    LyraIC-&amp;gt;BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Look_Stick, ETriggerEvent::Triggered, this, &amp;amp;ThisClass::Input_LookStick, /*bLogIfNotFound=*/ false);
                    LyraIC-&amp;gt;BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Crouch, ETriggerEvent::Triggered, this, &amp;amp;ThisClass::Input_Crouch, /*bLogIfNotFound=*/ false);
                    LyraIC-&amp;gt;BindNativeAction(InputConfig, LyraGameplayTags::InputTag_AutoRun, ETriggerEvent::Triggered, this, &amp;amp;ThisClass::Input_AutoRun, /*bLogIfNotFound=*/ false);
                }
            }
        }
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LyraInputComponent&lt;/code&gt;에서 AbilityActions를 묶고, 이동 관련 함수를 바인드한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class UserClass, typename PressedFuncType, typename ReleasedFuncType&amp;gt;
void ULyraInputComponent::BindAbilityActions(const ULyraInputConfig* InputConfig, UserClass* Object, PressedFuncType PressedFunc, ReleasedFuncType ReleasedFunc, TArray&amp;lt;uint32&amp;gt;&amp;amp; BindHandles)
{
    check(InputConfig);

    for (const FLyraInputAction&amp;amp; Action : InputConfig-&amp;gt;AbilityInputActions)
    {
        if (Action.InputAction &amp;amp;&amp;amp; Action.InputTag.IsValid())
        {
            if (PressedFunc)
            {
                BindHandles.Add(BindAction(Action.InputAction, ETriggerEvent::Triggered, Object, PressedFunc, Action.InputTag).GetHandle());
            }

            if (ReleasedFunc)
            {
                BindHandles.Add(BindAction(Action.InputAction, ETriggerEvent::Completed, Object, ReleasedFunc, Action.InputTag).GetHandle());
            }
        }
    }
}

void ULyraHeroComponent::Input_AbilityInputTagPressed(FGameplayTag InputTag)
{
    if (const APawn* Pawn = GetPawn&amp;lt;APawn&amp;gt;())
    {
        if (const ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
        {
            if (ULyraAbilitySystemComponent* LyraASC = PawnExtComp-&amp;gt;GetLyraAbilitySystemComponent())
            {
                LyraASC-&amp;gt;AbilityInputTagPressed(InputTag);
            }
        }    
    }
}

void ULyraHeroComponent::Input_AbilityInputTagReleased(FGameplayTag InputTag)
{
    const APawn* Pawn = GetPawn&amp;lt;APawn&amp;gt;();
    if (!Pawn)
    {
        return;
    }

    if (const ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
    {
        if (ULyraAbilitySystemComponent* LyraASC = PawnExtComp-&amp;gt;GetLyraAbilitySystemComponent())
        {
            LyraASC-&amp;gt;AbilityInputTagReleased(InputTag);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BindAbilityActions&lt;/code&gt;에서는 &lt;code&gt;LyraInputConfig&lt;/code&gt;에 저장된 &lt;code&gt;&amp;lt;InputAciton, GameplayTag&amp;gt;&lt;/code&gt; 쌍에 대해 &lt;code&gt;Input_AbilityInputTagPressed&lt;/code&gt;와 &lt;code&gt;Input_AbilityInputTagReleased&lt;/code&gt;를 바인드한다. &lt;code&gt;Input&lt;/code&gt; 함수는 ASC의 &lt;code&gt;AbilityInputTagPressed/Released&lt;/code&gt; 함수를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;void ULyraAbilitySystemComponent::AbilityInputTagPressed(const FGameplayTag&amp;amp; InputTag)
{
    if (InputTag.IsValid())
    {
        for (const FGameplayAbilitySpec&amp;amp; AbilitySpec : ActivatableAbilities.Items)
        {
            if (AbilitySpec.Ability &amp;amp;&amp;amp; (AbilitySpec.GetDynamicSpecSourceTags().HasTagExact(InputTag)))
            {
                InputPressedSpecHandles.AddUnique(AbilitySpec.Handle);
                InputHeldSpecHandles.AddUnique(AbilitySpec.Handle);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASC에서는 실행 가능한 Ability 목록에서 태그를 검색해 &lt;code&gt;InputPressedSpecHandles&lt;/code&gt;와 &lt;code&gt;InputHeldSpecHandles&lt;/code&gt;에 핸들을 추가하고,&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;void ULyraAbilitySystemComponent::ProcessAbilityInput(float DeltaTime, bool bGamePaused)
{
    ...

    for (const FGameplayAbilitySpecHandle&amp;amp; SpecHandle : InputHeldSpecHandles)
    {
        if (const FGameplayAbilitySpec* AbilitySpec = FindAbilitySpecFromHandle(SpecHandle))
        {
            if (AbilitySpec-&amp;gt;Ability &amp;amp;&amp;amp; !AbilitySpec-&amp;gt;IsActive())
            {
                const ULyraGameplayAbility* LyraAbilityCDO = Cast&amp;lt;ULyraGameplayAbility&amp;gt;(AbilitySpec-&amp;gt;Ability);
                if (LyraAbilityCDO &amp;amp;&amp;amp; LyraAbilityCDO-&amp;gt;GetActivationPolicy() == ELyraAbilityActivationPolicy::WhileInputActive)
                {
                    AbilitiesToActivate.AddUnique(AbilitySpec-&amp;gt;Handle);
                }
            }
        }
    }

    for (const FGameplayAbilitySpecHandle&amp;amp; SpecHandle : InputPressedSpecHandles)
    {
        ...
    }


    for (const FGameplayAbilitySpecHandle&amp;amp; AbilitySpecHandle : AbilitiesToActivate)
    {
        TryActivateAbility(AbilitySpecHandle);
    }

    for (const FGameplayAbilitySpecHandle&amp;amp; SpecHandle : InputReleasedSpecHandles)
    {
        ...
    }

    InputPressedSpecHandles.Reset();
    InputReleasedSpecHandles.Reset();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ProcessAbilityInput&lt;/code&gt;에서 &lt;code&gt;TryActivateAbility(AbilitySpecHandle);&lt;/code&gt;을 통해 모든 입력을 한번에 처리한다. &lt;code&gt;ProcessAbilityInput&lt;/code&gt;는 &lt;code&gt;PostProcessInput&lt;/code&gt;에서 실행된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void ALyraPlayerController::PostProcessInput(const float DeltaTime, const bool bGamePaused)
{
    if (ULyraAbilitySystemComponent* LyraASC = GetLyraAbilitySystemComponent())
    {
        LyraASC-&amp;gt;ProcessAbilityInput(DeltaTime, bGamePaused);
    }

    Super::PostProcessInput(DeltaTime, bGamePaused);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2429&quot; data-origin-height=&quot;1920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XESZC/dJMcadAkLvK/BTesEQJhufW6nR0pzvBpK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XESZC/dJMcadAkLvK/BTesEQJhufW6nR0pzvBpK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XESZC/dJMcadAkLvK/BTesEQJhufW6nR0pzvBpK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXESZC%2FdJMcadAkLvK%2FBTesEQJhufW6nR0pzvBpK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2429&quot; height=&quot;1920&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2429&quot; data-origin-height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략 이런 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Game Programming/Unreal Engine</category>
      <category>gas</category>
      <category>LYRA</category>
      <category>Unreal 5</category>
      <author>타자치는 문돌이</author>
      <guid isPermaLink="true">https://code-in-coffee.tistory.com/48</guid>
      <comments>https://code-in-coffee.tistory.com/48#entry48comment</comments>
      <pubDate>Thu, 30 Oct 2025 18:20:29 +0900</pubDate>
    </item>
    <item>
      <title>Unreal Engine에서 기본 요소의 초기화 순서 (Host-Client)</title>
      <link>https://code-in-coffee.tistory.com/47</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;각 요소를 초기화하고 의존성을 묶는 시작 지점에서 연결이 안되는 문제가 발생했다.&lt;br /&gt;그래서 GameMode, PlayerController, PlayerState, DefaultPawn, HUD 클래스의 초기화 순서에 대해 알아보기로 하였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실험 방법&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20251024101826.png&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nxyaO/dJMb9LxeCgI/P6WTNTkkX9VJzBRiAJ8oC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nxyaO/dJMb9LxeCgI/P6WTNTkkX9VJzBRiAJ8oC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nxyaO/dJMb9LxeCgI/P6WTNTkkX9VJzBRiAJ8oC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnxyaO%2FdJMb9LxeCgI%2FP6WTNTkkX9VJzBRiAJ8oC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;722&quot; height=&quot;191&quot; data-filename=&quot;Pasted image 20251024101826.png&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20251024101901.png&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSgXT2/dJMb9LxeCgU/xAc4UWin4C3Q6F6UOw1Mu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSgXT2/dJMb9LxeCgU/xAc4UWin4C3Q6F6UOw1Mu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSgXT2/dJMb9LxeCgU/xAc4UWin4C3Q6F6UOw1Mu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSgXT2%2FdJMb9LxeCgU%2FxAc4UWin4C3Q6F6UOw1Mu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;245&quot; data-filename=&quot;Pasted image 20251024101901.png&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TEST 클래스를 만든 뒤, 클래스를 상속 받는 BP를 만들고, GameMode에 적용했다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;ATESTGameModeBase::ATESTGameModeBase()  
{  
    NET_LOG(Log, TEXT(&quot;GameMode::Constructor&quot;));  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TEST 클래스는 NET_LOG 매크로를 구현해 로그를 출력했다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;LogNetRole: [PIE0][Standalone][SID=-1] [Server] GameMode::Constructor&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;[PIE0]&lt;/b&gt; : PlayInEditor ID. 구분하기 제일 좋은 기준&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[SID]&lt;/b&gt; : PlayerState의 PlayerId로 없으면 -1&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[Server]&lt;/b&gt; : &lt;code&gt;HasAuthority&lt;/code&gt;면 Server, 아니면 Client. HUD와 Standalone은 Client가 권한이 있어 Server로 나오는 한계가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Standalone&lt;/h2&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;LogNetRole: [PIE0][Standalone][SID=-1] [Server] GameMode::Constructor
LogNetRole: [PIE0][Standalone][SID=-1] [Server] PlayerController::Constructor
LogNetRole: [PIE0][Standalone][SID=-1] [Server] PlayerState::Constructor
LogNetRole: [PIE0][Standalone][SID=-1] [Server] PlayerState::PostInitializeComponents
LogNetRole: [PIE0][Standalone][SID=262] [Server] HUD::Constuctor
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerCharacter::Constructor
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerCharacter::PossessedBy
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerCharacter::SetupPlayerInputComponent
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerController::OnPossess
LogNetRole: [PIE0][Standalone][SID=262] [Server] GameMode::BeginPlay
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerController::BeginReplication
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerController::BeginPlay
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerState::BeginPlay
LogNetRole: [PIE0][Standalone][SID=262] [Server] HUD::BeginPlay
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerCharacter::BeginPlay&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20251024104953.png&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;1126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3so5s/dJMb9VGxc0O/4ZwzIKIi5BOvyaHx9fMcAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3so5s/dJMb9VGxc0O/4ZwzIKIi5BOvyaHx9fMcAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3so5s/dJMb9VGxc0O/4ZwzIKIi5BOvyaHx9fMcAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3so5s%2FdJMb9VGxc0O%2F4ZwzIKIi5BOvyaHx9fMcAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;918&quot; height=&quot;1126&quot; data-filename=&quot;Pasted image 20251024104953.png&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;1126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GameMode-&amp;gt;PlayerController-&amp;gt;PlayerState-&amp;gt;HUD-&amp;gt;DefaultPawn 순서로 생성되고, BeginPlay도 같은 순서로 실행된다. HUD가 PlayerCharacter보다 먼저 실행되므로 PlayerCharacter의 BeginPlay에서 HUD가 참조해야 할 요소가 초기화된다면 문제가 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Listen Server (Host-Client)&lt;/h2&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;LogNetRole: [PIE0][ListenServer][SID=-1] [Server] GameMode::Constructor
LogNetRole: [PIE0][ListenServer][SID=-1] [Server] PlayerController::Constructor
LogNetRole: [PIE0][ListenServer][SID=-1] [Server] PlayerState::Constructor
LogNetRole: [PIE0][ListenServer][SID=-1] [Server] PlayerState::PostInitializeComponents
LogNetRole: [PIE0][ListenServer][SID=264] [Server] HUD::Constuctor
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::Constructor
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::PossessedBy
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::SetupPlayerInputComponent
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::OnPossess
LogNetRole: [PIE0][ListenServer][SID=264] [Server] GameMode::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::BeginReplication
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerState::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] HUD::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::Constructor
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerState::Constructor
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerState::PostInitializeComponents
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerState::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::BeginReplication
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::BeginReplication
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::Constructor
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::PossessedBy
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::OnPossess
LogNetRole: [PIE1][Client][SID=-1] [Server] PlayerController::Constructor
LogNetRole: [PIE1][Client][SID=-1] [Server] HUD::Constuctor
LogNetRole: [PIE1][Client][SID=-1] [Server] PlayerCharacter::Constructor
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerController::OnRep_Pawn
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerCharacter::OnRep_Controller
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerController::BeginReplication
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerController::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Server] HUD::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerCharacter::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Server] PlayerCharacter::Constructor
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerCharacter::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerController::OnRep_Pawn
LogNetRole: [PIE1][Client][SID=-1] [Server] PlayerState::Constructor
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerState::PostInitializeComponents
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerState::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Server] PlayerState::Constructor
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerState::PostInitializeComponents
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerState::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerCharacter::OnRep_PlayerState
LogNetRole: [PIE1][Client][SID=265] [Client] PlayerState::ClientInitialize
LogNetRole: [PIE1][Client][SID=265] [Client] PlayerController::OnRep_PlayerState
LogNetRole: [PIE1][Client][SID=265] [Client] PlayerCharacter::OnRep_PlayerState
LogNetRole: [PIE1][Client][SID=265] [Client] PlayerCharacter::OnRep_Controller
LogNetRole: [PIE1][Client][SID=265] [Client] PlayerCharacter::SetupPlayerInputComponent&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20251024122810.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;2000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ytrDg/dJMb84DuGoC/K8YuBczG7xCxDlXKBZeKbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ytrDg/dJMb84DuGoC/K8YuBczG7xCxDlXKBZeKbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ytrDg/dJMb84DuGoC/K8YuBczG7xCxDlXKBZeKbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FytrDg%2FdJMb84DuGoC%2FK8YuBczG7xCxDlXKBZeKbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;2000&quot; data-filename=&quot;Pasted image 20251024122810.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;2000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20251024130550.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;2000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vYxtL/dJMb9WyFTwk/MSfNS2A7PAZKm0UWmDeVjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vYxtL/dJMb9WyFTwk/MSfNS2A7PAZKm0UWmDeVjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vYxtL/dJMb9WyFTwk/MSfNS2A7PAZKm0UWmDeVjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvYxtL%2FdJMb9WyFTwk%2FMSfNS2A7PAZKm0UWmDeVjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;2000&quot; data-filename=&quot;Pasted image 20251024130550.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;2000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host 쪽에서의 순서는 Host 생성 -&amp;gt; 순차적으로 Client 생성으로 예측 가능한 범주지만, Client 쪽은 아무것도 보장되지 않는다. GPT에 따르면,Connection 이후에 액터 채널이 먼저 열린 것부터 생성한 뒤 데이터를 받는다고 한다. 그리고 생성 직후 BeginPlay가 실행되지만, 이 시점에 데이터 복사가 완료되지 않아 OnRep은 더 늦게 호출된다. 즉, 클라이언트는 BeginPlay 때 의존성 있는 요소가 모두 생성되었는 지 보장이 안되고, 생성되었어도 서버 데이터가 적용되었는 지 보장할 수 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 났던 상황을 검토해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;PlayerState는 AbilitySystemComponent를 가진다.&lt;/li&gt;
&lt;li&gt;AbilitySystemComponent는 InitAbilityActorInfo라는 함수를 써서 초기화하는 과정이 서버와 클라이언트 모두에서 한번 필요한데, 이때 PlayerCharacter가 필요하다.&lt;/li&gt;
&lt;li&gt;HUD는 AbilitySystemComponent를 통해 Player의 스탯을 출력한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 경우, PlayerState-&amp;gt;HUD-&amp;gt;PlayerCharacter 순서대로 생성되므로 PlayerCharacter 생성 후 PossessedBy에서 InitAbilityActorInfo를 실행한 뒤, HUD의 BeginPlay에서 데이터를 읽도록 할 수 있다.&lt;br /&gt;클라이언트의 경우, OnRep_PlayerState에서 InitAbilityActorInfo와 HUD의 데이터 읽기가 가능하다. 그런데 상황에 따라 순서가 바뀔 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 로비에서부터 PlayerState를 만들고 넘기면 될 것 같기도...&lt;/p&gt;</description>
      <category>Game Programming/Unreal Engine</category>
      <category>Unreal 5</category>
      <author>타자치는 문돌이</author>
      <guid isPermaLink="true">https://code-in-coffee.tistory.com/47</guid>
      <comments>https://code-in-coffee.tistory.com/47#entry47comment</comments>
      <pubDate>Fri, 24 Oct 2025 17:04:27 +0900</pubDate>
    </item>
  </channel>
</rss>