<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>power_overwhelming</title>
        <link>https://velog.io/</link>
        <description>이것 저것 다해보는 삶</description>
        <lastBuildDate>Tue, 26 May 2026 10:06:56 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>power_overwhelming</title>
            <url>https://images.velog.io/images/bak_chun8/profile/fb237a59-9e1d-4b15-8aec-77cf4716f727/8300.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. power_overwhelming. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/bak_chun8" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[TIL: C++ TextRPG 팀플 36일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-C-TextRPG-%ED%8C%80%ED%94%8C-36%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-C-TextRPG-%ED%8C%80%ED%94%8C-36%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 26 May 2026 10:06:56 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--370시간-34분-">*<em>누적 학습 시간 : 370시간 34분 *</em></h2>
<h4 id="📅-2026-05-22">📅 2026-05-22</h4>
<p>개인적으로 캠프에서 만난 다양한 분들과 튜터님 매니저님들 너무 다 좋다.
커리큘럼도 솔직히 만족스러웠다.
그런데 아예 개발과 연이 없던 분들도 오셔서 같이 수업받기 때문에 Git사용법이나 협업 관련된 수업을 2시간하고 끝내는게 아니라 더 많은 시간을 투자해주시는게 좋을 것 같다.
Git이라는게 원래 사용법도 막연해서 어려운데 갑자기 협업이라는게 하루아침에 뚝딱 되는것도 아니고 많이 난감했다.</p>
<h1 id="팀협업으로-textrpg-만들기">팀(협업)으로 TextRPG 만들기</h1>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/ba4061e2-5686-47c1-811c-91aa390ba5ba/image.png" alt=""></p>
<p>대략적으로 이런 형태로 만들기로 했다.
내부 함수를 어떤식으로 만드는지는 유관끼리 얘기하기로 했고 나는 LoggingManager를 담당하게 됐다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 사이드프로젝트 회고 35일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-34%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-34%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 21 May 2026 05:16:00 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--358시간-34분-">*<em>누적 학습 시간 : 358시간 34분 *</em></h2>
<h4 id="📅-2026-05-21">📅 2026-05-21</h4>
<p>프로젝트가 끝났다.</p>
<h3 id="프로젝트-진행기간--260511--260521">프로젝트 진행기간 : 26.05.11 ~ 26.05.21</h3>
<p>주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 <a href="https://github.com/NBC-SideProject/Vampire-Survival">레포지토리 참조</a>
-&gt; 일신상의 이유로 Perforce로 변경</p>
<p>MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.</p>
<h1 id="프로젝트-종료-회고록">프로젝트 종료 회고록</h1>
<h2 id="1-프로젝트-개요">1. 프로젝트 개요</h2>
<ul>
<li>프로젝트명: 뱀파이어 서바이벌 류 게임 트레이싱</li>
<li>진행 기간:  26.05.11 ~ 26.05.21</li>
<li>팀 인원: 송용수, 최배성, 양석현, 정규상, 이정준, 임우현</li>
</ul>
<hr>
<h2 id="2-프로젝트-목표">2. 프로젝트 목표</h2>
<h3 id="목표">목표</h3>
<ul>
<li>규상 : 언리얼 C++ 문법익숙해지기</li>
<li>용수 : 유니티 UI를 언리얼로 포팅 &gt; MVP패턴 이용해서 모델/뷰 완벽하게 분리</li>
<li>배성 : 흘러가는 게임을 구성해보자. + 협업 스킬 레벨업</li>
<li>석현 : 협업 경험 + 언리얼 학습</li>
<li>정준 : 협업 경험 + 언리얼 학습</li>
<li>우현 : 협업 경험 + 언리얼, 게임개발과정 학습</li>
</ul>
<h3 id="주요-기능">주요 기능</h3>
<p>용수</p>
<ul>
<li>체력바</li>
<li>경험치바 material 애니메이션 구현</li>
<li>경험치/데미지 위젯구현 &gt; 풀링으로 화면에 띄우는 것</li>
<li>UIManager 경유 , 모델과 뷰 바인딩 자동화</li>
</ul>
<p>배성</p>
<ul>
<li>무기, 총알 등의 BaseClass 선언 &gt; 확장성 확보 고려</li>
<li>컴포넌트를 사용해서 기능분리</li>
</ul>
<p>석현</p>
<ul>
<li>몬스터 class 구현하면서 확장성 고려</li>
</ul>
<p>정준</p>
<ul>
<li>플레이어 보유 장비 상태 관리</li>
<li>소유자 Actor &gt; Enemy가 특정 조건 만족 시 아이템 드롭</li>
</ul>
<p>규상</p>
<ul>
<li>PoolingManager 기반으로 액터 생성하는 기법</li>
<li>전체적인 Gamemode를 어떻게 사용해야하는지</li>
<li>협업할때 어떻게 해야하는지 (게임) 구체화</li>
</ul>
<p>우현</p>
<ul>
<li>캐릭터 MovementComponent C++로 구현</li>
<li>카메라arm 없이 달기 &gt; arm 없으면 캐릭터 따라가기를 따로 구현해야함</li>
<li>8방향 BS 구현 &gt; ABP 바인딩</li>
</ul>
<h3 id="사용-기술-스택">사용 기술 스택</h3>
<ul>
<li>Engine: UE 5.5.4 (강의 디폴트여서) / 5.7 컴파일 오래걸려서</li>
<li>Language: C++, BP</li>
<li>Version Control: Github -&gt; Perforce\</li>
</ul>
<hr>
<h1 id="3-프로젝트-결과">3. 프로젝트 결과</h1>
<h2 id="미완료-또는-축소된-사항">미완료 또는 축소된 사항</h2>
<p>용수</p>
<ul>
<li>완전자동화 실패, 프레젠터 하드코딩 &gt; 완벽한 MVP 패턴 구현 실패</li>
<li>언리얼 CommonUI+MVVM 같이 사용해야 시너지 있음 &gt; 팀원들에게 MVVM 패턴 전파 해야함(커뮤니케이션 코스트 있어서 진행X)</li>
</ul>
<p>배성</p>
<ul>
<li>AttackBaseComponent 현재 사양에서 필요없는데 포함되어있음</li>
<li>ObjectPooling 적용X (둥근검, 짧은칼 지원안함) </li>
</ul>
<p>석현</p>
<ul>
<li>SetActorLocation, MovementComponent 사용안함 &gt; 확장에 어려움 발생</li>
<li>EnemyClass를 확장성있게 만들지 못했다(상태머신 도입을 못했다.)</li>
</ul>
<p>정준</p>
<ul>
<li>패시브 장비 구현 못함</li>
<li>드랍 &gt; Pooling 미적용 성능저하</li>
</ul>
<p>규상</p>
<ul>
<li>비동기 풀링 최적화 &gt; Tick으로 하는게 더 좋다라는 ChatGPT의 의견 (왜인지는 모르나 .. 나중에 알아봐야함)</li>
<li>풀링매니저 &gt; AActor말고 UObject로 구현했어야했다.
팀원들이 풀링인터페이스 적용하지 않은 이유</li>
<li>프리웜 타이밍 잡기가 애매했다. &gt; EquipComponet 구현상 프리웜 하기가 어려웠다.</li>
<li>이미 아이템을 다 개발하고 나서 PoolingManager 개발이 완료되어서 적용하려면 리팩토링이 필요했음(소통문제)</li>
<li>UI 풀링 따로 구현 &gt; PoolingManger가 AActor Class를 부모로해서 못씀 &gt; UI들은 WidgetClass를 부모로 해서 사용불가</li>
</ul>
<p>우현</p>
<ul>
<li>구현을 더 많이 했으면 좋았을텐데 범위가 좁았다.</li>
</ul>
<hr>
<h1 id="4-스프린트-운영-회고">4. 스프린트 운영 회고</h1>
<h2 id="잘된-점">잘된 점</h2>
<p>용수</p>
<ul>
<li>팀원들이 UI신경안쓰고 Delegate만 달아서 개발 가능하도록 하고 싶었고 어느정도 목표 달성</li>
<li>UI Widget 디자이너탭 사용에 익숙해짐.</li>
</ul>
<p>배성</p>
<ul>
<li>무기 관련 클래스 확장성 잘 나눠서 구현이 쉽게 되어있음</li>
<li>급하게 추가한 soundplayersubsystem 블루프린트랑 사용하는 구조가 좋은듯</li>
</ul>
<p>석현</p>
<ul>
<li>위치고정을 사용한 넉백과 같은 힘을 주는 시스템 구현</li>
</ul>
<p>정준</p>
<ul>
<li>Class별 Pool 분리된 것 사용하기 편함</li>
<li>반복 생성 객체 재사용 가능</li>
</ul>
<p>규상</p>
<ul>
<li>Pooling 생각보다 잘 작동</li>
<li>협업시스템 인프라 구축 잘함</li>
</ul>
<p>우현</p>
<ul>
<li>팀원들이랑 친해져서 좋았따.</li>
<li>게임개발흐름을 직접적으로 볼수 있어 좋은 경험이였다 생각된다.</li>
</ul>
<hr>
<h2 id="아쉬웠던-점">아쉬웠던 점</h2>
<p>용수</p>
<ul>
<li>언리얼 문법에 익숙하지 않아서 GC관련 디버깅에 문제가있었다. &gt; 앞으로 언리얼C++ 리플렉션 시스템을 더 공부해야겠다.</li>
<li>Delegate 필요 명세를 더 구체화하고 작업할 필요가 있었다. (기획문제)</li>
<li>UI는 결국 BP 작업 필요 &gt; BP로 작업해야할 것을 CPP로 작업한 부분들이 있음 &gt; 익숙치 않아서 분리가 어려웟다.</li>
</ul>
<p>배성</p>
<ul>
<li>무기 설계 시 내부기능을 알아왔다면 설계를 더 잘할 수 있었을 것.</li>
<li>설계 후 구현에 시간이 과투자됐다. (설계가 잘 되어있었다면 더 빠르게 개발 가능했을 것)</li>
<li>리소스 에셋을 미리 디깅했었다면 역시 구현 속도가 빨랏을 것. &gt; 트레이드오프가 잘 안됐다.</li>
</ul>
<p>석현</p>
<ul>
<li>더 많은 부분을 맡았으면 밀도있게 공부가 됐을 것 같은데 생각보다 시간이 많이 남았다.</li>
</ul>
<p>정준</p>
<ul>
<li>플레이어 스킬까지 구현인줄 알았는데 범위가 좁았음. &gt; 아이템 틀만 구현하니 작업량이 부족했다.</li>
<li>Delegate를 필요한곳에서 Broadcast했어야했는데 애매하게 했다.</li>
<li>패시브 아이템 미구현(시간부족)</li>
<li>작업량이 1/n이 안됐다.</li>
</ul>
<p>규상</p>
<ul>
<li>spawner &gt; 한번 만들어 봤어서 생각보다 구현 속도가 빨라서 이후 할게 많이 없었다.</li>
<li>pooling &gt; 개념이라 이해 하고 나서는 너무 쉬워서 구현/수정도 빨랐다. &gt; 진짜 할게 많이 없었던 것 같다.</li>
<li>로딩화면 굳이라고 생각했지만 그래도 한번 해봤으면 좋았을 것 같다.</li>
</ul>
<hr>
<h1 id="5-팀-협업-회고">5. 팀 협업 회고</h1>
<h2 id="좋았던-협업-방식">좋았던 협업 방식</h2>
<ul>
<li>개발 전 업무영역 분배하고 시작한 부분 좋았다.</li>
<li>PR 코드리뷰 하는 것 좋았다.</li>
<li>다같이 마이크 키고 화면공유 상시로 되어있어서 커뮤니케이션에 도움이 많이 됐다.</li>
<li>브랜치 전략을 생각보다 잘 지켜주셨다.</li>
</ul>
<hr>
<h2 id="개선이-필요한-부분">개선이 필요한 부분</h2>
<ul>
<li>업무 분배 후 분배된 업무 영역이라는 울타리 때문에 조금더 적극적이지 못했던 것 같다. &gt; 필요하다면 더 할 수 있었을 부분들이 확실히 있다.</li>
<li>작업 분배 시 1/n이었다면 좋았을 것 같다. -&gt; 인원 설정 또한 기획 단계에서 정해졌어야 했는데 아쉽다.</li>
<li>Perforce 작업/사용 전략을 좀 잡고가면 좋을것같다.</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 사이드프로젝트 34일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-33%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-33%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 20 May 2026 13:37:37 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--346시간-34분-">*<em>누적 학습 시간 : 346시간 34분 *</em></h2>
<h4 id="📅-2026-05-20">📅 2026-05-20</h4>
<p>무한성이라고 불리던 스테이지를 일찍 탈출한 6명이서 사이드 프로젝트를 진행하기로 했다.
순전히 기능 공부를 위함이고 출시계획같은건 당연하게도 없다.</p>
<h3 id="프로젝트-진행기간--260511--260521">프로젝트 진행기간 : 26.05.11 ~ 26.05.21</h3>
<p>주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 <a href="https://github.com/NBC-SideProject/Vampire-Survival">레포지토리 참조</a>
-&gt; 일신상의 이유로 Perforce로 변경</p>
<p>MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.</p>
<h1 id="poolingmanager-최적화">PoolingManager 최적화</h1>
<h2 id="기존구현"><a href="https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-30%EC%9D%BC%EC%B0%A8">기존구현</a></h2>
<p>기존에는 액터 300개를 미리 생성해서 Pool에 넣어두고 가져왔다가 넣었다가 하는 식이었는데</p>
<p>로딩화면으로 최적화 할까 고민하다가
Tick에서 3초에 20개씩 생성 &gt; 300개되면 스탑 되는 형식으로 최적화했다.</p>
<p>실제로 게임 시작 후 world time 00:01에서 잠깐 스로틀링 걸리던 문제가 해결됐다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 사이드프로젝트 33일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-32%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-32%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 19 May 2026 11:50:48 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--334시간-34분-">*<em>누적 학습 시간 : 334시간 34분 *</em></h2>
<h4 id="📅-2026-05-19">📅 2026-05-19</h4>
<p>무한성이라고 불리던 스테이지를 일찍 탈출한 6명이서 사이드 프로젝트를 진행하기로 했다.
순전히 기능 공부를 위함이고 출시계획같은건 당연하게도 없다.</p>
<h3 id="프로젝트-진행기간--260511--260521">프로젝트 진행기간 : 26.05.11 ~ 26.05.21</h3>
<p>주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 <a href="https://github.com/NBC-SideProject/Vampire-Survival">레포지토리 참조</a>
-&gt; 일신상의 이유로 Perforce로 변경</p>
<p>MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.</p>
<h1 id="마무리-작업">마무리 작업</h1>
<p>오늘은 하루종일 게임 내 벨런싱이나 스프라이트 찍거나 게임모드 내에서 설정들을 미세 수정하는 작업들을 진행했다.</p>
<p>내일은 아마 풀링매니저 + 로딩화면을 통해 최적화를 진행해보려고한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 사이드프로젝트 32일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-31%EC%9D%BC%EC%B0%A8-hmqm6t26</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-31%EC%9D%BC%EC%B0%A8-hmqm6t26</guid>
            <pubDate>Mon, 18 May 2026 12:41:32 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--320시간-34분-">*<em>누적 학습 시간 : 320시간 34분 *</em></h2>
