Sparta Unreal 부트캠프 82일차

정찬호·2026년 3월 30일

TIL - 2026.03.30

오늘 학습한 것
1. 코딩 테스트 - 큰 수 만들기 (미완)
2. Challenge 분반 — GA_Dodge 무적 구르기 구현
3. SetByCaller 제대로 알아보기
4. 인강 마지막 챕터 — 언리얼 네트워크 이론 정리


1. 코딩 테스트 — 프로그래머스 큰 수 만들기

문제 분석

number에서 k개의 문자를 제거했을 때 얻을 수 있는 가장 큰 수를 구하는 문제입니다. 가능한 경우를 전부 탐색해야 하니 DFS나 BFS가 적합해 보였고, 좀 더 간단하게 느껴지는 DFS로 접근했습니다.

실패 코드 (시간 초과)

처음에는 제거할 인덱스를 하나씩 선택하며 DFS로 모든 경우를 탐색하고 unordered_set에 담는 방식으로 접근했습니다.

void DFS(unordered_set<string>& outSet, string number, int k, int loopCnt, int startIdx)
{
    if (k == loopCnt)
    {
        outSet.insert(number);
        return;
    }
    else if (number.length() <= startIdx)
    {
        return;
    }

    for (int i = startIdx; i < number.length(); i++)
    {
        string newNumber = number.substr(0, i);
        newNumber += number.substr(i + 1, number.length() - i - 1);
        DFS(outSet, newNumber, k, loopCnt + 1, i);
    }
}

string solution(string number, int k) {
    string answer = "";
    unordered_set<string> answerSet;
    DFS(answerSet, number, k, 0, 0);

    answer = *(answerSet.begin());
    for (const string& num : answerSet)
    {
        if (answer < num) answer = num;
    }
    return answer;
}

최대 100만 자짜리 입력을 너무 얕봤네요. 시간 초과가 약 70% 발생했습니다. 이 방식은 완전 탐색이라 number의 길이가 길어질수록 지수적으로 경우의 수가 폭발합니다.

⏰ 시간이 너무 지나버려서 내일 이어서 풀기로 했습니다. 그리디 방식으로 접근해야 할 것 같습니다.


2. Challenge 분반 — GA_Dodge 무적 구르기 구현

구현 목표

  • 구르기 애니메이션 재생
  • 구르는 방향으로 캐릭터 회전
  • 구르는 동안 무적 처리 (I-Frame)
  • 스테미나 소모

애니메이션은 이전에 사용했던 Mixamo_Converter로 변환했습니다.

완성 결과:


ANS 이름 고민

무적 구간을 담당하는 AnimNotifyState의 이름을 무엇으로 할지 고민했습니다.

  • ANS_IFrames: 격투 게임이나 소울라이크에서 흔히 부르는 "I-Frame(Invincibility Frame)" 표현을 쓴 것
  • ANS_ApplyTag: 좀 더 범용적이고 재사용성이 좋은 이름

처음에는 ANS_IFrames로 결정했다가, 이쪽이 범용성과 재사용성이 더 좋다는 판단에 ANS_ApplyTag 로 변경했습니다.


무적 처리 (I-Frame)

State.Invincible 태그를 가지고 있을 때 데미지를 무시하도록 UExecCalc_Damage::Execute_Implementation에 아래 코드를 추가했습니다.

if (TargetASC->HasMatchingGameplayTag(
    FGameplayTag::RequestGameplayTag(FName("State.Invinsible"))))
{
    UE_LOG(LogTemp, Log, TEXT("[State.Invinsible] : No Damage!"));
    return;
}

이 태그는 Dodge 몽타주에서 ANS_ApplyTag를 통해 부여/제거됩니다. ANS_ComboWindow의 코드 구조를 참고해서 동일하게 AddLooseGameplayTag / RemoveLooseGameplayTag 방식으로 구현했습니다.

