<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://infisword.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://infisword.github.io/" rel="alternate" type="text/html" /><updated>2026-07-05T16:40:37+00:00</updated><id>https://infisword.github.io/feed.xml</id><title type="html">Minhyuk Lee | Game Developer</title><subtitle>An amazing website.</subtitle><author><name>이민혁 (Minhyuk Lee)</name></author><entry><title type="html">PROJECT REPORT // AUTONOMOUS_RACING_AGENT</title><link href="https://infisword.github.io/project/autonomous-racing-agent/" rel="alternate" type="text/html" title="PROJECT REPORT // AUTONOMOUS_RACING_AGENT" /><published>2026-05-23T00:00:00+00:00</published><updated>2026-05-23T00:00:00+00:00</updated><id>https://infisword.github.io/project/autonomous-racing-agent</id><content type="html" xml:base="https://infisword.github.io/project/autonomous-racing-agent/"><![CDATA[<style>
    /* --- Premium Report Layout Styles --- */
    
    .chapter-title {
        font-size: 2.2rem;
        color: #fff !important;
        background: linear-gradient(90deg, #007bff 0%, #58A6FF 100%);
        padding: 25px 35px;
        border-radius: 12px;
        margin-top: 60px !important;
        margin-bottom: 40px !important;
        box-shadow: 0 10px 30px rgba(0, 123, 255, 0.15);
        display: flex;
        align-items: center;
        border: none !important;
    }
    .chapter-title::before {
        content: "CHAPTER.";
        font-family: 'Fira Code', monospace;
        font-size: 0.9rem;
        letter-spacing: 2px;
        margin-right: 15px;
        opacity: 0.8;
    }

    .pf-visual-frame {
        width: 100%; padding: 35px; background: #fcfcfc;
        border: 1px solid #e1e4e8; border-radius: 16px;
        margin: 30px 0; text-align: center;
        box-shadow: inset 0 2px 10px rgba(0,0,0,0.02);
    }
    .pf-arch-diagram {
        display: flex; flex-direction: column; gap: 20px; margin: 25px 0;
    }
    .pf-arch-layer {
        padding: 20px 25px; border: 1px solid #e1e4e8; border-radius: 8px;
        background: #fff; position: relative; text-align: center;
        box-shadow: 0 4px 12px rgba(0,0,0,0.03);
    }
    .pf-arch-layer::before {
        content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
        background: #007bff; border-radius: 8px 0 0 8px;
    }
    .pf-arch-layer-title {
        color: #007bff; font-weight: 700; margin-bottom: 10px;
        font-family: 'Fira Code', monospace; font-size: 0.95rem; text-align: center;
    }
    .pf-arch-layer-items {
        display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; justify-content: center;
    }
    .pf-arch-item {
        padding: 6px 14px; background: rgba(0, 123, 255, 0.08);
        border: 1px solid rgba(0, 123, 255, 0.3); border-radius: 6px;
        font-size: 0.85rem; color: #0056b3; text-align: center;
    }

    /* --- Reward Engineering --- */
    .reward-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin: 20px 0; }
    .reward-box { padding: 18px; border-radius: 8px; text-align: center; }
    .reward-box.positive { border: 2px solid #28a745; background: rgba(40, 167, 69, 0.02); }
    .reward-box.negative { border: 2px solid #ff7b72; background: rgba(255, 123, 114, 0.02); }
    .reward-box.info { border: 2px solid #007bff; background: rgba(0, 123, 255, 0.02); }
    .reward-title { font-weight: 700; margin-bottom: 12px; font-family: 'Fira Code'; font-size: 0.95rem; border-bottom: 1px solid #eee; padding-bottom: 8px; }

    /* --- Code Details --- */
    details.pf-details {
        margin: 20px 0;
        border: 1px solid #e1e4e8;
        border-radius: 8px;
        background: #f8f9fa;
        overflow: hidden;
    }
    details.pf-details summary {
        padding: 15px 20px;
        font-weight: 700;
        color: #007bff;
        cursor: pointer;
        outline: none;
        background: #fff;
        display: flex;
        align-items: center;
    }

    /* --- Training Table --- */
    .pf-data-table {
        width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 0.9rem;
        background: #fff; border: 1px solid #e1e4e8; border-radius: 8px; overflow: hidden;
    }
    .pf-data-table th { background: rgba(0, 123, 255, 0.08); color: #007bff; padding: 12px; text-align: left; }
    .pf-data-table td { padding: 10px 12px; border-bottom: 1px solid #e1e4e8; }
    
    /* --- Flow Arrows --- */
    .flow-arrow {
        color: #007bff; font-weight: bold; font-size: 1.5rem; margin: 10px 0;
    }
    
    /* --- Responsive Grid --- */
    .pf-grid {
        display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0;
    }

    .highlight-box {
        background: #f0f7ff;
        border-left: 5px solid #007bff;
        padding: 20px;
        margin: 20px 0;
        border-radius: 0 8px 8px 0;
    }
</style>

<p>Unity ML-Agents를 활용하여 정해진 트랙을 안정적으로 주행하는 자율 주행 경주차 에이전트를 학습시킨 프로젝트입니다. 시뮬레이션 환경 구축부터 보상 체계 설계, 신경망 관측 데이터 최적화까지의 전 과정을 다룹니다.</p>

<hr />

<h2 class="chapter-title" id="1-프로젝트-개요">1. 프로젝트 개요</h2>

<p>본 프로젝트는 강화학습(Reinforcement Learning)을 통해 복잡한 트랙 환경에서 자율 주행이 가능한 경주차 에이전트를 개발하는 것을 목표로 합니다. <strong>체크포인트 기반 네비게이션 시스템</strong>과 <strong>다층적 보상 설계</strong>를 통해 에이전트가 트랙을 순차적으로 완주하도록 유도하며, <strong>레이캐스트 기반 환경 감지</strong>와 <strong>입력 스무딩</strong>을 통해 물리적으로 안정적인 주행 성능을 확보했습니다.</p>

<div class="pf-visual-frame">
    <img src="/assets/Gifs/autonomous-racing.gif" alt="Unity ML-Agents Training Visualization" style="width: 100%; border-radius: 8px;" />
    <p style="text-align: center; color: #8b949e; font-size: 0.9rem; margin-top: 10px;">Unity ML-Agents를 통한 자율 주행 학습 과정</p>
</div>

<div class="pf-visual-frame">
    <img src="/assets/images/mlagent_4.png" alt="ML-Agents Track Environment" style="width: 100%; border-radius: 8px;" />
    <p style="text-align: center; color: #8b949e; font-size: 0.9rem; margin-top: 10px;">구불구불한 도로와 바위 지형으로 구성된 학습 트랙 환경</p>
</div>

<hr />

<h2 class="chapter-title" id="2-체크포인트-기반-네비게이션-시스템">2. 체크포인트 기반 네비게이션 시스템</h2>

<p>강화학습 초기 단계에서 에이전트들이 최단 경로를 찾는 과정에서 트랙을 역주행하거나 경로를 이탈하는 문제가 빈번하게 발생했습니다. 이를 해결하기 위해 <strong>순차적 체크포인트 시스템</strong>을 구축하여 에이전트가 반드시 트랙의 순방향으로 전진하도록 강제했습니다.</p>

<div class="highlight-box">
    <strong style="color: #007bff;">시행착오 및 문제 해결:</strong><br />
    초기 설계 시 에이전트가 목표 지점까지의 최단 경로를 찾는 과정에서 역주행하는 현상이 발생했습니다. 이를 방지하기 위해 목표 지점 사이에 다수의 체크포인트를 순차적으로 배치하고, 현재 가야 할 체크포인트를 통과해야만 다음 포인트에서 보상을 받을 수 있도록 논리를 강화했습니다. 추가로, 트랙 생성의 효율성을 위해 스플라인(Spline)을 따라 체크포인트를 자동 생성하는 툴을 개발하여 개발 생산성을 크게 높였습니다.
</div>

<h3 id="21-시스템-아키텍처">2.1 시스템 아키텍처</h3>

<p>트랙의 스플라인 곡선을 분석하여 체크포인트를 자동 생성하고, 에이전트의 진행 상태를 관리하는 계층적 구조를 설계했습니다. <code class="language-plaintext highlighter-rouge">SplineToCheckpointGenerator</code>를 통해 수작업 없이 일정한 간격으로 체크포인트와 스타팅 그리드를 구성할 수 있습니다.</p>

<div class="pf-visual-frame">
<div class="pf-arch-diagram">
<div class="pf-arch-layer">
<div class="pf-arch-layer-title">SplineToCheckpointGenerator</div>
<div class="pf-arch-layer-items">
<span class="pf-arch-item">스플라인 경로 자동 분석</span>
<span class="pf-arch-item">Trigger Collider 일괄 배치</span>
<span class="pf-arch-item">태그 자동 할당</span>
</div>
</div>
<div class="flow-arrow" style="text-align: center;">↓</div>
<div class="pf-arch-layer">
<div class="pf-arch-layer-title">SpawnPointManager / TrackData</div>
<div class="pf-arch-layer-items">
<span class="pf-arch-item">체크포인트 인덱스 배열 관리</span>
<span class="pf-arch-item">다중 스폰 포인트 제어</span>
<span class="pf-arch-item">Finish 라인 및 랩(Lap) 정보 관리</span>
</div>
</div>
<div class="flow-arrow" style="text-align: center;">↓</div>
<div class="pf-arch-layer">
<div class="pf-arch-layer-title">SimcadeCarAgent_Auto</div>
<div class="pf-arch-layer-items">
<span class="pf-arch-item">체크포인트 도달 감지 및 보상</span>
<span class="pf-arch-item">목표 위치 실시간 계산</span>
<span class="pf-arch-item">에피소드 종료 및 리셋 제어</span>
</div>
</div>
</div>
</div>

<p><strong>스플라인 기반 체크포인트 자동 생성 로직</strong>
Unity Spline을 기반으로 정해진 개수만큼의 체크포인트를 트랙의 곡률(탄젠트)에 맞게 회전시켜 자동 생성하고, TrackData에 연동하는 핵심 로직입니다.</p>

<details class="pf-details">
  <summary>코드 보기: 체크포인트 자동 생성 로직</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SplineToCheckpointGenerator.cs: 체크포인트 자동 생성 로직</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">GenerateCheckpoints</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// ... 초기화 및 검증 생략 ...</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">totalCheckpoints</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
    <span class="p">{</span>
        <span class="c1">// 스플라인을 따라 위치 계산</span>
        <span class="kt">float</span> <span class="n">progress</span> <span class="p">=</span> <span class="p">(</span><span class="kt">float</span><span class="p">)(</span><span class="n">i</span> <span class="p">+</span> <span class="m">1</span><span class="p">)</span> <span class="p">/</span> <span class="n">totalCheckpoints</span><span class="p">;</span>
        <span class="n">spline</span><span class="p">.</span><span class="n">Spline</span><span class="p">.</span><span class="nf">Evaluate</span><span class="p">(</span><span class="n">progress</span><span class="p">,</span> <span class="k">out</span> <span class="n">float3</span> <span class="n">pos</span><span class="p">,</span> <span class="k">out</span> <span class="n">float3</span> <span class="n">tan</span><span class="p">,</span> <span class="k">out</span> <span class="n">float3</span> <span class="n">up</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">math</span><span class="p">.</span><span class="nf">lengthsq</span><span class="p">(</span><span class="n">tan</span><span class="p">)</span> <span class="p">==</span> <span class="m">0</span><span class="p">)</span> <span class="n">tan</span> <span class="p">=</span> <span class="n">math</span><span class="p">.</span><span class="nf">forward</span><span class="p">();</span> 

        <span class="c1">// 체크포인트 GameObject 생성 및 배치</span>
        <span class="n">GameObject</span> <span class="n">checkpointObj</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">GameObject</span><span class="p">(</span><span class="s">$"Checkpoint_</span><span class="p">{</span><span class="n">i</span><span class="p">:</span><span class="m">00</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
        <span class="n">checkpointObj</span><span class="p">.</span><span class="n">transform</span><span class="p">.</span><span class="nf">SetParent</span><span class="p">(</span><span class="n">checkpointRoot</span><span class="p">.</span><span class="n">transform</span><span class="p">);</span>
        <span class="n">checkpointObj</span><span class="p">.</span><span class="n">transform</span><span class="p">.</span><span class="n">localPosition</span> <span class="p">=</span> <span class="p">(</span><span class="n">Vector3</span><span class="p">)</span><span class="n">pos</span><span class="p">;</span>
        <span class="n">checkpointObj</span><span class="p">.</span><span class="n">transform</span><span class="p">.</span><span class="n">localRotation</span> <span class="p">=</span> <span class="n">Quaternion</span><span class="p">.</span><span class="nf">LookRotation</span><span class="p">((</span><span class="n">Vector3</span><span class="p">)</span><span class="n">tan</span><span class="p">,</span> <span class="p">(</span><span class="n">Vector3</span><span class="p">)</span><span class="n">up</span><span class="p">);</span>

        <span class="c1">// BoxCollider(Trigger)와 Tag 자동 추가</span>
        <span class="n">BoxCollider</span> <span class="n">collider</span> <span class="p">=</span> <span class="n">checkpointObj</span><span class="p">.</span><span class="n">AddComponent</span><span class="p">&lt;</span><span class="n">BoxCollider</span><span class="p">&gt;();</span>
        <span class="n">collider</span><span class="p">.</span><span class="n">isTrigger</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
        <span class="n">collider</span><span class="p">.</span><span class="n">size</span> <span class="p">=</span> <span class="n">checkpointColliderSize</span><span class="p">;</span>
        <span class="n">checkpointObj</span><span class="p">.</span><span class="n">tag</span> <span class="p">=</span> <span class="n">checkpointTag</span><span class="p">;</span>

        <span class="n">CheckPoint</span> <span class="n">cpScript</span> <span class="p">=</span> <span class="n">checkpointObj</span><span class="p">.</span><span class="n">AddComponent</span><span class="p">&lt;</span><span class="n">CheckPoint</span><span class="p">&gt;();</span>
        <span class="n">cpScript</span><span class="p">.</span><span class="nf">Init</span><span class="p">(</span><span class="n">i</span><span class="p">);</span> 
        <span class="n">checkPointsTemp</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">=</span> <span class="n">checkpointObj</span><span class="p">.</span><span class="n">transform</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="c1">// TrackData에 최종 등록</span>
    <span class="n">trackData</span><span class="p">.</span><span class="nf">SetCheckPoints</span><span class="p">(</span><span class="n">checkPointsTemp</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<p><strong>순차적 체크포인트 검증 로직</strong>
차량이 통과한 체크포인트의 인덱스가 현재 기대되는 순서(NextCheckpointIndex)와 일치하는지 확인하여 논리적 주행 경로를 보장합니다.</p>

<details class="pf-details">
  <summary>코드 보기: 순차적 체크포인트 검증 로직</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CarProgress.cs: 체크포인트 충돌 처리 (내부 상태 갱신)</span>
<span class="k">private</span> <span class="k">void</span> <span class="nf">CheckpointPassed</span><span class="p">(</span><span class="kt">int</span> <span class="n">checkpointIndex</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">IsFinished</span> <span class="p">||</span> <span class="n">trackData</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="c1">// 내가 가야 할 체크포인트가 맞는지 확인</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">checkpointIndex</span> <span class="p">==</span> <span class="n">NextCheckpointIndex</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// 다음 체크포인트로 인덱스 증가</span>
        <span class="n">NextCheckpointIndex</span><span class="p">++;</span>

        <span class="c1">// 마지막 체크포인트를 넘었으면? (랩 종료 또는 완주)</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">NextCheckpointIndex</span> <span class="p">&gt;=</span> <span class="n">trackData</span><span class="p">.</span><span class="n">CheckpointCount</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">NextCheckpointIndex</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="c1">// 다시 0번부터</span>
            <span class="n">CurLap</span><span class="p">++;</span>

            <span class="k">if</span> <span class="p">(</span><span class="n">CurLap</span> <span class="p">&gt;</span> <span class="n">maxLab</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="n">IsFinished</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
                <span class="n">FinalRank</span> <span class="p">=</span> <span class="n">CurRank</span><span class="p">;</span>
                <span class="n">GameManager</span><span class="p">.</span><span class="n">Race</span><span class="p">.</span><span class="nf">AddFinishedCount</span><span class="p">();</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="3-정교한-보상-설계-reward-engineering">3. 정교한 보상 설계 (Reward Engineering)</h2>

<p>에이전트가 단순히 전진하는 것을 넘어, 사고 없이 효율적으로 주행하도록 <strong>다층적 보상 시스템</strong>을 설계했습니다.</p>

<h3 id="31-목표-지향-보상-goal-oriented">3.1 목표 지향 보상 (Goal-Oriented)</h3>

<p>에이전트가 트랙의 흐름에 따라 체크포인트를 순차적으로 정복하도록 유도합니다.</p>

<div class="reward-container">
    <div class="reward-box positive">
        <div class="reward-title">Positive Rewards</div>
        <div style="text-align: left; font-size: 0.85rem; color: #555;">
            • 체크포인트 통과: +1.0 (정밀 도달 확인)<br />
            • Finish 라인 통과: +10.0 (최종 목표 달성)<br />
            • 거리 감소 보상: 매 프레임 목표에 근접 시 비례 보상<br />
            • 방향 정렬 보상: 차량 전방과 목표 방향 일치 시 가점
        </div>
    </div>
    <div class="reward-box negative">
        <div class="reward-title">Negative Penalties</div>
        <div style="text-align: left; font-size: 0.85rem; color: #555;">
            • 잘못된 체크포인트: -0.5 (역주행/우회 방지)<br />
            • 시간 페널티: 매 프레임 -0.001 (빠른 완주 유도)<br />
            • 체크포인트 타임아웃: 30초 내 미도달 시 에피소드 종료
        </div>
    </div>
</div>

<h3 id="32-안전성-및-복구-보상-safety--recovery">3.2 안전성 및 복구 보상 (Safety &amp; Recovery)</h3>

<p>차량이 벽에 충돌하거나 멈추는 등의 비효율적 상황을 최소화하도록 설계된 시스템입니다.</p>

<div class="reward-container">
    <div class="reward-box negative">
        <div class="reward-title">Safety Penalties</div>
        <div style="text-align: left; font-size: 0.85rem; color: #555;">
            • 벽 충돌: -1.0 (즉각적 물리 충돌 발생 시)<br />
            • 벽 근접 페널티: 거리에 따라 비선형적 감점<br />
            • 역주행 페널티: 목표와 150도 이상 차이 시 -0.2 (per frame)<br />
            • 미끄러짐(Slip): 드리프트 임계값 초과 시 비례 감점
        </div>
    </div>
    <div class="reward-box info">
        <div class="reward-title">Recovery &amp; Exit</div>
        <div style="text-align: left; font-size: 0.85rem; color: #555;">
            • 벽 탈출 후진: +0.01 (복구를 위한 전략적 행동)<br />
            • 정지 감지: 2초 이상 속도 0.5 미만 시 감점<br />
            • 뒤집힘 감지: 160도 이상 기울어짐 5초 지속 시 종료<br />
            • 맵 이탈: FallZone 도달 시 -5.0 및 에피소드 종료
        </div>
    </div>
</div>

<p><strong>통합 보상 계산 및 페널티 부여 로직</strong>
목표 지점을 향한 거리 보상, 방향 정렬(Dot Product), 그리고 전방/후방 레이캐스트 센서를 활용한 벽 근접 페널티를 종합적으로 계산하는 핵심 함수입니다.</p>

<details class="pf-details">
  <summary>코드 보기: 벽 근접 페널티 및 역주행 감지 로직</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SimcadeCarAgent_Auto.cs: 보상 계산 로직</span>
<span class="k">private</span> <span class="k">void</span> <span class="nf">CalculateRewards</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// 1. 기본 시간 페널티 (빠른 완주 유도)</span>
    <span class="nf">AddReward</span><span class="p">(</span><span class="n">timePenalty</span><span class="p">);</span>

    <span class="n">Vector3</span> <span class="n">targetPosition</span> <span class="p">=</span> <span class="nf">GetTargetPosition</span><span class="p">();</span>
    <span class="kt">float</span> <span class="n">distanceToTarget</span> <span class="p">=</span> <span class="n">Vector3</span><span class="p">.</span><span class="nf">Distance</span><span class="p">(</span><span class="n">transform</span><span class="p">.</span><span class="n">position</span><span class="p">,</span> <span class="n">targetPosition</span><span class="p">);</span> 

    <span class="k">if</span> <span class="p">(</span><span class="n">targetPosition</span> <span class="p">!=</span> <span class="n">transform</span><span class="p">.</span><span class="n">position</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 2. 방향 정렬 보상 (목표 방향과 현재 차량의 전방 방향 일치도)</span>
        <span class="n">Vector3</span> <span class="n">targetDirection</span> <span class="p">=</span> <span class="p">(</span><span class="n">targetPosition</span> <span class="p">-</span> <span class="n">transform</span><span class="p">.</span><span class="n">position</span><span class="p">).</span><span class="n">normalized</span><span class="p">;</span>
        <span class="kt">float</span> <span class="n">directionDot</span> <span class="p">=</span> <span class="n">Vector3</span><span class="p">.</span><span class="nf">Dot</span><span class="p">(</span><span class="n">transform</span><span class="p">.</span><span class="n">forward</span><span class="p">,</span> <span class="n">targetDirection</span><span class="p">);</span>
        <span class="nf">AddReward</span><span class="p">(</span><span class="n">directionDot</span> <span class="p">*</span> <span class="n">directionAlignmentRewardFactor</span><span class="p">);</span>

        <span class="c1">// 3. 거리 기반 보상 (가까워질수록 보상)</span>
        <span class="kt">float</span> <span class="n">distanceChange</span> <span class="p">=</span> <span class="n">lastDistanceToTarget</span> <span class="p">-</span> <span class="n">distanceToTarget</span><span class="p">;</span>
        <span class="kt">float</span> <span class="n">weight</span> <span class="p">=</span> <span class="p">(</span><span class="n">Vector3</span><span class="p">.</span><span class="nf">Dot</span><span class="p">(</span><span class="n">rb</span><span class="p">.</span><span class="n">linearVelocity</span><span class="p">,</span> <span class="n">transform</span><span class="p">.</span><span class="n">forward</span><span class="p">)</span> <span class="p">&gt;</span> <span class="m">0.1f</span><span class="p">)</span> <span class="p">?</span> <span class="m">1.0f</span> <span class="p">:</span> <span class="m">0.5f</span><span class="p">;</span>
        <span class="nf">AddReward</span><span class="p">(</span><span class="n">distanceChange</span> <span class="p">*</span> <span class="n">distanceRewardFactor</span> <span class="p">*</span> <span class="n">weight</span><span class="p">);</span>
        
        <span class="n">lastDistanceToTarget</span> <span class="p">=</span> <span class="n">distanceToTarget</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// 4. 벽 근접 페널티 (레이캐스트 활용)</span>
    <span class="k">if</span> <span class="p">(!</span><span class="n">isStuckInWall</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">ApplyProximityPenalty</span><span class="p">(</span><span class="n">wallRayDirections</span><span class="p">,</span> <span class="n">wallLayer</span><span class="p">,</span> <span class="n">wallProximityDistance</span><span class="p">,</span> <span class="n">proximityPenalty</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 레이캐스트 기반 근접 페널티 계산</span>
<span class="k">private</span> <span class="k">void</span> <span class="nf">ApplyProximityPenalty</span><span class="p">(</span><span class="n">Vector3</span><span class="p">[]</span> <span class="n">directions</span><span class="p">,</span> <span class="n">LayerMask</span> <span class="n">layer</span><span class="p">,</span> <span class="kt">float</span> <span class="n">distance</span><span class="p">,</span> <span class="kt">float</span> <span class="n">penaltyCoefficient</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">directions</span><span class="p">.</span><span class="n">Length</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span> <span class="p">{</span>
        <span class="n">Vector3</span> <span class="n">worldRayDir</span> <span class="p">=</span> <span class="n">transform</span><span class="p">.</span><span class="nf">TransformDirection</span><span class="p">(</span><span class="n">directions</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">Physics</span><span class="p">.</span><span class="nf">Raycast</span><span class="p">(</span><span class="n">transform</span><span class="p">.</span><span class="n">position</span><span class="p">,</span> <span class="n">worldRayDir</span><span class="p">,</span> <span class="k">out</span> <span class="n">RaycastHit</span> <span class="n">hit</span><span class="p">,</span> <span class="n">distance</span><span class="p">,</span> <span class="n">layer</span><span class="p">))</span> <span class="p">{</span>
            <span class="c1">// 거리가 가까울수록 더 큰 페널티를 부여하여 충돌 회피 유도</span>
            <span class="kt">float</span> <span class="n">penalty</span> <span class="p">=</span> <span class="n">penaltyCoefficient</span> <span class="p">*</span> <span class="p">(</span><span class="m">1f</span> <span class="p">-</span> <span class="p">(</span><span class="n">hit</span><span class="p">.</span><span class="n">distance</span> <span class="p">/</span> <span class="n">distance</span><span class="p">));</span>
            <span class="nf">AddReward</span><span class="p">(</span><span class="n">penalty</span> <span class="p">*</span> <span class="n">Time</span><span class="p">.</span><span class="n">fixedDeltaTime</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="4-환경-감지-및-행동-결정-아키텍처">4. 환경 감지 및 행동 결정 아키텍처</h2>

<p>에이전트는 주변 환경을 입체적으로 인식하기 위해 <strong>총 36차원의 관측(Observation) 데이터</strong>를 수집하며, 이를 기반으로 물리적인 제어를 수행합니다.</p>

<h3 id="41-레이캐스트-기반-환경-센싱">4.1 레이캐스트 기반 환경 센싱</h3>

<p>에이전트가 도로의 폭, 곡률, 장애물과의 거리를 정확히 파악하도록 다방향 레이캐스트 시스템을 구축했습니다.</p>

<div class="pf-visual-frame">
    <img src="/assets/images/mlagent_1.png" alt="Raycast Sensor Visualization" style="width: 100%; border-radius: 8px;" />
    <p style="text-align: center; color: #8b949e; font-size: 0.9rem; margin-top: 10px;">차량의 레이캐스트 센서 시스템 시각화 (빨간색: 감지된 대상, 초록색: 일반 센서 범위, 체크포인트 및 경로 스플라인 표시)</p>
</div>

<div class="pf-visual-frame">
<div class="pf-grid">
<div class="pf-arch-layer">
<div class="pf-arch-layer-title">전방 센서 (9개)</div>
<div style="font-size: 0.85rem; color: #666;">
180도 범위 (-90~90)<br />감지 거리 15m (안전 제동 거리)<br />도로 경계 및 벽 식별
</div>
</div>
<div class="pf-arch-layer">
<div class="pf-arch-layer-title">후방 센서 (3개)</div>
<div style="font-size: 0.85rem; color: #666;">
60도 범위 (-30~30)<br />감지 거리 5m<br />후진 시 후방 충돌 방지
</div>
</div>
<div class="pf-arch-layer">
<div class="pf-arch-layer-title">관측 데이터 구성</div>
<div style="font-size: 0.85rem; color: #666;">
선속도/각속도 (7차원)<br />레이캐스트 정규화 거리 (21차원)<br />목표 방향 및 거리 (4차원)
</div>
</div>
</div>
</div>

<h3 id="42-인지-행동-지연-없는-직접-제어-direct-action-control">4.2 인지-행동 지연 없는 직접 제어 (Direct Action Control)</h3>

<p>초기 학습 단계에서는 급격한 조향 지터링(Jittering)을 방지하기 위해 프레임 간 조향값의 변화량을 제한하는 <strong>입력 보정(Action Smoothing)</strong> 알고리즘을 테스트했습니다. 그러나 차량의 반응성을 떨어뜨려 코너링에서 에이전트의 의도가 지연되는 현상이 확인되었습니다.</p>

<div class="pf-visual-frame">
    <img src="/assets/images/mlagent_3.png" alt="Direct Control Visualization" style="width: 100%; border-radius: 8px;" />
    <p style="text-align: center; color: #8b949e; font-size: 0.9rem; margin-top: 10px;">신경망 출력을 물리 차량 컨트롤러에 직접 매핑하여 반응성을 극대화</p>
</div>

<div class="highlight-box">
    <strong style="color: #007bff;">입력 보정 제거 및 반응성 향상:</strong><br />
    최종 모델에서는 신경망이 현재 상황을 보고 판단한 원시 출력값(Continuous Actions)을 즉각적으로 물리 엔진(Vehicle Controller)에 전달하도록 수정했습니다. 지터링 문제는 강화학습의 하이퍼파라미터 조정 및 보상 체계(Slip Penalty 등) 강화를 통해 궤적 자체를 부드럽게 그리도록 유도하는 방식으로 근본적인 해결을 이끌어냈습니다.
</div>

<p><strong>즉각적 행동 매핑 로직</strong>
입력 스무딩을 제거하여 AI의 조향 및 가속 의도가 지연 없이 차량에 반영되도록 최적화한 컨트롤 메서드입니다.</p>

<details class="pf-details">
  <summary>코드 보기: 즉각적 행동 매핑 로직</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">OnActionReceived</span><span class="p">(</span><span class="n">ActionBuffers</span> <span class="n">actions</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// 액션 스무딩 제거: AI의 의도가 즉각적으로 반영되도록 수정</span>
    <span class="kt">float</span> <span class="n">steeringInput</span> <span class="p">=</span> <span class="n">Mathf</span><span class="p">.</span><span class="nf">Clamp</span><span class="p">(</span><span class="n">actions</span><span class="p">.</span><span class="n">ContinuousActions</span><span class="p">[</span><span class="m">0</span><span class="p">],</span> <span class="p">-</span><span class="m">1f</span><span class="p">,</span> <span class="m">1f</span><span class="p">);</span>
    <span class="kt">float</span> <span class="n">accelerationInput</span> <span class="p">=</span> <span class="n">Mathf</span><span class="p">.</span><span class="nf">Clamp</span><span class="p">(</span><span class="n">actions</span><span class="p">.</span><span class="n">ContinuousActions</span><span class="p">[</span><span class="m">1</span><span class="p">],</span> <span class="p">-</span><span class="m">1f</span><span class="p">,</span> <span class="m">1f</span><span class="p">);</span>

    <span class="n">lastSteeringInput</span> <span class="p">=</span> <span class="n">steeringInput</span><span class="p">;</span>
    <span class="n">lastAccelerationInput</span> <span class="p">=</span> <span class="n">accelerationInput</span><span class="p">;</span>

    <span class="c1">// 보정 없이 차량 컨트롤러에 원시 입력값 직접 전달</span>
    <span class="n">vehicleController</span><span class="p">.</span><span class="nf">ProvideInputs</span><span class="p">(</span><span class="n">accelerationInput</span><span class="p">,</span> <span class="n">steeringInput</span><span class="p">,</span> <span class="m">0f</span><span class="p">);</span>
    
    <span class="nf">CalculateRewards</span><span class="p">();</span>
    <span class="nf">CheckEpisodeEndConditions</span><span class="p">();</span>
    <span class="n">GetComponent</span><span class="p">&lt;</span><span class="n">CarProgress</span><span class="p">&gt;().</span><span class="nf">UpdateProgress</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="5-학습-설정-및-일반화-성능-확보">5. 학습 설정 및 일반화 성능 확보</h2>

<p>특정 트랙에 과적합(Overfitting)되지 않고 다양한 환경에서 안정적으로 주행할 수 있도록 학습 설정을 최적화했습니다.</p>

<h3 id="51-하이퍼파라미터-ppo-기반">5.1 하이퍼파라미터 (PPO 기반)</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: left">파라미터</th>
      <th style="text-align: left">값</th>
      <th style="text-align: left">설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Trainer Type</strong></td>
      <td style="text-align: left">PPO</td>
      <td style="text-align: left">Proximal Policy Optimization</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Batch Size</strong></td>
      <td style="text-align: left">2048</td>
      <td style="text-align: left">대규모 에이전트 동시 업데이트 및 안정성 최적화</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Buffer Size</strong></td>
      <td style="text-align: left">10240</td>
      <td style="text-align: left">정책 업데이트 전 수집할 경험 데이터 크기</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Learning Rate</strong></td>
      <td style="text-align: left">0.0002</td>
      <td style="text-align: left">안정적인 수렴을 위한 선형 감소 학습률</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Beta (Entropy)</strong></td>
      <td style="text-align: left">0.002</td>
      <td style="text-align: left">에이전트의 탐험(Exploration)을 유도하는 가중치</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Hidden Units</strong></td>
      <td style="text-align: left">256</td>
      <td style="text-align: left">신경망의 복잡도 설정 (2개 Hidden Layer)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Max Steps</strong></td>
      <td style="text-align: left">5,000,000</td>
      <td style="text-align: left">모델의 완전한 학습을 위한 총 스텝 수</td>
    </tr>
  </tbody>
</table>

<h3 id="52-다중-트랙-랜덤-학습-시스템">5.2 다중 트랙 랜덤 학습 시스템</h3>

<p>에이전트의 일반화 성능을 극대화하기 위해 매 에피소드 시작 시 랜덤하게 트랙과 스폰 포인트를 교체하는 시스템을 도입했습니다.</p>

<p><strong>다중 트랙 랜덤 스폰 로직</strong>
매 에피소드 초기화 시 다양한 트랙과 위치에서 시작하도록 설정하여 에이전트의 일반화 능력을 향상시킵니다.</p>

<details class="pf-details">
  <summary>코드 보기: 다중 트랙 랜덤 스폰 로직</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">OnEpisodeBegin</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// 1. 등록된 모든 트랙 중 하나를 랜덤 선택</span>
    <span class="n">currentTrack</span> <span class="p">=</span> <span class="n">allTracks</span><span class="p">[</span><span class="n">Random</span><span class="p">.</span><span class="nf">Range</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="n">allTracks</span><span class="p">.</span><span class="n">Count</span><span class="p">)];</span>
    
    <span class="c1">// 2. 해당 트랙의 스폰 포인트 리스트 중 랜덤 위치 선정</span>
    <span class="n">Transform</span> <span class="n">spawnTransform</span> <span class="p">=</span> <span class="n">currentTrack</span><span class="p">.</span><span class="nf">GetStartPoint</span><span class="p">(</span><span class="n">Random</span><span class="p">.</span><span class="nf">Range</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="n">currentTrack</span><span class="p">.</span><span class="n">StartPointsCount</span><span class="p">));</span>
    
    <span class="c1">// 3. 물리 상태 초기화 및 배치</span>
    <span class="n">transform</span><span class="p">.</span><span class="n">position</span> <span class="p">=</span> <span class="n">spawnTransform</span><span class="p">.</span><span class="n">position</span><span class="p">;</span>
    <span class="n">transform</span><span class="p">.</span><span class="n">rotation</span> <span class="p">=</span> <span class="n">spawnTransform</span><span class="p">.</span><span class="n">rotation</span><span class="p">;</span>
    <span class="n">rb</span><span class="p">.</span><span class="n">linearVelocity</span> <span class="p">=</span> <span class="n">Vector3</span><span class="p">.</span><span class="n">zero</span><span class="p">;</span>
    <span class="n">carProgress</span><span class="p">.</span><span class="nf">Initialize</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="6-기술적-성과-및-결론">6. 기술적 성과 및 결론</h2>

<ul>
  <li><strong>주행 안정성:</strong> 보상 체계 정밀화를 통해 조향 지터링을 스스로 극복하는 부드러운 코너링 구현.</li>
  <li><strong>환경 적응력:</strong> 다중 트랙 학습을 통해 학습되지 않은 새로운 트랙에서도 90% 이상의 완주율 달성.</li>
  <li><strong>복구 지능:</strong> 충돌 시 스스로 후진하여 경로로 복귀하거나, 복구 불가능 시 빠르게 재시도하는 효율적 학습 루프 구축.</li>
</ul>

<p class="notice--success"><strong>Conclusion:</strong> ML-Agents의 연속 행동 공간(Continuous Action Space)을 물리 기반 차량 컨트롤러와 결합하여, 단순한 경로 추적을 넘어 물리적 한계를 고려하며 최적의 궤적을 그리는 자율 주행 AI를 성공적으로 구축했습니다.</p>]]></content><author><name>이민혁 (Minhyuk Lee)</name></author><category term="project" /><category term="Machine Learning" /><category term="ML-Agents" /><category term="AI" /><category term="Unity" /><category term="Reinforcement Learning" /><summary type="html"><![CDATA[Unity ML-Agents를 활용한 자율 주행 경주차 에이전트 학습 시스템 구축 및 보상 설계 최적화]]></summary></entry><entry><title type="html">PROJECT REPORT // FILE_TOWER_DEFENSE</title><link href="https://infisword.github.io/project/file-tower-defense/" rel="alternate" type="text/html" title="PROJECT REPORT // FILE_TOWER_DEFENSE" /><published>2026-05-23T00:00:00+00:00</published><updated>2026-05-23T00:00:00+00:00</updated><id>https://infisword.github.io/project/file-tower-defense</id><content type="html" xml:base="https://infisword.github.io/project/file-tower-defense/"><![CDATA[<style>
    /* --- Premium Report Layout Styles --- */
    
    .chapter-title {
        font-size: 2.2rem;
        color: #fff !important;
        background: linear-gradient(90deg, #007bff 0%, #58A6FF 100%);
        padding: 25px 35px;
        border-radius: 12px;
        margin-top: 60px !important;
        margin-bottom: 40px !important;
        box-shadow: 0 10px 30px rgba(0, 123, 255, 0.15);
        display: flex;
        align-items: center;
        border: none !important;
    }
    .chapter-title::before {
        content: "CHAPTER.";
        font-family: 'Fira Code', monospace;
        font-size: 0.9rem;
        letter-spacing: 2px;
        margin-right: 15px;
        opacity: 0.8;
    }

    .pf-visual-frame {
        width: 100%; padding: 35px; background: #fcfcfc;
        border: 1px solid #e1e4e8; border-radius: 16px;
        margin: 30px 0; text-align: center;
        box-shadow: inset 0 2px 10px rgba(0,0,0,0.02);
    }
    .pf-arch-diagram {
        display: flex; flex-direction: column; gap: 20px; margin: 25px 0;
    }
    .pf-arch-layer {
        padding: 20px 25px; border: 1px solid #e1e4e8; border-radius: 8px;
        background: #fff; position: relative; text-align: center;
        box-shadow: 0 4px 12px rgba(0,0,0,0.03);
    }
    .pf-arch-layer::before {
        content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
        background: #007bff; border-radius: 8px 0 0 8px;
    }
    .pf-arch-layer-title {
        color: #007bff; font-weight: 700; margin-bottom: 10px;
        font-family: 'Fira Code', monospace; font-size: 0.95rem; text-align: center;
    }
    .pf-arch-layer-items {
        display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; justify-content: center;
    }
    .pf-arch-item {
        padding: 6px 14px; background: rgba(0, 123, 255, 0.08);
        border: 1px solid rgba(0, 123, 255, 0.3); border-radius: 6px;
        font-size: 0.85rem; color: #0056b3; text-align: center;
    }
    .pf-diagram-grid {
        display: grid; grid-template-columns: repeat(3, 45px); gap: 8px; justify-content: center; margin: 15px 0;
    }
    .pf-grid-cell { width: 45px; height: 45px; border: 1px solid #58A6FF; opacity: 0.2; border-radius: 4px; }
    .pf-grid-cell.active { background: #58A6FF; opacity: 1; box-shadow: 0 0 15px #58A6FF; }
    .pf-grid-cell.near { border-color: #28a745; background: rgba(40, 167, 69, 0.15); opacity: 1; }
    
    .pf-logic-container {
        display: flex; flex-direction: column; gap: 10px; margin: 20px 0; text-align: left;
    }
    .pf-logic-row {
        display: grid; grid-template-columns: 120px 1fr; gap: 15px; align-items: center;
        padding: 15px; border-radius: 8px; background: #fff; border: 1px solid #eee;
    }
    .pf-logic-label {
        font-weight: 700; color: #007bff; font-family: 'Fira Code', monospace; text-align: center;
        background: rgba(0, 123, 255, 0.05); padding: 5px; border-radius: 4px;
    }

    details.pf-details {
        margin: 20px 0;
        border: 1px solid #e1e4e8;
        border-radius: 8px;
        background: #f8f9fa;
        overflow: hidden;
    }
    details.pf-details summary {
        padding: 15px 20px;
        font-weight: 700;
        color: #007bff;
        cursor: pointer;
        outline: none;
        background: #fff;
        display: flex;
        align-items: center;
    }
    .details-desc {
        padding: 15px 20px;
        background: #fff;
        color: #666;
        font-size: 0.95rem;
        border-top: 1px solid #e1e4e8;
        line-height: 1.6;
    }

    /* --- Transaction Flow --- */
    .pf-transaction-flow {
        display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-family: 'Fira Code', monospace;
        gap: 12px; flex-wrap: wrap; margin: 20px 0;
    }
    .pf-flow-step { padding: 10px 18px; border: 1px solid #e1e4e8; border-radius: 6px; background: #fff; color: #333; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.05); line-height: 1.4; }
    .pf-flow-arrow { color: #007bff; font-weight: bold; font-size: 1.2rem; }

    /* --- Tables --- */
    .pf-table-wrapper { display: block !important; width: 100% !important; overflow-x: auto; margin: 20px 0; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.03); }
    .pf-data-table {
        width: 100% !important; border-collapse: collapse; font-size: 0.9rem;
        background: #fff; border: 1px solid #e1e4e8; table-layout: fixed;
    }
    .pf-data-table th {
        background: rgba(0, 123, 255, 0.08); color: #007bff;
        padding: 15px; text-align: center; font-weight: 700; border-bottom: 2px solid #e1e4e8;
        font-family: 'Fira Code', monospace;
    }
    .pf-data-table td {
        padding: 12px 15px; border-bottom: 1px solid #e1e4e8; color: #555;
        text-align: center; line-height: 1.5;
    }
    .pf-data-table tr:last-child td { border-bottom: none; }
    .pf-data-table tr:hover { background: rgba(0, 123, 255, 0.02); }

    /* Coordinate Flow */
    .pf-coord-flow {
        display: flex; gap: 15px; margin: 25px 0;
        align-items: stretch; overflow-x: auto; padding-bottom: 10px;
    }
    .pf-coord-box {
        padding: 18px; border: 2px solid #007bff; border-radius: 8px;
        background: rgba(0, 123, 255, 0.02); text-align: center; display: flex; flex-direction: column;
        justify-content: center; min-height: 110px; min-width: 190px; flex: 0 0 auto;
    }
    .pf-coord-box-title {
        color: #007bff; font-weight: 700; margin-bottom: 10px;
        font-family: 'Fira Code', monospace; font-size: 0.9rem; border-bottom: 1px solid #eee;
        padding-bottom: 8px;
    }
    .pf-coord-box-formula {
        color: #28a745; font-size: 0.75rem; font-family: 'Fira Code', monospace;
        margin-top: 8px; padding: 6px; background: rgba(40, 167, 69, 0.05); border-radius: 4px;
    }
    
    /* Comparison Boxes */
    .pf-comp-container { display: flex; justify-content: space-around; align-items: center; flex-wrap: wrap; gap: 20px; }
    .pf-comp-box { font-family: 'Fira Code', monospace; font-size: 0.8rem; border-radius: 8px; padding: 15px; min-width: 200px; text-align: left; line-height: 1.6; }
    .pf-comp-box.old { border: 1px solid #ff7b72; background: rgba(255, 123, 114, 0.05); color: #ff7b72; }
    .pf-comp-box.new { border: 1px solid #28a745; background: rgba(40, 167, 69, 0.05); color: #28a745; }
</style>

<p><strong>File Tower Defense</strong> 프로젝트의 코어 시스템 설계를 담당하며, 유니티의 좌표계 특성을 분석한 아키텍처 재설계부터 대규모 객체의 입력 처리, 동적 버프 시스템, 그리고 그리드 기반 배치 로직까지 전반적인 시스템 개선 과정을 기록한 리포트입니다.</p>

<hr />

<h2 class="chapter-title" id="1-시스템-리팩토링-및-개선">1. 시스템 리팩토링 및 개선</h2>

<h3 id="11-uirecttransform에서-gameobjecttransform-기반-전환">1.1 UI(RectTransform)에서 GameObject(Transform) 기반 전환</h3>

<p>초기 설계에서는 윈도우 바탕화면의 아이콘 느낌을 살리기 위해 모든 파일 유닛을 UI 시스템(<code class="language-plaintext highlighter-rouge">RectTransform</code>)으로 구축했습니다. 그러나 프로젝트가 고도화됨에 따라 다음과 같은 한계에 직면했습니다.</p>

<ul>
  <li><strong>좌표계 종속성:</strong> 캔버스의 앵커/피벗 설정 및 해상도에 따라 월드 좌표가 상대적으로 변하여 게임 월드 내 정밀한 위치 계산과 사거리 판정이 매우 까다로움.</li>
  <li><strong>연산 오버헤드 (Canvas Rebuilding):</strong> 유닛의 움직임, 호버 효과, 드래그 등의 UI 요소가 갱신될 때마다 Unity 내부에서 Canvas Rebuild가 강제로 유발되어 CPU 프레임 드랍 발생.</li>
  <li><strong>물리 엔진의 부재:</strong> 월드 좌표 기반의 바이러스(적)와 UI 유닛 간 물리 충돌 판정을 위해 매 프레임 <code class="language-plaintext highlighter-rouge">Camera.WorldToScreenPoint</code> 등의 물리-화면 좌표 변환 함수 호출 비용 발생.</li>
</ul>

<div class="pf-visual-frame">
  <div class="pf-comp-container">
    <div class="pf-comp-box old">
      <strong>RectTransform (UI)</strong><br />
      - Anchor/Pivot Dependent<br />
      - Frequent Canvas Rebuilds<br />
      - Heavy Screen-to-World conversions
    </div>
    <div class="pf-flow-arrow">&gt;&gt;&gt;</div>
    <div class="pf-comp-box new">
      <strong>Transform (World)</strong><br />
      - Absolute 2D Coordinates<br />
      - Zero Canvas Overhead<br />
      - Native Physics 2D &amp; Colliders
    </div>
  </div>
</div>

<p>이를 해결하기 위해 모든 유닛을 <strong>GameObject(Transform) 기반</strong>으로 전면 교체하여 월드 좌표계로 통일하였으며, Physics2D와의 직접적인 호환성을 확보하여 불필요한 연산을 제거하고 성능을 극적으로 최적화했습니다.</p>

<h3 id="12-gameobjectgridlayout-커스텀-레이아웃-엔진">1.2 GameObjectGridLayout: 커스텀 레이아웃 엔진</h3>

<p>화면 해상도에 맞춰 그리드의 간격과 셀 크기를 동적으로 계산하는 레이아웃 엔진을 구축했습니다. 유니티의 <code class="language-plaintext highlighter-rouge">GridLayoutGroup</code>은 UI 전용이므로, 일반 월드 공간 오브젝트를 위해 화면 크기에 맞게 자동으로 셀 크기를 스케일링하는 컴포넌트를 직접 개발했습니다.</p>

<div class="pf-visual-frame">
  <strong>해상도 기반 자동 조절 공식:</strong><br />
  <code>cellSize = (screenSize - padding*2 - spacing*(n-1)) / n</code>
</div>

<details class="pf-details">
  <summary>코드 보기: FitToScreen 로직</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// GameObjectGridLayout.cs: 해상도 대응 셀 크기 계산</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">FitToScreen</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">Camera</span> <span class="n">cam</span> <span class="p">=</span> <span class="n">Camera</span><span class="p">.</span><span class="n">main</span><span class="p">;</span>
    <span class="kt">float</span> <span class="n">height</span> <span class="p">=</span> <span class="n">cam</span><span class="p">.</span><span class="n">orthographicSize</span> <span class="p">*</span> <span class="m">2f</span><span class="p">;</span> <span class="c1">// 오르토그래픽 카메라 높이</span>
    <span class="kt">float</span> <span class="n">width</span> <span class="p">=</span> <span class="n">height</span> <span class="p">*</span> <span class="n">cam</span><span class="p">.</span><span class="n">aspect</span><span class="p">;</span>         <span class="c1">// 카메라 종횡비 기반 너비</span>

    <span class="c1">// 가용 영역에서 패딩과 스페이싱을 제외한 실제 셀 크기 도출</span>
    <span class="n">cellSize</span><span class="p">.</span><span class="n">x</span> <span class="p">=</span> <span class="p">(</span><span class="n">width</span> <span class="p">-</span> <span class="p">(</span><span class="n">padding</span><span class="p">.</span><span class="n">x</span> <span class="p">*</span> <span class="m">2</span><span class="p">)</span> <span class="p">-</span> <span class="p">(</span><span class="n">spacing</span><span class="p">.</span><span class="n">x</span> <span class="p">*</span> <span class="p">(</span><span class="n">columns</span> <span class="p">-</span> <span class="m">1</span><span class="p">)))</span> <span class="p">/</span> <span class="n">columns</span><span class="p">;</span>
    <span class="n">cellSize</span><span class="p">.</span><span class="n">y</span> <span class="p">=</span> <span class="p">(</span><span class="n">height</span> <span class="p">-</span> <span class="p">(</span><span class="n">padding</span><span class="p">.</span><span class="n">y</span> <span class="p">*</span> <span class="m">2</span><span class="p">)</span> <span class="p">-</span> <span class="p">(</span><span class="n">spacing</span><span class="p">.</span><span class="n">y</span> <span class="p">*</span> <span class="p">(</span><span class="n">rows</span> <span class="p">-</span> <span class="m">1</span><span class="p">)))</span> <span class="p">/</span> <span class="n">rows</span><span class="p">;</span>

    <span class="c1">// 그리드의 좌상단 시작 월드 좌표 계산</span>
    <span class="n">StartPos</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Vector2</span><span class="p">(-(</span><span class="n">width</span> <span class="p">/</span> <span class="m">2</span><span class="p">)</span> <span class="p">+</span> <span class="n">padding</span><span class="p">.</span><span class="n">x</span> <span class="p">+</span> <span class="p">(</span><span class="n">cellSize</span><span class="p">.</span><span class="n">x</span> <span class="p">/</span> <span class="m">2</span><span class="p">),</span> 
                            <span class="p">(</span><span class="n">height</span> <span class="p">/</span> <span class="m">2</span><span class="p">)</span> <span class="p">-</span> <span class="n">padding</span><span class="p">.</span><span class="n">y</span> <span class="p">-</span> <span class="p">(</span><span class="n">cellSize</span><span class="p">.</span><span class="n">y</span> <span class="p">/</span> <span class="m">2</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="2-파일바이러스ui-입력-및-상호작용-시스템">2. 파일/바이러스/UI 입력 및 상호작용 시스템</h2>

<p>수많은 유닛이 각자 <code class="language-plaintext highlighter-rouge">Update()</code>에서 마우스 충돌을 검사하면 연산량이 기하급수적으로 늘어납니다. 이를 방지하기 위해 <strong>Mediator 패턴</strong>을 활용하여 <strong>InputManager</strong>가 모든 마우스 입력을 통합 관리하고, <strong>IInteractable</strong> 인터페이스를 구현한 대상 객체에게만 이벤트를 전송하도록 설계했습니다.</p>

<h3 id="21-중앙-집중식-입력-아키텍처">2.1 중앙 집중식 입력 아키텍처</h3>

<p><code class="language-plaintext highlighter-rouge">InputManager</code>는 마우스의 상태 머신(<code class="language-plaintext highlighter-rouge">InputState</code>)을 기반으로 클릭, 드래그, 다중 선택(Ctrl+드래그)을 판단하여 <code class="language-plaintext highlighter-rouge">InteractionHandler</code>를 통해 이벤트를 전파합니다.</p>

<pre><code class="language-mermaid">graph TD
    A[Mouse / Touch Input] --&gt; B[InputManager]
    B --&gt;|1. Raycast &amp; UI Check| C{Is Pointer Over UI?}
    C --&gt;|Yes| D[Ignore World Interaction / Handle UI]
    C --&gt;|No| E[GetInteractableUnderMouse]
    E --&gt;|2. Sort by Layer/Order| F[Identify Target IInteractable]
    B --&gt;|3. Update InputState| G{InputState}
    G --&gt;|Pressing| H[Check Drag Threshold]
    G --&gt;|DraggingObject| I[OnDragHandler]
    G --&gt;|DraggingBox| J[Multi-Select Drag Box]
    B --&gt;|4. Dispatch Events| K[InteractionHandler]
    K --&gt;|Trigger Interface Methods| L[IInteractable Objects]
    
    style B fill:#007bff,stroke:#0056b3,color:#fff
    style K fill:#28a745,stroke:#1e7e34,color:#fff
    style L fill:#17a2b8,stroke:#117a8b,color:#fff
</code></pre>

<div class="pf-visual-frame">
    <img src="/assets/Gifs/file-tower-defense-interaction.gif" alt="File Tower Defense Input Interaction Demo" style="width: 100%; border-radius: 8px;" />
    <p style="color: #666; font-size: 0.85rem; margin-top: 10px; font-family: 'Fira Code', monospace;">[DEMO] InputManager 기반 마우스 호버 및 클릭 감지</p>
</div>

<h4 id="객체-중첩-판정-알고리즘">객체 중첩 판정 알고리즘</h4>
<p>마우스 아래에 여러 유닛이나 바이러스, 혹은 UI 판정이 겹쳐있을 때, 다음과 같은 우선순위 기준을 사용하여 모호함을 해결합니다.</p>
<ol>
  <li><strong>UI 레이어 우선:</strong> <code class="language-plaintext highlighter-rouge">EventSystem.current.IsPointerOverGameObject()</code>를 통해 UI 상호작용 우선 처리.</li>
  <li><strong>물리 레이어 필터링:</strong> <code class="language-plaintext highlighter-rouge">Physics2D.RaycastNonAlloc</code>을 활용하여 쓰레기 메모리(GC Alloc) 없이 감지된 콜라이더 배열을 가져옴.</li>
  <li><strong>정렬 레이어(Sorting Order) 정렬:</strong> 감지된 객체들의 <code class="language-plaintext highlighter-rouge">SpriteRenderer</code> 내 <code class="language-plaintext highlighter-rouge">Sorting Layer ID</code> 및 <code class="language-plaintext highlighter-rouge">Order in Layer</code> 값을 비교하여 카메라와 가장 가까운(최상단에 렌더링된) 오브젝트를 타깃으로 최종 선택.</li>
</ol>

<h3 id="22-iinteractable-인터페이스-기반-확장성">2.2 IInteractable 인터페이스 기반 확장성</h3>

<p>클릭 및 드래그 상호작용이 필요한 모든 인게임 오브젝트(유닛 파일, 바이러스 등)는 <a href="file:///D:/Workspace/codeReference/file_tower_defence/IInteractable.cs">IInteractable</a> 인터페이스를 상속받아 유연하게 확장할 수 있습니다.</p>

<div class="pf-visual-frame">
    <img src="/assets/Gifs/file-tower-defense-drag.gif" alt="File Tower Defense Mouse Drag Event Demo" style="width: 100%; border-radius: 8px;" />
    <p style="color: #666; font-size: 0.85rem; margin-top: 10px; font-family: 'Fira Code', monospace;">[DEMO] IInteractable 상속 객체의 실시간 마우스 드래그 &amp; 배치 흐름</p>
</div>

<details class="pf-details">
  <summary>코드 보기: IInteractable 인터페이스</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IInteractable</span> <span class="p">{</span>
    <span class="n">GameObject</span> <span class="n">targetObj</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>
    <span class="kt">bool</span> <span class="n">IsSelectable</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>
    <span class="kt">bool</span> <span class="n">IsDraggable</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>
    
    <span class="k">void</span> <span class="nf">OnHoverEnter</span><span class="p">();</span>
    <span class="k">void</span> <span class="nf">OnHoverExit</span><span class="p">();</span>
    <span class="k">void</span> <span class="nf">OnClick</span><span class="p">();</span>
    <span class="k">void</span> <span class="nf">OnBeginDrag</span><span class="p">();</span>
    <span class="k">void</span> <span class="nf">OnDrag</span><span class="p">(</span><span class="n">Vector2</span> <span class="n">mouseDelta</span><span class="p">);</span>
    <span class="k">void</span> <span class="nf">OnEndDrag</span><span class="p">();</span>
    <span class="k">void</span> <span class="nf">OnSelected</span><span class="p">(</span><span class="kt">bool</span> <span class="n">isSelected</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="3-그리드-기반-오브젝트-배치-시스템">3. 그리드 기반 오브젝트 배치 시스템</h2>

<h3 id="31-filegrid-데이터-중심의-지능형-셀-매니저">3.1 FileGrid: 데이터 중심의 지능형 셀 매니저</h3>

<p><a href="file:///D:/Workspace/codeReference/file_tower_defence/FileGrid.cs">FileGrid</a>는 단순한 위치 정보 홀더가 아니라, 유닛 배치 상태와 해당 셀에 작용 중인 오라(버프) 목록을 독자적으로 관리하는 지능형 컨테이너입니다.</p>
<ul>
  <li><strong>HashSet 기반 버프 소스 관리:</strong> 현재 그리드 공간에 영향을 주는 버프 제공 유닛의 목록을 <code class="language-plaintext highlighter-rouge">HashSet&lt;File_Base&gt;</code>로 관리하여 버프의 중복 적용을 제거하고 $O(1)$의 빠른 조회 속도를 유지합니다.</li>
  <li><strong>유닛 탈부착 시 자동 스탯 갱신:</strong> 유닛이 배치되거나 이탈할 때 그리드에 축적된 버프 목록을 분석하여 대상 유닛의 스탯을 실시간으로 갱신해 줍니다.</li>
</ul>

<h3 id="32-원자적-배치-트랜잭션-transactional-pattern">3.2 원자적 배치 트랜잭션 (Transactional Pattern)</h3>

<p>드래그 앤 드롭으로 유닛의 그리드 위치를 옮길 때 발생할 수 있는 데이터 불일치 및 예외 상황을 원자적으로 보장하기 위해 배치 트랜잭션 흐름을 <strong>탐색/검증, 성공(Commit), 실패(Rollback)</strong>의 3가지 흐름으로 분할 설계하여 안전성을 확보했습니다.</p>

<h4 id="1-탐색-및-검증-단계-search--validation">1. 탐색 및 검증 단계 (Search &amp; Validation)</h4>
<p>배치 위치의 그리드를 감지하고, 해당 그리드가 배치 가능한 지역인지(장애물이나 다른 유닛 유무) 검사합니다.</p>

<pre><code class="language-mermaid">sequenceDiagram
    autonumber
    actor Player
    participant IM as InputManager
    participant IH as InteractionHandler
    participant FGM as FileGridManager
    
    Player-&gt;&gt;IM: Drag &amp; Release Unit
    IM-&gt;&gt;IH: End Drag Event
    IH-&gt;&gt;FGM: GetGrid(Release Position)
    FGM--&gt;&gt;IH: Return Target Grid (New Grid)
    IH-&gt;&gt;FGM: Validate Placement (New Grid, No Obstacles)
</code></pre>

<h4 id="2-트랜잭션-확정-단계-commit---검증-성공-시">2. 트랜잭션 확정 단계 (Commit - 검증 성공 시)</h4>
<p>검증에 성공하면 기존 그리드의 점유를 해제하고, 새 그리드에 유닛을 정식 등록한 후 스탯(버프) 정보를 동적으로 갱신합니다.</p>

<pre><code class="language-mermaid">sequenceDiagram
    autonumber
    participant IH as InteractionHandler
    participant FG_Old as Old FileGrid
    participant FG_New as New FileGrid
    participant Unit as File Unit (File_Base)

    Note over IH, Unit: Validation Success (Commit)
    IH-&gt;&gt;FG_Old: RemoveFileUnit() (Clear occupant &amp; remove old buffs)
    IH-&gt;&gt;FG_New: SetFileUnit(Unit) (Assign occupant &amp; apply new buffs)
    IH-&gt;&gt;Unit: Commit Position (Smooth Lerp / Snap)
</code></pre>

<h4 id="3-트랜잭션-복구-단계-rollback---검증-실패-시">3. 트랜잭션 복구 단계 (Rollback - 검증 실패 시)</h4>
<p>새 그리드가 유효하지 않거나 장애물이 감지된 경우, 상태를 복구하고 유닛을 드래그 시작 전 원래의 그리드 위치로 되돌립니다.</p>

<pre><code class="language-mermaid">sequenceDiagram
    autonumber
    participant IH as InteractionHandler
    participant Unit as File Unit (File_Base)

    Note over IH, Unit: Validation Fails (Rollback)
    IH-&gt;&gt;Unit: Rollback (Return to Old Grid position)
</code></pre>

<h3 id="33-동적-양방향-좌표-변환-시스템">3.3 동적 양방향 좌표 변환 시스템</h3>

<p>마우스의 월드 스페이스 좌표와 그리드의 인덱스(Row, Column) 좌표계를 상호 변환하기 위해 오프셋 기반의 연산 로직을 정밀하게 구현했습니다.</p>

<div class="pf-visual-frame">
  <div class="pf-coord-flow">
    <div class="pf-coord-box">
      <div class="pf-coord-box-title">World Position</div>
      <div class="pf-coord-box-formula">InverseTransformPoint(worldPos)</div>
    </div>
    <div style="display: flex; align-items: center; font-size: 1.5rem; color: #007bff;">⇄</div>
    <div class="pf-coord-box">
      <div class="pf-coord-box-title">Grid Index (X, Y)</div>
      <div class="pf-coord-box-formula">RoundToInt((local - start) / cellSize)</div>
    </div>
  </div>
</div>

<h3 id="34-공간-분할-기반-탐색-최적화-o1">3.4 공간 분할 기반 탐색 최적화 (O(1))</h3>

<p>마우스를 드래그할 때 가장 가까운 그리드를 찾기 위해 전체 그리드($N \times M$개)를 전수 검사하는 것은 낭비입니다. 이를 극복하기 위해 <strong>공간 분할(Spatial Partitioning)</strong> 개념을 도입했습니다. 
마우스 월드 좌표를 기반으로 연산 $O(1)$ 만에 예상되는 타깃 그리드 인덱스를 수학적으로 산출하고, 해당 인덱스를 중심으로 <strong>인접한 3x3 그리드 셀(총 9개)</strong>만 가중치(거리 제곱) 계산을 수행하여 탐색 성능을 획기적으로 개선했습니다.</p>

<div class="pf-visual-frame">
  <div class="pf-diagram-grid">
    <div class="pf-grid-cell near"></div><div class="pf-grid-cell near"></div><div class="pf-grid-cell near"></div>
    <div class="pf-grid-cell near"></div><div class="pf-grid-cell active"></div><div class="pf-grid-cell near"></div>
    <div class="pf-grid-cell near"></div><div class="pf-grid-cell near"></div><div class="pf-grid-cell near"></div>
  </div>
  <p style="font-size: 0.85rem; color: #666; margin-top: 10px;">Spatial Partitioning: Searching 9 cells instead of Entire Grids</p>
</div>

<details class="pf-details">
  <summary>코드 보기: 공간 분할 탐색 알고리즘</summary>

  <div class="details-desc">
월드 좌표를 인덱스로 즉시 변환한 뒤, 해당 인덱스를 중심으로 3x3 영역 내의 그리드만 제곱 거리(`sqrMagnitude`)로 비교하여 최적의 그리드를 탐색합니다.
</div>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// FileGridManager.cs: 공간 분할 기반 탐색 최적화 로직</span>
<span class="k">public</span> <span class="n">FileGrid</span> <span class="nf">GetGrid</span><span class="p">(</span><span class="n">Vector2</span> <span class="n">worldPos</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 1. 월드 좌표를 그리드 인덱스로 수학적 변환 (O(1))</span>
    <span class="k">if</span> <span class="p">(!</span><span class="nf">WorldToGridIndex</span><span class="p">(</span><span class="n">worldPos</span><span class="p">,</span> <span class="k">out</span> <span class="kt">int</span> <span class="n">xCenter</span><span class="p">,</span> <span class="k">out</span> <span class="kt">int</span> <span class="n">yCenter</span><span class="p">))</span> <span class="k">return</span> <span class="k">null</span><span class="p">;</span>

    <span class="c1">// 2. 인덱스가 유효하면 주변 3x3 영역만 빠르게 검사 (O(1))</span>
    <span class="k">if</span> <span class="p">(</span><span class="nf">IsValidGridIndex</span><span class="p">(</span><span class="n">xCenter</span><span class="p">,</span> <span class="n">yCenter</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nf">FindClosestGridInRange</span><span class="p">(</span><span class="n">worldPos</span><span class="p">,</span> <span class="n">xCenter</span><span class="p">,</span> <span class="n">yCenter</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// 화면 영역 밖의 비정상 예외 상황에서만 전체 탐색 수행 (Safety Net)</span>
    <span class="k">return</span> <span class="nf">FindGlobalClosestGrid</span><span class="p">(</span><span class="n">worldPos</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">private</span> <span class="n">FileGrid</span> <span class="nf">FindClosestGridInRange</span><span class="p">(</span><span class="n">Vector2</span> <span class="n">worldPos</span><span class="p">,</span> <span class="kt">int</span> <span class="n">centerX</span><span class="p">,</span> <span class="kt">int</span> <span class="n">centerY</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">float</span> <span class="n">minDistSqr</span> <span class="p">=</span> <span class="kt">float</span><span class="p">.</span><span class="n">MaxValue</span><span class="p">;</span>
    <span class="n">FileGrid</span> <span class="n">closestGrid</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>

    <span class="c1">// 중심 기준 3x3 루프</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">dx</span> <span class="p">=</span> <span class="p">-</span><span class="m">1</span><span class="p">;</span> <span class="n">dx</span> <span class="p">&lt;=</span> <span class="m">1</span><span class="p">;</span> <span class="n">dx</span><span class="p">++)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">dy</span> <span class="p">=</span> <span class="p">-</span><span class="m">1</span><span class="p">;</span> <span class="n">dy</span> <span class="p">&lt;=</span> <span class="m">1</span><span class="p">;</span> <span class="n">dy</span><span class="p">++)</span> <span class="p">{</span>
            <span class="kt">int</span> <span class="n">x</span> <span class="p">=</span> <span class="n">centerX</span> <span class="p">+</span> <span class="n">dx</span><span class="p">;</span>
            <span class="kt">int</span> <span class="n">y</span> <span class="p">=</span> <span class="n">centerY</span> <span class="p">+</span> <span class="n">dy</span><span class="p">;</span>

            <span class="k">if</span> <span class="p">(</span><span class="nf">IsValidGridIndex</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">))</span> <span class="p">{</span>
                <span class="n">FileGrid</span> <span class="n">candidate</span> <span class="p">=</span> <span class="n">gridArray</span><span class="p">[</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">];</span>
                <span class="c1">// 제곱 거리(sqrMagnitude) 사용으로 비용이 큰 제곱근(Sqrt) 연산 생략</span>
                <span class="kt">float</span> <span class="n">distSqr</span> <span class="p">=</span> <span class="p">((</span><span class="n">Vector2</span><span class="p">)</span><span class="n">candidate</span><span class="p">.</span><span class="n">transform</span><span class="p">.</span><span class="n">position</span> <span class="p">-</span> <span class="n">worldPos</span><span class="p">).</span><span class="n">sqrMagnitude</span><span class="p">;</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">distSqr</span> <span class="p">&lt;</span> <span class="n">minDistSqr</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">minDistSqr</span> <span class="p">=</span> <span class="n">distSqr</span><span class="p">;</span>
                    <span class="n">closestGrid</span> <span class="p">=</span> <span class="n">candidate</span><span class="p">;</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">closestGrid</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>  </div>

</details>

<h3 id="35-플래그-기반-확장-가능한-검색-시스템">3.5 플래그 기반 확장 가능한 검색 시스템</h3>

<p>그리드를 검색할 때 ‘비어 있는 곳’, ‘장애물이 없는 곳’, ‘이미 아군이 배치된 곳’ 등 다양한 복합 조건을 비트 플래그 형태로 손쉽게 검색할 수 있도록 가변 플래그 검색 시스템을 설계했습니다.</p>

<div class="pf-visual-frame">
  <div class="pf-table-wrapper">
    <table class="pf-data-table" style="width: 100% !important; table-layout: fixed !important;">
    <thead>
        <tr>
            <th style="width: 20%; text-align: center;">플래그</th>
            <th style="width: 35%; text-align: center;">설명</th>
            <th style="width: 45%; text-align: center;">사용 예시 및 기대 결과</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td style="text-align: center;"><code>Occupied</code></td>
            <td style="text-align: left;">현재 파일 유닛이 배치되어 점유 중인 그리드 셀만을 탐색 대상으로 한정합니다.</td>
            <td style="text-align: left;">이미 설치된 특정 타워의 위치를 추적하거나, 인접한 유닛의 시너지를 계산할 때 활용됩니다.</td>
        </tr>
        <tr>
            <td style="text-align: center;"><code>NotOccupied</code></td>
            <td style="text-align: left;">파일 유닛이 배치되지 않은 비어있는 상태의 그리드 셀만을 필터링하여 검색합니다.</td>
            <td style="text-align: left;">플레이어가 새로운 파일 유닛을 드래그하여 설치 가능한 빈 공간을 유효성 검사할 때 필수적으로 사용됩니다.</td>
        </tr>
        <tr>
            <td style="text-align: center;"><code>Obstacle</code></td>
            <td style="text-align: left;">시스템 장애물(땅굴 등)이 생성되어 일반적인 유닛 배치가 불가능한 그리드만을 검색합니다.</td>
            <td style="text-align: left;">맵 파괴 이벤트나 바이러스의 특수 공격으로 인해 생성된 장애물 객체의 위치를 파악할 때 사용됩니다.</td>
        </tr>
        <tr>
            <td style="text-align: center;"><code>NotObstacle</code></td>
            <td style="text-align: left;">장애물이 존재하지 않아 물리적으로 객체 배치가 가능한 클린한 상태의 그리드만을 검색합니다.</td>
            <td style="text-align: left;">장애물을 피해 안전하게 유닛을 배치하거나, 투사체가 지나갈 수 있는 경로를 계산할 때 필터로 활용됩니다.</td>
        </tr>
        <tr>
            <td style="text-align: center;"><code>None</code></td>
            <td style="text-align: left;">별도의 필터 조건을 적용하지 않고 그리드 레이아웃 내의 모든 셀을 탐색 범위에 포함합니다.</td>
            <td style="text-align: left;">전체 그리드의 초기화, 일괄 색상 변경, 또는 모든 셀에 대한 거리 기반 전수 조사가 필요할 때 사용됩니다.</td>
        </tr>
    </tbody>
</table>
  </div>

  <div style="margin-top: 20px; padding: 20px; background: rgba(88, 166, 255, 0.05); border-radius: 8px; border: 1px solid #e1e4e8; text-align: left;">
    <div style="color: #007bff; font-weight: 700; margin-bottom: 12px; font-family: 'Fira Code', monospace; font-size: 1rem; border-bottom: 1px solid #eee; padding-bottom: 8px;">💡 복합 쿼리 조합 예시 (High-Density Logic)</div>
    <div style="color: #333; font-size: 0.95rem; font-family: 'Fira Code', monospace; line-height: 1.8;">
        <code style="background: #eef5ff; padding: 4px 8px; border-radius: 4px; color: #0056b3; font-weight: bold;">FindFlagGridWorld(pos, NotOccupied, NotObstacle)</code><br />
        <span style="color: #555; display: inline-block; margin-top: 10px;">➔ <strong>"유닛이 없고 + 동시에 장애물도 없는"</strong> 가장 인접한 유효 그리드를 즉시 검색해 줍니다.</span>
    </div>
</div>
</div>

<hr />

<h2 class="chapter-title" id="4-다형성-기반-동적-버프-시스템">4. 다형성 기반 동적 버프 시스템</h2>

<h3 id="41-observer-패턴-기반-버프-자동-전파">4.1 Observer 패턴 기반 버프 자동 전파</h3>

<p>파일 유닛이 그리드 상에 배치되거나 이동할 때, 버프 영역을 동적으로 계산하고 전파하기 위해 <strong>Observer 패턴</strong> 구조를 활용했습니다.</p>

<pre><code class="language-mermaid">graph TD
    A[Place Unit on Grid] --&gt; B{Is Unit a Buff Source?}
    
    B --&gt;|Yes| C[ApplyBuffActive]
    C --&gt;|Register Aura Area| D[Skill_BuffMain.ApplyBuffArea]
    D --&gt;|For each grid in range| E[Grid.AddBuffSource]
    E --&gt;|If grid has occupant| F[ApplyBuffFromSource]
    F --&gt;|Apply BuffStat| G[Target Unit.ApplyBuff]
    
    B --&gt;|No| H[ApplyGridBuffs]
    H --&gt;|For each source in Grid._activeBuffSources| I[ApplyBuffFromSource]
    I --&gt;|Apply BuffStat| J[Placed Unit.ApplyBuff]

    style C fill:#007bff,stroke:#0056b3,color:#fff
    style H fill:#28a745,stroke:#1e7e34,color:#fff
    style G fill:#17a2b8,stroke:#117a8b,color:#fff
    style J fill:#17a2b8,stroke:#117a8b,color:#fff
</code></pre>

<ul>
  <li><strong>버프 전파 흐름:</strong>
    <ol>
      <li>버프 특성을 가진 파일 유닛(예: <code class="language-plaintext highlighter-rouge">.mp3</code> 힐링 버프)이 그리드에 배치되면, 자신의 사거리(Aura Area) 내에 존재하는 모든 주변 <code class="language-plaintext highlighter-rouge">FileGrid</code> 셀들을 찾아 자신을 버프 소스로 등록(<code class="language-plaintext highlighter-rouge">AddBuffSource(this)</code>)합니다.</li>
      <li>이후 새로운 유닛이 해당 버프 영향권 내부의 빈 그리드로 들어올 경우, <a href="file:///D:/Workspace/codeReference/file_tower_defence/FileGrid.cs">FileGrid</a>가 즉각 감지하여 보관하고 있던 버프 데이터 목록(<code class="language-plaintext highlighter-rouge">_activeBuffSources</code>)을 진입한 유닛에게 자동으로 갱신/적용(<code class="language-plaintext highlighter-rouge">ApplyBuffFromSource</code>)합니다.</li>
      <li>버프 유닛이 죽거나 이동하면, 영향권 내의 모든 그리드에서 버프 소스를 제거(<code class="language-plaintext highlighter-rouge">RemoveBuffSource</code>)하고 즉시 피적용 유닛의 스탯을 원복시킵니다.</li>
    </ol>
  </li>
</ul>

<h3 id="42-다형성을-활용한-틱tick-기반-버프-아키텍처">4.2 다형성을 활용한 틱(Tick) 기반 버프 아키텍처</h3>

<p>단순 스탯 증가 버프 외에도 주기적인 리젠(HP 회복), 지속 피해(도트 데미지) 등을 단일 코루틴 남발 없이 처리하기 위해 <code class="language-plaintext highlighter-rouge">MyBuff</code> 추상 클래스와 <code class="language-plaintext highlighter-rouge">BuffData</code> 구조를 결합한 중앙 업데이트 방식을 채택했습니다.</p>

<details class="pf-details">
  <summary>코드 보기: 다형적 버프 실행 루프</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// BuffData_Base.cs: 다형적 버프 업데이트 루프</span>
<span class="k">public</span> <span class="k">virtual</span> <span class="k">void</span> <span class="nf">Tick</span><span class="p">(</span><span class="kt">float</span> <span class="n">deltaTime</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 딕셔너리 순회 중 요소 삭제 에러 방지를 위해 복사본 리스트로 순회</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">int</span> <span class="n">key</span> <span class="k">in</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;(</span><span class="n">activeBuffs</span><span class="p">.</span><span class="n">Keys</span><span class="p">))</span> <span class="p">{</span>
        <span class="c1">// BuffData 내부에서 누적 시간을 체크하여 틱 주기 도달 시 true 반환</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">activeBuffs</span><span class="p">[</span><span class="n">key</span><span class="p">].</span><span class="nf">Tick</span><span class="p">(</span><span class="n">deltaTime</span><span class="p">))</span> <span class="p">{</span>
            <span class="nf">OnTick</span><span class="p">(</span><span class="n">activeBuffs</span><span class="p">[</span><span class="n">key</span><span class="p">]);</span> <span class="c1">// 다형성에 의해 상속받은 자식 클래스의 고유 로직 실행</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Buff_MP3.cs: 구체적인 힐링(HP 지속 회복) 버프 클래스 구현</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">Buff_MP3</span> <span class="p">:</span> <span class="n">MyBuff</span> <span class="p">{</span>
    <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">Init</span><span class="p">(</span><span class="n">Transform</span> <span class="n">_target</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">buffTarget</span> <span class="p">=</span> <span class="n">_target</span><span class="p">;</span>
        <span class="n">buffParticleType</span> <span class="p">=</span> <span class="n">Define</span><span class="p">.</span><span class="n">ParticleType</span><span class="p">.</span><span class="n">Buff_MP3</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">OnTick</span><span class="p">(</span><span class="n">BuffData</span> <span class="n">buffData</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 대상 오브젝트의 IHealth 인터페이스를 찾아 체력 회복</span>
        <span class="kt">var</span> <span class="n">health</span> <span class="p">=</span> <span class="n">buffTarget</span><span class="p">.</span><span class="n">GetComponent</span><span class="p">&lt;</span><span class="n">IHealth</span><span class="p">&gt;();</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">health</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">health</span><span class="p">.</span><span class="nf">Heal</span><span class="p">(</span><span class="n">buffData</span><span class="p">.</span><span class="n">Amount</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="5-전체-시스템-기술적-특징-요약">5. 전체 시스템 기술적 특징 요약</h2>

<ul>
  <li><strong>성능 최적화 (Optimization):</strong> UI 구조에서 물리 공간 오브젝트 구조로 전면 교체하여 <strong>Canvas Rebuilding 부하를 0으로</strong> 격리시켰으며, 마우스 드래그 시 인접 3x3 영역만 거리를 탐색하는 <strong>공간 분할 알고리즘</strong>으로 불필요한 연산을 대폭 감축했습니다.</li>
  <li><strong>지능형 상호작용 (Mediator Pattern):</strong> 개별 유닛의 업데이트 루프에 의존하지 않는 중앙 집중식 <code class="language-plaintext highlighter-rouge">InputManager</code>를 구축하고 렌더링 우선순위 판정을 수식화하여, 깔끔하게 설계된 <code class="language-plaintext highlighter-rouge">IInteractable</code> 객체 이벤트 구조를 실현했습니다.</li>
  <li><strong>데이터 무결성 (Transaction-Safe):</strong> 배치 취소, 배치 위치 이동, 장애물 충돌 등으로 발생 가능한 데이터 오차를 예방하기 위해 사전 예외 검증과 배치 롤백이 보장되는 원자적 배치 단계를 구현했습니다.</li>
  <li><strong>유연한 확장성 (Extensibility):</strong> 비트 필터링 느낌의 플래그 기반 그리드 검색 시스템과 다형성 버프 구조를 구현하여, 향후 새로운 유닛 유형이나 기믹이 추가되더라도 기존 코드를 크게 수정하지 않고 확장할 수 있는 견고한 OOP 기반 아키텍처를 확보했습니다.</li>
</ul>

<p class="notice--success"><strong>기술적 성과:</strong> 윈도우 바탕화면의 파일 정렬 및 압축(Folder ZIP) 등의 조작 감성을 유니티 엔진 상에서 고성능으로 재해석하였으며, 대규모 유닛과 바이러스가 중첩되는 난전 상황 속에서도 <strong>지속적인 60 FPS 이상을 안정적으로 방어</strong>하는 고품질 인디 게임 코어 시스템을 완성했습니다.</p>]]></content><author><name>이민혁 (Minhyuk Lee)</name></author><category term="project" /><category term="Unity" /><category term="Optimization" /><category term="Architecture" /><category term="Algorithms" /><summary type="html"><![CDATA[Systems Engineering: UI-to-GameObject 리팩토링, 통합 입력 시스템 및 그리드 아키텍처]]></summary></entry><entry><title type="html">PROJECT REPORT // SLIME PROJECT</title><link href="https://infisword.github.io/project/slime-project/" rel="alternate" type="text/html" title="PROJECT REPORT // SLIME PROJECT" /><published>2026-05-23T00:00:00+00:00</published><updated>2026-05-23T00:00:00+00:00</updated><id>https://infisword.github.io/project/slime-project</id><content type="html" xml:base="https://infisword.github.io/project/slime-project/"><![CDATA[<style>
    /* --- Premium Report Layout Styles --- */
    
    .chapter-title {
        font-size: 2.2rem;
        color: #fff !important;
        background: linear-gradient(90deg, #007bff 0%, #58A6FF 100%);
        padding: 25px 35px;
        border-radius: 12px;
        margin-top: 60px !important;
        margin-bottom: 40px !important;
        box-shadow: 0 10px 30px rgba(0, 123, 255, 0.15);
        display: flex;
        align-items: center;
        border: none !important;
    }
    .chapter-title::before {
        content: "CHAPTER.";
        font-family: 'Fira Code', monospace;
        font-size: 0.9rem;
        letter-spacing: 2px;
        margin-right: 15px;
        opacity: 0.8;
    }

    .pf-visual-frame {
        width: 100%; padding: 35px; background: #fcfcfc;
        border: 1px solid #e1e4e8; border-radius: 16px;
        margin: 30px 0; text-align: center;
        box-shadow: inset 0 2px 10px rgba(0,0,0,0.02);
    }
    .pf-arch-diagram {
        display: flex; flex-direction: column; gap: 20px; margin: 25px 0;
    }
    .pf-arch-layer {
        padding: 20px 25px; border: 1px solid #e1e4e8; border-radius: 8px;
        background: #fff; position: relative; text-align: center;
        box-shadow: 0 4px 12px rgba(0,0,0,0.03);
    }
    .pf-arch-layer::before {
        content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
        background: #007bff; border-radius: 8px 0 0 8px;
    }
    .pf-arch-layer-title {
        color: #007bff; font-weight: 700; margin-bottom: 10px;
        font-family: 'Fira Code', monospace; font-size: 0.95rem; text-align: center;
    }
    .pf-arch-layer-items {
        display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; justify-content: center;
    }
    .pf-arch-item {
        padding: 6px 14px; background: rgba(0, 123, 255, 0.08);
        border: 1px solid rgba(0, 123, 255, 0.3); border-radius: 6px;
        font-size: 0.85rem; color: #0056b3; text-align: center;
    }

    /* --- Code Details --- */
    details.pf-details {
        margin: 20px 0;
        border: 1px solid #e1e4e8;
        border-radius: 8px;
        background: #f8f9fa;
        overflow: hidden;
    }
    details.pf-details summary {
        padding: 15px 20px;
        font-weight: 700;
        color: #007bff;
        cursor: pointer;
        outline: none;
        background: #fff;
        display: flex;
        align-items: center;
    }

    .highlight-box {
        background: #f0f7ff;
        border-left: 5px solid #007bff;
        padding: 20px;
        margin: 20px 0;
        border-radius: 0 8px 8px 0;
    }

    /* --- Flow Arrows --- */
    .flow-arrow {
        color: #007bff; font-weight: bold; font-size: 1.5rem; margin: 10px 0;
    }

    /* --- Transaction Flow --- */
    .pf-transaction-flow {
        display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-family: 'Fira Code', monospace;
        gap: 12px; flex-wrap: wrap; margin: 20px 0;
    }
    .pf-flow-step { padding: 10px 18px; border: 1px solid #e1e4e8; border-radius: 6px; background: #fff; color: #333; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.05); line-height: 1.4; }
    .pf-flow-arrow { color: #007bff; font-weight: bold; font-size: 1.2rem; }
    
    /* --- Grid Systems --- */
    .pf-grid {
        display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; margin: 25px 0;
    }
    .pf-grid-item {
        background: #fff; border: 1px solid #e1e4e8; border-radius: 12px; padding: 25px; text-align: center;
        transition: 0.3s;
    }
    .pf-grid-item:hover { border-color: #007bff; transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,123,255,0.05); }
    .pf-grid-label { color: #007bff; font-weight: 700; margin-bottom: 12px; font-family: 'Fira Code', monospace; font-size: 1rem; display: block; }
    .pf-grid-desc { color: #555; font-size: 0.85rem; line-height: 1.6; }
</style>

<p>Slime Project는 하드코어 액션 퍼즐 플랫포머 게임으로, <strong>FiniteStateMachine(FSM) 기반 상태 관리 시스템</strong>을 통해 모든 게임 엔티티의 행동을 체계적으로 관리합니다. 플레이어 슬라임의 움직임, 변신 능력, 그리고 변신 시 능력 변화를 구현했으며, 보스 몬스터와 일반 몬스터의 AI 로직을 담당하여 확장 가능한 게임 시스템을 구축했습니다.</p>

<hr />

<h2 class="chapter-title" id="1-프로젝트-개요">1. 프로젝트 개요</h2>

<p>본 프로젝트는 복잡한 게임 로직을 디자인 패턴을 활용해 구조화하는 데 집중했습니다. 모든 캐릭터와 몬스터는 통일된 인터페이스를 공유하며, 기능 중심의 컴포지션 설계를 통해 유지보수성을 극대화했습니다.</p>

<div class="pf-visual-frame">
    <div class="pf-grid">
        <div class="pf-grid-item">
            <span class="pf-grid-label">상태머신 시스템</span>
            <p class="pf-grid-desc">FiniteStateMachine 기반<br />상태 전환 관리 및 라이프사이클</p>
        </div>
        <div class="pf-grid-item">
            <span class="pf-grid-label">플레이어 시스템</span>
            <p class="pf-grid-desc">슬라임 움직임/공격<br />변신 능력 동적 교체 구현</p>
        </div>
        <div class="pf-grid-item">
            <span class="pf-grid-label">보스 AI</span>
            <p class="pf-grid-desc">델리게이트 기반 스킬<br />HP 단계별 패턴 업그레이드</p>
        </div>
        <div class="pf-grid-item">
            <span class="pf-grid-label">일반 몬스터 AI</span>
            <p class="pf-grid-desc">상태머신 기반<br />유형별 행동 패턴 확장</p>
        </div>
    </div>
</div>

<hr />

<h2 class="chapter-title" id="2-finitestatemachine-기반-상태-관리-시스템">2. FiniteStateMachine 기반 상태 관리 시스템</h2>

<p>모든 몬스터와 게임 엔티티의 행동을 <strong>FiniteStateMachine</strong> 패턴으로 관리하는 시스템을 구현했습니다. <code class="language-plaintext highlighter-rouge">State</code> 베이스 클래스를 통해 각 상태의 Enter/Exit/LogicUpdate/PhysicsUpdate 라이프사이클을 체계적으로 관리합니다.</p>

<h3 id="21-상태머신-구조-및-전환-플로우">2.1 상태머신 구조 및 전환 플로우</h3>

<p>FiniteStateMachine은 현재 상태를 관리하고, 상태 전환 시 이전 상태의 Exit와 새 상태의 Enter를 자동으로 호출하여 물리 및 애니메이션 상태의 정합성을 보장합니다.</p>

<div class="pf-visual-frame" style="overflow-x: auto;">
    <div style="display: flex; align-items: center; justify-content: flex-start; min-width: 800px; padding: 20px 0;">
        <!-- Init -->
        <div style="background: rgba(40, 167, 69, 0.05); border: 2px solid #28a745; border-radius: 8px; padding: 12px 20px; text-align: center; flex: 0 0 160px;">
            <div style="color: #28a745; font-weight: 700; font-size: 0.85rem; font-family: 'Fira Code';">State::Enter</div>
            <div style="color: #888; font-size: 0.7rem; margin-top: 4px;">상태 초기화</div>
        </div>
        
        <div style="color: #007bff; font-weight: bold; margin: 0 15px; font-size: 1.2rem;">→</div>
        
        <!-- Update Loop -->
        <div style="background: rgba(0, 123, 255, 0.05); border: 2px solid #007bff; border-radius: 8px; padding: 12px; text-align: center; flex: 0 0 240px; position: relative;">
            <div style="color: #007bff; font-weight: 700; font-size: 0.85rem; font-family: 'Fira Code'; margin-bottom: 8px;">Execution Loop</div>
            <div style="display: flex; gap: 8px;">
                <div style="background: #fff; border: 1px solid #e1e4e8; padding: 6px; border-radius: 4px; font-size: 0.7rem; flex: 1;">
                    <strong>LogicUpdate</strong>
                </div>
                <div style="background: #fff; border: 1px solid #e1e4e8; padding: 6px; border-radius: 4px; font-size: 0.7rem; flex: 1;">
                    <strong>PhysicsUpdate</strong>
                </div>
            </div>
            <!-- Loop Arrow -->
            <div style="position: absolute; top: -15px; left: 50%; transform: translateX(-50%); width: 40px; height: 10px; border: 2px solid #007bff; border-bottom: none; border-radius: 10px 10px 0 0;"></div>
        </div>
        
        <div style="color: #007bff; font-weight: bold; margin: 0 15px; font-size: 1.2rem;">→</div>
        
        <!-- Decision -->
        <div style="background: rgba(255, 123, 114, 0.05); border: 2px solid #ff7b72; border-radius: 50%; width: 120px; height: 120px; display: flex; align-items: center; justify-content: center; text-align: center; flex: 0 0 120px; font-size: 0.75rem; font-weight: 700; color: #ff7b72; font-family: 'Fira Code';">
            Trigger<br />Transition?
        </div>
        
        <div style="color: #007bff; font-weight: bold; margin: 0 15px; font-size: 1.2rem; position: relative;">
            <span style="position: absolute; top: -20px; left: 0; font-size: 0.7rem; color: #007bff;">[YES]</span>
            →
        </div>
        
        <!-- Exit & Transition -->
        <div style="display: flex; flex-direction: column; gap: 10px;">
            <div style="background: rgba(0, 123, 255, 0.05); border: 1px solid #007bff; border-radius: 6px; padding: 8px 15px; text-align: center; font-size: 0.75rem; color: #333; font-family: 'Fira Code';">
                State::Exit
            </div>
            <div style="background: #007bff; color: #fff; border-radius: 6px; padding: 8px 15px; text-align: center; font-size: 0.75rem; font-weight: 700; font-family: 'Fira Code';">
                NewState::Enter
            </div>
        </div>
    </div>
</div>

<p><strong>상태머신 핵심 로직</strong>
상태머신 초기화 및 안전한 상태 전환을 보장하는 핵심 로직입니다.</p>

<details class="pf-details">
  <summary>코드 보기: FiniteStateMachine.cs</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// FiniteStateMachine.cs: 상태머신 핵심 로직</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">FiniteStateMachine</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">State</span> <span class="n">currentState</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> 

    <span class="c1">// 상태머신 초기화: 시작 상태 설정 및 Enter() 호출</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">Init</span><span class="p">(</span><span class="n">State</span> <span class="n">startingState</span><span class="p">)</span> 
    <span class="p">{</span>
        <span class="n">currentState</span> <span class="p">=</span> <span class="n">startingState</span><span class="p">;</span>     <span class="c1">// 현재 상태 설정</span>
        <span class="n">currentState</span><span class="p">.</span><span class="nf">Enter</span><span class="p">();</span>             <span class="c1">// 시작 상태 진입</span>
    <span class="p">}</span>

    <span class="c1">// 상태 전환: 이전 상태 종료 후 새 상태 진입</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">ChangeState</span><span class="p">(</span><span class="n">State</span> <span class="n">newState</span><span class="p">)</span> 
    <span class="p">{</span>
        <span class="n">currentState</span><span class="p">.</span><span class="nf">Exit</span><span class="p">();</span>                <span class="c1">// 이전 상태 종료</span>
        <span class="n">currentState</span> <span class="p">=</span> <span class="n">newState</span><span class="p">;</span>            <span class="c1">// 새 상태로 전환</span>
        <span class="n">currentState</span><span class="p">.</span><span class="nf">Enter</span><span class="p">();</span>               <span class="c1">// 새 상태 진입</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<h3 id="22-state-베이스-클래스">2.2 State 베이스 클래스</h3>

<p>모든 상태는 <code class="language-plaintext highlighter-rouge">State</code> 베이스 클래스를 상속받아 구현됩니다. 각 상태는 애니메이션 파라미터 제어와 상태 시작 시간 기록 등을 자동으로 처리합니다.</p>

<details class="pf-details">
  <summary>코드 보기: State.cs</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// State.cs: 모든 상태의 베이스 클래스</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">State</span>
<span class="p">{</span>
    <span class="k">protected</span> <span class="n">FiniteStateMachine</span> <span class="n">stateMachine</span><span class="p">;</span>  <span class="c1">// 상태머신 참조</span>
    <span class="k">protected</span> <span class="n">Entity</span> <span class="n">entity</span><span class="p">;</span>                    <span class="c1">// 엔티티 참조</span>
    <span class="k">protected</span> <span class="kt">float</span> <span class="n">startTime</span><span class="p">;</span>                  <span class="c1">// 상태 시작 시간</span>
    <span class="k">protected</span> <span class="kt">string</span> <span class="n">animBoolName</span><span class="p">;</span>              <span class="c1">// 애니메이션 파라미터 이름</span>

    <span class="k">public</span> <span class="k">virtual</span> <span class="k">void</span> <span class="nf">Enter</span><span class="p">()</span>                     <span class="c1">// 상태 진입 시 호출</span>
    <span class="p">{</span>
        <span class="n">startTime</span> <span class="p">=</span> <span class="n">Time</span><span class="p">.</span><span class="n">time</span><span class="p">;</span>                      <span class="c1">// 시작 시간 기록</span>
        <span class="n">entity</span><span class="p">.</span><span class="n">anim</span><span class="p">.</span><span class="nf">SetBool</span><span class="p">(</span><span class="n">animBoolName</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span>    <span class="c1">// 애니메이션 트리거</span>
        <span class="nf">DoChecks</span><span class="p">();</span>                                 <span class="c1">// 상태 전환 조건 확인</span>
    <span class="p">}</span>
    
    <span class="k">public</span> <span class="k">virtual</span> <span class="k">void</span> <span class="nf">Exit</span><span class="p">()</span>                     <span class="c1">// 상태 종료 시 호출</span>
    <span class="p">{</span>
        <span class="n">entity</span><span class="p">.</span><span class="n">anim</span><span class="p">.</span><span class="nf">SetBool</span><span class="p">(</span><span class="n">animBoolName</span><span class="p">,</span> <span class="k">false</span><span class="p">);</span>   <span class="c1">// 애니메이션 종료</span>
    <span class="p">}</span>
    
    <span class="k">public</span> <span class="k">virtual</span> <span class="k">void</span> <span class="nf">LogicUpdate</span><span class="p">()</span>         <span class="c1">// Update()에서 호출</span>
    <span class="p">{</span>
    <span class="p">}</span>
    
    <span class="k">public</span> <span class="k">virtual</span> <span class="k">void</span> <span class="nf">PhysicsUpdate</span><span class="p">()</span>      <span class="c1">// FixedUpdate()에서 호출</span>
    <span class="p">{</span>
        <span class="nf">DoChecks</span><span class="p">();</span>                                 <span class="c1">// 물리 업데이트마다 조건 확인</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">virtual</span> <span class="k">void</span> <span class="nf">DoChecks</span><span class="p">()</span>          <span class="c1">// 상태 전환 조건 확인</span>
    <span class="p">{</span>
        <span class="c1">// 플레이어 감지, 벽/절벽 감지 등</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="3-플레이어-슬라임-및-변신-시스템">3. 플레이어 슬라임 및 변신 시스템</h2>

<p>플레이어 슬라임의 움직임, 공격, 대시, 피격 처리를 구현했습니다. <strong>컴포지션 패턴</strong>을 활용하여 각 기능을 독립적인 클래스로 분리하고, <code class="language-plaintext highlighter-rouge">Player_Slime</code>에서 통합 관리합니다.</p>

<h3 id="31-시스템-구조-composition-pattern">3.1 시스템 구조 (Composition Pattern)</h3>

<p><code class="language-plaintext highlighter-rouge">Player_Slime</code>은 모든 컴포넌트의 허브 역할을 수행하며, 각 기능 모듈은 자신의 도메인 로직에 집중합니다.</p>

<div class="pf-visual-frame">
    <div class="pf-arch-diagram">
        <div class="pf-arch-layer">
            <div class="pf-arch-layer-title">Player_Slime (Main Controller)</div>
            <div class="pf-arch-layer-items">
                <span class="pf-arch-item">Player_Entity Inherited</span>
                <span class="pf-arch-item">Component Hub</span>
            </div>
        </div>
        <div class="pf-flow-arrow" style="text-align: center;">↓ Dispatch</div>
        <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px;">
            <div class="pf-arch-layer">
                <div class="pf-arch-layer-title">SlimeMovement</div>
                <div style="font-size: 0.8rem; color: #666;">이동, 점프, 입력 관리</div>
            </div>
            <div class="pf-arch-layer">
                <div class="pf-arch-layer-title">SlimeAttack</div>
                <div style="font-size: 0.8rem; color: #666;">근접 공격, 데미지 트리거</div>
            </div>
            <div class="pf-arch-layer">
                <div class="pf-arch-layer-title">SlimeDash</div>
                <div style="font-size: 0.8rem; color: #666;">대시 이동, 무적 프레임</div>
            </div>
            <div class="pf-arch-layer">
                <div class="pf-arch-layer-title">SlimeDead</div>
                <div style="font-size: 0.8rem; color: #666;">사망 처리, 게임오버 연동</div>
            </div>
        </div>
    </div>
</div>

<h3 id="32-변신-능력-시스템">3.2 변신 능력 시스템</h3>

<p>플레이어가 몬스터를 처치하면 해당 몬스터의 형태로 변신할 수 있습니다. 변신 시 각 형태의 고유 능력을 사용할 수 있으며, <code class="language-plaintext highlighter-rouge">Player_FormMovement</code>를 통해 특수 이동 로직이 활성화됩니다.</p>

<ul>
  <li><strong>슬라임 (기본):</strong> 원거리 포물선 공격, 대시 무적 프레임 활용.</li>
  <li><strong>토끼 (변신):</strong> 더블 점프 및 벽 점프 능력 획득. <code class="language-plaintext highlighter-rouge">Rabbit_ability</code>와 연동하여 벽 점프 중 조작 제한 물리 로직 적용.</li>
</ul>

<details class="pf-details">
  <summary>코드 보기: Player_Slime.cs - 초기화 로직</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Player_Slime.cs: 플레이어 슬라임 메인 클래스</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">Player_Slime</span> <span class="p">:</span> <span class="n">Player_Entity</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">Player_SlimeMovement</span> <span class="n">movement</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="n">Player_Attack</span> <span class="n">attack</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="n">Player_Dash</span> <span class="n">dash</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">Start</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">base</span><span class="p">.</span><span class="nf">Start</span><span class="p">();</span>
        <span class="c1">// 각 기능 컴포넌트 초기화 (Composition)</span>
        <span class="n">movement</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Player_SlimeMovement</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">movementData</span><span class="p">,</span> <span class="k">this</span><span class="p">);</span>
        <span class="n">attack</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Player_Attack</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">attackData</span><span class="p">,</span> <span class="n">meleePos</span><span class="p">);</span>
        <span class="n">dash</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Player_Dash</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">dashData</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="4-보스-및-일반-몬스터-ai-시스템">4. 보스 및 일반 몬스터 AI 시스템</h2>

<p>보스 몬스터의 스킬 시스템을 <strong>델리게이트 기반</strong>으로 구현하여 확장 가능한 AI를 설계했습니다. HP 단계에 따라 스킬이 업그레이드되며, 거리와 상황에 따라 지능적인 패턴을 생성합니다.</p>

<h3 id="41-보스-ai-의사결정-및-스킬-시스템">4.1 보스 AI 의사결정 및 스킬 시스템</h3>

<p>보스는 매 업데이트마다 플레이어와의 거리를 산출하고, 델리게이트 배열에 등록된 스킬 풀(Pool)에서 최적의 패턴을 선택하여 실행합니다.</p>

<div class="pf-visual-frame">
    <div class="pf-transaction-flow">
        <div class="pf-flow-step"><strong>1. Distance Calc</strong><br />플레이어 거리 및 방향 분석</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>2. Phase Check</strong><br />HP 50% 이하 스킬 업그레이드</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>3. Skill Select</strong><br />델리게이트 배열 랜덤 인덱싱</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>4. Action</strong><br />Invoke Skill Action</div>
    </div>
</div>

<details class="pf-details">
  <summary>코드 보기: MainBossBear.cs - 델리게이트 기반 스킬 관리</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MainBossBear.cs: 보스 스킬 시스템</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">MainBossBear</span> <span class="p">:</span> <span class="n">BossBearSetting</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">delegate</span> <span class="k">void</span> <span class="nf">BossBear_SkillAction</span><span class="p">();</span>
    <span class="k">private</span> <span class="n">BossBear_SkillAction</span><span class="p">[]</span> <span class="n">BossBear_Skills</span><span class="p">;</span>

    <span class="k">public</span> <span class="kt">bool</span> <span class="n">IsSkillUpgrade</span><span class="p">;</span> <span class="c1">// HP 단계 플래그</span>

    <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">Start</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">base</span><span class="p">.</span><span class="nf">Start</span><span class="p">();</span>
        <span class="c1">// 델리게이트 배열에 메서드 직접 할당</span>
        <span class="n">BossBear_Skills</span> <span class="p">=</span> <span class="k">new</span> <span class="n">BossBear_SkillAction</span><span class="p">[</span><span class="m">4</span><span class="p">]</span> <span class="p">{</span> <span class="n">Rest</span><span class="p">,</span> <span class="n">Smash</span><span class="p">,</span> <span class="n">Dash</span><span class="p">,</span> <span class="n">Rumbling</span> <span class="p">};</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">void</span> <span class="nf">RandomBossBearSkillState</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">int</span> <span class="n">RandomSkill_Index</span> <span class="p">=</span> <span class="n">Random</span><span class="p">.</span><span class="nf">Range</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">BossBear_SkillEnum</span><span class="p">.</span><span class="n">Smash</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">BossBear_SkillEnum</span><span class="p">.</span><span class="n">Skill_Max</span><span class="p">);</span>
        <span class="c1">// 선택된 스킬 메서드 대리 호출</span>
        <span class="n">BossBear_Skills</span><span class="p">[</span><span class="n">RandomSkill_Index</span><span class="p">].</span><span class="nf">Invoke</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<h3 id="42-일반-몬스터-ai-확장-사례">4.2 일반 몬스터 AI 확장 사례</h3>

<p>일반 몬스터들은 공통의 <code class="language-plaintext highlighter-rouge">Enemy_State</code>를 상속받아 고유한 행동 패턴을 가집니다.</p>

<ul>
  <li><strong>Bear (돌진형):</strong> <code class="language-plaintext highlighter-rouge">DashState</code>를 통한 강력한 물리 충격 기반 공격.</li>
  <li><strong>Spider (매복형):</strong> <code class="language-plaintext highlighter-rouge">JumpState</code>를 활용한 공중 도약 및 플레이어 타겟팅 낙하 공격.</li>
  <li><strong>Ant (기본형):</strong> 정형화된 순찰 및 근접 공격 루틴 수행.</li>
</ul>

<hr />

<h2 class="chapter-title" id="5-기술적-성과-요약">5. 기술적 성과 요약</h2>

<ul>
  <li><strong>FSM 아키텍처:</strong> Enter/Exit 라이프사이클 관리를 통해 복잡한 상태 전이 시 발생할 수 있는 버그를 근본적으로 차단.</li>
  <li><strong>컴포지션 기반 확장성:</strong> 플레이어 기능을 모듈화하여 새로운 변신 형태나 능력을 추가할 때 기존 코드 수정을 최소화.</li>
  <li><strong>델리게이트 기반 AI:</strong> 보스의 스킬 패턴을 인덱스 기반으로 관리하여 패턴 추가 및 확률 조정의 유연성 확보.</li>
  <li><strong>물리/애니메이션 동기화:</strong> LogicUpdate와 PhysicsUpdate를 분리하여 프레임 드랍 환경에서도 일관된 충돌 판정 및 움직임 보장.</li>
</ul>

<p class="notice--success"><strong>Conclusion:</strong> Slime Project는 디자인 패턴을 실무에 어떻게 적용하여 대규모 게임 로직을 관리할 수 있는지에 대한 깊은 고민이 담긴 프로젝트입니다. FSM과 컴포지션을 통해 복잡한 액션 퍼즐의 메커니즘을 견고하게 구현해냈습니다.</p>]]></content><author><name>이민혁 (Minhyuk Lee)</name></author><category term="project" /><category term="Unity" /><category term="AI" /><category term="FSM" /><category term="Design Pattern" /><summary type="html"><![CDATA[상태머신 기반 게임 로직 및 확장 가능한 몬스터 AI 시스템]]></summary></entry><entry><title type="html">PROJECT REPORT // WINAPI_DONT_STARVE</title><link href="https://infisword.github.io/project/winapi-dont-starve/" rel="alternate" type="text/html" title="PROJECT REPORT // WINAPI_DONT_STARVE" /><published>2026-05-23T00:00:00+00:00</published><updated>2026-05-23T00:00:00+00:00</updated><id>https://infisword.github.io/project/winapi-dont-starve</id><content type="html" xml:base="https://infisword.github.io/project/winapi-dont-starve/"><![CDATA[<style>
    /* --- Premium Report Layout Styles --- */
    
    .chapter-title {
        font-size: 2.2rem;
        color: #fff !important;
        background: linear-gradient(90deg, #007bff 0%, #58A6FF 100%);
        padding: 25px 35px;
        border-radius: 12px;
        margin-top: 60px !important;
        margin-bottom: 40px !important;
        box-shadow: 0 10px 30px rgba(0, 123, 255, 0.15);
        display: flex;
        align-items: center;
        border: none !important;
    }
    .chapter-title::before {
        content: "CHAPTER.";
        font-family: 'Fira Code', monospace;
        font-size: 0.9rem;
        letter-spacing: 2px;
        margin-right: 15px;
        opacity: 0.8;
    }

    .pf-visual-frame {
        width: 100%; padding: 35px; background: #fcfcfc;
        border: 1px solid #e1e4e8; border-radius: 16px;
        margin: 30px 0; text-align: center;
        box-shadow: inset 0 2px 10px rgba(0,0,0,0.02);
    }
    .pf-arch-diagram {
        display: flex; flex-direction: column; gap: 20px; margin: 25px 0;
    }
    .pf-arch-layer {
        padding: 20px 25px; border: 1px solid #e1e4e8; border-radius: 8px;
        background: #fff; position: relative; text-align: center;
        box-shadow: 0 4px 12px rgba(0,0,0,0.03);
    }
    .pf-arch-layer::before {
        content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
        background: #007bff; border-radius: 8px 0 0 8px;
    }
    .pf-arch-layer-title {
        color: #007bff; font-weight: 700; margin-bottom: 10px;
        font-family: 'Fira Code', monospace; font-size: 0.95rem; text-align: center;
    }
    .pf-arch-layer-items {
        display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; justify-content: center;
    }
    .pf-arch-item {
        padding: 6px 14px; background: rgba(0, 123, 255, 0.08);
        border: 1px solid rgba(0, 123, 255, 0.3); border-radius: 6px;
        font-size: 0.85rem; color: #0056b3; text-align: center;
    }

    /* --- Code Details --- */
    details.pf-details {
        margin: 20px 0;
        border: 1px solid #e1e4e8;
        border-radius: 8px;
        background: #f8f9fa;
        overflow: hidden;
    }
    details.pf-details summary {
        padding: 15px 20px;
        font-weight: 700;
        color: #007bff;
        cursor: pointer;
        outline: none;
        background: #fff;
        display: flex;
        align-items: center;
    }

    .highlight-box {
        background: #f0f7ff;
        border-left: 5px solid #007bff;
        padding: 20px;
        margin: 20px 0;
        border-radius: 0 8px 8px 0;
    }

    /* --- Transaction Flow --- */
    .pf-transaction-flow {
        display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-family: 'Fira Code', monospace;
        gap: 12px; flex-wrap: wrap; margin: 20px 0;
    }
    .pf-flow-step { padding: 10px 18px; border: 1px solid #e1e4e8; border-radius: 6px; background: #fff; color: #333; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.05); line-height: 1.4; }
    .pf-flow-arrow { color: #007bff; font-weight: bold; font-size: 1.2rem; }

    /* --- Flow Arrows --- */
    .flow-arrow {
        color: #007bff; font-weight: bold; font-size: 1.5rem; margin: 10px 0;
    }
</style>

<p>게임 엔진에 의존하지 않고 C++와 WinAPI만을 이용해 완전 밑바닥부터 게임을 구축하는 데 도전한 프로젝트입니다. 게임 엔진의 추상화된 기능 뒤에 숨겨진 전체적인 실행 파이프라인을 깊이 있게 이해하고 스스로 설계해보고자 시작하였으며, 기존 게임인 ‘Don’t Starve’의 핵심 시스템을 참고하여 제작했습니다.</p>

<hr />

<h2 class="chapter-title" id="1-주요-시스템-아키텍처">1. 주요 시스템 아키텍처</h2>

<h3 id="11-컴포넌트-기반-객체-설계-component-based">1.1 컴포넌트 기반 객체 설계 (Component-Based)</h3>

<p>단일 상속의 복잡성을 피하고 객체 지향적인 유연성을 극대화하기 위해 <strong>컴포넌트 기반 구조</strong>를 채택했습니다. 모든 게임 객체(<code class="language-plaintext highlighter-rouge">GameObject</code>)는 기능 단위의 컴포넌트를 소유하며, 실행 시점에 필요한 기능을 동적으로 탈부착할 수 있습니다.</p>

<div class="pf-visual-frame">
    <div class="pf-arch-diagram">
        <div class="pf-arch-layer">
            <div class="pf-arch-layer-title">GameObject (Entity Container)</div>
            <div class="pf-arch-layer-items">
                <span class="pf-arch-item">ID / Type Management</span>
                <span class="pf-arch-item">std::vector&lt;Component*&gt;</span>
            </div>
        </div>
        <div class="flow-arrow" style="text-align: center;">↓ Has Components</div>
        <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px;">
            <div class="pf-arch-layer">
                <div class="pf-arch-layer-title">Transform</div>
                <div style="font-size: 0.8rem; color: #666;">Position, Scale, Direction</div>
            </div>
            <div class="pf-arch-layer">
                <div class="pf-arch-layer-title">SpriteRenderer</div>
                <div style="font-size: 0.8rem; color: #666;">GDI+ Sprite, Layer Info</div>
            </div>
            <div class="pf-arch-layer">
                <div class="pf-arch-layer-title">Collider</div>
                <div style="font-size: 0.8rem; color: #666;">AABB Interaction Logic</div>
            </div>
            <div class="pf-arch-layer">
                <div class="pf-arch-layer-title">Animator</div>
                <div style="font-size: 0.8rem; color: #666;">Clip Control, Frame Events</div>
            </div>
        </div>
    </div>
</div>

<p>이러한 구조를 통해 새로운 객체를 생성할 때 상속 계층에 얽매이지 않고 유연하게 기능을 조립할 수 있습니다.</p>

<details class="pf-details">
  <summary>코드 보기: GameObject.h - 컴포넌트 추가 및 획득 핵심 로직</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// GameObject.h - 컴포넌트 추가 및 획득 핵심 로직</span>
<span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">,</span> <span class="k">typename</span><span class="o">...</span> <span class="nc">Args</span><span class="p">&gt;</span>
<span class="n">T</span><span class="o">*</span> <span class="nf">AddComponent</span><span class="p">(</span><span class="n">Args</span><span class="o">&amp;&amp;</span><span class="p">...</span> <span class="n">args</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">auto</span> <span class="n">newComponent</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">make_unique</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">forward</span><span class="o">&lt;</span><span class="n">Args</span><span class="o">&gt;</span><span class="p">(</span><span class="n">args</span><span class="p">)...);</span>
    <span class="n">T</span><span class="o">*</span> <span class="n">componentPtr</span> <span class="o">=</span> <span class="n">newComponent</span><span class="p">.</span><span class="n">get</span><span class="p">();</span>
    <span class="n">m_components</span><span class="p">.</span><span class="n">push_back</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">move</span><span class="p">(</span><span class="n">newComponent</span><span class="p">));</span>
    <span class="k">return</span> <span class="n">componentPtr</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">template</span> <span class="o">&lt;</span><span class="k">typename</span> <span class="nc">T</span><span class="p">&gt;</span>
<span class="n">T</span><span class="o">*</span> <span class="n">GetComponent</span><span class="p">()</span> <span class="k">const</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">component</span> <span class="o">:</span> <span class="n">m_components</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">component</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>
        <span class="n">T</span><span class="o">*</span> <span class="n">target</span> <span class="o">=</span> <span class="k">dynamic_cast</span><span class="o">&lt;</span><span class="n">T</span><span class="o">*&gt;</span><span class="p">(</span><span class="n">component</span><span class="p">.</span><span class="n">get</span><span class="p">());</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">target</span><span class="p">)</span> <span class="k">return</span> <span class="n">target</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nb">nullptr</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<h3 id="12-중앙-집중식-매니저-패턴-singleton-managers">1.2 중앙 집중식 매니저 패턴 (Singleton Managers)</h3>

<p>시스템의 전역적인 상태 관리와 자원 공유를 위해 <strong>싱글톤 기반 매니저</strong>들을 구축했습니다.</p>

<div class="pf-visual-frame">
<div class="pf-arch-diagram">
<div class="pf-arch-layer">
<div class="pf-arch-layer-title">ObjectManager</div>
<div class="pf-arch-layer-items">
<span class="pf-arch-item">객체 생명주기 관리</span>
<span class="pf-arch-item">공간 분할 그리드 업데이트</span>
</div>
</div>
<div class="pf-arch-layer">
<div class="pf-arch-layer-title">RenderManager</div>
<div class="pf-arch-layer-items">
<span class="pf-arch-item">커맨드 예약</span>
<span class="pf-arch-item">레이어별 Y-Sorting 렌더링</span>
</div>
</div>
<div class="pf-arch-layer">
<div class="pf-arch-layer-title">ResourceManager</div>
<div class="pf-arch-layer-items">
<span class="pf-arch-item">비트맵/스프라이트 캐싱</span>
<span class="pf-arch-item">중복 로드 방지</span>
</div>
</div>
<div class="pf-arch-layer">
<div class="pf-arch-layer-title">CameraManager</div>
<div class="pf-arch-layer-items">
<span class="pf-arch-item">가시 영역 컬링</span>
<span class="pf-arch-item">좌표계 변환 (World ↔ Screen)</span>
</div>
</div>
</div>
</div>

<hr />

<h2 class="chapter-title" id="2-렌더링-시스템-및-파이프라인">2. 렌더링 시스템 및 파이프라인</h2>

<p>GDI+ 환경에서 대규모 오브젝트를 효율적으로 출력하기 위해 <strong>커맨드 패턴 기반의 렌더링 파이프라인</strong>을 구축했습니다. 매 프레임 모든 객체를 즉시 그리는 대신, 렌더링 명령을 레이어별로 수집하고 정렬한 뒤 일괄 실행(Flush)하는 방식을 취합니다.</p>

<h3 id="21-렌더링-파이프라인-흐름">2.1 렌더링 파이프라인 흐름</h3>

<p>렌더링 프로세스는 크게 <strong>제출(Submission) -&gt; 정렬(Sorting) -&gt; 실행(Execution)</strong>의 3단계로 구성됩니다.</p>

<div class="pf-visual-frame">
    <div class="pf-transaction-flow">
        <div class="pf-flow-step"><strong>1. Submission</strong><br />GameObject::Render()<br />DrawCommand 생성</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>2. Sorting</strong><br />RenderManager::Flush()<br />Layer &amp; Y-Sorting</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>3. Execution</strong><br />ExecuteDrawCommand()<br />GDI+ DrawImage 호출</div>
    </div>
</div>

<p>Top-down 뷰의 입체감을 위해 객체의 발밑(Pivot) 위치의 Y 좌표를 기준으로 정렬하여 출력함으로써 오브젝트 간의 앞뒤 관계를 정확히 표현합니다.</p>

<details class="pf-details">
  <summary>코드 보기: RenderManager.cpp - Y-Sorting 및 일괄 렌더링 처리</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// RenderManager.cpp - Y-Sorting 및 일괄 렌더링 처리</span>
<span class="kt">void</span> <span class="n">RenderManager</span><span class="o">::</span><span class="n">Flush</span><span class="p">(</span><span class="n">Gdiplus</span><span class="o">::</span><span class="n">Graphics</span><span class="o">*</span> <span class="n">pGraphics</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">pGraphics</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">LAYER_TILE_BACKGROUND</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">LAYER_COUNT</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">m_layerCommands</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">empty</span><span class="p">())</span> <span class="k">continue</span><span class="p">;</span>

        <span class="c1">// zOrder(Y좌표)를 기준으로 오름차순 정렬하여 뒤에 있는 객체가 먼저 그려지도록 함</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">m_layerCommands</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">size</span><span class="p">()</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">std</span><span class="o">::</span><span class="n">stable_sort</span><span class="p">(</span><span class="n">m_layerCommands</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">begin</span><span class="p">(),</span> <span class="n">m_layerCommands</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">end</span><span class="p">(),</span> <span class="p">[](</span><span class="k">const</span> <span class="n">DrawCommand</span><span class="o">&amp;</span> <span class="n">a</span><span class="p">,</span> <span class="k">const</span> <span class="n">DrawCommand</span><span class="o">&amp;</span> <span class="n">b</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">zOrder</span> <span class="o">!=</span> <span class="n">b</span><span class="p">.</span><span class="n">zOrder</span><span class="p">)</span> <span class="k">return</span> <span class="n">a</span><span class="p">.</span><span class="n">zOrder</span> <span class="o">&lt;</span> <span class="n">b</span><span class="p">.</span><span class="n">zOrder</span><span class="p">;</span>
                <span class="k">return</span> <span class="n">a</span><span class="p">.</span><span class="n">layer</span> <span class="o">&lt;</span> <span class="n">b</span><span class="p">.</span><span class="n">layer</span><span class="p">;</span>
            <span class="p">});</span>
        <span class="p">}</span>

        <span class="k">for</span> <span class="p">(</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">cmd</span> <span class="o">:</span> <span class="n">m_layerCommands</span><span class="p">[</span><span class="n">i</span><span class="p">])</span> <span class="p">{</span>
            <span class="n">ExecuteDrawCommand</span><span class="p">(</span><span class="n">pGraphics</span><span class="p">,</span> <span class="n">cmd</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="n">m_layerCommands</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">clear</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<h3 id="22-동적-타일-캐싱-및-가시-영역-렌더링">2.2 동적 타일 캐싱 및 가시 영역 렌더링</h3>

<p>광활한 맵 전체를 그리는 부하를 막기 위해, 카메라의 뷰포트에 해당하는 타일만 선택적으로 렌더링합니다. 특히 타일 비트맵을 매번 로드하지 않고 <strong>동적 캐싱 시스템</strong>을 통해 메모리 효율을 극대화했습니다.</p>

<details class="pf-details">
  <summary>코드 보기: CameraManager.cpp - 타일 맵 최적화 렌더링</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CameraManager.cpp - 그리드 기반 타일 컬링 및 캐싱</span>
<span class="kt">void</span> <span class="n">CameraManager</span><span class="o">::</span><span class="n">RenderVisibleTiles</span><span class="p">(</span><span class="k">const</span> <span class="n">MapData</span><span class="o">*</span> <span class="n">mapData</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">mapData</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
    <span class="n">Gdiplus</span><span class="o">::</span><span class="n">RectF</span> <span class="n">vp</span> <span class="o">=</span> <span class="n">GetViewportWorldRect</span><span class="p">();</span>

    <span class="c1">// 256px 그리드 단위로 가시 범위 인덱스 계산</span>
    <span class="k">const</span> <span class="kt">int</span> <span class="n">gridCellSize</span> <span class="o">=</span> <span class="mi">256</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">sx</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">floor</span><span class="p">(</span><span class="n">vp</span><span class="p">.</span><span class="n">X</span> <span class="o">/</span> <span class="n">gridCellSize</span><span class="p">))</span> <span class="o">*</span> <span class="p">(</span><span class="n">gridCellSize</span> <span class="o">/</span> <span class="n">TILE_SIZE</span><span class="p">);</span>
    <span class="kt">int</span> <span class="n">ex</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">min</span><span class="p">(</span><span class="n">MAP_WIDTH</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">ceil</span><span class="p">((</span><span class="n">vp</span><span class="p">.</span><span class="n">X</span> <span class="o">+</span> <span class="n">vp</span><span class="p">.</span><span class="n">Width</span><span class="p">)</span> <span class="o">/</span> <span class="n">gridCellSize</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">gridCellSize</span> <span class="o">/</span> <span class="n">TILE_SIZE</span><span class="p">));</span>
    <span class="c1">// ... (Y 인덱스 생략)</span>

    <span class="c1">// 가시 영역 변경 시에만 미사용 캐시 정리</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">sx</span> <span class="o">!=</span> <span class="n">m_lastStartTileX</span> <span class="o">||</span> <span class="n">ex</span> <span class="o">!=</span> <span class="n">m_lastEndTileX</span> <span class="o">||</span> <span class="p">...)</span> <span class="p">{</span>
        <span class="n">CleanupUnusedTileCache</span><span class="p">(</span><span class="n">mapData</span><span class="p">,</span> <span class="n">sx</span><span class="p">,</span> <span class="n">ex</span><span class="p">,</span> <span class="n">sy</span><span class="p">,</span> <span class="n">ey</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="n">sy</span><span class="p">;</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="n">ey</span><span class="p">;</span> <span class="o">++</span><span class="n">y</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="n">sx</span><span class="p">;</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="n">ex</span><span class="p">;</span> <span class="o">++</span><span class="n">x</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">auto</span><span class="o">&amp;</span> <span class="n">tileData</span> <span class="o">=</span> <span class="n">mapData</span><span class="o">-&gt;</span><span class="n">tiles</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">];</span>
            <span class="c1">// 캐시 확인 및 RenderManager에 명령 제출</span>
            <span class="n">Gdiplus</span><span class="o">::</span><span class="n">Bitmap</span><span class="o">*</span> <span class="n">bm</span> <span class="o">=</span> <span class="n">GetOrLoadTileBitmap</span><span class="p">(</span><span class="n">tileData</span><span class="p">.</span><span class="n">id</span><span class="p">);</span>
            <span class="n">RenderManager</span><span class="o">::</span><span class="n">GetInstance</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">AddWorldObjectCommand</span><span class="p">(</span><span class="n">bm</span><span class="p">,</span> <span class="p">...);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="3-최적화-및-핵심-기술">3. 최적화 및 핵심 기술</h2>

<h3 id="31-공간-분할-기법-그리드-기반-컬링-grid-based-culling">3.1 공간 분할 기법: 그리드 기반 컬링 (Grid-based Culling)</h3>

<p>월드에 수천 개의 오브젝트가 존재할 때, 모든 객체에 대해 매 프레임 업데이트와 충돌 검사(Brute-force, <code class="language-plaintext highlighter-rouge">O(N)</code>)를 수행하면 심각한 성능 저하가 발생합니다. 이를 방지하기 위해 맵 전체를 256x256 픽셀 크기의 <strong>정적 2D 그리드 배열(Spatial Partitioning)</strong>로 분할했습니다.</p>

<div class="pf-visual-frame">
    <div class="pf-arch-diagram">
        <div class="pf-arch-layer">
            <div class="pf-arch-layer-title">1. Viewport Index Calculation</div>
            <div style="font-size: 0.8rem; color: #666;">카메라 영역을 기반으로 활성 그리드 범위(StartX~EndY) 즉시 산출 (O(1))</div>
        </div>
        <div class="flow-arrow" style="text-align: center;">↓</div>
        <div class="pf-arch-layer">
            <div class="pf-arch-layer-title">2. Query Stamp &amp; Filtering</div>
            <div style="font-size: 0.8rem; color: #666;">다중 셀에 걸친 객체의 중복 처리를 막기 위한 고유 Stamp 대조</div>
        </div>
        <div class="flow-arrow" style="text-align: center;">↓</div>
        <div class="pf-arch-layer">
            <div class="pf-arch-layer-title">3. Precise AABB Check</div>
            <div style="font-size: 0.8rem; color: #666;">해당 셀 내의 활성 객체들만 뷰포트 사각형과 최종 교차 검사</div>
        </div>
    </div>
</div>

<h4 id="311-쿼리-스탬프를-활용한-중복-검사-방지">3.1.1 쿼리 스탬프를 활용한 중복 검사 방지</h4>
<p>크기가 큰 오브젝트는 여러 개의 그리드 셀에 걸쳐 존재할 수 있습니다. 뷰포트 영역 내의 셀들을 순회할 때 동일한 객체가 여러 번 쿼리되는 것을 막기 위해 <strong>쿼리 스탬프(Query Stamp)</strong> 기법을 도입했습니다. 쿼리가 발생할 때마다 전역 스탬프 값을 1씩 증가시키고, 이미 검사한 객체에 스탬프를 남겨 불필요한 중복 연산을 완전히 제거했습니다.</p>

<details class="pf-details">
  <summary>코드 보기: ObjectManager.cpp - O(1) 인덱싱 및 가시 객체 추출</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ObjectManager.cpp - 뷰포트 기반 공간 분할 쿼리</span>
<span class="kt">void</span> <span class="n">ObjectManager</span><span class="o">::</span><span class="n">QueryObjectsInRectArea</span><span class="p">(</span><span class="k">const</span> <span class="n">Gdiplus</span><span class="o">::</span><span class="n">RectF</span><span class="o">&amp;</span> <span class="n">rectArea</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">GameObject</span><span class="o">*&gt;&amp;</span> <span class="n">targetOutObjects</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// 월드 좌표를 셀 크기로 나누어 O(1)로 배열 인덱스 획득</span>
    <span class="kt">int</span> <span class="n">startX</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">floor</span><span class="p">(</span><span class="n">rectArea</span><span class="p">.</span><span class="n">X</span> <span class="o">/</span> <span class="n">GRID_CELL_SIZE</span><span class="p">);</span>
    <span class="kt">int</span> <span class="n">startY</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">floor</span><span class="p">(</span><span class="n">rectArea</span><span class="p">.</span><span class="n">Y</span> <span class="o">/</span> <span class="n">GRID_CELL_SIZE</span><span class="p">);</span>
    <span class="kt">int</span> <span class="n">endX</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">ceil</span><span class="p">((</span><span class="n">rectArea</span><span class="p">.</span><span class="n">X</span> <span class="o">+</span> <span class="n">rectArea</span><span class="p">.</span><span class="n">Width</span><span class="p">)</span> <span class="o">/</span> <span class="n">GRID_CELL_SIZE</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">endY</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">ceil</span><span class="p">((</span><span class="n">rectArea</span><span class="p">.</span><span class="n">Y</span> <span class="o">+</span> <span class="n">rectArea</span><span class="p">.</span><span class="n">Height</span><span class="p">)</span> <span class="o">/</span> <span class="n">GRID_CELL_SIZE</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>
    
    <span class="c1">// ... 인덱스 클램핑 생략 ...</span>

    <span class="c1">// 매 쿼리마다 전역 스탬프 갱신</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">++</span><span class="n">m_spatialQueryStamp</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="n">m_spatialQueryStamp</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="n">startY</span><span class="p">;</span> <span class="n">y</span> <span class="o">&lt;=</span> <span class="n">endY</span><span class="p">;</span> <span class="o">++</span><span class="n">y</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="n">startX</span><span class="p">;</span> <span class="n">x</span> <span class="o">&lt;=</span> <span class="n">endX</span><span class="p">;</span> <span class="o">++</span><span class="n">x</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">for</span> <span class="p">(</span><span class="k">auto</span><span class="o">*</span> <span class="n">obj</span> <span class="o">:</span> <span class="n">m_spatialGrid</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">])</span> <span class="p">{</span>
                <span class="c1">// 이미 이번 프레임(쿼리)에서 처리된 객체는 O(1) 스킵</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">obj</span><span class="o">-&gt;</span><span class="n">GetLastSpatialQueryStamp</span><span class="p">()</span> <span class="o">==</span> <span class="n">m_spatialQueryStamp</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>
                <span class="n">obj</span><span class="o">-&gt;</span><span class="n">SetLastSpatialQueryStamp</span><span class="p">(</span><span class="n">m_spatialQueryStamp</span><span class="p">);</span>

                <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">obj</span><span class="o">-&gt;</span><span class="n">IsEnabled</span><span class="p">()</span> <span class="o">||</span> <span class="n">obj</span><span class="o">-&gt;</span><span class="n">IsDead</span><span class="p">())</span> <span class="k">continue</span><span class="p">;</span>

                <span class="c1">// 해당 셀에 속한 객체만 AABB 교차 정밀 검사 수행</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">rectArea</span><span class="p">.</span><span class="n">IntersectsWith</span><span class="p">(</span><span class="n">obj</span><span class="o">-&gt;</span><span class="n">GetBounds</span><span class="p">()))</span> <span class="p">{</span>
                    <span class="n">targetOutObjects</span><span class="p">.</span><span class="n">push_back</span><span class="p">(</span><span class="n">obj</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<h4 id="312-동적-오브젝트의-그리드-이동-처리">3.1.2 동적 오브젝트의 그리드 이동 처리</h4>
<p>플레이어나 몬스터처럼 실시간으로 좌표가 변하는 동적 객체들은 이동할 때마다 자신이 속한 그리드 셀을 갱신해야 합니다. <code class="language-plaintext highlighter-rouge">UpdateObjectGrid</code> 메서드는 객체의 위치가 셀의 경계를 넘어갔을 때만 이전 벡터 배열에서 객체를 <code class="language-plaintext highlighter-rouge">erase</code>하고 새 배열에 <code class="language-plaintext highlighter-rouge">push_back</code>하여 갱신 비용을 최소화합니다.</p>

<details class="pf-details">
  <summary>코드 보기: ObjectManager.cpp - 동적 그리드 갱신</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ObjectManager.cpp - 객체 이동 시 그리드 셀 갱신</span>
<span class="kt">void</span> <span class="n">ObjectManager</span><span class="o">::</span><span class="n">UpdateObjectGrid</span><span class="p">(</span><span class="n">GameObject</span><span class="o">*</span> <span class="n">pObj</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">pObj</span> <span class="o">||</span> <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">GetType</span><span class="p">()</span> <span class="o">==</span> <span class="n">GO_TYPE_UI</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="kt">int</span> <span class="n">oldX</span> <span class="o">=</span> <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">GetGridCellX</span><span class="p">();</span>
    <span class="kt">int</span> <span class="n">oldY</span> <span class="o">=</span> <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">GetGridCellY</span><span class="p">();</span>

    <span class="n">Gdiplus</span><span class="o">::</span><span class="n">RectF</span> <span class="n">bounds</span> <span class="o">=</span> <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">GetBounds</span><span class="p">();</span>
    <span class="kt">int</span> <span class="n">newX</span> <span class="o">=</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">max</span><span class="p">)(</span><span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">min</span><span class="p">)(</span><span class="n">GRID_WIDTH</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">floor</span><span class="p">((</span><span class="n">bounds</span><span class="p">.</span><span class="n">X</span> <span class="o">+</span> <span class="n">bounds</span><span class="p">.</span><span class="n">Width</span> <span class="o">*</span> <span class="mf">0.5</span><span class="n">f</span><span class="p">)</span> <span class="o">/</span> <span class="n">GRID_CELL_SIZE</span><span class="p">)));</span>
    <span class="kt">int</span> <span class="n">newY</span> <span class="o">=</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">max</span><span class="p">)(</span><span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">min</span><span class="p">)(</span><span class="n">GRID_HEIGHT</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">floor</span><span class="p">((</span><span class="n">bounds</span><span class="p">.</span><span class="n">Y</span> <span class="o">+</span> <span class="n">bounds</span><span class="p">.</span><span class="n">Height</span> <span class="o">*</span> <span class="mf">0.5</span><span class="n">f</span><span class="p">)</span> <span class="o">/</span> <span class="n">GRID_CELL_SIZE</span><span class="p">)));</span>

    <span class="c1">// 속한 셀이 변경된 경우에만 이동 처리</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">oldX</span> <span class="o">==</span> <span class="n">newX</span> <span class="o">&amp;&amp;</span> <span class="n">oldY</span> <span class="o">==</span> <span class="n">newY</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">oldX</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">oldX</span> <span class="o">&lt;</span> <span class="n">GRID_WIDTH</span> <span class="o">&amp;&amp;</span> <span class="n">oldY</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">oldY</span> <span class="o">&lt;</span> <span class="n">GRID_HEIGHT</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">GameObject</span><span class="o">*&gt;&amp;</span> <span class="n">cell</span> <span class="o">=</span> <span class="n">m_spatialGrid</span><span class="p">[</span><span class="n">oldX</span><span class="p">][</span><span class="n">oldY</span><span class="p">];</span>
        <span class="n">cell</span><span class="p">.</span><span class="n">erase</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">remove</span><span class="p">(</span><span class="n">cell</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">cell</span><span class="p">.</span><span class="n">end</span><span class="p">(),</span> <span class="n">pObj</span><span class="p">),</span> <span class="n">cell</span><span class="p">.</span><span class="n">end</span><span class="p">());</span>
    <span class="p">}</span>

    <span class="n">m_spatialGrid</span><span class="p">[</span><span class="n">newX</span><span class="p">][</span><span class="n">newY</span><span class="p">].</span><span class="n">push_back</span><span class="p">(</span><span class="n">pObj</span><span class="p">);</span>
    <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">SetGridCell</span><span class="p">(</span><span class="n">newX</span><span class="p">,</span> <span class="n">newY</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<h4 id="313-오브젝트-그리드-추가제거-및-선언-구조-spatial-lifecycle-management">3.1.3 오브젝트 그리드 추가/제거 및 선언 구조 (Spatial Lifecycle Management)</h4>
<p>오브젝트가 새로 생성되거나 소멸할 때, 해당 오브젝트를 그리드 시스템에 안전하게 등록하고 지워주는 과정이 필수적입니다. 또한, 월드의 크기와 타일 크기에 맞추어 그리드의 너비와 높이를 런타임 최적화를 위해 상수로 정의했습니다.</p>

<details class="pf-details">
  <summary>코드 보기: ObjectManager.h - 공간 분할 시스템 헤더 정의</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ObjectManager.h - 공간 분할 시스템의 선언부</span>
<span class="k">class</span> <span class="nc">ObjectManager</span> <span class="p">{</span>
<span class="nl">public:</span>
    <span class="kt">void</span> <span class="n">UpdateObjectGrid</span><span class="p">(</span><span class="n">GameObject</span><span class="o">*</span> <span class="n">pObj</span><span class="p">);</span>
    <span class="kt">void</span> <span class="n">AddToGrid</span><span class="p">(</span><span class="n">GameObject</span><span class="o">*</span> <span class="n">pObj</span><span class="p">);</span>
    <span class="kt">void</span> <span class="n">RemoveFromGrid</span><span class="p">(</span><span class="n">GameObject</span><span class="o">*</span> <span class="n">pObj</span><span class="p">);</span>

<span class="nl">private:</span>
    <span class="c1">// 256px 정사각형 셀 기준의 정적 그리드 크기 정의</span>
    <span class="k">static</span> <span class="k">constexpr</span> <span class="kt">int</span> <span class="n">GRID_CELL_SIZE</span> <span class="o">=</span> <span class="mi">256</span><span class="p">;</span>
    <span class="k">static</span> <span class="k">constexpr</span> <span class="kt">int</span> <span class="n">GRID_WIDTH</span> <span class="o">=</span> <span class="p">(</span><span class="n">MAP_WIDTH</span> <span class="o">*</span> <span class="n">TILE_SIZE</span> <span class="o">/</span> <span class="n">GRID_CELL_SIZE</span><span class="p">)</span> <span class="o">+</span> <span class="mi">2</span><span class="p">;</span>
    <span class="k">static</span> <span class="k">constexpr</span> <span class="kt">int</span> <span class="n">GRID_HEIGHT</span> <span class="o">=</span> <span class="p">(</span><span class="n">MAP_HEIGHT</span> <span class="o">*</span> <span class="n">TILE_SIZE</span> <span class="o">/</span> <span class="n">GRID_CELL_SIZE</span><span class="p">)</span> <span class="o">+</span> <span class="mi">2</span><span class="p">;</span>

    <span class="c1">// 2D 공간 분할 그리드 (각 셀마다 속한 오브젝트 포인터 벡터 관리)</span>
    <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">GameObject</span><span class="o">*&gt;</span> <span class="n">m_spatialGrid</span><span class="p">[</span><span class="n">GRID_WIDTH</span><span class="p">][</span><span class="n">GRID_HEIGHT</span><span class="p">];</span>
    <span class="kt">uint32_t</span> <span class="n">m_spatialQueryStamp</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div>  </div>
</details>

<details class="pf-details">
  <summary>코드 보기: ObjectManager.cpp - 그리드 추가 및 삭제 구현</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ObjectManager.cpp - 객체 생성 시 그리드 등록</span>
<span class="kt">void</span> <span class="n">ObjectManager</span><span class="o">::</span><span class="n">AddToGrid</span><span class="p">(</span><span class="n">GameObject</span><span class="o">*</span> <span class="n">pObj</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">pObj</span> <span class="o">||</span> <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">GetType</span><span class="p">()</span> <span class="o">==</span> <span class="n">GO_TYPE_UI</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="n">Gdiplus</span><span class="o">::</span><span class="n">RectF</span> <span class="n">bounds</span> <span class="o">=</span> <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">GetBounds</span><span class="p">();</span>
    <span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">floor</span><span class="p">((</span><span class="n">bounds</span><span class="p">.</span><span class="n">X</span> <span class="o">+</span> <span class="n">bounds</span><span class="p">.</span><span class="n">Width</span> <span class="o">*</span> <span class="mf">0.5</span><span class="n">f</span><span class="p">)</span> <span class="o">/</span> <span class="n">GRID_CELL_SIZE</span><span class="p">);</span>
    <span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">floor</span><span class="p">((</span><span class="n">bounds</span><span class="p">.</span><span class="n">Y</span> <span class="o">+</span> <span class="n">bounds</span><span class="p">.</span><span class="n">Height</span> <span class="o">*</span> <span class="mf">0.5</span><span class="n">f</span><span class="p">)</span> <span class="o">/</span> <span class="n">GRID_CELL_SIZE</span><span class="p">);</span>

    <span class="n">x</span> <span class="o">=</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">max</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">)(</span><span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">min</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">)(</span><span class="n">GRID_WIDTH</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">x</span><span class="p">));</span>
    <span class="n">y</span> <span class="o">=</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">max</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">)(</span><span class="mi">0</span><span class="p">,</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">min</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">)(</span><span class="n">GRID_HEIGHT</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">y</span><span class="p">));</span>

    <span class="n">m_spatialGrid</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">].</span><span class="n">push_back</span><span class="p">(</span><span class="n">pObj</span><span class="p">);</span>
    <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">SetGridCell</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// ObjectManager.cpp - 객체 소멸 시 그리드 제거</span>
<span class="kt">void</span> <span class="n">ObjectManager</span><span class="o">::</span><span class="n">RemoveFromGrid</span><span class="p">(</span><span class="n">GameObject</span><span class="o">*</span> <span class="n">pObj</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">pObj</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">GetGridCellX</span><span class="p">();</span>
    <span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">GetGridCellY</span><span class="p">();</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">x</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">x</span> <span class="o">&lt;</span> <span class="n">GRID_WIDTH</span> <span class="o">&amp;&amp;</span> <span class="n">y</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">y</span> <span class="o">&lt;</span> <span class="n">GRID_HEIGHT</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">GameObject</span><span class="o">*&gt;&amp;</span> <span class="n">cell</span> <span class="o">=</span> <span class="n">m_spatialGrid</span><span class="p">[</span><span class="n">x</span><span class="p">][</span><span class="n">y</span><span class="p">];</span>
        <span class="n">cell</span><span class="p">.</span><span class="n">erase</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">remove</span><span class="p">(</span><span class="n">cell</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">cell</span><span class="p">.</span><span class="n">end</span><span class="p">(),</span> <span class="n">pObj</span><span class="p">),</span> <span class="n">cell</span><span class="p">.</span><span class="n">end</span><span class="p">());</span>
        <span class="n">pObj</span><span class="o">-&gt;</span><span class="n">SetGridCell</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="4-애니메이션-시스템-및-이벤트-콜백">4. 애니메이션 시스템 및 이벤트 콜백</h2>

<p><code class="language-plaintext highlighter-rouge">Animator</code>와 <code class="language-plaintext highlighter-rouge">AnimationClip</code>을 분리하여 고도로 제어 가능한 프레임 기반 애니메이션 시스템을 구축했습니다. 특히, 애니메이션의 시각적 흐름과 게임의 논리적 흐름(데미지 판정, 사운드 재생 등)을 일치시키기 위해 <strong>프레임 이벤트 콜백(Frame Event Callback)</strong> 시스템을 도입했습니다.</p>

<h3 id="41-클립-분리-및-프레임-진행">4.1 클립 분리 및 프레임 진행</h3>

<p>하나의 <code class="language-plaintext highlighter-rouge">SpriteSheet</code>에서 특정 프레임들을 잘라내어 <code class="language-plaintext highlighter-rouge">AnimationClip</code> 객체를 생성합니다. <code class="language-plaintext highlighter-rouge">Animator</code>는 현재 재생 중인 클립의 경과 시간(<code class="language-plaintext highlighter-rouge">m_elapsed</code>)을 바탕으로 현재 출력해야 할 프레임 인덱스를 계산합니다.</p>

<div class="pf-visual-frame">
    <div class="pf-transaction-flow">
        <div class="pf-flow-step"><strong>Update()</strong><br />m_elapsed += deltaTime</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>Frame Sync</strong><br />GetCurrentFrameIndex()</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>Event Trigger</strong><br />지나친 프레임의 이벤트 검사</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>Logic Sync</strong><br />Attack / Sound / Effect 콜백 실행</div>
    </div>
</div>

<h3 id="42-프레임-이벤트-검사-및-콜백-실행">4.2 프레임 이벤트 검사 및 콜백 실행</h3>

<p>프레임 델타 타임에 의해 여러 프레임을 한 번에 건너뛰더라도(Frame Skip), 이벤트가 누락되지 않도록 <code class="language-plaintext highlighter-rouge">m_lastTriggeredFrame</code>부터 <code class="language-plaintext highlighter-rouge">currentFrameIndex</code>까지 반복문을 돌며 등록된 모든 이벤트를 순차적으로 실행합니다.</p>

<details class="pf-details">
  <summary>코드 보기: Animator.cpp - 누락 없는 이벤트 처리 로직</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Animator.cpp - 프레임 이벤트 처리 로직</span>
<span class="k">if</span> <span class="p">(</span><span class="n">currentFrameIndex</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="n">currentFrameIndex</span> <span class="o">!=</span> <span class="n">m_lastTriggeredFrame</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">map</span><span class="o">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="n">std</span><span class="o">::</span><span class="n">wstring</span><span class="o">&gt;&amp;</span> <span class="n">eventFrames</span> <span class="o">=</span> <span class="n">m_currentClip</span><span class="o">-&gt;</span><span class="n">GetEventFrames</span><span class="p">();</span>
    <span class="k">const</span> <span class="n">AnimationEventCallback</span><span class="o">&amp;</span> <span class="n">callback</span> <span class="o">=</span> <span class="n">m_currentClip</span><span class="o">-&gt;</span><span class="n">GetEventCallback</span><span class="p">();</span>

    <span class="n">AnimationClip</span><span class="o">*</span> <span class="n">pCurrentClipBeforeCallback</span> <span class="o">=</span> <span class="n">m_currentClip</span><span class="p">;</span>

    <span class="c1">// 건너뛴 프레임(startIdx)부터 현재 프레임(endIdx)까지 모두 검사하여 누락 방지</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">fi</span> <span class="o">=</span> <span class="n">m_lastTriggeredFrame</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="n">fi</span> <span class="o">&lt;=</span> <span class="n">currentFrameIndex</span> <span class="o">&amp;&amp;</span> <span class="n">callback</span><span class="p">;</span> <span class="o">++</span><span class="n">fi</span> <span class="p">)</span> <span class="p">{</span>
        <span class="k">auto</span> <span class="n">eventIt</span> <span class="o">=</span> <span class="n">eventFrames</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="n">fi</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">eventIt</span> <span class="o">!=</span> <span class="n">eventFrames</span><span class="p">.</span><span class="n">end</span><span class="p">())</span>
        <span class="p">{</span>
            <span class="n">callback</span><span class="p">(</span><span class="n">fi</span><span class="p">,</span> <span class="n">eventIt</span><span class="o">-&gt;</span><span class="n">second</span><span class="p">);</span> <span class="c1">// 등록된 콜백 실행 (공격 판정, 사운드 등)</span>

            <span class="c1">// 콜백 내부의 로직(예: 사망, 피격)으로 인해 애니메이션 상태가 변경되었다면 즉시 루프 중단</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">pCurrentClipBeforeCallback</span> <span class="o">!=</span> <span class="n">m_currentClip</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">return</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">m_lastTriggeredFrame</span> <span class="o">=</span> <span class="n">currentFrameIndex</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<h3 id="43-람다-캡처를-활용한-이벤트-바인딩">4.3 람다 캡처를 활용한 이벤트 바인딩</h3>

<p>객체(Player 등) 초기화 시점에 <code class="language-plaintext highlighter-rouge">RegisterAnimation</code>을 통해 클립을 생성하고, 특정 프레임 번호에 이벤트 문자열(<code class="language-plaintext highlighter-rouge">eventName</code>)을 등록합니다. 이후 람다 함수를 통해 콜백을 바인딩하여 직관적으로 로직을 구성했습니다.</p>

<details class="pf-details">
  <summary>코드 보기: Player.cpp - 도끼질(Chop) 애니메이션 이벤트 바인딩 예시</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Player.cpp - 도끼질 애니메이션 설정 및 이벤트 콜백 바인딩</span>
<span class="k">const</span> <span class="kt">int</span> <span class="n">CHOP_HIT_FRAME</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span> <span class="c1">// 도끼가 나무에 맞는 프레임</span>

<span class="c1">// 클립에 이벤트 프레임 등록</span>
<span class="n">AnimationClip</span><span class="o">*</span> <span class="n">clip</span> <span class="o">=</span> <span class="n">m_animator</span><span class="o">-&gt;</span><span class="n">GetAnimationClip</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">PlayerState</span><span class="o">::</span><span class="n">CHOP</span><span class="p">,</span> <span class="n">dir</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">clip</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">clip</span><span class="o">-&gt;</span><span class="n">AddEventFrame</span><span class="p">(</span><span class="n">CHOP_HIT_FRAME</span><span class="p">,</span> <span class="s">L"chop_hit"</span><span class="p">);</span>
    <span class="n">clip</span><span class="o">-&gt;</span><span class="n">AddEventFrame</span><span class="p">(</span><span class="n">CHOP_LAST_FRAME</span><span class="p">,</span> <span class="s">L"chop_end"</span><span class="p">);</span>

    <span class="c1">// 이벤트 콜백 람다 바인딩</span>
    <span class="n">clip</span><span class="o">-&gt;</span><span class="n">SetEventCallback</span><span class="p">([</span><span class="k">this</span><span class="p">](</span><span class="kt">int</span> <span class="n">frameIndex</span><span class="p">,</span> <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">wstring</span><span class="o">&amp;</span> <span class="n">eventName</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">eventName</span> <span class="o">==</span> <span class="s">L"chop_hit"</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">SoundManager</span><span class="o">::</span><span class="n">GetInstance</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">PlaySFX</span><span class="p">(</span><span class="s">L"Resource/Sound/PlayerSound/Chop_tree.wav"</span><span class="p">);</span>
            <span class="k">this</span><span class="o">-&gt;</span><span class="n">OnChopHit</span><span class="p">();</span> <span class="c1">// 타겟 나무에 데미지 적용</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">eventName</span> <span class="o">==</span> <span class="s">L"chop_end"</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">this</span><span class="o">-&gt;</span><span class="n">OnChopEnd</span><span class="p">();</span> <span class="c1">// 상태 전이</span>
        <span class="p">}</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="5-데이터-로드-및-게임-진행-정보-저장">5. 데이터 로드 및 게임 진행 정보 저장</h2>

<p>하드코딩을 배제하고 유지보수성을 높이기 위해 게임의 거의 모든 요소를 데이터 주도(Data-Driven) 방식으로 설계했습니다. 맵 배치부터 오브젝트의 고유 속성, 그리고 플레이어의 진행 상황까지 파일 기반으로 관리됩니다.</p>

<h3 id="51-커스텀-맵-포맷dsm-파싱-시스템">5.1 커스텀 맵 포맷(.dsm) 파싱 시스템</h3>

<p>자체 제작한 맵 에디터에서 출력되는 <code class="language-plaintext highlighter-rouge">.dsm</code> (Don’t Starve Map) 포맷을 파싱하여 게임 씬을 초기화합니다. 이 파일에는 맵의 메타데이터, 타일 정보, 이동 가능 구역(Walkable Area), 그리고 오브젝트의 배치 좌표가 포함됩니다.</p>

<div class="pf-visual-frame">
    <div class="pf-transaction-flow">
        <div class="pf-flow-step"><strong>1. File I/O</strong><br />std::wifstream<br />BOM 제거 및 라인 단위 리드</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>2. Section Parsing</strong><br /># METADATA, # TILES<br /># OBJECTS 분류</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>3. Object Mapping</strong><br />문자열 ID ↔ Enum 변환<br />DataManager 리소스 조회</div>
        <div class="pf-flow-arrow">→</div>
        <div class="pf-flow-step"><strong>4. MapData 생성</strong><br />SceneManager를 통한<br />최종 씬 초기화 적용</div>
    </div>
</div>

<details class="pf-details">
  <summary>코드 보기: Function.h - 맵 파일 파싱(ParseMapFileInto) 핵심 로직</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Function.h - 맵 파일 파싱 로직 일부</span>
<span class="k">template</span><span class="o">&lt;</span><span class="k">typename</span> <span class="nc">GetObjectResourceInfoFunc</span><span class="p">&gt;</span>
<span class="kr">inline</span> <span class="kt">bool</span> <span class="nf">ParseMapFileInto</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">wstring</span><span class="o">&amp;</span> <span class="n">mapFileName</span><span class="p">,</span> <span class="n">MapData</span><span class="o">&amp;</span> <span class="n">outMapData</span><span class="p">,</span> <span class="n">GetObjectResourceInfoFunc</span> <span class="n">getObjectResourceInfo</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">wifstream</span> <span class="n">file</span><span class="p">(</span><span class="n">mapFileName</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">file</span><span class="p">.</span><span class="n">is_open</span><span class="p">())</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
    
    <span class="c1">// ... BOM 처리 생략 ...</span>

    <span class="n">std</span><span class="o">::</span><span class="n">wstring</span> <span class="n">line</span><span class="p">;</span>
    <span class="k">enum</span> <span class="n">Section</span> <span class="p">{</span> <span class="n">NONE</span><span class="p">,</span> <span class="n">METADATA</span><span class="p">,</span> <span class="n">PLAYER</span><span class="p">,</span> <span class="n">TILES</span><span class="p">,</span> <span class="n">OBJECTS</span><span class="p">,</span> <span class="n">WALKABLE</span> <span class="p">}</span> <span class="n">section</span> <span class="o">=</span> <span class="n">NONE</span><span class="p">;</span>

    <span class="k">while</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">getline</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">line</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="n">empty</span><span class="p">()</span> <span class="o">||</span> <span class="n">line</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">L'#'</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">L"# TILES"</span><span class="p">)</span> <span class="o">!=</span> <span class="n">std</span><span class="o">::</span><span class="n">wstring</span><span class="o">::</span><span class="n">npos</span><span class="p">)</span> <span class="n">section</span> <span class="o">=</span> <span class="n">TILES</span><span class="p">;</span>
            <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">L"# OBJECTS"</span><span class="p">)</span> <span class="o">!=</span> <span class="n">std</span><span class="o">::</span><span class="n">wstring</span><span class="o">::</span><span class="n">npos</span><span class="p">)</span> <span class="n">section</span> <span class="o">=</span> <span class="n">OBJECTS</span><span class="p">;</span>
            <span class="c1">// ... 섹션 분기 ...</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// 오브젝트 배치 정보 파싱</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">section</span> <span class="o">==</span> <span class="n">OBJECTS</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">std</span><span class="o">::</span><span class="n">wstringstream</span> <span class="n">ss</span><span class="p">(</span><span class="n">line</span><span class="p">);</span>
            <span class="n">std</span><span class="o">::</span><span class="n">wstring</span> <span class="n">id</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">;</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">getline</span><span class="p">(</span><span class="n">ss</span><span class="p">,</span> <span class="n">id</span><span class="p">,</span> <span class="sc">L','</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">std</span><span class="o">::</span><span class="n">getline</span><span class="p">(</span><span class="n">ss</span><span class="p">,</span> <span class="n">x</span><span class="p">,</span> <span class="sc">L','</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">std</span><span class="o">::</span><span class="n">getline</span><span class="p">(</span><span class="n">ss</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="sc">L','</span><span class="p">))</span> <span class="p">{</span>
                
                <span class="n">GameObjectID</span> <span class="n">objID</span> <span class="o">=</span> <span class="n">EnumTables</span><span class="o">::</span><span class="n">GetGameObjectID</span><span class="p">(</span><span class="n">id</span><span class="p">.</span><span class="n">c_str</span><span class="p">());</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">objID</span> <span class="o">!=</span> <span class="n">GOID_NONE</span><span class="p">)</span> <span class="p">{</span>
                    <span class="n">ResourcePathUtils</span><span class="o">::</span><span class="n">ObjectResourceDef</span> <span class="n">objData</span><span class="p">;</span>
                    <span class="n">objData</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">objID</span><span class="p">;</span>
                    <span class="n">objData</span><span class="p">.</span><span class="n">x</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">stof</span><span class="p">(</span><span class="n">x</span><span class="p">);</span>
                    <span class="n">objData</span><span class="p">.</span><span class="n">y</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">stof</span><span class="p">(</span><span class="n">y</span><span class="p">);</span>
                    <span class="n">outMapData</span><span class="p">.</span><span class="n">gameObjects</span><span class="p">.</span><span class="n">push_back</span><span class="p">(</span><span class="n">objData</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">file</span><span class="p">.</span><span class="n">close</span><span class="p">();</span>
    <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<h3 id="52-동적-속성-덮어쓰기-object-overrides">5.2 동적 속성 덮어쓰기 (Object Overrides)</h3>

<p>맵 파싱 과정에서 생성될 오브젝트들의 크기, 콜라이더 정보, 피벗 좌표 등은 소스 코드를 수정하지 않고 텍스트 파일(<code class="language-plaintext highlighter-rouge">object_resource_overrides.txt</code>)을 수정하여 즉시 게임에 반영할 수 있습니다. <code class="language-plaintext highlighter-rouge">DataManager</code>가 게임 초기화 시점에 이 파일을 읽어 기본값을 덮어씌웁니다.</p>

<details class="pf-details">
  <summary>코드 보기: DataManager.cpp - 오브젝트 리소스 데이터 오버라이드</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// DataManager.cpp - 오브젝트 리소스 및 콜라이더 데이터 오버라이드</span>
<span class="kt">void</span> <span class="n">DataManager</span><span class="o">::</span><span class="n">Init</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// ... 정적 테이블 초기화 생략 ...</span>

    <span class="n">ResourcePathUtils</span><span class="o">::</span><span class="n">ParseObjectResourceOverridesFile</span><span class="p">(</span><span class="s">L"GameData/object_resource_overrides.txt"</span><span class="p">,</span>
        <span class="p">[</span><span class="k">this</span><span class="p">](</span><span class="n">GameObjectID</span> <span class="n">id</span><span class="p">,</span> <span class="k">const</span> <span class="n">ResourcePathUtils</span><span class="o">::</span><span class="n">ObjectResourceDef</span><span class="o">&amp;</span> <span class="n">overrideDef</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">auto</span> <span class="n">it</span> <span class="o">=</span> <span class="n">m_objectResources</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">it</span> <span class="o">!=</span> <span class="n">m_objectResources</span><span class="p">.</span><span class="n">end</span><span class="p">())</span> <span class="p">{</span>
                <span class="c1">// 텍스트 파일에 정의된 값을 실제 게임 데이터 딕셔너리에 덮어씌움</span>
                <span class="n">it</span><span class="o">-&gt;</span><span class="n">second</span><span class="p">.</span><span class="n">pivotX</span> <span class="o">=</span> <span class="n">overrideDef</span><span class="p">.</span><span class="n">pivotX</span><span class="p">;</span>
                <span class="n">it</span><span class="o">-&gt;</span><span class="n">second</span><span class="p">.</span><span class="n">pivotY</span> <span class="o">=</span> <span class="n">overrideDef</span><span class="p">.</span><span class="n">pivotY</span><span class="p">;</span>
                <span class="n">it</span><span class="o">-&gt;</span><span class="n">second</span><span class="p">.</span><span class="n">hasCollider</span> <span class="o">=</span> <span class="n">overrideDef</span><span class="p">.</span><span class="n">hasCollider</span><span class="p">;</span>
                <span class="n">it</span><span class="o">-&gt;</span><span class="n">second</span><span class="p">.</span><span class="n">colliderWidth</span> <span class="o">=</span> <span class="n">overrideDef</span><span class="p">.</span><span class="n">colliderWidth</span><span class="p">;</span>
                <span class="n">it</span><span class="o">-&gt;</span><span class="n">second</span><span class="p">.</span><span class="n">colliderHeight</span> <span class="o">=</span> <span class="n">overrideDef</span><span class="p">.</span><span class="n">colliderHeight</span><span class="p">;</span>
                <span class="c1">// ...</span>
            <span class="p">}</span>
        <span class="p">});</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<h3 id="53-스냅샷-기반-씬scene-상태-보존">5.3 스냅샷 기반 씬(Scene) 상태 보존</h3>

<p>게임 플레이 도중 씬을 이동(포탈 이용 등)할 때, 플레이어의 현재 체력과 인벤토리 상태를 유지해야 합니다. <code class="language-plaintext highlighter-rouge">GameProgressManager</code>는 씬 전환 직전에 <code class="language-plaintext highlighter-rouge">PlayerStateSnapshot</code> 구조체를 이용해 플레이어의 상태를 캡처하고, 새로운 씬이 로드된 직후 <code class="language-plaintext highlighter-rouge">RestoreState</code> 함수를 통해 완벽하게 복원합니다.</p>

<details class="pf-details">
  <summary>코드 보기: Player.cpp - 상태 저장 및 복원</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Player.cpp - 상태 캡처 (씬 전환용)</span>
<span class="n">PlayerStateSnapshot</span> <span class="n">Player</span><span class="o">::</span><span class="n">SaveState</span><span class="p">()</span> <span class="k">const</span>
<span class="p">{</span>
    <span class="n">PlayerStateSnapshot</span> <span class="n">snapshot</span><span class="p">;</span>
    <span class="n">snapshot</span><span class="p">.</span><span class="n">hp</span> <span class="o">=</span> <span class="n">m_hp</span><span class="p">;</span>
    <span class="n">snapshot</span><span class="p">.</span><span class="n">equippedSlotIndex</span> <span class="o">=</span> <span class="n">m_equippedSlotIndex</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">m_inventory</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">snapshot</span><span class="p">.</span><span class="n">inventoryItems</span> <span class="o">=</span> <span class="n">m_inventory</span><span class="o">-&gt;</span><span class="n">GetAllItemsSnapshot</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">snapshot</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Player.cpp - 상태 복원</span>
<span class="kt">void</span> <span class="n">Player</span><span class="o">::</span><span class="n">RestoreState</span><span class="p">(</span><span class="k">const</span> <span class="n">PlayerStateSnapshot</span><span class="o">&amp;</span> <span class="n">snapshot</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">m_hp</span> <span class="o">=</span> <span class="n">snapshot</span><span class="p">.</span><span class="n">hp</span><span class="p">;</span>
    
    <span class="k">if</span> <span class="p">(</span><span class="n">m_inventory</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">m_inventory</span><span class="o">-&gt;</span><span class="n">ClearAllItems</span><span class="p">();</span>
        <span class="k">for</span> <span class="p">(</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">item</span> <span class="o">:</span> <span class="n">snapshot</span><span class="p">.</span><span class="n">inventoryItems</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">m_inventory</span><span class="o">-&gt;</span><span class="n">AddItem</span><span class="p">(</span><span class="n">item</span><span class="p">.</span><span class="n">first</span><span class="p">,</span> <span class="n">item</span><span class="p">.</span><span class="n">second</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">snapshot</span><span class="p">.</span><span class="n">equippedSlotIndex</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">ToggleEquipItem</span><span class="p">(</span><span class="n">snapshot</span><span class="p">.</span><span class="n">equippedSlotIndex</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<p>또한, 특정 보스를 클리어했을 때 신규 캐릭터가 해금되는 요소는 <code class="language-plaintext highlighter-rouge">game_progress.txt</code>에 문자열 포맷(<code class="language-plaintext highlighter-rouge">CHARACTER:1002,UNLOCKED:1</code>)으로 디스크에 영구 저장되어 다음 실행 시에도 로드됩니다.</p>

<h3 id="54-영구적인-게임-진행도-저장-saveload">5.4 영구적인 게임 진행도 저장 (Save/Load)</h3>

<p>보스를 클리어하여 캐릭터가 해금되는 등의 영구적인 게임 진행 상황은 <code class="language-plaintext highlighter-rouge">GameProgressManager</code>를 통해 파일(<code class="language-plaintext highlighter-rouge">game_progress.txt</code>) 시스템에 기록됩니다. 파일 입출력을 통해 진행도를 파싱하고 직렬화하여 데이터를 안전하게 보존합니다.</p>

<details class="pf-details">
  <summary>코드 보기: GameProgressManager.cpp - 진행도 세이브 및 로드</summary>

  <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// GameProgressManager.cpp - 캐릭터 해금 정보 저장 및 로드</span>
<span class="kt">void</span> <span class="n">GameProgressManager</span><span class="o">::</span><span class="n">SaveToFile</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">wstring</span><span class="o">&amp;</span> <span class="n">filePath</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// ... 디렉토리 생성 로직 생략 ...</span>
    <span class="n">std</span><span class="o">::</span><span class="n">wofstream</span> <span class="n">file</span><span class="p">(</span><span class="n">filePath</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">file</span><span class="p">.</span><span class="n">is_open</span><span class="p">())</span> <span class="k">return</span><span class="p">;</span>
    
    <span class="k">for</span> <span class="p">(</span><span class="k">const</span> <span class="k">auto</span><span class="o">&amp;</span> <span class="n">charInfo</span> <span class="o">:</span> <span class="n">m_gameProgress</span><span class="p">.</span><span class="n">characterUnlockInfos</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">file</span> <span class="o">&lt;&lt;</span> <span class="s">L"CHARACTER:"</span> <span class="o">&lt;&lt;</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">charInfo</span><span class="p">.</span><span class="n">characterID</span>
             <span class="o">&lt;&lt;</span> <span class="s">L",UNLOCKED:"</span> <span class="o">&lt;&lt;</span> <span class="p">(</span><span class="n">charInfo</span><span class="p">.</span><span class="n">isUnlocked</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="s">L"</span><span class="se">\n</span><span class="s">"</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">file</span><span class="p">.</span><span class="n">close</span><span class="p">();</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">GameProgressManager</span><span class="o">::</span><span class="n">LoadFromFile</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">wstring</span><span class="o">&amp;</span> <span class="n">filePath</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">wifstream</span> <span class="n">file</span><span class="p">(</span><span class="n">filePath</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">file</span><span class="p">.</span><span class="n">is_open</span><span class="p">())</span> <span class="k">return</span><span class="p">;</span>
    
    <span class="n">std</span><span class="o">::</span><span class="n">wstring</span> <span class="n">line</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">getline</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">line</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="n">empty</span><span class="p">()</span> <span class="o">||</span> <span class="n">line</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">L'#'</span> <span class="o">||</span> <span class="n">line</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">L'['</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>
        
        <span class="k">if</span> <span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">L"CHARACTER:"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">ParseCharacterLine</span><span class="p">(</span><span class="n">line</span><span class="p">);</span> <span class="c1">// 문자열을 파싱하여 해금 데이터 배열 업데이트</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">file</span><span class="p">.</span><span class="n">close</span><span class="p">();</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">GameProgressManager</span><span class="o">::</span><span class="n">ParseCharacterLine</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">wstring</span><span class="o">&amp;</span> <span class="n">line</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">size_t</span> <span class="n">charPos</span> <span class="o">=</span> <span class="n">line</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">L"CHARACTER:"</span><span class="p">)</span> <span class="o">+</span> <span class="mi">10</span><span class="p">;</span>
    <span class="kt">size_t</span> <span class="n">unlockedPos</span> <span class="o">=</span> <span class="n">line</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="s">L",UNLOCKED:"</span><span class="p">)</span> <span class="o">+</span> <span class="mi">10</span><span class="p">;</span>
    
    <span class="kt">int</span> <span class="n">characterID</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">stoi</span><span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="n">substr</span><span class="p">(</span><span class="n">charPos</span><span class="p">,</span> <span class="n">line</span><span class="p">.</span><span class="n">find</span><span class="p">(</span><span class="sc">L','</span><span class="p">,</span> <span class="n">charPos</span><span class="p">)</span> <span class="o">-</span> <span class="n">charPos</span><span class="p">));</span>
    <span class="kt">int</span> <span class="n">unlocked</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">stoi</span><span class="p">(</span><span class="n">line</span><span class="p">.</span><span class="n">substr</span><span class="p">(</span><span class="n">unlockedPos</span><span class="p">));</span>
    
    <span class="k">for</span> <span class="p">(</span><span class="k">auto</span><span class="o">&amp;</span> <span class="n">charInfo</span> <span class="o">:</span> <span class="n">m_gameProgress</span><span class="p">.</span><span class="n">characterUnlockInfos</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">charInfo</span><span class="p">.</span><span class="n">characterID</span> <span class="o">==</span> <span class="n">characterID</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">unlocked</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="n">charInfo</span><span class="p">.</span><span class="n">isUnlocked</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<p class="notice--success"><strong>Conclusion:</strong> <code class="language-plaintext highlighter-rouge">WINAPI_DONT_STARVE</code>는 저수준의 WinAPI 환경에서도 고성능 아키텍처와 공간 최적화 기법을 통해 대규모 오브젝트 시스템을 안정적으로 구현해냈습니다.</p>]]></content><author><name>이민혁 (Minhyuk Lee)</name></author><category term="project" /><category term="C++" /><category term="WinAPI" /><category term="Optimization" /><category term="Game Logic" /><summary type="html"><![CDATA[C++, WinAPI를 이용한 Don't Starve 모사 및 성능 최적화]]></summary></entry><entry><title type="html">PROJECT REPORT // WORLDFIRSTKILL</title><link href="https://infisword.github.io/project/worldfirstkill/" rel="alternate" type="text/html" title="PROJECT REPORT // WORLDFIRSTKILL" /><published>2026-05-23T00:00:00+00:00</published><updated>2026-05-23T00:00:00+00:00</updated><id>https://infisword.github.io/project/worldfirstkill</id><content type="html" xml:base="https://infisword.github.io/project/worldfirstkill/"><![CDATA[<style>
    /* --- Premium Report Layout Styles --- */
    
    .chapter-title {
        font-size: 2.2rem;
        color: #fff !important;
        background: linear-gradient(90deg, #007bff 0%, #58A6FF 100%);
        padding: 25px 35px;
        border-radius: 12px;
        margin-top: 60px !important;
        margin-bottom: 40px !important;
        box-shadow: 0 10px 30px rgba(0, 123, 255, 0.15);
        display: flex;
        align-items: center;
        border: none !important;
    }
    .chapter-title::before {
        content: "CHAPTER.";
        font-family: 'Fira Code', monospace;
        font-size: 0.9rem;
        letter-spacing: 2px;
        margin-right: 15px;
        opacity: 0.8;
    }

    .pf-visual-frame {
        width: 100%; padding: 35px; background: #fcfcfc;
        border: 1px solid #e1e4e8; border-radius: 16px;
        margin: 30px 0; text-align: center;
        box-shadow: inset 0 2px 10px rgba(0,0,0,0.02);
    }
    .pf-arch-diagram {
        display: flex; flex-direction: column; gap: 20px; margin: 25px 0;
    }
    .pf-arch-layer {
        padding: 20px 25px; border: 1px solid #e1e4e8; border-radius: 8px;
        background: #fff; position: relative; text-align: center;
        box-shadow: 0 4px 12px rgba(0,0,0,0.03);
    }
    .pf-arch-layer::before {
        content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
        background: #007bff; border-radius: 8px 0 0 8px;
    }
    .pf-arch-layer-title {
        color: #007bff; font-weight: 700; margin-bottom: 10px;
        font-family: 'Fira Code', monospace; font-size: 0.95rem; text-align: center;
    }
    .pf-arch-layer-items {
        display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; justify-content: center;
    }
    .pf-arch-item {
        padding: 6px 14px; background: rgba(0, 123, 255, 0.08);
        border: 1px solid rgba(0, 123, 255, 0.3); border-radius: 6px;
        font-size: 0.85rem; color: #0056b3; text-align: center;
    }

    /* --- Token System Styling --- */
    .pf-token-container { display: flex; flex-direction: column; gap: 20px; margin-top: 25px; }
    .pf-token-row { display: flex; align-items: center; gap: 15px; flex-wrap: wrap; justify-content: center; }
    .pf-token-box { background: rgba(0, 123, 255, 0.05); padding: 15px 20px; border-radius: 8px; border: 2px solid #007bff; flex: 0 1 auto; min-width: 180px; text-align: center; }
    .pf-token-box-title { color: #007bff; font-weight: 700; margin-bottom: 8px; font-family: 'Fira Code', monospace; }
    .pf-domain-info { margin: 20px auto; padding: 20px; background: #fff; border-radius: 8px; border-left: 4px solid #28a745; border: 1px solid #eee; max-width: 600px; }
    .pf-domain-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
    .pf-domain-item { background: rgba(40, 167, 69, 0.05); padding: 12px; border-radius: 6px; border: 1px solid #28a745; text-align: center; }

    /* --- Code Details --- */
    details.pf-details {
        margin: 20px 0;
        border: 1px solid #e1e4e8;
        border-radius: 8px;
        background: #f8f9fa;
        overflow: hidden;
    }
    details.pf-details summary {
        padding: 15px 20px;
        font-weight: 700;
        color: #007bff;
        cursor: pointer;
        outline: none;
        background: #fff;
        display: flex;
        align-items: center;
    }

    .highlight-box {
        background: #f0f7ff;
        border-left: 5px solid #007bff;
        padding: 20px;
        margin: 20px 0;
        border-radius: 0 8px 8px 0;
    }

    /* --- Flow Arrows --- */
    .flow-arrow {
        color: #007bff; font-weight: bold; font-size: 1.5rem; margin: 10px 0;
    }
</style>

<p>서버(구글 스프레드시트) 기반 게임 데이터 관리와, Seed/Token을 활용한 완벽한 상태 복원 시스템입니다. 리플렉션을 통한 동적 파싱과 비동기 로딩 시스템을 통해 유지보수성과 유저 경험을 동시에 확보했습니다.</p>

<hr />

<h2 class="chapter-title" id="1-리플렉션-기반-csv-파싱-및-버전-관리-시스템">1. 리플렉션 기반 CSV 파싱 및 버전 관리 시스템</h2>

<p>서버에서 다운로드한 CSV 파일의 헤더와 데이터 클래스의 필드를 자동으로 매핑하여 동적으로 파싱하는 시스템을 구현했습니다. <strong>리플렉션(Reflection)</strong>을 활용하여 타입 안전성을 보장하면서도 확장 가능한 파싱 로직을 설계했습니다.</p>

<h3 id="11-서버-버전-동기화-및-선택적-다운로드">1.1 서버 버전 동기화 및 선택적 다운로드</h3>

<p>게임 시작 시 <code class="language-plaintext highlighter-rouge">ServerCSV_ConvertData</code> 클래스가 메타 시트를 우선 다운로드하여 서버의 데이터 버전 정보를 확인합니다. 이후 로컬에 저장된 <code class="language-plaintext highlighter-rouge">data_versions.json</code> 파일과 대조하여 <strong>변경된 파일만 선택적으로 다운로드</strong>함으로써 네트워크 비용과 로딩 시간을 최소화했습니다.</p>

<h3 id="12-csv-파싱-프로세스-플로우">1.2 CSV 파싱 프로세스 플로우</h3>

<p>다운로드 된 데이터 파싱은 다음과 같은 4단계로 진행됩니다:</p>

<div class="pf-visual-frame">
    <div style="font-weight: 700; margin-bottom: 20px; color: #007bff; font-family: 'Fira Code', monospace;">CSV Parsing Process</div>
    <div class="pf-arch-diagram">
        <div class="pf-arch-layer">
            <div class="pf-arch-layer-title">1단계: 타입 정보 수집</div>
            <div style="font-size: 0.85rem; color: #666;">리플렉션으로 제네릭 타입 T의 모든 필드 정보(FieldInfo) 추출</div>
        </div>
        <div class="flow-arrow" style="text-align: center;">↓</div>
        <div class="pf-arch-layer">
            <div class="pf-arch-layer-title">2단계: 헤더 추출 및 매핑</div>
            <div style="font-size: 0.85rem; color: #666;">CSV 첫 줄(헤더)을 분리하고 클래스 필드명과 자동 매핑을 위한 딕셔너 구축</div>
        </div>
        <div class="flow-arrow" style="text-align: center;">↓</div>
        <div class="pf-arch-layer">
            <div class="pf-arch-layer-title">3단계: 데이터 행 처리 및 객체 생성</div>
            <div style="font-size: 0.85rem; color: #666;">정규식(SmartSplit)으로 쉼표/따옴표 처리 후 빈 인스턴스(Activator) 생성</div>
        </div>
        <div class="flow-arrow" style="text-align: center;">↓</div>
        <div class="pf-arch-layer">
            <div class="pf-arch-layer-title">4단계: 타입 변환 및 필드 할당</div>
            <div style="font-size: 0.85rem; color: #666;">Convert.ChangeType을 통해 string 데이터를 실제 필드 타입으로 변환 후 할당</div>
        </div>
    </div>
</div>

<h3 id="13-핵심-파싱-로직-및-smartsplit">1.3 핵심 파싱 로직 및 SmartSplit</h3>

<p>제네릭 메서드를 사용하여 타입에 독립적인 파싱을 수행합니다. 특히, CSV 내부의 쉼표(,)를 포함하는 문자열(“이름, 설명”)을 안전하게 처리하기 위해 정규식을 활용한 <code class="language-plaintext highlighter-rouge">SmartSplit</code> 함수를 구현했습니다.</p>

<p><strong>CSV 파싱 핵심 로직 및 SmartSplit</strong>
정규식을 활용하여 따옴표 내부의 쉼표를 안전하게 처리하고, 리플렉션을 통해 동적으로 객체에 값을 할당합니다.</p>

<details class="pf-details">
  <summary>코드 보기: CSV 파싱 핵심 로직 및 SmartSplit</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// CSVParser.cs: 정규식을 이용한 안전한 CSV 행 분할</span>
<span class="kt">string</span><span class="p">[]</span> <span class="nf">SmartSplit</span><span class="p">(</span><span class="kt">string</span> <span class="n">line</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">Regex</span><span class="p">.</span><span class="nf">Matches</span><span class="p">(</span><span class="n">line</span><span class="p">,</span> <span class="s">@"(?:^|,)(?:""(?&lt;val&gt;[^""]*)""|(?&lt;val&gt;[^,""]*))"</span><span class="p">)</span>
                <span class="p">.</span><span class="n">Cast</span><span class="p">&lt;</span><span class="n">Match</span><span class="p">&gt;()</span>
                <span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">m</span> <span class="p">=&gt;</span> <span class="n">m</span><span class="p">.</span><span class="n">Groups</span><span class="p">[</span><span class="s">"val"</span><span class="p">].</span><span class="n">Value</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">ToArray</span><span class="p">();</span>
<span class="p">}</span>

<span class="c1">// 리플렉션 기반 CSV 파싱 핵심 메서드</span>
<span class="k">public</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="n">parseCSV</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="kt">string</span> <span class="n">csvData</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">isCutColumn</span><span class="p">,</span> <span class="kt">int</span> <span class="n">start</span><span class="p">,</span> <span class="kt">int</span> <span class="n">end</span><span class="p">)</span> <span class="k">where</span> <span class="n">T</span> <span class="p">:</span> <span class="n">BaseData</span><span class="p">,</span> <span class="k">new</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// 1. 제네릭 타입 T의 구조 분석</span>
    <span class="n">Type</span> <span class="n">myType</span> <span class="p">=</span> <span class="k">typeof</span><span class="p">(</span><span class="n">T</span><span class="p">);</span>
    <span class="n">FieldInfo</span><span class="p">[]</span> <span class="n">myFieldInfo</span> <span class="p">=</span> <span class="n">myType</span><span class="p">.</span><span class="nf">GetFields</span><span class="p">(</span><span class="n">BindingFlags</span><span class="p">.</span><span class="n">NonPublic</span> <span class="p">|</span> <span class="n">BindingFlags</span><span class="p">.</span><span class="n">Instance</span> <span class="p">|</span> <span class="n">BindingFlags</span><span class="p">.</span><span class="n">Public</span> <span class="p">|</span> <span class="n">BindingFlags</span><span class="p">.</span><span class="n">Static</span><span class="p">);</span>

    <span class="kt">string</span><span class="p">[]</span> <span class="n">csvIndexData</span> <span class="p">=</span> <span class="n">csvData</span><span class="p">.</span><span class="nf">Split</span><span class="p">(</span><span class="sc">'\n'</span><span class="p">).</span><span class="nf">Where</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="n">x</span><span class="p">)).</span><span class="nf">ToArray</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">csvIndexData</span><span class="p">.</span><span class="n">Length</span> <span class="p">==</span> <span class="m">0</span><span class="p">)</span> <span class="k">return</span> <span class="k">null</span><span class="p">;</span>

    <span class="c1">// 2. 헤더 매핑 딕셔너리 세팅</span>
    <span class="kt">string</span><span class="p">[]</span> <span class="n">attributeColumnArray</span> <span class="p">=</span> <span class="n">csvIndexData</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="nf">Trim</span><span class="p">().</span><span class="nf">Split</span><span class="p">(</span><span class="sc">','</span><span class="p">);</span>
    <span class="nf">SetupFieldDict</span><span class="p">(</span><span class="n">attributeColumnArray</span><span class="p">,</span> <span class="n">myFieldInfo</span><span class="p">);</span>

    <span class="n">List</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="n">csvDataList</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;();</span>
    <span class="c1">// 3. 데이터 행 순회 및 파싱</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">rowIndex</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span> <span class="n">rowIndex</span> <span class="p">&lt;</span> <span class="n">csvIndexData</span><span class="p">.</span><span class="n">Length</span><span class="p">;</span> <span class="n">rowIndex</span><span class="p">++)</span>
    <span class="p">{</span>
        <span class="kt">string</span><span class="p">[]</span> <span class="n">column</span> <span class="p">=</span> <span class="nf">SmartSplit</span><span class="p">(</span><span class="n">csvIndexData</span><span class="p">[</span><span class="n">rowIndex</span><span class="p">].</span><span class="nf">Trim</span><span class="p">());</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">column</span><span class="p">.</span><span class="n">Length</span> <span class="p">==</span> <span class="m">0</span> <span class="p">||</span> <span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="n">column</span><span class="p">[</span><span class="m">0</span><span class="p">]))</span> <span class="k">continue</span><span class="p">;</span>

        <span class="n">T</span> <span class="n">objectData</span> <span class="p">=</span> <span class="n">Activator</span><span class="p">.</span><span class="n">CreateInstance</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;();</span>
        
        <span class="c1">// 4. 타입 변환 및 할당</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">column</span><span class="p">.</span><span class="n">Length</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
        <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">DataIndex_Dict</span><span class="p">.</span><span class="nf">TryGetValue</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="k">out</span> <span class="kt">string</span> <span class="n">attribute</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="n">Data_Dict</span><span class="p">.</span><span class="nf">TryGetValue</span><span class="p">(</span><span class="n">attribute</span><span class="p">,</span> <span class="k">out</span> <span class="kt">string</span> <span class="n">fieldName</span><span class="p">))</span>
            <span class="p">{</span>
                <span class="n">FieldInfo</span> <span class="n">fieldInfo</span> <span class="p">=</span> <span class="n">myType</span><span class="p">.</span><span class="nf">GetField</span><span class="p">(</span><span class="n">fieldName</span><span class="p">);</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">fieldInfo</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
                <span class="p">{</span>
                    <span class="nf">SetFieldValue</span><span class="p">(</span><span class="n">objectData</span><span class="p">,</span> <span class="n">fieldInfo</span><span class="p">,</span> <span class="n">column</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span> <span class="c1">// ChangeType 수행</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="n">csvDataList</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">objectData</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">csvDataList</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="2-비동기-로딩-및-ui-블로킹-방지">2. 비동기 로딩 및 UI 블로킹 방지</h2>

<p>방대한 CSV 데이터와 복잡한 맵 데이터를 메인 스레드에서 동기적으로 처리하면 화면 멈춤(Freezing) 현상이 발생합니다. 이를 방지하기 위해 <code class="language-plaintext highlighter-rouge">async/await</code> 기반의 <strong>비동기 로딩 및 맵 초기화 시스템</strong>을 구축했습니다.</p>

<h3 id="21-가중치-기반-로딩-시스템-loadingmanager">2.1 가중치 기반 로딩 시스템 (LoadingManager)</h3>

<p>데이터 다운로드, 파싱, 그리고 맵 생성의 진행 과정을 각 파트의 비중에 따라 가중치(Weight)를 두어 세밀하게 계산합니다. <code class="language-plaintext highlighter-rouge">Task.Yield()</code>를 활용하여 연산 도중 주기적으로 메인 렌더링 루프에 제어권을 양보함으로써 로딩 UI 애니메이션이 부드럽게 유지되도록 최적화했습니다.</p>

<p><strong>비동기 데이터 로딩 흐름</strong>
서버 데이터 다운로드부터 파싱 적재까지 하나의 비동기 파이프라인으로 관리합니다.</p>

<details class="pf-details">
  <summary>코드 보기: 비동기 데이터 로딩 흐름</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// LoadingManager.cs: 비동기 데이터 로딩 흐름</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">bool</span><span class="p">&gt;</span> <span class="nf">StartLoadingDataAsync</span><span class="p">(</span><span class="n">CancellationToken</span> <span class="n">cancellationToken</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nf">UpdateProgress</span><span class="p">(</span><span class="m">0.01f</span><span class="p">,</span> <span class="s">"서버 목록 확인 중..."</span><span class="p">);</span>

    <span class="c1">// 1. 메타 시트 비동기 다운로드</span>
    <span class="kt">bool</span> <span class="n">metaSuccess</span> <span class="p">=</span> <span class="k">await</span> <span class="n">ServerCSV_ConvertData</span><span class="p">.</span><span class="nf">DownloadMetaSheetAsync</span><span class="p">();</span>
    
    <span class="c1">// 2. 조건부 CSV 다운로드 (진행률 가중치 적용 5% ~ 80%)</span>
    <span class="kt">bool</span> <span class="n">csvSuccess</span> <span class="p">=</span> <span class="k">await</span> <span class="n">ServerCSV_ConvertData</span><span class="p">.</span><span class="nf">DownloadAllCSVAsync</span><span class="p">((</span><span class="n">progress</span><span class="p">,</span> <span class="n">fileName</span><span class="p">)</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="nf">UpdateProgress</span><span class="p">(</span><span class="m">0.05f</span> <span class="p">+</span> <span class="p">(</span><span class="n">progress</span> <span class="p">*</span> <span class="m">0.75f</span><span class="p">),</span> <span class="s">$"데이터 수신 중: </span><span class="p">{</span><span class="n">fileName</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="p">});</span>

    <span class="c1">// 3. 서버 데이터 로드 (DataManager 비동기 파싱)</span>
    <span class="nf">UpdateProgress</span><span class="p">(</span><span class="m">0.80f</span><span class="p">,</span> <span class="s">"데이터 로드 중..."</span><span class="p">);</span>
    <span class="k">await</span> <span class="n">Managers</span><span class="p">.</span><span class="n">Data</span><span class="p">.</span><span class="nf">ServerDataLoadAsync</span><span class="p">();</span>

    <span class="nf">UpdateProgress</span><span class="p">(</span><span class="m">1.0f</span><span class="p">,</span> <span class="s">"데이터 로딩 완료!"</span><span class="p">);</span>
    <span class="k">return</span> <span class="k">true</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="3-퀘스트-수락-진행-및-동적-보상-시스템">3. 퀘스트 수락, 진행 및 동적 보상 시스템</h2>

<p>구글 시트에서 파싱된 퀘스트 데이터 형식을 바탕으로, <code class="language-plaintext highlighter-rouge">QuestManager</code>가 상황에 맞는 퀘스트 객체를 팩토리 패턴(Handler)으로 생성합니다. 퀘스트 진행 상황은 이벤트 버스 시스템에 의해 완벽히 디커플링되어 처리됩니다.</p>

<h3 id="31-이벤트-구동형-퀘스트-진행-event-driven">3.1 이벤트 구동형 퀘스트 진행 (Event-Driven)</h3>

<p>몬스터가 처치될 때마다 발생하는 <code class="language-plaintext highlighter-rouge">OnMonsterKilled</code> 이벤트를 <code class="language-plaintext highlighter-rouge">QuestManager</code>가 구독합니다. 활성화된 퀘스트(ActiveQuestList)의 타겟 정보와 일치할 경우 진행도를 올리며, 목표 수치에 도달하면 퀘스트 완료 이벤트를 다시 발행합니다.</p>

<h3 id="32-시드seed-기반-동적-보상">3.2 시드(Seed) 기반 동적 보상</h3>

<p>퀘스트 레벨과 <code class="language-plaintext highlighter-rouge">Data_Resource</code> 베이스 값을 기준으로, 퀘스트 수락 시점의 난이도와 <code class="language-plaintext highlighter-rouge">GameSeed</code> 가중치를 곱하여 게임할 때마다 조금씩 달라지는 동적 보상(Gold, Exp)을 산출합니다.</p>

<p><strong>퀘스트 이벤트 및 보상 계산 로직</strong>
몬스터 처치 이벤트 구독과 시드 기반 보상 변동 로직입니다.</p>

<details class="pf-details">
  <summary>코드 보기: 퀘스트 이벤트 및 보상 계산 로직</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// QuestManager.cs: 몬스터 처치 이벤트 핸들링 및 진행도 체크</span>
<span class="k">private</span> <span class="k">void</span> <span class="nf">HandleMonsterKilled</span><span class="p">(</span><span class="kt">int</span> <span class="n">monsterId</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">key</span> <span class="p">=</span> <span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">Unit_Monster</span><span class="p">),</span> <span class="n">monsterId</span><span class="p">);</span>
    <span class="n">List</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="n">completedQuestIndices</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;();</span>

    <span class="k">foreach</span> <span class="p">(</span><span class="n">Quest</span> <span class="n">quest</span> <span class="k">in</span> <span class="n">ActiveQuestList</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nf">IsTargetQuest</span><span class="p">(</span><span class="n">quest</span><span class="p">,</span> <span class="n">key</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="c1">// 진행도 증가 및 UI 업데이트 이벤트 발행</span>
            <span class="nf">UpdateQuestProgress</span><span class="p">(</span><span class="n">quest</span><span class="p">);</span>

            <span class="k">if</span> <span class="p">(</span><span class="n">quest</span><span class="p">.</span><span class="nf">IsQuestCompleted</span><span class="p">())</span>
                <span class="n">completedQuestIndices</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nf">CompleteQuest</span><span class="p">(</span><span class="n">quest</span><span class="p">));</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">completedQuestIndices</span><span class="p">.</span><span class="n">Count</span> <span class="p">&gt;</span> <span class="m">0</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Managers</span><span class="p">.</span><span class="n">Event</span><span class="p">.</span><span class="nf">QuestCompleted</span><span class="p">(</span><span class="n">completedQuestIndices</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Quest.cs: 시드 기반 동적 보상 산출 (생성자 단계)</span>
<span class="c1">// GameSeed의 Reward 도메인을 사용하여 동일한 시드일 때 동일한 퀘스트 보상을 보장</span>
<span class="kt">float</span> <span class="n">RewardRatio</span> <span class="p">=</span> <span class="p">(</span><span class="kt">float</span><span class="p">)</span><span class="n">GameSeed</span><span class="p">.</span><span class="nf">NextInt</span><span class="p">(</span><span class="n">GameSeed</span><span class="p">.</span><span class="n">Domain</span><span class="p">.</span><span class="n">Reward</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">11</span><span class="p">)</span> <span class="p">/</span> <span class="m">10f</span><span class="p">;</span>

<span class="n">m_rewardGold</span> <span class="p">=</span> <span class="n">Mathf</span><span class="p">.</span><span class="nf">RoundToInt</span><span class="p">(</span><span class="n">Managers</span><span class="p">.</span><span class="n">Data</span><span class="p">.</span><span class="n">ResourceData</span><span class="p">[</span><span class="n">m_questLevel</span><span class="p">].</span><span class="n">questGold</span> <span class="p">*</span> <span class="p">((</span><span class="kt">float</span><span class="p">)</span><span class="n">ctx</span><span class="p">.</span><span class="n">difficultyTag</span> <span class="p">*</span> <span class="n">RewardRatio</span><span class="p">));</span>
<span class="n">m_rewardExp</span> <span class="p">=</span> <span class="n">Mathf</span><span class="p">.</span><span class="nf">RoundToInt</span><span class="p">(</span><span class="n">Managers</span><span class="p">.</span><span class="n">Data</span><span class="p">.</span><span class="n">ResourceData</span><span class="p">[</span><span class="n">m_questLevel</span><span class="p">].</span><span class="n">questExp</span> <span class="p">*</span> <span class="p">((</span><span class="kt">float</span><span class="p">)</span><span class="n">ctx</span><span class="p">.</span><span class="n">difficultyTag</span> <span class="p">*</span> <span class="p">(</span><span class="m">1f</span> <span class="p">-</span> <span class="n">RewardRatio</span><span class="p">)));</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="4-계층적-맵-생성-및-이동-아키텍처">4. 계층적 맵 생성 및 이동 아키텍처</h2>

<p>게임의 월드는 거대한 1개의 세계(Map) 안에 여러 지역(Region)이 있고, 지역 안에 수많은 전투 전장(Field)이 존재하는 <strong>Map -&gt; Region -&gt; Field</strong>의 계층적 구조를 가집니다.</p>

<h3 id="41-비동기-맵-초기화-및-구성">4.1 비동기 맵 초기화 및 구성</h3>
<p>대규모 맵 객체를 한 번에 생성하면 부하가 크기 때문에, <code class="language-plaintext highlighter-rouge">MapManager</code>의 <code class="language-plaintext highlighter-rouge">NewGame_InitMap</code> 메서드가 <code class="language-plaintext highlighter-rouge">async/await</code> 흐름을 타고 Map과 Region 내부의 퀘스트/필드를 순차적으로 생성합니다. 생성에 관여하는 모든 무작위 확률은 <code class="language-plaintext highlighter-rouge">GameSeed.Domain.Map</code>, <code class="language-plaintext highlighter-rouge">GameSeed.Domain.Region</code> 등 분리된 난수 스트림에 의해 결정론적(Deterministic)으로 구성됩니다.</p>

<h3 id="42-이벤트-기반-맵-이동-시스템">4.2 이벤트 기반 맵 이동 시스템</h3>
<p>유저가 UI를 통해 특정 Field를 선택하면, <code class="language-plaintext highlighter-rouge">Managers.Event.OnEnterFieldEvent</code>가 발생합니다. <code class="language-plaintext highlighter-rouge">MapManager</code>는 이를 수신하여 현재 전투 환경(Battle Manager 상태)을 업데이트하고 <code class="language-plaintext highlighter-rouge">BGRender</code> 컴포넌트를 이용해 배경 이미지를 교체하며 페이드인 효과를 발생시킵니다.</p>

<p><strong>비동기 맵 계층 초기화 로직</strong>
Map 객체 하위에 Region을 묶고, Region 내부에 퀘스트와 Field를 생성하여 인덱스로 연결합니다.</p>

<details class="pf-details">
  <summary>코드 보기: 비동기 맵 초기화 및 계층 구조 생성 로직</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MapManager.cs: 비동기 맵 초기화 진입점</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">bool</span><span class="p">&gt;</span> <span class="nf">NewGame_InitMap</span><span class="p">(</span><span class="n">Func</span><span class="p">&lt;</span><span class="kt">float</span><span class="p">,</span> <span class="kt">string</span><span class="p">,</span> <span class="n">Task</span><span class="p">&gt;</span> <span class="n">onProgress</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// 1. 최상위 월드 맵 생성 (레벨/개수 지정)</span>
    <span class="n">Map</span> <span class="n">map1</span> <span class="p">=</span> <span class="n">DataFactory</span><span class="p">.</span><span class="nf">MakeMap</span><span class="p">(</span><span class="m">1</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="n">m_mapParentObj</span><span class="p">.</span><span class="n">transform</span><span class="p">);</span>
    <span class="n">Map</span> <span class="n">map10</span> <span class="p">=</span> <span class="n">DataFactory</span><span class="p">.</span><span class="nf">MakeMap</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="n">m_mapParentObj</span><span class="p">.</span><span class="n">transform</span><span class="p">);</span>
    <span class="n">Map</span> <span class="n">map20</span> <span class="p">=</span> <span class="n">DataFactory</span><span class="p">.</span><span class="nf">MakeMap</span><span class="p">(</span><span class="m">20</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="n">m_mapParentObj</span><span class="p">.</span><span class="n">transform</span><span class="p">);</span>

    <span class="c1">// 2. 월드맵 하위에 할당된 모든 지역(Region)들에 대한 내부 필드/퀘스트 비동기 생성 </span>
    <span class="k">if</span> <span class="p">(</span><span class="n">WorldRegionList</span> <span class="p">!=</span> <span class="k">null</span> <span class="p">&amp;&amp;</span> <span class="n">WorldRegionList</span><span class="p">.</span><span class="n">Count</span> <span class="p">!=</span> <span class="m">0</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="n">Region</span> <span class="n">region</span> <span class="k">in</span> <span class="n">WorldRegionList</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="c1">// Region.GenerateRegion 안에서 Field 객체들이 생성되며 진행도 보고를 전달</span>
            <span class="k">await</span> <span class="n">region</span><span class="p">.</span><span class="nf">GenerateRegion</span><span class="p">(</span><span class="n">onProgress</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="n">IsLoadedData</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
    <span class="k">return</span> <span class="k">true</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Region.cs: 지역 내 필드 및 퀘스트 생성</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">GenerateRegion</span><span class="p">(</span><span class="n">Func</span><span class="p">&lt;</span><span class="kt">float</span><span class="p">,</span> <span class="kt">string</span><span class="p">,</span> <span class="n">Task</span><span class="p">&gt;</span> <span class="n">onProgress</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">int</span> <span class="n">questCount</span> <span class="p">=</span> <span class="n">GameSeed</span><span class="p">.</span><span class="nf">NextInt</span><span class="p">(</span><span class="n">GameSeed</span><span class="p">.</span><span class="n">Domain</span><span class="p">.</span><span class="n">Region</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="m">7</span><span class="p">);</span>
    <span class="n">MonsterType</span> <span class="n">monsterType</span> <span class="p">=</span> <span class="n">GameSeed</span><span class="p">.</span><span class="n">RandomEnum</span><span class="p">&lt;</span><span class="n">MonsterType</span><span class="p">&gt;(</span><span class="n">GameSeed</span><span class="p">.</span><span class="n">Domain</span><span class="p">.</span><span class="n">Region</span><span class="p">);</span>
    <span class="n">monsterSet</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MonsterSet</span><span class="p">(</span><span class="n">monsterType</span><span class="p">);</span>
    
    <span class="c1">// ... 생략 ...</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">questCount</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
    <span class="p">{</span>
        <span class="n">Quest</span> <span class="n">mainQuest</span> <span class="p">=</span> <span class="n">Managers</span><span class="p">.</span><span class="n">Quest</span><span class="p">.</span><span class="nf">GenerateQuest</span><span class="p">(</span><span class="n">monsterSet</span><span class="p">,</span> <span class="k">ref</span> <span class="n">currentQuestLevel</span><span class="p">,</span> <span class="n">questDataList</span><span class="p">,</span> <span class="n">questDialogList</span><span class="p">);</span>
        <span class="kt">int</span> <span class="n">questIndex</span> <span class="p">=</span> <span class="n">Managers</span><span class="p">.</span><span class="n">Quest</span><span class="p">.</span><span class="n">MainQuestList</span><span class="p">.</span><span class="nf">IndexOf</span><span class="p">(</span><span class="n">mainQuest</span><span class="p">);</span>

        <span class="c1">// Field 객체를 생성하고, 방금 생성된 메인 퀘스트의 인덱스를 넘겨 결합시킴</span>
        <span class="n">Field</span> <span class="n">field</span> <span class="p">=</span> <span class="n">DataFactory</span><span class="p">.</span><span class="nf">MakeField</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">questIndex</span><span class="p">,</span> <span class="n">transform</span><span class="p">);</span>
        
        <span class="c1">// ... (인덱스 저장 관리 생략)</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">onProgress</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
            <span class="k">await</span> <span class="nf">onProgress</span><span class="p">((</span><span class="kt">float</span><span class="p">)(</span><span class="n">i</span> <span class="p">+</span> <span class="m">1</span><span class="p">)</span> <span class="p">/</span> <span class="n">questCount</span><span class="p">,</span> <span class="s">$"지역 생성 중..."</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<hr />

<h2 class="chapter-title" id="5-seed--token-시스템-rng-완전-복원">5. Seed / Token 시스템 (RNG 완전 복원)</h2>

<p>게임을 세이브하고 로드했을 때, 단순히 맵과 캐릭터의 상태만을 저장하는 것을 넘어 생성 규칙(시드) 자체를 완벽히 롤백해야 합니다. 초기 시드값만 저장할 경우 게임 도중 이미 소모된 난수 횟수로 인해 세이브 로드 시점에 다음 생성되는 아이템이나 맵이 달라지는 문제가 발생합니다. 이를 극복하기 위한 <strong>Seed/Token 복원 시스템</strong>입니다.</p>

<h3 id="51-도메인-격리-구조-domain-isolation">5.1 도메인 격리 구조 (Domain Isolation)</h3>

<p><code class="language-plaintext highlighter-rouge">System.Random</code> 객체를 하나만 사용하면, 퀘스트 생성 시 난수를 소모한 횟수에 따라 맵 생성 결과가 뒤틀리게 됩니다. 이를 방지하기 위해 <code class="language-plaintext highlighter-rouge">GameSeed</code>는 <code class="language-plaintext highlighter-rouge">Domain</code>(Map, Quest, Field, Reward 등)을 분리하여 각각 독립된 Random 객체 스트림을 보유합니다.</p>

<h3 id="52-고정-폭-인코딩-기반-토큰-다이어그램">5.2 고정 폭 인코딩 기반 토큰 다이어그램</h3>

<p>모든 도메인의 난수 사용 횟수(Usage Count)를 하나의 암호화된 토큰(Token)으로 병합합니다. 파싱 시 데이터가 꼬이지 않도록 <strong>고정 폭(Fixed-Width) 인코딩</strong>을 사용했습니다.</p>

<div class="pf-visual-frame">
    <div style="font-weight: 700; margin-bottom: 20px; color: #007bff; font-family: 'Fira Code', monospace;">Seed Token 구조 (Fixed-Width)</div>
    <div class="pf-token-container">
        <div class="pf-token-row">
            <div class="pf-token-box">
                <div class="pf-token-box-title">BaseSeed</div>
                <div style="font-size: 0.85rem;">20자리 고정 (ulong 패딩)</div>
            </div>
            <div class="flow-arrow" style="text-align: center;">+</div>
            <div class="pf-token-box">
                <div class="pf-token-box-title">Domain 개수</div>
                <div style="font-size: 0.85rem;">3자리 고정 (예: 008)</div>
            </div>
        </div>
        <div class="pf-domain-info">
            <div style="color: #28a745; font-weight: 700; margin-bottom: 10px; font-family: 'Fira Code', monospace;">Domain 정보 블록 (등록된 도메인 개수만큼 반복 병합)</div>
            <div class="pf-domain-grid">
                <div class="pf-domain-item">
                    <strong style="color: #28a745;">Domain ID</strong><br />
                    <span style="font-size: 0.85rem; color: #666;">(3자리 패딩)</span>
                </div>
                <div class="pf-domain-item">
                    <strong style="color: #28a745;">Usage Count</strong><br />
                    <span style="font-size: 0.85rem; color: #666;">(12자리 패딩)</span>
                </div>
            </div>
        </div>
    </div>
</div>

<h3 id="53-토큰-빌드-및-상태-복원-흐름">5.3 토큰 빌드 및 상태 복원 흐름</h3>

<p>저장 시 위 규칙대로 텍스트 토큰을 빌드하여 저장하고, 로드할 때는 이 문자열을 길이 단위로 잘라낸 뒤 각 도메인의 Random 객체를 생성하고 사용 횟수만큼 <code class="language-plaintext highlighter-rouge">Next()</code>를 더미 호출(Skip) 시켜 게임의 미래 확률 상태를 정확히 세이브 시점과 동일하게 동기화합니다.</p>

<p><strong>토큰 인코딩 및 RNG 상태 강제 복구 로직</strong>
고정 폭 문자열을 결합하여 저장하고, 로드시 추출하여 각 도메인의 스트림을 동기화합니다.</p>

<details class="pf-details">
  <summary>코드 보기: 토큰 인코딩 및 RNG 상태 복구 로직</summary>

  <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SaveLoadManager.cs: 시드와 도메인 사용량을 하나의 고정 폭 토큰으로 결합</span>
<span class="k">private</span> <span class="k">static</span> <span class="kt">string</span> <span class="nf">BuildSeedToken</span><span class="p">(</span><span class="kt">long</span> <span class="n">baseSeed</span><span class="p">,</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="n">GameSeed</span><span class="p">.</span><span class="n">Domain</span><span class="p">,</span> <span class="kt">ulong</span><span class="p">&gt;</span> <span class="n">usage</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">ulong</span> <span class="n">encodedSeed</span> <span class="p">=</span> <span class="k">unchecked</span><span class="p">((</span><span class="kt">ulong</span><span class="p">)</span><span class="n">baseSeed</span><span class="p">);</span>
    <span class="kt">string</span> <span class="n">token</span> <span class="p">=</span> <span class="nf">Pad</span><span class="p">(</span><span class="n">encodedSeed</span><span class="p">,</span> <span class="m">20</span><span class="p">);</span> <span class="c1">// 20자리 패딩</span>

    <span class="n">List</span><span class="p">&lt;</span><span class="n">GameSeed</span><span class="p">.</span><span class="n">Domain</span><span class="p">&gt;</span> <span class="n">domains</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">GameSeed</span><span class="p">.</span><span class="n">Domain</span><span class="p">&gt;((</span><span class="n">GameSeed</span><span class="p">.</span><span class="n">Domain</span><span class="p">[])</span><span class="n">Enum</span><span class="p">.</span><span class="nf">GetValues</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">GameSeed</span><span class="p">.</span><span class="n">Domain</span><span class="p">)));</span>
    <span class="n">domains</span><span class="p">.</span><span class="nf">Sort</span><span class="p">((</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">((</span><span class="kt">uint</span><span class="p">)</span><span class="n">a</span><span class="p">).</span><span class="nf">CompareTo</span><span class="p">((</span><span class="kt">uint</span><span class="p">)</span><span class="n">b</span><span class="p">));</span>

    <span class="n">token</span> <span class="p">+=</span> <span class="nf">Pad</span><span class="p">((</span><span class="kt">uint</span><span class="p">)</span><span class="n">domains</span><span class="p">.</span><span class="n">Count</span><span class="p">,</span> <span class="m">3</span><span class="p">);</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">d</span> <span class="k">in</span> <span class="n">domains</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">ulong</span> <span class="n">count</span> <span class="p">=</span> <span class="n">usage</span> <span class="p">!=</span> <span class="k">null</span> <span class="p">?</span> <span class="n">usage</span><span class="p">.</span><span class="nf">GetValueOrDefault</span><span class="p">(</span><span class="n">d</span><span class="p">,</span> <span class="m">0U</span><span class="n">L</span><span class="p">)</span> <span class="p">:</span> <span class="m">0U</span><span class="n">L</span><span class="p">;</span>
        <span class="n">token</span> <span class="p">+=</span> <span class="nf">Pad</span><span class="p">((</span><span class="kt">uint</span><span class="p">)</span><span class="n">d</span><span class="p">,</span> <span class="m">3</span><span class="p">);</span>    <span class="c1">// 도메인 ID 3자리</span>
        <span class="n">token</span> <span class="p">+=</span> <span class="nf">Pad</span><span class="p">(</span><span class="n">count</span><span class="p">,</span> <span class="m">12</span><span class="p">);</span>     <span class="c1">// 사용량 12자리</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">token</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// GameSeed.cs: 토큰에서 추출된 정보로 RNG 엔진을 과거 상태로 복원</span>
<span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="nf">RestoreRNG</span><span class="p">(</span><span class="kt">long</span> <span class="n">baseSeed</span><span class="p">,</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="n">Domain</span><span class="p">,</span> <span class="kt">ulong</span><span class="p">&gt;</span> <span class="n">usageCounts</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">kv</span> <span class="k">in</span> <span class="n">usageCounts</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Domain</span> <span class="n">d</span> <span class="p">=</span> <span class="n">kv</span><span class="p">.</span><span class="n">Key</span><span class="p">;</span>
        <span class="kt">ulong</span> <span class="n">count</span> <span class="p">=</span> <span class="n">kv</span><span class="p">.</span><span class="n">Value</span><span class="p">;</span>     
        
        <span class="c1">// BaseSeed를 해싱(Salt)하여 도메인별 고유 서브 시드로 RNG 재생성</span>
        <span class="n">System</span><span class="p">.</span><span class="n">Random</span> <span class="n">rng</span> <span class="p">=</span> <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="nf">Random</span><span class="p">(</span><span class="nf">DeriveSubSeed</span><span class="p">(</span><span class="n">baseSeed</span><span class="p">,</span> <span class="p">(</span><span class="kt">uint</span><span class="p">)</span><span class="n">d</span><span class="p">));</span>
        
        <span class="c1">// 토큰에 기록된 횟수만큼 난수를 버림(Skip) 호출하여 확률 스트림 위치를 강제 동기화</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">ulong</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">count</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
        <span class="p">{</span>
            <span class="n">rng</span><span class="p">.</span><span class="nf">Next</span><span class="p">();</span>     
        <span class="p">}</span>
        
        <span class="n">domainRng</span><span class="p">[</span><span class="n">d</span><span class="p">]</span> <span class="p">=</span> <span class="n">rng</span><span class="p">;</span>
        <span class="n">domainUsageCounts</span><span class="p">[</span><span class="n">d</span><span class="p">]</span> <span class="p">=</span> <span class="n">count</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
</details>

<p class="notice--success"><strong>Conclusion:</strong> <code class="language-plaintext highlighter-rouge">WorldFirstKill</code>은 비동기 파싱과 계층적 맵 생성으로 방대한 데이터 로딩 문제를 최적화했으며, Seed/Token 시스템을 통해 로그라이크 특유의 ‘완벽한 게임 플레이(RNG) 복원력’을 기술적으로 완성한 수준 높은 아키텍처를 자랑합니다.</p>]]></content><author><name>이민혁 (Minhyuk Lee)</name></author><category term="project" /><category term="Data Structure" /><category term="Server" /><category term="C#" /><category term="System Design" /><summary type="html"><![CDATA[리플렉션 기반 동적 파싱 및 완전한 상태 복원 시스템]]></summary></entry></feed>