<h4 id="📅-2026-05-18">📅 2026-05-18</h4>
<p>무한성이라고 불리던 스테이지를 일찍 탈출한 6명이서 사이드 프로젝트를 진행하기로 했다.
순전히 기능 공부를 위함이고 출시계획같은건 당연하게도 없다.</p>
<h3 id="프로젝트-진행기간--260511--260521">프로젝트 진행기간 : 26.05.11 ~ 26.05.21</h3>
<p>주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 <a href="https://github.com/NBC-SideProject/Vampire-Survival">레포지토리 참조</a>
-&gt; 일신상의 이유로 Perforce로 변경</p>
<p>MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.</p>
<h1 id="github---perforce-마이그레이션">Github -&gt; Perforce 마이그레이션</h1>
<p>계획 중 하나였던 9기의 모든 프로젝트를 하나의 Organization으로 관리하고 각 다른 팀의 PR과 코드를 볼 수 있게 하고싶었는데 LFS 대역폭이 너무 적어서 어떤 대체제가 있을지 찾던 중
Perforce 라는 걸 알게됐다.</p>
<p>Perforce는 Git과 비슷한 형상관리툴인데 다른점은 Github같이 플랫폼이 있어서 거기서 관리하는게 아니라 개인 서버 혹은 클라우드 저장소를 연결해서 사용한다.</p>
<h2 id="github">Github</h2>
<h3 id="장점">장점</h3>
<ul>
<li>Branch 전략과 Workflow를 통해 코드 diff와 수정, 배포, 버저닝이 매우 간편</li>
<li>상대적으로 대중적이라(Perforce에 비해) 적응하기 쉬움</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>언리얼에셋(uasset, umap 등) 수정 시 diff 감지를 못하고 최후에 merge된 파일로 덮어씀</li>
<li>자체 플랫폼의 클라우드 이용하기 때문에 결제 안하면 lfs 대역폭등 제한 사항이 많음</li>
</ul>
<h2 id="perforce">Perforce</h2>
<h3 id="장점-1">장점</h3>
<ul>
<li>바이너리 그대로 업로드 가능</li>
<li>바이너리 에셋 수정 시 checkin/out 기능 제공
checkin시 Perfoce에서 파일을 Readonly로 바꿔서 작업하는 동안 다른 사람의 수정을 막을 수 있음 (기존 Github는 바이너리 에셋의 수정사항을 인식하지 못하고 가장 나중에 merge된 파일을 기준으로 덮어씀)</li>
<li>Get Latest(pull) / Submit(push) 속도가 매우 빠름</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>클라우드 서버 설정에 대한 진입장벽
남는 pc나 노트북이 있다면 하루종일 켜놔야하고 서버 초기 설정에 대한 진입장벽 있음
난 웹개발자였어서 쉽게 했지만 다른 사람들은... 잘 모르겠음</li>
<li>사용자 경험이 좋지않음
UI가 상상 이상으로 좋지않음 아래 이미지를 첨부할테지만 정말 각오해야함</li>
<li>Git처럼 branch를 통해서 세세하게 관리하는 것이 아니라 이게 정말 관리가 될까? 싶음
이 또한 UI/UX가 좋지않아서 그렇다고 생각함.</li>
</ul>
<p>Perforce 사용법에 대해서는 나중에 다시한번 작성할 예정</p>
<h1 id="gamemode-승리조건-추가">Gamemode 승리조건 추가</h1>
<pre><code class="language-cpp">#include &quot;Gamemode/VampireSurvivalGamemode.h&quot;

#include &quot;EngineUtils.h&quot;
#include &quot;DataAsset/RunConfigDataAsset.h&quot;
#include &quot;DataAsset/WaveDataAsset.h&quot;
#include &quot;Kismet/GameplayStatics.h&quot;
#include &quot;Spawner/EnemySpawner.h&quot;

AVampireSurvivalGamemode::AVampireSurvivalGamemode()
{
}

void AVampireSurvivalGamemode::BeginPlay()
{
    Super::BeginPlay();
    AWorldSettings* WorldSettings = GetWorldSettings();

    if (bUseDebugTimeAcceleration)
    {
        WorldSettings-&gt;SetTimeDilation(30.0f);
    }

    for (TActorIterator&lt;AEnemySpawner&gt; It(GetWorld()); It; ++It)
    {
        EnemySpawner = *It;
        break;
    }

    StartRun();
}

void AVampireSurvivalGamemode::StartRun()
{
    if (!RunConfig)
    {
        UE_LOG(LogTemp, Warning, TEXT(&quot;RunConfig is not set.&quot;));
        return;
    }

    //Debug, Production duration set 및 판단
    RunDurationSeconds = bUseDebugDuration
                             ? RunConfig-&gt;DebugDurationSeconds
                             : RunConfig-&gt;ProductionDurationSeconds;

    ElapsedTime = 0.f;
    CurrentWave = nullptr;
    CurrentWaveIndex = INDEX_NONE;

    SetCurrentWave(FindWaveForElapsedTime(ElapsedTime));

    GetWorldTimerManager().SetTimer(
        RunTimerHandler,
        this,
        &amp;AVampireSurvivalGamemode::UpdateRunTime,
        1.f,
        true
    );
}

void AVampireSurvivalGamemode::UpdateRunTime()
{
    ElapsedTime += 1.f;
    OnRunElapsedTimeChanged.Broadcast(ElapsedTime);

    if (ElapsedTime &gt;= RunDurationSeconds)
    {
        EndRun();
        return;
    }

    SetCurrentWave(FindWaveForElapsedTime(ElapsedTime));
}

void AVampireSurvivalGamemode::EndRun()
{
    GetWorldTimerManager().ClearTimer(RunTimerHandler);

    if (EnemySpawner)
    {
        EnemySpawner-&gt;StopSpawning();
    }

    // 게임 종료
    SetGameCompletedState();

    UE_LOG(LogTemp, Warning, TEXT(&quot;Run ended.&quot;));
}

UWaveDataAsset* AVampireSurvivalGamemode::FindWaveForElapsedTime(float InElapsedTime) const
{
    if (!RunConfig)
    {
        return nullptr;
    }

    for (UWaveDataAsset* Wave : RunConfig-&gt;Waves)
    {
        if (!Wave)
        {
            continue;
        }

        if (InElapsedTime &gt;= Wave-&gt;StartTime &amp;&amp; InElapsedTime &lt; Wave-&gt;EndTime)
        {
            return Wave;
        }
    }

    return nullptr;
}

void AVampireSurvivalGamemode::SetCurrentWave(UWaveDataAsset* NewWave)
{
    if (CurrentWave == NewWave)
    {
        return;
    }

    CurrentWave = NewWave;

    if (!CurrentWave)
    {
        return;
    }

    CurrentWaveIndex = CurrentWave-&gt;WaveIndex;

    UE_LOG(LogTemp, Warning, TEXT(&quot;Wave changed: %d&quot;), CurrentWaveIndex);

    if (EnemySpawner)
    {
        EnemySpawner-&gt;SetElapsedTime(ElapsedTime);
        EnemySpawner-&gt;SetWaveData(CurrentWave);
        EnemySpawner-&gt;StartSpawning();
    }
}

void AVampireSurvivalGamemode::SetGameCompletedState()
{
    AWorldSettings* WorldSettings = GetWorldSettings();

    if (!bIsGameCompleted &amp;&amp; ElapsedTime &gt;= 1800.f)
    {
        bIsGameCompleted = true;

        OnIsGameCompletedState.Broadcast(bIsGameCompleted);
        UGameplayStatics::SetGamePaused(GetWorld(), true);

    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 사이드프로젝트 31일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-30%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-30%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 14 May 2026 12:04:35 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--308시간-34분-">*<em>누적 학습 시간 : 308시간 34분 *</em></h2>
<h4 id="📅-2026-05-14">📅 2026-05-14</h4>
<p>무한성이라고 불리던 스테이지를 일찍 탈출한 6명이서 사이드 프로젝트를 진행하기로 했다.
순전히 기능 공부를 위함이고 출시계획같은건 당연하게도 없다.</p>
<h3 id="프로젝트-진행기간--260511--260521">프로젝트 진행기간 : 26.05.11 ~ 26.05.21</h3>
<p>주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 <a href="https://github.com/NBC-SideProject/Vampire-Survival">레포지토리 참조</a></p>
<p>MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.</p>
<h1 id="enemy-spawn-object-pooling-구조로-바꾸기">Enemy Spawn Object Pooling 구조로 바꾸기</h1>
<p>이번 작업에서는 기존 Enemy 스폰 방식을 <code>SpawnActor</code> / <code>Destroy</code> 중심 구조에서 <strong>Object Pooling</strong> 기반 구조로 전환했다.</p>
<p>Vampire Survivors류 게임은 짧은 시간 안에 많은 적이 생성되고 사라진다. 이때 매번 Actor를 생성하고 파괴하면 성능 비용이 커질 수 있다.</p>
<p>그래서 Enemy가 죽었을 때 바로 <code>Destroy()</code>하지 않고, 비활성화한 뒤 풀에 반환하고, 다음 스폰 때 다시 꺼내 쓰는 구조를 추가했다.</p>
<hr>
<h2 id="기존-구조의-문제">기존 구조의 문제</h2>
<p>기존 Enemy 흐름은 단순했다.</p>
<ol>
<li>Enemy Spawn</li>
<li><code>SpawnedEnemiesList</code>에 등록</li>
<li>Enemy 사망</li>
<li><code>Destroy()</code> 호출</li>
<li><code>CleanupInvalidEnemies()</code>에서 <code>!IsValid(Enemy)</code> 기준으로 리스트 정리</li>
</ol>
<p>기존에는 Actor가 죽으면 Destroy되었기 때문에 <code>IsValid()</code>만으로도 리스트 정리가 가능했다.</p>
<p>하지만 풀링 구조에서는 Enemy가 죽어도 Actor가 파괴되지 않는다.</p>
<p>대신 다음과 같은 상태가 된다.</p>
<ul>
<li>Actor는 여전히 존재함</li>
<li><code>IsValid(Enemy) == true</code></li>
<li>화면에서는 숨김 처리됨</li>
<li>Collision 비활성화</li>
<li>Tick 비활성화</li>
<li>Pool의 Idle 상태로 들어감</li>
</ul>
<p>즉 기존처럼 <code>!IsValid(Enemy)</code>만 검사하면, 이미 죽어서 풀에 들어간 Enemy가 <code>SpawnedEnemiesList</code>에 계속 남는다.</p>
<p>나중에 리스트 기반으로 살아있는 적 수를 계산하거나 Wave 진행 조건을 판단하면, 죽은 Enemy도 살아있는 Enemy처럼 카운트될 수 있다.</p>
<p>이번 변경의 핵심은 이 문제를 해결하는 것이었다.</p>
<hr>
<h2 id="poolsubsystem-추가">PoolSubsystem 추가</h2>
<p>먼저 Actor Pool을 관리할 <code>UPoolSubsystem</code>을 추가했다.</p>
<p><code>PoolSubsystem</code>은 <code>UWorldSubsystem</code>으로 구현했다. 그래서 GameMode나 Spawner에 컴포넌트처럼 붙이지 않는다.</p>
<p>필요한 곳에서 다음처럼 가져와 사용한다.</p>
<pre><code class="language-cpp">UPoolSubsystem* PoolSubsystem = GetWorld()-&gt;GetSubsystem&lt;UPoolSubsystem&gt;();</code></pre>
<p>풀은 ActorClass 기준으로 관리한다.</p>
<pre><code class="language-cpp">USTRUCT()
struct FActorPool
{
    GENERATED_BODY()

    UPROPERTY()
    TArray&lt;TObjectPtr&lt;AActor&gt;&gt; IdlePool;
};</code></pre>
<pre><code class="language-cpp">UPROPERTY()
TMap&lt;TSubclassOf&lt;AActor&gt;, FActorPool&gt; IdlePools;</code></pre>
<p>즉 <code>BP_EnemyBase_C</code> 풀, 다른 Enemy Blueprint 풀을 각각 따로 관리할 수 있다.</p>
<hr>
<h2 id="prewarmpool">PrewarmPool</h2>
<p><code>PrewarmPool()</code>은 게임 시작 시 특정 ActorClass를 미리 생성해 IdlePool에 넣는 함수다.</p>
<pre><code class="language-cpp">void UPoolSubsystem::PrewarmPool(TSubclassOf&lt;AActor&gt; ActorClass, int32 Count)
{
    if (!ActorClass || Count &lt;= 0 || !GetWorld())
    {
        return;
    }

    FActorPool&amp; Pool = IdlePools.FindOrAdd(ActorClass);

    for (int32 i = 0; i &lt; Count; ++i)
    {
        FActorSpawnParameters SpawnParams;
        SpawnParams.SpawnCollisionHandlingOverride =
            ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

        AActor* Actor = GetWorld()-&gt;SpawnActor&lt;AActor&gt;(
            ActorClass,
            FVector::ZeroVector,
            FRotator::ZeroRotator,
            SpawnParams
        );

        if (!Actor)
        {
            UE_LOG(LogTemp, Warning, TEXT(&quot;[Pool] Prewarm spawn failed: %s&quot;), *ActorClass-&gt;GetName());
            continue;
        }

        PrepareActorForPool(Actor);
        Pool.IdlePool.Add(Actor);
    }

    UE_LOG(LogTemp, Warning, TEXT(&quot;[Pool] Prewarm %s / Available: %d&quot;),
        *ActorClass-&gt;GetName(),
        Pool.IdlePool.Num());
}</code></pre>
<p>여기서 중요한 부분은 <code>AlwaysSpawn</code>이다.</p>
<p>Prewarm 시점에는 Actor를 생성한 뒤 바로 숨기고 Collision을 끌 것이기 때문에, 스폰 위치 충돌 때문에 생성이 실패할 필요가 없다.</p>
<pre><code class="language-cpp">SpawnParams.SpawnCollisionHandlingOverride =
    ESpawnActorCollisionHandlingMethod::AlwaysSpawn;</code></pre>
<hr>
<h2 id="getactorfrompool">GetActorFromPool</h2>
<p>Enemy를 스폰할 때는 직접 <code>SpawnActor</code>하지 않고 PoolSubsystem에서 Actor를 꺼낸다.</p>
<pre><code class="language-cpp">AActor* UPoolSubsystem::GetActorFromPool(TSubclassOf&lt;AActor&gt; ActorClass, const FTransform&amp; SpawnTransform)
{
    if (!ActorClass || !GetWorld())
    {
        return nullptr;
    }

    FActorPool&amp; Pool = IdlePools.FindOrAdd(ActorClass);

    AActor* Actor = nullptr;

    while (Pool.IdlePool.Num() &gt; 0)
    {
        Actor = Pool.IdlePool.Pop();

        if (IsValid(Actor))
        {
            break;
        }

        Actor = nullptr;
    }

    if (!Actor)
    {
        Actor = GetWorld()-&gt;SpawnActor&lt;AActor&gt;(ActorClass, SpawnTransform);
    }

    if (!Actor)
    {
        return nullptr;
    }

    PrepareActorForUse(Actor, SpawnTransform);

    UE_LOG(LogTemp, Warning, TEXT(&quot;[Pool] Get %s / Available: %d / Active: %d&quot;),
        *ActorClass-&gt;GetName(),
        Pool.IdlePool.Num(),
        IdlePools.Num());

    return Actor;
}</code></pre>
<p>흐름은 다음과 같다.</p>
<ol>
<li>IdlePool에 남은 Actor가 있으면 꺼낸다.</li>
<li>유효하지 않은 Actor면 버린다.</li>
<li>IdlePool이 비어 있으면 새로 Spawn한다.</li>
<li>Actor를 사용할 수 있는 상태로 전환한다.</li>
</ol>
<p>즉 <code>PrewarmCount</code>는 최대 제한이 아니라 “미리 만들어둘 개수”다.</p>
<p>예를 들어 <code>PrewarmCount = 300</code>인데 동시에 301번째 Enemy가 필요하면, 301번째는 그 순간 새로 Spawn된다. 이후 죽으면 다시 Pool에 들어가므로 Pool 크기는 301개까지 늘어날 수 있다.</p>
<hr>
<h2 id="returnactortopool">ReturnActorToPool</h2>
<p>Enemy가 죽으면 Destroy하지 않고 Pool로 반환한다.</p>
<pre><code class="language-cpp">void UPoolSubsystem::ReturnActorToPool(AActor* Actor)
{
    if (!IsValid(Actor))
    {
        return;
    }

    PrepareActorForPool(Actor);

    IdlePools.FindOrAdd(Actor-&gt;GetClass()).IdlePool.Add(Actor);
}</code></pre>
<p>Actor를 Pool에 넣기 전에는 비활성 상태로 만든다.</p>
<pre><code class="language-cpp">void UPoolSubsystem::PrepareActorForPool(AActor* Actor)
{
    if (!IsValid(Actor))
    {
        return;
    }

    if (IPoolable* Poolable = Cast&lt;IPoolable&gt;(Actor))
    {
        Poolable-&gt;ReturnToPool();
    }

    Actor-&gt;SetActorHiddenInGame(true);
    Actor-&gt;SetActorEnableCollision(false);
    Actor-&gt;SetActorTickEnabled(false);
}</code></pre>
<p>반대로 Pool에서 꺼낼 때는 다시 활성화한다.</p>
<pre><code class="language-cpp">void UPoolSubsystem::PrepareActorForUse(AActor* Actor, const FTransform&amp; SpawnTransform)
{
    if (!IsValid(Actor))
    {
        return;
    }

    Actor-&gt;SetActorTransform(SpawnTransform);
    Actor-&gt;SetActorHiddenInGame(false);
    Actor-&gt;SetActorEnableCollision(true);
    Actor-&gt;SetActorTickEnabled(true);

    if (IPoolable* Poolable = Cast&lt;IPoolable&gt;(Actor))
    {
        Poolable-&gt;GetFromPool();
    }
}</code></pre>
<hr>
<h2 id="ipoolable-인터페이스">IPoolable 인터페이스</h2>
<p>PoolSubsystem은 모든 Actor의 내부 상태를 알 수 없다.</p>
<p>Enemy는 풀에서 나올 때 체력을 초기화해야 하고, 접촉 데미지 대상 목록도 비워야 한다. Projectile은 Projectile 나름의 초기화가 필요할 수 있다.</p>
<p>그래서 풀링 대상 Actor가 직접 초기화 로직을 구현할 수 있도록 <code>IPoolable</code> 인터페이스를 추가했다.</p>
<pre><code class="language-cpp">UINTERFACE(MinimalAPI)
class UPoolable : public UInterface
{
    GENERATED_BODY()
};

class VAMPIRESURVIVAL_API IPoolable
{
    GENERATED_BODY()

public:
    virtual void GetFromPool()
        PURE_VIRTUAL(IPoolable::GetFromPool,);

    virtual void ReturnToPool()
        PURE_VIRTUAL(IPoolable::ReturnToPool,);
};</code></pre>
<p>이제 PoolSubsystem은 Actor가 <code>IPoolable</code>이면 다음 두 타이밍에 알려준다.</p>
<ul>
<li>Pool에서 꺼낼 때: <code>GetFromPool()</code></li>
<li>Pool로 반환할 때: <code>ReturnToPool()</code></li>
</ul>
<hr>
<h2 id="poolablecomponent-추가">PoolableComponent 추가</h2>
<p>풀링 구조에서 중요한 문제는 “Actor가 유효한가?”와 “게임상 살아있는가?”가 다르다는 점이다.</p>
<p>풀에 들어간 Enemy는 여전히 유효하다.</p>
<pre><code class="language-cpp">IsValid(Enemy) == true</code></pre>
<p>하지만 게임상으로는 죽은 Enemy다.</p>
<p>그래서 별도의 상태값이 필요했다. 이를 위해 <code>PoolableComponent</code>를 추가했다.</p>
<pre><code class="language-cpp">UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class VAMPIRESURVIVAL_API UPoolableComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    UPoolableComponent();

    void SetPoolStatus(bool Status);
    bool GetPoolStatus() const;

protected:
    bool isIdle = true;
};</code></pre>
<pre><code class="language-cpp">bool UPoolableComponent::GetPoolStatus() const
{
    return isIdle;
}

void UPoolableComponent::SetPoolStatus(bool Status)
{
    isIdle = Status;
}</code></pre>
<p>현재는 <code>bool</code>로 idle 상태만 관리한다.</p>
<ul>
<li><code>true</code>: Pool 안에 있음</li>
<li><code>false</code>: 현재 사용 중</li>
</ul>
<p>나중에는 이름을 <code>IsIdle()</code> / <code>SetIdle()</code>처럼 바꾸면 더 읽기 좋아질 수 있다.</p>
<hr>
<h2 id="enemybase-변경">EnemyBase 변경</h2>
<p><code>EnemyBase</code>는 이제 <code>IPoolable</code>을 구현한다.</p>
<pre><code class="language-cpp">class VAMPIRESURVIVAL_API AEnemyBase : public ACharacter, public IHitable, public IPoolable</code></pre>
<p>그리고 PoolableComponent를 가진다.</p>
<pre><code class="language-cpp">UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = &quot;Enemy|Component&quot;)
TObjectPtr&lt;UPoolableComponent&gt; PoolableComponent;</code></pre>
<p>생성자에서는 컴포넌트를 생성한다.</p>
<pre><code class="language-cpp">PoolableComponent = CreateDefaultSubobject&lt;UPoolableComponent&gt;(TEXT(&quot;PoolableComponent&quot;));</code></pre>
<hr>
<h2 id="death에서-destroy-대신-pool-반환">Death에서 Destroy 대신 Pool 반환</h2>
<p>기존에는 Enemy가 죽으면 바로 <code>Destroy()</code>했다.</p>
<p>풀링 구조에서는 먼저 PoolSubsystem에 반환을 시도한다.</p>
<pre><code class="language-cpp">void AEnemyBase::Death()
{
    if (UWorld* World = GetWorld())
    {
        if (UPoolSubsystem* PoolSubsystem = World-&gt;GetSubsystem&lt;UPoolSubsystem&gt;())
        {
            PoolSubsystem-&gt;ReturnActorToPool(this);
            return;
        }
    }

    Destroy();
}</code></pre>
<p>PoolSubsystem을 가져오지 못한 경우에는 fallback으로 <code>Destroy()</code>를 호출한다.</p>
<hr>
<h2 id="enemybase의-getfrompool--returntopool">EnemyBase의 GetFromPool / ReturnToPool</h2>
<p>Enemy가 Pool에서 다시 나올 때는 전투 상태를 초기화해야 한다.</p>
<pre><code class="language-cpp">void AEnemyBase::GetFromPool()
{
    ContactDamageTargets.Empty();

    if (PoolableComponent)
    {
        PoolableComponent-&gt;SetPoolStatus(false);
    }

    if (HitableComponent)
    {
        HitableComponent-&gt;Initialize(MaxHP);
    }

    TargetActor = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);
}</code></pre>
<p>풀에서 나올 때 하는 일은 다음과 같다.</p>
<ul>
<li>접촉 데미지 대상 목록 초기화</li>
<li>idle 상태 해제</li>
<li>체력 재초기화</li>
<li>플레이어 타겟 재설정</li>
</ul>
<p>반대로 Pool로 돌아갈 때는 임시 상태를 비운다.</p>
<pre><code class="language-cpp">void AEnemyBase::ReturnToPool()
{
    ContactDamageTargets.Empty();
    TargetActor = nullptr;

    if (PoolableComponent)
    {
        PoolableComponent-&gt;SetPoolStatus(true);

        UE_LOG(LogTemp, Warning, TEXT(&quot;Enemy Returned To Pool: %s&quot;), *GetName());
    }
}</code></pre>
<p>여기서 <code>PoolableComponent</code>의 상태를 idle로 바꾸는 것이 중요하다.</p>
<p>이 값은 나중에 <code>SpawnedEnemiesList</code> 정리에 사용된다.</p>
<hr>
<h2 id="enemyspawner-변경">EnemySpawner 변경</h2>
<p>기존 EnemySpawner는 Enemy를 직접 Spawn했다.</p>
<pre><code class="language-cpp">AEnemyBase* SpawnedEnemy = GetWorld()-&gt;SpawnActor&lt;AEnemyBase&gt;(...);</code></pre>
<p>이제는 PoolSubsystem에서 꺼내온다.</p>
<pre><code class="language-cpp">UPoolSubsystem* PoolSubsystem = GetWorld()-&gt;GetSubsystem&lt;UPoolSubsystem&gt;();
if (!PoolSubsystem)
{
    return;
}