void UANS_ComboWindow::NotifyBegin(...)
{
    if (UAbilitySystemComponent* ASC = GetASC(MeshComp))
    {
        if (ComboWindowTag.IsValid())
        {
            ASC->AddLooseGameplayTag(ComboWindowTag);
        }
    }
}

void UANS_ComboWindow::NotifyEnd(...)
{
    if (UAbilitySystemComponent* ASC = GetASC(MeshComp))
    {
        if (ComboWindowTag.IsValid())
        {
            ASC->RemoveLooseGameplayTag(ComboWindowTag);
        }
    }
}

테스트에 사용된 트랩 액터는 BP로 구현되어 있어 아래와 같이 블루프린트에서 State.Invincible 태그 여부를 체크합니다.


방향 전환

GA_Dodge::ActivateAbility에서 몽타주 재생 전에 마지막 입력 방향을 바라보도록 구현했습니다.

TWeakObjectPtr<AActor> TargetActor = ActorInfo->AvatarActor;
if (TargetActor.IsValid() == true)
{
    if (UCharacterMovementComponent* MoveComp =
        Cast<UCharacterMovementComponent>(
            TargetActor->GetComponentByClass(UCharacterMovementComponent::StaticClass())))
    {
        FVector InputVector = MoveComp->GetLastInputVector();
        TargetActor->SetActorRotation(InputVector.Rotation());
    }
}

GetLastInputVector()는 마지막으로 입력받은 이동 방향을 반환합니다. 뒤늦게 눈치챈 것이 입력이 없으면 제로 벡터가 반환되니, 추후에 제로 벡터 체크를 추가해야 겠습니다.


스테미나 소모

스테미나 소모는 두 곳에서 처리했습니다.

ActivateAbility — 소모 적용

FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(StaminaCostEffectClass, 1.0f);
if (SpecHandle.IsValid())
{
    SpecHandle.Data->SetSetByCallerMagnitude(
        FGameplayTag::RequestGameplayTag(FName("Data.Stamina.Cost")),
        -DodgeStaminaCost  // 음수로 소모
    );
    ApplyGameplayEffectSpecToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, SpecHandle);
}

CanActivateAbility — 스테미나 체크

bool UGA_Dodge::CanActivateAbility(...)
{
    if (!Super::CanActivateAbility(...)) return false;

    UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get();
    if (!ASC) return false;

    const UMyAttributeSet* AttributeSet = ASC->GetSet<UMyAttributeSet>();
    if (!AttributeSet) return false;

    float CurrentStamina = AttributeSet->GetStamina();
    if (CurrentStamina < DodgeStaminaCost)
    {
        UE_LOG(LogTemp, Warning, TEXT("[Dodge] Not enough Stamina! (%.1f / %.1f)"),
            CurrentStamina, DodgeStaminaCost);
        return false;
    }

    return true;
}

3. SetByCaller 제대로 알아보기

PoisonTrap의 데미지를 SetByCaller로 바꾸려다 예상보다 잘 되지 않아 제대로 파고들어 봤습니다. 그 과정에서 Gemini 할루시네이션으로 시간을 많이 날렸어요... 이미지와 로그를 건네주면서 방법을 알려달라니까 엉뚱한 말만 반복하다가, 나중에 다시 물어보니 처음부터 불가하다고 말을 바꾸네요.

SetByCaller의 설계 의도

SetByCaller의 기본값은 SpecHandle 단위로 적용된다.

MakeOutgoingSpec()으로 Spec을 생성할 때마다 SetSetByCallerMagnitude()로 값을 지정해야 합니다. 게임 전체에 적용되는 기본값 같은 건 없습니다.

SetByCaller의 설계 의도는 "Effect를 적용하는 시점에 호출자(Caller)가 값을 직접 지정한다" 는 것입니다. Effect는 그냥 "이 태그에 해당하는 값을 써라"라는 틀만 가지고 있고, 실제 값은 SpecHandle 생성 후 외부에서 채워 넣는 구조입니다.

