<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>lee_raccoon.log</title>
        <link>https://velog.io/</link>
        <description>영차 영차</description>
        <lastBuildDate>Tue, 24 Sep 2024 14:03:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. lee_raccoon.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/lee_raccoon" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[GameEffect 적용해보기]]></title>
            <link>https://velog.io/@lee_raccoon/GameEffect-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@lee_raccoon/GameEffect-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 24 Sep 2024 14:03:49 GMT</pubDate>
            <description><![CDATA[<p>직접 간단한 포션을 GameEffect를 사용하여 만들어보자.</p>
<p>대충의 계획을 세워보면 아래와 같다.</p>
<ol>
<li><code>EffectActor</code>를 만들어 범용적으로 사용할 기본 클래스를 만들어 준다.</li>
<li><code>EffectActor</code>를 블루프린트로 파생하여 포션 클래스를 만든다.</li>
<li>포션 클래스에 맞는 <code>UGameplayEffect</code>클래스를 만들어서 적용 시켜준다.</li>
</ol>
<p>차근차근 해보자</p>
<h2 id="gameplayeffect">GameplayEffect</h2>
<p>우선 Actor를 만들어볼건데 <code>UGameplayEffect</code>가 우리가 생각하는 GameplayEffect를 만들어 놓은 클래스이다.</p>
<p><code>UGameplayEffect</code>의 정의를 피킹해보자
<img src="https://velog.velcdn.com/images/lee_raccoon/post/6c1db66d-2421-44d1-a72d-6c517a317a12/image.png" alt=""></p>
<p>무수한 코드..
훑어보다보면 익숙한 친구들이 보인다.
<img src="https://velog.velcdn.com/images/lee_raccoon/post/d6d3d16d-2173-4cc9-a0d3-2ba5939c6f75/image.png" alt=""></p>
<p>Attributes에 영향을 줄 수 있는 Modifiers와 Executions들이 잘 있는 것을 볼 수 있다.
블루프린트로 만들어서 사용할 것이기 때문에 코드를 쓸 일은 지금 없지만..</p>
<p>그래서 이 GameplayEffect를 어떻게 적용시키냐?</p>
<h3 id="asc의-applygameeffect">ASC의 ApplyGameEffect</h3>
<p>그것은 <code>UAbilitySystemComponent</code>의 ApplyGameplayEffect 친구들을 사용하면 된다.
<img src="https://velog.velcdn.com/images/lee_raccoon/post/00f50fe3-6e60-4c28-a363-ab636549ad94/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_raccoon/post/ca88d014-8d28-47e3-a4b4-57b1e6e1e106/image.png" alt=""></p>
<p>무수한 함수들이 있는데 지금 사용할 것은 이 중 <code>ApplyGameplayEffectSpecToSelf</code>이다.
포션은 그냥 체력 Attibutes에 단순한 Add를 플레이어 <strong>본인</strong>에게 해주는 것이기 때문에.
만약 플레이어가 적에게 대미지를 입히거나 한다면 <code>ApplyGameplayEffectSpecToTarget</code>이 될 것이다.
근데 함수 인자에 <code>FGameplayEffectSpecHandle</code>이라는 못보던 구조체가 있다.
뭐지?</p>
<h3 id="gameplayspechandle">GameplaySpecHandle</h3>
<p>GameplaySpec은 GameplayEffect의 인스턴스 개념이다.
GameplayEffect를 게임에 실제로 적용시키기 위해 사용되며 GameplayEffect에 설정된 기본 속성, 파라미터 값 등을 포함하고 있다.
GameplaySpecHandle는 이를 감싸고 있는 타입이다.
FString과 문자열의 관계랑 비슷한거 같기도 하고?</p>
<p>그래서 이를 어떻게 만들어서 넣어주느냐!
ASC에는 <code>MakeOutgoingSpec</code>이라는 함수가 있다.
<img src="https://velog.velcdn.com/images/lee_raccoon/post/aef141ed-6602-43bf-a4d1-b2825ca50225/image.png" alt=""></p>
<p>근데 문제는 여기 인자에도 이상한게 있다.
<code>FGameplayEffectContextHandle</code>.. 이건 뭐지</p>
<h3 id="fgameplayeffectcontext">FGameplayEffectContext</h3>
<p>GameplayEffect의 문맥.
말그대로다.
이게 어떤 상황에서 일어났는지를 담고 있는 구조체이다.</p>
<p>안에 무엇이 있는고 하니
<img src="https://velog.velcdn.com/images/lee_raccoon/post/a9d3f1bd-620e-4d5c-858b-ebce97801de3/image.png" alt=""></p>
<p>대충 효과를 받는 쪽과 입히는 쪽, 리플리케이트 되지 않는 어빌리티 등
확실히 Effect에 대한 문맥을 나타내주고 있는 것 같다.</p>
<p>얘도 ASC에 <code>MakeEffectContext</code>라는 함수가 있어서 이걸로 만들면 된다.
만들어주고 특별한 내용은 없으니 SourceObject만 포션 클래스로 지정해주고 넘겨주자.</p>
<pre><code class="language-cpp">FGameplayEffectContextHandle EffectContextHandle = TargetASC-&gt;MakeEffectContext();
EffectContextHandle.AddSourceObject(this);</code></pre>
<p>그럼 이제 Spec도 만들 수 있다.</p>
<pre><code class="language-cpp">const FGameplayEffectSpecHandle EffectSpecHandle = TargetASC-&gt;MakeOutgoingSpec(GameplayEffectClass, 1.f, EffectContextHandle);</code></pre>
<h3 id="블루프린트의-적절한-사용">블루프린트의 적절한 사용</h3>
<p>EffectActor라는 기본 클래스를 만들어서 사용하면 재사용성이 커지니 아주 좋다고 할 수 있다.
그렇다면 GameplayEffect를 적용하는 것도 재사용 가능하게 만들어야하니 BlueprintCallable 함수로 만들어 주자.</p>
<pre><code class="language-cpp">void AAuraEffectActor::ApplyEffectToTarget(AActor* TargetActor, TSubclassOf&lt;UGameplayEffect&gt; GameplayEffectClass)
{
    UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
    if (TargetASC == nullptr) return;

    check(GameplayEffectClass);
    FGameplayEffectContextHandle EffectContextHandle = TargetASC-&gt;MakeEffectContext();
    EffectContextHandle.AddSourceObject(this);
    const FGameplayEffectSpecHandle EffectSpecHandle = TargetASC-&gt;MakeOutgoingSpec(GameplayEffectClass, 1.f, EffectContextHandle);
    TargetASC-&gt;ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
}</code></pre>
<p>포션에 닿으면 먹게끔 포션에 Sphere Collision을 달아주고 Begin Overlap에서 위 함수를 호출해준다.</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/46fe68d4-d8c9-4a96-aebd-12958817351f/image.png" alt=""></p>
<p>Effect Class는 어디서 났냐고? 블루프린트로 만들었당
<img src="https://velog.velcdn.com/images/lee_raccoon/post/4cd8c571-cd12-4325-9740-f95341c5e340/image.png" alt="">
만들어주면 블루프린트 디테일 패널에서 GameplayEffect를 수정할 수 있다.
<img src="https://velog.velcdn.com/images/lee_raccoon/post/0297b3b9-53d1-4798-8bff-b744f8b39e70/image.png" alt=""></p>
<p>아까 C++ 코드 베이스에서 본 그 두녀석이 얘네가 아닐까 싶다.
Attribute를 Health로 설정하고 Add로 25를 더해준다.</p>
<p>똑같은 방법으로 마나 포션도 만들 수 있다.</p>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/0378eb87-32e5-4020-baa6-9fc1ec228df8/image.gif" alt=""></p>
<p>성공!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Gameplay Effects 개념]]></title>
            <link>https://velog.io/@lee_raccoon/Gameplay-Effects</link>
            <guid>https://velog.io/@lee_raccoon/Gameplay-Effects</guid>
            <pubDate>Thu, 12 Sep 2024 06:38:01 GMT</pubDate>
            <description><![CDATA[<p><a href="https://dev.epicgames.com/documentation/ko-kr/unreal-engine/gameplay-effects-for-the-gameplay-ability-system-in-unreal-engine?application_version=5.3">게임플레이 어트리뷰트 및 게임플레이 이펙트</a></p>
<h1 id="gameplay-effects">Gameplay Effects</h1>
<h2 id="그게-뭔데">그게 뭔데</h2>
<p><code>UGameplayEffect</code>라는 타입의 오브젝트가 있다.
이 오브젝트는 <code>Attrubutes</code>와 <code>GameplayTags</code>를 바꾸는데 사용된다.</p>
<p><code>UGameplayEffect</code>의 서브클래스를 만들지 않는다.
블루프린트로도, 코드 베이스로도 만들지 않으며 오로지 변수를 통해 설정되도록 설계되었다.</p>
<h2 id="주요-프로퍼티">주요 프로퍼티</h2>
<h3 id="modifier">Modifier</h3>
<p>얘는 Attributes를 변경할 때 어떤 계산, 로직 등을 게임 플레이에 맞춰 수행할 수 있다.
사용 할 수 있는 연산자는 아래와 같다.</p>
<ul>
<li>Add</li>
<li>Multiply</li>
<li>Devide</li>
<li>Override : 받은 값으로 덮어 씌우기</li>
</ul>
<p>연산에 사용할 수 있는 값 타입에는 아래와 같은 것들이 있다.</p>
<ul>
<li><strong>Scalable Float</strong> : 그냥 기본적인 수</li>
<li><strong>Attributes Based</strong> : Attribute의 값을 가져오는 것</li>
<li><strong>Custom Calculation Class</strong> (MMC) : 스탯에 따라 효과가 다르게 적용 되는 등의 계산에 쓰임</li>
<li><strong>Set by Caller</strong> : GameplayTag를 이용해서 값을 외부 요인에 따라 변경할 수 있다.</li>
</ul>
<h3 id="execution">Execution</h3>
<p>얘도 Attribute 변경할 때 쓰고 특히 복잡한 계산에 쓴다.</p>
<ul>
<li><strong>GameplayEffectExecutionCalculation</strong> :
장황한 이름만큼 Attribute를 변경하는데 가장 강력한 방법이다.
복잡한 계산에 쓰인다. (피해량 계산을 생각해보면 공격력, 방어력, 디버프 등 많은 것을 고려해야하는데 그런 거 아닐까)</li>
</ul>
<h3 id="duration">Duration</h3>
<ul>
<li>Instant -&gt; 즉각적 효과 (예: 포션먹고 HP 회복)</li>
<li>Duration -&gt; 일시적 효과 (예: 디퍼브 맞고 20초동안 방어력 감소)</li>
<li>Infinite -&gt; 영구적 효과 (예: 레벨업으로 능력치 상승)</li>
</ul>
<p>저번에 Attribute에는 Base Value와 Current Value 두가지가 있다고 했다.
Instant는 Base Value에 영향을 미치고
Duration, Infinite는 Current Value에 영향을 미친다. 즉, 영향을 받기 전으로 되돌아 갈수가 있다.
Periodic이라는 것도 있는데, 얘는 Base Value에 점진적으로 영향을 끼치는 것이다.
활용할 때 생각하면서 사용하자!</p>
<h3 id="grant-abilities">Grant Abilities</h3>
<p>게임 플레이 이펙트가 적용되면 어빌리티를 부여할 수 있다.
이를 Execution과 함께 적용하면 구체적인 게임플레이를 구현할 수 있다.
예를 들어 빙속성 공격을 맞은 경우 액터가 수속성일 때 &#39;얼어붙음&#39;이라는 어빌리티를 얻어 해당 시간 동안 얼어붙은 이펙트와 대미지를 받은 그런 로직을 구현할 수 있다.</p>
<h3 id="stacking">Stacking</h3>
<p>Overflow 상태를 처리하는 정책이다.
Overflow 상태란, 원래 타깃에 적용되어 있던 게임플레이 이펙터가 포화상태일 때 새로운 효과를 발동 시키는 것이다.
우리가 평소에 아는 스택과 비슷한 개념이라고 생각하면 될 듯 하다.
스몰더가 스택을 다 쌓으면 처형 능력을 얻는 것 처럼</p>
<h2 id="effect와-attribute의-상호작용">Effect와 Attribute의 상호작용</h2>
<p>예를 들어 Effect로 Health라는 Attribute를 깎았다.
그럼 우리는 Health가 0이 되면 죽게 될 것이라고 예상하는데 실제로는 여기서는 아무 일도 일어나지 않는다.
0이 되면 죽는 로직을 추가해주어야 하는데 이를 쉽게 추가할 수 있도록 여러 가상 함수를 오버라이드 할 수 있다.</p>
<p><code>PreAttributeChange / PreAttributeBaseChange</code> : 수정 직전에 호출되는 함수
<code>PreGameplayEffectExecute</code> : 수정 직전에 제안된 수정을 거부하거나 변경할 수 있다.
<code>PostGameplayEffectExecute</code> : 수정 직후 변경사항에 대한 반응을 여기서 처리할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UI / Widget 설계 (Model View Controller)]]></title>
            <link>https://velog.io/@lee_raccoon/UI-Widget-%EC%84%A4%EA%B3%84-Model-View-Controller</link>
            <guid>https://velog.io/@lee_raccoon/UI-Widget-%EC%84%A4%EA%B3%84-Model-View-Controller</guid>
            <pubDate>Wed, 11 Sep 2024 08:27:49 GMT</pubDate>
            <description><![CDATA[<h1 id="ui--widget-설계">UI / Widget 설계</h1>
<h2 id="model-view-controller-개념-도입">Model View Controller 개념 도입</h2>
<p>웹 개발 경험이 있기 때문에 MVC 패턴에 대해서는 잘 알고 있다.
쉽게 말하자면 유저가 컨트롤러를 통해 모델을 조작하고 모델이 뷰에게 값을 주면 뷰가 그것을 보여주는 형식이다.</p>
<p>조금은 다를지 몰라도 이 개념을 게임 UI에서 다룰 수 있다.
또한 실제 Fortnite같은 AAA게임에서도 이를 사용하고 있다고 한다.
<img src="https://velog.velcdn.com/images/lee_raccoon/post/dc20f1a4-96a2-420b-99db-e9aac1617f50/image.png" alt=""></p>
<ul>
<li>View
우리가 보는 UI, 위젯</li>
<li>Model
View에 나타나는 다양한 수치들  ( 예 : 체력, 마나, 레벨, 경험치 )</li>
</ul>
<p>그렇다면 View 입장에서는 Model을 알아야 이를 보여줄 수 있는데
Model의 값을 View에 주는 방법에는 수많은 방법이 있다.</p>
<p>하지만 View와 Model 사이에 델리게이트를 바인드 시켜서 값을 주고 받는다고 해도 바인딩 시킬 때는 View 입장에서 Model을 알아야하기 때문에 의존성이 발생할 수 밖에 없다.
그렇다면 View의 재사용성이 떨어지고 모듈화가 어렵게 된다.
그러니까 A라는 View를 사용할 때 A라는 모델밖에 사용할 수 없다는 뜻.
확장성을 챙기는 것은 항상 달콤한 일이기에.. 이를 해결하고 싶어진다.</p>
<p>그렇기 때문에 Controller로써 클래스를 하나 만든 후, 여기에서 모델의 데이터를 처리하여 View로 Broadcast 해주는 역할을 한다면?
Model 자신은 어떤 Controller와 연결되어 있는지 몰라도 된다.
Controller 자신은 어떤 View와 연결되어 있는지 몰라도 된다.</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/e9d585c4-9709-4f9c-9e55-5a23ec357a7e/image.png" alt=""></p>
<p>그럼, Model 입장에서는 Controller를 바꿔도 Model 입장에서는 변경 사항이 전혀 없다는 뜻
Controller 입장에서는 View를 바꿔도 변경 사항이 전혀 없다는 뜻이다.
이제 변경사항이 생겨도 View나 Model 자체를 갈아엎기보다는 조금의 수정 사항만 갈아끼우면 된다.</p>
<h2 id="실제-구현해보기">실제 구현해보기</h2>
<p>View : 우리가 사용할 위젯이다.
Controller : 위젯 컨트롤러라는 클래스를 직접 UObject로 만들어 줄 것이다.
Model : 필요한 값을 가지고 있는 클래스이다. 현 예시에서는 Attribute Set이다.</p>
<h3 id="구현-방법">구현 방법</h3>
<ul>
<li>View(Widget)은 각자 자신의 Controller를 가지고 있으며, 필요한 값을 Controller에서 구독한다. (Delegate Bind)</li>
<li>Controller는 Model의 변경을 감지하면 이를 변경값과 함께 Broadcast한다.</li>
<li>Model은 값이 변경될 때마다 이를 Broadcast한다. (Attribute Set은 이미 이게 구현돼있다.)</li>
</ul>
<h3 id="widget-만들기">Widget 만들기</h3>
<p>프로젝트에서 사용할 위젯으로써 위젯 클래스를 하나 만들어 주자.
앞으로 프로젝트에서 사용하는 모든 위젯은 이 위젯을 상속받는다는 개념이다.</p>
<pre><code class="language-cpp">class AURA_API UAuraUserWidget : public UUserWidget
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable)
    void SetWidgetController(UObject* InWidgetController);

    //모든 위젯은 Controller를 가지고 있다.
    UPROPERTY(BlueprintReadOnly)
    TObjectPtr&lt;UObject&gt; WidgetController;

protected:
    //Controller가 설정돼었을 때 실행할 함수, Controller가 있음을 보장받음으로 여기서 필요한 값을 구독한다.
    UFUNCTION(BlueprintImplementableEvent)
    void WidgetControllerSet();
};</code></pre>
<h3 id="controller-만들기">Controller 만들기</h3>
<p>Controller Base를 만들어주자.
값을 받고 처리해주고를 다 여기서 하기 때문에 여기서 할 일이 좀 많다.</p>
<pre><code class="language-cpp">/*
이 구조체는 Controller가 의존성을 가질 Model들을 모아둔 것이다.
Controller를 생성할 때 해당 구조체를 넘겨주어 초기화 할 수 있도록 한다.
*/
USTRUCT(BlueprintType)
struct FWidgetControllerParams
{
    GENERATED_BODY()