AActor* PooledActor = PoolSubsystem-&gt;GetActorFromPool(
    EnemyData-&gt;EnemyClass,
    FTransform(FRotator::ZeroRotator, SpawnLocation)
);

AEnemyBase* SpawnedEnemy = Cast&lt;AEnemyBase&gt;(PooledActor);</code></pre>
<p>여기서 중요한 점은 <code>EnemyData-&gt;EnemyClass</code>를 기준으로 Pool에서 꺼낸다는 것이다.</p>
<p>현재 프로젝트는 Bat, Slime 같은 Enemy 차이를 별도 ActorClass로 나누기보다 <code>EnemyDataAsset</code>으로 주입하는 구조다. 따라서 Pool에서 꺼낸 뒤에는 현재 EnemyData를 다시 적용한다.</p>
<pre><code class="language-cpp">if (SpawnedEnemy)
{
    SpawnedEnemy-&gt;InitializeFromData(EnemyData);
    SpawnedEnemiesList.AddUnique(SpawnedEnemy);

    UE_LOG(LogTemp, Warning, TEXT(&quot;SpawnedEnemiesList Count After Spawn: %d&quot;), SpawnedEnemiesList.Num());
}</code></pre>
<p><code>Add()</code> 대신 <code>AddUnique()</code>를 사용한 이유는 풀링 구조에서는 같은 Actor가 여러 번 재사용되기 때문이다.</p>
<p>만약 정리 타이밍이 어긋나면 같은 Enemy Actor가 리스트에 중복 등록될 수 있다. <code>AddUnique()</code>는 이 문제를 막는 안전장치다.</p>
<hr>
<h2 id="spawnedenemieslist-정리-방식-변경">SpawnedEnemiesList 정리 방식 변경</h2>
<p>기존 정리 방식은 단순했다.</p>
<pre><code class="language-cpp">SpawnedEnemiesList.RemoveAll([](const TObjectPtr&lt;AActor&gt;&amp; Enemy)
{
    return !IsValid(Enemy);
});</code></pre>
<p>하지만 풀링 구조에서는 이 조건만으로 부족하다. 풀로 돌아간 Enemy는 여전히 유효하기 때문이다.</p>
<p>그래서 <code>PoolableComponent</code>의 idle 상태도 함께 확인한다.</p>
<pre><code class="language-cpp">void AEnemySpawner::CleanupInvalidEnemies()
{
    const int32 BeforeCount = SpawnedEnemiesList.Num();

    SpawnedEnemiesList.RemoveAll([](const TObjectPtr&lt;AActor&gt;&amp; Enemy)
    {
        if (!IsValid(Enemy))
        {
            return true;
        }

        const UPoolableComponent* PoolableComponent = Enemy-&gt;FindComponentByClass&lt;UPoolableComponent&gt;();
        if (PoolableComponent &amp;&amp; PoolableComponent-&gt;GetPoolStatus())
        {
            return true;
        }

        return false;
    });

    const int32 AfterCount = SpawnedEnemiesList.Num();

    UE_LOG(LogTemp, Warning, TEXT(&quot;Cleanup SpawnedEnemiesList: %d -&gt; %d&quot;), BeforeCount, AfterCount);
}</code></pre>
<p>이제 제거 조건은 두 가지다.</p>
<ol>
<li>Actor가 더 이상 유효하지 않다.</li>
<li>Actor는 유효하지만 Pool에 들어간 idle 상태다.</li>
</ol>
<p>이 변경으로 <code>SpawnedEnemiesList</code>는 “현재 활성화된 Enemy 목록”에 가까운 의미를 갖게 된다.</p>
<hr>
<h2 id="prewarm-위치-조정">Prewarm 위치 조정</h2>
<p>처음에는 Wave가 바뀔 때마다 WaveData를 읽어서 필요한 EnemyClass들을 Prewarm하는 방식도 고려했다.</p>
<p>하지만 현재 프로젝트 구조에서는 대부분의 Enemy가 같은 <code>BP_EnemyBase_C</code>를 사용한다.</p>
<p>즉 Bat, Slime, Skeleton이 서로 다른 ActorClass가 아니라 같은 EnemyBase Blueprint를 공유하고, 실제 차이는 <code>EnemyDataAsset</code>으로 적용된다.</p>
<p>그래서 Wave마다 Prewarm할 필요가 없다.</p>
<p>현재 방향은 <code>EnemySpawner</code>에 기본 Prewarm 설정을 두고, <code>BeginPlay()</code>에서 한 번만 Pool을 생성하는 것이다.</p>
<pre><code class="language-cpp">UPROPERTY(EditAnywhere, Category = &quot;Spawner|Pool&quot;)
TSubclassOf&lt;AEnemyBase&gt; PrewarmEnemyClass;

UPROPERTY(EditAnywhere, Category = &quot;Spawner|Pool&quot;)
int32 PrewarmCount = 300;</code></pre>
<pre><code class="language-cpp">void AEnemySpawner::BeginPlay()
{
    Super::BeginPlay();

    CachedPlayerCharacter = Cast&lt;APlayerCharacter&gt;(UGameplayStatics::GetPlayerCharacter(this, 0));
    if (!CachedPlayerCharacter)
    {
        UE_LOG(LogTemp, Error, TEXT(&quot;Player character not found&quot;));
    }

    if (!PrewarmEnemyClass || PrewarmCount &lt;= 0)
    {
        return;
    }

    UPoolSubsystem* PoolSubsystem = GetWorld()-&gt;GetSubsystem&lt;UPoolSubsystem&gt;();
    if (!PoolSubsystem)
    {
        return;
    }

    PoolSubsystem-&gt;PrewarmPool(PrewarmEnemyClass, PrewarmCount);
}</code></pre>
<p>주의할 점은 <code>PrewarmEnemyClass</code>와 <code>EnemyData-&gt;EnemyClass</code>가 반드시 같아야 한다는 것이다.</p>
<p>예를 들어 Prewarm은 C++ <code>AEnemyBase</code>로 하고, 실제 스폰은 <code>BP_EnemyBase_C</code>로 하면 서로 다른 풀로 취급된다.</p>
<p>따라서 에디터에서 EnemySpawner의 <code>PrewarmEnemyClass</code>에는 실제 EnemyDataAsset들이 사용하는 <code>BP_EnemyBase_C</code>를 넣어야 한다.</p>
<hr>
<h2 id="디버그-로그로-확인한-흐름">디버그 로그로 확인한 흐름</h2>
<p>테스트 중에는 다음 흐름을 로그로 확인했다.</p>
<pre><code class="language-text">[Pool] Prewarm BP_EnemyBase_C / Available: 300
[Pool] Get BP_EnemyBase_C / Available: 299
SpawnedEnemiesList Count After Spawn: 1
[Pool] Get BP_EnemyBase_C / Available: 298
SpawnedEnemiesList Count After Spawn: 2</code></pre>
<p><code>Available</code>이 줄어든다는 것은 새로 Spawn하는 것이 아니라, 미리 생성해둔 Pool에서 꺼내고 있다는 뜻이다.</p>
<p>Enemy가 죽으면 다음 흐름을 기대한다.</p>
<pre><code class="language-text">Enemy Returned To Pool: BP_EnemyBase_C_12
Cleanup SpawnedEnemiesList: 10 -&gt; 9</code></pre>
<p>이렇게 나오면 Enemy가 Destroy되지 않고 Pool로 돌아가며, <code>SpawnedEnemiesList</code>에서도 제거되고 있다는 뜻이다.</p>
<hr>
<h2 id="현재-구조의-주의점">현재 구조의 주의점</h2>
<p>현재 <code>GetActorFromPool()</code> 로그의 <code>Active</code> 값은 실제 Active Enemy 수가 아니다.</p>
<pre><code class="language-cpp">IdlePools.Num()</code></pre>
<p>은 활성 Actor 수가 아니라 Pool에 등록된 ActorClass 종류 개수에 가깝다.</p>
<p>따라서 정확한 사용량을 보려면 나중에 다음과 같은 값이 필요하다.</p>
<pre><code class="language-cpp">TMap&lt;TSubclassOf&lt;AActor&gt;, int32&gt; TotalCreatedCounts;</code></pre>
<p>그 후 다음처럼 계산할 수 있다.</p>
<pre><code class="language-cpp">Active = TotalCreated - Available;</code></pre>
<p>또한 현재 <code>PrewarmCount</code>는 최대 제한이 아니다.</p>
<p><code>PrewarmCount = 300</code>이어도 301번째 Enemy가 필요하면 새로 Spawn된다. 그리고 그 Enemy가 죽으면 Pool에 들어가므로 Pool 크기는 301까지 늘어날 수 있다.</p>
<p>즉 현재 구조는 고정 크기 풀이 아니라 <strong>동적 확장 풀</strong>이다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>이번 변경으로 Enemy 스폰 구조는 단순 생성/파괴 방식에서 재사용 가능한 Pooling 구조로 바뀌었다.</p>
<p>핵심은 단순히 <code>Destroy()</code>를 없애는 것이 아니었다.</p>
<p>풀링 구조에서는 Actor가 살아 있어도 게임상으로는 죽은 Enemy일 수 있다. 따라서 게임 로직이 이 상태를 이해할 수 있어야 한다.</p>
<p>이를 위해 다음 요소를 추가했다.</p>
<ul>
<li><code>UPoolSubsystem</code></li>
<li><code>IPoolable</code></li>
<li><code>UPoolableComponent</code></li>
<li><code>EnemyBase</code>의 Pool 반환 처리</li>
<li><code>EnemySpawner</code>의 Pool 기반 스폰 처리</li>
<li><code>SpawnedEnemiesList</code>의 idle Enemy 정리</li>
</ul>
<p>결과적으로 Enemy는 죽어도 Actor가 파괴되지 않고 Pool로 돌아가며, 다음 스폰 때 재사용된다.</p>
<p>또한 <code>PoolableComponent</code>의 idle 상태를 기준으로 <code>SpawnedEnemiesList</code>를 정리해, 죽은 Enemy가 살아있는 Enemy처럼 카운트되는 문제도 막을 수 있게 됐다.</p>
<hr>
<h2 id="다음-개선-방향">다음 개선 방향</h2>
<p>아직 개선할 부분도 남아 있다.</p>
<ul>
<li>개발용 로그 정리</li>
<li><code>Active</code> 수 계산 개선</li>
<li><code>PrewarmPool()</code>을 부족분만 생성하는 방식으로 변경</li>
<li>Pool 최대 크기 제한 검토</li>
<li><code>SpawnedEnemiesList</code> 타입을 <code>AActor</code>에서 <code>AEnemyBase</code> 중심으로 변경</li>
<li><code>ReturnToPool</code> 시 Spawner에 이벤트로 알리는 방식 검토</li>
</ul>
<p>현재 단계에서는 Enemy Pooling의 핵심 흐름이 잡혔다.<br>다음 단계에서는 디버그용 코드를 정리하고, Pool 사용량을 더 정확하게 추적하는 방향으로 개선하면 좋을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 사이드프로젝트 30일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-29%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-29%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 13 May 2026 12:02:06 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--296시간-34분-">*<em>누적 학습 시간 : 296시간 34분 *</em></h2>
<h4 id="📅-2026-05-13">📅 2026-05-13</h4>
<p>무한성이라고 불리던 스테이지를 일찍 탈출한 6명이서 사이드 프로젝트를 진행하기로 했다.
순전히 기능 공부를 위함이고 출시계획같은건 당연하게도 없다.</p>
<h3 id="프로젝트-진행기간--260511--260521">프로젝트 진행기간 : 26.05.11 ~ 26.05.21</h3>
<p>주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 <a href="https://github.com/NBC-SideProject/Vampire-Survival">레포지토리 참조</a></p>
<p>MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.</p>
<h1 id="enemy-spawner-pooling">Enemy Spawner Pooling</h1>
<h2 id="풀링이란">풀링이란?</h2>
<p>기존 언리얼에서 스포너로 액터 생성 시 아래와 같은 형태를 취한다.</p>
<pre><code>- 필요할 때 Spawn
- 사용 끝나면 Destroy
- 필요하면 다시 Spawn
- 사용 끝나면 다시 Destroy</code></pre><p>풀링 적용 시</p>
<pre><code>- 게임 시작 시 미리 Spawn
- 비활성화 상태로 보관
- 필요할 때 활성화
- 사용 끝나면 Destroy하지 않고 다시 비활성화
- 다음에 재사용</code></pre><p><strong>풀링(Object Pooling)은 오브젝트를 필요할 때마다
계속 생성 → 사용 → 제거하지 않고, 미리 여러 개 만들어 둔 뒤 재사용하는 기법</strong></p>
<h2 id="왜-쓰는가">왜 쓰는가?</h2>
<p>언리얼에서 SpawnActor는 단순히 메모리에 객체 생성하는 수준이 아니고 생각보다 복잡하다.</p>
<pre><code>- Actor 생성
- Component 생성
- Transform 설정
- Collision 등록
- BeginPlay 호출
- Tick 등록
- Replication 준비
- Physics 등록
- Rendering 등록</code></pre><p>반대의 경우로 <code>Destory()</code>시에도 바로 사라지는게 아니라 엔진 라이프 사이클에 따라서 제거 예약, 참조 정리, GC 처리 등이 얽힌다.</p>
<p>그래서 아래와 같은 상황에서는 풀링을 사용하는게 유리하다.</p>
<pre><code>- 총알
- 투사체
- 피격 이펙트
- 폭발 이펙트
- 데미지 숫자UI
- 드랍 아이템
- 몬스터 스폰/웨이브
- 특정 잔해 조각들
- 발자국, 탄피, 흔적들</code></pre><h2 id="조심해야하는-포인트">조심해야하는 포인트</h2>
<p>풀링에서 가장 많이 터지는 버그는 이전 상태가 남아있는 것이다.
반환시 반드시 초기화 해야하는 것들이 있는데 총알로 예를 들 경우</p>
<pre><code>- Location
- Rotation
- Velocity
- Direction
- LifeCycle
- Coliision state
- Tick
- Visibility
- Particle State
- Sound State
- Damage Owner
- Instigator
- Target
- Overlap List
- Timer
- Delegate Binding</code></pre><p>위 항목들을 반드시 초기화 해줘야한다.
언리얼에서 특히 버그가 많이 발생하는 부분은
<code>Timer</code>, <code>Delegate</code>, <code>Collision</code>, <code>Overlap</code>, <code>Particle</code>이다.</p>
<h4 id="멀티플레이에서-풀링-시-host-기준">멀티플레이에서 풀링 시 HOST 기준</h4>
<h2 id="실제-구현-고민">실제 구현 고민</h2>
<p>프로젝트 같이하는 조에서 라이브러리를 추천받아서 사용해볼까 했으나
직접 구현을 한번 해보는게 좋을 것 같아서 구현하게됐다.</p>
<h2 id="c로-구현구상">C++로 구현구상</h2>
<p>대충 요런 느낌인데 Unreal에서는 vector말고 TArray 써야되니까 Unreal버전으로도 작성해보자.</p>
<pre><code class="language-cpp">// poolingManager
std::vector&lt;std::unique_ptr&lt;AEnemyBase&gt;&gt; DeactiveEnemyPool;
std::vector&lt;std::unique_ptr&lt;AEnemyBase&gt;&gt; IdleEnemyPool;