💡 고정값을 사용할 거면 ScalableFloat를 쓰는 것이 훨씬 적합합니다. 오기가 도져서 계속 매달렸지만 결국 ScalableFloat로 되돌렸습니다.


4. 인강 마지막 챕터 — 언리얼 네트워크 이론

C++로 구현했던 ChatX 프로젝트를 블루프린트로 구현하는 내용이었습니다. 이론은 이전에 설명했던 내용이어서 복습하는 마음으로 수강했네요.


서버의 종류

종류설명예시
P2P (Peer to Peer)각각의 컴퓨터가 클라이언트이자 서버인 구조다크소울
Listen Server클라이언트이자 서버인 방장(Host)이 있고, 나머지는 클라이언트만 담당. P2P의 일종마인크래프트, 어몽어스
Dedicated Server온전히 서버 역할만 하는 서버. 클라이언트 로직을 수행하지 않음MMO, FPS

Dedicated Server 실행 흐름

  1. DedicatedServer.exe를 실행하거나 PIE에서 Run as Client를 선택
  2. OpenLevel 명령어로 특정 레벨을 띄움
  3. Listen 명령어가 실행되어 Socket이 생성되고 다른 PC가 접속 가능해짐
  4. Listen 명령어가 없다면 싱글 플레이가 됨

GameMode 액터 생성

  • Level에는 해당 Level이 사용할 GameMode 정보가 있어서, Level이 열릴 때 GameMode 액터를 생성합니다.
  • GameMode 액터는 오직 서버 PC에만 존재한다. 때문에 GameMode가 전체 게임 진행을 감독합니다.

클라이언트 접속 흐름
1. 클라이언트가 Open (서버IP) 명령어로 접속 요청
2. 서버는 자신이 오픈한 Level을 알려주고, 클라이언트도 동일한 Level을 오픈
3. 클라이언트가 Level을 열었다는 응답을 보내면, 서버가 해당 클라이언트 전용 PlayerController를 생성
4. 해당 PlayerController를 클라이언트에 복제하고 패킷으로 전달
5. PlayerController가 빙의할 Pawn도 서버에서 생성되어 복제됨

중요한 특징

  • PlayerController서버 PC와 Owning PC에만 존재하고, 다른 클라이언트 PC에는 없습니다.
  • Pawn은 모든 PC에 존재합니다.
  • 클라이언트 간의 직접 통신은 불가능하다. 오직 서버↔클라이언트 사이의 통신만 가능합니다.

Editor Play 설정

Editor → Editor Preferences → Play에 있는 주요 설정들입니다.

설정설명
Run Under One Process모든 클라이언트를 하나의 프로세스에서 실행. 에셋 공유가 발생하므로 멀티플레이 테스트 시 반드시 체크 해제
Launch Separate Server서버를 따로 띄워줌. 안 하면 에디터 화면이 클라이언트이자 서버
Allow Late Joining플레이 시작 후에도 클라이언트 추가 가능
Always on Top새 클라이언트 화면이 항상 위에 뜸

NetMode, NetDriver, NetConnection

NetMode

해당 게임 프로세스가 네트워크 상에서 어떤 역할을 하는지를 나타냅니다.

NetMode설명
NM_Standalone싱글 플레이
NM_ListenServer / NM_DedicatedServer서버
NM_Client클라이언트

클라이언트는 해킹에 매우 취약하기 때문에 중요 로직을 서버에서만 처리해야 합니다. 지금 프로세스가 서버인지 클라이언트인지 구분하기 위해 NetMode가 필요합니다.

NetDriver

언리얼 네트워크 통신에서 로우레벨 동작을 관리하는 클래스입니다. 싱글 플레이에서는 생성되지 않고, 멀티플레이에서 UWorld::Listen() 함수를 통해 생성됩니다. 내부에서 NetConnection 객체들을 관리하니다.

NetConnection