    FWidgetControllerParams() {}
    FWidgetControllerParams(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
        : PlayerController(PC), PlayerState(PS), AbilitySystemComponent(ASC), AttributeSet(AS){}

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TObjectPtr&lt;APlayerController&gt; PlayerController = nullptr;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TObjectPtr&lt;APlayerState&gt; PlayerState = nullptr;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TObjectPtr&lt;UAbilitySystemComponent&gt; AbilitySystemComponent = nullptr;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TObjectPtr&lt;UAttributeSet&gt; AttributeSet = nullptr;
};</code></pre>
<p>그 다음 Controller의 헤더이다.</p>
<pre><code class="language-cpp">UCLASS()
class AURA_API UAuraWidgetController : public UObject
{
    GENERATED_BODY()

public:
    //구조체를 넣으면 해당 값으로 Model들을 설정한다.
    UFUNCTION(BlueprintCallable)
    void SetWidgetControllerParams(const FWidgetControllerParams&amp; WCParams);

    //처음 Widget에 Controller가 설정되었을 때 호출할 함수이다.
    //이걸 안해주면 위젯 내의 값은 모델의 값이 변하기 전에는 초기값으로 남기 때문에
    virtual void BroadcastInitialValues();

    //Model의 값을 구독하는 함수
    virtual void BindCallbackToDependencies();
protected:
    //Model들
    UPROPERTY(BlueprintReadOnly, Category = &quot;WidgetController&quot;)
    TObjectPtr&lt;APlayerController&gt; PlayerController;

    UPROPERTY(BlueprintReadOnly, Category = &quot;WidgetController&quot;)
    TObjectPtr&lt;APlayerState&gt; PlayerState;

    UPROPERTY(BlueprintReadOnly, Category = &quot;WidgetController&quot;)
    TObjectPtr&lt;UAbilitySystemComponent&gt; AbilitySystemComponent;

    UPROPERTY(BlueprintReadOnly, Category = &quot;WidgetController&quot;)
    TObjectPtr&lt;UAttributeSet&gt; AttributeSet;
    //
};</code></pre>
<p>여기까지 했다면 이제 어떤 위젯을 만들 것이냐에 따라서
이 클래스를 상속받아 Controller를 만들어주면 된다.</p>
<h2 id="예시--hud-만들기">예시 ) HUD 만들기</h2>
<p>예시로 간단히 체력, 마나를 보여주는 HUD를 만들어보자</p>
<h3 id="위젯-만들기">위젯 만들기</h3>
<p>만들어둔 베이스 위젯으로 위젯 블루프린트를 만들어 사용하였다.
체력, 마나 통 위젯은 뭐 프로그레스 바로 대충 만들 수 있을 것이니 생략하고
이를 붙여서 만들었다.
<img src="https://velog.velcdn.com/images/lee_raccoon/post/82bcf1eb-5cd8-4764-887f-78b66c89fa73/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_raccoon/post/cf439518-c67b-4c67-a2a9-4b91c200e75a/image.png" alt=""></p>
<p>체력 마나 위젯도 각자의 Controller가 존재하기 때문에 이를 설정해준다.</p>
<h3 id="controller-만들기-1">Controller 만들기</h3>
<p>체력 하나만 예시로 들어 만들어보겠다.</p>
<pre><code class="language-cpp">DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChangedSignature, float, NewHealth);

UCLASS(BlueprintType, Blueprintable)
class AURA_API UOverlayWidgetController : public UAuraWidgetController
{
    GENERATED_BODY()

public:
    virtual void BroadcastInitialValues() override;
    virtual void BindCallbackToDependencies() override;

    //View가 구독할 델리게이트
    UPROPERTY(BlueprintAssignable, Category = &quot;GAS|Attributes&quot;)
    FOnHealthChangedSignature OnHealthChanged;
protected:
    //모델의 Broadcast에 바인드할 함수
    void HealthChanged(const FOnAttributeChangeData&amp; Data) const;
};

//cpp
void UOverlayWidgetController::BroadcastInitialValues()
{
    const UAuraAttributeSet* AuraAttributeSet = CastChecked&lt;UAuraAttributeSet&gt;(AttributeSet);
    OnHealthChanged.Broadcast(AuraAttributeSet-&gt;GetHealth());
}

void UOverlayWidgetController::HealthChanged(const FOnAttributeChangeData&amp; Data) const
{
    OnHealthChanged.Broadcast(Data.NewValue);
}

void UOverlayWidgetController::BindCallbackToDependencies()
{
    const UAuraAttributeSet* AuraAttributeSet = CastChecked&lt;UAuraAttributeSet&gt;(AttributeSet);

    AbilitySystemComponent-&gt;GetGameplayAttributeValueChangeDelegate(
        AuraAttributeSet-&gt;GetHealthAttribute()).AddUObject(this, &amp;UOverlayWidgetController::HealthChanged);
}</code></pre>
<p>똑같은 형식으로 값만 바꿔주면 마나도 만들어줄 수 있다.</p>
<h3 id="hud-클래스-만들기">HUD 클래스 만들기</h3>
<p>자 이제 필요한 위젯과 컨트롤러를 다 만들었으니 HUD 클래스를 만들어서 이 위젯과 컨트롤러를 연결시켜주자.</p>
<pre><code class="language-cpp">//WidgetController가 있으면 반환하고 없으면 생성한다. 싱글톤 개념이랄까
UOverlayWidgetController* AAuraHUD::GetOverlayWidgetController(const FWidgetControllerParams&amp; WCParams)
{
    if (OverlayWidgetController == nullptr)
    {
        OverlayWidgetController = NewObject&lt;UOverlayWidgetController&gt;(this, OverlayWidgetControllerClass);
        OverlayWidgetController-&gt;SetWidgetControllerParams(WCParams);
        OverlayWidgetController-&gt;BindCallbackToDependencies();

        return OverlayWidgetController;
    }

    return OverlayWidgetController;
}

//Model에 관한 인자들을 받아서 Controller를 View에 연결해주는 함수이다.
void AAuraHUD::InitOverlay(APlayerController* PC, APlayerState* PS, UAbilitySystemComponent* ASC, UAttributeSet* AS)
{
    checkf(OverlayWidgetClass, TEXT(&quot;Overlay Widget Class가 초기화되지 않았습니다. BP_AuraHUD에서 설정해주세요&quot;));
    checkf(OverlayWidgetControllerClass, TEXT(&quot;Overlay Widget Controller Class가 초기화되지 않았습니다. BP_AuraHUD에서 설정해주세요&quot;))

    UUserWidget* Widget = CreateWidget&lt;UUserWidget&gt;(GetWorld(), OverlayWidgetClass);
    OverlayWidget = Cast&lt;UAuraUserWidget&gt;(Widget);

    const FWidgetControllerParams WidgetControllerParams(PC, PS, ASC, AS);
    UOverlayWidgetController* WidgetController = GetOverlayWidgetController(WidgetControllerParams);

    OverlayWidget-&gt;SetWidgetController(WidgetController);
    WidgetController-&gt;BroadcastInitialValues();

    Widget-&gt;AddToViewport();
}</code></pre>
<h3 id="그래서-이걸-어디서-호출해요">그래서 이걸 어디서 호출해요?</h3>
<p>이걸 부를 수 있는 가장 적절한 곳은 Model이 존재한다는 것을 보장 받을 수 있는 곳에서 호출해주는 것이 좋다.</p>
<p>지금같은 경우에는 Attribute Set이 Model인 경우인데, 이것이 확실히 존재한다고 할 수 있는 곳은 캐릭터에서 AbilityActorInfo를 설정할 때라고 할 수 있겠다.
Controller에서 필요한 파라미터인 값들도 모두 여기서 알 수 있기 때문에 아주 좋은 곳이다.
<code>InitAbilityActorInfo</code>가 뭐하는 건지 모르겠으면 ASC 게시글로..</p>
<pre><code class="language-cpp">void AAuraCharacter::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);

    //Init Ability Actor Info for the Server
    InitAbilityActorInfo();
}

void AAuraCharacter::OnRep_PlayerState()
{
    Super::OnRep_PlayerState();

    //Init Ability Actor Info for the Cilent
    InitAbilityActorInfo();
}
void AAuraCharacter::InitAbilityActorInfo()
{
    AAuraPlayerState* AuraPlayerState = GetPlayerState&lt;AAuraPlayerState&gt;();
    check(AuraPlayerState);
    //오너와 아바타를 설정해주는 함수이다. 왜 PlayerState인지는 이미 알아보았다.
    AuraPlayerState-&gt;GetAbilitySystemComponent()-&gt;InitAbilityActorInfo(AuraPlayerState, this);
    AbilitySystemComponent = AuraPlayerState-&gt;GetAbilitySystemComponent();
    AttributeSet = AuraPlayerState-&gt;GetAttributeSet();

    //멀티의 경우 자신 이외의 캐릭터는 컨트롤러가 null일 수 있기 때문에 assert가 아닌 null체크만 해준다.
    if (AAuraPlayerController* AuraPlayerController = Cast&lt;AAuraPlayerController&gt;(GetController()))
    {
        if (AAuraHUD* AuraHUD = Cast&lt;AAuraHUD&gt;(AuraPlayerController-&gt;GetHUD()))
        {
            AuraHUD-&gt;InitOverlay(AuraPlayerController, AuraPlayerState, AbilitySystemComponent, AttributeSet);
        }
    }
}</code></pre>
<p>이렇게만 해주면?
<img src="https://velog.velcdn.com/images/lee_raccoon/post/d326fca3-1076-4a6e-b6e2-3878feb23be8/image.png" alt=""></p>
<p>야호~ HUD 완성!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 등굣길]]></title>
            <link>https://velog.io/@lee_raccoon/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%93%B1%EA%B5%A3%EA%B8%B8</link>
            <guid>https://velog.io/@lee_raccoon/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%93%B1%EA%B5%A3%EA%B8%B8</guid>
            <pubDate>Mon, 09 Sep 2024 08:10:14 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/42898#">프로그래머스 등굣길</a>
<img src="https://velog.velcdn.com/images/lee_raccoon/post/8e9f262a-a121-4ffc-bcb4-583a8efb12dc/image.png" alt=""></p>
<h2 id="접근-방식">접근 방식</h2>
<p>문제를 보고 처음 떠오른 방법은 아래와 같다.</p>
<p>m*n 크기의 map 배열을 만들어서
map[m][n] 는 m,n에 갈 수 있는 경로의 수로 두고</p>
<p>map[m][n] = 왼쪽, 위쪽을 더한 값이므로 (오른쪽과 아래로 밖에 이동할 수 없기 때문에 왼쪽과 위쪽 칸의 경로 수를 더하면 된다)
<code>map[m][n] = map[m-1][n] + map[m][n-1]</code>을 하면 되겠지? 하고 바로 구현해봤다.</p>
<pre><code class="language-cpp">int getRouteCnt(vector&lt;vector&lt;int&gt;&gt;&amp; map, int m, int n)
{
    //범위 밖이므로 0
    if (m &lt; 0 || n &lt; 0) return 0;

    //침수 구역은 못가므로 0
    if (map[m][n] == -1)
    {
        return 0;
    }

    //값이 존재한다면 return
    if (map[m][n] != -2)
    {
        return map[m][n] % 1000000007;
    }

    //아직 경로를 구하지 않은 칸을 만나면 경로를 구하여 return
    return getRouteCnt(map, m - 1, n) + getRouteCnt(map, m, n - 1);
}

int solution(int m, int n, vector&lt;vector&lt;int&gt;&gt; puddles) {
    int answer = 0;

    //-1 : 침수 -2 : 아직 구하지 않은 경우의 수
    vector&lt;vector&lt;int&gt;&gt; map(m, vector&lt;int&gt;(n, -2));

    map[0][0] = 1;

    //침수지역 -1로 초기화
    for (int i = 0; i &lt; puddles.size(); i++)
    {
        map[puddles[i][0] - 1][puddles[i][1] - 1] = -1;
    }

    answer = getRouteCnt(map, m - 1, n - 1);

    return answer;
}</code></pre>
<p>그 결과는?
<img src="https://velog.velcdn.com/images/lee_raccoon/post/ff2f160a-9961-41e4-b531-9eb3bc2dec5a/image.png" alt=""></p>
<p>아.
100*100이라서 방심하고 그냥 쉽게쉽게 구했더니 재귀의 힘을 버티지 못하고 시간 초과가 떠버렸다.</p>
<p>흠.. 이러면 BFS를 한번 써보면 되겠다는 생각이 들었다.
인접한 노드가 아래와 오른쪽만으로 한정시키면 딱 현재 문제의 경우와 똑같다.
BFS로 탐색한다면 자연스레 왼쪽과 위쪽의 값이 존재하게 되므로
식은 원래 식과 동일하게 넣어주었다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;queue&gt;

using namespace std;

int bfs(vector&lt;vector&lt;int&gt;&gt; &amp;map, int m, int n)
{
    vector&lt;vector&lt;int&gt;&gt; isVisited(m+1,vector&lt;int&gt;(n+1, 0));
    queue&lt;pair&lt;int,int&gt;&gt; q;
    q.push({1,1});
    map[1][1] = 1;
    isVisited[1][1] = 1;

    //오른쪽, 아래
    int dx[2] = {1, 0};
    int dy[2] = {0, 1};

    while(!q.empty())
    {
        pair&lt;int,int&gt; cur = q.front();
        q.pop();

        for(int i=0;i&lt;2;i++)
        {
            int nextX = cur.first + dx[i];
            int nextY = cur.second + dy[i];

            //범위 밖인 경우 제외
            if(nextX &gt; m || nextY &gt; n) continue;
            if(map[nextX][nextY] != -1 &amp;&amp; isVisited[nextX][nextY] == 0)
            {
                q.push({nextX,nextY});
                isVisited[nextX][nextY] = 1;
                int up = ( map[nextX][nextY-1] == -1) ? 0 : map[nextX][nextY-1];
                int left = ( map[nextX-1][nextY] == -1) ? 0 : map[nextX-1][nextY];
                map[nextX][nextY] = (up + left) % 1000000007;
            }            
        }
    }

    return map[m][n];
}

int solution(int m, int n, vector&lt;vector&lt;int&gt;&gt; puddles) {
    int answer = 0;

    vector&lt;vector&lt;int&gt;&gt; map(m+1,vector&lt;int&gt;(n+1,0));

    //침수지역은 -1로 초기화
    for(int i=0;i&lt;puddles.size();i++)
    {
        map[puddles[i][0]][puddles[i][1]] = -1;
    }

    answer = bfs(map, m, n);
    return answer;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/f1ab6af3-6933-4638-8fbe-665985ace0e7/image.png" alt=""></p>
<p>매우 빨라진 것을 확인할 수 있었다.</p>
<h2 id="회고">회고</h2>
<p>생각나는 접근 방식 중에 가장 빠른 알고리즘을 생각해낼 수 있도록 하자..
그리고 실행시간에 대한 직관력이 더 필요하다.
물론 여기에는 몇초 이내라는 말이 명시되어있지는 않았지만
일단 재귀함수를 쓴 것은 별로 좋은 생각이 아니었던 것 같다.</p>
<p>BFS를 처음부터 썼더라면 참 깔끔하게 끝났을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] N으로 표현]]></title>
            <link>https://velog.io/@lee_raccoon/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-N%EC%9C%BC%EB%A1%9C-%ED%91%9C%ED%98%84</link>
            <guid>https://velog.io/@lee_raccoon/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-N%EC%9C%BC%EB%A1%9C-%ED%91%9C%ED%98%84</guid>
            <pubDate>Sun, 08 Sep 2024 14:03:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/1a947c98-d6fc-4f29-aed1-5f6395df3942/image.png" alt=""></p>
<p>접근 방식에 문제가 있어서 결국 문제를 풀지 못하고 다른 사람의 풀이를 참고했던 문제이다..</p>
<h2 id="접근했던-방식">접근했던 방식</h2>
<p>일단은 다이나믹 프로그래밍 방식을 떠올렸다. 문제는 그 방식이 살짝 잘못됐었다.
D[i]를 자연수 i를 만드는 최소 N의 갯수로 둬버렸던 것이다.
그렇게 해서 만들었던 점화식이
D[i] = min(D[i+N] +1, D[i-N] +1, D[i*N] +1, D[i/N] +1, D[i+1] + 2, D[i-1] + 2)
뭐 이런 괴랄한 식을 세워버렸다.
근데 이 방식의 가장 문제점은 이미 구한 D[i]를 다시 갱신할 방법이 없다는 것
예를 들어 N이 5라고 쳤을 때, 12를 만들 때의 수식에는 55가 들어가는데 이를 탐색할 능력이 없다는 것이다.
그래서 D배열에 미리 5, 55, 555, 5555 ... 이들을 초기화 시켜놓고 문제에 접근을 해보았으나..
이 방식으로는 탐색 범위도 상당히 애매모호해지기 때문에 D배열을 얼마나 잡아야할지 감이 잡히지 않았다.
아무튼 이 문제에 적합한 해결 방식이 아니었다.</p>
<p>하지만 나는 D[i]를 자연수 i를 만드는 최소 N의 갯수로 둔 것에 너무 사로잡혀버려서 문제 풀이가 막혀버렸다.
풀이를 보고나서야 최솟값이 8보다 크면 -1을 return 합니다.의 진정한 의미를 깨닫게 되었다.</p>
<h2 id="풀이-방법">풀이 방법</h2>
<p><strong>D[i]를 N을 i개 조합해서 만들 수 있는 수들의 집합으로</strong> 만들면 된다.
그렇다면 D[i] 은 i개 이하의 모든 사칙연산 조합으로 볼 수 있다.
말이 좀 이상한 것 같은데, 쉽게 말해 그냥 <strong>모든 경우의 수를 조합</strong>하는 것.</p>
<p>예를 들어 D[2]이면 N 3개로 조합할 수 있는 모든 식을 구하면 된다.
이 말은 D[0] D[1]의 사칙연산으로 만들 수 있는 모든 조합 (정확히는 순열, 나누기나 빼기는 순서에 따라 값이 달라지니까), 그리고 N이 3개 있는 수로 나타낼 수 있다.</p>
<p>이 D[i]를 설정하는 법을 몰라서 냅다 몇시간을 버렸다..
결국 DP의 핵심은 무엇을 D[i]로 지정하고 그것을 어떻게 구할 것인가. 인데 사실상 아무것도 하지 못한 것이다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;unordered_set&gt;