int32 EnemyPoolSize = 200;

for(int32 i = 0; i &lt; EnemyPoolSize; i++)
{
    // EnemyClass는 실제 BP클래스
    AEnemyBase* Enemy = GetWorld()-&gt;SpawnActor&lt;AEnemyBase&gt;(EnemyClass);
    DeactiveEnemyPool.Add(Enemy);
}


// Enemy Spawner
void AEnemySpawner::SpawnEnemy() 
{
    AEnemyBase* Enemy = nullptr;

    // 기존 SpawnActor 대신 Pool 
    Enemy-&gt;ActiveFromPool(EnemyData);
}

void AEnemySpawner::ReturnEnemyToPool(AEnemyBase* Enemy)
{
    // 기존 Destory() 대신 Pool
    SpawnedEnemyList.Remove(Enemy);
    Enemy-&gt;DeactiveToPool();
    IdleEnemyPool.Add(Enemy);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 사이드프로젝트 29일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-28%EC%9D%BC%EC%B0%A8-ubvnnky5</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-28%EC%9D%BC%EC%B0%A8-ubvnnky5</guid>
            <pubDate>Tue, 12 May 2026 11:36:24 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--284시간-34분-">*<em>누적 학습 시간 : 284시간 34분 *</em></h2>
<h4 id="📅-2026-05-12">📅 2026-05-12</h4>
<p>무한성이라고 불리던 스테이지를 일찍 탈출한 6명이서 사이드 프로젝트를 진행하기로 했다.
순전히 기능 공부를 위함이고 출시계획같은건 당연하게도 없다.</p>
<h3 id="프로젝트-진행기간--260511--260521">프로젝트 진행기간 : 26.05.11 ~ 26.05.21</h3>
<p>주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 <a href="https://github.com/NBC-SideProject/Vampire-Survival">레포지토리 참조</a></p>
<p>MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.</p>
<h1 id="data-asset-기반으로-리팩토링">Data Asset 기반으로 리팩토링</h1>
<h2 id="enemyspawnercpp">EnemySpawner.cpp</h2>
<pre><code class="language-cpp">// Fill out your copyright notice in the Description page of Project Settings.


#include &quot;Spawner/EnemySpawner.h&quot;

#include &quot;NavigationSystem.h&quot;
#include &quot;Entity/Character/PlayerCharacter.h&quot;
#include &quot;Components/CapsuleComponent.h&quot;
#include &quot;DataAsset/EnemyDataAsset.h&quot;
#include &quot;DataAsset/WaveDataAsset.h&quot;
#include &quot;Entity/Enemy/EnemyBase.h&quot;
#include &quot;Kismet/GameplayStatics.h&quot;

// Sets default values
AEnemySpawner::AEnemySpawner()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don&#39;t need it.
    PrimaryActorTick.bCanEverTick = true;
}

// Called when the game starts or when spawned
void AEnemySpawner::BeginPlay()
{
    Super::BeginPlay();

    CachedPlayerCharacter = Cast&lt;APlayerCharacter&gt;(UGameplayStatics::GetPlayerCharacter(this, 0));
    if (!CachedPlayerCharacter)
    {
        UE_LOG(LogTemp, Error, TEXT(&quot;Player character not found&quot;));
    }
}

void AEnemySpawner::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    if (!bIsSpawning || !CurrentWaveData)
    {
        return;
    }

    WaveElapsedTime += DeltaTime;
    TimeSinceLastSpawn += DeltaTime;

    const FSpawnSegment* CurrentSegment = FindCurrentSpawnSegment();

    if (!CurrentSegment)
    {
        return;
    }

    if (TimeSinceLastSpawn &gt;= CurrentSegment-&gt;SpawnInterval)
    {
        TimeSinceLastSpawn = 0.f;

        if (const FEnemySpawnGroup* SpawnGroup = SelectEnemySpawnGroup(*CurrentSegment))
        {
            SpawnEnemyFromGroup(*SpawnGroup);
        }
    }

    if (!bBossSpawned &amp;&amp; CurrentWaveData-&gt;BossEnemyDataAsset &amp;&amp; WaveElapsedTime &gt;= CurrentWaveData-&gt;BossSpawnTime)
    {
        bBossSpawned = true;
        SpawnBoss();
    }
}

void AEnemySpawner::SetWaveData(UWaveDataAsset* NewWaveData)
{
    CurrentWaveData = NewWaveData;
    WaveElapsedTime = 0.f;
    TimeSinceLastSpawn = 0.f;
    bBossSpawned = false;

    if (CurrentWaveData)
    {
        UE_LOG(LogTemp, Warning, TEXT(&quot;EnemySpawner: Wave data set. WaveIndex=%d&quot;), CurrentWaveData-&gt;WaveIndex);
    }
}

FVector AEnemySpawner::GetPlayerLocation() const
{
    if (!CachedPlayerCharacter)
    {
        return FVector::ZeroVector;
    }

    return CachedPlayerCharacter-&gt;GetActorLocation();
}

void AEnemySpawner::StartSpawning()
{
    bIsSpawning = true;

    TimeSinceLastSpawn = 0.f;
}

void AEnemySpawner::StopSpawning()
{
    bIsSpawning = false;
}

const FSpawnSegment* AEnemySpawner::FindCurrentSpawnSegment() const
{
    if (!CurrentWaveData)
    {
        return nullptr;
    }

    for (const FSpawnSegment&amp; Segment : CurrentWaveData-&gt;SpawnSegments)
    {
        if (WaveElapsedTime &gt;= Segment.StartTime &amp;&amp; WaveElapsedTime &lt; Segment.EndTime)
        {
            return &amp;Segment;
        }
    }

    return nullptr;
}

const FEnemySpawnGroup* AEnemySpawner::SelectEnemySpawnGroup(const FSpawnSegment&amp; Segment) const
{
    float totalWeight = 0.f;

    for (const FEnemySpawnGroup&amp; Group : Segment.EnemyGroups)
    {
        if (Group.EnemyData &amp;&amp; Group.SpawnWeight &gt; 0.f)
        {
            totalWeight += Group.SpawnWeight;
        }
    }

    if (totalWeight &lt;= 0.f)
    {
        return nullptr;
    }

    float randValue = FMath::FRandRange(0.f, totalWeight);

    for (const FEnemySpawnGroup&amp; Group : Segment.EnemyGroups)
    {
        if (!Group.EnemyData || Group.SpawnWeight &lt;= 0.f)
        {
            continue;
        }

        randValue -= Group.SpawnWeight;
        if (randValue &lt;= 0.f)
        {
            return &amp;Group;
        }
    }

    return nullptr;
}

void AEnemySpawner::SpawnEnemyFromGroup(const FEnemySpawnGroup&amp; SpawnGroup)
{
    SpawnEnemyFromData(
        SpawnGroup.EnemyData,
        SpawnGroup.CountPerSpawn
        );
}

void AEnemySpawner::SpawnEnemyFromData(class UEnemyDataAsset* EnemyData, int32 Count)
{
    if (!EnemyData || !EnemyData-&gt;EnemyClass)
    {
        UE_LOG(LogTemp, Warning, TEXT(&quot;EnemySpawner: Invalid EnemyData&quot;));
        return;
    }

    CleanupInvalidEnemies();

    for (int32 i=0; i &lt; Count; ++i)
    {
        FVector SpawnLocation;
        if (!FindSpawnLocation(SpawnLocation))
        {
            UE_LOG(LogTemp, Warning, TEXT(&quot;EnemySpawner: FindSpawnLocation failed&quot;));
            continue;
        }

        AEnemyBase* DefaultEnemy = EnemyData-&gt;EnemyClass-&gt;GetDefaultObject&lt;AEnemyBase&gt;();
        if (DefaultEnemy &amp;&amp; DefaultEnemy-&gt;GetCapsuleComponent())
        {
            SpawnLocation.Z = DefaultEnemy-&gt;GetCapsuleComponent()-&gt;GetScaledCapsuleHalfHeight();
        }

        FActorSpawnParameters SpawnParams;

        SpawnParams.Owner = this;
        SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;

        AEnemyBase* SpawnedEnemy = GetWorld()-&gt;SpawnActor&lt;AEnemyBase&gt;(
            EnemyData-&gt;EnemyClass,
            SpawnLocation,
            FRotator::ZeroRotator,
            SpawnParams
        );

        if (SpawnedEnemy)
        {
            SpawnedEnemy-&gt;InitializeFromData(EnemyData);
            SpawnedEnemiesList.Add(SpawnedEnemy);
        }
    }
}

void AEnemySpawner::SpawnBoss()
{
    if (!CurrentWaveData || !CurrentWaveData-&gt;BossEnemyDataAsset)
    {
        return;
    }

    SpawnEnemyFromData(CurrentWaveData-&gt;BossEnemyDataAsset, 1);
}


bool AEnemySpawner::FindSpawnLocation(FVector&amp; SpawnLocation) const
{
    const float angle = FMath::FRandRange(0.f, 2.f * PI);

    FVector direction(
        FMath::Cos(angle),
        FMath::Sin(angle),
        0.f
    );

    const float distance = FMath::FRandRange(MinSpawnDistance, MaxSpawnDistance);

    const FVector candidateLocation = GetPlayerLocation() + direction * distance;


    UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());
    if (!NavSystem)
    {
        return false;
    }

    FNavLocation navLocation;

    // candidateLocation이 정확히 NavMesh 위가 아닐 수도 있음
    // xyz 5미터 탐색지역 생성
    const FVector QueryExtent(500.f, 500.f, 500.f);

    // candidateLocation 주변 QueryExtent 내에서 Nav Mesh 탐색
    // ProjectPointToNavigation = 입력위치를 navigation data위로 투영 &gt; FNavLocation return
    const bool bProjected = NavSystem-&gt;ProjectPointToNavigation(
        candidateLocation,
        navLocation,
        QueryExtent
    );

    if (!bProjected)
    {
        return false;
    }

    UE_LOG(LogTemp, Warning, TEXT(&quot;Spawned&quot;));

    SpawnLocation = navLocation.Location;
    return true;
}

