<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>박왕건 님의 블로그</title>
    <link>https://talk87604.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 27 Jul 2026 17:26:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>왕건</managingEditor>
    <item>
      <title>2026-7-3 TIL</title>
      <link>https://talk87604.tistory.com/62</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIL: Unreal Engine FQuat와 FRotator의 차이점, 장점과 단점 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 배운 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Unreal Engine에서 회전을 표현할 때 사용하는 FRotator와 FQuat의 차이점에 대해 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 둘 다 회전을 나타내는 자료형이라서 차이가 크게 와닿지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 정리해보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;FRotator
&amp;rarr; Pitch, Yaw, Roll 세 개의 각도로 회전을 표현하는 방식

FQuat
&amp;rarr; X, Y, Z, W 네 개의 값으로 회전을 표현하는 쿼터니언 방식
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 회전을 나타내지만 사용하는 목적이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator는 사람이 읽고 이해하기 쉬운 회전 표현이고, FQuat는 내부 계산이나 보간에서 더 안정적인 회전 표현이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 정리의 핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;FRotator
&amp;rarr; 보기 쉬운 회전값

FQuat
&amp;rarr; 계산하기 좋은 회전값

FRotator의 장점
&amp;rarr; 직관적이고 디버깅하기 쉽다

FRotator의 단점
&amp;rarr; 짐벌락 문제가 생길 수 있다

FQuat의 장점
&amp;rarr; 짐벌락을 피하고 부드러운 회전 계산에 유리하다

FQuat의 단점
&amp;rarr; 사람이 직접 읽거나 수정하기 어렵다
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 단순한 회전값 설정에는 FRotator가 편하고, 복잡한 회전 계산이나 부드러운 보간에는 FQuat가 더 적합하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. FRotator의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator는 Unreal Engine에서 회전을 Pitch, Yaw, Roll 세 개의 각도로 표현하는 구조체다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 형태는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;FRotator Rotation(Pitch, Yaw, Roll);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 코드는 액터를 Yaw 방향으로 90도 회전시키는 코드다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FRotator NewRotation(0.0f, 90.0f, 0.0f);

SetActorRotation(NewRotation);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 각 값의 의미는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Pitch
&amp;rarr; 위아래로 고개를 드는 회전

Yaw
&amp;rarr; 좌우로 방향을 트는 회전

Roll
&amp;rarr; 몸이나 카메라가 옆으로 기울어지는 회전
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 FRotator는 사람이 생각하는 회전 방식과 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 캐릭터가 오른쪽으로 90도 돌아야 한다면 다음처럼 바로 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FRotator LookRightRotation(0.0f, 90.0f, 0.0f);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 보면 대략 어떤 회전인지 바로 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 FRotator는 디버깅이나 간단한 회전 설정에 많이 사용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. FRotator의 장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator의 가장 큰 장점은 직관적이라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 Pitch, Yaw, Roll로 나뉘어 있어서 사람이 읽기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 코드를 보면 어떤 회전인지 바로 이해할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FRotator Rotation(30.0f, 90.0f, 0.0f);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음과 같이 해석할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Pitch 30도
&amp;rarr; 위쪽으로 30도 회전

Yaw 90도
&amp;rarr; 오른쪽으로 90도 회전

Roll 0도
&amp;rarr; 기울어짐 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 장점은 에디터와 블루프린트에서 다루기 쉽다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unreal Editor의 Details 패널에서도 Rotation 값은 사람이 보기 쉬운 각도 형태로 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 오브젝트의 회전값을 직접 확인하거나 수정하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 FRotator의 장점은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 사람이 읽기 쉽다.
2. 디버깅하기 쉽다.
3. 에디터에서 확인하기 편하다.
4. 단순한 회전 설정에 적합하다.
5. 코드 작성이 간단하다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 액터를 특정 방향으로 바라보게 만드는 정도라면 FRotator만으로도 충분하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FRotator TargetRotation(0.0f, 180.0f, 0.0f);

SetActorRotation(TargetRotation);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 굳이 FQuat를 사용할 필요가 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. FRotator의 단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator의 단점은 오일러 각 방식이라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오일러 각은 세 축의 회전을 순서대로 적용해서 최종 회전을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 직관적이지만, 특정 상황에서 축이 겹치면서 회전 자유도가 줄어드는 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 짐벌락(Gimbal Lock)이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음처럼 매 프레임 FRotator 값을 직접 더해서 회전시키는 코드는 문제가 생길 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    FRotator CurrentRotation = GetActorRotation();

    CurrentRotation.Pitch += 90.0f * DeltaTime;
    CurrentRotation.Yaw += 90.0f * DeltaTime;
    CurrentRotation.Roll += 90.0f * DeltaTime;

    SetActorRotation(CurrentRotation);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 겉으로 보기에는 세 축을 계속 회전시키는 코드처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 회전이 누적되면서 특정 각도에 도달하면 축이 겹치거나, 기대한 방향과 다르게 회전할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 카메라 회전, 비행체 회전, 우주선 회전처럼 여러 축을 자유롭게 돌리는 경우에는 FRotator만으로 회전을 누적하면 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator의 단점은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 짐벌락 문제가 생길 수 있다.
2. 여러 축 회전을 계속 누적하면 예상과 다른 결과가 나올 수 있다.
3. 회전 보간이 부자연스러울 수 있다.
4. 같은 방향을 여러 개의 다른 각도 조합으로 표현할 수 있다.
5. 복잡한 3D 회전 계산에는 불리하다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 두 회전값은 숫자는 다르지만 실제 방향은 같게 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FRotator RotA(0.0f, 360.0f, 0.0f);
FRotator RotB(0.0f, 0.0f, 0.0f);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자로는 다르지만, 방향으로 보면 둘 다 한 바퀴 돌아 원래 방향을 보는 것과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 점 때문에 회전값 비교나 보간에서 헷갈릴 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. FQuat의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FQuat는 Quaternion, 즉 쿼터니언 방식으로 회전을 표현하는 구조체다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 형태는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;FQuat RotationQuat(X, Y, Z, W);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator가 세 개의 각도를 사용하는 것과 달리, FQuat는 네 개의 값 X, Y, Z, W를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 FQuat의 X, Y, Z, W는 사람이 직접 보고 이해하기 쉬운 값이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 값을 봐도 어떤 방향으로 몇 도 회전한 것인지 바로 알기 어렵다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FQuat Quat(0.0f, 0.0f, 0.7071f, 0.7071f);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 값은 수학적으로는 의미가 있지만, FRotator(0, 90, 0)처럼 직관적으로 읽히지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 FQuat는 보통 직접 값을 넣기보다는 FRotator에서 변환하거나, 특정 축과 각도로 생성해서 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 FRotator를 FQuat로 바꾸는 방법은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FRotator Rotator(0.0f, 90.0f, 0.0f);

FQuat QuatA = FQuat(Rotator);
FQuat QuatB = Rotator.Quaternion();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 FQuat를 다시 FRotator로 바꿀 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;FQuat Quat = FQuat(FRotator(0.0f, 90.0f, 0.0f));

FRotator Rotator = Quat.Rotator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 FQuat와 FRotator는 서로 변환할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;FRotator
&amp;rarr; FQuat로 변환 가능

FQuat
&amp;rarr; FRotator로 변환 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 중요한 점은 내부 계산에는 FQuat가 더 안정적인 경우가 많고, 사람이 확인할 때는 FRotator가 더 편하다는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. FQuat의 장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FQuat의 가장 큰 장점은 복잡한 회전 계산에서 안정적이라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 여러 축의 회전을 계속 누적하거나, 두 회전 사이를 부드럽게 보간할 때 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FQuat는 오일러 각 방식이 아니기 때문에 FRotator에서 발생할 수 있는 짐벌락 문제를 피하는 데 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 매 프레임 회전을 누적해야 하는 경우에는 다음처럼 FQuat를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    const float DegreesPerSecond = 90.0f;
    const float DeltaAngleRad = FMath::DegreesToRadians(DegreesPerSecond * DeltaTime);

    FQuat CurrentQuat = GetActorQuat();

    FQuat DeltaQuat(FVector::UpVector, DeltaAngleRad);

    FQuat NewQuat = DeltaQuat * CurrentQuat;

    NewQuat.Normalize();

    SetActorRotation(NewQuat);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 현재 액터의 회전을 FQuat로 가져온 뒤, Z축 기준으로 회전을 추가하는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 부분은 다음이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;FQuat CurrentQuat = GetActorQuat();

FQuat DeltaQuat(FVector::UpVector, DeltaAngleRad);

FQuat NewQuat = DeltaQuat * CurrentQuat;

NewQuat.Normalize();

SetActorRotation(NewQuat);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator처럼 Pitch, Yaw, Roll 값을 직접 더하는 것이 아니라, 쿼터니언끼리 곱해서 회전을 합성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 연속적인 회전에서 더 안정적인 결과를 낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 FQuat는 보간에도 자주 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 두 회전 사이를 부드럽게 회전시키고 싶다면 FQuat::Slerp를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FRotator StartRotator(0.0f, 0.0f, 0.0f);
FRotator EndRotator(0.0f, 180.0f, 0.0f);

FQuat StartQuat = StartRotator.Quaternion();
FQuat EndQuat = EndRotator.Quaternion();

float Alpha = 0.5f;

FQuat ResultQuat = FQuat::Slerp(StartQuat, EndQuat, Alpha);

FRotator ResultRotator = ResultQuat.Rotator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 시작 회전과 목표 회전의 중간 회전을 구하는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Alpha가 0이면 시작 회전이고, 1이면 목표 회전이다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;Alpha = 0.0f
&amp;rarr; StartQuat

Alpha = 0.5f
&amp;rarr; 중간 회전

Alpha = 1.0f
&amp;rarr; EndQuat
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 FQuat는 두 방향 사이를 자연스럽게 회전시키는 데 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 FQuat의 장점은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 짐벌락 문제를 피하는 데 유리하다.
2. 연속적인 회전 누적에 안정적이다.
3. 회전 보간에 적합하다.
4. 복잡한 3D 회전 계산에 유리하다.
5. 카메라, 비행체, 투사체, 물리 회전 등에 적합하다.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. FQuat의 단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FQuat의 단점은 사람이 읽기 어렵다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator는 다음처럼 보면 바로 이해할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FRotator Rotation(0.0f, 90.0f, 0.0f);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 Yaw 방향으로 90도 회전이라는 것을 바로 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 FQuat는 다음처럼 봐도 무슨 회전인지 바로 알기 어렵다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FQuat RotationQuat(0.0f, 0.0f, 0.7071f, 0.7071f);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 사람이 직접 수정하기에도 적합하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X, Y, Z, W 중 하나를 마음대로 바꾸면 원하는 회전이 나오는 것이 아니라, 잘못된 회전값이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 쿼터니언은 보통 단위 쿼터니언 상태를 유지해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 계산을 반복하다 보면 부동소수점 오차가 누적될 수 있으므로, 필요하면 정규화를 해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;FQuat NewQuat = DeltaQuat * CurrentQuat;

NewQuat.Normalize();

SetActorRotation(NewQuat);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 FQuat의 단점은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 사람이 읽기 어렵다.
2. 직접 값을 수정하기 어렵다.
3. 수학적 개념이 필요해서 처음 이해하기 어렵다.
4. 회전 곱셈 순서가 헷갈릴 수 있다.
5. 디버깅할 때는 FRotator보다 불편하다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 FQuat는 내부 계산용으로 사용하고, 로그나 디버깅에서는 FRotator로 변환해서 보는 경우가 많다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;FQuat Quat = GetActorQuat();

FRotator DebugRotator = Quat.Rotator();

UE_LOG(LogTemp, Warning, TEXT(&quot;Rotation: %s&quot;), *DebugRotator.ToString());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 내부 회전은 FQuat로 관리하면서, 출력은 사람이 읽기 쉬운 FRotator 형태로 확인할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. FRotator와 FQuat 변환 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator와 FQuat는 서로 변환할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 FRotator에서 FQuat로 변환하는 방법은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FRotator Rotator(0.0f, 90.0f, 0.0f);

FQuat Quat = Rotator.Quaternion();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 생성자를 사용할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FRotator Rotator(0.0f, 90.0f, 0.0f);

FQuat Quat(Rotator);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 FQuat에서 FRotator로 변환하려면 Rotator()를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;FQuat Quat = FQuat(FRotator(0.0f, 90.0f, 0.0f));

FRotator Rotator = Quat.Rotator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사람이 이해하기 쉬운 값이 필요함
&amp;rarr; FRotator 사용

안정적인 회전 계산이 필요함
&amp;rarr; FQuat로 변환

계산이 끝난 뒤 로그나 에디터 확인이 필요함
&amp;rarr; 다시 FRotator로 변환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;FRotator InputRotator(0.0f, 90.0f, 0.0f);

FQuat Quat = InputRotator.Quaternion();

FQuat AdditionalRotation(FVector::UpVector, FMath::DegreesToRadians(45.0f));

FQuat ResultQuat = AdditionalRotation * Quat;

FRotator ResultRotator = ResultQuat.Rotator();

SetActorRotation(ResultRotator);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 처음에는 사람이 이해하기 쉬운 FRotator로 회전값을 만들고, 계산은 FQuat로 처리한 뒤, 다시 FRotator로 바꿔서 적용하는 예시다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. FRotator를 사용하기 좋은 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator는 간단하고 명확한 회전 설정에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 캐릭터를 특정 방향으로 돌리거나, 오브젝트의 초기 회전값을 설정할 때는 FRotator가 편하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;void AMyActor::BeginPlay()
{
    Super::BeginPlay();

    FRotator StartRotation(0.0f, 90.0f, 0.0f);

    SetActorRotation(StartRotation);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 게임이 시작될 때 액터를 오른쪽 방향으로 회전시키는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 특정 방향을 바라보게 만들 때도 FRotator를 자주 사용한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;FVector Direction = TargetLocation - GetActorLocation();

FRotator LookAtRotation = Direction.Rotation();

SetActorRotation(LookAtRotation);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 타겟 위치를 향하는 방향 벡터를 구한 뒤, 그 방향을 FRotator로 변환해서 액터가 타겟을 바라보게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator를 사용하기 좋은 경우는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 특정 각도로 회전시키는 경우
2. 에디터에서 입력한 회전값을 코드에서 사용하는 경우
3. 캐릭터의 Yaw 회전처럼 단순한 회전이 필요한 경우
4. 로그로 회전값을 확인해야 하는 경우
5. 블루프린트와 연결해서 직관적으로 다루고 싶은 경우
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 일반적인 캐릭터 방향 전환은 FRotator로도 충분하다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;FRotator ControlRotation = GetControlRotation();

FRotator YawRotation(0.0f, ControlRotation.Yaw, 0.0f);

SetActorRotation(YawRotation);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서는 FQuat를 사용하면 오히려 코드가 더 복잡해질 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. FQuat를 사용하기 좋은 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FQuat는 복잡한 회전 계산이나 부드러운 회전 보간이 필요할 때 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 비행기, 드론, 우주선처럼 여러 축으로 자유롭게 회전하는 오브젝트는 FQuat가 더 안정적이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void AMyActor::AddYawRotation(float DeltaTime)
{
    const float RotationSpeed = 90.0f;

    const float DeltaAngleRad = FMath::DegreesToRadians(RotationSpeed * DeltaTime);

    FQuat CurrentQuat = GetActorQuat();

    FQuat DeltaQuat(FVector::UpVector, DeltaAngleRad);

    FQuat NewQuat = DeltaQuat * CurrentQuat;

    NewQuat.Normalize();

    SetActorRotation(NewQuat);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 Z축 기준으로 회전을 누적한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 목표 방향으로 부드럽게 회전시키고 싶을 때도 FQuat를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;void AMyActor::RotateToTarget(float DeltaTime)
{
    FQuat CurrentQuat = GetActorQuat();

    FQuat TargetQuat = FRotator(0.0f, 180.0f, 0.0f).Quaternion();

    const float InterpSpeed = 3.0f;

    const float Alpha = FMath::Clamp(DeltaTime * InterpSpeed, 0.0f, 1.0f);

    FQuat NewQuat = FQuat::Slerp(CurrentQuat, TargetQuat, Alpha);

    NewQuat.Normalize();

    SetActorRotation(NewQuat);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 현재 회전에서 목표 회전까지 부드럽게 보간한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FQuat를 사용하기 좋은 경우는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 여러 축 회전을 동시에 처리해야 하는 경우
2. 회전을 매 프레임 누적해야 하는 경우
3. 짐벌락을 피하고 싶은 경우
4. 두 회전 사이를 부드럽게 보간해야 하는 경우
5. 카메라, 비행체, 투사체, 물리 기반 회전을 다루는 경우
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 FQuat는 사람이 직접 입력하는 회전값이라기보다는, 회전 계산을 안정적으로 처리하기 위한 내부 표현에 가깝다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 짐벌락이 헷갈렸던 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 짐벌락이 단순히 회전이 안 되는 버그처럼 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 오일러 각 방식에서 축이 겹치면서 회전 자유도가 줄어드는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator는 Pitch, Yaw, Roll 세 개의 축 회전을 순서대로 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 특정 각도에서는 두 축이 같은 방향을 가리키는 것처럼 되어, 한 축 회전이 사라진 것처럼 보일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;FRotator로 Pitch, Yaw, Roll 회전 적용
    &amp;darr;
특정 각도에서 회전축이 겹침
    &amp;darr;
한 축의 회전 자유도가 줄어듦
    &amp;darr;
원하는 방향으로 회전하지 않음
    &amp;darr;
짐벌락처럼 보임
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 단순히 회전값을 한 번 설정하는 경우에는 괜찮지만, 여러 축을 계속 누적해서 돌리는 경우에는 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 코드는 단순해서 문제가 없어 보인다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;FRotator Rotation = GetActorRotation();

Rotation.Pitch += 90.0f * DeltaTime;
Rotation.Yaw += 90.0f * DeltaTime;
Rotation.Roll += 90.0f * DeltaTime;

SetActorRotation(Rotation);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 세 축의 회전이 계속 누적되면서 예상과 다른 결과가 나올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때는 FQuat로 회전을 합성하는 방식이 더 적합하다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;FQuat CurrentQuat = GetActorQuat();

FQuat DeltaPitch(FVector::RightVector, FMath::DegreesToRadians(90.0f * DeltaTime));
FQuat DeltaYaw(FVector::UpVector, FMath::DegreesToRadians(90.0f * DeltaTime));
FQuat DeltaRoll(FVector::ForwardVector, FMath::DegreesToRadians(90.0f * DeltaTime));

FQuat NewQuat = DeltaYaw * DeltaPitch * DeltaRoll * CurrentQuat;

NewQuat.Normalize();

SetActorRotation(NewQuat);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서는 FRotator 값에 직접 각도를 더하지 않고, 쿼터니언을 곱해서 회전을 합성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식이 여러 축 회전 누적에는 더 안정적이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11. FQuat 곱셈 순서 주의하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FQuat를 사용할 때 가장 헷갈리는 부분은 곱셈 순서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FQuat는 단순히 숫자를 더하는 방식이 아니라 곱셈으로 회전을 합성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 두 코드는 결과가 달라질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;FQuat NewQuatA = DeltaQuat * CurrentQuat;
FQuat NewQuatB = CurrentQuat * DeltaQuat;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 코드 모두 회전을 합성하지만, 어느 좌표계를 기준으로 회전을 적용하느냐가 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 다음처럼 이해하면 된다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;DeltaQuat * CurrentQuat
&amp;rarr; 월드 축 기준 회전에 가깝게 사용

CurrentQuat * DeltaQuat
&amp;rarr; 로컬 축 기준 회전에 가깝게 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 월드의 Z축을 기준으로 계속 회전하고 싶다면 다음처럼 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;FQuat CurrentQuat = GetActorQuat();

FQuat DeltaQuat(FVector::UpVector, FMath::DegreesToRadians(90.0f * DeltaTime));

FQuat NewQuat = DeltaQuat * CurrentQuat;

SetActorRotation(NewQuat);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 액터 자신의 로컬 축 기준으로 회전하고 싶다면 곱셈 순서를 바꿔야 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;FQuat CurrentQuat = GetActorQuat();

FQuat DeltaQuat(FVector::UpVector, FMath::DegreesToRadians(90.0f * DeltaTime));

FQuat NewQuat = CurrentQuat * DeltaQuat;

SetActorRotation(NewQuat);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 처음에 가장 많이 헷갈릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 FQuat를 사용할 때는 다음을 항상 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 월드 축 기준 회전인지
2. 로컬 축 기준 회전인지
3. 곱셈 순서가 의도한 결과와 맞는지
4. 결과가 이상하면 곱셈 순서를 바꿔서 확인해볼 것
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 FQuat는 안정적이지만, 곱셈 순서를 잘못 쓰면 원하는 회전이 나오지 않을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12. FRotator와 FQuat 비교 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator와 FQuat를 비교하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;FRotator
&amp;rarr; Pitch, Yaw, Roll 세 개의 각도로 회전 표현
&amp;rarr; 사람이 읽기 쉬움
&amp;rarr; 간단한 회전에 적합
&amp;rarr; 짐벌락 문제가 생길 수 있음
&amp;rarr; 디버깅하기 쉬움

FQuat
&amp;rarr; X, Y, Z, W 네 개의 값으로 회전 표현
&amp;rarr; 사람이 읽기 어려움
&amp;rarr; 복잡한 회전 계산에 적합
&amp;rarr; 짐벌락을 피하는 데 유리함
&amp;rarr; 보간과 회전 누적에 안정적임
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표처럼 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분FRotatorFQuat&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;표현 방식&lt;/td&gt;
&lt;td&gt;Pitch, Yaw, Roll&lt;/td&gt;
&lt;td&gt;X, Y, Z, W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기반 개념&lt;/td&gt;
&lt;td&gt;오일러 각&lt;/td&gt;
&lt;td&gt;쿼터니언&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사람이 읽기&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에디터 친화성&lt;/td&gt;
&lt;td&gt;좋음&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;짐벌락&lt;/td&gt;
&lt;td&gt;발생 가능&lt;/td&gt;
&lt;td&gt;피하는 데 유리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;회전 누적&lt;/td&gt;
&lt;td&gt;불안정할 수 있음&lt;/td&gt;
&lt;td&gt;안정적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보간&lt;/td&gt;
&lt;td&gt;단순 보간은 가능하지만 한계 있음&lt;/td&gt;
&lt;td&gt;Slerp에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 난이도&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주 사용 목적&lt;/td&gt;
&lt;td&gt;간단한 회전 설정&lt;/td&gt;
&lt;td&gt;복잡한 회전 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 둘 중 하나만 무조건 좋은 것이 아니라, 상황에 따라 다르게 써야 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;보기 쉽고 간단한 회전
&amp;rarr; FRotator

안정적인 회전 계산
&amp;rarr; FQuat
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구분하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13. 실제 사용 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로젝트에서 사용할 때는 다음 기준으로 선택하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 FRotator를 사용하기 좋은 경우는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;캐릭터가 좌우로 도는 정도의 회전
에디터에서 회전값을 직접 넣는 경우
특정 각도로 한 번만 회전시키는 경우
로그로 회전값을 확인해야 하는 경우
블루프린트와 연동해서 보기 쉽게 다루는 경우
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;FRotator SpawnRotation(0.0f, 90.0f, 0.0f);

GetWorld()-&amp;gt;SpawnActor&amp;lt;AMyActor&amp;gt;(
    AMyActor::StaticClass(),
    SpawnLocation,
    SpawnRotation
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 스폰 회전값은 FRotator가 훨씬 직관적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 FQuat를 사용하기 좋은 경우는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;회전을 매 프레임 누적하는 경우
여러 축으로 동시에 회전하는 경우
비행체나 우주선처럼 자유 회전이 필요한 경우
카메라를 부드럽게 회전시키는 경우
두 회전 사이를 자연스럽게 보간하는 경우
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;FQuat CurrentQuat = GetActorQuat();

FQuat TargetQuat = TargetRotation.Quaternion();

FQuat NewQuat = FQuat::Slerp(CurrentQuat, TargetQuat, 0.1f);

NewQuat.Normalize();

SetActorRotation(NewQuat);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 회전 보간은 FQuat가 더 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;입력, 확인, 단순 설정
&amp;rarr; FRotator

계산, 누적, 보간
&amp;rarr; FQuat
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기준으로 생각하면 둘을 언제 써야 하는지 훨씬 명확해진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14. FRotator와 FQuat를 같이 쓰는 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 FRotator와 FQuat 중 하나만 고집하는 것이 아니라, 둘을 같이 쓰는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람이 입력하거나 확인할 때는 FRotator를 사용하고, 내부 계산은 FQuat로 처리하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;사람이 이해하기 쉬운 회전값 입력
    &amp;darr;
FRotator 사용
    &amp;darr;
안정적인 계산을 위해 FQuat로 변환
    &amp;darr;
회전 합성 또는 보간
    &amp;darr;
필요하면 다시 FRotator로 변환
    &amp;darr;
로그 출력 또는 SetActorRotation 적용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;FRotator InputRotation(0.0f, 90.0f, 0.0f);

FQuat CurrentQuat = GetActorQuat();

FQuat InputQuat = InputRotation.Quaternion();

FQuat NewQuat = InputQuat * CurrentQuat;

NewQuat.Normalize();

SetActorRotation(NewQuat);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅할 때는 다시 FRotator로 바꿔서 출력할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;FRotator DebugRotation = NewQuat.Rotator();

UE_LOG(LogTemp, Warning, TEXT(&quot;Debug Rotation: %s&quot;), *DebugRotation.ToString());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 계산은 안정적으로 처리하면서, 확인은 사람이 이해하기 쉬운 방식으로 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 실제 사용 방식은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;FRotator
&amp;rarr; 사람이 보기 위한 회전 표현

FQuat
&amp;rarr; 엔진이 계산하기 좋은 회전 표현
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 이해하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;15. 오늘 헷갈렸던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 헷갈렸던 부분은 FRotator와 FQuat가 둘 다 회전인데 왜 굳이 두 개가 필요한지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 FRotator 하나만 있으면 충분해 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 Pitch, Yaw, Roll로 회전값을 직접 넣을 수 있고, 에디터에서도 보기 쉽기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 계속 회전하거나 여러 축을 동시에 회전시키는 경우에는 FRotator 방식이 한계를 가진다는 것을 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 짐벌락 문제 때문에, 복잡한 회전에서는 FQuat가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 헷갈렸던 부분은 FQuat의 값이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FRotator(0, 90, 0)은 바로 이해되지만, FQuat(0, 0, 0.7071, 0.7071) 같은 값은 직접 봐도 의미를 알기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 FQuat는 직접 값을 수정하는 용도라기보다는, 회전 계산을 안정적으로 처리하기 위한 자료형으로 이해하는 것이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째로 헷갈렸던 부분은 FQuat 곱셈 순서였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 그냥 곱하면 되는 줄 알았지만, DeltaQuat * CurrentQuat와 CurrentQuat * DeltaQuat는 결과가 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 FQuat를 사용할 때는 월드 축 기준인지, 로컬 축 기준인지 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 헷갈렸던 부분은 둘 중 어떤 것을 써야 하는지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해보면 기준은 단순하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사람이 보고 입력하는 회전
&amp;rarr; FRotator

내부적으로 계산하는 회전
&amp;rarr; FQuat
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구분하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘의 핵심 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;FRotator는 Pitch, Yaw, Roll로 회전을 표현해서 사람이 이해하기 쉽다.&lt;/li&gt;
&lt;li&gt;FQuat는 쿼터니언 방식이라 사람이 읽기는 어렵지만 회전 계산에 안정적이다.&lt;/li&gt;
&lt;li&gt;단순한 회전 설정이나 디버깅에는 FRotator가 편하다.&lt;/li&gt;
&lt;li&gt;여러 축 회전, 회전 누적, 부드러운 보간에는 FQuat가 더 적합하다.&lt;/li&gt;
&lt;li&gt;FRotator는 짐벌락 문제가 생길 수 있고, FQuat는 이를 피하는 데 유리하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 FRotator는 보기 쉬운 회전 표현이고, FQuat는 계산하기 좋은 회전 표현이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;단순 설정과 디버깅
&amp;rarr; FRotator

복잡한 계산과 보간
&amp;rarr; FQuat
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 내용은 둘 중 하나만 무조건 사용하는 것이 아니라, 상황에 따라 나눠서 사용해야 한다는 것이다.&lt;/p&gt;</description>
      <author>왕건</author>
      <guid isPermaLink="true">https://talk87604.tistory.com/62</guid>
      <comments>https://talk87604.tistory.com/62#entry62comment</comments>
      <pubDate>Fri, 3 Jul 2026 21:58:40 +0900</pubDate>
    </item>
    <item>
      <title>언리얼 팀 프로젝트(멀티플레이) - 제시어 도형으로만들고 맞추기(1)</title>
      <link>https://talk87604.tistory.com/61</link>
      <description>&lt;h1&gt;Unreal 테스트 배치 시스템 구현 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Unreal 프로젝트에서 테스트용 캐릭터 입력과 도형 배치 시스템의 기본 흐름을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 플레이어가 도형을 선택하고, 화면 앞에 프리뷰를 띄운 뒤, 원하는 위치와 회전값으로 서버에 설치 요청을 보내는 구조를 만드는 것이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 테스트 캐릭터 입력 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 BP_TestCharacter가 사용할 테스트 Pawn 기반 C++ 클래스를 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가한 기본 구성은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CapsuleComponent&lt;/li&gt;
&lt;li&gt;MeshComponent&lt;/li&gt;
&lt;li&gt;SpringArmComponent&lt;/li&gt;
&lt;li&gt;CameraComponent&lt;/li&gt;
&lt;li&gt;FloatingPawnMovement&lt;/li&gt;
&lt;li&gt;Enhanced Input용 MappingContext&lt;/li&gt;
&lt;li&gt;MoveAction&lt;/li&gt;
&lt;li&gt;MoveUpAction&lt;/li&gt;
&lt;li&gt;LookAction&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동 입력은 클라이언트에서 바로 반응하도록 처리했고, 동시에 서버에도 RPC로 이동 요청을 보내도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서는 위치를 직접 갱신하고 SetReplicateMovement(true)를 통해 다른 클라이언트에도 위치가 보이도록 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Shape DataTable Row 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도형 정보를 DataTable로 관리하기 위해 FShapeDefinitionRow를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;USTRUCT(BlueprintType)
struct TEAMPROJECT_API FShapeDefinitionRow : public FTableRowBase
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;Shape&quot;)
	TObjectPtr&amp;lt;UStaticMesh&amp;gt; Mesh;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;Shape&quot;)
	TObjectPtr&amp;lt;UMaterialInterface&amp;gt; PreviewMaterial;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;Shape&quot;)
	TObjectPtr&amp;lt;UMaterialInterface&amp;gt; PlacedMaterial;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;Shape&quot;)
	FVector DefaultScale = FVector::OneVector;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataTable의 RowName은 Box, Sphere처럼 도형 ID로 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Row에는 Mesh, PreviewMaterial, PlacedMaterial, DefaultScale을 등록했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해두면 코드에서는 RowName만으로 어떤 도형을 설치할지 결정할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Placement Preview Actor 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 전 미리보기를 담당하는 APlacementPreviewActor를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Actor는 클라이언트에서만 보이는 프리뷰용 Actor다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SetPreviewShape() 함수에서는 DataTable Row를 찾아 Static Mesh, Preview Material, Default Scale을 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리뷰 Actor는 서버에 복제하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리뷰는 설치 전에 현재 플레이어만 보는 임시 표시이기 때문에 서버에 올릴 필요가 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실제 설치 Actor 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 설치되는 도형은 APlacedShapeActor로 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Actor는 서버에서 Spawn되고 다른 클라이언트에도 보여야 하므로 복제를 켰다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;bReplicates = true;