using namespace std;

int solution(int N, int number) {
    vector&lt;unordered_set&lt;int&gt;&gt; d(8);
    for (int i = 0; i &lt; 8; ++i)
    {
        //N, NN, NNN 형식의 수를 만들어서 추가해줌
        d[i].insert(stoi(string(i + 1, &#39;0&#39; + N))); 

        //i이하의 d로 모든 경우의 수를 탐색하여 d[i]에 추가
        for (int j = 0; j &lt; i; ++j)
        {
            for (int op1 : d[j])
            {
                for (int op2 : d[i - j - 1])
                {
                    d[i].insert(op1 + op2);
                    d[i].insert(op1 - op2);
                    d[i].insert(op1 * op2);
                    if (op2 != 0) d[i].insert(op1 / op2);
                }
            }
        }
    }
    //제일 먼저 찾아 낸 것이 최소
    for(int i=0;i&lt;8;i++)
    {
        if(d[i].find(number) != d[i].end()) return i+1;
    }
    //8개 이내에 못찾았으니 -1
    return -1;
}</code></pre>
<h2 id="회고">회고</h2>
<p>일단 다이나믹 프로그래밍이라는 방식으로 바로 풀이법이 떠오른 것 까지는 매우 만족스러웠다.
(방식이 틀려 해결은 못했지만 그럼에도 불구하고)</p>
<p>근데 아직 DP로 접근함에 있어 무엇을 저장해야하는 값으로 정해야하는지에 대한 직관력이 부족한 것 같다.
이 문제에서도 8 이상이라면 -1을 반환한다. 라는 꽤나 명시적인 힌트가 있었음에도 불구하고 이를 써먹을 생각을 제대로 하지 못한 것 같아서 아쉽다.</p>
<p>또한 DP로 풀이할 때, 나도 모르게 DP 배열은 어떤 값으로 고정관념이 박혀있었던 것 같다. 예를 들어 어떤 최솟값이라던가, 최댓값이라던가.
이 문제에서는 집합의 배열으로써 문제를 풀게 된다.
사실 하루종일 이 문제를 잡고 있었어도 떠올리지 못했을 수도 있다.</p>
<p>하지만 이렇게 접근 방법을 하나씩 알아간다면 내 인사이트도 언젠가 이런 문제쯤은 바로 꿰뚫어볼 수 있을 정도가 되지 않을까</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Attributes 추가해보기 (GameAbilitySystem)]]></title>
            <link>https://velog.io/@lee_raccoon/Attributes-%EC%B6%94%EA%B0%80%ED%95%B4%EB%B3%B4%EA%B8%B0-GameAbilitySystem</link>
            <guid>https://velog.io/@lee_raccoon/Attributes-%EC%B6%94%EA%B0%80%ED%95%B4%EB%B3%B4%EA%B8%B0-GameAbilitySystem</guid>
            <pubDate>Sat, 07 Sep 2024 14:52:51 GMT</pubDate>
            <description><![CDATA[<p><a href="https://dev.epicgames.com/documentation/ko-kr/unreal-engine/gameplay-attributes-and-attribute-sets-for-the-gameplay-ability-system-in-unreal-engine?application_version=5.3">게임플레이 어트리뷰트</a></p>
<h1 id="attributes">Attributes</h1>
<h2 id="attributes란">Attributes란</h2>
<p>쉽게 말하자면 게임 내 요소와 관련된 수치들이라고 할 수 있다.
이들은 <code>FGameplayAttributeData</code>라는 구조체로 존재한다.
그리고 Attribute Set에 저장되어 관리된다.
우리는 이 Attributes의 변경을 감지할 수 있고 그에 따른 Function을 호출할 수 있다.</p>
<p>이들은 코드에서 바로 세팅할 수 있지만, 더 선호되는 방식은 Gameplay Effect를 사용하는 방법이다.
Gameplay Effect를 사용하여 다양한 방식으로 Attributes에 영향을 주고 이를 <code>predict</code>할 수 있다.</p>
<h3 id="prediction">Prediction?</h3>
<blockquote>
<p>여기서의 <code>predict</code>란 클라이언트가 서버의 허가를 받지 않고도 어떤 값을 바꿀 수 있다는 것이다.
값을 클라이언트 측에서 변경하고 이를 서버에 알린다.
(서버는 이 변경이 유효하지 않다면 다시 롤백해버린다.)</p>
</blockquote>
<p>이 Prediction은 멀티플레이에서 아주 이점이 많다.
Prediction이 없는 경우, 클라이언트는 서버에게 값을 변경한 것을 알리고 서버는 이를 검사한 후 유효하다고 판단하면 그제서야 클라이언트들에게 알린다.
이 방식으로는 서버에 갔다가 클라이언트까지 다시 알려질 때 까지 delay가 생기게 된다.</p>
<p>하지만 Prediction을 사용하는 경우,
클라이언트에서 바로 값을 변경한다. 플레이어는 delay가 없는 게임 환경을 경험할 수 있다.
서버는 이 변경이 유효한지 확인하고 클라이언트에게 알릴 지 다시 롤백 시킬 지 판단하게 된다.
여전히 서버는 Authority를 갖고 있고, 클라이언트는 Delay에서 해방되는
그런 좋은 방식이다.</p>
<h2 id="attributes-data-추가하기">Attributes Data 추가하기</h2>
<p>Attributes를 사용할 때에는 일종의 보일러 플레이트라고 불릴만한 것이 있다.
아래의 코드에서 <code>Health</code>라는 캐릭터의 체력에 해당하는 데이터를 만들어보겠다.</p>
<pre><code class="language-cpp">//AttributeSet Class의 헤더 파일
UCLASS()
class AURA_API UAuraAttributeSet : public UAttributeSet
{
    GENERATED_BODY()

public:
    UAuraAttributeSet();

    //Replicated로써 등록하기 위해서 필요한 함수
    virtual void GetLifetimeReplicatedProps(TArray&lt;FLifetimeProperty&gt;&amp; OutLifetimeProps) const override;

    //FGameplayAttributeData라는 구조체로 Attribute를 만들 수 있다.
    UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = &quot;Character Attribute&quot;)
    FGameplayAttributeData Health;

    //Health가 Replicated 되었을 때 호출할 함수이다.
    UFUNCTION()
    void OnRep_Health(const FGameplayAttributeData&amp; OldHealth) const; //Last Value가 들어가게됨
};

//구현 파일 (.cpp)
#include &quot;AbilitySystem/AuraAttributeSet.h&quot;
#include &quot;AbilitySystem/AuraAbilitySystemComponent.h&quot;
#include &quot;Net/UnrealNetwork.h&quot;

void UAuraAttributeSet::GetLifetimeReplicatedProps(TArray&lt;FLifetimeProperty&gt;&amp; OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    //Replicated로 지정하는 것, 현재 GAS를 사용하면서 값의 반응을 보고 싶기 때문에 REPNOTIFY_Always로 설정
    DOREPLIFETIME_CONDITION_NOTIFY(UAuraAttributeSet, Health, COND_None, REPNOTIFY_Always);
}

void UAuraAttributeSet::OnRep_Health(const FGameplayAttributeData&amp; OldHealth) const
{
    //서버에 알리고 뭐가 이상하면 원래 값, OldHealth로 롤백 된다.
    GAMEPLAYATTRIBUTE_REPNOTIFY(UAuraAttributeSet, Health, OldHealth);
}</code></pre>
<p>대충 이렇게 이루어져 있다.</p>
<ul>
<li><p><code>GetLifetimeReplicatedProps</code>
네트워크를 통해 복제되어야 하는, Replicated 변수들을 지정하는데 사용되는 함수이다. 내부에서 <code>DOREPLIFETIME_CONDITION_NOTIFY</code>를 사용하여 프로퍼티를 등록할 수 있다.
<code>#include &quot;Net/UnrealNetwork.h&quot;</code> 헤더파일을 추가해주어야 사용할 수 있다.</p>
</li>
<li><p><code>OnRep_Health</code>
<code>Health</code> 프로퍼티가 Replicated 되었을 때 호출될 함수이다.
Attribute를 만들 때 UPROPERTY 매크로 내부에서 <code>ReplicatedUsing  = OnRep_Health</code> 형식으로 지정할 수 있다.
함수 내에서는 <code>GAMEPLAYATTRIBUTE_REPNOTIFY</code>를 호출 해주어야한다.
앞에 말했던 Predict 시스템에서 필요한 것이다. 서버에 변경사항을 보내고, 서버는 이를 검증한 후 이를 다른 클라이언트에게 전달할지 그냥 롤백시킬 지 정한다.</p>
</li>
</ul>
<h2 id="attribute-accessor">Attribute Accessor</h2>
<p>Attribute에 접근하는 (Getter, Setter 등)을 쉽게 구현할 수 있는 방법이 있다.
바로 특별한 매크로 몇개를 추가해주는 것이다.</p>
<pre><code class="language-cpp">//Health로 예를 들어 보자
UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = &quot;Character Attribute&quot;)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UAuraAttributeSet, Health);</code></pre>
<p>위 코드처럼 매크로<code>ATTRIBUTE_ACCESSORS</code>를 붙여준다면 해당 FGameplayAttributeData에 대한 Getter,Setter,Initter를 사용할 수 있다.
이 매크로가 무엇인고 정의를 찾아보면</p>
<pre><code class="language-cpp">/**
 * This defines a set of helper functions for accessing and initializing attributes, to avoid having to manually write these functions.
 * It would creates the following functions, for attribute Health
 *
 *    static FGameplayAttribute UMyHealthSet::GetHealthAttribute();
 *    FORCEINLINE float UMyHealthSet::GetHealth() const;
 *    FORCEINLINE void UMyHealthSet::SetHealth(float NewVal);
 *    FORCEINLINE void UMyHealthSet::InitHealth(float NewVal);
 *
 * To use this in your game you can define something like this, and then add game-specific functions as necessary:
 * 
 *    #define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
 *    GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
 *    GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
 *    GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
 *    GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
 * 
 *    ATTRIBUTE_ACCESSORS(UMyHealthSet, Health)
 */
#define GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
    static FGameplayAttribute Get##PropertyName##Attribute() \
    { \
        static FProperty* Prop = FindFieldChecked&lt;FProperty&gt;(ClassName::StaticClass(), GET_MEMBER_NAME_CHECKED(ClassName, PropertyName)); \
        return Prop; \
    }

    .
    .
    .
</code></pre>
<p><code>GAMEPLAYATTRIBUTE_PROPERTY_GETTER</code>를 비롯하여 Setter, Initter같은 매크로 정의가 존재하고 위에는 관련 주석을 확인할 수 있다.</p>
<p>주석을 요약하면
Getter, Setter, Init 함수를 ATTRIBUTE_ACCESSORS 매크로를 붙여줌으로써 쉽게 사용할 수 있게 도와준다고 한다.
그리고 이 매크로를 정의하는 법을 알려준다.</p>
<pre><code class="language-cpp">#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
     GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
     GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
     GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
     GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)</code></pre>
<p>얘네를 코드에 붙여주면 된다.
(또한 이 정의는 AbilitySystemComponent에 존재하는 것이므로, AbilitySystemComponent 헤더를 꼭 include 할 것)</p>
<p>Getter가 두개인 이유는, 값인 Float를 반환하는 함수와 Attribute 자체를 반환하는 함수 두 개가 존재하기 때문이다.</p>
<p>그리고 Set과 Init이 뭐가 다른지 궁금한데, 정의에서 바로 찾을 수 있었다.</p>
<pre><code class="language-cpp">#define GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
    FORCEINLINE void Set##PropertyName(float NewVal) \
    { \
        UAbilitySystemComponent* AbilityComp = GetOwningAbilitySystemComponent(); \
        if (ensure(AbilityComp)) \
        { \
            AbilityComp-&gt;SetNumericAttributeBase(Get##PropertyName##Attribute(), NewVal); \
        }; \
    }

#define GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName) \
    FORCEINLINE void Init##PropertyName(float NewVal) \
    { \
        PropertyName.SetBaseValue(NewVal); \
        PropertyName.SetCurrentValue(NewVal); \
    }</code></pre>
<p>Setter : BaseValue만 수정
Initter : BaseValue와 CurrentValue 둘 다 수정</p>
<p>Setter가 Current Value를 못바꾸는 걸 봐서는 Setter로 Attribute에 영향을 주는 것은 불가능한가보다.</p>
<p>그래서 <code>ATTRIBUTE_ACCESSORS</code>를 붙여주면 어떤게 가능해지냐?</p>
<pre><code class="language-cpp">UAuraAttributeSet::UAuraAttributeSet()
{
    InitHealth(100.f);
}</code></pre>
<p>이런 식으로 정의하지 않아도 Getter Setter Initter를 프로퍼티 이름만 붙여서 사용할 수 있다는 것이다.
매우 아주 편리해보인다.</p>
<p>적용되는지 확인해보자.
콘솔 창에 <code>showdebug abilitysystem</code>을 입력하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/88374dc4-a08f-4313-8c6f-92630af20adc/image.png" alt=""></p>
<p>Health가 100인걸 확인할 수 있다.
그리고 이와 함께 위를 보면 아바타가 캐릭터이고 소유자가 플레이어 스테이트인 것을 확인할 수 있다.
원하는 대로 잘 설정이 되어 있다.
멋져..</p>
<h2 id="정리">정리</h2>
<p>Attribute를 사용할 때, 보일러 플래이트 코드로써 이것들을 기억하자.</p>
<ol>
<li><code>GetLifetimeReplicatedProps</code>에 Attribute를 추가하기 (<code>DOREPLIFETIME_CONDITION_NOTIFY</code> 사용)</li>
<li><code>On_Rep</code> 함수를 추가하여 Attribute의 UPROPERTY 매크로 내에 추가해주기 (<code>GAMEPLAYATTRIBUTE_REPNOTIFY</code> 사용)</li>
<li><code>ATTRIBUTE_ACCESSORS</code> 매크로 추가</li>
</ol>
<p>1, 2번은 Replicated 관련 기능이긴 해도 멀티 게임은 싱글 플레이가 가능하지만 싱글 플레이는 멀티가 안되니까 아무튼 알아두는게 이득이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 피로도]]></title>
            <link>https://velog.io/@lee_raccoon/%ED%94%BC%EB%A1%9C%EB%8F%84</link>
            <guid>https://velog.io/@lee_raccoon/%ED%94%BC%EB%A1%9C%EB%8F%84</guid>
            <pubDate>Tue, 03 Sep 2024 11:32:15 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/87946?language=cpp">프로그래머스 피로도 문제</a></p>
<p>갖고있는 피로도를 가지고
<code>[입장 피로도, 소모 피로도]</code>를 갖고 있는 던전을
최대 몇개나 돌 수 있는지 구하는 문제이다.</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/58d86ffe-78c6-41ce-905d-1adc12cf468e/image.png" alt=""></p>
<h2 id="풀이-과정">풀이 과정</h2>
<p>던전의 갯수가 몇 개 되지 않기 때문에 그냥 DFS로 완전 탐색을 하면 쉽게 풀 수 있을 듯 하여 그렇게 방향을 잡았다.</p>
<p>그리하여 dfs의 방식을 사용하여 완전탐색을 계획하였다.</p>
<p>dfs 구현 방식은 간단히 재귀 방식으로 진행하였다.</p>
<p>dfs에서 조건문을 걸어 백트래킹 느낌으로 구현해보았다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;vector&gt;

using namespace std;

//cur : 현재 피로도 cnt : 현재 입장한 던전 수
void permutation(const vector&lt;vector&lt;int&gt;&gt;&amp; dungeons, vector&lt;int&gt; selected, int cur, int cnt, int&amp; outAnswer)
{   
    cnt++;
    if(cnt&gt;outAnswer)
    {
        outAnswer = cnt;
    }
    for(int i=0;i&lt;dungeons.size();i++)
    {
        //아직 입장하지 않았고, 현재 피로도로 입장 가능하다면
        if(selected[i] == 0 &amp;&amp; dungeon[0]&lt;=cur)
        {
            selected[i] = 1;
            cur -= dungeons[i][1];
            permutation(dungeons, selected, cur, cnt, outAnswer);
            selected[i] = 0;
            cur += dungeons[i][1];
        }
    }
}