void AEnemySpawner::CleanupInvalidEnemies()
{
    SpawnedEnemiesList.RemoveAll([](const TObjectPtr&lt;AActor&gt;&amp; Enemy)
    {
        return !IsValid(Enemy);
    });
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 사이드프로젝트 28일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-28%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-%EC%82%AC%EC%9D%B4%EB%93%9C%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-28%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 11 May 2026 13:09:07 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--272시간-34분-">*<em>누적 학습 시간 : 272시간 34분 *</em></h2>
<h4 id="📅-2026-05-11">📅 2026-05-11</h4>
<p>무한성이라고 불리던 스테이지를 일찍 탈출한 6명이서 사이드 프로젝트를 진행하기로 했다.
순전히 기능 공부를 위함이고 출시계획같은건 당연하게도 없다.</p>
<h3 id="프로젝트-진행기간--260511--260521">프로젝트 진행기간 : 26.05.11 ~ 26.05.21</h3>
<p>주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 <a href="https://github.com/NBC-SideProject/Vampire-Survival">레포지토리 참조</a></p>
<p>MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.</p>
<h1 id="enemyspawner">EnemySpawner</h1>
<h2 id="enemyspawn-시-에러발생">EnemySpawn() 시 에러발생</h2>
<pre><code>LogTemp: Warning: Spawned
LogSpawn: Warning: SpawnActor failed because of collision at the spawn location [X=2194.936 Y=-2181.662 Z=10.000] for [BP_EnemyBase_C]
LogTemp: Warning: Spawned
LogSpawn: Warning: SpawnActor failed because of collision at the spawn location [X=-354.411 Y=2057.717 Z=10.000] for [BP_EnemyBase_C]
LogTemp: Warning: Spawned
LogSpawn: Warning: SpawnActor failed because of collision at the spawn location [X=-2411.997 Y=-519.608 Z=10.000] for [BP_EnemyBase_C]
LogTemp: Warning: Spawned
LogSpawn: Warning: SpawnActor failed because of collision at the spawn location [X=1004.887 Y=-2139.610 Z=10.000] for [BP_EnemyBase_C]
LogTemp: Warning: Spawned
LogSpawn: Warning: SpawnActor failed because of collision at the spawn location [X=445.794 Y=-2630.596 Z=10.000] for [BP_EnemyBase_C]</code></pre><p>에러를 확인해보면 해당 스폰 지점 콜리전 충돌로 스폰에 실패했다고 한다.</p>
<p>아마도 스폰되는 BP_EnemyBase의 캡슐컴포넌트의 height값이 88인데 10에 스폰하려고 하면 안되지않을까.. 싶었다.</p>
<pre><code class="language-cpp">    // 스폰 위치 선언
    FVector SpawnLocation;

    // spawn location 체크결과 false일 경우 return
    if (!FindSpawnLocation(SpawnLocation))
    {
        UE_LOG(LogTemp, Warning, TEXT(&quot;Enemy Spawner: Failed to find spawn location&quot;));
        return;
    }

    SpawnLocation.Z = 100;</code></pre>
<p>그래서 위와같이 spawnlocation.z 를 100으로 설정해주니까</p>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/bbd92688-6620-4f90-b49e-dc371d6c04b7/image.png" alt=""></p>
<p>스폰이 잘 된다!
그런데 문제가 하나 더 있다.</p>
<p>위에서 말했듯 BP_EnemyBase의 캡슐컴포넌트의 height가 88인데 100으로 설정해 주었더니</p>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/5e93652b-3935-452e-b30b-e61f02f89985/image.png" alt=""></p>
<p>이놈들이 공중부양해서 다닌다..
그래서 코드를 수정해서 BP_EnemyClass의 캡슐 컴포넌트의 halfheight값을 z에 넣어주도록 하겠다.</p>
<pre><code class="language-cpp">    ACharacter* DefaultEnemy = EnemyClass-&gt;GetDefaultObject&lt;ACharacter&gt;();
    if (!DefaultEnemy || !DefaultEnemy-&gt;GetCapsuleComponent())
    {
        return;
    }
    const float EnemyCapsuleHeight = DefaultEnemy-&gt;GetCapsuleComponent()-&gt;GetScaledCapsuleHalfHeight();
    SpawnLocation.Z = EnemyCapsuleHeight;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 27일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-27%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-27%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Fri, 08 May 2026 12:12:45 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--260시간-34분-">*<em>누적 학습 시간 : 260시간 34분 *</em></h2>
<h4 id="📅-2026-05-08">📅 2026-05-08</h4>
<p>무한성이라고 불리던 스테이지를 일찍 탈출한 6명이서 사이드 프로젝트를 진행하기로 했다.
순전히 기능 공부를 위함이고 출시계획같은건 당연하게도 없다.</p>
<h3 id="프로젝트-진행기간--260511--260521">프로젝트 진행기간 : 26.05.11 ~ 26.05.21</h3>
<p>주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 <a href="https://github.com/NBC-SideProject/Vampire-Survival">레포지토리 참조</a></p>
<p>MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.</p>
<h1 id="enemyspawner">EnemySpawner</h1>
<p>마침 해당 사이드 프로젝트를 진행하기 전 EnemySpawner를 만들어봤었다.
해당 코드 첨부</p>
<h2 id="enemyspawnerh">EnemySpawner.h</h2>
<pre><code class="language-cpp">// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;GameFramework/Actor.h&quot;
#include &quot;EnemySpawner.generated.h&quot;

class ACharacter;

UCLASS()
class METAL3D_API AEnemySpawner : public AActor
{
    GENERATED_BODY()

public:
    // Sets default values for this actor&#39;s properties
    AEnemySpawner();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:
    UFUNCTION(BlueprintCallable, Category=&quot;Spawner&quot;)
    void StartSpawning();

    UFUNCTION(BlueprintCallable, Category=&quot;Spawner&quot;)
    void StopSpawning();

private:
    UPROPERTY(EditAnywhere, Category=&quot;Spawner&quot;)
    TSubclassOf&lt;ACharacter&gt; EnemyClass;

    UPROPERTY(EditAnywhere, Category=&quot;Spawner&quot;)
    float SpawnRadius = 1200.f;

    UPROPERTY(EditAnywhere, Category=&quot;Spawner&quot;)
    float SpawnInterval = 3.f;

    UPROPERTY(EditAnywhere, Category=&quot;Spawner&quot;)
    int32 MaxEnemyCount = 100;

    UPROPERTY(EditAnywhere, Category=&quot;Spawner&quot;)
    bool bAutoStart = true;

    UPROPERTY()
    TArray&lt;TObjectPtr&lt;AActor&gt;&gt; SpawnedEnemies;

    FTimerHandle SpawnTimerHandler;

    void SpawnEnemy();

    bool FindSpawnLocation(FVector&amp; OutLocation) const;

    void CleanupInvalidEnemies();
};
</code></pre>
<h2 id="enemyspawnercpp">EnemySpawner.cpp</h2>
<pre><code class="language-cpp">// Fill out your copyright notice in the Description page of Project Settings.


#include &quot;Spawner/EnemySpawner.h&quot;

#include &quot;NavigationSystem.h&quot;
#include &quot;GameFramework/Character.h&quot;
#include &quot;TimerManager.h&quot;
#include &quot;Components/CapsuleComponent.h&quot;
#include &quot;Engine/World.h&quot;

// Sets default values
AEnemySpawner::AEnemySpawner()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don&#39;t need it.
    PrimaryActorTick.bCanEverTick = true;
}

// Called when the game starts or when spawned
void AEnemySpawner::BeginPlay()
{
    Super::BeginPlay();

    if (bAutoStart)
    {
        StartSpawning();
    }
}

void AEnemySpawner::StartSpawning()
{
    if (!GetWorld())
    {
        return;
    }

    GetWorld()-&gt;GetTimerManager().SetTimer(
        SpawnTimerHandler,
        this,
        &amp;AEnemySpawner::SpawnEnemy,
        SpawnInterval,
        true,
        0.f
    );
}

void AEnemySpawner::StopSpawning()
{
    if (!GetWorld())
    {
        return;
    }

    GetWorld()-&gt;GetTimerManager().ClearTimer(SpawnTimerHandler);
}

void AEnemySpawner::SpawnEnemy()
{
    if (!EnemyClass)
    {
        UE_LOG(LogTemp, Warning, TEXT(&quot;Enemy Spawner: EnemyClass not set&quot;));
        return;
    }

    CleanupInvalidEnemies();
    if (SpawnedEnemies.Num() &gt;= MaxEnemyCount)
    {
        return;
    }

    FVector SpawnLocation;
    if (!FindSpawnLocation(SpawnLocation))
    {
        UE_LOG(LogTemp, Warning, TEXT(&quot;Enemy Spawner: Failed to find spawn location&quot;));
        return;
    }

    ACharacter* DefaultEnemy = EnemyClass-&gt;GetDefaultObject&lt;ACharacter&gt;();
    if (!DefaultEnemy || !DefaultEnemy-&gt;GetCapsuleComponent())
    {
        return;
    }

    const float CapsuleHalfHeight = DefaultEnemy-&gt;GetCapsuleComponent()-&gt;GetScaledCapsuleHalfHeight();
    SpawnLocation.Z += CapsuleHalfHeight;

    const FRotator SpawnRotation = FRotator::ZeroRotator;

    FActorSpawnParameters SpawnParams;

    SpawnParams.Owner = this;
    SpawnParams.SpawnCollisionHandlingOverride =
        ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;

    ACharacter* SpawnedEnemy = GetWorld()-&gt;SpawnActor&lt;ACharacter&gt;(
        EnemyClass,
        SpawnLocation,
        SpawnRotation,
        SpawnParams
    );

    if (SpawnedEnemy)
    {
        SpawnedEnemies.Add(SpawnedEnemy);
    }
}

bool AEnemySpawner::FindSpawnLocation(FVector&amp; OutLocation) const
{
    UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());

    if (!NavSystem)
    {
        return false;
    }

    FNavLocation NavLocation;

    const bool bFound = NavSystem-&gt;GetRandomReachablePointInRadius(
        GetActorLocation(),
        SpawnRadius,
        NavLocation
    );

    if (!bFound)
    {
        return false;
    }

    OutLocation = NavLocation.Location;
    return true;
}

void AEnemySpawner::CleanupInvalidEnemies()
{
    SpawnedEnemies.RemoveAll([](const TObjectPtr&lt;AActor&gt;&amp; Enemy)
    {
        return !IsValid(Enemy);
    });
}
</code></pre>
<p>해당 코드를 복붙 후 수정.. 하고싶지만 우선은 Unreal c++에 익숙해지는게 먼저기 때문에 다시 손으로 따라쳐볼 예정이다. 거기에 달라지는 점들이 몇가지 있어서 추가 수정을 붙일 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 26일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-26%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-26%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 07 May 2026 12:01:28 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--248시간-34분-">*<em>누적 학습 시간 : 248시간 34분 *</em></h2>
<h4 id="📅-2026-05-07">📅 2026-05-07</h4>
<h1 id="c-캐릭터-생성">C++ 캐릭터 생성</h1>
<h2 id="playercharacterh">PlayerCharacter.h</h2>
<pre><code class="language-cpp">#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;GameFramework/Character.h&quot;
#include &quot;InputActionValue.h&quot;

#include &quot;PlayerCharacter.generated.h&quot;

class UInputMappingContext;
class UInputAction;
class USceneComponent;

UCLASS()
class METAL3D_API APlayerCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    // Sets default values for this character&#39;s properties
    APlayerCharacter();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

    // 인풋 컴포넌트 오버라이드
    virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=&quot;Input&quot;)
    TObjectPtr&lt;UInputMappingContext&gt; DefaultMappingContext;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=&quot;Input&quot;)
    TObjectPtr&lt;UInputAction&gt; MoveAction;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=&quot;Input&quot;)
    TObjectPtr&lt;UInputAction&gt; LookAction;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=&quot;Input&quot;)
    TObjectPtr&lt;UInputAction&gt; FireAction;

    virtual void NotifyControllerChanged() override;


    // Projectile
    UPROPERTY(EditDefaultsOnly, Category=&quot;Combat&quot;)
    TSubclassOf&lt;class AProjectile&gt; ProjectileClass;

    USceneComponent* FindMuzzlePoint() const;
    UPROPERTY(EditDefaultsOnly, Category=&quot;Combat&quot;)
    FName MuzzlePointComponentName = TEXT(&quot;MuzzlePoint&quot;);

    UPROPERTY(EditDefaultsOnly, Category=&quot;Combat&quot;)
    float AimTraceDistance = 10000.f;

    void Fire();

public:
    // Called every frame
    virtual void Tick(float DeltaTime) override;

private:
    void Move(const FInputActionValue&amp; Value);
    void Look(const FInputActionValue&amp; Value);
};
</code></pre>
<h2 id="playercharactercpp">PlayerCharacter.cpp</h2>
<pre><code class="language-cpp">// Fill out your copyright notice in the Description page of Project Settings.


#include &quot;Player/PlayerCharacter.h&quot;

#include &quot;EnhancedInputComponent.h&quot;
#include &quot;EnhancedInputSubsystems.h&quot;
#include &quot;Projectile/Projectile.h&quot;
#include &quot;GameFramework/PlayerController.h&quot;
#include &quot;Camera/CameraComponent.h&quot;
#include &quot;Kismet/GameplayStatics.h&quot;
#include &quot;GameFramework/PlayerController.h&quot;
#include &quot;DrawDebugHelpers.h&quot;

// Sets default values
APlayerCharacter::APlayerCharacter()
{
    // Set this character to call Tick() every frame.  You can turn this off to improve performance if you don&#39;t need it.
    PrimaryActorTick.bCanEverTick = true;
}

// Called when the game starts or when spawned
void APlayerCharacter::BeginPlay()
{
    Super::BeginPlay();
}

// Called every frame
void APlayerCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

// Called to bind functionality to input
void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    UEnhancedInputComponent* EnhancedInputComponent = Cast&lt;UEnhancedInputComponent&gt;(PlayerInputComponent);

    if (!EnhancedInputComponent)
    {
        return;
    }

    if (MoveAction)
    {
        EnhancedInputComponent-&gt;BindAction(
            MoveAction,
            ETriggerEvent::Triggered,
            this,
            &amp;APlayerCharacter::Move
        );
    }

    if (LookAction)
    {
        EnhancedInputComponent-&gt;BindAction(
            LookAction,
            ETriggerEvent::Triggered,
            this,
            &amp;APlayerCharacter::Look
        );
    }

    if (FireAction)
    {
        EnhancedInputComponent-&gt;BindAction(
            FireAction,
            ETriggerEvent::Triggered,
            this,
            &amp;APlayerCharacter::Fire
        );
    }
}

void APlayerCharacter::NotifyControllerChanged()
{
    Super::NotifyControllerChanged();

    APlayerController* PlayerController = Cast&lt;APlayerController&gt;(GetController());
    if (!PlayerController)
    {
        return;
    }

    ULocalPlayer* LocalPlayer = PlayerController-&gt;GetLocalPlayer();
    if (!LocalPlayer)
    {
        return;
    }

    UEnhancedInputLocalPlayerSubsystem* InputSubsystem = LocalPlayer-&gt;GetSubsystem&lt;
        UEnhancedInputLocalPlayerSubsystem&gt;();
    if (InputSubsystem &amp;&amp; DefaultMappingContext)
    {
        InputSubsystem-&gt;AddMappingContext(DefaultMappingContext, 0);
    }
}


void APlayerCharacter::Move(const FInputActionValue&amp; Value)
{
    const FVector2D MovementVector = Value.Get&lt;FVector2D&gt;();

    if (Controller == nullptr)
    {
        return;
    }

    const FRotator ControlRotation = Controller-&gt;GetControlRotation();
    const FRotator YawRotation(0.0f, ControlRotation.Yaw, 0.0f);

    const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
    const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

    AddMovementInput(ForwardDirection, MovementVector.Y);
    AddMovementInput(RightDirection, MovementVector.X);
}

void APlayerCharacter::Look(const FInputActionValue&amp; Value)
{
    const FVector2D LookAxis = Value.Get&lt;FVector2D&gt;();

    AddControllerYawInput(LookAxis.X);
    AddControllerPitchInput(-LookAxis.Y);
}

USceneComponent* APlayerCharacter::FindMuzzlePoint() const
{
    TArray&lt;USceneComponent*&gt; SceneComponents;

    GetComponents&lt;USceneComponent&gt;(SceneComponents);

    for (USceneComponent* Component : SceneComponents)
    {
        if (IsValid(Component) &amp;&amp; Component-&gt;GetName() == MuzzlePointComponentName.ToString())
        {
            return Component;
        }
    }

    return nullptr;
}


