TIL - 파츠 드롭·비주얼 장착, 데디서버/리슨서버, NetMode/NetRole, 정리

Kyu_·2026년 6월 10일

29주차

목록 보기
2/4

TIL - 파츠 드롭·비주얼 장착, 데디서버/리슨서버, NetMode/NetRole, 정리


파츠 드롭 테스트 정리

현재 확인하려는 것

현재 테스트 목표는 다음 두 가지로 볼 수 있다.

  • 바닥에 떨어진 파츠와 현재 장착 중인 파츠가 정상적으로 교환되는지.
  • 교환 결과로 GE, GA 테스트용 효과가 정상 적용되는지.

즉, 이 작업은 단순히 메시를 바꾸는 테스트가 아니라 데이터 교환 + 시각 교체 + GAS 적용 이 한 흐름으로 이어지는지 검증하는 테스트다.

드롭 액터에 물리를 켤지 고민한 이유

물리를 켜면 바닥에 자연스럽게 떨어지는 연출은 쉬울 수 있지만, 현재 구조에서는 단점이 더 크다.

  • 루트가 구체 콜리전이면 반지름만큼 바닥에서 뜨는 문제가 생길 수 있다.
  • 멀티플레이에서 물리 시뮬레이션 액터는 단순 복제보다 관리 포인트가 늘어난다.
  • 특히 서버 권위 하에서 트랜스폼 복제와 물리 결과 동기화를 신경써야 해서 복잡도가 빠르게 증가한다.

그래서 현재 상황에서는 물리 시뮬레이션을 켜지 않고, 스폰 시 1회 위치 보정으로 해결하는 방식 이 더 적절하다.

왜 메시가 공중에 뜨는가

문제의 핵심은 파츠 메시의 피벗이 지면 기준이 아니라 본 위치 기준 이라는 점이다. 예를 들어 헬멧 메시의 원점은 머리 높이 근처, 각반 메시의 원점은 다리 본 근처에 잡혀 있을 수 있으므로, 드랍 액터 원점을 바닥에 그대로 두면 메시가 그 높이만큼 공중에 떠 보인다.

이건 메시가 잘못된 것이 아니라 모듈러 장착 기준으로 제작된 메시를 드랍 기준으로 재사용 하면서 생기는 자연스러운 현상이다.

해결 방법: 메시 바운드 기반 Z 보정

해결 방법은 드랍 시 물리 대신 메시 바운드의 밑면을 바닥에 맞추는 오프셋을 한 번 적용 하는 것이다.

흐름은 다음과 같다.

  1. 드랍할 메시의 로컬 바운드를 구한다.
  2. 바운드의 Min.Z 를 이용해 메시 밑면 위치를 계산한다.
  3. 스폰 위치의 Z 에 그 값을 반영해 한 번만 보정한다.
  4. 이후에는 물리 없이 고정된 드랍 액터로 유지한다.

예시:

const FBoxSphereBounds Bounds = Mesh->CalcBounds(FTransform::Identity);
const float BottomZ = Bounds.GetBox().Min.Z;

FVector SpawnLocation = DropLocation;
SpawnLocation.Z -= BottomZ;

이 방식의 장점은 명확하다.

  • 루트 콜리전 모양에 덜 의존한다.
  • 멀티플레이 물리 복제 문제를 피할 수 있다.
  • 스폰 시 1회 계산으로 끝나므로 유지비가 낮다.
  • 드랍 메시와 장착 메시를 같은 에셋으로 재사용하기 좋다.

스폰 관련 개념

ESpawnActorCollisionHandlingMethod

ESpawnActorCollisionHandlingMethod 는 액터를 스폰할 때, 스폰 위치에서 다른 오브젝트와 겹침이 발생하면 어떻게 처리할지 결정하는 열거형이다.

자주 보는 옵션은 다음과 같다.

의미
AlwaysSpawn겹침을 무시하고 무조건 스폰
AdjustIfPossibleButAlwaysSpawn가능하면 위치를 조정하고, 그래도 스폰
AdjustIfPossibleButDontSpawnIfStillColliding조정 후에도 겹치면 스폰하지 않음
DontSpawnIfColliding겹치면 아예 스폰하지 않음

현재 드랍 액터처럼 “일단 생성은 되어야 하고, 위치는 후처리로 맞출 수 있다”는 상황에서는 AlwaysSpawn 가 단순하고 예측 가능하다.