다른 PC와 연결이 발생할 때 생성되는 UNetConnection 객체입니다.

  • 서버 PC의 NetDriver: 접속하는 클라이언트 수만큼 ClientConnection을 관리
  • 클라이언트 PC의 NetDriver: ServerConnection 하나만 관리

Role (액터의 권한)

NetMode는 월드 단위의 정보지만, 액터 단위로 "이 액터가 어느 PC에서 스폰되었는지" 를 파악하기 위해 Role이 필요합니다.

Authority와 Proxy

  • Authority: 서버에 스폰된 액터. 신뢰할 수 있는 원본. (HasAuthority())
  • Proxy: 클라이언트에 복제된 액터. 대부분 서버 액터의 허상입니다.

액터 Role 종류

Role설명
None액터가 존재하지 않음
Authority신뢰할 수 있는 룰. 게임 로직을 수행
Autonomous ProxyAuthority의 복제. 서버에도 통신 가능 (입력 전송 등)
Simulated ProxyAuthority의 복제. 서버로부터 데이터를 수신만 함

Autonomous vs Simulated

  • Autonomous Proxy: 클라이언트의 입력 정보를 서버에 보내는 송신 로직 수행 가능. 플레이어 컨트롤러와 빙의된 폰이 해당.
  • Simulated Proxy: 일방적으로 서버로부터 데이터를 수신만 함.

Local Rule과 Remote Rule로 나누는 이유

LocalRole만으로는 액터가 서버와 클라이언트 중 어디에서 스폰되었는지 알 수 없습니다. 예를 들어 서버에 접속한 플레이어 A와 서버에서 스폰된 액터 B가 있다면, LocalRole은 둘 다 Authority입니다. RemoteRole에서 차이가 납니다:

  • A: 서버에서 복제되어 클라이언트에 생성되므로 Autonomous Proxy
  • B: 서버에서 생성되어 클라이언트에 복제되지 않으므로 None

NetMode에 따른 액터 존재 위치

존재 위치액터 종류
서버에만 존재GameMode
서버 + 모든 클라이언트배경 액터, Pawn
서버 + 소유하는 클라이언트에만PlayerController
클라이언트에만 존재애니메이션 블루프린트, HUD

액터 특성별 주의사항

  • GameMode: 서버에만 존재하므로 HasAuthority() 호출 불필요
  • Pawn: Autonomous Proxy와 Simulated Proxy가 혼재
  • Animation & UI: 클라이언트에서만 로직 수행. 서버는 변경된 속성만 전달하고, 변경된 속성에 따라 애니메이션과 UI를 바꾸도록 설계

주요 함수

  • AActor::HasAuthority(): 현재 Role이 Authority인지 반환
  • AController::IsLocalController(): 현재 컨트롤러가 클라이언트에 의해 조종되고 있는지 반환

RPC (Remote Procedure Call)

RPC는 "함수를 호출하는 PC가 아닌 다른 PC에서 실행되게끔 해주는 통신 기법" 입니다. 주로 사운드, 파티클 등 코스메틱 효과에 사용되니다다. 게임에 큰 영향을 끼치는 것들은 Property Replication을 사용해야 합니다.

💡 Call vs Invoke

  • Call: 컴파일 타임에 어떤 함수인지 결정. 일반적인 함수 호출.
  • Invoke: 런타임에 어떤 함수인지 결정. 함수 포인터, 동적 바인딩, RPC. "RPC를 Invoke한다"고 표현.

언리얼에서의 Own

언리얼에서 "Own"은 NetConnection에 의해 소유되고 있음을 뜻합니다.

  • Client-Owned Actor: 해당 ClientConnection이 소유하는 PlayerController
  • Owned by different client: 다른 플레이어의 캐릭터
  • Server-Owned Actor: 서버에 스폰되었고 Owner는 없으며 bReplicates가 true인 액터

RPC 키워드