void APlayerCharacter::Fire()
{
    if (!ProjectileClass)
    {
        return;
    }

    APlayerController* PC = Cast&lt;APlayerController&gt;(GetController());
    if (!PC)
    {
        return;
    }

    int32 ViewportX = 0;
    int32 ViewportY = 0;

    PC-&gt;GetViewportSize(ViewportX, ViewportY);

    const float ScreenCenterX = ViewportX * 0.5f;
    const float ScreenCenterY = ViewportY * 0.5f;

    FVector WorldLocation;
    FVector WorldDirection;

    const bool bDeprojected = PC-&gt;DeprojectScreenPositionToWorld(
        ScreenCenterX,
        ScreenCenterY,
        WorldLocation,
        WorldDirection
    );

    if (!bDeprojected)
    {
        return;
    }

    const FVector TraceStart = WorldLocation;
    const FVector TraceEnd = TraceStart + WorldDirection * AimTraceDistance;

    FHitResult CameraHit;

    FCollisionQueryParams QueryParams;
    QueryParams.AddIgnoredActor(this);

    GetWorld()-&gt;LineTraceSingleByChannel(
        CameraHit,
        TraceStart,
        TraceEnd,
        ECC_Visibility,
        QueryParams
    );

    const FVector AimTarget = CameraHit.bBlockingHit ? CameraHit.ImpactPoint : TraceEnd;

    const USceneComponent* MuzzlePoint = FindMuzzlePoint();
    const FVector MuzzleLocation = MuzzlePoint-&gt;GetComponentLocation();

    const FVector FireDirection = (AimTarget - MuzzleLocation).GetSafeNormal();

    const FRotator SpawnRotation = FireDirection.Rotation();

    DrawDebugLine(
        GetWorld(),
        TraceStart,
        TraceEnd,
        FColor::Green,
        false,
        2.f,
        0,
        1.f
    );

    DrawDebugLine(
        GetWorld(),
        MuzzleLocation,
        MuzzleLocation + FireDirection * 3000.f,
        FColor::Red,
        false,
        2.f,
        0,
        2.f
    );

    FActorSpawnParameters SpawnParams;
    SpawnParams.Owner = this;
    SpawnParams.Instigator = this;
    SpawnParams.SpawnCollisionHandlingOverride =
        ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

    GetWorld()-&gt;SpawnActor&lt;AProjectile&gt;(
        ProjectileClass,
        MuzzleLocation,
        SpawnRotation,
        SpawnParams
    );
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 25일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-25%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-25%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 06 May 2026 11:20:14 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--236시간-34분-">*<em>누적 학습 시간 : 236시간 34분 *</em></h2>
<h4 id="📅-2026-05-06">📅 2026-05-06</h4>
<h1 id="ue-handbook"><a href="https://ue-docs-korea.github.io/UE-handbook/">UE-Handbook</a></h1>
<p>기존 에픽게임즈 언리얼 공식문서가 사실 초보가 보기에는 너무 어렵게 되어있다.
그래서 Docusaurus이용해서 handbook을 만들어봤다.
아이디어는 예전에 TS handbook이 도움이 많이됐던게 기억나서 만들어봤다.
유지보수를 계속 할지는 모르겠지만 혹시 필요한사람들이 많이들 봤으면 좋겠다.</p>
<h1 id="c-사용-imc-ia-등록">C++ 사용 IMC, IA 등록</h1>
<p>언리얼에서 pawn, character가 사용자의 입력대로 이동하려면 <code>Input Mapping Context</code>와 <code>Input Action</code>을 등록해주어야한다.</p>
<p>이전에는 블루프린트로 가볍게 했지만 C++에서는 어떻게 하는지 알아보자.</p>
<h2 id="imc-ia-생성">IMC, IA 생성</h2>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/1ce039c6-5013-45dc-bb2d-d740a4a25a89/image.png" alt=""></p>
<p>일단 IMC, IA생성은 블루프린트와 동일하다.</p>
<h3 id="imc">IMC</h3>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/a62d40d8-f824-4916-9e5a-4854ea790fe7/image.png" alt=""></p>
<h3 id="ia">IA</h3>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/b310024f-d2ee-4d62-bf6c-5c6cf01c9bf6/image.png" alt=""></p>
<h2 id="c-playercharacter-클래스-생성">C++ PlayerCharacter 클래스 생성</h2>
<p>Character 클래스를 부모로 만든 PlayerCharacter 클래스를 생성해준다.</p>
<h3 id="playercharacterh">PlayerCharacter.h</h3>
<pre><code class="language-cpp">#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;GameFramework/Character.h&quot;
#include &quot;APlayerCharacter.generated.h&quot;

UCLASS()
class METAL3D_API APlayerCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    // Sets default values for this character&#39;s properties
    APlayerCharacter();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:    
    // Called every frame
    virtual void Tick(float DeltaTime) override;

    // Called to bind functionality to input
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};</code></pre>
<h3 id="playercharactercpp">PlayerCharacter.cpp</h3>
<pre><code class="language-cpp">#include &quot;Player/PlayerCharacter.h&quot;

// Sets default values
APlayerCharacter::APlayerCharacter()
{
     // Set this character to call Tick() every frame.  You can turn this off to improve performance if you don&#39;t need it.
    PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void APlayerCharacter::BeginPlay()
{
    Super::BeginPlay();

}

// Called every frame
void APlayerCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

}</code></pre>
<p>생성 해준 뒤에는 미리 만들어놓은 IMC와 IA를 연결하는 작업을 진행해준다.</p>
<h2 id="playercharacterh-수정">PlayerCharacter.h 수정</h2>
<pre><code class="language-cpp">#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;GameFramework/Character.h&quot;
#include &quot;InputActionValue.h&quot;

#include &quot;PlayerCharacter.generated.h&quot;

class UInputMappingContext;
class UInputAction;

UCLASS()
class METAL3D_API APlayerCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    // Sets default values for this character&#39;s properties
    APlayerCharacter();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

    // 인풋 컴포넌트 오버라이드
    virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=&quot;Input&quot;)
    TObjectPtr&lt;UInputMappingContext&gt; DefaultMappingContext;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=&quot;Input&quot;)
    TObjectPtr&lt;UInputAction&gt; MoveAction;

    virtual void NotifyControllerChanged() override;

public:
    // Called every frame
    virtual void Tick(float DeltaTime) override;

private:
    void Move(const FInputActionValue&amp; Value);
};</code></pre>
<h2 id="해설-및-수정사항">해설 및 수정사항</h2>
<p>뭐가 많이 추가됐는데 한줄씩 보겠다.</p>
<h3 id="헤더-include">헤더 include</h3>
<pre><code class="language-cpp">include &quot;InputActionValue.h&quot;</code></pre>
<p>이 헤더는 아래서 정의한 <code>void Move(const FInputActionValue&amp; Value)</code>에서
<code>FInputActionValue</code>타입을 헤더의 상수 시그니처에서 사용 중이기 때문에 필요하다.</p>
<pre><code class="language-cpp">include &quot;PlayerCharacter.generated.h&quot;</code></pre>
<p>해당 헤더는는 언리얼 리플렉션 코드 생성용 헤더이다.
<code>*.generated.h</code>는 반드시 해당 헤더의 include중 마지막에 와야한다.</p>
<h3 id="전방선언">전방선언</h3>
<pre><code class="language-cpp">class UInputMappingContext;
class UInputAction;</code></pre>
<p><code>protected:</code>에서 선언된</p>
<pre><code class="language-cpp">UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=&quot;Input&quot;)
TObjectPtr&lt;UInputMappingContext&gt; DefaultMappingContext;

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=&quot;Input&quot;)
TObjectPtr&lt;UInputAction&gt; MoveAction;</code></pre>
<p>에서 포인터로써만 들고있기 때문에 헤더에서는 전체 include가 아닌 전방선언만 해도 오케이다.</p>
<h3 id="uclass">UCLASS()</h3>
<p>언리얼 리플렉션 시스템에 해당 클래스를 등록한다.</p>
<p>이게 있어야 BP생성시 해당 클래스를 부모로 선택 가능하고 <code>UPROPERTY</code>,<code>UFUNCTION</code> 같은 시스템도 작동한다.</p>
<h3 id="metal3d_api">METAL3D_API</h3>
<p>프로젝트 모듈 export/import 매크로
프로젝트명이 Metal3D라서 자동으로 붙은 것이다.</p>
<h3 id="aplayercharacter--public-acharacter">APlayerCharacter : public ACharacter</h3>
<p>ACharacter를 상속받은 PlayerCharacter 클래스</p>
<pre><code>AActor
자식 &gt; APawn
의 자식 &gt; ACharacter
의 자식 &gt; APlayerCharacter</code></pre><p>class 이름이 PlayerCharacter인데 <a href="https://ue-docs-korea.github.io/UE-handbook/prefixes-and-naming">왜 APlayerCharacter일까?</a></p>
<h3 id="generated_body">GENERATED_BODY()</h3>
<p>언리얼이 생성한 리플렉션 코드를 클래스 안에 삽입하는 매크로
<code>UCLASS()</code> 에는 필수로 들어간다.</p>
<h3 id="aplayercharacter">APlayerCharacter()</h3>
<pre><code class="language-cpp">APlayerCharacter();</code></pre>
<p>생성자</p>
<h3 id="beginplay">Beginplay</h3>
<pre><code class="language-cpp">virtual void BeginPlay() override;</code></pre>
<p>블프에서 많이 보던 BeginPlay 노드가 이 부분이다.</p>
<h3 id="setupplayerinputcomponent">SetupPlayerInputComponent</h3>
<pre><code class="language-cpp">virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;</code></pre>
<p>Pawn/Character가 Possess되어 입력 컴포넌트가 준비될 때 호출</p>
<p>블프와는 다르게 여기서 IMC, IA를 연결한다.</p>
<h3 id="uproperty">UPROPERTY(...)</h3>
<pre><code class="language-cpp">UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=&quot;Input&quot;)</code></pre>
<p><code>UPROPERTY</code>
일반 C++변수를 리플렉션 시스템에 등록하는 매크로
등록된 변수는 언리얼 에디터에서 노출되며 블루프린트 접근, 직렬화, GC 추적같은 언언리얼 기능의 대상이 된다.</p>
<p><code>EditDefaultsOnly</code>
인스턴스마다 수정하는 값이 아닌
클래스 기본값, BP defaults에서만 수정 가능하게 한다.</p>
<p><code>BlueprintReadOnly</code>
블루프린트에서 해당 값을 읽을 수는 있지만 수정할 수는 없다.</p>
<p><code>Category=&quot;Input&quot;</code>
언리얼 에디터 &gt; Detail 패널 내에서 Input 카테고리 아래에서 보여준다.</p>
<h3 id="tobjectptruinputmappingcontext"><code>TObjectPtr&lt;UInputMappingContext&gt;</code></h3>
<pre><code class="language-cpp">TObjectPtr&lt;UInputMappingContext&gt; DefaultMappingContext;</code></pre>
<p>UinputMappingContext 타입의 UObject를 가리키는 포인터.
IMC_PlayerCharacter같은 IMC 에셋을 참조한다.</p>
<h3 id="notifycontrollerchanged">NotifyControllerChanged()</h3>
<pre><code class="language-cpp">virtual void NotifyControllerChanged() override;</code></pre>
<p>pawn, character를 소유하거나 조종하는 Controller가 변경되었음을 알리는 함수</p>
<p>플레이어가 조종하면 <code>APlayerController</code> AI가 조종하면 <code>AAIController</code>가 붙는다.</p>
<p>해당 함수는 Controller가 변경되는 시점에 호출되는데 대표적으로</p>
<ul>
<li>PlayerController가 Character에 Possess됐을 때</li>
<li>Controller가 바뀌었을 때</li>
<li>Possess, UnPossess 흐름에서 Controller 상태가 달라졌을 때</li>
</ul>
<p>블프와 달리 IMC를 해당 함수에서 붙이게 되는데 이유가
<code>BeginPlay()</code>시점에 아직 Controller나 LocalPlayer가 준비되지 않았을 수 있기 때문이다.</p>
<h3 id="move">Move()</h3>
<p>이동 입력을 처리하는 <strong>사용자 정의 함수</strong></p>
<h2 id="playercharactercpp-수정">PlayerCharacter.cpp 수정</h2>
<pre><code class="language-cpp">#include &quot;Player/PlayerCharacter.h&quot;

#include &quot;EnhancedInputComponent.h&quot;
#include &quot;EnhancedInputSubsystems.h&quot;

// Sets default values
APlayerCharacter::APlayerCharacter()
{
    // Set this character to call Tick() every frame.  You can turn this off to improve performance if you don&#39;t need it.
    PrimaryActorTick.bCanEverTick = true;
}

// Called when the game starts or when spawned
void APlayerCharacter::BeginPlay()
{
    Super::BeginPlay();


}

// Called every frame
void APlayerCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

// Called to bind functionality to input
void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    UEnhancedInputComponent* EnhancedInputComponent = Cast&lt;UEnhancedInputComponent&gt;(PlayerInputComponent);

    if (!EnhancedInputComponent)
    {
        return;
    }

    if (MoveAction)
    {
        EnhancedInputComponent-&gt;BindAction(
            MoveAction,
            ETriggerEvent::Triggered,
            this,
            &amp;APlayerCharacter::Move
        );
    }
}

void APlayerCharacter::NotifyControllerChanged()
{
    Super::NotifyControllerChanged();

    APlayerController* PlayerController = Cast&lt;APlayerController&gt;(GetController());
    if (!PlayerController)
    {
        return;
    }

    ULocalPlayer* LocalPlayer = PlayerController-&gt;GetLocalPlayer();
    if (!LocalPlayer)
    {
        return;
    }

    UEnhancedInputLocalPlayerSubsystem* InputSubsystem = LocalPlayer-&gt;GetSubsystem&lt;
        UEnhancedInputLocalPlayerSubsystem&gt;();
    if (InputSubsystem &amp;&amp; DefaultMappingContext)
    {
        InputSubsystem-&gt;AddMappingContext(DefaultMappingContext, 0);
    }
}


void APlayerCharacter::Move(const FInputActionValue&amp; Value)
{
    const FVector2D MovementVector = Value.Get&lt;FVector2D&gt;();

    if (Controller == nullptr)
    {
        return;
    }

    const FRotator ControlRotation = Controller-&gt;GetControlRotation();
    const FRotator YawRotation(0.0f, ControlRotation.Yaw, 0.0f);

    const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
    const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

    AddMovementInput(ForwardDirection, MovementVector.Y);
    AddMovementInput(RightDirection, MovementVector.X);
}
</code></pre>
<h2 id="해설-및-수정사항-1">해설 및 수정사항</h2>
<h3 id="헤더">헤더</h3>
<pre><code class="language-cpp">#include &quot;EnhancedInputComponent.h&quot;</code></pre>
<p>헤더와는 다르게 EnhancedInputComponent를 포인터로 들고만 있는게 아니라
실제로 멤버 함수를 호출하거나 사용하기 때문에 필요하다.</p>
<h3 id="setupplayerinputcomponent-1">SetupPlayerInputComponent</h3>
<pre><code class="language-cpp">void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    UEnhancedInputComponent* EnhancedInputComponent = Cast&lt;UEnhancedInputComponent&gt;(PlayerInputComponent);

    if (!EnhancedInputComponent)
    {
        return;
    }

    if (MoveAction)
    {
        EnhancedInputComponent-&gt;BindAction(
            MoveAction,
            ETriggerEvent::Triggered,
            this,
            &amp;APlayerCharacter::Move
        );
    }
}</code></pre>
<p>pawn, character의 입력 바인딩 구성 함수</p>
<h3 id="supersetupplayerinputcomponentplayerinputcomponent">Super::SetupPlayerInputComponent(PlayerInputComponent)</h3>
<p><code>Super::</code>는 언리얼 C++에서 부모클래스의 별칭이다.
해당 클래스의 부모클래스에서 해야할 처리를 먼저 수행하겠다는 의미다.</p>
<p>언리얼 오버라이드 함수에서 특별한 이유가 없으면 <code>Super::...()</code>를 호출하는게 기본</p>
<h3 id="castuenhancedinputcomponent"><code>Cast&lt;UEnhancedInputComponent&gt;</code></h3>
<p>언리얼의 Cast&lt;&gt;는 c++의 dynamic_cast와 비슷한 목적이다.
다른 점은 Cast&lt;&gt;는 리플렉션 시스템을 기반으로 UObject 타입 변환을 수행한다.
블프에서 많이 보던 Cast to 다</p>
<p>캐스팅을 하는 이유는 함수 인자가 기본 타입이기 때문이다.</p>
<pre><code class="language-cpp">// 함수인자 UInputComponent &gt; UEnhancedInputComponent로 캐스팅
UEnhancedInputComponent* EnhancedInputComponent = 
            Cast&lt;UEnhancedInputComponent&gt;(PlayerInputComponent);</code></pre>
<p>실패할 수도 있기 때문에 바로 체크도 해준다.</p>
<pre><code class="language-cpp">    if (!EnhancedInputComponent)
    {
        return;
    }</code></pre>