SpawnActorDeferred

SpawnActorDeferred 는 액터 스폰을 객체 생성 단계최종 초기화 단계 로 나누는 방식이다.
일반 SpawnActor 는 생성 후 초기화가 즉시 진행되지만, SpawnActorDeferredFinishSpawning() 을 호출하기 전까지 중간 초기화 구간을 확보할 수 있다.

즉, 흐름은 아래처럼 이해하면 된다.

  1. SpawnActorDeferred 로 액터 객체를 만든다.

  2. BeginPlay 전에 필요한 값을 세팅한다.

  3. FinishSpawning() 으로 스폰을 완료한다.

예시:

ADroppedPart* DroppedPart = GetWorld()->SpawnActorDeferred<ADroppedPart>(
    DroppedPartClass,
    SpawnTransform,
    Owner,
    Instigator,
    ESpawnActorCollisionHandlingMethod::AlwaysSpawn
);

DroppedPart->InitializeFromInstance(StoredInstance);
DroppedPart->SetDesiredDropMesh(DropMesh);

UGameplayStatics::FinishSpawningActor(DroppedPart, SpawnTransform);

현재 케이스에서 이 방식이 중요한 이유는 DroppedPart 가 시작 시점에 StoredInstance 를 이미 가지고 있어야 하기 때문이다. 일반 SpawnActor 로 만들면 BeginPlay 또는 초기화 로직이 먼저 돌고, 그 뒤에 값을 세팅하는 순서 문제가 생길 수 있다.


파츠 비주얼 구조 정리

역할 분리

장착 데이터와 시각 표시를 분리한 현재 설계는 방향이 좋다.

컴포넌트부착 위치역할
UNSPartEquipComponentPlayerState파츠 데이터, 슬롯 관리, GAS 연동
UNSPartVisualComponentPlayerCharacter파츠 메시 표시 전용

핵심은 EquipComponent 가 파츠 하나당 붙는 구조가 아니라, 플레이어 하나당 1개 존재하면서 전 슬롯을 관리한다 는 점이다. 따라서 VisualComponent 는 EquipComponent 의 상태를 구독해서 렌더링만 담당하면 된다.

BindToEquipComponent 흐름

BindToEquipComponent(EquipComp, GetMesh())
 ├── GetMesh() = 캐릭터 본체 스켈레탈 메시(리더)
 ├── EnsureSlotComponents() = 슬롯별 메시 컴포넌트 최초 1회 생성
 └── OnPartChanged 델리게이트 구독

이 구조의 장점은 초기화와 갱신 책임이 깔끔히 분리된다는 점이다. 슬롯용 메시 컴포넌트는 한 번 만들어 두고, 실제 파츠 변경은 이벤트 기반으로 처리하면 된다.

EnsureSlotComponents

EnsureSlotComponents슬롯별 비주얼 컴포넌트가 존재하는지 보장하는 초기화 함수 로 이해하면 된다. 보통 BindToEquipComponent 안에서 한 번만 호출되며, 하는 일은 크게 두 가지다.

  • BindToEquipComponent 가 중복되지 않게 준비한다.
  • 슬롯별 USkeletalMeshComponent 를 생성하고 등록한다.

즉, 이 함수의 책임은 그릇 생성 이다. 아직 어떤 파츠를 보여줄지는 모르더라도, Body/Arm/Leg 슬롯이 사용할 메시 컴포넌트를 준비해 두는 단계다.

UpdateSlotVisual

UpdateSlotVisual실제 파츠 변경이 발생했을 때 해당 슬롯의 메시를 바꾸는 함수 다. 즉, EnsureSlotComponents 가 그릇을 만드는 단계라면, UpdateSlotVisual 은 그 그릇에 어떤 스켈레탈 메시를 담을지 결정하는 단계다.

파츠가 바뀔 때마다 새 컴포넌트를 생성할 필요는 없다. 이미 만들어 둔 슬롯 컴포넌트에 SetSkeletalMesh() 로 에셋만 교체하면 된다.


SetLeaderPoseComponent 와 부착 원리

파츠가 제자리에 붙는 이유

SetLeaderPoseComponent() 가 파츠 메시의 위치를 직접 계산해 주는 것은 아니다. 위치가 맞는 이유는 파츠 메시 자체가 캐릭터 전신 스켈레톤 기준 위치에 맞춰 모델링되어 있기 때문 이고, SetLeaderPoseComponent() 는 그 본 변형을 따라가게 해 주는 역할을 한다.