int solution(int k, vector&lt;vector&lt;int&gt;&gt; dungeons) {
    int answer = -1;

    vector&lt;int&gt; selected(dungeons.size(), 0);

    permutation(dungeons, selected, k, -1, answer);
    return answer;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백트래킹]]></title>
            <link>https://velog.io/@lee_raccoon/%EB%B0%B1%ED%8A%B8%EB%9E%98%ED%82%B9%EA%B3%BC-DFS</link>
            <guid>https://velog.io/@lee_raccoon/%EB%B0%B1%ED%8A%B8%EB%9E%98%ED%82%B9%EA%B3%BC-DFS</guid>
            <pubDate>Thu, 29 Aug 2024 13:25:30 GMT</pubDate>
            <description><![CDATA[<h1 id="백트래킹">백트래킹</h1>
<p><a href="https://ko.wikipedia.org/wiki/%ED%87%B4%EA%B0%81%EA%B2%80%EC%83%89">백트래킹 위키</a></p>
<blockquote>
<p>백트래킹의 주요 개념은 해를 얻을 때까지 모든 가능성을 시도한다는 점이다. 모든 가능성은 하나의 트리처럼 구성할 수 있으며, 가지 중에 해결책이 있다. 트리를 검사하기 위해 깊이 우선 탐색을 사용한다. 탐색 중에 오답을 만나면 이전 분기점으로 돌아간다. 시도해보지 않은 다른 해결 방법이 있으면 시도한다. 해결 방법이 더 없으면 더 이전의 분기점으로 돌아간다. 모든 트리의 노드를 검사해도 답을 못 찾을 경우, 이 문제의 해결책은 없는 것이다.
보통 재귀 함수로 구현된다. 재귀로 파생된 해결 방법은 하나 이상의 변수가 필요한데 , 이것은 현재 시점에서 적용할 수 있는 변수값들을 알고 있다. 퇴각검색은 깊이 우선 탐색과 대략 같으나 기억 공간은 덜 차지한다. 현재의 상태를 보관하고 바꾸는 동안만 차지한다.
탐색 속도를 높이기 위해, 재귀 호출을 하기 전에 시도할 값을 정하고 조건(전진 탐색의 경우)을 벗어난 값을 지우는 알고리즘을 적용할 수 있다. 아니면 그 값을 제외한 다른 값들을 탐색할 수도 있다.
(위키출처)</p>
</blockquote>
<p>한마디로, 문제를 해결해나가다가 이게 답이 될 수 없다고 판단하면 <strong>되돌아가는</strong> 방식이다.
그 방향으로 나아가지 않고 되돌아가는 것을 <strong>가지치기</strong>라고 한다.
이 가지치기를 하게 될 조건을 잘 생각해야하는 것이 포인트이다.</p>
<p>보통 모든 경우의 수를 탐색해야할 경우에 유용하게 쓰인다.
상태를 트리로써 나타낼 수 있을 때 유용하게 쓰인다.</p>
<p>방식에 따라 DFS 말고도 BFS를 사용하는 백트래킹이 있다.</p>
<p>근데 보통 모든 경우의 수를 따져봐야할 경우 <code>DFS</code>가 낫다.
<code>BFS</code>로 구현하면 큐의 크기가 매우 커질 수 있어 주의해야하기 때문.
그리고 구현 용이성도 DFS가 더 좋다.
근데 트리의 깊이가 무한대일 경우 DFS가 시작과 동시에 무한 루프에 빠져서 나갈 수가 없다.</p>
<p>DFS를 활용한 백트래킹의 예시를 살펴보자</p>
<h2 id="예제">예제</h2>
<p>N-Queen이라는 매우 유명한 백준 백트래킹 문제이다.
<a href="https://www.acmicpc.net/problem/9663">백준 N-Queen</a>
<img src="https://velog.velcdn.com/images/lee_raccoon/post/cc84a1fa-0015-4644-87e4-6eba37427381/image.png" alt=""></p>
<h3 id="풀이-아이디어">풀이 아이디어</h3>
<p>퀸이 공격할 수 있는 경우라면 탐색을 더 이상 할 필요가 없다.
근데 퀸이 공격할 수 있는 경우는 다음과 같다</p>
<ul>
<li>같은 행에 있을 때</li>
<li>같은 열에 있을 때</li>
<li>대각선 상에 있을 때</li>
</ul>
<p>그렇다면 같은 행에 있는 경우는 아예 없으므로, 한 행에는 하나의 퀸만 있을 수 있다.
그렇다면 퀸의 좌표를 하나의 벡터로 나타낼 수 있다.
벡터의 인덱스가 행이고 값이 열인 것이다.
예를 들어 vec[1]가 2일때, 퀸은 (1,2)에 있는 것이다.</p>
<p>그렇다면 임의의 i, j에 있는 퀸이 같은 열에 있는 경우는 다음과 같다
<code>vec[i] == vec[j]</code></p>
<p>또한 대각선에 있는 경우는 다음과 같다
<code>vec[i]와 vec[j]의 차 == i와 j의 차</code></p>
<p>그러면, N을 받았을 때, 0~N-1까지의 값들을 하나의 vec에 중복없이 모든 경우의 수를 구하되, 위의 조건에 맞는다면 탐색을 중지하고 되돌아가면 된다.</p>
<h3 id="코드">코드</h3>
<pre><code class="language-cpp">#include &lt;iostream&gt;

using namespace std;

int answer = 0;
int vec[15];

bool checkCanAttack(int depth)
{
    if (depth &lt; 1) return false;

    for (int i = 0; i &lt; depth; i++)
    {
        if ((abs(i - (depth)) == abs(vec[i] - vec[depth])) || vec[i] == vec[depth])
        {
            return true;
        }
    }

    return false;
}

void backTracking(int n, int depth)
{
    if (depth == n)
    {
        answer++;
        return;
    }

    for (int i = 0; i &lt; n; i++)
    {
        vec[depth] = i;
        if (!checkCanAttack(depth))
        {
            backTracking(n, depth + 1);
        }
    }
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin &gt;&gt; n;
    backTracking(n, 0);

    cout &lt;&lt; answer;
}</code></pre>
<h3 id="회고">회고</h3>
<p>이 문제에서 시간 초과로 상당히 애를 먹었는데 그 이유가 바로 벡터를 쓴 것이었다.
벡터에 너무 익숙해진 나머지 그냥 처음에 아래처럼 벡터로 풀면서 push_back, pop_back을 사용하며 풀이를 해버렸다.
완전 동일한 로직인데도 불구하고 시간초과로 통과가 되지 않아 혹시나 싶어 배열로 바꿔보니 통과가 됐다..
가능하다면 알고리즘 문제에서는 배열을 사용하는 습관을 들여야할 것 같다.</p>
<pre><code class="language-cpp">//시간초과 났던 코드...
#include &lt;iostream&gt;
#include &lt;algorithm&gt;
#include &lt;vector&gt;

using namespace std;

int answer = 0;

bool checkCanAttack(vector&lt;int&gt; vec)
{
    int curIdx = vec.size()-1;

    if (curIdx &lt; 1) return false;

    for (int i = 0; i &lt; curIdx; i++)
    {
        if ((abs(i - (curIdx)) == abs(vec[i] - vec[curIdx])) || vec[i] == vec[curIdx])
        {
            return true;
        }
    }

    return false;
}

void backTracking(int n, vector&lt;int&gt; vec)
{
    //공격 가능 시 되돌아감
    if (checkCanAttack(vec))
    {
        return;
    }

    if (vec.size() == n)
    {
        answer++;
        return;
    }

    for (int i = 0; i &lt; n; i++)
    {
        vec.push_back(i);
        backTracking(n, vec);
        vec.pop_back();
    }
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin &gt;&gt; n;
    vector&lt;int&gt; vec;
    backTracking(n, vec);

    cout &lt;&lt; answer;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ability System Component]]></title>
            <link>https://velog.io/@lee_raccoon/Ability-System-Component</link>
            <guid>https://velog.io/@lee_raccoon/Ability-System-Component</guid>
            <pubDate>Wed, 28 Aug 2024 12:55:14 GMT</pubDate>
            <description><![CDATA[<h1 id="ability-system-component">Ability System Component</h1>
<h2 id="컴포넌트-생성">컴포넌트 생성</h2>
<p>이전 포스트에서 환경을 설정 했으니 컴포넌트 생성을 해보았다.</p>
<p>현재 Enemy와 플레이어 캐릭터가 같은 Base를 상속받는 설계이다.
Base에다가 만들어주자.</p>
<p>전에 알아봤던대로 플레이어의 ASC는 플레이어 스테이트에 위치하게 될 것이긴한데 나중에 처리하도록 한다.</p>
<pre><code class="language-cpp">UPROPERTY()
TObjectPtr&lt;UAbilitySystemComponent&gt; AbilitySystemComponent;

UPROPERTY()
TObjectPtr&lt;UAttributeSet&gt; AttributeSet;</code></pre>
<p>현재 플레이어와 몬스터 공용으로 쓸 캐릭터 클래스에 생성을 해주었다.
그리고 생성자에서 이들을 초기화해주자</p>
<pre><code class="language-cpp">//PlayerState 생성자 내 코드 예시
AbilitySystemComponent = CreateDefaultSubobject&lt;UAuraAbilitySystemComponent&gt;(&quot;AbilitySystemComponent&quot;);
AbilitySystemComponent-&gt;SetIsReplicated(true);
AbilitySystemComponent-&gt;SetReplicationMode(EGameplayEffectReplicationMode::Mixed);

AttributeSet = CreateDefaultSubobject&lt;UAuraAttributeSet&gt;(&quot;AbttributeSet&quot;);</code></pre>
<p>멀티플레이를 전제로 게임을 만들기 때문에 Replicated를 true로 해준다.
여기서 SetReplicationMode에서 GamePlayEffectReplication을 설정할 수 있는데</p>
<p>아직 GamePlayEffect에 대해서는 배운게 없기 때문에 일단 아래 표를 보고 설정을 해주었다.</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/2665819e-890e-40c6-b7ad-4607dd8d0e97/image.png" alt=""></p>
<p>UseCase를 따라서 플레이어는 Mixed를, Enemy들은 Minimal Mode로 설정하기로한다.</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/41d323be-f5a6-4905-a37e-c9cf8ce6a3e7/image.png" alt=""></p>
<p>주의할 점은 MixedReplicationMode로 설정하게 되면 오너 액터의 오너는 반드시 컨트롤러여야한다.
그러니까, ASC의 오너인 액터는 컨트롤러가 오너여야한다는 것이다.</p>
<p>지금 상황에서는 ASC의 오너는 Player State이다.
다행히 PlayerState의 경우, 자동으로 Controller가 오너가 되기 때문에 신경을 쓸 필요가 없다.
하지만 반드시 알아두어야 할 주의사항이다.
PlayerState를 오너로 하지 않는 경우, 꼭 <code>SetOwner</code>를 사용해서 Controller를 Owner로 지정해주자!</p>
<h2 id="abilitysysteminterface">AbilitySystemInterface</h2>
<pre><code class="language-cpp">UCLASS(Abstract)
class AURA_API AAuraCharacterBase : public ACharacter, public IAbilitySystemInterface
{
    GENERATED_BODY()

public:
    AAuraCharacterBase();

    //~Begin AbilitySystemInterface
    virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;

    UAttributeSet* GetAttributeSet() const { return AttributeSet; }
}</code></pre>
<p>Getter가 하나 들어있는 인터페이스이다. 이걸로 해당 액터가 ASC를 구현하는 지 확인할 수 있어 유용하게 사용할 수 있다.
인터페이스에 포함되어 있지는 않지만 <code>GetAttributeSet</code>도 만들어 둔다면 유용하게 사용할 수 있다.</p>
<p>만약 추후에도 ASC를 사용하게 된다면 이렇게 Getter를 설정해두는 것이 일종의 보일러 플레이트이다.</p>
<h2 id="ability-actor-info">Ability Actor Info</h2>
<p>어빌리티 시스템은 항상 이 시스템을 소유하고 있는 액터의 정보를 알 수 있다.</p>
<p>이 ActorInfo는 Owner Actor와 Avatar Actor로 나뉘게 된다.</p>
<p>Owner Actor는 말 그대로 이 시스템을 소유한 액터를 나타낸다.
Avatar Actor는 월드에서 이 시스템을 대표하는 액터를 나타낸다. 실제로 게임 내에서 보이게 될 액터라고 생각하면 편하다.
중요한 것은, 이 둘이 다를 수도 있다는 것이다.</p>
<p>예를 들어보자면,
현재 Enemy의 경우 생성자에서 직접 ASC를 생성하기 때문에 Owner Actor라고 할 수 있고, 또한 Avatar Actor도 Enemy이다.</p>
<p>하지만 플레이어 캐릭터의 경우,
현재 ASC는 플레이어 스테이트에서 생성되어 Owner Actor는 플레이어 스테이트다.
그런데 월드에서 이 시스템을 대표하여 활동하게 되는 것은 플레이어 캐릭터이기 때문에 캐릭터가 Avatar Actor가 된다.</p>
<p>이들은 각각
<code>UAbilitySystemComponent::InitAbilityActorInfo(AActor* InOwnerActor, AActor* InAvatarActor);</code>
위 함수를 통해서 이를 정해줄 수 있다.</p>
<p>근데 이 함수를 어디서 불러야하는가? 를 알아보자.</p>
<h3 id="actor-info-설정">Actor Info 설정</h3>
<ul>
<li>빙의가 끝난 후 호출되어야 한다. (컨트롤러가 pawn에 set 된 상태여야한다.)</li>
</ul>
<h4 id="pawn이-소유자-아바타인-경우">Pawn이 소유자, 아바타인 경우</h4>
<ul>
<li>Server에서 호출한다면 <code>PossessedBy</code>에서 호출</li>
<li>Client에서 호출한다면 <code>AcknowledgePossession</code>에서 호출  </li>
</ul>
<p>위 방법으로 빙의를 보장할 수 있다.</p>
<p>문제는 지금은 Pawn이 소유자가 아닌 경우라는 것.</p>
<h4 id="playerstate가-소유자-캐릭터가-아바타인-경우">PlayerState가 소유자, 캐릭터가 아바타인 경우</h4>
<ul>
<li>Server에서 호출한다면 <code>PossessedBy</code>에서 호출</li>
<li>Client에서 호출한다면 <code>OnRep_PlayerState</code>에서 호출  </li>
</ul>
<p>PlayerState가 소유자인 경우, 컨트롤러가 set되는 것만이 아니라 PlayerState도 valid한지 확인해주어야하기 때문
(<code>OnRep_</code>함수들은 Replicated 되었을 때 호출되는 함수이다.)</p>
<h4 id="ai-캐릭터의-경우">AI 캐릭터의 경우</h4>
<ul>
<li>그냥 BeginPlay에서 호출하면 된다.</li>
</ul>
<pre><code class="language-cpp">//플레이어 캐릭터의 경우
void AAuraCharacter::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);

    //Init Ability Actor Info for the Server
    InitAbilityActorInfo();
}

void AAuraCharacter::OnRep_PlayerState()
{
    Super::OnRep_PlayerState();

    //Init Ability Actor Info for the Cilent
    InitAbilityActorInfo();
}

void AAuraCharacter::InitAbilityActorInfo()
{
    AAuraPlayerState* AuraPlayerState = GetPlayerState&lt;AAuraPlayerState&gt;();
    check(AuraPlayerState);
    AuraPlayerState-&gt;GetAbilitySystemComponent()-&gt;InitAbilityActorInfo(AuraPlayerState, this);
    AbilitySystemComponent = AuraPlayerState-&gt;GetAbilitySystemComponent();
    AttributeSet = AuraPlayerState-&gt;GetAttributeSet();
}

//AI의 경우
void AAuraEnemy::BeginPlay()
{
    Super::BeginPlay();
    AbilitySystemComponent-&gt;InitAbilityActorInfo(this, this);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[정수 삼각형]]></title>
            <link>https://velog.io/@lee_raccoon/%EC%A0%95%EC%88%98-%EC%82%BC%EA%B0%81%ED%98%95</link>
            <guid>https://velog.io/@lee_raccoon/%EC%A0%95%EC%88%98-%EC%82%BC%EA%B0%81%ED%98%95</guid>
            <pubDate>Tue, 27 Aug 2024 08:01:21 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/350255d0-5ffb-4cae-adf8-aef3e2b54dc2/image.png" alt=""></p>
<p>전형적인 DP 문제였다.</p>
<p>보자마자 DP 배열을 만들어서 최대값을 찾아나가면 될 것이라고 생각이 들었다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;vector&gt;

using namespace std;

int solution(vector&lt;vector&lt;int&gt;&gt; triangle) {
    int answer = 0;

    //D 벡터 사용 용이하도록 미리 할당
    vector&lt;vector&lt;int&gt;&gt; D(triangle.size());
    for (int i = 1; i &lt; triangle.size()+1; i++)
    {
        D[i - 1].resize(i);
    }
    //

    //초기값 설정, 사이즈가 1이면 그냥 return
    D[0][0] = triangle[0][0];
    if (triangle.size() &gt; 2)
    {
        D[1][0] = D[0][0] + triangle[1][0];
        D[1][1] = D[0][0] + triangle[1][1];
    }
    else
    {
        return D[0][0];
    }
    //

    //바텀 업 방식으로 최대값을 찾아나간다.
    for (int i = 2; i &lt; triangle.size(); i++)
    {
        for (int j = 0; j &lt; triangle[i].size(); j++)
        {
            //삼각형 제일 왼쪽일 때
            if (j == 0)
            {
                D[i][j] = D[i - 1][j] + triangle[i][j];
            }
            //삼각형 제일 오른쪽 일 때
            else if (j == triangle[i].size()-1)
            {
                D[i][j] = D[i - 1][j - 1] + triangle[i][j];
            }
               //대각선 왼쪽과 오른쪽 비교 후 큰 쪽을 선택
            else
            {
                D[i][j] = max(D[i - 1][j - 1] + triangle[i][j], D[i - 1][j] + triangle[i][j]);
            }
        }
    }

    //D벡터 마지막 줄에서 제일 큰 수 찾기
    for (int i = 0; i &lt; triangle[triangle.size() - 1].size(); i++)
    {
        if (answer &lt; D[triangle.size() - 1][i])
        {
            answer = D[triangle.size() - 1][i];
        }
    }

    return answer;
}</code></pre>
<h2 id="회고">회고</h2>
<p>어떻게 풀어야할지 바로 떠올라서 슥슥 했는데 계속 segmentation fault가 떠서 난감했다.
D를 편하게 쓰려고 맨 처음에 미리 할당을 해줬는데 여기서 실수가 있었다..
그것도 모르고 계속 바텀업을 진행하는 과정에 뭐가 잘못됐나 찾아보고 있었던 내 시간이 매우 아깝다 ㅜ</p>
<p>out of range를 항상 조심하도록 하자</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동적계획법 (Dynamic Programming, DP)]]></title>
            <link>https://velog.io/@lee_raccoon/%EB%8F%99%EC%A0%81%EA%B3%84%ED%9A%8D%EB%B2%95-Dynamic-Programming-DP</link>
            <guid>https://velog.io/@lee_raccoon/%EB%8F%99%EC%A0%81%EA%B3%84%ED%9A%8D%EB%B2%95-Dynamic-Programming-DP</guid>
            <pubDate>Mon, 26 Aug 2024 05:07:08 GMT</pubDate>
            <description><![CDATA[<h1 id="동적계획법">동적계획법</h1>
<h2 id="동적계획법이란">동적계획법이란?</h2>
<p>문제를 여러 개의 간단한 문제로 분리하여 부분의 문제들을 해결함으로써 최종적으로 복잡한 문제의 답을 구하는 방법이다.</p>
<h2 id="원리-및-구현방식">원리 및 구현방식</h2>
<ol>
<li>큰 문제를 작은 문제로 나눌 수 있어야 한다.</li>
<li>작은 문제들이 반복돼 나타나고 사용되며 이 작은 문제들의 결괏값은 항상 같아야한다.</li>
<li>모든 작은 문제들은 한 번만 계산해 DP 테이블에 저장하며 추후 재사용할 때는 이 DP 테이블을 이용한다.</li>
<li>동적 계획법은 <code>톱-다운</code>, <code>바텀-업</code> 방식으로 구현할 수 있다.</li>
</ol>
<h2 id="예시">예시</h2>
<h3 id="피보나치-수열">피보나치 수열</h3>
<h4 id="피보나치-수열-공식">피보나치 수열 공식</h4>
<p><code>D[N] = D[N-1] + D[N+2]</code></p>
<h4 id="1-동적-계획법으로-풀-수-있는지-확인하기">1. 동적 계획법으로 풀 수 있는지 확인하기</h4>
<ul>
<li>피보나치 수열은 위 공식 처럼 작은 문제로 나눌 수 있기 때문에 DP로 풀 수 있다.</li>
</ul>
<h4 id="2-점화식-세우기">2. 점화식 세우기</h4>
<ul>
<li>다양한 문제를 만났을 때 이들의 인과관계를 파악하는 것이 중요하다.</li>
<li>피보나치 수열의 경우 알고 있는 공식을 그대로 사용하면 된다.</li>
</ul>
<h4 id="3-dp-테이블-사용-이해하기">3. DP 테이블 사용 이해하기</h4>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/d5d89692-2321-49ad-98a3-0e8aa6c3f782/image.png" alt=""></p>
<h4 id="4-톱다운-구현-방식">4. 톱다운 구현 방식</h4>
<ul>
<li>톱다운 구현방식은 말 그대로 위에서부터 내려가기 때문에 보통 재귀함수로 많이 구현된다.</li>
</ul>
<h4 id="5-바텀-업-구현-방식">5. 바텀 업 구현 방식</h4>
<ul>
<li>바텀없 구현방식은 밑에서 부터 올라가며 값을 찾아나가기 때문에 보통 반복문으로 많이 구현된다.</li>
</ul>
<h2 id="예제">예제</h2>
<p>예제를 풀며 두가지 방식 모두 구현해보자.
<a href="https://www.acmicpc.net/problem/1463">백준 정수 1로 만들기</a>
<img src="https://velog.velcdn.com/images/lee_raccoon/post/50f7e58a-7cf4-497c-bb97-f7f088fbea31/image.png" alt=""></p>
<h3 id="톱다운-방식">톱다운 방식</h3>
<p>N에서 부터 1까지 내려가면서 값을 찾는 방식으로 접근하였다. 재귀함수 방식을 사용했다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

using namespace std;

int a[1000001] = {0,0,1,1};

int func(int N) {
    int x;
    if (N == 1 || N == 0) return 0;
    else if (a[N] != 0) return a[N];
    else if (N &gt; 3) {
        x = 1 + func(N - 1);
        if (N % 3 == 0) x = min(x, a[N / 3] + 1);
        if (N % 2 == 0) x = min(x, a[N / 2] + 1);
        a[N] = x;
        return x;
    }
}
int main() {
    int N = 0;
    cin &gt;&gt; N;
    cout &lt;&lt; func(N);
}</code></pre>
<h3 id="바텀업-방식">바텀업 방식</h3>
<p>1에서부터 N으로 올라가면서 값을 찾는 방식으로 접근하였다. 해당 문제의 경우 이 방식이 훨씬 빠르게 탐색이 가능하다.
재귀함수 특성상 그냥 왠만하면 반복문을 사용하는 방식이 빠르긴 하다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

using namespace std;

int main()
{
    int n;
    cin &gt;&gt; n;

    vector&lt;int&gt; vec(n + 1, 1e9);
    vec[0] = 0;
    vec[1] = 0;

    for (int i = 1; i &lt;= n; ++i)
    {
        if (i + 1 &lt;= n)
        {
            if (vec[i] + 1 &lt; vec[i + 1])
            {
                vec[i + 1] = vec[i] + 1;
            }
        }

        if (i * 2 &lt;= n)
        {
            if (vec[i] + 1 &lt; vec[i * 2])
            {
                vec[i * 2] = vec[i] + 1;
            }
        }

        if (i * 3 &lt;= n)
        {
            if (vec[i] + 1 &lt; vec[i * 3])
            {
                vec[i * 3] = vec[i] + 1;
            }
        }
    }

    cout &lt;&lt; vec[n];
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/36827912-1b21-42a0-ae46-7b4c846c8a91/image.png" alt=""></p>
<p>위가 바텀업이고 아래가 톱다운 방식이다.
확실히 재귀함수 때문에 메모리를 많이 먹는 것을 볼 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[언리얼 Interface 사용 시 주의할 점]]></title>
            <link>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-Interface-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90</link>
            <guid>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-Interface-%EC%82%AC%EC%9A%A9-%EC%8B%9C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90</guid>
            <pubDate>Thu, 08 Aug 2024 08:59:27 GMT</pubDate>
            <description><![CDATA[<h1 id="1blueprinttype-blueprintable">1.BlueprintType? Blueprintable?</h1>
<p>언리얼 Interface를 사용할 때 저 둘 중 하나가 붙어있지 않으면?
언리얼에서 인터페이스 함수를 못 가져와서 <code>UFUNCTION</code>으로 블루프린트에서 사용할 수 있게 해놓더라도
여기저기서 에러가 나는 참사가 벌어진다.</p>
<p>근데 저 두개의 차이가 무엇일까? 그냥 말 그대로 받아들이면 된다.</p>
<p><code>BlueprintType</code>
말 그대로 타입, 블루프린트에서 가져와서 사용할 수 있는 타입이 될 수 있다는 것이라고 생각하면 된다.</p>
<p><code>Blueprintable</code>
블루프린트로 만들 수 있다는 뜻이다. C++로 만들어진 인터페이스 클래스를 파생시켜서 블루프린트로 만들 수 있다.
확장할 일이 생기면 이를 사용하면 될 것이다.</p>
<h1 id="2-blueprintnavtiveevent">2. BlueprintNavtiveEvent</h1>
<pre><code class="language-cpp">    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = &quot;Interaction&quot;)
    void DoInteraction();</code></pre>
<p>이렇듯 상호작용 인터페이스를 만든다고 가정했을 때,
C++에서든 블루프린트에서든 구현하여 쓸 수 있도록 하려면 <code>NativeEvnet</code>를 사용하여야한다.</p>
<p>근데 이 상태에서 C++ 구현 도중 해당 함수를 호출을 그냥 원래 하던 대로 해버리면?
<img src="https://velog.velcdn.com/images/lee_raccoon/post/508f739d-2711-4d6a-b6b5-9cbc7473fc2d/image.png" alt=""></p>
<p>바로 크래시가 나버린다. (이 크래시는 위 예시 인터페이스랑은 무관)</p>
<p><code>BlueprintNativeEvent</code>는 바로 호출이 금지되어 있다. 크래시를 읽어보면 Execute를 사용해서 함수를 호출해달라고 한다.
<code>Execute_&#39;함수명&#39;(실행할 오브젝트, 인자1, 2...)</code>이걸 대신해서 써주어야한다.</p>
<p>그래서 만약에 그냥 C++에서 사용하던 인터페이스가 갑자기 블루프린트에서도 사용이 되어야할 상황이 된다면 이를 유의해야한다.
무지성으로 <code>NativeEvent</code>를 붙여서 사용을 했다가는 무수한 곳에서 크래시가 터져버린다.
그렇게 바꿔줬다면 모든  호출부에서 Execute로 대체해주던가 아니면
블루프린트에서 사용할 인터페이스 함수를 따로 만들어서 사용하는게 오히려 나을 것이다.</p>
<p>위 크래시는 오늘 프로젝트에서 나타나버린 아주 싱싱한 예시이다..
본래 C++ 클래스에서만 사용하던 함수를 다른 사람이 필요로 의해 블루프린트에서 부를 수 있게 바꾸려다가 벌어진 상황이다.
타 개발자가 인터페이스 내 함수를 건드림으로써 충분히 벌어질 수 있는 상황이다.</p>
<p>만약 이런 상황이 되면 신중히, 해당 파트를 개발한 분에게 묻거나 레퍼런스를 확인해보고 작업을 해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GameAbilitySystem 환경 세팅]]></title>
            <link>https://velog.io/@lee_raccoon/GameAbilitySystem-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@lee_raccoon/GameAbilitySystem-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Tue, 06 Aug 2024 07:59:29 GMT</pubDate>
            <description><![CDATA[<h1 id="gas-적용하기">GAS 적용하기</h1>
<p>GAS를 적용하기 위해서 우선 <code>Ability System Component</code>와 <code>Ability Set</code>을 추가해 볼 것이다.
근데 생각해보아야 할점.
언리얼을 하면서 얼마나 많이 생각하던 것인가
바로바로</p>
<p><strong>어디에 추가해야하는가?</strong></p>
<p>Pawn에 GAS를 적용하고 싶다면, Pawn 클래스 자체에 해야하나?
또 다른 방법이 있나?</p>
<p>우선 두가지 선택지가 있다.</p>
<h2 id="pawn-vs-player-state">Pawn Vs Player State</h2>
<p>Pawn 클래스에 추가했을 때와 Player State에 추가 했을 때의 차이점이 무엇이 있을까?
이 두 방식의 큰 차이점 중 하나는 Pawn이 사라졌을 때, 이 컴포넌트가 남아있는가 하는 것이다.</p>
<p>만약 게임에서 플레이어가 죽거나 다른 어떤 게임 내 조작으로 캐릭터가 사라진 상황이라 치자.
그럼 Pawn에 컴포넌트가 있던 쪽은 다시 Pawn이 생성되면 어빌리티가 초기화될 것이다.</p>
<p>사라질 때 저장하고 생성될 때 로드하는 뭐 그런 기능을 만들 수도 있겠지만 Player State가 더 편리하다.
Pawn이 사라져도 Player State는 남아있기 때문에, 새로운 폰이 생긴다면 그대로 연결만 해주면 되기 때문!</p>
<p>그런데 뭐 굳이 상관 없이 Pawn에다가 컴포넌트를 바로 넣는게 편리한 경우도 있다.
맵에 돌아다니는 잡몹들은 그냥 죽었다가 원래 상태로 다시 스폰되는게 일이기 때문에 굳이 Player State를 써도 되지 않아도 될 것 같다.</p>
<hr>
<h2 id="플러그인">플러그인</h2>
<p>일단 플러그인을 적용하고 재시작 해보자
<img src="https://velog.velcdn.com/images/lee_raccoon/post/07978958-a7a2-4f14-94bb-c8cc6d569d0b/image.png" alt=""></p>
<p>그리고 빌드 파일(.build.cs)에서 모듈 추가를 해주자</p>
<pre><code class="language-cpp">PublicDependencyModuleNames.AddRange(new string[] { &quot;Core&quot;, &quot;CoreUObject&quot;, &quot;Engine&quot;, &quot;InputCore&quot;, &quot;EnhancedInput&quot;, &quot;GameplayAbilities&quot; });
PrivateDependencyModuleNames.AddRange(new string[] { &quot;GameplayTags&quot;, &quot;GameplayTasks&quot; });</code></pre>
<hr>
<h2 id="abilitysystemcomponent">AbilitySystemComponent</h2>
<p>AbilitySystemComponent 클래스를 찾아서 생성을 해주자.
하는 김에 Attribute Set도 함께 생성하자.
<img src="https://velog.velcdn.com/images/lee_raccoon/post/686fe1d5-fe5f-4dea-a6f4-002e75265425/image.png" alt="">
<img src="https://velog.velcdn.com/images/lee_raccoon/post/5346a95e-e81d-4941-a294-fa0d17f3ace0/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Game Ability System(GAS)란?]]></title>
            <link>https://velog.io/@lee_raccoon/Game-Ability-SystemGAS%EB%9E%80</link>
            <guid>https://velog.io/@lee_raccoon/Game-Ability-SystemGAS%EB%9E%80</guid>
            <pubDate>Tue, 06 Aug 2024 06:21:50 GMT</pubDate>
            <description><![CDATA[<h1 id="game-ability-system-gas의-등장">Game Ability System (GAS)의 등장</h1>
<p>게임을 하면서 플레이어는 정말 다양한 어트리뷰트와 어빌리티가 있다.</p>
<p>어트리뷰트의 예</p>
<ul>
<li>체력</li>
<li>경험치</li>
<li>공격력</li>
<li>방어력</li>
</ul>
<p>어빌리티의 예</p>
<ul>
<li>공격</li>
<li>방어</li>
<li>애니메이션 재생</li>
<li>데미지 받기</li>
<li>죽음</li>
</ul>
<p>등등..</p>
<p>대충 생각나는 것만 해도 정말 다양한 것들이 있다.
심지어는 어빌리티에 따라서 사운드, 이펙트 등이 필요할 수 있기 때문에 더욱 많을 것이다.</p>
<p>만드려는 게임이 RPG인 경우에는
경험치에 따라 레벨업을 할 것이고,
다양한 어트리뷰트에 변화가 생길 것이고,
새로운 스킬을 배워 어빌리티가 추가될 수도 있다.</p>
<p>이렇듯 게임의 규모가 커질 수록 이런 복잡성이 증가할 수 밖에 없는데
그러면 이 어트리뷰트와 어빌리티가 서로 섞여서 관리하기 정말 어려워질 것이다.</p>
<p>거기서 등장한 것이 바로 이 GAS이다.</p>
<hr>
<h1 id="gas의-주요-핵심">GAS의 주요 핵심</h1>
<p>GAS의 핵심 구성 요소에 대해 알아보자.</p>
<h2 id="ability-system-component">Ability System Component</h2>
<p>GAS의 가장 중요한 것 중 하나이다.
Actor에게 추가하여 사용할 수 있으며</p>
<p>보통 어빌리티를 활성화시키고,
특정 어빌리티가 활성화 됐을 때의 동작을 알린다던지하는 주요한 친구이다.</p>
<h2 id="attribute-set">Attribute Set</h2>
<p>위에 말했듯 게임은 다양한 어트리뷰트를 가질 수 있는데, 이를 GAS에서는 Attribute Set이라는 곳에 저장해둔다.
이를 통해 GAS의 다양한 파트와 함께 쓰일 수 있도록 해준다.</p>
<h2 id="gameplay-ability">Gameplay Ability</h2>
<p>캐릭터나 오브젝트 등이 할 수 있는 행동, 즉 어빌리티를 위해 쓰는 것이다.
이를 위한 코드를 캡슐화하여 갖고 있는 것이라고 생각하면 될 듯 하다.</p>
<h2 id="ability-task">Ability Task</h2>
<p>Gameplay Ability를 비동기적으로 처리할 수 있는 기능이다.</p>
<h2 id="gameplay-effect">Gameplay Effect</h2>
<p>이것으로 어트리뷰트 값을 조절하게 된다.
즉시 바꿀 수도, 정해진 시간 후에 바꿀 수도, 주기적으로 바꿀 수도 있다.</p>
<h2 id="gameplay-cue">Gameplay Cue</h2>
<p>이펙트나 파티클 시스템, 사운드를 다루게 된다.</p>
<h2 id="gameplay-tag">Gameplay Tag</h2>
<p>Gameplay Tag는 특별히 GAS에만 속해 있는 것은 아니지만 GAS에서 아주 중요한 역할을 한다.</p>
<hr>
<p>앞으로 이 시리즈에서 하나씩 알아가보도록 할 것이다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[언리얼 패키지와 애셋]]></title>
            <link>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-%ED%8C%A8%ED%82%A4%EC%A7%80</link>
            <guid>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-%ED%8C%A8%ED%82%A4%EC%A7%80</guid>
            <pubDate>Thu, 25 Jul 2024 09:52:08 GMT</pubDate>
            <description><![CDATA[<h1 id="패키지">패키지</h1>
<p>언리얼에서 패키징은 다양한 의미를 갖는다.</p>
<ul>
<li>언리얼 오브젝트를 감싼 포장 오브젝트</li>
<li>개발된 최종 콘텐츠를 정리해 프로그램으로 만드는 작업</li>
<li>DLC 혹은 향후 확장 콘텐츠에 사용되는 별도의 데이터</li>
</ul>
<p>등의 의미를 갖는다.</p>
<p>이번에 알아본 것은 1번, 언리얼 오브젝트를 감싸는 오브젝트로써의 패키지이다.</p>
<p>사실 지금까지 써온 모든 언리얼 오브젝트들은 Transient라는 패키지에 속해있다.</p>
<p>패키지에는 애셋이라는 서브 오브젝트가 존재하고 그 하위에 또 다양한 서브 오브젝트들이 있다.
언리얼 에디터에 노출되는 친구들은 이 애셋이라는 친구들이다.</p>
<p>일반적으로 한 패키지에는 한 애셋이 존재한다고 한다.</p>
<h2 id="패키지-저장과-로드">패키지 저장과 로드</h2>
<h3 id="패키지-저장">패키지 저장</h3>
<pre><code class="language-cpp">void UMyGameInstance::SaveStudentPackage() const
{
    /*패키지를 저장하기 전에 일단 로드를 해주는 것이 안전한 방법이라고 한다.
    이미 존재할 수도 있는 데이터를 메모리로 가져와 현재 상태를 반영한 작업을 할 수 있다.
    로드된 패키지는 다른 객체와의 참조 관계를 포함하는데,
    이 때 새 객체를 생성하면 참조가 손상될 수도 있다.
    */
    UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
    if (StudentPackage)
    {
        StudentPackage-&gt;FullyLoad();
    }

    //패키지 생성
    StudentPackage = CreatePackage(*PackageName);
    EObjectFlags ObjectFlag = RF_Public | RF_Standalone;

    //서브 오브젝트(애셋) 생성
    UStudent* TopStudent = NewObject&lt;UStudent&gt;(StudentPackage, UStudent::StaticClass(), *AssetName, ObjectFlag);
    TopStudent-&gt;SetName(TEXT(&quot;너굴&quot;));
    TopStudent-&gt;SetOrder(42);

    //애셋의 서브 오브젝트를 생성
    const int32 NumofSubs = 10;
    for (int32 ix = 1; ix &lt;= NumofSubs; ++ix)
    {
        FString SubObjectName = FString::Printf(TEXT(&quot;Strudent%d&quot;), ix);
        UStudent* SubStudent = NewObject&lt;UStudent&gt;(TopStudent, UStudent::StaticClass(), *SubObjectName, ObjectFlag);
        SubStudent-&gt;SetName(FString::Printf(TEXT(&quot;학생%d&quot;), ix));
        SubStudent-&gt;SetOrder(ix);
    }

    //패키지 파일 이름과 확장자 및 플래그를 설정
    const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
    FSavePackageArgs SaveArgs;
    SaveArgs.TopLevelFlags = ObjectFlag;

    if (UPackage::SavePackage(StudentPackage, nullptr, *PackageFileName, SaveArgs))
    {
        UE_LOG(LogTemp, Display, TEXT(&quot;패키지가 성공적으로 저장되었습니다.&quot;));
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/43e36c7c-5404-4ea2-baa7-2d94a4ff6510/image.png" alt="">
오. 애셋이 생성되었다.
이제 이 애셋을 한번 로드 해보자</p>
<h3 id="패키지-로딩">패키지 로딩</h3>
<pre><code class="language-cpp">void UMyGameInstance::LoadStudentPackage() const
{    
    //패키지 로드
    UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
    if (nullptr == StudentPackage)
    {
        UE_LOG(LogTemp, Display, TEXT(&quot;패키지를 찾을 수 없습니다.&quot;));
        return;
    }

    StudentPackage-&gt;FullyLoad();

    //패키지에서 애셋을 찾아 TopStudent에 저장 후 출력 해보기
    UStudent* TopStudent = FindObject&lt;UStudent&gt;(StudentPackage, *AssetName);
    PrintStudentInfo(TopStudent, TEXT(&quot;FindObject&quot;));
}</code></pre>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/575ee8f4-7a9f-4112-9c40-1ead7f2119fd/image.png" alt=""></p>
<p>잘 되는 것을 보니 기분이 좋다.</p>
<h2 id="애셋-정보의-저장과-로딩-전략">애셋 정보의 저장과 로딩 전략</h2>
<p>에디터에서 애셋 간의 연결 작업을 할 일이 많은데 이럴 때마다 직접 패키지를 불러서 할당하기에는 부하가 크다.</p>
<p>그래서 무조건 로딩을 한다기 보다는 일단 해당 애셋이나 패키지의 경로를 갖고 있다고 생각하면 편하다. (오브젝트 경로)</p>
<p>오브젝트 경로는 프로젝트 내에서 유일한 값이기에 키 값으로 쓰일 수 있게 된다.
-&gt;오브젝트 경로로 연결을 가능하게 한다.
<code>오브젝트 경로: {패키지 이름}.{애셋이름}</code></p>
<p>로딩할 경우에는 크게 세가지가 있다.</p>
<ul>
<li>프로젝트에서 애셋이 반드시 필요 -&gt; 엔진이 초기화될 때, 생성자 코드에서 미리 로딩</li>
<li>런타임에서 필요한 때 로딩 -&gt; 런타임 로직에서 정적 로딩
(이 때는 다른 프로세스의 실행을 막아 게임이 멈춤)</li>
<li>런타임에서 비동기적으로 로딩 -&gt; 런타임 로직에서 관리자를 사용해 비동기 로딩</li>
</ul>
<p><a href="https://docs.unrealengine.com/4.27/ko/ProgrammingAndScripting/ProgrammingWithCPP/Assets/ReferencingAssets/">언리얼 엔진 애셋 참조</a></p>
<p>공식 문서에서 자세히 알아볼 수 있다.
문서에서는 크게 4가지로 나누고 있다.</p>
<h3 id="직접-프로퍼티-참조">직접 프로퍼티 참조</h3>
<p>가장 기본적인 경우이며 UPROPERTY 매크로를 통해 노출되는 프로퍼티이다.</p>
<h3 id="생성-시간-참조">생성 시간 참조</h3>
<p>로드 해야할 애셋이 명확한 경우에 그 프로퍼티를 오브젝트 생성자에서 설정해주는 경우이다.
<code>ConstructorHelpers</code> 같은 특수 클래스가 사용된다.
해당 애셋을 찾을 수 없는 경우에는 <code>nullptr</code>이 반환되고 이를 참조하려 한다면 크래시가 뜨기 때문에 조심해야한다.</p>
<h3 id="간접-프로퍼티-참조">간접 프로퍼티 참조</h3>
<p>애셋 로드 시점을 쉽게 제어할 수 있는 방법이다.
<code>TSoftObjectPtr</code>를 사용하고 <code>LoadObject&lt;&gt;()</code>이나 <code>StaticLoadObject()</code>,<code>FStreamingManager</code>를 사용해서 오브젝트를 로드할 수 있다.
<code>LoadObject</code>같은 경우에는 애셋을 동기식으로 로드한다. 그러므로 호출 시점에 따라 프레임이 출렁일 수 있으니 사용 시 조심해야한다.
<code>FStreamingManager</code>는 비동기 로딩을 지원한다.</p>
<h3 id="오브젝트-검색로드">오브젝트 검색/로드</h3>
<p>이미 생성, 로드 된 UObject를 사용하는 경우에 <code>FindObject&lt;&gt;()</code>
아직 로드되지 않은 오브젝트를 로드하려면 <code>LoadObject&lt;&gt;()</code>를 사용한다.</p>
<h3 id="오브젝트-로드-테스트">오브젝트 로드 테스트</h3>
<p>패키지로써 저장하고 로드하는 것은 해보았으니 이번에는 <code>LoadObject</code>로 오브젝트를 불러와보자.</p>
<pre><code class="language-cpp">void UMyGameInstance::LoadStudentObject() const
{
    //{패키지 이름}.{애셋 이름}으로 오브젝트 경로를 설정한다. 위에서 사용했던 내용이 있기 때문에 그대로 썼다.
    const FString TopSoftObjectPath = FString::Printf(TEXT(&quot;%s.%s&quot;), *PackageName, *AssetName);

    //오브젝트 경로를 통해서 오브젝트를 로딩한다. 패키지 로딩이 아니기 때문에 첫 인자는 nullptr로 설정해준다!
    UStudent* TopStudent = LoadObject&lt;UStudent&gt;(nullptr, *TopSoftObjectPath);
    PrintStudentInfo(TopStudent, TEXT(&quot;LoadObject Asset&quot;));
}</code></pre>
<p>그 다음 이 함수를 실행시켜 보면?</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/7b2ae115-d079-4954-ab7c-b5e7bd62a361/image.png" alt=""></p>
<p>성공!</p>
<h3 id="생성-시간-참조-테스트">생성 시간 참조 테스트</h3>
<p>이번엔 생성자에서 한번 로딩을 해보자.
그렇다면 이 애셋은 게임이 시작하기 전에 이미 메모리에 올라와있다는 것을 뜻할 것이다.</p>
<p>생성자에서 애셋을 로딩할 때는 <code>LoadObject</code>를 사용하는 것이 아니라.
<code>ConstructorHelpers</code>라는 클래스를 사용하게 된다.</p>
<pre><code class="language-cpp">UMyGameInstance::UMyGameInstance()
{
    const FString TopSoftObjectPath = FString::Printf(TEXT(&quot;%s.%s&quot;), *PackageName, *AssetName);

    static ConstructorHelpers::FObjectFinder&lt;UStudent&gt; UASSET_TopStudent(*TopSoftObjectPath);
    if (UASSET_TopStudent.Succeeded())
    {
        PrintStudentInfo(UASSET_TopStudent.Object, TEXT(&quot;Constructor&quot;));
    }
}</code></pre>
<p>한번 실행해보면,
<img src="https://velog.velcdn.com/images/lee_raccoon/post/867824a3-67f0-48ed-a2a0-c25eb2531280/image.png" alt=""></p>
<p>Constructor 태그가 붙은 로그가 제일 먼저 찍혀있는 것을 볼 수 있다.
당연하다. 생성자에 있는 함수였으니까</p>
<p>근데 왜 두번 실행이 되는가?가 문제인데</p>
<p>이는 에디터가 로딩될 때 생성자들을 모두 호출하면서 찍힌 것이고
두번째는 에디터 내에 게임이 실행될 때 또 생성자에 관련된 함수들이 자동으로 호출되므로 두 번 찍히게 된다.</p>
<p>생성자 코드에서 애셋을 로딩할 경우 해당 애셋이 반드시 존재해야하며
존재하지 않는다면 무시무시한 일이..</p>
<h3 id="비동기-로딩-테스트">비동기 로딩 테스트</h3>
<p>이번에는 비동기로딩이다. <code>FStreamableManager</code>라는 클래스와 <code>FStreamableHandle</code>을 사용하게 된다.</p>
<pre><code class="language-cpp">//헤더
#include &quot;Engine/StreamableManager.h&quot;

FStreamableManager StreamableManager;
TSharedPtr&lt;FStreamableHandle&gt; Handle;

//구현부
void UMyGameInstance::LoadStudnetAsync()
{
    const FString TopSoftObjectPath = FString::Printf(TEXT(&quot;%s.%s&quot;), *PackageName, *AssetName);
    //핸들을 통해 비동기 로딩을 리퀘스트하고 람다함수를 넣어줄 수 있다.
    Handle = StreamableManager.RequestAsyncLoad(TopSoftObjectPath,
        [&amp;]()
        {
            if (Handle.IsValid() &amp;&amp; Handle-&gt;HasLoadCompleted())
            {
                UStudent* TopStudent = Cast&lt;UStudent&gt;(Handle-&gt;GetLoadedAsset());
                if (TopStudent)
                {
                    PrintStudentInfo(TopStudent, TEXT(&quot;AsyncLoad&quot;));

                    Handle-&gt;ReleaseHandle();
                    Handle.Reset();
                }
            }
        });
}</code></pre>
<p>이를 실행해보면?</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/22720454-e746-4af0-b6e5-184510ed97c1/image.png" alt=""></p>
<p>야호~</p>
<h1 id="마무리">마무리</h1>
<p>이렇게 애셋을 저장하고 로딩하는 다양한 방법에 대해 알아보았다.</p>
<p>사실 프로젝트를 진행하며 이미 계속 쓰고 있는 방법이지만 <code>ConstructorHelpers</code>와 <code>LoadObject</code>가 뭐가 다른건지는 정확히 알고 쓰지는 않아서 이제 보니 큰일이 날 뻔 했다.</p>
<p>사실 데모 패키징할 때 게임에서 fatal error의 등장에 정신이 나가버릴 뻔 하기도 했지만..</p>
<p>애셋 저장과 로딩에 대해 몰랐다면 이제라도 제대로 알고 넘어가는 것이 좋지 않을까!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[직렬화]]></title>
            <link>https://velog.io/@lee_raccoon/%EC%A7%81%EB%A0%AC%ED%99%94</link>
            <guid>https://velog.io/@lee_raccoon/%EC%A7%81%EB%A0%AC%ED%99%94</guid>
            <pubDate>Wed, 17 Jul 2024 10:31:23 GMT</pubDate>
            <description><![CDATA[<h2 id="직렬화">직렬화</h2>
<ul>
<li>오브젝트나 연결된 오브젝트의 묶음을 바이트 스트림으로 변환하는 과정이다.</li>
</ul>
<h3 id="장점-및-활용">장점 및 활용</h3>
<ul>
<li>현재 프로그램 상태를 저장하고 필요할 때 복원 가능 (게임 저장)</li>
<li>현재 객체의 정보를 클립보드에 복사해서 다른 프로그램에 전송 가능</li>
<li>네트워크를 통해 현재 프로그램의 상태를 다른 컴퓨터에 복원 가능 (멀티플레이)</li>
<li>데이터 압축, 암호화를 통해 데이터를 효율적이고 안전하게 보관 가능</li>
</ul>
<h2 id="언리얼의-직렬화-시스템">언리얼의 직렬화 시스템</h2>
<p>언리얼은 직렬화 시스템을 위해 자체적으로
<code>FArchive</code> 클래스와 연산자들을 제공하고 있다.</p>
<p>다양한 아카이브 클래스로써</p>
<ul>
<li>메모리 아카이브 (<code>FMemoryReader</code> <code>FMemoryWriter</code>)</li>
<li>파일 아카이브 (<code>FArchiveFileReaderGeneric</code>, <code>FArchiveFileWriterGeneric</code>)</li>
<li>기타 언리얼 오브젝트와 관련된 아카이브 클래스 (<code>FArchiveUObject</code>)</li>
</ul>
<p>등을 제공하고 있으며 <code>Json</code> 형태의 직렬화는 별도의 라이브러리를 통해 제공하고 있다.</p>
<h3 id="아카이브-예제">아카이브 예제</h3>
<pre><code class="language-cpp">//언리얼 오브젝트
class SERIALIZATIONTEST_API UStudent : public UObject
{
    GENERATED_BODY()

public:
    UStudent();

    int32 GetOrder() const { return Order; }
    void SetOrder(int32 InOrder) { Order = InOrder; }

    const FString&amp; GetName() const { return Name; }
    void SetName(const FString&amp; InName) { Name = InName; }

    virtual void Serialize(FArchive&amp; Ar) override;


private:
    int32 Order;

    FString Name;
};

UStudent::UStudent()
{
    Order = -1;
    Name = TEXT(&quot;학생이름 기본값&quot;);
}

void UStudent::Serialize(FArchive&amp; Ar)
{
    Super::Serialize(Ar);

    Ar &lt;&lt; Order;
    Ar &lt;&lt; Name;
}

//cpp 구조체
struct FStudentData
{
    FStudentData() {};
    FStudentData(int32 InOrder, const FString&amp; InName) : Order(InOrder), Name(InName) {}

    friend FArchive&amp; operator&lt;&lt;(FArchive&amp; Ar, FStudentData&amp; InStudentData)
    {
        Ar &lt;&lt; InStudentData.Order;
        Ar &lt;&lt; InStudentData.Name;
        return Ar;
    }

    int32 Order = -1;
    FString Name = TEXT(&quot;홍길동&quot;);
};

//GameInstance.cpp
void UMyGameInstance::Init()
{
    Super::Init();

    FStudentData RawDataSrc(16, TEXT(&quot;너굴&quot;));

    const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT(&quot;Saved&quot;));
    UE_LOG(LogTemp, Display, TEXT(&quot;저장할 파일 폴더 : %s&quot;), *SavedDir);

    const FString RawDataFileName(TEXT(&quot;RawData.bin&quot;));
    FString RawDataAbsolutePath = FPaths::Combine(*SavedDir, *RawDataFileName);

    UE_LOG(LogTemp, Display, TEXT(&quot;저장할 파일 전체 경로 : %s&quot;), *RawDataAbsolutePath);
    FPaths::MakeStandardFilename(RawDataAbsolutePath);

    UE_LOG(LogTemp, Display, TEXT(&quot;저장할 파일 전체 경로 : %s&quot;), *RawDataAbsolutePath);

    FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbsolutePath);
    if(RawFileWriterAr != nullptr)
    {
        *RawFileWriterAr &lt;&lt; RawDataSrc;
        RawFileWriterAr-&gt;Close();
        delete RawFileWriterAr;
        RawFileWriterAr = nullptr;
    }

    FStudentData RawDataDest;
    FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbsolutePath);
    if (RawFileReaderAr != nullptr)
    {
        *RawFileReaderAr &lt;&lt; RawDataDest;
        RawFileReaderAr-&gt;Close();
        delete(RawFileReaderAr);
        RawFileReaderAr = nullptr;

        UE_LOG(LogTemp, Display, TEXT(&quot;[RawData] 이름 %s, 순번 %d&quot;), *RawDataDest.Name, RawDataDest.Order);
    }
    StudentSrc = NewObject&lt;UStudent&gt;();
    StudentSrc-&gt;SetName(TEXT(&quot;너굴&quot;));
    StudentSrc-&gt;SetOrder(14);

    const FString ObjectDataFileName(TEXT(&quot;ObjectData.bin&quot;));
    FString ObjectDataAbsolutePath = FPaths::Combine(*SavedDir, *ObjectDataFileName);
    FPaths::MakeStandardFilename(ObjectDataAbsolutePath);

    TArray&lt;uint8&gt; BufferArray;
    FMemoryWriter MemoryWriterAr(BufferArray);
    StudentSrc-&gt;Serialize(MemoryWriterAr);

    if(TUniquePtr&lt;FArchive&gt; FileWriterAr = TUniquePtr&lt;FArchive&gt;(IFileManager::Get().CreateFileWriter(*ObjectDataAbsolutePath)))
    {
        *FileWriterAr &lt;&lt; BufferArray;
        FileWriterAr-&gt;Close();
    }

    TArray&lt;uint8&gt; BufferArrayFromFile;
    if (TUniquePtr&lt;FArchive&gt; FileReaderAr = TUniquePtr&lt;FArchive&gt;(IFileManager::Get().CreateFileReader(*ObjectDataAbsolutePath)))
    {
        *FileReaderAr &lt;&lt; BufferArrayFromFile;
        FileReaderAr-&gt;Close();
    }

    FMemoryReader MemoryReaderAr(BufferArrayFromFile);
    UStudent* StudentDest = NewObject&lt;UStudent&gt;();
    StudentDest-&gt;Serialize(MemoryReaderAr);
    PrintStudentInfo(StudentDest, TEXT(&quot;ObjectData&quot;));
}</code></pre>
<pre><code>//결과
LogTemp: Display: 저장할 파일 폴더 : ../../../../UnrealProjects/SerializationTest/Saved
LogTemp: Display: 저장할 파일 전체 경로 : ../../../../UnrealProjects/SerializationTest/Saved/RawData.bin
LogTemp: Display: 저장할 파일 전체 경로 : C:/UnrealEngine/UnrealProjects/SerializationTest/Saved/RawData.bin
LogTemp: Display: 저장할 파일 폴더 : ../../../../UnrealProjects/SerializationTest/Saved
LogTemp: Display: 저장할 파일 전체 경로 : ../../../../UnrealProjects/SerializationTest/Saved/RawData.bin
LogTemp: Display: 저장할 파일 전체 경로 : C:/UnrealEngine/UnrealProjects/SerializationTest/Saved/RawData.bin
LogTemp: Display: [RawData] 이름 너굴, 순번 16
LogTemp: Display: [ObjectData] 이름 너굴 순번 14</code></pre><p><img src="https://velog.velcdn.com/images/lee_raccoon/post/548b9e24-2b68-4ada-988d-04fc5d363211/image.png" alt=""></p>
<p>이렇게 C++ 클래스 구조체와 언리얼 오브젝트를 각각 파일로 저장하고 불러올 수 있게 되었다!</p>
<h3 id="json-직렬화-예제">Json 직렬화 예제</h3>
<pre><code class="language-cpp">StudentSrc = NewObject&lt;UStudent&gt;();
StudentSrc-&gt;SetName(TEXT(&quot;너굴&quot;));
StudentSrc-&gt;SetOrder(14);

const FString JsonDataFileName(TEXT(&quot;StudentJsonData.txt&quot;));
FString JsonDataAbsolutePath = FPaths::Combine(*SavedDir, *JsonDataFileName);
FPaths::MakeStandardFilename(JsonDataAbsolutePath);

TSharedRef&lt;FJsonObject&gt; JsonObjectSrc = MakeShared&lt;FJsonObject&gt;();
FJsonObjectConverter::UStructToJsonObject(StudentSrc-&gt;GetClass(), StudentSrc, JsonObjectSrc);

FString JsonOutString;
TSharedRef&lt;TJsonWriter&lt;TCHAR&gt;&gt; JsonWriterAr = TJsonWriterFactory&lt;TCHAR&gt;::Create(&amp;JsonOutString);
if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
{
    FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
}

FString JsonInString;
FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);

TSharedRef&lt;TJsonReader&lt;TCHAR&gt;&gt; JsonReaderAr = TJsonReaderFactory&lt;TCHAR&gt;::Create(JsonInString);

TSharedPtr&lt;FJsonObject&gt; JsonObjectDest;
if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
{
    UStudent* JsonStudentDest = NewObject&lt;UStudent&gt;();
    if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest-&gt;GetClass(), JsonStudentDest))
    {
        PrintStudentInfo(JsonStudentDest, TEXT(&quot;JsonData&quot;));
    }
}</code></pre>
<pre><code>//결과
LogTemp: Display: [JsonData] 이름 너굴 순번 14</code></pre><p><img src="https://velog.velcdn.com/images/lee_raccoon/post/06bbb115-710a-4817-9070-e302ae7172a3/image.png" alt=""></p>
<p>txt 파일로 잘 저장되고 불러와짐을 확인할 수 있다!</p>
<p>직렬화를 수행할 때 만약 언리얼 오브젝트를 직렬화 한다면
그 때 필요한 모든 프로퍼티들이 UPROPERTY 매크로가 잘 붙어 있는지 확인해야함에 주의하자!</p>
<p>안붙이면 언리얼이 인식을 못해버릴 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[언리얼 엔진의 메모리 관리]]></title>
            <link>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Thu, 27 Jun 2024 13:34:33 GMT</pubDate>
            <description><![CDATA[<p>C++은 프로그래머가 직접 <code>new</code>, <code>delete</code>로 할당과 해지를 해주어야했다.
이걸 잘못 관리하면 아래와 같은 문제점이 나타나게 된다.</p>
<ul>
<li><code>Memory Leak</code> : 메모리가 해지되지 않아서 힙에 메모리가 그대로 남음</li>
<li><code>Dangling Pointer</code> : 이미 해제된 메모리의 주소를 가르키는 포인터</li>
<li><code>Wild Pointer</code> : 값이 초기화 되지 않아서 엉뚱한 주소를 가르키는 포인터</li>
</ul>
<p>이런 실수를 줄이기 위해 많은 프로그래밍 언어들이 가비지 컬렉터를 사용한다.
언리얼 엔진도 이런 가비지 컬렉션 시스템을 사용하고, 그 중 <code>마크-스윕</code> 방식을 사용한다.</p>
<h2 id="가비지-컬렉션">가비지 컬렉션</h2>
<p>프로그램에서 더 이상 사용하지 않는 오브젝트를 자동으로 감지해 메모리를 회수하는 시스템이다.</p>
<h3 id="마크-스윕-방식">마크-스윕 방식</h3>
<ol>
<li>저장소에서 최초 검색을 시작하는 루트 오브젝트를 표기한다.</li>
<li>루트 오브젝트가 참조하는 객체를 찾아 마크한다.</li>
<li>마크된 객체로부터 다시 참조하는 객체를 찾아 마크하고 이를 계속 반복한다.</li>
<li>이제 저장소에는 마크된 객체와 마크되지 않은 객체의 두 그룹으로 나뉜다.</li>
<li>가비지 컬렉터가 저장소에서 마크되지 않은 객체들의 메모리를 회수(스윕)한다.</li>
</ol>
<h2 id="언리얼-엔진의-가비지-컬렉션">언리얼 엔진의 가비지 컬렉션</h2>
<p>이런 가비지 컬렉션을 백그라운드에서 계속 돌리는 것이 생각보다 비용이 꽤 든다.
이것을 조정할 수 있도록 프로젝트 설정에서 다양한 값을 변경할 수 있다.
<img src="https://velog.velcdn.com/images/lee_raccoon/post/35401862-f5b2-48d2-a864-ef4c7db70399/image.png" alt=""></p>
<p>언리얼에는 관리되는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수 <code>GUObjectArray</code>가 존재한다.
이 변수의 각 요소에는 <code>Flag</code>가 설정되어 있다.
주요 Flag로는 아래와 같은 친구들이 있다.
가비지 컬렉터는 여기의 플래그를 주기적으로 확인해서 메모리를 회수해간다.</p>
<ul>
<li>Garbage Flag : 다른 언리얼 오브젝트로부터의 참조가 없어 회수 예정</li>
<li>RootSet Flag : 다른 언리얼 오브젝트로부터의 참조가 없어도 회수하지 않는 오브젝트
(GUObjectArray에서 제공하는 AddToRoot 함수를 호출하여 설정 가능, 근데 별로 쓰지는 않음)</li>
</ul>
<p>언리얼 오브젝트를 메모리에서 삭제하려면 C++마냥 <code>delete</code>를 사용해서 하는 것이 아니라 레퍼런스를 없앰으로써 가비지 컬렉터가 자동으로 메모리를 회수할 수 있도록 하는 것이 좋다.</p>
<p>언리얼 오브젝트를 사용한다면 위에서 알아봤던 포인터를 사용함에 있어 나오는 문제점을 해결 할 수 있다.</p>
<ul>
<li><code>Memory Leak</code> : 가비지 컬렉션을 사용하여 해결</li>
<li><code>Dangling Pointer</code> : <code>IsValid()</code>등의 함수를 사용하여 해결</li>
<li><code>Wild Pointer</code> : <code>UPROPERTY</code> 매크로를 사용하면 자동으로 초기화</li>
</ul>
<h3 id="회수되지-않는-오브젝트">회수되지 않는 오브젝트</h3>
<ul>
<li>UPROPERTY로 참조된 언리얼 오브젝트 (대부분)</li>
<li>FGCObject의 AddReferenceObject 함수를 통해 참조를 설정한 언리얼 오브젝트
(UPROPERTY를 사용하지 못할 때 Ex : C++ 클래스에서 언리얼 오브젝트를 프로퍼티로 가질 때)</li>
<li>루트셋으로 지정된 오브젝트</li>
</ul>
<h3 id="언리얼-오브젝트-관리-원칙">언리얼 오브젝트 관리 원칙</h3>
<ul>
<li>생성된 어리얼 오브젝트를 유지하기 위해 레퍼런스 참조 방법을 설계할 것<ul>
<li>일반 C++ 오브젝트 내의 언리얼 오브젝트 : FGCObject의 상속 후 구현</li>
<li>언리얼 오브젝트 내의 언리얼 오브젝트 : UPROPERTY</li>
<li><blockquote>
<p>UPROPERTY를 붙이지 않는다면 <code>nullptr</code>은 아닌데 <code>IsValid</code>에서는 유효하지 않다고 나온다.</p>
</blockquote>
</li>
</ul>
</li>
<li>생성된 언리얼 오브젝트는 강제로 지우려 하지 말 것<ul>
<li>참조를 끊는다는 생각으로 설계</li>
<li>가비지 컬렉터에게 회수를 부탁할 수는 있음 (ForceGarbageCollection)</li>
<li>Destroy라는 함수를 사용할 수 있다. 근데 이것도 회수 플래그를 세우는 것이지 바로 없애는 건 아님</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[언리얼 컨테이너 라이브러리]]></title>
            <link>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC</link>
            <guid>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC</guid>
            <pubDate>Thu, 27 Jun 2024 11:56:44 GMT</pubDate>
            <description><![CDATA[<h2 id="주요-언리얼-컨테이너-라이브러리">주요 언리얼 컨테이너 라이브러리</h2>
<p>언리얼의 대표 컨테이너 라이브러리에는 TArray, TMap, TSet이 있다.
줄여서 UCL이라고 불리기도 하는 이들은 언리얼 오브젝트들을 안정적으로 지원을 해준다.</p>
<h3 id="stl과의-차이점">STL과의 차이점</h3>
<p>그냥 C++ STL의 컨테이너들과 무엇이 다를까?
STL은 범용성이 높게 설계되어있기 때문에 한번 컴파일 하는데 시간이 오래 걸리게 된다.</p>
<p>언리얼은 언리얼에서 제공하는 오브젝트만 안정적으로 지원을 하고 최대한 효율을 뽑아내야하기 때문에 게임 제작에 최적화 되어 있다고 생각하면 된다.</p>
<h3 id="각-컨테이너들의-특징">각 컨테이너들의 특징</h3>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/80e98a33-47c5-4939-9eea-a951f7f25962/image.png" alt=""></p>
<ul>
<li>TArray : 그냥 오브젝트를 순서대로 담음</li>
<li>TSet : 중복되지 않는 요소로 구성된 집합</li>
<li>TMap : 키, 밸류 조합의 레코드를 관리하는 용도로 사용</li>
</ul>
<p>조금 더 자세히 알아보자</p>
<h3 id="tarray">TArray</h3>
<p>언리얼의 가장 간단하고 가장 많이 쓰게 되는 컨테이너 클래스라고 할 수 있다.</p>
<p>가변 배열 자료구조로써, STL의 Vector와 비슷하다고 생각하면 된다.</p>
<p>데이터가 메모리에 모여있기 때문에 캐시 지역성으로 인한 성능 향상에 유리하다.</p>
<p>하지만 가변 배열의 단점으로써, 중간에 요소를 추가하거나 삭제하는 작업의 비용이 비교적 크다.</p>
<p>데이터가 많아지면 검색, 삭제, 수정 등의 작업이 느려지기 때문에 데이터가 방대한데 검색 작업이 빈번하다면 TArray 대신 TSet 사용을 고려해보는 것도 좋다.</p>
<p>TArray는 밸류 타입이기 때문에 동적 할당을 하는 것은 좋지 않다는 것을 명심하자.</p>
<p>TArray에 값을 넣는 방법은 Add와 Emplace가 있는 데, Emplace의 효율이 더 좋다고 한다. 근데 Add은 가독성이 좋으니 간단한 작업에 쓴다고 한다.</p>
<h3 id="tset">TSet</h3>
<p>TSet은 STL의 Set과는 다르게 해시테이블 형태로 키 데이터가 구축되어 있어서 서로 활용 용도가 다르다.
(STL은 이진 트리로 구성)</p>
<p>TSet은 독립된 키로 데이터 값을 연결하기 보다는 데이터 값 자체를 키로 사용하며 중복을 허용하지 않는다.</p>
<p>언리얼에서 제공하는 것이 아닌 독자적인 커스텀 타입을 만들어서 사용할 시, <code>GetTypeHash</code>라는 함수를 만들어서 해시를 만들 수 있게 해야 사용 가능하다. 그리고 동일성 비교를 위한 operator ==를 만들어야한다.</p>
<p>TSet은 검색도 빠르고 추가, 삭제할 때도 재구축이 일어나지 않아서 빠르게 사용하기 좋다.</p>
<p>대신 재구축이 일어나지 않는 만큼, 비어있는 데이터가 발생할 수도 있다.</p>
<p>비어있는 공간에는 다음에 들어오게 될 데이터가 자리하게 된다.</p>
<p>TSet은 인덱스로 사용이 가능은 하지만 권장은 하고있지 않다.
위와같이 데이터가 들어오면 Set의 순서를 모두 파악하고 있기 쉽지 않기 때문도 있을 것이다.</p>
<h3 id="tmap">TMap</h3>
<p>TArray 다음으로 자주 쓰이는 컨테이너이다.</p>
<p>TSet 구조로 되어있으며 다른 점은 키, 밸류 구성의 튜플 데이터를 가진다는 점이다.
내부적으로 <code>TPair&lt;Key, Element&gt;</code> 이런 오브젝트로 구성이 되어 있다고 생각하면 된다.
TMultiMap을 사용하면 중복을 허용할 수 있다.</p>
<h4 id="keyfuncs">KeyFuncs</h4>
<p>기본적으로 해시 테이블 구조로 관리하기 떄문에 커스텀 데이터 자료 구조를 만들 때는 이 구조체에 해시 값을 만들어주어야한다고 위 TSet에서 알아보았다.</p>
<p>근데 여기서 특이한 경우가 발생하는데,
기본적으로 멤버변수에 ID와 프로퍼티가 존재할 때, 내용물은 같은데 ID만 다를때는 이 구조체가 서로 같다고 보기 힘들다.
TMap에서는 중복을 허용하지 않지만 ID를 제외하고서는 같은 구조체이기 때문에 이를 어떻게 처리를 해야할지 고민에 빠지는 상황이 생길 수 있다는 뜻이다.</p>
<p>이런 경우에 추가적으로 ==에 대해서 정의를 해주어야 할 필요가 있을 때가 있다.
이 때 사용하라고 만들어 둔 것들이 있는데, 바로 KeyFuncs이다.</p>
<p><a href="https://dev.epicgames.com/documentation/ko-kr/unreal-engine/map-containers-in-unreal-engine?application_version=5.3">Unreal TMap 공식문서</a></p>
<p>과연 이런 상황을 얼마나 마주칠까는 모르겠는데 알아두면 좋지 않을까 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[언리얼 엔진 온라인 서브시스템(스팀) 알아보기]]></title>
            <link>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84-%EC%98%A8%EB%9D%BC%EC%9D%B8-%EC%84%9C%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%8A%A4%ED%8C%80-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-%EC%97%94%EC%A7%84-%EC%98%A8%EB%9D%BC%EC%9D%B8-%EC%84%9C%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%8A%A4%ED%8C%80-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 22 Jun 2024 11:29:47 GMT</pubDate>
            <description><![CDATA[<p>멀티플레이 구현의 목표는 다른 유저들이 IP 주소를 갖고있지 않아도 로그인하여 참가할 수 있는 게임을 만드는 것이다.</p>
<h2 id="온라인-서브-시스템이-뭔데">온라인 서브 시스템이 뭔데</h2>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/5e531f87-dd51-4786-ab37-04ca08d082e4/image.png" alt=""></p>
<p>리슨 서버를 열었다고 해도 서버의 IP를 플레이어들은 알 방법이 없기 때문에 중간에 특정 서비스를 거쳐 이를 알아낼 수 있어야한다.</p>
<p>이런 서비스 중 대표적인 것이 스팀, XBox Live 등이 있다.</p>
<p>각 서비스들은 각각 유저 간의 연결을 처리하기 위해 고유한 코드를 갖고 있다.</p>
<p>그렇다면 스팀에 올릴 떄는 스팀의 코드를, XBox에 올릴 때는 XBox의 코드를 따라 가야하는가?
이건 너무 불편하다.
그래서 언리얼에서는 공통적으로 사용할 수 있는 코드 베이스를 제공한다!</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/84fde247-f7dd-4f60-88c9-a15ad35615d9/image.png" alt=""></p>
<p>언리얼 엔진에서는 어떤 서비스에 연결되었느냐에 따라서 알아서 세부 사항을 처리해주며 <strong>추상 계층 (Abstraction Layer)</strong>이라고 불린다.</p>
<p>그리고 이렇게 추상화된 네트워킹 계층을 <strong>온라인 서브시스템</strong>이라고 부른다.</p>
<p>이로써 우리 개발자들은 언리얼 코드 베이스와만 친해진다면 된다!
이렇게 감사할 수가..</p>
<p>한 문장으로 정리하자면 온라인 플랫폼 서비스 기능에 액세스 하는 방법, 인터페이스를 제공하는 것이다.
(플랫폼은 위에서 말한 스팀, XBox Live 등이 있겠지요)</p>
<h2 id="온라인-세션">온라인 세션</h2>
<p>다양한 인터페이스 중 온라인 세션에 대해 알아보자.</p>
<p>세션 인터페이스는 게임 세션을 생성, 관리, 파괴하는 것을 담당한다.
세션 검색과 매치메이킹, 세션 검색도 다룬다.
(세션은 서버에서 돌아가고 있는 게임의 인스턴스라고 생각하면 편하다.)</p>
<h3 id="세션의-생명주기">세션의 생명주기</h3>
<p>보통 게임을 한다면 이런 타입의 세션 생명주기를 갖게 된다.</p>
<ul>
<li>세션 생성</li>
<li>플레이어 등록</li>
<li>세션 시작</li>
<li>게임 플레이</li>
<li>세션 종료</li>
<li>플레이어 연결 종료</li>
<li>세션을 업데이트하거나 세션 파괴</li>
</ul>
<p>그리고 세션 인터페이스에는 다양한 기능이 있는데 그 중</p>
<ul>
<li>CreateSession()</li>
<li>FindSession()</li>
<li>JoinSession()</li>
<li>StartSession()</li>
<li>DestroySession()</li>
</ul>
<p>이렇게 다섯함수만으로도 이미 훌륭하게 작동하는 게임을 만들 수 있다.</p>
<p>그러면 게임에서 Host, Join 버튼을 나누어 게임을 만들어 볼 수 있다.</p>
<p><strong>Host</strong>
호스트의 경우는 방만들기라고 생각하면 쉽다. Host를 누르면 <code>CreateSession()</code>을 하고 로비에서 대기한다.
<strong>Join</strong>
조인 하는 사람들은 우선 <code>FindSession()</code>으로 적절한 세션을 검색한다.
세션을 찾으면 <code>JoinSession()</code>으로 세션에 대한 IP 주소를 얻어낼 수 있다.
그렇다면 이제 <code>ClientTravel()</code> 함수를 사용하여 해당 세션에 접속할 수 있을 것이다.</p>
<h2 id="한번-해보자">한번 해보자</h2>
<h3 id="환경-구축">환경 구축</h3>
<p>플러그인에 <code>Online Subsystem Steam</code>을 검색하면 아래와 같은 플러그인이 나오는데, 활성화 시켜준다.
<img src="https://velog.velcdn.com/images/lee_raccoon/post/56e47d4b-9932-4ec0-a299-72b9ba36eed4/image.png" alt="">
그리고 .Build.cs에서 모듈 추가를 해준다.
<code>OnlineSubsystem</code>과 <code>OnlineSubsystemSteam</code>은 서로 다른 모듈이기 때문에 둘 중 하나만 해주는 게 아니라 꼭 둘 다 해주어야한다</p>
<pre><code>PublicDependencyModuleNames.AddRange(new string[] { &quot;Core&quot;, &quot;CoreUObject&quot;, &quot;Engine&quot;, &quot;InputCore&quot;, &quot;EnhancedInput&quot;, &quot;OnlineSubsystemSteam&quot;, &quot;OnlineSubsystem&quot; });</code></pre><p>그리고 프로젝트 Config 폴더에 있는 DefaultEngine.ini에서 뭘 좀 추가해주어야한다.</p>
<p><a href="https://docs.unrealengine.com/4.27/ko/ProgrammingAndScripting/Online/Steam/">Unreal OnlineSubsystemSteam</a>
공식 문서에서도 찾아볼 수 있다.</p>
<pre><code>[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName=&quot;GameNetDriver&quot;,DriverClassName=&quot;OnlineSubsystemSteam.SteamNetDriver&quot;,DriverClassNameFallback=&quot;OnlineSubsystemUtils.IpNetDriver&quot;)

[OnlineSubsystem]
DefaultPlatformService=Steam

[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480
bInitServerOnClient=true

[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName=&quot;OnlineSubsystemSteam.SteamNetConnection&quot;</code></pre><p>얘네를 추가해주고 다시 프로젝트 파일을 생성해주자</p>
<p>프로젝트 파일 오른쪽 클릭하면 나온다 (혹시 모르시는 분들을 위해,,)
<img src="https://velog.velcdn.com/images/lee_raccoon/post/cf68ae3f-7d44-481f-8381-9a0c95ea9a3e/image.png" alt=""></p>
<h3 id="테스트">테스트</h3>
<p>온라인 서브시스템에 연결됐는지 확인을 해보자</p>
<pre><code class="language-cpp">#include &quot;OnlineSubsystem.h&quot;
#include &quot;Interfaces/OnlineSessionInterface.h&quot;

IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get();
if (OnlineSubsystem)
{
    OnlineSubsystem-&gt;GetSessionInterface();

    if (GEngine)
    {
        GEngine-&gt;AddOnScreenDebugMessage(
            -1,
            15.f,
            FColor::Blue,
            FString::Printf(TEXT(&quot;Found Subsystem %s&quot;), *OnlineSubsystem-&gt;GetSubsystemName().ToString())
        );
    }
}</code></pre>
<p>우리는 스팀을 설정했으니 이제 스팀이 뜨겠지? 라는 설레는 마음으로 실행을 해보면?
<img src="https://velog.velcdn.com/images/lee_raccoon/post/1f9f9de9-42be-41e5-9764-c3c54ecc4277/image.png" alt="">
??
NULL이 뜨는 것을 볼 수 있다.
NULL인데 잘도 오류는 안떴네.. 라는 생각을 했는데
실제로 언리얼 엔진은 NULL이라는 온라인 서브시스템을 갖고 있다(?)
LAN 환경에서를 위해 라는데 신기하다</p>
<p>아무튼 왜 안뜨는고 하니 패키징을 해야한다고 한다.
PIE는 지원을 안하는건가..
<img src="https://velog.velcdn.com/images/lee_raccoon/post/af7dc8b5-41a4-46ce-bd1a-3951a7443364/image.png" alt=""></p>
<p>패키징을 하니 바로 잘 되는 모습을 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/50b23b75-9c5d-48fe-b023-6830eac4485a/image.gif" alt=""></p>
<p>스팀 화면도 띄울 수 있으니 뭔가 기분이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[언리얼 멀티플레이 구현 개념]]></title>
            <link>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-%EB%A9%80%ED%8B%B0%ED%94%8C%EB%A0%88%EC%9D%B4-%EA%B5%AC%ED%98%84-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@lee_raccoon/%EC%96%B8%EB%A6%AC%EC%96%BC-%EB%A9%80%ED%8B%B0%ED%94%8C%EB%A0%88%EC%9D%B4-%EA%B5%AC%ED%98%84-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Sat, 22 Jun 2024 08:39:21 GMT</pubDate>
            <description><![CDATA[<h2 id="멀티플레이-통신-방식">멀티플레이 통신 방식</h2>
<p>게임에서 멀티플레이를 구현하는 데에는 크게 두가지 방법으로 나뉜다.</p>
<ul>
<li>P2P</li>
<li>클라이언트-서버 모델</li>
</ul>
<p>간단하게 설명하자면 P2P는 Peer to Peer의 약어로 모든 플레이어가 이어져있는 것을 뜻한다.</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/be87a6ba-05db-4fb7-9515-d632dd4a7f37/image.png" alt=""></p>
<p>클라이언트 서버 모델은 하나의 시스템을 서버로 정의하고 다른 모든 시스템은 클라이언트로 정의 한 후, 모든 클라이언트가 서버에 연결되어 통신하는 방식이다.</p>
<p>모든 클라이언트는 오직 서버와만 대화할 수 있다.</p>
<p>P2P가 각각의 플레이어와 모두 통신을 해야했던 것과는 다르게 서버에게만 데이터를 보내면 서버가 다른 클라이언트에게 이 정보를 모두 공유하게 된다.</p>
<p>또한 서버가 클라이언트에게 받은 내용이 적절치 않다고 판단되면 다른 클라이언트에게 이를 레플리케이션하지 않을 수 있기 때문에 보안적인 측면에서도 더 좋다고 할 수 있다.</p>
<p>클라이언트 서버 모델에는 크게 두 가지가 있다.</p>
<ul>
<li><p>Dedicated 서버 </p>
<ul>
<li>Player를 갖지 않음</li>
<li>렌더링을 하지 않음</li>
<li>그저 게임을 시뮬레이트 하고 있을 뿐</li>
</ul>
</li>
<li><p>Listen 서버</p>
<ul>
<li>Player가 호스팅 하는 서버임</li>
<li>호스트는 지연 시간이 발생하지 않으므로 어드밴티지를 가짐</li>
</ul>
</li>
</ul>
<p>리슨 서버는 직접 게임을 플레이 하는 플레이어가 서버를 맡게 되는 것이다.
데디케이티드 서버는 플레이어가 아닌 전용 서버 시스템을 구축해두는 것이다.</p>
<p>규모가 큰 게임같은 경우 고사양의 서버가 필요할 수 있기 때문에 보통 전용 서버를 사용하는 것이 일반적이다.</p>
<p>리슨 서버는 본인이 곧 서버인 플레이어가 존재하기 때문에 이를 통해 이점을 얻을 수 있기도 하다.
그래서 경쟁이 주요 게임 콘텐츠인 경우 리슨 서버 사용을 피하기도 한다.</p>
<h2 id="언리얼의-통식-방식">언리얼의 통식 방식</h2>
<p>언리얼은 클라이언트-서버 방식을 사용한다.
싱글 플레이 게임의 경우도 여전히 클라이언트 서버 방식을 사용한다는 점을 처음 알았는데, 어짜피 본인이 서버이자 클라이언트이기 때문에 그냥 그대로 쓰는 것 같다.</p>
<h2 id="멀티플레이어-게임의-특성">멀티플레이어 게임의 특성</h2>
<p>멀티플레이어 게임은 보통 서버가 존재한다.</p>
<p>서버는 다른 클라이언트들과는 독자적으로 존재하며 각 클라이언트들은 그들만의 게임을 갖고 있다.
언리얼에서는 서버가 Authority를 가진다.
각자가 각자의 게임을 갖고 있기 때문에 어떤 게임이 올바른 게임인지 딱 정해야하는게 그게 서버 측의 게임이라는 것.</p>
<p>위와 같은 특성으로 멀티플레이어에는 몇가지 특징이 있다.</p>
<ul>
<li><p>Game Mode는 서버에 하나만 존재한다. </p>
<ul>
<li>클라이언트에서 GameMode를 얻으려하면 nullptr를 얻게 된다.</li>
</ul>
</li>
<li><p>PlayerController는 서버에도, 클라이언트에도 존재한다.</p>
<ul>
<li>서버는 모든 플레이어의 컨트롤러가 존재, 클라이언트는 자기만의 컨트롤러가 존재.</li>
</ul>
</li>
<li><p>PlayerState는 서버와 클라이언트 둘 다 모든 플레이어의 State가 존재한다.</p>
<ul>
<li>클라이언트가 플레이하는 Pawn 이외에도 Pawn이 존재하므로 그에 대한 State가 있는 것은 당연할 것</li>
</ul>
</li>
<li><p>HUD는 클라이언트 각자의 것만 가짐 
  -서버가 Dedicated인 경우 HUD가 없고 Listen 서버인 경우 해당 플레이어를 위한 HUD가 존재</p>
</li>
</ul>
<h3 id="레플리케이션-replication">레플리케이션 (Replication)</h3>
<p>이제 서버와 클라이언트들은 레플리케이트를 통해
어떤 Value가 레플리케이트 설정이 되어 있으면 이제 NetUpdate 때 마다 서버의 값을 가져와서 동기화 시키게 된다.</p>
<p>서버의 값이 바뀐 경우는 다음 업데이트에서 클라이언트에게 보내질 것이다.
근데 클라이언트 값이 바뀐 경우는?
클라이언트에서 서버로 값이 이동하나?</p>
<p>결론을 말하자면 Replication은 서버 -&gt; 클라이언트로 일방통행이다.</p>
<p>어? 근데 그러면 클라이언트들의 입력을 서버가 받아서 서버가 클라이언트들에게 &quot;야, 얘 움직였다&quot; 라고 말해줘야할텐데
어떻게 서버에게 전달하지? 싶다.</p>
<p>그건 RPC (Remote Procedure Control)이라는 방식을 통해서 서버에 전달되게 된다.</p>
<h2 id="언리얼-멀티플레이-테스트">언리얼 멀티플레이 테스트</h2>
<p>LAN 환경에서 간단하게 멀티플레이 환경을 테스트 해보자</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/0c222b98-84bd-4184-b490-0dd309e3115a/image.png" alt="">
프로젝트에 간단하게 로비를 만들었다.
캐릭터에 1번을 눌러 본인을 리슨서버로써 레벨을 여는 블루프린트를 작성하였다.
2번을 누르면 내 IP 주소에서 열린 서버에 참여하는 것이다.</p>
<p>하지만 코드로도 하는 법을 알아야겠지?
(그래봤자 언리얼 형님들이 만들어놓으신 함수를 가져다 쓰는 것 밖에 안되지만..)</p>
<pre><code class="language-cpp">void AMultiPlayTestCharacter::OpenLobby()
{
    UWorld* World = GetWorld();
    if (World)
    {
        World-&gt;ServerTravel(&quot;/Game/Levels/Lobby?listen&quot;);
    }
}

void AMultiPlayTestCharacter::CallOpenLevel(const FString&amp; Address)
{
    UGameplayStatics::OpenLevel(this, *Address);
}

void AMultiPlayTestCharacter::CallClientTravel(const FString&amp; Address)
{
    APlayerController* PlayerController = GetGameInstance()-&gt;GetFirstLocalPlayerController();
    if (PlayerController)
    {
        PlayerController-&gt;ClientTravel(Address, ETravelType::TRAVEL_Absolute);
    }
}</code></pre>
<p>위와같은 함수들을 만들어서 <code>BlueprintCallable</code>로 만들어줬다.</p>
<p><code>World-&gt;ServerTravel(&quot;/Game/Levels/Lobby?listen&quot;);</code>에서 레벨과 함께 뒤에 <code>?</code>를 붙이고 옵션을 붙일 수 있다.
여기서는 <code>listen</code>을 붙임으로써 리슨 서버로 레벨을 열 수 있었다.</p>
<p><code>UGameplayStatics</code>의 <code>OpenLevel</code>에서는 <code>WorldContext</code>와 레벨을 넣어서 레벨을 열 수 있는데, 여기서 레벨이 아니라 IP 주소, URL을 넣음으로써 해당하는 곳으로 접속이 가능하다.</p>
<p><code>PlayerController</code>의 <code>ClientTravel</code>는 서버의 주소와 <code>TravelType</code>을 지정하여 새 서버로 이동하는 함수이다.
서버 측에서 이를 호출하면 클라이언트에게 다른 맵으로 이동시키는 데 쓰이고
클라이언트 측에서 호출하면 본인이 새 서버로 이동하는 데 쓰인다고 한다.</p>
<p>참고로 <code>UWorld</code>에 <code>ServerTravel</code>이라는 함수가 있는데, 서버 전용 함수로써 접속된 모든 클라이언트들의 <code>ClientTravel</code>을 호출시켜서 서버가 이동하는 맵으로 모두 따라가게 된다고 한다.</p>
<p>로비에서 본 게임으로 넘어갈 때 월드에서 이를 호출시키면 되는거겠지.</p>
<p><img src="https://velog.velcdn.com/images/lee_raccoon/post/3eba5fd0-bc4d-4d87-87d7-2ad265a4b966/image.png" alt=""></p>
<p>이제 실행해서 순서대로 1, 2, 3을 눌러보면?</p>
<p><img src="blob:https://velog.io/8635c99f-cb02-4024-ad84-dc513e1c5a3f" alt="업로드중.."></p>
<p>잘 된다! ServerTravel을 해본다는게 깜빡해서 한번 더 했는데
<img src="blob:https://velog.io/07a8bfd6-869a-4f2c-908b-712ff12395ba" alt="업로드중.."></p>
<p>아니... 어째서?
다들 잘 넘어와지긴 했는데 스폰 위치가 이상하게 설정됐는지 스폰되자마자 머나먼곳으로 떠나버렸다..</p>
<p>다음 맵으로 넘어갈 때 스폰 되는 걸 신경쓰지 않으면 클라이언트가 끔찍한 경험을 하게 될지도..</p>
]]></description>
        </item>
    </channel>
</rss>