SetReplicateMovement(true);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Mesh, Material, Scale이 다른 클라이언트에서도 제대로 적용되도록 복제 필드를 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 도형을 Spawn한 뒤 DataTable Row를 읽어 Mesh와 Material을 적용하고, 그 값이 클라이언트로 복제되도록 구성했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. PlayerController에서 배치 흐름 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ATestPlayerController에 도형 배치 흐름을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 입력은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1: Box 프리뷰 시작&lt;/li&gt;
&lt;li&gt;2: Sphere 프리뷰 시작&lt;/li&gt;
&lt;li&gt;마우스 휠 위: 프리뷰를 시점 방향으로 더 멀리 이동&lt;/li&gt;
&lt;li&gt;마우스 휠 아래: 프리뷰를 플레이어 쪽으로 이동&lt;/li&gt;
&lt;li&gt;Q: 누르고 있는 동안 반시계 방향 회전&lt;/li&gt;
&lt;li&gt;E: 누르고 있는 동안 시계 방향 회전&lt;/li&gt;
&lt;li&gt;좌클릭: 현재 프리뷰 위치와 회전으로 서버에 설치 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리뷰 위치는 Tick()에서 계속 갱신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어 시점에서 앞으로 LineTrace를 쏘고, 맞은 지점이 있으면 그 위치에 프리뷰를 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞은 지점이 없으면 현재 설정된 PreviewDistance만큼 앞에 프리뷰를 둔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 프리뷰 거리 조절&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마우스 휠로 프리뷰 거리를 조절할 수 있게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;PreviewDistance = 600.0f;
MinPreviewDistance = 150.0f;
MaxPreviewDistance = 2000.0f;
PreviewDistanceStep = 100.0f;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마우스 휠을 올리면 PreviewDistance가 증가해서 프리뷰가 더 멀어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마우스 휠을 내리면 PreviewDistance가 감소해서 프리뷰가 플레이어 쪽으로 가까워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치할 때는 현재 프리뷰 Actor의 Transform을 그대로 서버에 보내기 때문에, 조절한 거리 그대로 설치된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 프리뷰 회전 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Q/E를 누를 때마다 한 번씩 회전하도록 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사용성이 좋지 않아서, 키를 누르고 있는 동안 계속 회전하도록 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 PreviewYaw 값을 PlayerController가 가지고 있고, Q/E 입력 상태를 bool로 저장한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;bool bIsRotatingCounterClockwise;
bool bIsRotatingClockwise;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Tick()에서 DeltaTime 기준으로 회전값을 계속 누적한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;PreviewYaw = FRotator::NormalizeAxis(
	PreviewYaw + (RotationDirection * PreviewRotationStep * DeltaTime)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 프레임레이트에 크게 의존하지 않고 부드럽게 회전할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 회전 속도는 초당 90도다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;PreviewRotationStep = 90.0f;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 서버 Spawn 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌클릭하면 클라이언트는 현재 프리뷰의 Transform을 서버 RPC로 보낸다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;Server_RequestSpawnShape(CurrentPreviewShapeId, PreviewActor-&amp;gt;GetActorTransform());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서는 다시 DataTable Row를 찾아 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증이 끝나면 실제 설치 Actor를 Spawn한다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;APlacedShapeActor* PlacedShape = World-&amp;gt;SpawnActor&amp;lt;APlacedShapeActor&amp;gt;(
	PlacedShapeActorClass,
	SpawnTransform,
	SpawnParameters
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spawn 이후에는 SetPlacedShape()를 호출해서 Mesh, Material, Scale을 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 클라이언트가 프리뷰로 본 위치와 회전 그대로 서버에 설치되고, 다른 클라이언트에도 복제된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 전체 흐름 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 배치 시스템 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;1 또는 2 키 입력&lt;/li&gt;
&lt;li&gt;ShapeId 선택&lt;/li&gt;
&lt;li&gt;DataTable에서 Mesh / Material / Scale 조회&lt;/li&gt;
&lt;li&gt;PreviewActor 생성&lt;/li&gt;
&lt;li&gt;PreviewActor에 Mesh / PreviewMaterial / Scale 적용&lt;/li&gt;
&lt;li&gt;Tick()에서 프리뷰 위치 갱신&lt;/li&gt;
&lt;li&gt;마우스 휠로 프리뷰 거리 조절&lt;/li&gt;
&lt;li&gt;Q/E로 프리뷰 회전 조절&lt;/li&gt;
&lt;li&gt;좌클릭&lt;/li&gt;
&lt;li&gt;PreviewActor의 Transform을 서버 RPC로 전송&lt;/li&gt;
&lt;li&gt;서버에서 DataTable Row 재검증&lt;/li&gt;
&lt;li&gt;실제 Shape Actor Spawn&lt;/li&gt;
&lt;li&gt;Mesh / Material / Scale 적용&lt;/li&gt;
&lt;li&gt;다른 클라이언트에 복제&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 오늘 구현하면서 정리한 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리뷰는 클라이언트에서만 보여도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 설치 결과는 반드시 서버에서 처리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 직접 Actor를 Spawn하면 자기 화면에서는 보일 수 있지만, 서버나 다른 클라이언트 기준에서는 제대로 반영되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최종 설치는 서버 RPC를 통해 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 설치 위치와 회전은 클라이언트가 보고 있는 프리뷰 Transform을 서버로 보내는 방식으로 처리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 서버에서는 DataTable Row를 다시 확인해서 유효한 ShapeId인지 검증한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 다음에 할 일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 프리뷰 단계에서 색상 팔레트를 선택할 수 있게 만들 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상 구조는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;도형 선택&lt;/li&gt;
&lt;li&gt;프리뷰 생성&lt;/li&gt;
&lt;li&gt;팔레트에서 색상 선택&lt;/li&gt;
&lt;li&gt;프리뷰 색상 변경&lt;/li&gt;
&lt;li&gt;좌클릭 설치&lt;/li&gt;
&lt;li&gt;서버 RPC로 ShapeId / Transform / SelectedColor 전송&lt;/li&gt;
&lt;li&gt;서버에서 검증 후 실제 Actor Spawn&lt;/li&gt;
&lt;li&gt;선택한 색상 적용&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리뷰 색상 변경은 클라이언트에서만 처리해도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 최종 설치 색상은 게임 결과에 영향을 주기 때문에 서버로 보내야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 남은 고민&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 테스트 캐릭터는 APawn + UFloatingPawnMovement 기반으로 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 단계에서는 충분하지만, 실제 플레이어 이동까지 생각하면 나중에는 ACharacter + CharacterMovementComponent 기반으로 정리하는 것이 더 안정적일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 멀티플레이 환경에서는 CharacterMovementComponent가 네트워크 예측과 보정을 기본적으로 지원하기 때문에 장기적으로는 더 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업은 배치 시스템의 기본 흐름을 빠르게 검증하기 위한 테스트 구현이라고 보면 된다.&lt;/p&gt;</description>
      <category>unreal 8기</category>
      <author>왕건</author>
      <guid isPermaLink="true">https://talk87604.tistory.com/61</guid>
      <comments>https://talk87604.tistory.com/61#entry61comment</comments>
      <pubDate>Tue, 30 Jun 2026 21:03:29 +0900</pubDate>
    </item>
    <item>
      <title>2026-6-23 TIL</title>
      <link>https://talk87604.tistory.com/60</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIL: Unreal Engine 모듈 간 상호작용과 독립 플러그인 생성 및 검증 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 배운 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Unreal Engine에서 &lt;b&gt;서로 다른 모듈이 어떻게 상호작용하는지&lt;/b&gt;, 그리고 프로젝트 안에 &lt;b&gt;독립 플러그인(Temporary)&lt;/b&gt; 을 직접 구성하고 프로젝트에 연결하는 과정을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Primary 모듈, Test 모듈, Temporary 플러그인이라는 표현이 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 정리해보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;ModuleAndPlugin 모듈
&amp;rarr; 현재 프로젝트의 주 게임 모듈

Test 모듈
&amp;rarr; 별도로 만든 실습용 Runtime 모듈

Temporary 플러그인
&amp;rarr; 프로젝트의 Plugins 폴더 아래에 만든 독립 플러그인
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습의 핵심은 단순히 클래스를 만드는 것이 아니라, &lt;b&gt;모듈과 플러그인이 서로 어떻게 분리되고 연결되는지&lt;/b&gt;를 이해하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 TestActor를 Test 모듈에 만들고, 주 게임 모듈인 ModuleAndPlugin에서 include한 뒤 SpawnActor로 생성하면서 &lt;b&gt;모듈 간 참조 구조&lt;/b&gt;를 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 Temporary 플러그인을 직접 만들면서 .uplugin, Build.cs, StartupModule(), ShutdownModule(), .uproject 등록 과정까지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;모듈 간 상호작용
&amp;rarr; 한 모듈에서 만든 클래스를 다른 모듈에서 include하고 사용하는 구조

플러그인
&amp;rarr; 프로젝트에 붙였다 뗄 수 있는 독립적인 기능 패키지

.uplugin
&amp;rarr; 플러그인의 설명서

Build.cs
&amp;rarr; 모듈의 빌드 의존성 설정 파일

StartupModule()
&amp;rarr; 플러그인 모듈이 로드될 때 실행

ShutdownModule()
&amp;rarr; 플러그인 모듈이 언로드될 때 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 모듈 간 상호작용 구현의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과제에서 나온 내용은 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;TestActor 생성
&amp;rarr; Test 모듈에 속한 C++ 클래스 Actor 부모를 생성하고 BeginPlay에서 로그 출력

Primary 모듈에서 참조
&amp;rarr; 주 게임 모듈의 캐릭터 클래스에서 TestActor.h를 include하고 SpawnActor 로직 작성
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Primary 모듈이라는 말 때문에 Primary라는 이름의 모듈을 새로 만들어야 하는 것처럼 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 말하는 Primary 모듈은 새 모듈 이름이 아니라, &lt;b&gt;현재 프로젝트의 주 게임 모듈&lt;/b&gt;을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트 기준으로는 다음과 같이 이해하면 된다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;Primary 모듈 역할
&amp;rarr; ModuleAndPlugin 모듈

TestActor를 만드는 위치
&amp;rarr; Test 모듈

TestActor를 참조하고 생성하는 위치
&amp;rarr; ModuleAndPlugin 모듈
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 새로 Primary라는 모듈을 만드는 것이 아니라, 기존 주 게임 모듈인 ModuleAndPlugin에서 Test 모듈을 참조하는 구조였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. TestActor를 Test 모듈에 만들어야 하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과제 문장에는 Test 모듈에 속한 C++ 클래스라고 되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 TestActor는 주 게임 모듈인 ModuleAndPlugin이 아니라, Test 모듈에 생성되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적인 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Source
├─ ModuleAndPlugin
│  ├─ ModuleAndPlugin.Build.cs
│  ├─ Public
│  └─ Private
│
└─ Test
   ├─ Test.Build.cs
   ├─ Public
   │  └─ TestActor.h
   └─ Private
      └─ TestActor.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 TestActor.h를 Test/Public 폴더에 두는 것이다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;다른 모듈에서 include해야 하는 헤더
&amp;rarr; Public 폴더

해당 모듈 내부에서만 사용하는 구현 파일
&amp;rarr; Private 폴더
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ModuleAndPlugin 모듈에서 TestActor.h를 include해야 하므로, TestActor.h는 반드시 Test 모듈의 Public 폴더에 있어야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. C++ 클래스 생성 화면에서 주의할 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 C++ 클래스를 만들 때 Class Type을 Public으로 선택한 것은 맞았다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;Class Type: Public
&amp;rarr; 맞음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 모듈 선택이 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 화면에서는 모듈이 다음처럼 되어 있었다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;ModuleAndPlugin (Runtime)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태로 만들면 생성 경로가 다음처럼 잡힌다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Source/ModuleAndPlugin/Public/TestActor.h
Source/ModuleAndPlugin/Private/TestActor.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 TestActor는 Test 모듈이 아니라 ModuleAndPlugin 모듈에 속하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과제 의도는 TestActor를 Test 모듈에 만드는 것이므로, 모듈 선택을 다음처럼 바꿔야 했다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Module: Test (Runtime)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 경로가 다음처럼 바뀌는 것이 정상이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Source/Test/Public/TestActor.h
Source/Test/Private/TestActor.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. API 매크로의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈이 달라지면 클래스 앞에 붙는 API 매크로도 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 TestActor를 ModuleAndPlugin 모듈에 만들면 클래스 선언이 다음처럼 생성될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class MODULEANDPLUGIN_API ATestActor : public AActor
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Test 모듈에 제대로 만들면 다음처럼 되어야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class TEST_API ATestActor : public AActor
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 TEST_API는 Test 모듈 밖에서도 이 클래스를 사용할 수 있도록 내보내는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 ModuleAndPlugin 모듈에서 ATestActor를 사용하려면 ATestActor 클래스에 TEST_API가 붙어 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;Test 모듈 소속 클래스
&amp;rarr; TEST_API

ModuleAndPlugin 모듈 소속 클래스
&amp;rarr; MODULEANDPLUGIN_API
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. TestActor.h 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TestActor.h는 다음과 같이 구성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#pragma once

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

UCLASS()
class TEST_API ATestActor : public AActor
{
	GENERATED_BODY()
	
public:	
	ATestActor();

protected:
	virtual void BeginPlay() override;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 부분은 다음이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class TEST_API ATestActor : public AActor
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스가 Test 모듈 소속이고, 다른 모듈에서도 사용할 수 있게 공개된다는 의미다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. TestActor.cpp 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TestActor.cpp에서는 BeginPlay()에 로그와 화면 메시지를 넣어 생성 여부를 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;#include &quot;TestActor.h&quot;
#include &quot;Engine/Engine.h&quot;

ATestActor::ATestActor()
{
	PrimaryActorTick.bCanEverTick = false;
}

void ATestActor::BeginPlay()
{
	Super::BeginPlay();

	UE_LOG(LogTemp, Warning, TEXT(&quot;ATestActor BeginPlay&quot;));

	if (GEngine)
	{
		GEngine-&amp;gt;AddOnScreenDebugMessage(
			-1,
			5.0f,
			FColor::Green,
			TEXT(&quot;ATestActor BeginPlay&quot;)
		);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 ATestActor가 실제로 월드에 생성되고 BeginPlay()가 호출되면 로그와 화면 메시지를 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이 Actor가 정상적으로 생성되었는지 확인하는 테스트용 코드다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. ModuleAndPlugin 모듈에서 Test 모듈 의존성 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ModuleAndPlugin 모듈에서 TestActor.h를 include하려면, 먼저 ModuleAndPlugin.Build.cs에 Test 모듈 의존성을 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 위치는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Source/ModuleAndPlugin/ModuleAndPlugin.Build.cs
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 의존성 목록에 &quot;Test&quot;를 추가한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;PublicDependencyModuleNames.AddRange(new string[]
{
	&quot;Core&quot;,
	&quot;CoreUObject&quot;,
	&quot;Engine&quot;,
	&quot;InputCore&quot;,
	&quot;Test&quot;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음과 같은 의미다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;ModuleAndPlugin 모듈은 Test 모듈의 Public 헤더와 기능을 사용하겠다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 다른 모듈의 클래스를 include하기 위해서는 단순히 헤더 파일만 있는 것이 아니라, Build.cs에서 모듈 의존성을 연결해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 주 게임 모듈의 캐릭터에서 TestActor include&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 ModuleAndPlugin 모듈의 캐릭터 cpp 파일에서 TestActor.h를 include할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 파일에서 작업할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Source/ModuleAndPlugin/Private/ModuleAndPluginCharacter.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상단에 include를 추가한다.&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;#include &quot;TestActor.h&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이 include에서 오류가 난다면 보통 다음 원인을 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;1. TestActor.h가 Source/Test/Public에 있는지
2. ModuleAndPlugin.Build.cs에 &quot;Test&quot;를 추가했는지
3. 프로젝트 파일을 다시 생성하고 빌드했는지
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. Character BeginPlay에서 SpawnActor 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐릭터의 BeginPlay()에서 ATestActor를 생성한다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;void AModuleAndPluginCharacter::BeginPlay()
{
	Super::BeginPlay();

	if (UWorld* World = GetWorld())
	{
		FVector SpawnLocation = GetActorLocation() + FVector(200.0f, 0.0f, 100.0f);
		FRotator SpawnRotation = FRotator::ZeroRotator;

		ATestActor* SpawnedActor = World-&amp;gt;SpawnActor&amp;lt;ATestActor&amp;gt;(
			ATestActor::StaticClass(),
			SpawnLocation,
			SpawnRotation
		);

		if (SpawnedActor)
		{
			UE_LOG(LogTemp, Warning, TEXT(&quot;Spawned ATestActor Successfully&quot;));

			if (GEngine)
			{
				GEngine-&amp;gt;AddOnScreenDebugMessage(
					-1,
					5.0f,
					FColor::Yellow,
					TEXT(&quot;Spawned ATestActor from ModuleAndPlugin&quot;)
				);
			}
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 주 게임 모듈의 캐릭터가 시작될 때, Test 모듈에 있는 ATestActor를 월드에 생성하는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;게임 시작
    &amp;darr;
ModuleAndPlugin 모듈의 Character BeginPlay 실행
    &amp;darr;
TestActor.h include
    &amp;darr;
SpawnActor&amp;lt;ATestActor&amp;gt;() 실행
    &amp;darr;
Test 모듈의 ATestActor 생성
    &amp;darr;
ATestActor::BeginPlay() 실행
    &amp;darr;
로그와 화면 메시지 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 모듈 간 상호작용 실습의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습은 단순히 Actor 하나를 Spawn하는 것이 목적이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 목적은 다음 구조를 확인하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;Test 모듈
&amp;rarr; ATestActor 클래스를 제공

ModuleAndPlugin 모듈
&amp;rarr; Test 모듈을 의존성에 추가
&amp;rarr; TestActor.h include
&amp;rarr; ATestActor를 SpawnActor로 생성
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 한 모듈에서 만든 클래스를 다른 모듈에서 사용하려면 다음 조건이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 사용될 클래스의 헤더가 Public 폴더에 있어야 한다.
2. 클래스에 올바른 API 매크로가 붙어 있어야 한다.
3. 사용하는 쪽 모듈의 Build.cs에 대상 모듈 의존성이 추가되어야 한다.
4. cpp에서 헤더를 include해야 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조건들이 맞아야 모듈 간 상호작용이 정상적으로 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11. 독립 플러그인 Temporary 생성의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에서는 Temporary라는 독립 플러그인을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 플러그인은 프로젝트의 Source 폴더 안에 만드는 것이 아니라, 프로젝트 루트의 Plugins 폴더 아래에 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ModuleAndPlugin
├─ Config
├─ Content
├─ Source
├─ Plugins
│  └─ Temporary
│     ├─ Temporary.uplugin
│     ├─ Content
│     └─ Source
│        └─ Temporary
│           ├─ Temporary.Build.cs
│           ├─ Public
│           │  └─ Temporary.h
│           └─ Private
│              └─ Temporary.cpp
└─ ModuleAndPlugin.uproject
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 Plugins/Temporary 위치다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;올바른 위치
&amp;rarr; 프로젝트루트/Plugins/Temporary

잘못된 위치
&amp;rarr; Source/ModuleAndPlugin/Plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 프로젝트의 주 소스코드 안에 넣는 것이 아니라, 프로젝트 루트의 Plugins 폴더 안에 독립적으로 둔다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12. Temporary.uplugin 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Temporary.uplugin 파일은 플러그인의 설명서 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Plugins/Temporary/Temporary.uplugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
	&quot;FileVersion&quot;: 3,
	&quot;Version&quot;: 1,
	&quot;VersionName&quot;: &quot;1.0&quot;,
	&quot;FriendlyName&quot;: &quot;Temporary&quot;,
	&quot;Description&quot;: &quot;Temporary plugin for module practice.&quot;,
	&quot;Category&quot;: &quot;Practice&quot;,
	&quot;CreatedBy&quot;: &quot;Me&quot;,
	&quot;CanContainContent&quot;: true,
	&quot;IsBetaVersion&quot;: false,
	&quot;Installed&quot;: false,
	&quot;Modules&quot;: [
		{
			&quot;Name&quot;: &quot;Temporary&quot;,
			&quot;Type&quot;: &quot;Runtime&quot;,
			&quot;LoadingPhase&quot;: &quot;Default&quot;
		}
	]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 부분은 다음이다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;&quot;CanContainContent&quot;: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 이 플러그인이 Content 폴더를 가질 수 있다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 중요한 부분은 Modules 항목이다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;&quot;Modules&quot;: [
	{
		&quot;Name&quot;: &quot;Temporary&quot;,
		&quot;Type&quot;: &quot;Runtime&quot;,
		&quot;LoadingPhase&quot;: &quot;Default&quot;
	}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 다음과 같은 의미다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;이 플러그인 안에는 Temporary라는 Runtime 모듈이 있다.
기본 로딩 단계에서 이 모듈을 로드한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13. Temporary.Build.cs 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Temporary.Build.cs는 플러그인 내부의 Temporary 모듈이 어떤 Unreal 모듈을 사용할지 설정하는 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;Plugins/Temporary/Source/Temporary/Temporary.Build.cs
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;using UnrealBuildTool;

public class Temporary : ModuleRules
{
	public Temporary(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[]
		{
			&quot;Core&quot;,
			&quot;CoreUObject&quot;,
			&quot;Engine&quot;
		});

		PrivateDependencyModuleNames.AddRange(new string[]
		{
		});
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음과 같은 의미다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Temporary 모듈은 Core, CoreUObject, Engine 기능을 사용한다.
빌드할 때 이 모듈들을 참조해야 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Build.cs는 모듈의 빌드 의존성을 관리하는 파일이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14. Temporary.h 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Temporary.h에서는 IModuleInterface를 상속받은 모듈 클래스를 선언한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Plugins/Temporary/Source/Temporary/Public/Temporary.h
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#pragma once

#include &quot;CoreMinimal.h&quot;
#include &quot;Modules/ModuleManager.h&quot;

class FTemporaryModule : public IModuleInterface
{
public:
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 IModuleInterface는 Unreal 모듈이 로드되고 언로드될 때 호출되는 기본 인터페이스다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;StartupModule()
&amp;rarr; 모듈이 로드될 때 호출

ShutdownModule()
&amp;rarr; 모듈이 언로드될 때 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;15. Temporary.cpp 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Temporary.cpp에서는 StartupModule()과 ShutdownModule()을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Plugins/Temporary/Source/Temporary/Private/Temporary.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &quot;Temporary.h&quot;

#define LOCTEXT_NAMESPACE &quot;FTemporaryModule&quot;

void FTemporaryModule::StartupModule()
{
	UE_LOG(LogTemp, Warning, TEXT(&quot;Temporary Plugin Module Startup&quot;));
}

void FTemporaryModule::ShutdownModule()
{
	UE_LOG(LogTemp, Warning, TEXT(&quot;Temporary Plugin Module Shutdown&quot;));
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FTemporaryModule, Temporary)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 부분은 다음이다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;IMPLEMENT_MODULE(FTemporaryModule, Temporary)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 Unreal에게 다음과 같이 알려주는 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Temporary라는 모듈은 FTemporaryModule 클래스로 구현되어 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이 코드가 없으면 Unreal이 모듈을 정상적으로 등록하지 못한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;16. StartupModule과 ShutdownModule의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StartupModule()은 플러그인 모듈이 로드될 때 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 플러그인이 켜질 때 로그를 출력할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;void FTemporaryModule::StartupModule()
{
	UE_LOG(LogTemp, Warning, TEXT(&quot;Temporary Plugin Module Startup&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에는 이 함수 안에서 다음과 같은 일을 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;에디터 메뉴 등록
커스텀 툴 등록
외부 라이브러리 초기화
데이터 매니저 초기화
공통 시스템 초기화
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 ShutdownModule()은 모듈이 언로드될 때 실행된다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;void FTemporaryModule::ShutdownModule()
{
	UE_LOG(LogTemp, Warning, TEXT(&quot;Temporary Plugin Module Shutdown&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수에서는 다음과 같은 정리 작업을 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;등록한 메뉴 제거
사용하던 리소스 해제
외부 라이브러리 종료
생성한 객체 정리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 StartupModule()과 ShutdownModule()은 플러그인 모듈의 시작과 종료 시점을 관리하는 함수다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;17. .uproject에 Temporary 플러그인 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인을 만들었다고 해서 프로젝트가 자동으로 사용하는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 사용하려면 .uproject 파일의 Plugins 항목에 등록해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 ModuleAndPlugin.uproject 파일에 다음처럼 추가한다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;&quot;Plugins&quot;: [
	{
		&quot;Name&quot;: &quot;Temporary&quot;,
		&quot;Enabled&quot;: true
	}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음과 같은 의미다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;이 프로젝트에서 Temporary 플러그인을 사용하겠다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 다른 플러그인이 등록되어 있다면, 같은 객체 안에 이름을 여러 개 넣는 것이 아니라 플러그인마다 객체를 따로 추가해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;18. .uproject Plugins 항목 작성 시 실수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 다음처럼 잘못 작성했다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;&quot;Plugins&quot;: [
	{
		&quot;Name&quot;: &quot;ModelingToolsEditorMode&quot;, &quot;Temporary&quot;,
		&quot;Enabled&quot;: true,
		&quot;TargetAllowList&quot;: [
			&quot;Editor&quot;
		]
	}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 잘못된 JSON 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Name 하나에는 플러그인 이름 하나만 들어가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바른 방식은 플러그인 하나당 객체 하나를 따로 만드는 것이다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;&quot;Plugins&quot;: [
	{
		&quot;Name&quot;: &quot;ModelingToolsEditorMode&quot;,
		&quot;Enabled&quot;: true,
		&quot;TargetAllowList&quot;: [
			&quot;Editor&quot;
		]
	},
	{
		&quot;Name&quot;: &quot;Temporary&quot;,
		&quot;Enabled&quot;: true
	}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Plugins 배열
├─ ModelingToolsEditorMode 플러그인 객체
└─ Temporary 플러그인 객체
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON에서는 항목 사이 쉼표와 마지막 항목 뒤 쉼표 여부도 중요하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;항목 사이
&amp;rarr; 쉼표 필요

마지막 항목 뒤
&amp;rarr; 쉼표 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;19. Temporary 플러그인 이름 일치 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Temporary 플러그인이 정상 인식되려면 관련 이름들이 모두 일치해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해야 할 항목은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;.uproject 등록 이름
&amp;rarr; Temporary

플러그인 폴더 이름
&amp;rarr; Plugins/Temporary

플러그인 파일 이름
&amp;rarr; Temporary.uplugin

Source 내부 모듈 폴더
&amp;rarr; Source/Temporary

Build.cs 파일 이름
&amp;rarr; Temporary.Build.cs

Build.cs 클래스 이름
&amp;rarr; public class Temporary : ModuleRules

IMPLEMENT_MODULE 이름
&amp;rarr; IMPLEMENT_MODULE(FTemporaryModule, Temporary)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 하나라도 이름이 다르면 Unreal이 플러그인이나 모듈을 제대로 찾지 못할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;20. 플러그인 활성화 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.uproject에 등록한 뒤 에디터에서 Edit &amp;rarr; Plugins를 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색창에 tempo를 입력했을 때 Temporary 플러그인이 보였고, 체크되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 다음을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Temporary 플러그인 등록 성공
Temporary 플러그인 활성화 성공
Unreal Editor가 플러그인을 인식함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이 단계에서 플러그인 자체는 정상적으로 프로젝트에 연결된 상태였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;21. 콘텐츠 브라우저에서 플러그인 폴더가 안 보인 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 정상적으로 활성화되어 있었지만, 처음에는 콘텐츠 브라우저에서 플러그인 폴더가 보이지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 보통 다음 중 하나다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;1. Content Browser에서 Show Plugin Content가 꺼져 있음
2. Temporary.uplugin에서 CanContainContent가 true가 아님
3. Plugins/Temporary/Content 폴더가 비어 있음
4. C++ Source 폴더를 콘텐츠 브라우저에서 보려고 함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘텐츠 브라우저는 기본적으로 에셋을 보여주는 곳이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음 파일들은 콘텐츠 브라우저에 그대로 보이는 대상이 아니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Temporary.h
Temporary.cpp
Temporary.Build.cs
Temporary.uplugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘텐츠 브라우저에 보이는 것은 주로 플러그인의 Content 폴더 안에 있는 에셋이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Blueprint
Material
Texture
DataAsset
Widget Blueprint
Sound
Animation
Map
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;22. Show Plugin Content 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인 콘텐츠를 보려면 콘텐츠 브라우저에서 설정을 켜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Content Browser
    &amp;darr;
Settings 또는 톱니바퀴
    &amp;darr;
Show Plugin Content 체크
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정을 켜야 플러그인의 Content 폴더가 콘텐츠 브라우저에 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 보이면 다음과 같은 형태로 나타날 수 있다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Plugins
└─ Temporary Content
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 버전에 따라 Temporary Content가 별도로 보일 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;23. CanContainContent의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Temporary.uplugin에는 다음 옵션이 있어야 한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;&quot;CanContainContent&quot;: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션은 다음과 같은 의미다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;이 플러그인은 Content 폴더를 가질 수 있다.
이 플러그인 안에 에셋을 포함할 수 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이 값이 false이거나 없으면, 플러그인의 콘텐츠 폴더가 콘텐츠 브라우저에 표시되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 콘텐츠를 포함하는 플러그인이라면 CanContainContent를 true로 설정하는 것이 중요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;24. Source 폴더가 콘텐츠 브라우저에 안 보이는 것은 정상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인 안에는 다음과 같은 C++ 소스 파일이 있었다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Plugins/Temporary/Source/Temporary/Public/Temporary.h
Plugins/Temporary/Source/Temporary/Private/Temporary.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 파일들은 콘텐츠 브라우저에서 보이는 대상이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 콘텐츠 브라우저는 C++ 소스 폴더를 보여주는 곳이 아니라, Unreal 에셋을 보여주는 곳이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 FTemporaryModule은 UCLASS 기반 클래스가 아니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class FTemporaryModule : public IModuleInterface
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스는 Actor나 UObject가 아니기 때문에 콘텐츠 브라우저의 C++ Classes에서도 일반 Actor 클래스처럼 보이지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 플러그인 모듈 클래스는 콘텐츠 브라우저가 아니라 빌드와 로그로 확인하는 것이 핵심이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;25. 플러그인 검증 완료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 다음 상태를 확인했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Edit &amp;rarr; Plugins에서 Temporary 플러그인이 보임
Temporary 플러그인이 체크되어 있음
Content Browser에서 Plugin Content 표시 해결
Temporary 플러그인 콘텐츠 영역 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Temporary 플러그인 연결은 정상적으로 완료된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단계에서 성공한 전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Temporary 플러그인 폴더 생성
    &amp;darr;
Temporary.uplugin 작성
    &amp;darr;
Temporary 모듈 Source 구성
    &amp;darr;
StartupModule / ShutdownModule 구현
    &amp;darr;
.uproject에 Temporary 플러그인 등록
    &amp;darr;
Edit &amp;rarr; Plugins에서 Temporary 활성화 확인
    &amp;darr;
Content Browser에서 Plugin Content 표시 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;26. 우리가 만든 플러그인의 정체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 만든 Temporary 플러그인은 아직 실제 게임 기능은 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 다음 정도만 한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;플러그인이 로드될 때 로그 출력
플러그인이 언로드될 때 로그 출력
Content 폴더를 가질 수 있음
프로젝트에서 활성화 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 완성된 기능 플러그인이 아니라, 플러그인이 Unreal 프로젝트에 어떻게 등록되고 로드되는지 확인하는 기본 골격이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다르게 표현하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;완성된 인벤토리 시스템을 만든 것이 아님
&amp;rarr; 인벤토리 시스템을 넣을 수 있는 플러그인 구조를 만든 것

완성된 퀘스트 시스템을 만든 것이 아님
&amp;rarr; 퀘스트 시스템을 독립 기능으로 만들 수 있는 기반을 배운 것
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;27. 플러그인을 배운 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Temporary 플러그인이 실제 기능이 없어 보여서 어디에 써먹는지 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 플러그인을 배우는 이유는 &lt;b&gt;기능을 독립적인 부품처럼 분리하기 위해서&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 게임 프로젝트에 모든 기능을 한 모듈 안에 넣으면 나중에 구조가 복잡해진다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Source/ModuleAndPlugin
├─ Inventory
├─ Quest
├─ Dialogue
├─ Minimap
├─ Save
├─ UI
└─ Combat
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 프로젝트에서는 괜찮을 수 있지만, 프로젝트가 커질수록 관리가 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 재사용 가능하거나 독립성이 높은 기능은 플러그인으로 분리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Plugins
├─ InventoryPlugin
├─ QuestPlugin
├─ DialoguePlugin
├─ MinimapPlugin
└─ SaveSystemPlugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 기능별로 관리하기 쉽고, 다른 프로젝트로 옮겨 쓰기도 편하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;28. 플러그인을 실제로 써먹는 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 다음과 같은 기능을 만들 때 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;인벤토리 시스템
퀘스트 시스템
대화 시스템
미니맵 시스템
세이브 시스템
공통 UI 시스템
네트워크 유틸
상호작용 시스템
에디터 자동화 도구
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인벤토리 시스템을 플러그인으로 만들면 다음과 같은 구조가 될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;Plugins
└─ InventoryPlugin
   ├─ InventoryPlugin.uplugin
   ├─ Source
   │  └─ InventoryPlugin
   │     ├─ Public
   │     └─ Private
   └─ Content
      ├─ BP_InventoryWidget
      ├─ DA_ItemData
      ├─ T_ItemIcon_Sword
      └─ T_ItemIcon_Potion
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만든 플러그인은 다른 프로젝트에서도 재사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;RPG 프로젝트
&amp;rarr; InventoryPlugin 사용

생존게임 프로젝트
&amp;rarr; InventoryPlugin 복사해서 사용

멀티플레이 프로젝트
&amp;rarr; InventoryPlugin 수정해서 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 플러그인의 가장 큰 장점은 재사용성과 독립성이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;29. 에디터 도구 플러그인 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 게임 실행 중 기능만 만드는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unreal Editor에서 사용하는 도구도 플러그인으로 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;아이템 스폰 위치 자동 생성
데이터 테이블 자동 검사
잘못된 에셋 이름 검사
맵에 몬스터 자동 배치
퀘스트 데이터 자동 생성
커스텀 에디터 창 만들기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 기능은 게임 실행 중에는 필요 없지만, 개발할 때 반복 작업을 줄여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이런 기능은 Editor Plugin으로 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;MapToolPlugin
DataValidationPlugin
QuestEditorPlugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 프로젝트에서는 이런 에디터 플러그인을 만들어 작업 효율을 높일 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;30. 이번 실습에서 진짜 배운 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서 배운 것은 단순히 Temporary라는 이름의 플러그인을 만든 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 배운 내용은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 프로젝트는 여러 모듈로 구성될 수 있다.
2. 한 모듈에서 만든 클래스를 다른 모듈에서 사용할 수 있다.
3. 모듈 간 참조를 위해서는 Build.cs 의존성 설정이 필요하다.
4. 다른 모듈에서 사용할 헤더는 Public 폴더에 있어야 한다.
5. 다른 모듈에서 사용할 클래스에는 올바른 API 매크로가 필요하다.
6. 플러그인은 프로젝트에 붙였다 뗄 수 있는 독립 기능 패키지다.
7. .uplugin은 플러그인의 설명서 역할을 한다.
8. 플러그인도 내부에 Source와 Content를 가질 수 있다.
9. 플러그인 모듈도 Build.cs를 통해 의존성을 관리한다.
10. StartupModule과 ShutdownModule로 플러그인 모듈의 시작과 종료를 처리할 수 있다.
11. .uproject에 등록해야 프로젝트에서 플러그인을 활성화할 수 있다.
12. 콘텐츠 브라우저에서 플러그인 콘텐츠를 보려면 Show Plugin Content를 켜야 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이번 실습은 Unreal 프로젝트를 단순히 하나의 코드 덩어리로 보는 것이 아니라, &lt;b&gt;모듈과 플러그인 단위로 분리해서 설계하는 방법&lt;/b&gt;을 배우는 과정이었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 헷갈렸던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 헷갈렸던 첫 번째 부분은 Primary 모듈이라는 표현이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Primary라는 이름의 모듈을 새로 만들어야 하는 것처럼 보였지만, 실제로는 현재 프로젝트의 주 게임 모듈을 의미했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에서는 ModuleAndPlugin 모듈이 그 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Primary 모듈
&amp;rarr; 새로 만들 모듈 이름이 아님
&amp;rarr; 현재 프로젝트의 주 게임 모듈

현재 프로젝트 기준
&amp;rarr; ModuleAndPlugin 모듈
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 헷갈렸던 부분은 TestActor를 어디에 만들어야 하는지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 ModuleAndPlugin 모듈에 생성할 뻔했지만, 과제에서는 Test 모듈에 속한 C++ 클래스라고 되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 TestActor는 Source/Test/Public, Source/Test/Private 아래에 만들어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째로 헷갈렸던 부분은 .uproject의 Plugins 항목 작성 방식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Name에 두 플러그인 이름을 한 번에 넣으려고 했지만, 올바른 방식은 플러그인마다 객체를 따로 만드는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 번째로 헷갈렸던 부분은 콘텐츠 브라우저에서 플러그인 폴더가 보이지 않는 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 정상 활성화되어 있었지만, 콘텐츠 브라우저에서 Show Plugin Content 설정이 꺼져 있거나, 플러그인 Content 폴더가 비어 있으면 안 보이는 것처럼 느껴질 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 헷갈렸던 부분은 우리가 만든 플러그인이 실제로 무엇에 쓰이는지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Temporary 플러그인은 아직 기능이 거의 없는 기본 골격이지만, 이 구조를 알면 나중에 인벤토리, 퀘스트, 대화 시스템, 공통 UI, 에디터 도구 같은 기능을 독립 플러그인으로 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘의 핵심 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Primary 모듈은 새로 만들 모듈 이름이 아니라 주 게임 모듈을 의미한다.&lt;/li&gt;
&lt;li&gt;현재 프로젝트에서 주 게임 모듈은 ModuleAndPlugin이다.&lt;/li&gt;
&lt;li&gt;TestActor는 과제 기준으로 Test 모듈에 생성해야 한다.&lt;/li&gt;
&lt;li&gt;TestActor.h는 다른 모듈에서 include해야 하므로 Test/Public에 있어야 한다.&lt;/li&gt;
&lt;li&gt;TestActor.cpp는 구현 파일이므로 Test/Private에 있으면 된다.&lt;/li&gt;
&lt;li&gt;ATestActor 클래스에는 TEST_API 매크로가 붙어야 한다.&lt;/li&gt;
&lt;li&gt;다른 모듈에서 클래스를 사용하려면 해당 모듈의 Build.cs에 의존성을 추가해야 한다.&lt;/li&gt;
&lt;li&gt;ModuleAndPlugin.Build.cs에 &quot;Test&quot;를 추가하면 Test 모듈의 Public 헤더를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;캐릭터 cpp에서 #include &quot;TestActor.h&quot;로 다른 모듈의 클래스를 참조할 수 있다.&lt;/li&gt;
&lt;li&gt;SpawnActor&amp;lt;ATestActor&amp;gt;()를 통해 Test 모듈의 Actor를 월드에 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;모듈 간 상호작용은 한 모듈이 다른 모듈의 Public 클래스를 참조하고 사용하는 구조다.&lt;/li&gt;
&lt;li&gt;플러그인은 프로젝트에 붙였다 뗄 수 있는 독립적인 기능 패키지다.&lt;/li&gt;
&lt;li&gt;플러그인은 프로젝트 루트의 Plugins 폴더 아래에 둔다.&lt;/li&gt;
&lt;li&gt;Temporary.uplugin은 Temporary 플러그인의 설명서 역할을 한다.&lt;/li&gt;
&lt;li&gt;.uplugin의 Modules 항목은 플러그인 안의 모듈 정보를 Unreal에 알려준다.&lt;/li&gt;
&lt;li&gt;Temporary.Build.cs는 Temporary 모듈의 빌드 의존성을 관리한다.&lt;/li&gt;
&lt;li&gt;Temporary.h에서는 IModuleInterface를 상속받은 모듈 클래스를 선언한다.&lt;/li&gt;
&lt;li&gt;StartupModule()은 모듈이 로드될 때 호출된다.&lt;/li&gt;
&lt;li&gt;ShutdownModule()은 모듈이 언로드될 때 호출된다.&lt;/li&gt;
&lt;li&gt;IMPLEMENT_MODULE(FTemporaryModule, Temporary)는 Temporary 모듈의 구현 클래스를 Unreal에 등록한다.&lt;/li&gt;
&lt;li&gt;.uproject의 Plugins 항목에 Temporary를 등록해야 프로젝트에서 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;Plugins 배열에는 플러그인 하나당 객체 하나를 따로 작성해야 한다.&lt;/li&gt;
&lt;li&gt;플러그인 이름, 폴더 이름, .uplugin 이름, 모듈 이름, Build.cs 이름은 서로 일치해야 한다.&lt;/li&gt;
&lt;li&gt;Edit &amp;rarr; Plugins에서 Temporary가 보이고 체크되어 있으면 플러그인 활성화는 성공이다.&lt;/li&gt;
&lt;li&gt;콘텐츠 브라우저에서 플러그인 콘텐츠를 보려면 Show Plugin Content를 켜야 한다.&lt;/li&gt;
&lt;li&gt;CanContainContent: true는 플러그인이 Content 폴더와 에셋을 가질 수 있다는 뜻이다.&lt;/li&gt;
&lt;li&gt;플러그인의 C++ Source 폴더는 콘텐츠 브라우저에 그대로 보이는 대상이 아니다.&lt;/li&gt;
&lt;li&gt;콘텐츠 브라우저에 보이는 것은 주로 플러그인의 Content 폴더 안에 있는 에셋이다.&lt;/li&gt;
&lt;li&gt;우리가 만든 Temporary 플러그인은 아직 실제 기능이 없는 기본 골격이다.&lt;/li&gt;
&lt;li&gt;이 골격 위에 인벤토리, 퀘스트, 대화 시스템, 공통 UI, 에디터 도구 같은 기능을 추가할 수 있다.&lt;/li&gt;
&lt;li&gt;플러그인을 쓰면 기능을 독립적으로 관리하고 다른 프로젝트에 재사용하기 쉽다.&lt;/li&gt;
&lt;li&gt;큰 프로젝트에서는 기능을 모듈과 플러그인 단위로 나누는 것이 구조 관리에 유리하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 오늘 배운 내용은 &lt;b&gt;Unreal Engine에서 기능을 모듈과 플러그인 단위로 분리하고 연결하는 방법&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TestActor 실습을 통해 모듈 간 참조와 Build.cs 의존성의 중요성을 배웠고, Temporary 플러그인 실습을 통해 .uplugin, 플러그인 모듈, 프로젝트 등록, 콘텐츠 브라우저 표시까지 전체 흐름을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이번 실습의 핵심은 단순히 플러그인을 하나 만드는 것이 아니라, Unreal 프로젝트를 더 큰 규모로 관리하기 위해 &lt;b&gt;기능을 독립적인 단위로 분리하고 재사용 가능한 구조로 설계하는 방법&lt;/b&gt;을 배우는 것이었다.&lt;/p&gt;</description>
      <category>unreal 8기</category>
      <author>왕건</author>
      <guid isPermaLink="true">https://talk87604.tistory.com/60</guid>
      <comments>https://talk87604.tistory.com/60#entry60comment</comments>
      <pubDate>Tue, 23 Jun 2026 21:09:32 +0900</pubDate>
    </item>
    <item>
      <title>2026-6-22 TIL</title>
      <link>https://talk87604.tistory.com/59</link>
      <description>&lt;h1&gt;언리얼 C++ 새 모듈(Test) 생성 및 등록&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오늘 배운 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 언리얼 C++ 프로젝트에서 새로운 모듈인 Test 모듈을 직접 생성하고, 빌드 대상에 등록하는 과정을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 모듈은 언리얼 에디터 안에서 만드는 것이 아니라 프로젝트 폴더의 Source 폴더 안에서 직접 생성해야 한다.&lt;br /&gt;즉, Content 폴더가 아니라 Source/Test 경로에 모듈 폴더와 필수 파일을 만들어야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 새 모듈(Test) 생성 위치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 모듈은 프로젝트 폴더 안의 Source 폴더에 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;프로젝트폴더/
├─ Source/
│  ├─ 프로젝트명/
│  │  ├─ 프로젝트명.Build.cs
│  │  ├─ 프로젝트명.h
│  │  └─ 프로젝트명.cpp
│  │
│  └─ Test/
│     ├─ Test.Build.cs
│     ├─ Test.h
│     └─ Test.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 새로 만든 부분은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;Source/Test
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 안에 필요한 파일은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Test.Build.cs
Test.h
Test.cpp
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Test.Build.cs 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test.Build.cs는 Test 모듈의 빌드 설정 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일에는 해당 모듈이 어떤 언리얼 모듈에 의존하는지 작성한다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;using UnrealBuildTool;

public class Test : ModuleRules
{
    public Test(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[]
        {
            &quot;Core&quot;,
            &quot;CoreUObject&quot;,
            &quot;Engine&quot;
        });

        PrivateDependencyModuleNames.AddRange(new string[]
        {
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 PublicDependencyModuleNames는 헤더 파일과 cpp 파일 모두에서 사용할 수 있는 의존성 모듈이고,&lt;br /&gt;PrivateDependencyModuleNames는 cpp 파일 내부에서만 사용하는 의존성 모듈이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Public Dependency와 Private Dependency 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 정리한 핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Public Dependency
&amp;rarr; 헤더(.h)와 소스(.cpp) 모두에서 사용하는 모듈

Private Dependency
&amp;rarr; 소스(.cpp) 파일에서만 사용하는 모듈
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤 클래스를 .h 파일에서 상속받거나 멤버 변수로 사용한다면 해당 모듈은 PublicDependencyModuleNames에 넣는 것이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 .cpp 파일 안에서만 잠깐 사용하는 기능이라면 PrivateDependencyModuleNames에 넣으면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Test.h 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test.h 파일은 새 모듈의 기본 헤더 파일이다.&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;#pragma once

#include &quot;CoreMinimal.h&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreMinimal.h는 언리얼 C++에서 기본적으로 자주 사용하는 타입과 기능을 포함하는 헤더이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Test.cpp 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test.cpp 파일에는 모듈을 언리얼에 등록하는 코드를 작성한다.&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;#include &quot;Test.h&quot;
#include &quot;Modules/ModuleManager.h&quot;

IMPLEMENT_MODULE(FDefaultModuleImpl, Test);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 IMPLEMENT_MODULE이다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;IMPLEMENT_MODULE(FDefaultModuleImpl, Test);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 Test라는 모듈을 언리얼 엔진의 모듈 시스템에 등록하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 파일만 만든다고 모듈이 자동으로 인식되는 것이 아니라,&lt;br /&gt;IMPLEMENT_MODULE을 통해 이 모듈이 실제 언리얼 모듈이라는 것을 알려줘야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Target.cs에 Test 모듈 등록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 모듈을 만들었다면 컴파일 대상에도 등록해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정해야 하는 파일은 보통 Source 폴더 바로 아래에 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Source/프로젝트명.Target.cs
Source/프로젝트명Editor.Target.cs
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 파일 모두에서 ExtraModuleNames에 &quot;Test&quot;를 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 기존 코드가 다음과 같다면,&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ExtraModuleNames.AddRange(new string[] { &quot;프로젝트명&quot; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 수정한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ExtraModuleNames.AddRange(new string[] { &quot;프로젝트명&quot;, &quot;Test&quot; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 기존 코드가 이렇게 되어 있다면,&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;ExtraModuleNames.Add(&quot;프로젝트명&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 한 줄을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;ExtraModuleNames.Add(&quot;프로젝트명&quot;);
ExtraModuleNames.Add(&quot;Test&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업은 Test 모듈도 실제 빌드 과정에 포함시키기 위한 설정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. .uproject 파일에 Test 모듈 등록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Target.cs에 등록하는 것과 별개로 .uproject 파일에도 모듈 정보를 추가해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.uproject 파일은 프로젝트 폴더 가장 바깥에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;&quot;Modules&quot;: [
    {
        &quot;Name&quot;: &quot;프로젝트명&quot;,
        &quot;Type&quot;: &quot;Runtime&quot;,
        &quot;LoadingPhase&quot;: &quot;Default&quot;
    },
    {
        &quot;Name&quot;: &quot;Test&quot;,
        &quot;Type&quot;: &quot;Runtime&quot;,
        &quot;LoadingPhase&quot;: &quot;PreDefault&quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &quot;Name&quot;: &quot;Test&quot;는 새로 만든 모듈 이름이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Type&quot;: &quot;Runtime&quot;은 게임 실행 중에도 사용할 수 있는 런타임 모듈이라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;LoadingPhase&quot;: &quot;PreDefault&quot;는 기본 로딩 시점보다 조금 더 먼저 모듈을 로드하겠다는 의미이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Generate Visual Studio project files 실행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 생성과 등록이 끝났다면 프로젝트 파일을 다시 생성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 언리얼 에디터 종료
2. .uproject 파일 우클릭
3. Generate Visual Studio project files 실행
4. Visual Studio에서 프로젝트 열기
5. 빌드 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 해야 Visual Studio 솔루션에 새로 만든 Test 모듈이 반영된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 핵심은 새 모듈은 단순히 파일만 만든다고 끝나는 것이 아니라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Source/Test 폴더에 Test.Build.cs, Test.h, Test.cpp를 만든 뒤,&lt;br /&gt;Target.cs와 .uproject 파일에도 모듈을 등록해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. Source 폴더 안에 Test 폴더 생성
2. Test.Build.cs 작성
3. Test.h 작성
4. Test.cpp 작성
5. IMPLEMENT_MODULE로 모듈 등록
6. Target.cs의 ExtraModuleNames에 Test 추가
7. .uproject의 Modules 리스트에 Test 추가
8. Generate Visual Studio project files 실행
9. Visual Studio에서 빌드
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;느낀 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 새 모듈을 언리얼 에디터에서 만드는 줄 알았지만, 실제로는 프로젝트 폴더의 Source 안에서 직접 파일과 폴더를 만들어야 한다는 점을 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 모듈 파일을 만드는 것만으로는 부족하고, Target.cs와 .uproject에 등록해야 실제 컴파일 과정에 포함된다는 것도 이해했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 정리한 내용을 기준으로 보면 언리얼 C++ 모듈 생성 흐름은 다음 한 문장으로 정리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;모듈 파일 생성 &amp;rarr; 빌드 설정 작성 &amp;rarr; 모듈 매크로 등록 &amp;rarr; Target.cs 등록 &amp;rarr; .uproject 등록 &amp;rarr; 프로젝트 파일 재생성 &amp;rarr; 빌드
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>unreal 8기</category>
      <author>왕건</author>
      <guid isPermaLink="true">https://talk87604.tistory.com/59</guid>
      <comments>https://talk87604.tistory.com/59#entry59comment</comments>
      <pubDate>Mon, 22 Jun 2026 20:23:28 +0900</pubDate>
    </item>
    <item>
      <title>2026-6-19 TIL</title>
      <link>https://talk87604.tistory.com/58</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIL: Unreal Engine 프로젝트 구조와 플러그인의 특성 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 배운 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Unreal Engine 프로젝트 구조에서 Config, Content, Source, Plugins, .uproject 파일이 각각 어떤 역할을 하는지, 그리고 그중에서도 &lt;b&gt;플러그인(Plugin)&lt;/b&gt; 이 어떤 특성을 가지는지 자세히 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제의 핵심은 단순히 폴더 이름을 외우는 것이 아니라, Unreal Engine 프로젝트가 어떤 단위로 구성되고, 기능을 어떻게 분리해서 관리할 수 있는지 이해하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Plugin은 프로젝트에 기능을 추가하는 독립적인 패키지이며, 하나 이상의 모듈과 콘텐츠, 설정 파일을 포함할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;프로젝트
&amp;rarr; 게임 전체를 실행하는 가장 큰 단위

모듈
&amp;rarr; C++ 코드가 컴파일되는 최소 단위

플러그인
&amp;rarr; 모듈, 콘텐츠, 설정을 묶어 재사용 가능한 기능 단위로 만든 것
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 플러그인은 프로젝트 안에 붙였다 뗄 수 있는 기능 패키지라고 이해하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Unreal 프로젝트의 기본 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unreal Engine 프로젝트에는 보통 다음과 같은 폴더와 파일이 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Config
Content
Plugins
Source
LyraStarterGame.uproject
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 의미는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;Config
&amp;rarr; 프로젝트 설정 파일이 들어가는 폴더

Content
&amp;rarr; 블루프린트, 머티리얼, 텍스처, 사운드 같은 에셋이 들어가는 폴더

Source
&amp;rarr; C++ 소스코드가 들어가는 폴더

Plugins
&amp;rarr; 프로젝트에 추가 기능을 넣는 플러그인들이 들어가는 폴더

.uproject
&amp;rarr; Unreal 프로젝트 자체를 나타내는 파일
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Content와 Source 정도만 익숙했지만, 실제 Unreal 프로젝트는 설정, 코드, 에셋, 플러그인 구조가 서로 연결되어 구성된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 프로젝트의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 Unreal Engine에서 게임 전체를 구성하는 가장 큰 단위다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에는 게임 실행에 필요한 코드, 에셋, 설정, 플러그인 정보가 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 LyraStarterGame.uproject 파일은 이 프로젝트가 Unreal Engine 프로젝트라는 것을 알려주는 파일이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;LyraStarterGame.uproject
&amp;rarr; LyraStarterGame 프로젝트의 중심 파일
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 독립적으로 빌드하고 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 프로젝트는 게임 자체라고 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;프로젝트
= 게임 전체를 담고 있는 실행 가능한 단위
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 모듈의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈(Module)은 Unreal Engine에서 C++ 코드가 컴파일되는 단위다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 모듈은 Unreal 프로젝트 안에서 C++ 코드가 묶이는 최소 단위다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 프로젝트의 Source 폴더 안에 하나의 모듈이 있을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;Source
└─ MyGame
   ├─ MyGame.Build.cs
   ├─ Public
   └─ Private
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 MyGame이 하나의 모듈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyGame.Build.cs는 이 모듈이 어떤 Unreal 기능을 사용할지, 어떤 다른 모듈에 의존할지를 정하는 빌드 설정 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈은 레고 블록처럼 생각할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;모듈
= Unreal Engine에서 C++ 기능을 조립하는 코드 블록
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 플러그인의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인(Plugin)은 프로젝트에 추가할 수 있는 독립적인 기능 패키지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 하나 이상의 모듈을 포함할 수 있고, 필요하다면 콘텐츠도 포함할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 기능들을 플러그인으로 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;인벤토리 시스템
대화 시스템
퀘스트 시스템
미니맵 시스템
아이템 데이터 관리 시스템
공통 UI 시스템
온라인 로그인 기능
에디터 편의 기능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 기능들은 특정 프로젝트에만 박아두는 것보다 플러그인으로 분리하면 다른 프로젝트에서도 재사용하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 플러그인은 다음처럼 이해할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;플러그인
= 프로젝트에 붙였다 뗄 수 있는 재사용 가능한 기능 패키지
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 플러그인은 혼자 실행되지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트와 플러그인의 중요한 차이는 실행 가능 여부다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 독립적으로 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 플러그인은 혼자 실행될 수 없다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;프로젝트
&amp;rarr; 독자적으로 빌드하고 실행 가능

플러그인
&amp;rarr; 혼자 실행 불가능
&amp;rarr; 프로젝트나 엔진에 포함되어야 동작
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비유하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;프로젝트
= 자동차 전체

플러그인
= 내비게이션, 블랙박스, 오디오 장치

모듈
= 그 장치 안의 부품 또는 회로
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내비게이션은 자동차에 붙으면 기능을 하지만, 내비게이션 혼자 도로를 달릴 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인도 마찬가지로 프로젝트에 포함되어야 동작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 플러그인의 기본 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 보통 Plugins 폴더 안에 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 MyPlugin이라는 플러그인이 있다면 구조는 대략 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Plugins
└─ MyPlugin
   ├─ MyPlugin.uplugin
   ├─ Source
   │  └─ MyPlugin
   │     ├─ MyPlugin.Build.cs
   │     ├─ Public
   │     └─ Private
   └─ Content
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 폴더와 파일의 의미는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;MyPlugin.uplugin
&amp;rarr; 플러그인의 설명서 역할

Source
&amp;rarr; 플러그인 C++ 코드가 들어가는 폴더

MyPlugin.Build.cs
&amp;rarr; 플러그인 모듈의 빌드 설정 파일

Public
&amp;rarr; 외부에서 include할 수 있는 헤더 파일

Private
&amp;rarr; 내부 구현 cpp 파일

Content
&amp;rarr; 플러그인에서 사용하는 에셋
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 보면 플러그인은 단순한 코드 묶음이 아니라, C++ 코드와 콘텐츠, 설정을 함께 가질 수 있는 독립적인 기능 단위라는 것을 알 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. .uplugin 파일의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인에는 .uplugin 파일이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 파일이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;MyInventoryPlugin.uplugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일은 Unreal Engine에게 이 플러그인이 어떤 플러그인인지 알려주는 설명서 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.uplugin 파일에는 보통 다음 정보가 들어간다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;플러그인 이름
버전
설명
제작자
포함된 모듈
Runtime용인지 Editor용인지
기본 활성화 여부
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 개념적으로 보면 다음과 같은 구조를 가진다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;FileVersion&quot;: 3,
  &quot;VersionName&quot;: &quot;1.0&quot;,
  &quot;FriendlyName&quot;: &quot;My Inventory Plugin&quot;,
  &quot;Description&quot;: &quot;Inventory system plugin&quot;,
  &quot;Modules&quot;: [
    {
      &quot;Name&quot;: &quot;MyInventory&quot;,
      &quot;Type&quot;: &quot;Runtime&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 부분은 Modules이다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Modules
&amp;rarr; 이 플러그인 안에 어떤 모듈이 들어 있는지 알려준다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 .uplugin 파일은 플러그인을 Unreal Engine에 등록하고 설명하는 핵심 파일이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 플러그인은 하나 이상의 모듈을 가질 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 하나의 모듈만 가질 수도 있고, 여러 개의 모듈을 가질 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인벤토리 플러그인을 만든다면 다음처럼 나눌 수 있다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;InventoryPlugin
├─ InventoryRuntime Module
├─ InventoryEditor Module
└─ Content
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InventoryRuntime은 게임 실행 중 필요한 기능을 담당하고, InventoryEditor는 에디터에서만 필요한 도구를 담당할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 플러그인은 단순히 하나의 코드 덩어리가 아니라, 여러 모듈을 묶어서 하나의 기능 패키지로 관리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;플러그인
&amp;rarr; 더 큰 기능 패키지

모듈
&amp;rarr; 플러그인 안에서 실제 C++ 코드가 컴파일되는 단위
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. Runtime 모듈과 Editor 모듈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인 안의 모듈은 사용되는 상황에 따라 Runtime과 Editor로 나눌 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Runtime 모듈
&amp;rarr; 게임 실행 중에도 필요한 기능

Editor 모듈
&amp;rarr; Unreal Editor에서만 필요한 기능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인벤토리 시스템은 실제 게임 중에도 필요하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;아이템 획득
아이템 사용
인벤토리 UI 표시
장비 장착
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 기능은 Runtime 모듈에 들어가는 것이 자연스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 에디터에서 버튼을 추가하거나 자동 데이터 생성 도구를 만드는 기능은 Editor 모듈에 들어간다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;에디터 전용 툴
자동 데이터 생성 버튼
레벨 배치 도구
커스텀 에디터 창
디버그용 에디터 기능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 기능은 게임 실행 파일에는 필요 없고, 에디터에서만 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Runtime 모듈과 Editor 모듈을 나누면 불필요한 코드가 게임 빌드에 포함되는 것을 줄일 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 플러그인은 콘텐츠도 포함할 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 C++ 코드만 담는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요하다면 Content 폴더를 가지고 블루프린트, 머티리얼, 텍스처, 데이터 에셋 등을 포함할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인벤토리 플러그인은 다음과 같은 콘텐츠를 가질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Plugins
└─ InventoryPlugin
   ├─ Source
   └─ Content
      ├─ BP_InventoryWidget
      ├─ DA_ItemData
      ├─ M_ItemIcon
      └─ T_ItemIcon_Sword
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 인벤토리 기능에 필요한 코드와 UI 블루프린트, 아이템 데이터, 아이콘을 플러그인 안에 함께 넣을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 플러그인은 코드 플러그인일 수도 있고, 콘텐츠를 포함한 플러그인일 수도 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11. 플러그인은 켜고 끌 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인의 큰 특징 중 하나는 프로젝트 설정에서 활성화하거나 비활성화할 수 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unreal Editor에서는 보통 다음 경로에서 플러그인을 관리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Edit
&amp;rarr; Plugins
&amp;rarr; 원하는 플러그인 Enable / Disable
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 프로젝트에 직접 기능을 박아 넣는 것보다 더 유연하다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;필요하면 켠다.
필요 없으면 끈다.
다른 프로젝트로 옮긴다.
기능 단위로 관리한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점 때문에 플러그인은 프로젝트 구조를 유연하게 만드는 데 도움이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12. 플러그인은 재사용성이 좋다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인을 쓰는 가장 큰 이유 중 하나는 재사용성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 RPG 프로젝트에서 퀘스트 시스템을 만들었다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능을 프로젝트의 Source 폴더 안에만 만들면 다른 프로젝트에서 다시 쓰기 번거롭다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;ProjectA/Source 안에 QuestSystem 작성
    &amp;darr;
ProjectB에서 쓰려면 코드 복사 필요
    &amp;darr;
경로 수정 필요
    &amp;darr;
의존성 꼬일 수 있음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 플러그인으로 만들면 더 깔끔하게 재사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;Plugins/QuestPlugin
    &amp;darr;
다른 프로젝트의 Plugins 폴더에 복사
    &amp;darr;
플러그인 활성화
    &amp;darr;
사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 여러 프로젝트에서 반복해서 쓸 수 있는 기능은 플러그인으로 분리하는 것이 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13. 플러그인은 의존성을 가질 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 독립적인 기능 단위이지만, 아무것에도 의존하지 않는다는 뜻은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 Unreal 모듈이나 다른 플러그인에 의존할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인벤토리 플러그인에서 UI를 사용한다면 UMG 모듈이 필요할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Build.cs에 의존성을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;PublicDependencyModuleNames.AddRange(new string[]
{
    &quot;Core&quot;,
    &quot;CoreUObject&quot;,
    &quot;Engine&quot;,
    &quot;UMG&quot;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 다음과 같은 의미다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;이 모듈은 Core, CoreUObject, Engine, UMG 기능을 사용한다.
따라서 빌드할 때 이 모듈들을 참조해야 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 플러그인은 기능 단위로 분리되어 있지만, 필요한 기능을 사용하기 위해 다른 모듈과 연결될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14. 플러그인과 프로젝트 코드의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 Source 폴더에 모든 기능을 넣을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 구조가 가능하다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Source
└─ MyGame
   ├─ Inventory
   ├─ Quest
   ├─ Dialogue
   ├─ Combat
   ├─ UI
   └─ Save
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 작은 프로젝트에서는 괜찮을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프로젝트가 커지면 기능들이 서로 섞여 관리가 어려워질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 플러그인으로 나누면 다음처럼 기능 단위가 명확해진다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Plugins
├─ InventoryPlugin
├─ QuestPlugin
├─ DialoguePlugin
├─ SaveSystemPlugin
└─ CommonUIPlugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 각 기능의 책임이 분리된다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;인벤토리 관련 코드
&amp;rarr; InventoryPlugin 안에 위치

퀘스트 관련 코드
&amp;rarr; QuestPlugin 안에 위치

대화 관련 코드
&amp;rarr; DialoguePlugin 안에 위치
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 대규모 프로젝트일수록 플러그인 구조가 유용하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;15. 프로젝트와 플러그인의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트와 플러그인은 모두 Unreal Engine 안에서 기능과 파일을 관리하는 단위지만, 역할이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구분&amp;nbsp;&lt;/b&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;b&gt;프로젝트&amp;nbsp;&lt;/b&gt; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;b&gt; &amp;nbsp; 플러그인&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;실행 가능 여부&lt;/td&gt;
&lt;td&gt;독립적으로 실행 가능&lt;/td&gt;
&lt;td&gt;혼자 실행 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;td&gt;게임 전체를 구성&lt;/td&gt;
&lt;td&gt;특정 기능을 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 파일&lt;/td&gt;
&lt;td&gt;.uproject&lt;/td&gt;
&lt;td&gt;.uplugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source 폴더&lt;/td&gt;
&lt;td&gt;게임 코드&lt;/td&gt;
&lt;td&gt;플러그인 코드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content 폴더&lt;/td&gt;
&lt;td&gt;게임 에셋&lt;/td&gt;
&lt;td&gt;플러그인 전용 에셋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재사용성&lt;/td&gt;
&lt;td&gt;상대적으로 낮음&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;활성화/비활성화&lt;/td&gt;
&lt;td&gt;프로젝트 자체&lt;/td&gt;
&lt;td&gt;Plugins 메뉴에서 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예시&lt;/td&gt;
&lt;td&gt;RPG 게임 프로젝트&lt;/td&gt;
&lt;td&gt;인벤토리 플러그인, 퀘스트 플러그인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 프로젝트는 게임 전체이고, 플러그인은 그 게임에 추가되는 기능 부품이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;16. 플러그인과 모듈의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인과 모듈도 헷갈리기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;모듈
&amp;rarr; C++ 코드가 컴파일되는 단위

플러그인
&amp;rarr; 모듈과 콘텐츠, 설정을 묶은 기능 패키지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 플러그인이 더 큰 개념이고, 플러그인 안에 모듈이 들어갈 수 있다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Plugin
└─ Module
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음처럼 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;InventoryPlugin
├─ InventoryRuntime Module
├─ InventoryEditor Module
└─ Content
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서 InventoryPlugin은 플러그인이고, InventoryRuntime, InventoryEditor는 각각 모듈이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;17. 플러그인을 쓰면 좋은 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인은 다음과 같은 경우에 사용하기 좋다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;다른 프로젝트에서도 재사용하고 싶은 기능
기능 단위로 분리하고 싶은 시스템
팀 단위로 독립 개발하고 싶은 기능
에디터 도구를 만들고 싶은 경우
프로젝트 코드와 분리하고 싶은 공통 기능
켜고 끌 수 있는 기능으로 만들고 싶은 경우
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 기능들은 플러그인 후보가 될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;공통 인벤토리 시스템
공통 대화 시스템
공통 세이브 시스템
공통 네트워크 유틸
공통 UI 프레임워크
자동 맵 배치 에디터 툴
데이터 테이블 자동 생성 도구
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 기능들은 특정 프로젝트에만 묶어두기보다 플러그인으로 만들면 다른 프로젝트에서도 활용하기 쉽다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;18. 플러그인으로 만들지 않아도 되는 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 모든 기능을 무조건 플러그인으로 만들 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 게임에만 강하게 묶인 기능은 프로젝트 코드에 두는 것이 더 자연스러울 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이 게임의 주인공 전용 스킬
특정 보스의 패턴
특정 맵의 이벤트
이 프로젝트에서만 쓰는 임시 테스트 코드
재사용 가능성이 거의 없는 기능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 기능은 굳이 플러그인으로 분리하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기준은 다음과 같이 잡으면 된다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;다른 프로젝트에서도 쓸 수 있다.
기능 단위로 독립성이 높다.
켜고 끌 수 있으면 좋다.
&amp;rarr; 플러그인 후보

이 프로젝트에만 강하게 종속된다.
재사용 가능성이 낮다.
&amp;rarr; 프로젝트 코드에 두어도 됨
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;19. 플러그인을 쓰는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플러그인을 쓰는 이유는 단순히 폴더를 나누기 위해서가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 목적은 기능을 독립적으로 관리하고 재사용성을 높이기 위해서다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;기능을 독립된 단위로 분리한다.
다른 프로젝트로 옮기기 쉽게 만든다.
필요한 기능만 활성화할 수 있다.
팀 단위로 개발 영역을 나눌 수 있다.
프로젝트 구조를 깔끔하게 유지할 수 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 플러그인은 대규모 프로젝트에서 기능을 체계적으로 설계하고 관리하기 위한 구조다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;20. 이번 과제에서 플러그인을 배우는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제에서 플러그인과 모듈을 배우는 이유는 Unreal 프로젝트 구조를 더 깊게 이해하기 위해서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Content 폴더에 에셋을 넣고, Source 폴더에 C++ 코드를 넣는 정도만 알아도 프로젝트를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프로젝트가 커지면 모든 코드를 한 곳에 두는 방식은 관리가 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Unreal Engine은 모듈과 플러그인 구조를 통해 기능을 더 체계적으로 분리할 수 있게 한다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;작은 프로젝트
&amp;rarr; Source 안에 기능을 직접 작성해도 충분할 수 있음

큰 프로젝트
&amp;rarr; 모듈과 플러그인 단위로 기능을 나누는 것이 유리함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이번 과제는 단순히 플러그인을 만드는 법만 배우는 것이 아니라, Unreal 프로젝트를 기능 단위로 설계하고 재사용 가능한 구조로 만드는 방법을 배우는 과정이라고 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 헷갈렸던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 헷갈렸던 부분은 플러그인과 모듈의 차이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 플러그인과 모듈이 둘 다 기능을 나누는 단위처럼 보여서 비슷하게 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 모듈은 C++ 코드가 컴파일되는 단위이고, 플러그인은 그런 모듈과 콘텐츠, 설정을 묶은 더 큰 기능 패키지였다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;모듈
&amp;rarr; C++ 코드가 컴파일되는 단위

플러그인
&amp;rarr; 모듈, 콘텐츠, 설정을 묶은 기능 패키지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 플러그인은 프로젝트처럼 혼자 실행되는 것이 아니라, 프로젝트나 엔진에 포함되어야 동작한다는 점도 중요했다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;프로젝트
&amp;rarr; 게임 전체이므로 실행 가능

플러그인
&amp;rarr; 기능 부품이므로 혼자 실행 불가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 모든 기능을 무조건 플러그인으로 만들 필요는 없다는 점도 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재사용 가능성이 높고 독립적인 기능은 플러그인으로 만들기 좋지만, 특정 게임에만 강하게 묶인 기능은 프로젝트 코드에 두어도 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘의 핵심 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Unreal 프로젝트는 Config, Content, Source, Plugins, .uproject 파일로 구성된다.&lt;/li&gt;
&lt;li&gt;Config는 프로젝트 설정 파일이 들어가는 폴더다.&lt;/li&gt;
&lt;li&gt;Content는 블루프린트, 머티리얼, 텍스처 같은 에셋이 들어가는 폴더다.&lt;/li&gt;
&lt;li&gt;Source는 C++ 소스코드가 들어가는 폴더다.&lt;/li&gt;
&lt;li&gt;Plugins는 프로젝트에 추가 기능을 넣는 플러그인들이 들어가는 폴더다.&lt;/li&gt;
&lt;li&gt;.uproject는 Unreal 프로젝트 자체를 나타내는 파일이다.&lt;/li&gt;
&lt;li&gt;프로젝트는 게임 전체를 구성하는 실행 가능한 단위다.&lt;/li&gt;
&lt;li&gt;모듈은 Unreal Engine에서 C++ 코드가 컴파일되는 단위다.&lt;/li&gt;
&lt;li&gt;플러그인은 프로젝트에 붙였다 뗄 수 있는 재사용 가능한 기능 패키지다.&lt;/li&gt;
&lt;li&gt;플러그인은 혼자 실행될 수 없고 프로젝트나 엔진에 포함되어야 동작한다.&lt;/li&gt;
&lt;li&gt;플러그인은 하나 이상의 모듈을 포함할 수 있다.&lt;/li&gt;
&lt;li&gt;플러그인은 C++ 코드뿐 아니라 콘텐츠도 포함할 수 있다.&lt;/li&gt;
&lt;li&gt;.uplugin 파일은 플러그인의 이름, 버전, 설명, 모듈 정보 등을 관리하는 설명서 역할을 한다.&lt;/li&gt;
&lt;li&gt;Runtime 모듈은 게임 실행 중 필요한 기능을 담당한다.&lt;/li&gt;
&lt;li&gt;Editor 모듈은 Unreal Editor에서만 필요한 기능을 담당한다.&lt;/li&gt;
&lt;li&gt;플러그인은 Unreal Editor의 Plugins 메뉴에서 활성화하거나 비활성화할 수 있다.&lt;/li&gt;
&lt;li&gt;플러그인은 다른 프로젝트로 옮겨 재사용하기 쉽다.&lt;/li&gt;
&lt;li&gt;플러그인은 필요한 Unreal 모듈이나 다른 플러그인에 의존할 수 있다.&lt;/li&gt;
&lt;li&gt;플러그인의 의존성은 보통 Build.cs에서 관리한다.&lt;/li&gt;
&lt;li&gt;프로젝트 코드는 게임 전체에 직접 포함되는 코드이고, 플러그인 코드는 특정 기능 단위로 분리된 코드다.&lt;/li&gt;
&lt;li&gt;플러그인은 대규모 프로젝트에서 기능별 구조를 깔끔하게 관리하는 데 유용하다.&lt;/li&gt;
&lt;li&gt;프로젝트는 .uproject 파일을 가지고, 플러그인은 .uplugin 파일을 가진다.&lt;/li&gt;
&lt;li&gt;모듈은 플러그인보다 작은 개념이고, 플러그인 안에 모듈이 들어갈 수 있다.&lt;/li&gt;
&lt;li&gt;다른 프로젝트에서도 사용할 수 있는 기능은 플러그인으로 만들기 좋다.&lt;/li&gt;
&lt;li&gt;특정 게임에만 강하게 종속된 기능은 프로젝트 코드에 두어도 된다.&lt;/li&gt;
&lt;li&gt;플러그인은 기능 단위의 독립성, 재사용성, 확장성을 높이기 위해 사용한다.&lt;/li&gt;
&lt;li&gt;이번 과제는 Unreal 프로젝트 구조를 모듈과 플러그인 단위로 이해하기 위한 과정이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 플러그인은 Unreal 프로젝트에 기능을 추가하는 독립적인 확장 단위다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 게임 전체를 실행하는 단위이고, 모듈은 C++ 코드가 컴파일되는 단위이며, 플러그인은 이런 모듈과 콘텐츠, 설정을 묶어서 재사용 가능한 기능 패키지로 만든 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 플러그인을 이해한다는 것은 단순히 폴더 구조를 아는 것이 아니라, Unreal Engine에서 기능을 어떻게 분리하고, 재사용하고, 확장 가능한 구조로 설계하는지 이해하는 것이다.&lt;/p&gt;</description>
      <category>unreal 8기</category>
      <author>왕건</author>
      <guid isPermaLink="true">https://talk87604.tistory.com/58</guid>
      <comments>https://talk87604.tistory.com/58#entry58comment</comments>
      <pubDate>Fri, 19 Jun 2026 21:04:32 +0900</pubDate>
    </item>
    <item>
      <title>2026-6-18 TIL</title>
      <link>https://talk87604.tistory.com/57</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIL: Unreal Engine 상속과 인터페이스, NetUpdateFrequency, NetDormancy를 통한 네트워크 최적화 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 배운 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Unreal Engine에서 &lt;b&gt;상속과 인터페이스의 차이&lt;/b&gt;, NetUpdateFrequency의 의미, ForceNetUpdate(), 클라이언트 보간 처리, 그리고 NetDormancy를 사용하는 이유에 대해 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 상속과 인터페이스가 모두 기능을 재사용하거나 공통 기능을 만들기 위한 개념처럼 보여서 헷갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 정리해보면 상속은 &lt;b&gt;부모 클래스의 기능과 속성을 물려받는 구조&lt;/b&gt;이고, 인터페이스는 &lt;b&gt;서로 다른 클래스들이 같은 기능을 가지고 있다고 약속하는 구조&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 NetUpdateFrequency, ForceNetUpdate(), NetDormancy, FlushNetDormancy()는 모두 멀티플레이에서 서버와 클라이언트 간 복제 비용을 줄이거나, 필요한 순간에만 복제되도록 조절하는 네트워크 최적화 기능과 관련이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;상속
&amp;rarr; &quot;~의 한 종류다&quot; 관계

인터페이스
&amp;rarr; &quot;~할 수 있다&quot; 관계

NetUpdateFrequency
&amp;rarr; 액터를 최대 얼마나 자주 복제할지 정하는 값

ForceNetUpdate()
&amp;rarr; 다음 네트워크 업데이트 때 빠르게 복제되도록 요청

NetDormancy
&amp;rarr; 자주 안 바뀌는 액터를 휴면 상태로 두어 복제 비용을 줄이는 최적화

FlushNetDormancy()
&amp;rarr; 휴면 상태 액터를 깨워 변경사항을 복제할 수 있게 함
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 상속의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속은 부모 클래스의 변수와 함수를 자식 클래스가 물려받는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Unreal Engine에서 플레이어 캐릭터를 만들 때 보통 ACharacter를 상속받는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class AMyCharacter : public ACharacter
{
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 AMyCharacter가 ACharacter의 기능을 물려받는다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACharacter에는 이미 캐릭터를 만들기 위한 여러 기능이 들어 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이동
점프
캡슐 충돌
CharacterMovement
애니메이션 처리
Controller에게 Possess 되는 구조
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 AMyCharacter는 이 기능들을 처음부터 다시 만들 필요 없이 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속은 다음과 같은 관계로 이해하면 쉽다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;AMyCharacter는 ACharacter의 한 종류다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 상속은 &lt;b&gt;is-a 관계&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;mercury&quot;&gt;&lt;code&gt;PlayerCharacter is a Character.
MonsterCharacter is a Character.
NPCCharacter is a Character.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 상속을 쓰면 좋은 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속은 공통 기능과 데이터를 여러 클래스가 함께 사용할 때 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 플레이어, 몬스터, NPC가 모두 캐릭터처럼 움직이고 HP도 가진다면, 공통 부모 클래스를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;BaseCharacter
    ├─ PlayerCharacter
    ├─ MonsterCharacter
    └─ NPCCharacter
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 클래스에 HP, 데미지 처리, 사망 처리 같은 공통 기능을 넣어둘 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class ABaseCharacter : public ACharacter
{
public:
    float HP;

    void TakeDamage();
    void Die();
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 자식 클래스들은 이 기능을 물려받는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class APlayerCharacter : public ABaseCharacter
{
};

class AMonsterCharacter : public ABaseCharacter
{
};

class ANPCCharacter : public ABaseCharacter
{
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 상속이 자연스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 플레이어, 몬스터, NPC 모두 캐릭터의 한 종류이기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 인터페이스의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스는 기능을 직접 물려받는다기보다, 특정 기능을 가지고 있다고 약속하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 게임에 다음과 같은 오브젝트들이 있다고 하자.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;문
상자
NPC
아이템
스위치
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 오브젝트들은 서로 같은 종류가 아니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;문은 NPC가 아니다.
상자는 캐릭터가 아니다.
아이템은 문이 아니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 공통점이 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;플레이어가 상호작용할 수 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 Interact라는 인터페이스를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;BPI_Interactable
    └─ Interact()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인터페이스를 구현한 오브젝트들은 모두 Interact() 기능을 가지고 있다고 약속하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 오브젝트는 자기 방식대로 Interact()를 구현한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;문
&amp;rarr; Interact 실행 시 문 열기

상자
&amp;rarr; Interact 실행 시 상자 열기

NPC
&amp;rarr; Interact 실행 시 대화 시작

아이템
&amp;rarr; Interact 실행 시 아이템 줍기

스위치
&amp;rarr; Interact 실행 시 스위치 작동
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스는 다음과 같은 관계로 이해하면 쉽다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;이 객체는 상호작용할 수 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 인터페이스는 &lt;b&gt;can-do 관계&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Door can interact.
Chest can interact.
NPC can interact.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 상속과 인터페이스의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속과 인터페이스의 가장 큰 차이는 관계의 성격이다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;상속
&amp;rarr; &quot;~의 한 종류다&quot;

인터페이스
&amp;rarr; &quot;~할 수 있다&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표로 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분상속인터페이스&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;td&gt;부모 클래스의 기능과 속성을 물려받음&lt;/td&gt;
&lt;td&gt;특정 기능을 가지고 있다고 약속함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관계&lt;/td&gt;
&lt;td&gt;&quot;~이다&quot;&lt;/td&gt;
&lt;td&gt;&quot;~할 수 있다&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예시&lt;/td&gt;
&lt;td&gt;MyCharacter는 Character다&lt;/td&gt;
&lt;td&gt;Door는 Interact할 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;변수 상속&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;보통 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;함수 구현 상속&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;보통 함수 이름만 약속&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 목적&lt;/td&gt;
&lt;td&gt;공통 기능 재사용&lt;/td&gt;
&lt;td&gt;서로 다른 클래스를 같은 방식으로 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 예시&lt;/td&gt;
&lt;td&gt;Character 상속, Actor 상속&lt;/td&gt;
&lt;td&gt;Interactable, Damageable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 인터페이스가 필요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 상호작용 가능한 오브젝트를 전부 상속으로 처리하려고 하면 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 AInteractableActor라는 부모 클래스를 만들었다고 하자.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;AInteractableActor
    ├─ Door
    ├─ Chest
    └─ Item
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문, 상자, 아이템은 AActor 기반이라면 이 구조가 가능할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 NPC는 이미 ACharacter를 상속받고 있을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;NPC &amp;rarr; ACharacter
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unreal Engine의 UCLASS 구조에서는 보통 하나의 주요 부모 클래스만 상속받는 방식으로 설계한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 NPC를 동시에 ACharacter이면서 AInteractableActor로 만들기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 인터페이스가 유용하다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Door &amp;rarr; Actor + BPI_Interactable
Chest &amp;rarr; Actor + BPI_Interactable
NPC &amp;rarr; Character + BPI_Interactable
Item &amp;rarr; Actor + BPI_Interactable
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 부모 클래스가 서로 달라도 같은 인터페이스를 붙일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어는 대상이 문인지, 상자인지, NPC인지 몰라도 된다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;대상이 BPI_Interactable을 가지고 있는가?
    &amp;darr;
가지고 있으면 Interact 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 인터페이스는 서로 다른 클래스를 같은 방식으로 다루고 싶을 때 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. NetUpdateFrequency의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetUpdateFrequency는 액터가 서버에서 클라이언트로 얼마나 자주 복제될 수 있는지를 정하는 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같이 설정하면,&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SetNetUpdateFrequency(1.f);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 액터는 최대 1초에 1번 정도 복제 시도 대상이 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NetUpdateFrequency = 1
&amp;rarr; 1초에 최대 1번 복제 시도
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 중요한 점은 NetUpdateFrequency가 정확한 복제 주기를 보장하는 값은 아니라는 것이다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;NetUpdateFrequency는 최대치일 뿐
정확히 그 주기마다 복제된다고 보장하지 않는다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 서버 Tick Rate가 30이고 NetUpdateFrequency가 100이라면, 실제로 초당 100번 복제될 수 없다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;서버 Tick Rate = 30
NetUpdateFrequency = 100

결과:
초당 100번 복제 불가능
최대 초당 30번 수준
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 서버 성능, 액터 수, 네트워크 대역폭, 액터 우선순위, Relevancy 등에 따라 실제 복제 타이밍은 달라질 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. ForceNetUpdate()의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ForceNetUpdate()는 액터의 변경사항을 다음 네트워크 업데이트 때 최대한 빨리 복제하도록 요청하는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 HP가 바뀌었을 때 바로 클라이언트에 알려주고 싶다면 다음처럼 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void AMyCharacter::SetHP(float NewHP)
{
    if (HasAuthority())
    {
        HP = NewHP;

        ForceNetUpdate();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;서버에서 HP 변경
    &amp;darr;
ForceNetUpdate() 호출
    &amp;darr;
다음 네트워크 업데이트 때 빠르게 복제 시도
    &amp;darr;
클라이언트에서 OnRep_HP() 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 ForceNetUpdate()도 완전한 즉시 전송 보장은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히는 다음 의미에 가깝다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;지금 당장 무조건 보내라
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 아니라,&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;가능한 다음 네트워크 업데이트 때 우선적으로 보내라
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 NetUpdateFrequency는 최대 빈도를 조절하는 값이고, ForceNetUpdate()는 특정 순간에 빠른 복제를 요청하는 함수라고 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. Reliable RPC와 Replication의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태값을 동기화할 때는 replicated 변수와 OnRep을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;HP
점수
팀
현재 무기
문 열림 상태
사망 여부
준비 완료 여부
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 값들은 현재 상태가 중요하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재 HP가 얼마인가?
현재 문이 열려 있는가?
현재 무기가 무엇인가?
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Replicated 변수와 OnRep이 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 한 번만 발생하는 이벤트는 RPC가 더 적합하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;접속 알림 출력
채팅 메시지 출력
공격 요청
스킬 사용
아이템 줍기 요청
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 것은 계속 유지되는 상태가 아니라 특정 순간에 발생하는 동작이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 접속 알림 메시지는 한 번 출력되면 된다.&lt;/p&gt;
&lt;pre class=&quot;smalltalk&quot;&gt;&lt;code&gt;&quot;Player2 has joined the game.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우는 NetUpdateFrequency나 RepNotify보다 Reliable Client RPC가 더 자연스럽다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;UFUNCTION(Client, Reliable)
void Client_ShowJoinMessage(const FString&amp;amp; Message);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void AMyPlayerController::Client_ShowJoinMessage_Implementation(const FString&amp;amp; Message)
{
    ShowMessageOnUI(Message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;계속 유지되어야 하는 값
&amp;rarr; Replicated 변수 + OnRep

한 번만 발생하는 이벤트
&amp;rarr; RPC
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. DXBox 코드의 목적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DXBox 코드에서는 박스 액터의 NetUpdateFrequency를 1로 설정했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const static float BoxActorNetUpdateFrequency = 1.f;
SetNetUpdateFrequency(BoxActorNetUpdateFrequency);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 서버는 이 박스의 회전값을 대략 1초에 한 번 정도만 클라이언트에 복제하려고 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NetUpdateFrequency = 1
&amp;rarr; 1초에 최대 1번 복제 시도
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다음 코드로 복제 주기를 계산한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;NetUpdatePeriod = 1 / GetNetUpdateFrequency();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 NetUpdateFrequency가 1이므로,&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;NetUpdatePeriod = 1 / 1
NetUpdatePeriod = 1초
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이 박스는 클라이언트 기준으로 서버 회전값이 약 1초마다 한 번씩 온다고 가정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. DXBox 클라이언트 보간 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 회전값을 1초에 한 번만 보내면 클라이언트에서는 회전이 끊겨 보일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아무 처리를 하지 않으면 다음처럼 보일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0초: 0도
1초: 90도
2초: 180도
3초: 270도
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 회전이 부드럽게 도는 것이 아니라 순간이동하듯이 툭툭 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 클라이언트에서는 마지막으로 받은 서버 회전값을 기준으로, 다음 서버 회전값을 예상해서 부드럽게 보간한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;AccDeltaSecondSinceReplicated += DeltaSeconds;
const float LerpRatio = FMath::Clamp(AccDeltaSecondSinceReplicated / NetUpdatePeriod, 0.f, 1.f);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AccDeltaSecondSinceReplicated는 마지막으로 서버 회전값을 받은 뒤부터 얼마나 시간이 지났는지 누적하는 변수다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;서버 회전값 받음
&amp;rarr; AccDeltaSecondSinceReplicated = 0

0.25초 지남
&amp;rarr; AccDeltaSecondSinceReplicated = 0.25

0.5초 지남
&amp;rarr; AccDeltaSecondSinceReplicated = 0.5

1초 지남
&amp;rarr; AccDeltaSecondSinceReplicated = 1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LerpRatio는 보간 비율이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0초 지남 &amp;rarr; LerpRatio = 0
0.5초 지남 &amp;rarr; LerpRatio = 0.5
1초 지남 &amp;rarr; LerpRatio = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값을 이용해서 서버에서 받은 회전값과 다음 예상 회전값 사이를 부드럽게 이동한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11. 다음 서버 회전값 예측&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드는 다음 서버 회전값을 예측하는 부분이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const float NextServerRotationYaw = ServerRotationYaw + RotationSpeed * NetUpdatePeriod;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 마지막으로 서버에서 받은 회전값이 30도이고, 회전 속도가 초당 90도라면,&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;ServerRotationYaw = 30도
RotationSpeed = 90
NetUpdatePeriod = 1초
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 서버 회전값은 다음과 같이 예상할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NextServerRotationYaw = 30 + 90 * 1
NextServerRotationYaw = 120도
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 클라이언트는 이렇게 예측하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;서버가 마지막으로 30도라고 알려줬고,
이 박스는 초당 90도씩 회전하니까,
1초 뒤에는 아마 120도쯤 되어 있겠지?
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 실제 서버에서 받은 값이 아니라 클라이언트가 계산한 예상값이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12. Lerp로 부드럽게 회전시키기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드는 실제로 회전값을 보간하는 부분이다.&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;const float EstimatedClientRotationYaw = FMath::Lerp(ServerRotationYaw, NextServerRotationYaw, LerpRatio);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lerp는 두 값 사이를 비율에 따라 섞어주는 함수다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Lerp(A, B, 0.0)
&amp;rarr; A

Lerp(A, B, 0.5)
&amp;rarr; A와 B의 중간

Lerp(A, B, 1.0)
&amp;rarr; B
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같다면,&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;ServerRotationYaw = 30도
NextServerRotationYaw = 120도
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음처럼 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;LerpRatio = 0.0
&amp;rarr; 30도

LerpRatio = 0.5
&amp;rarr; 75도

LerpRatio = 1.0
&amp;rarr; 120도
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 계산된 회전값을 액터에 적용한다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;SetActorRotation(FRotator(0.f, EstimatedClientRotationYaw, 0.f));
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버는 1초에 한 번만 회전값을 보내지만 클라이언트는 그 사이를 직접 계산해서 부드럽게 회전시킨다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13. OnRep_ServerRotationYaw()의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OnRep_ServerRotationYaw()는 서버에서 ServerRotationYaw 값이 클라이언트로 복제되었을 때 호출된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;void ADXBox::OnRep_ServerRotationYaw()
{
    AccDeltaSecondSinceReplicated = 0.f;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 서버 회전값을 받았으므로, 마지막 복제 이후 지난 시간을 다시 0으로 초기화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;서버에서 ServerRotationYaw 복제
    &amp;darr;
클라이언트에서 OnRep_ServerRotationYaw() 호출
    &amp;darr;
AccDeltaSecondSinceReplicated = 0
    &amp;darr;
새로운 서버 회전값 기준으로 다시 보간 시작
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 클라이언트는 매번 새로운 서버 회전값을 기준으로 다시 자연스럽게 회전을 예측할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14. DXBox 코드의 전체 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름을 서버와 클라이언트로 나누면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[서버]

박스 회전 계산
    &amp;darr;
ServerRotationYaw 값 변경
    &amp;darr;
NetUpdateFrequency = 1 이므로 약 1초에 한 번 복제 시도
    &amp;darr;
클라이언트에게 ServerRotationYaw 전달
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;[클라이언트]

ServerRotationYaw 복제 받음
    &amp;darr;
OnRep_ServerRotationYaw() 호출
    &amp;darr;
AccDeltaSecondSinceReplicated = 0
    &amp;darr;
Tick마다 시간 누적
    &amp;darr;
현재 서버 회전값에서 다음 예상 회전값까지 Lerp
    &amp;darr;
SetActorRotation으로 부드럽게 회전
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 낮은 복제 빈도에서도 클라이언트 화면에서는 박스가 부드럽게 회전해 보이도록 만드는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 목적은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;복제는 적게 한다.
하지만 클라이언트 화면에서는 부드럽게 보이게 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;15. NetDormancy의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetDormancy는 액터를 네트워크 휴면 상태로 두는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;휴면 상태인 액터는 더 이상 프로퍼티 레플리케이션이나 RPC가 동작하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버가 계속해서 이 액터의 replicated property가 바뀌었는지 검사하지 않도록 하는 최적화 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Awake 상태
&amp;rarr; 서버가 계속 &quot;이 액터 바뀐 거 있나?&quot; 검사함

Dormant 상태
&amp;rarr; 서버가 &quot;이 액터는 당분간 안 바뀌니까 검사하지 말자&quot; 하고 쉬게 둠
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 Dormant 상태가 액터를 삭제하는 것은 아니라는 것이다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;액터는 서버와 클라이언트에 그대로 존재한다.
하지만 휴면 상태 동안은 복제 대상으로 거의 고려되지 않는다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;16. NetDormancy를 쓰는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetDormancy를 쓰는 이유는 자주 안 바뀌는 replicated 액터의 복제 비용을 줄이기 위해서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 맵에 다음과 같은 액터들이 많다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;보물상자 500개
문 100개
스위치 50개
레버 30개
퀘스트 오브젝트 100개
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 액터들이 모두 replicated 상태이고 계속 Awake 상태라면, 서버는 매번 이런 검사를 해야 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이 상자 상태 바뀌었나?
이 문 상태 바뀌었나?
이 스위치 상태 바뀌었나?
이 레버 상태 바뀌었나?
이 퀘스트 오브젝트 상태 바뀌었나?
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 대부분은 평소에 상태가 바뀌지 않는다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;상자는 열기 전까지 상태 변화 없음
문은 누가 열기 전까지 상태 변화 없음
스위치는 누르기 전까지 상태 변화 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 Dormancy를 쓰면 평소에는 액터를 휴면 상태로 두고, 상태가 바뀔 때만 깨워서 복제할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;평소
&amp;rarr; Dormant 상태
&amp;rarr; 서버의 복제 검사 비용 감소

상태 변경 시
&amp;rarr; 깨움
&amp;rarr; 변경사항 복제
&amp;rarr; 다시 Dormant
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;17. NetDormancy 상태 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 휴면 상태에는 여러 종류가 있다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;DORM_Never
&amp;rarr; 절대 휴면되지 않음

DORM_Awake
&amp;rarr; 현재 휴면 상태가 아님. 복제 대상

DORM_DormantPartial
&amp;rarr; 일부 커넥션에 대해서만 휴면 상태
&amp;rarr; Deprecated

DORM_Initial
&amp;rarr; 휴면 상태로 시작하고 필요할 때 깨울 수 있음

DORM_DormantAll
&amp;rarr; 모든 커넥션에게 휴면 상태
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실전에서는 보통 다음 상태들을 많이 생각하면 된다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;DORM_Never
&amp;rarr; 플레이어 캐릭터처럼 항상 업데이트가 필요한 액터

DORM_Awake
&amp;rarr; 현재 복제 중인 상태

DORM_Initial
&amp;rarr; 맵에 배치되어 있고 처음에는 자주 안 바뀌는 액터

DORM_DormantAll
&amp;rarr; 모든 클라이언트에 대해 휴면 상태
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;18. NetDormancy를 쓰면 좋은 액터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetDormancy는 replicated가 필요하지만 자주 바뀌지는 않는 액터에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;문
상자
레버
스위치
엘리베이터
퀘스트 오브젝트
점령 지점
함정 장치
파괴 가능한 오브젝트
월드에 배치된 아이템
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 액터들은 클라이언트에게 상태가 전달되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 문은 열렸는지 닫혔는지 알아야 하고, 상자는 이미 열렸는지 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 매 Tick마다 바뀌는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Dormancy와 잘 맞는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;19. NetDormancy를 쓰면 안 좋은 액터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 자주 바뀌는 액터에는 Dormancy가 적합하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;플레이어 캐릭터
움직이는 몬스터
실시간 이동하는 투사체
차량
계속 회전하는 액터
움직이는 플랫폼
매 Tick 상태가 바뀌는 액터
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 액터는 위치, 회전, 애니메이션, 상태가 계속 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Dormant로 재워두면 계속 깨워야 해서 오히려 비효율적일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 앞에서 본 DXBox처럼 계속 회전하는 액터는 Dormancy보다 다음 방식이 더 자연스럽다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;NetUpdateFrequency 조절
클라이언트 보간 Lerp
클라이언트 예측
ReplicateMovement 조절
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 계속 움직이는 액터는 휴면시키기보다 복제 빈도를 조절하고 클라이언트에서 부드럽게 보이도록 처리하는 것이 더 적합하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;20. 문 액터에서 NetDormancy 사용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문 액터는 Dormancy를 사용하기 좋은 예시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문은 평소에는 거의 상태가 바뀌지 않는다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;닫혀 있음
아무도 건드리지 않음
계속 그대로 있음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 누군가 문을 열면 상태가 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;bIsOpen = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 모든 클라이언트에게 전달되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 문 액터는 다음처럼 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;ADoor::ADoor()
{
    bReplicates = true;

    // 맵에 배치된 문이라면 처음에는 휴면 상태로 시작
    NetDormancy = ENetDormancy::DORM_Initial;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문이 열릴 때는 다음처럼 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;void ADoor::OpenDoor()
{
    if (HasAuthority())
    {
        FlushNetDormancy();

        bIsOpen = true;

        ForceNetUpdate();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;문은 평소에 Dormant 상태
    &amp;darr;
플레이어가 문과 상호작용
    &amp;darr;
서버에서 OpenDoor 실행
    &amp;darr;
FlushNetDormancy로 휴면 상태 깨움
    &amp;darr;
bIsOpen = true
    &amp;darr;
ForceNetUpdate로 빠른 복제 요청
    &amp;darr;
클라이언트에서 OnRep_IsOpen 실행
    &amp;darr;
문 열림 애니메이션 재생
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;21. 상자 액터에서 NetDormancy 사용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보물상자도 Dormancy를 사용하기 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상자는 열리기 전까지 상태 변화가 거의 없다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;bIsOpened = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어가 상자를 열면 서버에서 상태를 바꾼다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;void AChest::OpenChest()
{
    if (HasAuthority())
    {
        FlushNetDormancy();

        bIsOpened = true;
        RewardItemId = 3;

        ForceNetUpdate();

        SetNetDormancy(ENetDormancy::DORM_DormantAll);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;상자 닫힘 상태로 맵에 존재
    &amp;darr;
평소에는 Dormant 상태
    &amp;darr;
서버가 계속 검사하지 않음
    &amp;darr;
플레이어가 상자 열기
    &amp;darr;
서버에서 bIsOpened 변경
    &amp;darr;
변경사항 복제
    &amp;darr;
클라이언트에서 상자 열림 애니메이션 재생
    &amp;darr;
다시 Dormant 상태 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상자는 한 번 열리면 다시 자주 바뀌지 않기 때문에 Dormancy와 잘 맞는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;22. FlushNetDormancy와 SetNetDormancy 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FlushNetDormancy()와 SetNetDormancy()는 모두 Dormant 액터를 다시 복제되게 만들 수 있지만, 용도가 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FlushNetDormancy()는 이번 변경사항을 한 번 보내고 싶을 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;FlushNetDormancy();

bIsOpen = true;

ForceNetUpdate();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;문 열림
상자 열림
스위치 눌림
레버 작동
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 SetNetDormancy(DORM_Awake)는 액터가 당분간 계속 바뀔 예정일 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;SetNetDormancy(ENetDormancy::DORM_Awake);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;엘리베이터가 5초 동안 움직임
문이 열리는 애니메이션 동안 여러 상태가 바뀜
움직이는 플랫폼이 작동 시작함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 상태 변화가 끝나면 다시 재울 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;SetNetDormancy(ENetDormancy::DORM_DormantAll);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;한 번만 변경사항 보내고 싶다
&amp;rarr; FlushNetDormancy()

당분간 계속 복제하고 싶다
&amp;rarr; SetNetDormancy(DORM_Awake)

다시 안 바뀐다
&amp;rarr; SetNetDormancy(DORM_DormantAll)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;23. 지금 배우는 내용이 최적화와 관련 있는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 내용들은 대부분 멀티플레이 네트워크 최적화와 관련이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 전부 최적화만은 아니고, 크게 두 가지로 나눌 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 네트워크 동기화 구조 이해
2. 네트워크 최적화
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 다음 내용들은 멀티플레이 동기화 기본기에 해당한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;RPC
Replicated 변수
OnRep 함수
RepNotify
Owner
Possess
GameMode / GameState / PlayerController 흐름
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것들은 서버와 클라이언트가 어떻게 데이터를 주고받는지 이해하기 위한 기본 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 다음 내용들은 최적화와 관련이 크다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;NetUpdateFrequency
NetUpdatePeriod
Client-side Lerp
NetDormancy
FlushNetDormancy
ForceNetUpdate
NetCullDistance
Relevancy
DOREPLIFETIME_CONDITION
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능들은 공통적으로 다음 질문과 연결된다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;서버가 얼마나 자주 보낼 것인가?
누구에게 보낼 것인가?
언제 안 보낼 것인가?
꼭 필요한 데이터만 보낼 수 있는가?
복제를 줄여도 부드럽게 보이게 할 수 있는가?
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 지금 배우는 내용은 단순히 복제가 되는지 안 되는지만 보는 것이 아니라, 서버 부담과 네트워크 사용량을 줄이면서도 클라이언트 화면에서는 자연스럽게 보이게 만드는 방법이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;24. 동기화와 최적화의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화는 서버와 클라이언트의 상태를 맞추는 것이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;서버에서 HP 변경
    &amp;darr;
클라이언트로 HP 복제
    &amp;darr;
클라이언트에서 OnRep_HP 호출
    &amp;darr;
UI 갱신
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 멀티플레이를 정상적으로 작동시키기 위한 기본 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 최적화는 필요한 정보만 효율적으로 보내는 것이다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;자주 안 바뀌는 액터는 재워둔다.
멀리 있는 액터는 안 보낸다.
필요한 클라이언트에게만 보낸다.
복제 빈도를 낮춘다.
클라이언트에서 보간해서 부드럽게 보이게 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;서버와 클라이언트를 맞추는 법
&amp;rarr; 동기화

그걸 덜 보내고도 잘 맞아 보이게 하는 법
&amp;rarr; 최적화
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 헷갈렸던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 헷갈렸던 부분은 상속과 인터페이스의 차이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 둘 다 공통 기능을 만들 때 쓰는 것처럼 보여서 비슷하게 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 상속은 부모 클래스의 기능과 속성을 물려받는 구조이고, 인터페이스는 서로 다른 클래스들이 같은 기능을 가지고 있다고 약속하는 구조였다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;상속
&amp;rarr; PlayerCharacter는 Character의 한 종류다.

인터페이스
&amp;rarr; Door, Chest, NPC는 모두 Interact할 수 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 NetUpdateFrequency가 1이면 정확히 1초마다 복제되는 것처럼 보였지만, 실제로는 최대 빈도일 뿐 보장값이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 Tick Rate, 서버 성능, 네트워크 대역폭, 액터 우선순위 등에 따라 실제 복제 타이밍은 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 DXBox 코드에서 서버가 1초에 한 번만 회전값을 보내는데도 클라이언트에서 부드럽게 회전해 보이는 이유는, 클라이언트가 RotationSpeed와 NetUpdatePeriod를 이용해서 다음 회전값을 예상하고 Lerp로 보간하기 때문이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 NetDormancy는 복제를 더 빠르게 하는 기능이 아니라, 자주 바뀌지 않는 액터의 복제 검사를 줄이는 최적화 기능이라는 점이 중요했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘의 핵심 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;상속은 부모 클래스의 기능과 속성을 자식 클래스가 물려받는 구조다.&lt;/li&gt;
&lt;li&gt;상속은 &quot;~의 한 종류다&quot; 관계로 이해하면 쉽다.&lt;/li&gt;
&lt;li&gt;AMyCharacter : public ACharacter는 내 캐릭터가 ACharacter의 기능을 물려받는다는 뜻이다.&lt;/li&gt;
&lt;li&gt;인터페이스는 특정 기능을 가지고 있다고 약속하는 구조다.&lt;/li&gt;
&lt;li&gt;인터페이스는 &quot;~할 수 있다&quot; 관계로 이해하면 쉽다.&lt;/li&gt;
&lt;li&gt;문, 상자, NPC, 아이템이 모두 상호작용 가능하다면 BPI_Interactable 인터페이스를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;상속은 공통 기능과 데이터를 물려받고 싶을 때 사용한다.&lt;/li&gt;
&lt;li&gt;인터페이스는 서로 다른 클래스를 같은 방식으로 호출하고 싶을 때 사용한다.&lt;/li&gt;
&lt;li&gt;NetUpdateFrequency는 액터가 최대 얼마나 자주 복제될 수 있는지 정하는 값이다.&lt;/li&gt;
&lt;li&gt;NetUpdateFrequency는 정확한 복제 주기를 보장하지 않는다.&lt;/li&gt;
&lt;li&gt;서버 Tick Rate, 서버 성능, 네트워크 대역폭에 따라 실제 복제 빈도는 달라질 수 있다.&lt;/li&gt;
&lt;li&gt;ForceNetUpdate()는 다음 네트워크 업데이트 때 빠르게 복제되도록 요청하는 함수다.&lt;/li&gt;
&lt;li&gt;ForceNetUpdate()도 완전한 즉시 전송 보장은 아니다.&lt;/li&gt;
&lt;li&gt;상태값은 Replicated 변수와 OnRep으로 관리하는 것이 적합하다.&lt;/li&gt;
&lt;li&gt;한 번만 발생하는 이벤트는 RPC가 적합하다.&lt;/li&gt;
&lt;li&gt;DXBox 코드는 서버가 회전값을 낮은 빈도로 보내고, 클라이언트에서 Lerp로 부드럽게 보이게 하는 구조다.&lt;/li&gt;
&lt;li&gt;AccDeltaSecondSinceReplicated는 마지막 복제 이후 지난 시간을 누적하는 변수다.&lt;/li&gt;
&lt;li&gt;LerpRatio는 현재 보간이 얼마나 진행되었는지 나타내는 비율이다.&lt;/li&gt;
&lt;li&gt;NextServerRotationYaw는 클라이언트가 예상한 다음 서버 회전값이다.&lt;/li&gt;
&lt;li&gt;OnRep_ServerRotationYaw()에서는 새 서버 회전값을 받았으므로 시간을 0으로 초기화한다.&lt;/li&gt;
&lt;li&gt;NetDormancy는 자주 안 바뀌는 액터를 휴면 상태로 두어 복제 검사 비용을 줄이는 최적화 기능이다.&lt;/li&gt;
&lt;li&gt;DORM_Initial은 처음부터 휴면 상태로 시작하고 필요할 때 깨울 수 있는 상태다.&lt;/li&gt;
&lt;li&gt;DORM_DormantAll은 모든 클라이언트 연결에 대해 휴면 상태인 상태다.&lt;/li&gt;
&lt;li&gt;문, 상자, 레버처럼 가끔만 상태가 바뀌는 액터는 Dormancy와 잘 맞는다.&lt;/li&gt;
&lt;li&gt;플레이어 캐릭터, 움직이는 몬스터, 투사체처럼 자주 바뀌는 액터는 Dormancy에 적합하지 않다.&lt;/li&gt;
&lt;li&gt;FlushNetDormancy()는 휴면 상태 액터의 변경사항을 복제할 수 있게 깨우는 함수다.&lt;/li&gt;
&lt;li&gt;SetNetDormancy(DORM_Awake)는 액터가 당분간 계속 바뀔 예정일 때 깨워두는 용도로 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;SetNetDormancy(DORM_DormantAll)은 상태 변화가 끝난 뒤 다시 휴면 상태로 만들 때 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;멀티플레이에서 동기화는 서버와 클라이언트의 상태를 맞추는 것이다.&lt;/li&gt;
&lt;li&gt;멀티플레이에서 최적화는 필요한 정보만 효율적으로 보내는 것이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 오늘 배운 내용은 단순히 네트워크 복제를 사용하는 방법을 넘어서, &lt;b&gt;어떤 액터를 얼마나 자주 복제할지, 언제 복제를 쉬게 할지, 복제가 적어도 클라이언트 화면에서는 어떻게 부드럽게 보이게 할지&lt;/b&gt;를 이해하는 과정이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 멀티플레이 네트워크에서는 모든 것을 자주 보내는 것이 정답이 아니라, 필요한 데이터만 적절한 타이밍에 보내고 나머지는 클라이언트 보간이나 Dormancy 같은 방식으로 최적화하는 것이 중요하다.&lt;/p&gt;</description>
      <category>unreal 8기</category>
      <author>왕건</author>
      <guid isPermaLink="true">https://talk87604.tistory.com/57</guid>
      <comments>https://talk87604.tistory.com/57#entry57comment</comments>
      <pubDate>Fri, 19 Jun 2026 09:04:02 +0900</pubDate>
    </item>
    <item>
      <title>2026-6-17 TIL</title>
      <link>https://talk87604.tistory.com/56</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIL: Unreal Engine C++ OnRep 함수와 Blueprint RepNotify 사용 기준 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 배운 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Unreal Engine 멀티플레이에서 C++ OnRep_함수와 Blueprint RepNotify 함수를 언제 사용하면 좋은지 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 서버에서 변경된 replicated 변수 값을 클라이언트가 받았을 때 특정 함수를 실행할 수 있게 해주는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로 사용할 때는 성격이 조금 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;게임 규칙에 영향을 주는 중요한 상태값
&amp;rarr; C++ OnRep 함수

UI, 애니메이션, 이펙트처럼 화면 표현 중심의 값
&amp;rarr; Blueprint RepNotify 함수

한 번만 발생하는 알림이나 이벤트
&amp;rarr; RPC
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 무조건 RepNotify를 쓰는 것이 아니라 이 값이 계속 유지되어야 하는 상태인지, 아니면 한 번만 발생하는 이벤트인지 먼저 구분해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. C++ OnRep 함수의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++에서 OnRep_함수는 replicated 변수가 서버에서 변경되고, 그 값이 클라이언트로 복제되었을 때 자동으로 호출되는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 HP 값을 복제하고 싶다면 다음처럼 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;UPROPERTY(ReplicatedUsing = OnRep_HP)
float HP;

UFUNCTION()
void OnRep_HP();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 OnRep_HP() 함수 안에서 HP가 변경되었을 때 실행할 처리를 작성한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;void AMyCharacter::OnRep_HP()
{
    UpdateHPBar();
    PlayHitEffect();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;서버에서 HP 변경
    &amp;darr;
HP 값이 클라이언트로 복제됨
    &amp;darr;
클라이언트에서 OnRep_HP() 자동 호출
    &amp;darr;
HP UI 갱신 또는 피격 이펙트 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, C++ OnRep 함수는 서버에서 바뀐 상태를 클라이언트가 받았을 때 그 상태에 맞는 후처리를 실행하는 용도로 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. C++ OnRep 함수의 중요한 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++ OnRep 함수는 기본적으로 클라이언트에서 자동 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 값을 변경했다고 해서 서버에서도 OnRep 함수가 자동으로 호출되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 서버에서 데미지를 받아 HP를 줄였다고 하면,&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void AMyCharacter::ApplyDamage(float Damage)
{
    if (HasAuthority())
    {
        HP -= Damage;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 HP 값은 클라이언트로 복제되고, 클라이언트에서는 OnRep_HP()가 자동 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서버에서도 같은 처리가 필요하다면 직접 호출해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void AMyCharacter::ApplyDamage(float Damage)
{
    if (HasAuthority())
    {
        HP -= Damage;

        // 서버에서도 HP 변경 처리가 필요하다면 직접 호출
        OnRep_HP();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;서버
HP 변경
    &amp;darr;
필요하면 OnRep_HP() 직접 호출

클라이언트
HP 값 복제 받음
    &amp;darr;
OnRep_HP() 자동 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 C++ OnRep을 사용할 때 중요한 포인트다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. C++ OnRep을 쓰면 좋은 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++ OnRep 함수는 게임의 핵심 로직과 연결된 상태값에 사용하기 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;HP
MP
사망 여부
무기 장착 상태
탄약 수
팀 정보
점수
게임 진행 상태
캐릭터 상태
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 값들은 단순히 화면에 보여주기만 하는 값이 아니라 게임 규칙과 직접 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 HP가 0이 되면 캐릭터가 죽어야 하고, 무기가 바뀌면 공격 방식이나 애니메이션도 바뀔 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이런 중요한 상태는 Blueprint보다 C++에서 명확하게 관리하는 것이 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. HP 변경 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HP는 대표적으로 C++ OnRep을 쓰기 좋은 값이다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;UPROPERTY(ReplicatedUsing = OnRep_HP)
float HP;

UFUNCTION()
void OnRep_HP();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;void AMyCharacter::OnRep_HP()
{
    UpdateHPBar();

    if (HP &amp;lt;= 0)
    {
        PlayDeathAnimation();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;서버에서 데미지 계산
    &amp;darr;
HP 값 변경
    &amp;darr;
변경된 HP가 클라이언트로 복제됨
    &amp;darr;
클라이언트에서 OnRep_HP() 실행
    &amp;darr;
HP바 갱신
    &amp;darr;
HP가 0 이하라면 사망 애니메이션 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HP는 현재 값이 계속 유지되어야 하는 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이런 값은 RepNotify 방식이 잘 맞는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 무기 장착 상태 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무기 장착 상태도 C++ OnRep을 쓰기 좋은 예시다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;UPROPERTY(ReplicatedUsing = OnRep_CurrentWeapon)
AWeapon* CurrentWeapon;

UFUNCTION()
void OnRep_CurrentWeapon();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;void AMyCharacter::OnRep_CurrentWeapon()
{
    AttachWeaponMesh();
    UpdateWeaponUI();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 현재 무기를 변경하면, 클라이언트는 복제된 CurrentWeapon 값을 받고 OnRep_CurrentWeapon()을 실행한다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;서버에서 CurrentWeapon 변경
    &amp;darr;
CurrentWeapon 값이 클라이언트로 복제됨
    &amp;darr;
클라이언트에서 OnRep_CurrentWeapon() 호출
    &amp;darr;
무기 메시 부착
    &amp;darr;
무기 UI 갱신
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무기 장착은 단순한 화면 표시가 아니라 공격 가능 여부, 애니메이션, 충돌, 소켓 부착 등과 연결될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이런 기능은 C++ OnRep으로 관리하는 것이 더 안정적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Blueprint RepNotify 함수의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blueprint에서도 변수를 RepNotify로 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blueprint 변수에서 Replication 설정을 RepNotify로 바꾸면, 해당 변수 값이 변경되었을 때 자동으로 Notify 함수가 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 bIsDoorOpen이라는 변수를 RepNotify로 만들면 다음처럼 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;bIsDoorOpen 값 변경
    &amp;darr;
RepNotify 함수 실행
    &amp;darr;
문 열림 또는 닫힘 애니메이션 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blueprint RepNotify는 C++보다 빠르게 구현할 수 있고, 시각적인 처리에 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 UI, 애니메이션, 사운드, 이펙트처럼 게임 규칙보다 표현에 가까운 기능에 잘 맞는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Blueprint RepNotify를 쓰면 좋은 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blueprint RepNotify는 화면 표현이나 간단한 반응 처리에 사용하기 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;문 열림 / 닫힘 애니메이션
불 켜짐 / 꺼짐
캐릭터 머리 위 이름표 표시
Ready 아이콘 표시
닉네임 색상 변경
간단한 사운드 출력
간단한 이펙트 재생
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 기능들은 게임의 핵심 규칙보다는 플레이어에게 보여주는 표현에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Blueprint에서 빠르게 처리해도 충분한 경우가 많다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 문 열림 상태 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문이 열렸는지 닫혔는지를 나타내는 bIsDoorOpen 변수가 있다고 하면, Blueprint에서 RepNotify로 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;bIsDoorOpen = true
    &amp;darr;
RepNotify 실행
    &amp;darr;
문 열림 애니메이션 재생
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 값이 false가 되면 문 닫힘 애니메이션을 재생할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;bIsDoorOpen = false
    &amp;darr;
RepNotify 실행
    &amp;darr;
문 닫힘 애니메이션 재생
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우는 C++로 복잡하게 처리하지 않아도 Blueprint RepNotify로 충분히 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 문이 실제로 길을 막는 충돌 상태까지 바뀐다면 서버에서 상태를 관리하고 복제해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 문 애니메이션은 Blueprint에서 처리할 수 있지만, 문이 열렸는지 닫혔는지에 대한 최종 판단은 서버 기준으로 처리하는 것이 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 플레이어 Ready 표시 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티플레이 대기방에서 플레이어가 준비 완료 상태가 되었을 때도 Blueprint RepNotify를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 bIsReady 변수를 RepNotify로 설정한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;서버에서 bIsReady 변경
    &amp;darr;
클라이언트로 bIsReady 복제
    &amp;darr;
RepNotify 실행
    &amp;darr;
Ready 아이콘 표시
    &amp;darr;
닉네임 색상 변경
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능은 게임 규칙 자체보다는 UI 표시와 관련이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Blueprint RepNotify로 처리하기 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. C++ OnRep과 Blueprint RepNotify 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 기능은 비슷해 보이지만 실제 사용 기준은 다르다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;C++ OnRep 함수
&amp;rarr; 핵심 게임 상태를 안정적으로 관리할 때 사용

Blueprint RepNotify 함수
&amp;rarr; UI, 애니메이션, 이펙트처럼 표현 중심 처리에 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표로 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분C++ OnRep 함수Blueprint RepNotify 함수&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;주요 용도&lt;/td&gt;
&lt;td&gt;핵심 게임 로직&lt;/td&gt;
&lt;td&gt;화면 표현, UI, 애니메이션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;호출 위치&lt;/td&gt;
&lt;td&gt;주로 클라이언트에서 자동 호출&lt;/td&gt;
&lt;td&gt;서버와 클라이언트 모두에서 호출될 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;직접 호출&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;일반적으로 직접 호출보다는 변수 변경에 반응&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예시&lt;/td&gt;
&lt;td&gt;HP, 무기, 점수, 사망 상태&lt;/td&gt;
&lt;td&gt;문 애니메이션, Ready 표시, 이펙트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장점&lt;/td&gt;
&lt;td&gt;안정적이고 명확함&lt;/td&gt;
&lt;td&gt;빠르게 구현 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추천 상황&lt;/td&gt;
&lt;td&gt;나중에 확장될 중요한 시스템&lt;/td&gt;
&lt;td&gt;간단한 시각 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 나중에 시스템이 커질 가능성이 있는 중요한 기능은 C++ OnRep으로 만드는 것이 좋고, 빠르게 화면 표현을 처리하고 싶은 기능은 Blueprint RepNotify를 사용하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11. RepNotify와 RPC의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 중요한 기준은 RepNotify와 RPC의 차이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RepNotify는 상태값에 적합하다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;HP = 100
HP = 80
HP = 50
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 값은 현재 값이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어가 나중에 들어와도 현재 HP가 얼마인지 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이런 값은 RepNotify로 관리하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 RPC는 한 번 발생하는 이벤트에 적합하다.&lt;/p&gt;
&lt;pre class=&quot;smalltalk&quot;&gt;&lt;code&gt;&quot;Player2 has joined the game.&quot;
&quot;공격 버튼을 눌렀다.&quot;
&quot;채팅 메시지를 보냈다.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 것은 계속 유지되는 상태라기보다 특정 순간에 발생하는 이벤트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이런 경우는 RepNotify보다 RPC가 더 자연스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;계속 유지되어야 하는 값
&amp;rarr; RepNotify

한 번만 발생하는 행동이나 알림
&amp;rarr; RPC
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12. 접속 알림 기능에 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 만들고 있는 접속 알림 기능은 RepNotify보다 RPC가 더 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 클라이언트2가 접속했을 때 클라이언트1 화면에 다음 메시지를 띄우고 싶다고 하면,&lt;/p&gt;
&lt;pre class=&quot;smalltalk&quot;&gt;&lt;code&gt;&quot;Player2 has joined the game.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메시지는 계속 유지되어야 하는 상태가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 출력되면 되는 이벤트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 접속 알림은 다음과 같은 흐름이 적합하다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;클라이언트 접속
    &amp;darr;
서버 GameMode::PostLogin()에서 접속 감지
    &amp;darr;
GameState 또는 PlayerController를 통해 클라이언트들에게 RPC 전달
    &amp;darr;
각 클라이언트 UI에 접속 메시지 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 접속 알림은 상태 동기화가 목적이 아니라 순간 이벤트 전달이 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 RepNotify보다 RPC 구조가 더 알맞다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13. 상태와 이벤트 구분하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티플레이 기능을 만들 때 가장 먼저 구분해야 할 것은 상태인지 이벤트인지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태는 계속 유지되는 값이다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;HP
점수
팀
현재 무기
사망 여부
준비 완료 여부
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 값은 현재 상태가 중요하기 때문에 RepNotify가 잘 맞는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 이벤트는 특정 순간에 한 번 발생하는 동작이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;채팅 메시지 전송
접속 알림 출력
공격 요청
스킬 사용 요청
효과음 재생 요청
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 것은 RPC가 잘 맞는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;상태(State)
&amp;rarr; RepNotify

이벤트(Event)
&amp;rarr; RPC
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기준을 잡으면 네트워크 구조를 훨씬 깔끔하게 설계할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14. 실제 게임 제작 시 판단 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 게임을 만들 때는 다음 기준으로 선택하면 된다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;게임 규칙에 영향을 주는 값인가?
&amp;rarr; C++ OnRep

화면에 보여주는 표현인가?
&amp;rarr; Blueprint RepNotify

한 번만 발생하는 알림이나 행동인가?
&amp;rarr; RPC
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 HP는 게임 규칙에 영향을 준다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;HP가 0이 되면 죽음
데미지 계산과 연결됨
UI와도 연결됨
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 C++ OnRep이 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문 열림 애니메이션은 표현에 가깝다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;문이 열리는 모습을 보여줌
사운드나 애니메이션 재생
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Blueprint RepNotify로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접속 알림은 한 번 발생하는 이벤트다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;플레이어가 들어온 순간 메시지를 출력
계속 유지될 필요 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 RPC가 적합하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 헷갈렸던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 헷갈렸던 부분은 C++ OnRep과 Blueprint RepNotify가 둘 다 비슷해 보인다는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 값이 바뀌었을 때 함수를 실행한다는 점에서는 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 사용하는 목적이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++ OnRep은 HP, 무기, 점수처럼 게임 규칙과 연결되는 중요한 상태값에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Blueprint RepNotify는 문 애니메이션, Ready 아이콘, 이펙트처럼 화면 표현을 빠르게 처리할 때 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 접속 알림처럼 &amp;ldquo;OOO has joined the game.&amp;rdquo;을 출력하는 기능은 RepNotify보다 RPC가 더 적합하다는 점도 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 접속 알림도 RepNotify로 처리할 수 있을 것 같았지만, 접속 알림은 계속 유지되는 값이 아니라 한 번 발생하는 이벤트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 상태와 이벤트를 먼저 구분하는 것이 중요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘의 핵심 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;C++ OnRep 함수는 replicated 변수가 클라이언트로 복제되었을 때 호출되는 함수다.&lt;/li&gt;
&lt;li&gt;C++ OnRep은 보통 클라이언트에서 자동 호출된다.&lt;/li&gt;
&lt;li&gt;서버에서도 같은 처리가 필요하면 OnRep 함수를 직접 호출해야 한다.&lt;/li&gt;
&lt;li&gt;HP, MP, 사망 여부, 무기 장착 상태처럼 중요한 게임 상태는 C++ OnRep이 적합하다.&lt;/li&gt;
&lt;li&gt;무기 장착 상태는 공격, 애니메이션, 충돌, 소켓 부착과 연결될 수 있으므로 C++에서 관리하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;Blueprint RepNotify는 Blueprint 변수 값이 변경되었을 때 Notify 함수를 실행하는 기능이다.&lt;/li&gt;
&lt;li&gt;Blueprint RepNotify는 UI, 애니메이션, 이펙트처럼 화면 표현 중심 기능에 적합하다.&lt;/li&gt;
&lt;li&gt;문 열림 애니메이션, 불 켜짐, Ready 아이콘 표시 같은 기능은 Blueprint RepNotify로 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;RepNotify는 계속 유지되어야 하는 상태값에 적합하다.&lt;/li&gt;
&lt;li&gt;RPC는 한 번만 발생하는 이벤트에 적합하다.&lt;/li&gt;
&lt;li&gt;HP, 점수, 팀, 현재 무기처럼 현재 값이 중요한 것은 RepNotify로 관리하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;접속 알림, 채팅 메시지, 공격 요청처럼 순간적으로 발생하는 것은 RPC가 더 적합하다.&lt;/li&gt;
&lt;li&gt;접속 알림은 상태 동기화가 아니라 이벤트 전달이 목적이므로 RPC 구조가 자연스럽다.&lt;/li&gt;
&lt;li&gt;멀티플레이 기능을 만들 때는 먼저 상태인지 이벤트인지 구분해야 한다.&lt;/li&gt;
&lt;li&gt;상태라면 RepNotify를 고려하고, 이벤트라면 RPC를 고려한다.&lt;/li&gt;
&lt;li&gt;게임 규칙에 영향을 주는 값은 C++ OnRep으로 관리하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;화면 표현만 바꾸는 값은 Blueprint RepNotify로 빠르게 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;중요한 네트워크 구조일수록 C++에서 명확하게 관리하는 것이 좋다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 C++ OnRep과 Blueprint RepNotify는 둘 다 값 변경에 반응하는 기능이지만, 사용하는 목적이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++ OnRep은 게임의 핵심 상태를 동기화하고 후처리할 때 적합하고, Blueprint RepNotify는 UI나 애니메이션처럼 화면 표현을 처리할 때 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 접속 알림이나 채팅처럼 한 번 발생하는 기능은 RepNotify보다 RPC를 사용하는 것이 더 자연스럽다.&lt;/p&gt;</description>
      <category>unreal 8기</category>
      <author>왕건</author>
      <guid isPermaLink="true">https://talk87604.tistory.com/56</guid>
      <comments>https://talk87604.tistory.com/56#entry56comment</comments>
      <pubDate>Wed, 17 Jun 2026 21:13:00 +0900</pubDate>
    </item>
    <item>
      <title>2026-6-16 TIL</title>
      <link>https://talk87604.tistory.com/55</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIL: Unreal Engine 네트워크 초기화 흐름과 NetLoadOnClient 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 배운 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Unreal Engine 멀티플레이에서 GameState의 BeginPlay 동기화 흐름, PlayerController가 Pawn을 Possess하는 과정, Owner 복제, PostNetInit, 그리고 NetLoadOnClient 속성의 의미를 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 서버와 클라이언트가 같은 시점에 모든 객체를 동시에 가지는 것이 아니라, 서버에서 먼저 상태를 만들고 그 상태가 복제를 통해 클라이언트에 전달된다는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 레벨에 미리 배치된 액터와 게임 중 서버가 Spawn하는 액터는 네트워크 설정을 다르게 봐야 한다는 점도 중요했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. HandleBeginPlay()의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandleBeginPlay()는 GameState에서 게임 시작 상태를 처리하는 함수다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;virtual void HandleBeginPlay() override;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 서버에서 GameMode가 게임 시작을 처리할 때 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;서버에서 게임 시작
    &amp;darr;
GameMode::StartPlay()
    &amp;darr;
GameState::HandleBeginPlay()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandleBeginPlay()는 서버 기준으로 &amp;ldquo;게임이 시작되었다&amp;rdquo;는 상태를 GameState에 반영한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 bReplicatedHasBegunPlay 같은 값이 설정되고, 클라이언트로 복제될 준비가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, HandleBeginPlay()는 서버가 BeginPlay 시작 상태를 확정하는 단계라고 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. OnRep_ReplicatedHasBegunPlay()의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OnRep_ReplicatedHasBegunPlay()는 서버에서 설정된 BeginPlay 상태가 클라이언트에 복제되었을 때 호출되는 함수다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;virtual void OnRep_ReplicatedHasBegunPlay() override;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 bReplicatedHasBegunPlay 값이 바뀌고 이 값이 클라이언트로 복제되면, 클라이언트에서 OnRep_ReplicatedHasBegunPlay()가 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;서버에서 bReplicatedHasBegunPlay 설정
    &amp;darr;
값이 클라이언트로 복제됨
    &amp;darr;
클라이언트에서 OnRep_ReplicatedHasBegunPlay() 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 클라이언트는 이 함수를 통해 &amp;ldquo;서버에서 게임이 시작되었구나&amp;rdquo;라는 사실을 전달받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 HandleBeginPlay()는 서버에서 게임 시작을 처리하는 함수이고, OnRep_ReplicatedHasBegunPlay()는 클라이언트가 서버의 게임 시작 상태를 복제받아 반응하는 함수다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. BeginPlay 동기화 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 클라이언트의 BeginPlay 흐름은 다음과 같이 진행된다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;서버
GameMode::StartPlay()
    &amp;darr;
GameState::HandleBeginPlay()
    &amp;darr;
bReplicatedHasBegunPlay 설정
    &amp;darr;
WorldSettings::NotifyBeginPlay()
    &amp;darr;
서버 월드 액터들의 BeginPlay 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 StartPlay()를 통해 게임 시작을 시작하고, GameState의 HandleBeginPlay()를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 안에서 BeginPlay가 시작되었다는 상태를 기록하고, 서버 월드의 액터들에게 BeginPlay를 실행시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 해당 상태가 클라이언트로 복제된다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;클라이언트
bReplicatedHasBegunPlay 값 복제 받음
    &amp;darr;
GameState::OnRep_ReplicatedHasBegunPlay()
    &amp;darr;
WorldSettings::NotifyBeginPlay()
    &amp;darr;
클라이언트 월드 액터들의 BeginPlay 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 서버에서 복제된 값을 보고 자신의 월드에서도 BeginPlay를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 서버와 클라이언트는 직접 동시에 BeginPlay를 시작하는 것이 아니라, 서버가 먼저 시작 상태를 만들고 클라이언트가 그 상태를 복제받아 따라가는 구조다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 서버와 클라이언트의 BeginPlay 흐름 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 클라이언트를 나누어 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;[서버]                                [클라이언트]

GameMode::StartPlay()
    &amp;darr;
GameState::HandleBeginPlay()
    &amp;darr;
bReplicatedHasBegunPlay = true
    &amp;darr;
WorldSettings::NotifyBeginPlay()
    &amp;darr;
서버 액터 BeginPlay 실행
                                      &amp;darr;
                              bReplicatedHasBegunPlay 복제
                                      &amp;darr;
                              GameState::OnRep_ReplicatedHasBegunPlay()
                                      &amp;darr;
                              WorldSettings::NotifyBeginPlay()
                                      &amp;darr;
                              클라이언트 액터 BeginPlay 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 서버는 HandleBeginPlay()로 BeginPlay 시작을 결정하고, 클라이언트는 OnRep_ReplicatedHasBegunPlay()로 그 사실을 전달받는다는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. PlayerController와 Character의 Possess 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어가 접속하면 서버에서는 PlayerController가 생성되고, 이후 Pawn 또는 Character를 Possess한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;서버: PreLogin
    &amp;darr;
서버: Login
    &amp;darr;
서버: PlayerController 생성
    &amp;darr;
서버: PostLogin
    &amp;darr;
서버: Character 생성
    &amp;darr;
서버: PlayerController::OnPossess()
    &amp;darr;
서버: Character::PossessedBy()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OnPossess()는 PlayerController가 어떤 Pawn 또는 Character를 소유하게 될 때 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PossessedBy()는 Pawn 또는 Character 입장에서 자신이 어떤 Controller에게 Possess 되었는지 알게 되는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, PlayerController 쪽에서는 OnPossess()가 호출되고, Character 쪽에서는 PossessedBy()가 호출된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. PossessedBy()와 Owner 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PossessedBy()는 서버에서 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 PlayerController가 Character를 Possess하면 Character의 Owner 관련 정보가 설정된다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;서버에서 PlayerController::OnPossess()
    &amp;darr;
서버에서 Character::PossessedBy()
    &amp;darr;
서버에서 Character Owner 설정
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 Character가 생성되었을 때는 Owner가 아직 없을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Possess 과정에서 Owner가 설정되고, 이 Owner 정보가 클라이언트로 복제된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Owner는 서버에서 정하고 클라이언트는 복제를 통해 그 결과를 전달받는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. OnRep_Owner()의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OnRep_Owner()는 서버에서 변경된 Owner 정보가 클라이언트에 복제되었을 때 호출되는 함수다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;서버에서 Character Owner 변경
    &amp;darr;
Owner 속성이 클라이언트로 복제됨
    &amp;darr;
클라이언트에서 OnRep_Owner() 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 OnRep_Owner()를 통해 이 액터의 Owner가 누구인지 알게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Character의 Owner가 특정 PlayerController로 설정되면, 클라이언트는 복제된 Owner 값을 통해 해당 Character가 누구에게 소유되었는지 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, OnRep_Owner()는 클라이언트가 서버에서 정한 Owner 정보를 전달받고 반영하는 함수다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11. PostNetInit()의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostNetInit()은 액터의 초기 네트워크 복제가 끝난 뒤 호출되는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크로 생성되거나 복제되는 액터는 처음부터 모든 정보를 가지고 있는 것이 아니라, 서버에서 필요한 초기 정보들이 클라이언트로 복제되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 초기 복제가 끝나면 PostNetInit()이 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;서버에서 액터 생성
    &amp;darr;
액터 정보가 클라이언트로 복제됨
    &amp;darr;
Owner 같은 초기 속성 복제
    &amp;darr;
클라이언트에서 PostNetInit() 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 Owner 속성도 복제 대상이기 때문에, Owner 정보가 복제된 뒤 PostNetInit 흐름과 연결될 수 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 클라이언트는 서버에서 만들어진 액터의 기본 네트워크 정보를 받은 뒤 초기화 과정을 진행한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12. BeginPlay 흐름과 Possess 흐름을 합쳐서 보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 본 로그를 합치면 두 가지 흐름이 함께 진행된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 게임 시작 / BeginPlay 동기화 흐름
2. 플레이어 접속 / Possess / Owner 복제 흐름
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같이 정리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;[서버 월드 시작]
GameMode 생성
GameState 생성
    &amp;darr;
GameMode::StartPlay()
    &amp;darr;
GameState::HandleBeginPlay()
    &amp;darr;
bReplicatedHasBegunPlay 설정
    &amp;darr;
WorldSettings::NotifyBeginPlay()
    &amp;darr;
서버 월드 액터 BeginPlay 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 플레이어 접속 흐름이 진행된다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;[플레이어 접속]
PreLogin
    &amp;darr;
Login
    &amp;darr;
PlayerController 생성
    &amp;darr;
PostLogin
    &amp;darr;
Character 생성
    &amp;darr;
PlayerController::OnPossess()
    &amp;darr;
Character::PossessedBy()
    &amp;darr;
Character Owner 설정
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 정보들이 클라이언트로 복제된다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;[클라이언트 복제]
bReplicatedHasBegunPlay 복제
    &amp;darr;
GameState::OnRep_ReplicatedHasBegunPlay()
    &amp;darr;
클라이언트 BeginPlay 실행

Owner 복제
    &amp;darr;
Character::OnRep_Owner()
    &amp;darr;
PostNetInit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 서버는 게임 시작 상태와 Owner 정보를 만들고, 클라이언트는 그 정보를 복제받아 자신의 월드에서 초기화를 진행한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13. NetLoadOnClient 속성의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetLoadOnClient는 레벨에 미리 배치된 액터를 클라이언트도 맵 로드 시 함께 로드할지 결정하는 속성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;이 액터가 레벨에 배치되어 있을 때
클라이언트도 이 액터를 같이 로드할 것인가?
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 액터 정보를 서버가 모든 클라이언트에게 복제하는 것은 비효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 레벨 디자인을 통해 고정적으로 배치되는 액터는 NetLoadOnClient를 true로 지정해서 클라이언트가 스스로 로드하도록 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;레벨에 미리 배치되어 있고
모든 클라이언트가 알아야 하는 액터
&amp;rarr; NetLoadOnClient = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 서버에서만 필요한 액터라면 클라이언트가 로드할 필요가 없다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;서버만 알아도 되는 액터
&amp;rarr; NetLoadOnClient = false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14. NetLoadOnClient를 true로 둘 액터 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetLoadOnClient를 true로 두면 좋은 액터는 클라이언트가 맵을 볼 때 반드시 존재해야 하는 액터다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;벽
바닥
나무
바위
건물
울타리
가로등
문
엘리베이터
스위치
레버
버튼
보물상자
퀘스트 오브젝트
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 액터들은 맵에 미리 배치되어 있고 모든 플레이어가 볼 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 문, 레버, 버튼, 상자처럼 상호작용이 필요한 액터는 클라이언트에도 존재해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 상태가 변하는 액터라면 NetLoadOnClient만으로는 부족하고 Replicates도 함께 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 문이 열리고 닫히는 상태를 모든 클라이언트가 알아야 한다면 다음처럼 생각할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;문 액터
NetLoadOnClient = true
Replicates = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 바위나 벽처럼 고정되어 있고 상태가 변하지 않는 장식물은 다음처럼 둘 수 있다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;바위 액터
NetLoadOnClient = true
Replicates = false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;15. NetLoadOnClient를 false로 둘 수 있는 액터 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 직접 볼 필요가 없고 서버에서만 로직을 처리하면 되는 액터는 NetLoadOnClient를 false로 둘 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;몬스터 스폰 매니저
라운드 매니저
점수 계산 매니저
게임 규칙 처리 액터
서버 판정용 트리거
아이템 스폰 구역
보스방 입장 감지 트리거
퀘스트 지역 진입 체크 트리거
숨겨진 트랩 로직 액터
랜덤 보상 위치 계산 액터
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 액터들은 클라이언트가 직접 알 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서만 계산하고 결과만 필요한 경우가 많기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 몬스터 스폰 매니저는 서버에서 몬스터를 언제 어디에 생성할지만 결정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 이 매니저 액터를 직접 가지고 있을 필요는 없다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;서버 전용 스폰 매니저
NetLoadOnClient = false
Replicates = false
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;16. NetLoadOnClient와 Replicates의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetLoadOnClient와 Replicates는 서로 다른 개념이다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;NetLoadOnClient
= 맵 로드 시 클라이언트도 이 액터를 로드할지 결정

Replicates
= 서버에서 변경된 액터 상태를 클라이언트에게 동기화할지 결정
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 레벨에 배치된 문이 있다고 하면,&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;NetLoadOnClient = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 클라이언트도 맵을 로드할 때 문 액터를 가지게 한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문이 열렸는지 닫혔는지 같은 상태 변화까지 자동으로 동기화되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 변화까지 서버에서 클라이언트로 보내려면 다음 설정도 필요하다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Replicates = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 둘은 다음처럼 구분해야 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;클라이언트도 액터를 가지고 있어야 한다
&amp;rarr; NetLoadOnClient

서버에서 바뀐 상태를 클라이언트도 알아야 한다
&amp;rarr; Replicates
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;17. 게임 중 SpawnActor로 생성되는 액터는 다르게 봐야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetLoadOnClient는 주로 레벨에 미리 배치된 액터와 관련이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 게임 중 서버가 SpawnActor로 생성하는 액터는 NetLoadOnClient보다 Replicates 설정이 더 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 플레이어 캐릭터는 보통 맵에 미리 배치되는 것이 아니다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;플레이어 접속
    &amp;darr;
서버가 Pawn 또는 Character Spawn
    &amp;darr;
PlayerController가 Possess
    &amp;darr;
복제를 통해 클라이언트에 전달
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 다음 설정이 더 중요하다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;Replicates = true
Replicate Movement = true
Owner 설정
Possess 흐름
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총알, 투사체, 아이템처럼 게임 중 생성되는 액터도 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 생성한 액터를 클라이언트에게 보여줘야 한다면 해당 액터가 복제되어야 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;게임 중 생성되는 액터
&amp;rarr; NetLoadOnClient보다 Replicates를 먼저 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;18. 실제 게임 제작 시 판단 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임을 만들 때 모든 액터마다 무조건 NetLoadOnClient를 바꿀 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분은 기본값을 사용하고, 특별히 서버 전용으로만 필요한 액터인지 판단하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기준은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;플레이어 눈에 보여야 한다
&amp;rarr; NetLoadOnClient = true

맵에 고정적으로 배치되어 있다
&amp;rarr; NetLoadOnClient = true

모든 클라이언트가 위치를 알아야 한다
&amp;rarr; NetLoadOnClient = true

서버만 계산하면 된다
&amp;rarr; NetLoadOnClient = false

클라이언트가 몰라도 되는 로직 액터다
&amp;rarr; NetLoadOnClient = false

게임 중 서버가 SpawnActor로 생성한다
&amp;rarr; Replicates를 먼저 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 NetLoadOnClient는 모든 액터마다 고민하는 옵션이라기보다, 레벨에 미리 배치된 액터를 클라이언트도 로드해야 하는지 판단할 때 사용하는 옵션이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 헷갈렸던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 헷갈렸던 부분은 서버와 클라이언트에서 BeginPlay가 어떻게 동기화되는지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 서버와 클라이언트가 각자 BeginPlay를 바로 실행한다고 생각할 수 있었지만, 실제로는 서버에서 bReplicatedHasBegunPlay 상태를 만들고 그 값이 클라이언트로 복제되면서 클라이언트의 BeginPlay 흐름이 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 Possess 과정에서도 서버가 먼저 Character를 Possess하고 Owner를 설정한 뒤, 그 Owner 정보가 클라이언트로 복제되어 OnRep_Owner()가 호출된다는 점이 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetLoadOnClient도 처음에는 모든 액터에 설정해야 하는 옵션처럼 보였지만, 실제로는 레벨에 미리 배치된 액터를 클라이언트도 로드할지 결정하는 옵션이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 게임 중 서버가 생성하는 액터는 NetLoadOnClient보다 Replicates 설정을 먼저 확인해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘의 핵심 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;HandleBeginPlay()는 서버에서 GameState의 BeginPlay 시작 상태를 처리하는 함수다.&lt;/li&gt;
&lt;li&gt;OnRep_ReplicatedHasBegunPlay()는 클라이언트가 서버의 BeginPlay 상태를 복제받았을 때 호출된다.&lt;/li&gt;
&lt;li&gt;서버는 GameMode::StartPlay()에서 GameState::HandleBeginPlay()를 호출한다.&lt;/li&gt;
&lt;li&gt;서버는 bReplicatedHasBegunPlay를 설정하고 서버 월드의 BeginPlay를 실행한다.&lt;/li&gt;
&lt;li&gt;클라이언트는 bReplicatedHasBegunPlay를 복제받아 OnRep_ReplicatedHasBegunPlay()를 실행한다.&lt;/li&gt;
&lt;li&gt;클라이언트는 이 흐름을 통해 자신의 월드 액터 BeginPlay를 실행한다.&lt;/li&gt;
&lt;li&gt;PlayerController가 Pawn 또는 Character를 소유할 때 서버에서 OnPossess()가 호출된다.&lt;/li&gt;
&lt;li&gt;Character 입장에서는 서버에서 PossessedBy()가 호출된다.&lt;/li&gt;
&lt;li&gt;서버에서 설정된 Owner 정보는 클라이언트로 복제된다.&lt;/li&gt;
&lt;li&gt;클라이언트는 Owner 복제를 받으면 OnRep_Owner()를 호출한다.&lt;/li&gt;
&lt;li&gt;PostNetInit()은 액터의 초기 네트워크 복제가 끝난 뒤 호출되는 초기화 단계다.&lt;/li&gt;
&lt;li&gt;NetLoadOnClient는 레벨에 배치된 액터를 클라이언트도 맵 로드 시 함께 로드할지 결정한다.&lt;/li&gt;
&lt;li&gt;모든 클라이언트가 봐야 하는 레벨 배치 액터는 NetLoadOnClient = true가 적절하다.&lt;/li&gt;
&lt;li&gt;서버에서만 필요한 매니저나 판정용 액터는 NetLoadOnClient = false로 둘 수 있다.&lt;/li&gt;
&lt;li&gt;NetLoadOnClient는 액터 존재 여부와 관련 있고, Replicates는 상태 동기화와 관련 있다.&lt;/li&gt;
&lt;li&gt;게임 중 서버가 SpawnActor로 생성하는 액터는 NetLoadOnClient보다 Replicates 설정이 더 중요하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 서버는 게임 시작 상태, Possess, Owner 같은 중요한 네트워크 상태를 먼저 만들고 클라이언트는 그 정보를 복제받아 자신의 초기화 흐름을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 레벨에 미리 배치된 액터는 NetLoadOnClient로 클라이언트 로드 여부를 판단하고, 게임 중 생성되거나 상태가 바뀌는 액터는 Replicates를 함께 고려해야 한다.&lt;/p&gt;</description>
      <category>unreal 8기</category>
      <author>왕건</author>
      <guid isPermaLink="true">https://talk87604.tistory.com/55</guid>
      <comments>https://talk87604.tistory.com/55#entry55comment</comments>
      <pubDate>Tue, 16 Jun 2026 20:27:42 +0900</pubDate>
    </item>
    <item>
      <title>2026-6-15 TIL</title>
      <link>https://talk87604.tistory.com/54</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIL: Unreal Engine Server RPC 검증과 Reliable RPC 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 배운 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Unreal Engine 멀티플레이에서 Server RPC에 사용하는 WithValidation, _Implementation(), _Validate(), Reliable RPC의 의미를 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 RPC가 단순히 함수를 호출하는 것이 아니라, 네트워크를 통해 서버와 클라이언트 사이에서 실행 위치와 검증 과정을 구분한다는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Server RPC는 클라이언트가 서버에게 요청을 보내는 구조이기 때문에, 서버 입장에서는 이 요청이 정상적인지 확인할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 사용자가 조작할 수 있는 영역이기 때문에 서버는 클라이언트가 보낸 값을 그대로 믿으면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Server RPC에서는 요청을 검증하는 과정과 실제 실행하는 과정을 구분해서 이해해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Server RPC에서 WithValidation을 사용하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WithValidation은 Server RPC에서 클라이언트의 요청을 서버가 실행하기 전에 검사하기 위해 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 서버에게 어떤 요청을 보낼 때, 그 요청이 항상 정상적인 값이라고 보장할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 클라이언트가 비정상적인 값이나 조작된 요청을 서버로 보낼 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Server RPC에 WithValidation을 붙이면 서버가 RPC 실행 전에 해당 요청이 정상적인지 검증할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;UFUNCTION(Server, Reliable, WithValidation)
void ServerRPCPrintChatMessageString(
    const FString&amp;amp; InChatMessageString
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 선언하면 서버 RPC는 검증 함수와 실제 실행 함수로 나뉘어 처리된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;클라이언트가 Server RPC 호출
    &amp;darr;
서버에서 _Validate() 실행
    &amp;darr;
검증 결과가 true이면 통과
    &amp;darr;
_Implementation() 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 WithValidation은 RPC 실행 여부를 검증하고 위변조를 방지하기 위한 장치라고 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. WithValidation이 붙은 RPC는 두 함수로 나뉜다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WithValidation이 붙은 Server RPC는 구현부에서 두 개의 함수로 나뉜다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;_Validate()
_Implementation()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 Server RPC가 있다고 하면,&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;UFUNCTION(Server, Reliable, WithValidation)
void ServerRPCPrintChatMessageString(
    const FString&amp;amp; InChatMessageString
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPP에서는 다음 두 함수를 작성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;bool ACXPlayerController::
ServerRPCPrintChatMessageString_Validate(
    const FString&amp;amp; InChatMessageString)
{
    return true;
}

void ACXPlayerController::
ServerRPCPrintChatMessageString_Implementation(
    const FString&amp;amp; InChatMessageString)
{
    // 실제 서버에서 실행될 코드
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 _Validate()는 실행 가능 여부를 검사하는 함수이고, _Implementation()은 검사를 통과한 뒤 실제로 실행되는 함수다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. _Validate()의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_Validate()는 클라이언트가 보낸 RPC 요청이 정상적인지 서버에서 검사하는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환 타입은 bool이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;bool ACXPlayerController::
ServerRPCPrintChatMessageString_Validate(
    const FString&amp;amp; InChatMessageString)
{
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return true;를 하면 서버가 이 요청을 정상 요청으로 판단하고 _Implementation()을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 return false;를 하면 서버는 이 요청을 비정상 요청으로 판단하고 실행하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;return true
-&amp;gt; 정상 요청
-&amp;gt; _Implementation() 실행

return false
-&amp;gt; 비정상 요청
-&amp;gt; _Implementation() 실행 안 함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 채팅 메시지가 너무 길면 거부하고 싶을 때는 다음처럼 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;bool ACXPlayerController::
ServerRPCPrintChatMessageString_Validate(
    const FString&amp;amp; InChatMessageString)
{
    return InChatMessageString.Len() &amp;lt;= 100;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 메시지 길이가 100자 이하일 때만 서버 RPC 실행을 허용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, _Validate()는 서버 RPC의 검문소 같은 역할을 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. _Implementation()의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_Implementation()은 검증을 통과한 RPC가 실제로 실행되는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server RPC라면 이 함수는 서버에서 실행된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void ACXPlayerController::
ServerRPCPrintChatMessageString_Implementation(
    const FString&amp;amp; InChatMessageString)
{
    // 서버에서 실제로 실행할 코드
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 클라이언트가 채팅 메시지를 서버로 보내면, 서버는 _Implementation() 안에서 그 메시지를 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;void ACXPlayerController::
ServerRPCPrintChatMessageString_Implementation(
    const FString&amp;amp; InChatMessageString)
{
    for (TActorIterator&amp;lt;ACXPlayerController&amp;gt; It(GetWorld());
         It;
         ++It)
    {
        ACXPlayerController* CXPlayerController = *It;

        if (IsValid(CXPlayerController))
        {
            CXPlayerController
                -&amp;gt;ClientRPCPrintChatMessageString(
                    InChatMessageString
                );
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 서버가 모든 PlayerController를 순회하면서 각 클라이언트에게 메시지를 보내는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 _Implementation()은 RPC의 실제 실행부다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. _Validate()와 _Implementation()의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 함수의 차이는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;_Validate()
- RPC 요청을 실행해도 되는지 검사
- bool 값을 반환함
- true이면 실행 허용
- false이면 실행 거부

_Implementation()
- 검증을 통과한 뒤 실제로 실행되는 함수
- RPC의 본문 역할
- 실제 서버 로직을 작성하는 곳
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;클라이언트가 Server RPC 호출
    &amp;darr;
서버에서 _Validate() 실행
    &amp;darr;
true 반환
    &amp;darr;
서버에서 _Implementation() 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 _Validate()는 입장 검사이고, _Implementation()은 실제 작업이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Reliable RPC의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reliable RPC는 원격 PC에서 반드시 실행되어야 하는 중요한 로직에 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 채팅 메시지, 아이템 사용, 게임 결과 처리처럼 누락되면 안 되는 요청은 Reliable로 보내는 것이 적절하다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;UFUNCTION(Server, Reliable)
void ServerRPCSendChatMessage(
    const FString&amp;amp; Message
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reliable은 네트워크 상황이 좋지 않아도 해당 RPC가 전달되도록 보장하려는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 한두 번 누락되어도 큰 문제가 없는 이펙트, 사운드, 짧은 애니메이션 효과 같은 것은 Unreliable을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;Reliable
- 중요한 RPC
- 반드시 전달되어야 하는 요청
- 채팅, 아이템 사용, 게임 결과 처리 등에 사용

Unreliable
- 조금 누락되어도 괜찮은 RPC
- 이펙트, 사운드, 빈번한 위치 갱신 등에 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Reliable RPC는 원격 PC에서 반드시 실행되어야 하는 로직에 사용하는 것이 맞다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Reliable을 무조건 많이 쓰면 안 되는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reliable은 반드시 전달되어야 하는 RPC에 사용하지만, 모든 RPC를 Reliable로 만드는 것은 좋지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reliable RPC가 너무 많이 쌓이면 네트워크 큐에 부담이 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요하지 않은 이펙트나 반복적인 상태 갱신까지 모두 Reliable로 보내면 오히려 네트워크 성능에 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Reliable과 Unreliable은 로직의 중요도에 따라 구분해서 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;반드시 실행되어야 한다
-&amp;gt; Reliable

조금 빠져도 괜찮다
-&amp;gt; Unreliable
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅처럼 사용자가 입력한 메시지는 누락되면 안 되므로 Reliable이 적절하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 총알 이펙트나 발자국 소리처럼 순간적으로 지나가는 효과는 Unreliable도 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 헷갈렸던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 헷갈렸던 부분은 _Implementation()과 _Validate()의 역할이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 둘 다 RPC 실행과 관련된 함수라서 차이가 명확하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 정리해보면 _Validate()는 실행 전에 요청을 검사하는 함수이고, _Implementation()은 검증을 통과한 뒤 실제로 실행되는 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 Reliable RPC는 무조건 좋은 것이 아니라, 반드시 실행되어야 하는 중요한 로직에만 사용하는 것이 적절하다는 점도 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server RPC는 클라이언트가 서버에게 요청을 보내는 구조이기 때문에, 서버는 클라이언트의 요청을 그대로 신뢰하지 않고 검증할 수 있어야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘의 핵심 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;WithValidation은 Server RPC 요청을 서버에서 검증하기 위해 사용한다.&lt;/li&gt;
&lt;li&gt;Server RPC는 클라이언트가 서버에게 요청을 보내는 구조다.&lt;/li&gt;
&lt;li&gt;서버는 클라이언트 요청을 무조건 믿으면 안 된다.&lt;/li&gt;
&lt;li&gt;WithValidation이 붙은 RPC는 _Validate()와 _Implementation()으로 나뉜다.&lt;/li&gt;
&lt;li&gt;_Validate()는 RPC 요청을 실행해도 되는지 검사한다.&lt;/li&gt;
&lt;li&gt;_Validate()가 true를 반환하면 _Implementation()이 실행된다.&lt;/li&gt;
&lt;li&gt;_Validate()가 false를 반환하면 RPC 실행이 거부된다.&lt;/li&gt;
&lt;li&gt;_Implementation()은 RPC의 실제 실행부다.&lt;/li&gt;
&lt;li&gt;Reliable RPC는 원격 PC에서 반드시 실행되어야 하는 로직에 사용한다.&lt;/li&gt;
&lt;li&gt;모든 RPC를 Reliable로 만들면 네트워크에 부담이 될 수 있다.&lt;/li&gt;
&lt;li&gt;누락되면 안 되는 요청은 Reliable을 사용한다.&lt;/li&gt;
&lt;li&gt;조금 누락되어도 괜찮은 이펙트나 사운드는 Unreliable을 사용할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 Server RPC에서는 요청을 검증하는 단계와 실제 실행하는 단계를 구분해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 네트워크 RPC는 중요도에 따라 Reliable과 Unreliable을 구분해서 사용해야 한다.&lt;/p&gt;</description>
      <category>unreal 8기</category>
      <author>왕건</author>
      <guid isPermaLink="true">https://talk87604.tistory.com/54</guid>
      <comments>https://talk87604.tistory.com/54#entry54comment</comments>
      <pubDate>Mon, 15 Jun 2026 20:25:34 +0900</pubDate>
    </item>
    <item>
      <title>2026-6-11 TIL</title>
      <link>https://talk87604.tistory.com/53</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;TIL: Unreal Engine 접속 알림과 NetMulticast RPC 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 배운 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Unreal Engine 멀티플레이에서 플레이어가 접속했을 때 모든 클라이언트에 알림을 출력하는 흐름을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 GameMode, GameState, PostLogin, NetMulticast RPC의 역할을 구분하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 GameMode에서 바로 Multicast RPC를 호출하면 될 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 GameMode는 서버에만 존재하기 때문에 클라이언트에 직접 알림을 전달하는 용도로는 적절하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 서버에서 접속을 감지하는 역할은 GameMode가 맡고, 모든 클라이언트에 알림을 전달하는 역할은 GameState가 맡는 구조로 구현해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. GameMode는 서버에서 접속을 감지한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어가 서버에 접속하면 GameMode의 PostLogin()이 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GameMode는 서버에만 존재하는 클래스이기 때문에 플레이어 접속, 게임 규칙, 승패 판정과 같은 서버 권한 로직을 처리하기 좋다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void ACXGameModeBase::PostLogin(APlayerController* NewPlayer)
{
    Super::PostLogin(NewPlayer);

    // 새 플레이어 접속 처리
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 GameMode가 클라이언트에 복제되지 않는다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 GameMode에서 플레이어 접속을 감지할 수는 있지만, GameMode 자체를 이용해 클라이언트 화면에 직접 알림을 출력하는 것은 적절하지 않다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. GameState는 모든 클라이언트에 복제된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GameState는 게임 전체의 상태를 서버와 모든 클라이언트가 공유할 때 사용하는 클래스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 점수, 제한 시간, 라운드 상태, 전체 공지와 같은 정보는 GameState에서 관리하기 좋다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;GameMode
- 서버에만 존재
- 플레이어 접속 감지
- 게임 규칙 처리

GameState
- 서버와 모든 클라이언트에 존재
- 게임 전체 상태 전달
- 전체 공지와 접속 알림 처리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 접속 알림을 모든 클라이언트에 보여주고 싶다면 GameMode에서 접속을 감지한 뒤, GameState의 NetMulticast RPC를 호출하는 구조가 적절하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 접속 알림의 전체 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접속 알림은 다음과 같은 순서로 처리된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;플레이어 접속
    &amp;darr;
GameMode::PostLogin() 호출
    &amp;darr;
서버가 GameState를 가져옴
    &amp;darr;
GameState의 NetMulticast RPC 호출
    &amp;darr;
서버와 모든 클라이언트에서 RPC 실행
    &amp;darr;
각 플레이어 화면에 접속 알림 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GameMode에서는 GetGameState()를 이용해 현재 GameState를 가져온 뒤 Multicast RPC를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void ACXGameModeBase::PostLogin(APlayerController* NewPlayer)
{
    Super::PostLogin(NewPlayer);

    ACXGameStateBase* CXGameStateBase =
        GetGameState&amp;lt;ACXGameStateBase&amp;gt;();

    if (IsValid(CXGameStateBase))
    {
        CXGameStateBase-&amp;gt;MulticastRPCBroadcastLoginMessage(
            TEXT(&quot;XXXXXXX&quot;)
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 새로운 플레이어가 접속했을 때 서버에서 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 GameState에 선언된 MulticastRPCBroadcastLoginMessage()를 호출해 모든 클라이언트에 접속 사실을 전달한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. NetMulticast RPC는 서버에서 호출해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetMulticast RPC는 서버가 호출하면 서버와 현재 연결된 모든 관련 클라이언트에서 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더 파일에는 UFUNCTION의 NetMulticast 지정자를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;UFUNCTION(NetMulticast, Reliable)
void MulticastRPCBroadcastLoginMessage(
    const FString&amp;amp; InNameString
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPP 파일의 구현 함수에는 함수 이름 뒤에 _Implementation을 붙인다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void ACXGameStateBase::
MulticastRPCBroadcastLoginMessage_Implementation(
    const FString&amp;amp; InNameString)
{
    const FString NotificationString =
        InNameString + TEXT(&quot; has joined the game.&quot;);

    // 각 플레이어 화면에 메시지 출력
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 NetMulticast RPC를 서버에서 호출해야 한다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;서버에서 NetMulticast 호출
-&amp;gt; 서버와 모든 관련 클라이언트에서 실행

클라이언트에서 NetMulticast 호출
-&amp;gt; 호출한 클라이언트에서만 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 직접 NetMulticast RPC를 호출하면 다른 클라이언트에 전달되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 모든 플레이어에게 알려야 하는 이벤트는 서버가 Multicast RPC를 호출해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Listen Server에서 HasAuthority()가 문제가 된 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 가장 헷갈렸던 부분은 다음 조건이었다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;if (HasAuthority() == false)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 클라이언트 화면에서만 메시지를 출력하기 위해 이 조건을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Listen Server 환경에서는 첫 번째 플레이어 창이 서버이면서 동시에 실제 플레이어 화면을 가진다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;첫 번째 창
- Listen Server
- 서버 권한 있음
- HasAuthority() == true
- 플레이어 화면도 존재함

두 번째 창
- Client
- 서버 권한 없음
- HasAuthority() == false
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 클라이언트가 접속하면 GameState의 NetMulticast RPC는 첫 번째 Listen Server 창에서도 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음 조건이 있으면 첫 번째 창은 출력 코드를 실행하지 못한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;if (HasAuthority() == false)
{
    // 메시지 출력
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Listen Server는 HasAuthority()가 true이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 두 번째 클라이언트가 접속해도 첫 번째 Listen Server 화면에는 접속 알림이 나타나지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. HasAuthority() 조건을 제거해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 Listen Server 화면에도 접속 알림을 표시하려면 다음 조건을 제거해야 한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;if (HasAuthority() == false)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Multicast RPC는 이미 서버와 모든 클라이언트에서 각각 실행되기 때문에, 각 실행 환경에서 로컬 PlayerController를 가져와 메시지를 출력하면 된다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;void ACXGameStateBase::
MulticastRPCBroadcastLoginMessage_Implementation(
    const FString&amp;amp; InNameString)
{
    APlayerController* PC =
        UGameplayStatics::GetPlayerController(GetWorld(), 0);

    if (IsValid(PC))
    {
        ACXPlayerController* CXPC =
            Cast&amp;lt;ACXPlayerController&amp;gt;(PC);

        if (IsValid(CXPC))
        {
            const FString NotificationString =
                InNameString
                + TEXT(&quot; has joined the game.&quot;);

            CXPC-&amp;gt;PrintChatMessageString(
                NotificationString
            );
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 수정하면 새로운 클라이언트가 접속했을 때 기존 Listen Server 플레이어의 화면에도 접속 메시지가 출력된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Dedicated Server만 제외해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dedicated Server는 플레이어 화면이 없는 전용 서버다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Dedicated Server에서는 PrintString이나 UI 출력 코드를 실행할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dedicated Server만 제외하려면 HasAuthority()가 아니라 GetNetMode()를 확인하는 것이 정확하다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (GetNetMode() == NM_DedicatedServer)
{
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Listen Server는 서버 권한을 가지면서 동시에 플레이어 화면도 있으므로 출력 대상에 포함해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정된 전체 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;void ACXGameStateBase::
MulticastRPCBroadcastLoginMessage_Implementation(
    const FString&amp;amp; InNameString)
{
    if (GetNetMode() == NM_DedicatedServer)
    {
        return;
    }

    APlayerController* PC =
        UGameplayStatics::GetPlayerController(
            GetWorld(),
            0
        );

    if (!IsValid(PC))
    {
        return;
    }

    ACXPlayerController* CXPC =
        Cast&amp;lt;ACXPlayerController&amp;gt;(PC);

    if (!IsValid(CXPC))
    {
        return;
    }

    const FString NotificationString =
        InNameString
        + TEXT(&quot; has joined the game.&quot;);

    CXPC-&amp;gt;PrintChatMessageString(
        NotificationString
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 Dedicated Server만 화면 출력을 하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Listen Server와 일반 클라이언트에서는 각각 자신의 로컬 PlayerController를 찾아 접속 알림을 출력한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. GetPlayerController(0)의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드는 현재 실행 중인 컴퓨터의 첫 번째 로컬 PlayerController를 가져온다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;UGameplayStatics::GetPlayerController(
    GetWorld(),
    0
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 0은 서버에 접속한 전체 플레이어의 번호가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 컴퓨터에서 생성된 로컬 플레이어의 인덱스다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Listen Server 컴퓨터
GetPlayerController(0)
-&amp;gt; Listen Server 플레이어의 로컬 PlayerController

Client 1 컴퓨터
GetPlayerController(0)
-&amp;gt; Client 1의 로컬 PlayerController

Client 2 컴퓨터
GetPlayerController(0)
-&amp;gt; Client 2의 로컬 PlayerController
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Multicast RPC가 각 컴퓨터에서 실행되기 때문에, 각 컴퓨터에서 GetPlayerController(0)을 호출하면 해당 컴퓨터의 로컬 플레이어 컨트롤러를 가져올 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. GameMode와 GameState 클래스 설정도 확인해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++ 코드를 작성하더라도 실제 맵에서 다른 GameMode나 GameState를 사용하고 있으면 작성한 코드가 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월드 세팅에서 다음 항목을 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;GameMode Override
-&amp;gt; BP_GameModeBase

Player Controller Class
-&amp;gt; BP_PlayerController

Game State Class
-&amp;gt; BP_GameStateBase
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Blueprint 클래스의 부모 클래스도 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;BP_GameModeBase
-&amp;gt; Parent Class: CXGameModeBase

BP_GameStateBase
-&amp;gt; Parent Class: CXGameStateBase

BP_PlayerController
-&amp;gt; Parent Class: CXPlayerController
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 클래스가 기본 GameModeBase나 GameStateBase로 설정되어 있다면 C++에서 작성한 PostLogin과 Multicast RPC가 실행되지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. Client RPC로도 전체 알림을 구현할 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 클라이언트에 알림을 전달하는 방법이 NetMulticast RPC만 있는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 모든 PlayerController를 순회하면서 각 PlayerController의 Client RPC를 호출하는 방법도 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GameMode::PostLogin()
    &amp;darr;
서버의 모든 PlayerController 순회
    &amp;darr;
각 PlayerController에서 Client RPC 호출
    &amp;darr;
각 PlayerController의 소유 클라이언트에서 실행
    &amp;darr;
각 클라이언트 화면에 알림 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;for (TActorIterator&amp;lt;ACXPlayerController&amp;gt; It(GetWorld());
     It;
     ++It)
{
    ACXPlayerController* CXPlayerController = *It;

    if (IsValid(CXPlayerController))
    {
        CXPlayerController
            -&amp;gt;ClientRPCPrintChatMessageString(
                NotificationString
            );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 특정 클라이언트에만 메시지를 보내거나, 플레이어마다 다른 메시지를 보여줄 때 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 모든 플레이어에게 같은 일회성 알림을 전달하는 경우에는 GameState의 NetMulticast RPC가 더 간단하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘 헷갈렸던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 HasAuthority()가 false인 곳만 플레이어 화면을 가진 클라이언트라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Listen Server에서는 첫 번째 창이 서버 권한을 가지면서 동시에 플레이어 화면도 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 화면 출력 여부를 판단할 때 HasAuthority()만 사용하면 Listen Server 플레이어가 출력 대상에서 제외될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 GameMode는 플레이어 접속을 감지하기 좋은 위치지만 클라이언트에 복제되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 GameMode가 접속을 감지하고, 모든 클라이언트에 복제되는 GameState가 NetMulticast RPC를 통해 알림을 전달하는 구조가 적절하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오늘의 핵심 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GameMode는 서버에만 존재한다.&lt;/li&gt;
&lt;li&gt;플레이어 접속은 GameMode의 PostLogin()에서 감지할 수 있다.&lt;/li&gt;
&lt;li&gt;GameState는 서버와 모든 클라이언트에 복제된다.&lt;/li&gt;
&lt;li&gt;모든 플레이어에게 같은 알림을 보내려면 GameState의 NetMulticast RPC를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;NetMulticast RPC는 서버에서 호출해야 모든 클라이언트에 전달된다.&lt;/li&gt;
&lt;li&gt;클라이언트에서 호출한 NetMulticast RPC는 다른 클라이언트에 전달되지 않는다.&lt;/li&gt;
&lt;li&gt;Listen Server는 서버 권한을 가지면서 플레이어 화면도 가진다.&lt;/li&gt;
&lt;li&gt;Listen Server의 HasAuthority()는 true다.&lt;/li&gt;
&lt;li&gt;HasAuthority() == false 조건을 사용하면 Listen Server 화면에서 메시지가 출력되지 않을 수 있다.&lt;/li&gt;
&lt;li&gt;화면이 없는 전용 서버만 제외하려면 GetNetMode() == NM_DedicatedServer를 확인해야 한다.&lt;/li&gt;
&lt;li&gt;Multicast RPC 안에서 GetPlayerController(0)을 호출하면 각 컴퓨터의 첫 번째 로컬 PlayerController를 가져올 수 있다.&lt;/li&gt;
&lt;li&gt;특정 클라이언트에 개별 메시지를 보내려면 PlayerController의 Client RPC를 사용할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 플레이어 접속 알림은 GameMode에서 접속을 감지하고, GameState의 NetMulticast RPC를 서버에서 호출해 모든 플레이어에게 전달하는 구조로 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Listen Server 환경에서는 서버 권한을 가지고 있다는 이유만으로 화면 출력 대상에서 제외하면 안 된다는 점을 반드시 고려해야 한다.&lt;/p&gt;</description>
      <category>unreal 8기</category>
      <author>왕건</author>
      <guid isPermaLink="true">https://talk87604.tistory.com/53</guid>
      <comments>https://talk87604.tistory.com/53#entry53comment</comments>
      <pubDate>Thu, 11 Jun 2026 18:56:47 +0900</pubDate>
    </item>
  </channel>
</rss>