역할을 나누면 아래처럼 정리할 수 있다.

역할담당
씬 계층상 부모-자식 관계SetupAttachment()
본 애니메이션 동기화SetLeaderPoseComponent()
처음부터 올바른 공간에 존재하는 버텍스 배치파츠 스켈레탈 메시 에셋 자체

즉, 올바르게 모델링된 모듈러 메시 + 같은 스켈레톤 + Leader Pose 조합이 자연스러운 장착 비주얼을 만든다.

EnsureSlotComponents 코드 의미

MeshComp->SetupAttachment(LeaderMeshComp);

리더 메시를 부모로 붙여서 위치/회전/스케일을 함께 따라가게 만든다.

MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);

파츠 시각 메시 자체는 충돌이 필요 없으므로 꺼 두는 편이 합리적이다. 실제 캐릭터 충돌은 보통 캡슐 컴포넌트가 담당한다.

MeshComp->RegisterComponent();

NewObject 로 만든 컴포넌트는 등록 전까지 월드에서 정상적으로 렌더링·틱되지 않으므로, 수동 등록이 필요하다.

MeshComp->SetLeaderPoseComponent(LeaderMeshComp);

리더 메시의 본 포즈를 따라가게 하여, 별도 애니메이션 인스턴스 없이도 파츠가 동일한 애니메이션을 공유하게 만든다.


파츠 시각 장착 설계

설계 방향

현재 설계는 데이터 관리와 시각 표현을 분리하면서, 드랍용 메시를 장착용 메시로도 재사용하는 구조 다. 이 방향은 구현량과 유지보수 비용 사이 균형이 좋다.

항목결정이유
장착용 메시 소스DropMesh 재사용드랍 룩과 장착 룩이 동일하고 필드 추가가 필요 없음
부착 방식Leader Pose같은 스켈레톤 공유 시 애니메이션 자동 추종 가능
구조 위치UNSPartVisualComponent 분리캐릭터 클래스 비대화 방지, 기존 컴포넌트 스타일과 일관성 유지

구조

ANSPlayerCharacterBase
 └── UNSPartVisualComponent
     ├── BodyPartMeshComp : USkeletalMeshComponent
     ├── ArmPartMeshComp  : USkeletalMeshComponent
     └── LegPartMeshComp  : USkeletalMeshComponent

각 슬롯 메시 컴포넌트는 메인 캐릭터 메시를 리더로 사용한다.

신호 흐름

UNSPartEquipComponent::OnPartChanged(Slot, PartData)
 → UNSPartVisualComponent 가 구독
 → DefinitionPtr 로 Def 조회
   → 이미 로드됨: 즉시 SetSkeletalMesh()
   → 미로드: RequestAsyncLoad 후 콜백에서 적용
 → 비어 있는 슬롯: SetSkeletalMesh(nullptr)

이 흐름의 핵심은 EquipComponent 는 상태를 발행하고, VisualComponent 는 그 상태를 구독해서 렌더링만 한다 는 점이다.

구독 시점

VisualComponent 는 캐릭터에 붙어 있지만 EquipComponent 는 PlayerState 에 있으므로, BeginPlay 에서 무조건 바로 접근한다고 가정하면 위험할 수 있다. 따라서 PossessedBy()OnRep_PlayerState() 시점 모두에서 바인딩을 보장하는 방식 이 타당하다.

이 판단은 특히 멀티플레이에서 중요하다. 서버와 클라이언트는 PlayerState 획득 타이밍이 다를 수 있기 때문이다.


구현 계획

1단계 - UNSPartVisualComponent 신설

파일 위치:

  • Source/NeoSanctum/Character/Component/NSPartVisualComponent.h
  • Source/NeoSanctum/Character/Component/NSPartVisualComponent.cpp

구성:

  • 슬롯별 USkeletalMeshComponent 3개 보유
  • BindToEquipComponent(UNSPartEquipComponent*, USkeletalMeshComponent* LeaderMesh)
  • EnsureSlotComponents()
  • UpdateSlotVisual(ENSPartSlot, const FNSPartData&)
  • ClearVisual(ENSPartSlot)

2단계 - ANSPlayerCharacterBase 연결

  • UPROPERTY(VisibleAnywhere)PartVisualComp 추가
  • PossessedBy() 에서 서버 측 바인딩
  • OnRep_PlayerState() 에서 클라이언트 측 바인딩