<h3 id="moveaction-바인딩">MoveAction 바인딩</h3>
<pre><code class="language-cpp">if (MoveAction)
    {
        EnhancedInputComponent-&gt;BindAction(
            MoveAction,
            ETriggerEvent::Triggered,
            this,
            &amp;APlayerCharacter::Move
        );
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/6a591d67-2dcf-4746-8c37-70e8a833fdc3/image.png" alt=""></p>
<p>헤더에서 <code>UPROPERTY</code>로 등록한 MoveAction이 있다.
언리얼 에디터에서 실제로 IA_Move를 등록해준 IA를 MoveAction에 바인딩해준다.</p>
<p><code>MoveAction</code>
어떤 Input Action을 감시할지 지정</p>
<p><code>ETriggerEvent::Triggered</code>
Input Action이 어떤 상태일 때 호출할지 정함</p>
<p><code>this</code>
호출 대상 객체, 현재 PlayerCharacter를 말함</p>
<p><code>&amp;APlayerCharacter::Move</code>
호출할 멤버함수 포인터</p>
<h3 id="notifycontrollerchanged-1">NotifyControllerChanged</h3>
<pre><code class="language-cpp">void APlayerCharacter::NotifyControllerChanged()
{
    Super::NotifyControllerChanged();

    APlayerController* PlayerController = Cast&lt;APlayerController&gt;(GetController());
    if (!PlayerController)
    {
        return;
    }

    ULocalPlayer* LocalPlayer = PlayerController-&gt;GetLocalPlayer();
    if (!LocalPlayer)
    {
        return;
    }

    UEnhancedInputLocalPlayerSubsystem* InputSubsystem = LocalPlayer-&gt;GetSubsystem&lt;
        UEnhancedInputLocalPlayerSubsystem&gt;();
    if (InputSubsystem &amp;&amp; DefaultMappingContext)
    {
        InputSubsystem-&gt;AddMappingContext(DefaultMappingContext, 0);
    }
}</code></pre>
<p>어차피 여러번 할텐데 너무 길어져서 나중에 더 작성해야지</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: C++ TextRPG 개발 24일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-C-TextRPG-%EA%B0%9C%EB%B0%9C-24%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-C-TextRPG-%EA%B0%9C%EB%B0%9C-24%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 04 May 2026 09:57:04 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--224시간-34분-">*<em>누적 학습 시간 : 224시간 34분 *</em></h2>
<h4 id="📅-2026-05-04">📅 2026-05-04</h4>
<h1 id="textrpg-완성본">TextRPG 완성본</h1>
<p><a href="https://github.com/Carrymachine/CH2-quest/tree/main/TextRPG">깃허브 링크</a></p>
<h1 id="c-학습">C++ 학습</h1>
<h2 id="solid-원칙"><a href="https://velog.io/@bak_chun8/TIL-SOLID-%EC%9B%90%EC%B9%99">SOLID 원칙</a></h2>
<h2 id="inline-static과-constexpr"><a href="https://velog.io/@bak_chun8/TIL-inline-static">inline static과 constexpr</a></h2>
<h2 id="rider-ide-c-표준버전-변경법"><a href="https://velog.io/@bak_chun8/Rider-IDE-C%ED%91%9C%EC%A4%80-%EB%B2%84%EC%A0%84-%EB%B3%80%EA%B2%BD%EB%B2%95">Rider IDE C++ 표준버전 변경법</a></h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[Rider IDE C++표준 버전 변경법]]></title>
            <link>https://velog.io/@bak_chun8/Rider-IDE-C%ED%91%9C%EC%A4%80-%EB%B2%84%EC%A0%84-%EB%B3%80%EA%B2%BD%EB%B2%95</link>
            <guid>https://velog.io/@bak_chun8/Rider-IDE-C%ED%91%9C%EC%A4%80-%EB%B2%84%EC%A0%84-%EB%B3%80%EA%B2%BD%EB%B2%95</guid>
            <pubDate>Mon, 04 May 2026 06:16:52 GMT</pubDate>
            <description><![CDATA[<h4 id="📅-2026-05-04">📅 2026-05-04</h4>
<h1 id="rider-ide-c-표준-버전-변경">Rider IDE C++ 표준 버전 변경</h1>
<p>inline static 멤버변수 만들려고 하니까 C++ 17 이상에서만 가능하다고 에러가 표시됐다.</p>
<p>Unreal C++ 프로젝트는 자동으로 C++20이상으로 세팅되는데 일단 콘솔 프로젝트는 C++14로 세팅되는 것 같았다.</p>
<p>그래서 프로젝트 자체의 C++ 버전 업 하는 방법을 알아봤다.</p>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/29ed9cc7-9916-4d73-b849-f47764074bbb/image.png" alt=""></p>
<p>프로젝트 &gt; 우클릭 &gt; properties</p>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/9d47b79f-7090-4219-9acf-4ca42707e249/image.png" alt=""></p>
<p>C++20으로 변경</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: inline static과 constexpr]]></title>
            <link>https://velog.io/@bak_chun8/TIL-inline-static</link>
            <guid>https://velog.io/@bak_chun8/TIL-inline-static</guid>
            <pubDate>Mon, 04 May 2026 06:14:38 GMT</pubDate>
            <description><![CDATA[<h4 id="📅-2026-05-04">📅 2026-05-04</h4>
<h1 id="inline-static">inline static</h1>
<p>class 내무 멤버변수를 static으로 선언할 경우에 초기화가 불가능하다.
cpp에서 따로 초기화를 해줘야하는데</p>
<p>C++17부터 inline을 붙임으로써 선언과 동시에 초기화가 가능하다.</p>
<pre><code class="language-cpp">class Test
{
    static int a; // 오류X cpp에서 반드시 초기화
    static int a = 10; // 오류 발생

    inline static int a = 10; // 오류X cpp 초기화X

}</code></pre>
<h1 id="constexpr">constexpr</h1>
<pre><code class="language-cpp">class Test
{
    static int a;
    constexpr int a = 10;

    static constexpr int a = 10;

}</code></pre>
<h2 id="static">static</h2>
<pre><code class="language-cpp">class Test
{
    static int a;
}

int Test::a = 10;

Test::a = 20; // 가능</code></pre>
<ul>
<li>모든 객체가 하나의 x를 공유</li>
<li>런타임에 값 변경 가능</li>
<li>별도 정의 필요 (C++17 이전 기준)</li>
</ul>
<h2 id="constexpr-1">constexpr</h2>
<pre><code class="language-cpp">class Test
{
    constexpr int a = 10;
}


Test::a = 20; // 불가능</code></pre>
<ul>
<li>컴파일 타임에 값 확정</li>
<li>암묵적으로 const</li>
<li>변경 불가</li>
<li>함수에도 적용 가능 (constexpr function)</li>
</ul>
<h2 id="static-constexpr">static constexpr</h2>
<p>클래스에 속한 컴파일 타임 상수</p>
<ul>
<li>static &gt; 클래스 전체에서 공유</li>
<li>constexpr &gt; 컴파일 타임 상수</li>
</ul>
<h3 id="1-컴파일-타임-값">1. 컴파일 타임 값</h3>
<pre><code class="language-cpp">class Test
{
    static constexpr int a = 10;
}

int array[Test::a];</code></pre>
<p>컴파일 타임에 배열의 크기 요구시 static만으로는 불가능(런타임 초기화 해야하기 때문)</p>
<h3 id="2-헤더에서-안전하게-정의">2. 헤더에서 안전하게 정의</h3>
<pre><code class="language-cpp">class Test
{
    static constexpr int a = 10;
}</code></pre>
<p>ODR문제 X
별도의 cpp 필요X</p>
<h4 id="odr">ODR?</h4>
<p>One Definition Rule</p>
<h2 id="inline-static-vs-static-constexpr">inline static vs static constexpr</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>inline static</th>
<th>static constexpr</th>
</tr>
</thead>
<tbody><tr>
<td>변경 가능</td>
<td>O</td>
<td>X</td>
</tr>
<tr>
<td>컴파일 타임 상수</td>
<td>X</td>
<td>O</td>
</tr>
<tr>
<td>메모리</td>
<td>있음</td>
<td>거의 없음</td>
</tr>
<tr>
<td>용도</td>
<td>상태값</td>
<td>상수</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: SOLID 원칙]]></title>
            <link>https://velog.io/@bak_chun8/TIL-SOLID-%EC%9B%90%EC%B9%99</link>
            <guid>https://velog.io/@bak_chun8/TIL-SOLID-%EC%9B%90%EC%B9%99</guid>
            <pubDate>Mon, 04 May 2026 06:13:58 GMT</pubDate>
            <description><![CDATA[<h4 id="📅-2026-05-04">📅 2026-05-04</h4>
<h1 id="solid-원칙">SOLID 원칙</h1>
<p>TextRPG를 제작하면서 특정 물체(캐릭터, 적 등)를 Actor라는 최상위 클래스에서 기본적인 요소를 관리했다.
Unreal처럼 만들어보고 싶다는 이유에서 였는데 여러 문제가 발생했는데
해당 Actor가</p>
<ul>
<li>공격이 가능한가?</li>
<li>피해를 입을 수 있는가?</li>
</ul>
<p>위 두가지 때문에 캐릭터(폰)라는 한정된 역할만 할 수 있었다.
그때 같은 기수의 Dr. Dragon &#39;D&#39; Water 님께서 SOLID 원칙을 알려주셨다.</p>
<p>이전에도 FE엔지니어를 하면서 Repository 패턴을 사용할 일이 있었는데 그때 들어본 원칙이기도 하지만 제대로 알아볼 생각은 하지 않았었다.
실제로 3년넘게 실무하면서 class 사용해본 일이 손에 꼽을 정도로 FE에서는 class를 잘 사용하지 않는다.</p>
<h2 id="ssrp---single-responsibility-principle">S(SRP) - Single Responsibility Principle</h2>
<p><strong>단일 책임 원칙</strong>
클래스는 1개의 이유로만 변경되어야 한다.</p>
<pre><code class="language-cpp">class Player {
public:
    void Update();
    void Render();
    void SaveToFile();   // 저장 책임
    void SendNetwork();  // 네트워크 책임
};

// 분리
struct PlayerState {
    int hp;
    Vector3 position;
};

class PlayerSystem {
public:
    void Update(PlayerState&amp;);
};

class SaveSystem {
public:
    void Save(const PlayerState&amp;);
};</code></pre>
<h2 id="oocp---openclosed-principle">O(OCP) - Open/Closed Principle</h2>
<p><strong>개방/폐쇄 원칙</strong>
확장에는 열려있고 수정에는 닫혀있어야한다.</p>
<pre><code class="language-cpp">// 무기 추가할 때마다 값 추가해야함 
int CalcDamage(std::string type) {
    if (type == &quot;sword&quot;) return 10;
    if (type == &quot;gun&quot;) return 20;
}


// 개선
struct WeaponData
{
    int damage;
}

class Weapon
{
    WeaponData weaponData;

public:
    int ApplyDamage()
    {
        return weaponData.damage;
    }

}</code></pre>
<h2 id="llsp---liskov-substitution-principle">L(LSP) - Liskov Substitution Principle</h2>
<p><strong>리스코프 치환 원칙</strong>
자식 클래스는 부모 클래스를 완전히 대체 가능해야한다.</p>
<pre><code class="language-cpp">class Character
{
public:
    virtual void Attack() = 0;
};

class Pacifist : public Character
{
public:
    // 부모 Character는 Attack 호출 시 함수가 실행되는 것을 기대함
    // Pacifist에서 예외처리로 프로그램을 비정상 종료시키기 때문에 LSP 원칙에 어긋남
    void Attack() override { throw; } 
}



// 개선
class IAttackable
{
public:
    virtual void Attack() = 0;
}

class NPC { }; // 공격 없음
class Box { }; // 공격 없음

class Warrior : public IAttackable
{
    void Attack() override { ... }
}
</code></pre>
<h2 id="iisp---interface-segregation-principle">I(ISP) - Interface Segregation Principle</h2>
<p><strong>인터페이스 분리 원칙</strong>
클라이언트는 자신이 사용하지 않는 인터페이스에 의존해서는 안된다.</p>
<pre><code class="language-cpp">class IUnit
{
public:
    virtual void Move() = 0;
    virtual void Attack() = 0;
    virtual void Fly() = 0; // 거북이 Actor에 Fly???
}


// 개선
class MoveComponent { void Move(); }
class AttackComponent { void Attack(); }
class FlyComponenty { void Fly(); }

class Unit
{
    MoveComponent* move;
    AttackComponent* attack;
    // 해당 Unit은 Fly 기능이 없으므로 객체 생성X
}</code></pre>
<h2 id="ddip---dependency-inversion-principle">D(DIP) - Dependency Inversion Principle</h2>
<p><strong>의존성 역전 원칙</strong>
고수준 모듈은 저수준 모듈에 의존해서는 안되고 둘 다 추상화에 의존해야한다.</p>
<pre><code class="language-cpp">// Game이 더 고수준 모듈
class Game
{
    // 저수준 모듈 Sword에 의존하면 원칙 위배
    Sword sword;
}

// 개선

class Weapon
{
public:
    virtual void Attack() = 0;
}

class Game
{
    std::unique_ptr&lt;Weapon&gt; weapon;

    Game(satd::unique_ptr&lt;Weapon&gt; w) : weapon(std::move(w)) { };

}</code></pre>
<h2 id="총-정리">총 정리</h2>
<p>SRP → “변경 이유 1개” 시스템 단위 분리
OCP → “코드 말고 데이터로 확장” 데이터 기반 확장
LSP → “상속 대신 능력 분리” 상속 최소화
ISP → “인터페이스 → 컴포넌트로” 컴포넌트
DIP → “new 하지 말고 주입해라” DI + smart pointer</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: C++ TextRPG 개발 23일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-C-TextRPG-%EA%B0%9C%EB%B0%9C-23%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-C-TextRPG-%EA%B0%9C%EB%B0%9C-23%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 30 Apr 2026 11:35:34 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--212시간-34분-">*<em>누적 학습 시간 : 212시간 34분 *</em></h2>
<h4 id="📅-2026-04-30">📅 2026-04-30</h4>
<h1 id="가상함수와-추상클래스">가상함수와 추상클래스</h1>
<h5 id="참고--httpsbaggu4402tistorycom15">참고:  <a href="https://baggu4402.tistory.com/15">https://baggu4402.tistory.com/15</a></h5>
<p>TRPG 개발 중 캐릭터의 직업클래스와 부모 클래스를 만들면서 사용할 일이 있어서 알아봤다.</p>
<p>C++에서는 java처럼 abstact같은 키워드가 없다.</p>
<p>순수가상함수가 포함되어 있는 클래스는 추상클래스로 취급되고 순수가상함수는 상속받는 자식클래스에서 반드시 구현해야한다.</p>
<p><strong>순수 가상 함수는 부모의 강제 규칙, 오버라이딩은 자식의 실제 구현.</strong></p>
<pre><code class="language-cpp">class Job
{
public:
    virtual Stat SetBaseState() const = 0;
    virtual std::string GetJobName() const = 0;
}

class Warrior : public Job
{
    Stat SetBaseState() const override
    {
        ...구현...
    }
    std::string GetJobName() const override
    {
        ...구현...
    }
}
</code></pre>
<h2 id="ts-interface-생각하면-편함">TS interface 생각하면 편함!</h2>
<p>사실 class는 뭐 HttpClient 구현체 만들때나 쓰고 일부 백엔드 작업하면서나 써봤지 알고는 있지만 쓸일이 그닥 많지는 않았다.</p>
<p>어렵게 생각하지말고 내 입장에서는
추상클래스 = interface 라고 생각하면 기초수준에서의 이해는 되는 것 같다.</p>
<p>다만 다른점은 C++ 추상클래스는 클래스가 구현을 포함할 수 있고 TS interface는 구현을 위한 명세만을 다룬다는 점이다.
또한 TS interface는 ? 를 사용해 해당 함수가 상속받는 클래스 내부에 존재할수도 존재하지 않을 수도 있는 상태를 만들 수 있는데</p>
<p>C++에는 그런 방법이 없고 SOLID 원칙에서 안티패턴으로 간주하고 리팩토링을 하는 것을 권장하고있다.</p>
<h2 id="에러케이스">에러케이스</h2>
<pre><code class="language-cpp">#pragma once
#include &lt;functional&gt;
#include &lt;map&gt;
#include &lt;string&gt;

#include &quot;Actor/Stat.h&quot;

enum class JobState { Warrior, Mage };

class Job
{
    std::map&lt;char, std::function&lt;void()&gt;&gt; jobFuncMap;

public:
    virtual std::string Test();
    virtual Stat SetBaseStat() const = 0;
    virtual std::string GetJobName() const = 0;
};</code></pre>
<h4 id="순수가상함수-아니면-되는거-아니었나요">순수가상함수 아니면 되는거 아니었나요?</h4>
<blockquote>
<p>순수가상함수는 구현할 필요가 없습니다. 왜냐?
자식클래스에서 <strong>반드시</strong> 구현해야 하기 때문에 추상클래스에서 구현할 필요가 없습니다.
하지만 일반 가상함수는 컴파일러가 어딘가에 구현이 있다고 생각하고 링크를 하다가 구현이 없으면 LNK에러가 발생하게 됩니다.</p>
</blockquote>
<pre><code class="language-cpp">#pragma once
#include &lt;functional&gt;
#include &lt;map&gt;
#include &lt;string&gt;

#include &quot;Actor/Stat.h&quot;

enum class JobState { Warrior, Mage };

class Job
{
    std::map&lt;char, std::function&lt;void()&gt;&gt; jobFuncMap;

public:
    virtual std::string Test() { return &quot;&quot; } ;
    virtual Stat SetBaseStat() const = 0;
    virtual std::string GetJobName() const = 0;
};</code></pre>
<p>그래서 위와같이 구현을 추가해주면 에러가 사라집니다.</p>
<h1 id="랜덤">랜덤</h1>
<p>던전 이벤트 구현을 위해 C++에서는 랜덤을 어떻게 돌리나 검색을 해봤다.
그랬더니 나는 단순하게 Rand() 함수라던가.. 를 생각했는데
무슨 메르센 트위스터 알고리즘을 이용한 난수 생성기를 이용하는게 좋다더라.....
당최 무슨 소린지 모르겠지만 일단 써보고 랜덤을 사용해야할 경우에 계속 사용하다보면 익숙해지지 않을까 싶다.............</p>
<h2 id="사용법">사용법</h2>
<pre><code class="language-cpp">// 하드웨어 기반 난수 생성 시드
static std::random_device rd;

// tm19937 = 난수엔진
// 메르센 트위스터 알고리즘을 사용함
// gen(seed)를 통해 생성 &gt; gen(rd())
static std::mt19937 gen(rd());

// 균등 분포 정수 생성기(?????)
// dist() 를 통해 범위 지정
static std::uniform_real_distribution&lt;&gt; dist(0, 2);

dungeonEventState = static_cast&lt;DungeonEvent&gt;(dist(gen))

0 &gt; Nothing
1 &gt; EncounterEnemy
2 &gt; FindTreasure</code></pre>
<h1 id="static-키워드">static 키워드</h1>
<p>static은 lifetime과 scope를 고정한다.</p>
<h2 id="함수-내부-static">함수 내부 static</h2>
<pre><code class="language-cpp">void A()
{
    static int a = 10; // 함수 끝나도 살아있음
}</code></pre>
<p>최초 1회만 초기화 가능하며 함수 호출이 끝나면 값이 살아있다.</p>
<h2 id="class-내부-static">class 내부 static</h2>
<pre><code class="language-cpp">class A 
{
    static int count; // 해당 class로 생성된 객체와 모두 공유됨
}

A a;
A b;

a.count // 10
b.count // 10

//하지만 static 멤버변수 호출은 아래가 정석 다 공유되니까!
A::count  // 10</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: C++ TextRPG 개발 22일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-C-TextRPG-%EA%B0%9C%EB%B0%9C-22%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-C-TextRPG-%EA%B0%9C%EB%B0%9C-22%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 29 Apr 2026 12:10:12 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--200시간-34분-">*<em>누적 학습 시간 : 200시간 34분 *</em></h2>
<h4 id="📅-2026-04-29">📅 2026-04-29</h4>
<h1 id="directory-구조">Directory 구조</h1>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/9e77907d-9c56-4004-ab8d-d6dd381ff1ee/image.png" alt=""></p>
<p>C++에서 어떤 구조가 좋을지는 아직 학습을 못해서 일단 생각대로 만들어보려고 한다.
main() 함수는 TextRPG.cpp 안에 있다.</p>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/5d22a566-2443-4c02-b953-04023a48f694/image.png" alt=""></p>
<p>main() 함수는 오직 GameStart() 함수만 담고있다.</p>
<h1 id="gamestate-및-선택지-분기처리">GameState 및 선택지 분기처리</h1>
<p>여러 방법이 떠올랐는데 가장 기본적으로는 cin으로 입력받고 switch/case로 분기하는 것이었다.</p>
<p>근데 뭔가 최선은 아닌 듯 해서 방법을 찾아보니 Function Map이라는 자료구조가 있다는 걸 알게됐다.</p>
<h2 id="function-map">Function Map</h2>
<blockquote>
<p>입력 값(key)에 따라 실행할 함수를 연결 해놓은 자료구조</p>
</blockquote>
<pre><code class="language-cpp">switch (input)
{
case &#39;1&#39;: Attack(); break;
case &#39;2&#39;: Defend(); break;
}</code></pre>
<p>위와같은 switch case를 데이터 구조로 변경한 것이다.</p>
<h2 id="사용법">사용법</h2>
<pre><code class="language-cpp">#include &lt;map&gt;
#include &lt;functional&gt;

// 선언
std::map&lt;char, std::function&lt;void()&gt;&gt; funcMap;</code></pre>
<h2 id="실사용-예제">실사용 예제</h2>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;functional&gt;
#include &lt;map&gt;

void Attack() { std::cout &lt;&lt; &quot;Attack&quot;; } 
void Defend() { std::coud &lt;&lt; &quot;Defend&quot;; }

void main()
{
    std::map&lt;char, std::function&lt;void()&gt;&gt; funcMap;

    funcMap[&#39;q&#39;] = Attack;
    funcMap[&#39;w&#39;] = Defend;

    char input;

    std::cin &gt;&gt; input;

    funcMap[input]();
}</code></pre>
<p>결과적으로 함수를 값처럼 사용할 수 있다는 점에서 Delegate와 비슷하지만 이벤트 시스템은 직접 구현해야된다거나 하는 점에서 Delegate는 상위 개념으로 볼 수 있다.</p>
<p>그리고 코드를 딱 보면 알겠지만 switch/case 쓰는 것 보다 코드가 클린해진다. </p>
<p>그리고 이후 입력&gt;출력 데이터 추가에도 매우 용이하다.</p>
<h2 id="가상함수">가상함수</h2>
<p>Stat SetBaseStat() const override</p>
<p>override
부모클래스의 가상함수를 재정의 하겠다는 뜻</p>
<p>virtual Stat SetBaseStat() const = 0;</p>
<p>= 0 하는 이유 &gt; 이유없음 그냥 C++ 문법 얘는 순수가상함수다 라는걸 나타냄</p>
<p>내일  GameState &gt; GetPlayerCharacter부터 시작</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: Unreal C++ 21일차]]></title>
            <link>https://velog.io/@bak_chun8/TIL-Unreal-C-21%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@bak_chun8/TIL-Unreal-C-21%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 28 Apr 2026 12:32:06 GMT</pubDate>
            <description><![CDATA[<h2 id="누적-학습-시간--188시간-34분-">*<em>누적 학습 시간 : 188시간 34분 *</em></h2>
<h4 id="📅-2026-04-28">📅 2026-04-28</h4>
<h1 id="언리얼에서-c로-액터클래스-생성">언리얼에서 C++로 액터클래스 생성</h1>
<h2 id="클래스-생성">클래스 생성</h2>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/be0bbe5b-88e8-48e0-818d-e26a9e9bcedb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/5ab029e4-5d62-4bc9-ad0b-be66f050e903/image.png" alt=""></p>
<p>에디터 &gt; 상단 Tools &gt; New C++ Class &gt; Actor &gt; MyActor로 생성</p>
<h2 id="콘텐츠-브라우저에서-bp-생성">콘텐츠 브라우저에서 BP 생성</h2>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/a62675ed-3117-411a-a155-f1b948899703/image.png" alt=""></p>
<p>콘텐츠 브라우저 &gt; 우클릭 &gt; ALL CLASSES &gt; MyActor 검색</p>
<h1 id="location이동-및-rotation-구현">Location이동 및 Rotation 구현</h1>
<h2 id="myactorh">MyActor.h</h2>
<pre><code class="language-cpp">#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;GameFramework/Actor.h&quot;
#include &quot;MyActor.generated.h&quot;

UCLASS()
class PROJECT1TOGETHER_API AMyActor : public AActor
{
    GENERATED_BODY()

public:    
    // Sets default values for this actor&#39;s properties
    AMyActor();

    FVector PrevLocation;
    FVector CurrentLocation;
    float TotalDistance;
    int MoveCount;

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:    
    // Called every frame
    virtual void Tick(float DeltaTime) override;



    void Move();
    void Turn();
    void TriggerEvent();
};
</code></pre>
<h2 id="myactorcpp">MyActor.cpp</h2>
<pre><code class="language-cpp">#include &quot;MyActor.h&quot;

// Sets default values
AMyActor::AMyActor()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don&#39;t need it.
    PrimaryActorTick.bCanEverTick = true;
}

// Called when the game starts or when spawned
void AMyActor::BeginPlay()
{
    Super::BeginPlay();

}

// Called every frame
void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

}

void AMyActor::Move()
{
    FVector Target;

    // -50 ~ 50 randomeFloat
    Target.X = FMath::FRandRange(-50.0, 50.0);
    Target.Y = FMath::FRandRange(-50.0, 50.0);
    Target.Z = 0;

    AddActorWorldOffset(Target);

}

void AMyActor::Turn()
{
    FRotator DeltaRotation;

    DeltaRotation.Yaw = FMath::FRandRange(-180.0, 180.0);
    DeltaRotation.Pitch = 0;
    DeltaRotation.Roll = 0;

    AddActorWorldRotation(DeltaRotation);
}
</code></pre>
<h1 id="과제-1">과제 1</h1>
<ul>
<li><ol>
<li>이동할 때마다 좌표, MoveCount 출력</li>
</ol>
</li>
<li><ol start="2">
<li>50% 확률 이동</li>
</ol>
</li>
<li><ol start="3">
<li>10회 이동 후 최종결과 리포트</li>
</ol>
</li>
</ul>
<h2 id="myactorh-1">MyActor.h</h2>
<pre><code class="language-cpp">// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;GameFramework/Actor.h&quot;
#include &quot;MyActor.generated.h&quot;

UCLASS()
class PROJECT1TOGETHER_API AMyActor : public AActor
{
    GENERATED_BODY()

public:    
    // Sets default values for this actor&#39;s properties
    AMyActor();

    FVector PrevLocation;
    FVector CurrentLocation;
    float TotalDistance;
    int MoveCount;

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:    
    // Called every frame
    virtual void Tick(float DeltaTime) override;



    void Move();
    void Turn();
    void TriggerEvent(int key);
};</code></pre>
<h2 id="myactorcpp-1">MyActor.cpp</h2>
<pre><code class="language-cpp">// Fill out your copyright notice in the Description page of Project Settings.


#include &quot;MyActor.h&quot;

// Sets default values
AMyActor::AMyActor()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don&#39;t need it.
    PrimaryActorTick.bCanEverTick = true;
    TotalDistance = 0.0f;
    MoveCount = 0;
}

// Called when the game starts or when spawned
void AMyActor::BeginPlay()
{
    Super::BeginPlay();
    PrevLocation = GetActorLocation();

    for (int32 i = 0; i &lt; 10; i++)
    {
        bool CanMove = FMath::RandBool();
        if (CanMove)
        {
            TriggerEvent(i);
        }
    }
}

// Called every frame
void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

}