키워드설명
NetMulticast서버를 포함한 모든 클라이언트에서 실행. 서버에서 호출해야 함. 부하가 크므로 남용 금지
Server서버에서 실행. 클라이언트에서 호출해야 함. _Validate() 함수로 실행 여부 결정 가능
Client해당 클라이언트에서 실행. 서버에서 호출해야 함

서버 PC에서 RPC 호출 시 동작

클라이언트 PC에서 RPC 호출 시 동작

WithValidation

서버 실행 RPC에 사용하는 키워드입니다. _Implementation() 함수와 _Validate() 함수로 나뉘며, _Validate()는 해당 RPC가 실제로 실행될지 말지 결정하는 위변조 방어막 역할을 합니다.

Unreliable vs Reliable

언리얼 Dedicate 내부는 UDP 기반으로 통신한다고 합니다. RPC는 기본적으로 Unreliable이니다.

키워드특징사용 예
Unreliable원격 PC에서 실행 보장 없음코스메틱(이펙트, 사운드 등)
Reliable원격 PC에서 반드시 실행 보장충돌, 데미지, 스폰 등 게임에 중요한 로직

Property Replication

액터 속성 값이 변경될 때 해당 값을 클라이언트에 복제하는 기법입니다. 모든 속성을 복제하는 것은 비효율적이므로 원하는 속성만 선택적으로 복제합니다. 항상 Authority에서 Proxy로의 복제만 가능하다.

C++ 설정 방법

  1. 액터의 Replicates 속성을 true로 설정
  2. 복제할 속성에 Replicated 키워드 추가
  3. GetLifetimeReplicatedProps()에 복제할 속성 등록 (DOREPLIFETIME 매크로 사용)
// 헤더
UPROPERTY(Replicated)
int32 Health;

// 소스
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(AMyActor, Health);
}

OnRep_ vs RepNotify

속성 값이 변경되어 클라이언트에 복제될 때 콜백 함수를 바인딩하는 기능입니다. C++에서는 OnRep_, 블루프린트에서는 RepNotify라고 합니다.

비교C++ OnRep_Blueprint RepNotify
호출 시점클라이언트에서만 호출서버와 클라이언트 모두에서 호출
명시적 호출가능불가능
호출 조건속성 값이 변경될 때만서버는 항시 호출, 클라이언트는 값 변경 시

💡 오늘의 핵심 교훈

GAS (Gameplay Ability System) 관련

  • SetByCaller는 SpecHandle 단위다. 게임 전체에 적용되는 기본값은 없습니다. Effect는 "태그에 해당하는 값을 써라"는 틀만 가지고, 실제 값은 외부에서 주입하는 구조입니다.
  • 고정값이라면 ScalableFloat가 맞다. SetByCaller는 호출 시점마다 값이 달라지는 상황에서 써야 합니다.
  • 할루시네이션 조심. AI의 답변이 계속 일관되지 않거나 처음부터 불가하다고 말을 바꾼다면, 실제 문서나 엔진 소스를 직접 확인하는 것이 낫다는 것을 다시 한번 체감했습니다.

네트워크 관련

  • 클라이언트 간 직접 통신은 불가능하다. 모든 통신은 서버를 거쳐야 합니다.
  • PlayerController는 Server + Owning Client에만 존재한다. 다른 클라이언트에는 없습니다.
  • RPC는 코스메틱에, Property Replication은 게임 상태에 사용한다. 중요한 로직을 RPC로만 처리하면 패킷 유실 시 상태가 틀어질 수 있습니다.
  • Reliable이 항상 정답이 아니다. 이펙트나 사운드처럼 유실되어도 괜찮은 것은 Unreliable을 써서 불필요한 재전송 부하를 줄여야 합니다.
  • NetMode와 Role은 다른 개념이다. NetMode는 월드(프로세스) 단위, Role은 액터 단위의 정보입니다.
profile
게임 개발 지망생입니다.

0개의 댓글