검증 목표

1. 키 입력 → EquipPart → 캐릭터에 파츠 메시 부착
2. 같은 슬롯 드랍 픽업 → 기존 메시 제거 + 새 메시 부착
3. ClearAll 호출 → 모든 슬롯 메시 제거
4. GE 적용 여부 → showdebug abilitysystem 으로 확인

비동기 로드 메모

DropMesh 가 번들 태그 없이 관리된다면, 파츠 비주얼 적용 시점에 온디맨드 비동기 로드를 거는 것이 안전하다. 현재 구조에서는 슬롯별 로드 핸들을 관리하고, ClearVisual() 시 취소하거나 무효화하는 흐름이 적합하다.

특히 장착 시스템에서는 동기 로드(LoadSynchronous) 를 피하는 것 이 중요하다. 전투 중 장착 변경이나 드랍 픽업 시 프레임 스톨을 만들 수 있기 때문이다.


서버 구조 정리

데디서버와 리슨서버 차이

언리얼의 Dedicated Server 는 클라이언트와 분리된 별도 서버 프로세스로 동작하며, 시각 표시나 UI 없이 게임 상태 동기화와 규칙 처리만 담당한다.
반면 Listen Server 는 한 플레이어의 게임 클라이언트가 서버 역할까지 함께 수행하는 구조라서, 호스트는 직접 플레이하면서 동시에 서버 권한도 가진다.


NetMode 와 NetRole

NetMode

NetMode현재 월드 인스턴스가 어떤 네트워크 실행 모드인지 나타내는 값이다. 즉, “이 프로그램 인스턴스가 서버인지, 클라이언트인지, 혼자인지”를 구분하는 월드 단위 개념이다.

의미
NM_Standalone네트워크 연결 없이 단독 실행
NM_ListenServer서버이면서 동시에 로컬 플레이어도 존재
NM_DedicatedServer전용 서버, 플레이용 클라이언트 기능 없음
NM_Client서버에 접속한 클라이언트

정리하면 NetMode 는 월드 전체의 입장 이다. 같은 코드라도 어느 머신에서 실행하느냐에 따라 NM_Client, NM_ListenServer, NM_DedicatedServer 로 달라진다.

NetRole

NetRole개별 Actor 가 현재 머신에서 어떤 네트워크 역할을 갖는지 나타낸다. UE5 에서는 예전 Role 직접 접근보다 GetLocalRole() / GetRemoteRole() 형태로 확인하는 방식이 권장된다.

의미
ROLE_Authority이 Actor 의 권한을 가진 원본, 일반적으로 서버 측 인스턴스
ROLE_AutonomousProxy로컬 플레이어가 직접 조작하는 Pawn/Actor
ROLE_SimulatedProxy서버 상태를 복제받아 표시만 하는 원격 Actor
ROLE_None네트워크 역할 없음

핵심은 NetMode 는 월드 기준, NetRole 은 Actor 기준 이라는 점이다. 예를 들어 클라이언트 머신의 NetMode 는 항상 NM_Client 여도, 그 안의 Actor 들은 AutonomousProxySimulatedProxy 처럼 서로 다른 Role 을 가질 수 있다.

실무에서 어떻게 구분할까

  • “이 코드가 서버에서만 돌아야 하나?” → HasAuthority() 또는 NetMode 를 본다.
  • “이 Actor 가 내가 조작하는 대상인가?” → GetLocalRole() 과 소유 관계를 본다.
  • 초기화 시점이 아주 이른 경우에는 NetRole 보다 NetMode 확인이 더 안전한 경우가 있다.

오늘의 핵심 정리

  • Dedicated Server 는 서버 전용 프로세스, Listen Server 는 플레이어가 서버까지 겸하는 구조다.
  • NetMode 는 월드 단위 실행 모드, NetRole 은 Actor 단위 네트워크 역할이다.
  • 현재 드랍 파츠 문제는 물리보다 메시 바운드 기반 Z 보정 이 더 단순하고 멀티플레이 친화적이다.
  • SpawnActorDeferredStoredInstance 같은 필수 데이터를 BeginPlay 전 주입해야 할 때 적합하다.
  • 파츠 시각 장착은 EquipComponent(PlayerState)VisualComponent(Character) 분리 구조가 맞고, 슬롯 메시 컴포넌트는 최초 1회 생성 후 에셋만 교체하면 된다.

0개의 댓글