void AMyActor::Move()
{
    FVector Target;

    // -50 ~ 50 randomeFloat
    Target.X = FMath::FRandRange(-50.0, 50.0);
    Target.Y = FMath::FRandRange(-50.0, 50.0);
    Target.Z = 0;

    AddActorWorldOffset(Target);

}

void AMyActor::Turn()
{
    FRotator DeltaRotation;

    DeltaRotation.Yaw = FMath::FRandRange(-180.0, 180.0);
    DeltaRotation.Pitch = 0;
    DeltaRotation.Roll = 0;

    AddActorWorldRotation(DeltaRotation);
}

void AMyActor::TriggerEvent(int key)
{
    int32 BaseCountKey = MoveCount * 10 + key ;
    MoveCount++;
    Move();
    Turn();
    CurrentLocation = GetActorLocation();
    TotalDistance += FVector::Dist(PrevLocation,CurrentLocation);


    if (GEngine)
    {
        GEngine -&gt; AddOnScreenDebugMessage(BaseCountKey + 1, 5, FColor::Red, FString::Printf(TEXT(&quot;Prev: %ls&quot;),*PrevLocation.ToString()));
        GEngine -&gt; AddOnScreenDebugMessage(BaseCountKey + 2, 5, FColor::Red, FString::Printf(TEXT(&quot;Current: %ls&quot;),*CurrentLocation.ToString()));
        GEngine -&gt; AddOnScreenDebugMessage(BaseCountKey + 3, 5, FColor::Red, FString::Printf(TEXT(&quot;TotalDist: %f&quot;),TotalDistance));
        GEngine -&gt; AddOnScreenDebugMessage(BaseCountKey + 4, 5, FColor::Red, FString::Printf(TEXT(&quot;MoveCount: %d&quot;),MoveCount));
    }


    UE_LOG(LogTemp, Warning, TEXT(&quot;Init: %s&quot;), *PrevLocation.ToString());
    UE_LOG(LogTemp, Warning, TEXT(&quot;Current: %s&quot;), *CurrentLocation.ToString());
    UE_LOG(LogTemp, Warning, TEXT(&quot;Acc: %f&quot;), TotalDistance);
    UE_LOG(LogTemp, Warning, TEXT(&quot;MoveCount: %d&quot;), MoveCount);

    PrevLocation = CurrentLocation;
}
</code></pre>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/f0fd7110-eacb-4079-8ce2-f181fa89aadf/image.png" alt=""></p>
<h1 id="에디터-종료-후-액터-재생성-오류">에디터 종료 후 액터 재생성 오류</h1>
<p>언리얼 에디터 + Rider로 신나게 작업하다가 에디터, Rider 닫고 재시작을 했더니
레벨에서 액터가 사라지고 BP_MyActor를 드래그앤드랍으로 배치시 실패 오류 로그가 발생한다.</p>
<p>튜터님께 질문드린결과 실무에서도 C++ 프로젝트 열때 Rider에서 여는게 일반적이라고 알려주셨다.</p>
<p>에디터에서 열 경우 에디터 하단 Contents Browser에 C++ Classes 폴더가 없는데 이는 C++ 컴파일이 되지 않아서 인식해지 못해서 발생한다고 생각했다.</p>
<p><img src="https://velog.velcdn.com/images/bak_chun8/post/e6f9eabe-4a27-43d7-aa79-3e9ab195100d/image.png" alt=""></p>
<p>그래서 튜터님 말씀대로 언리얼과 Rider를 종료하고
Rider로 프로젝트를 연 뒤 우측 상단 프로젝트명 옆 실행 버튼이 활성화될 때 까지 기다렸다가 누르면 빌드가 시작되면서 언리얼에디터가 열리는데</p>
<p>이때 문제가 해결된 것을 볼 수 있다. 이때부터는 에디터부터 열어도 오류가 발생하지 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git사용법]]></title>
            <link>https://velog.io/@bak_chun8/Git%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@bak_chun8/Git%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Tue, 28 Apr 2026 05:03:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/bak_chun8/post/2ab1bd7e-96d9-47aa-a2d8-ea4241b01cad/image.png" alt=""></p>
<p><code>git init</code>
해당 폴더를 레포지토리에 업로드 하고싶어요.</p>
<blockquote>
<p>git repository에 push하기 위해 initializing</p>
</blockquote>
<p><code>git add README.md</code>
이 저장소는 <del>~</del>를 위한 저장소입니다.
사용법은 아래와 같습니다.</p>
<blockquote>
<p>git repository 사용 설명서 추가</p>
</blockquote>
<p><code>git add &lt;file-name&gt;</code>
해당 commit에는 이런 파일들이 변경(생성, 추가, 삭제 등)되었습니다.</p>
<blockquote>
<p>commit할 파일들 선택</p>
</blockquote>
<p><code>git commit -m &quot;first commit&quot;</code>
해당 커밋은 &quot;first commit&quot; 이라는 내용이 변경되었습니다.</p>
<blockquote>
<p>repository에 push하기 전 업로드할 파일 확정</p>
</blockquote>
<p><code>git branch -M main</code>
main이라는 브랜치를 만들어서 해당 브랜치를 master branch로 설정할께요.</p>
<blockquote>
<p>해당 레포지토리의 master branch를 생성 이름은 main</p>
</blockquote>
<p><code>git remote add origin https://github.com/Carrymachine/Test.git</code>
앞으로 수정되는 내용들은 모두 여기에 업로드할께요.</p>
<blockquote>
<p>업로드할 주소를 origin이라는 이름으로 추가할께요.</p>
</blockquote>
<p><code>git push -u origin main</code>
origin의 main 브랜치에 업로드해주세요.</p>
<blockquote>
<p>origin의 main 브랜치에 push -u는 업스트림인데 신경안써도 무방</p>
</blockquote>
<p><code>git switch -c &quot;브랜치 명&quot;</code>
현재 기준의 코드베이스에서 &quot;브랜치 명&quot;으로 브랜치 생성 후 이동할께요.</p>
<blockquote>
<p>(main에서 명령어 입력 시) main을 기준으로 &quot;브랜치 명&quot;의 브랜치를 생성후 이동</p>
</blockquote>
<pre><code>git pull
또는
git pull &lt;브랜치 명&gt;</code></pre><p>현재 선택된 브랜치의 내용을 현재 코드베이스로 가져올께요.</p>
<blockquote>
<p>(git pull만 할 경우) 현재 브랜치의 코드베이스 pull
(&lt;브랜치 명&gt;함께 입력 시) 현재 브랜치에 &lt;브랜치 명&gt; 코드베이스 pull</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>