현재 테스트 목표는 다음 두 가지로 볼 수 있다.
즉, 이 작업은 단순히 메시를 바꾸는 테스트가 아니라 데이터 교환 + 시각 교체 + GAS 적용 이 한 흐름으로 이어지는지 검증하는 테스트다.
물리를 켜면 바닥에 자연스럽게 떨어지는 연출은 쉬울 수 있지만, 현재 구조에서는 단점이 더 크다.
그래서 현재 상황에서는 물리 시뮬레이션을 켜지 않고, 스폰 시 1회 위치 보정으로 해결하는 방식 이 더 적절하다.
문제의 핵심은 파츠 메시의 피벗이 지면 기준이 아니라 본 위치 기준 이라는 점이다. 예를 들어 헬멧 메시의 원점은 머리 높이 근처, 각반 메시의 원점은 다리 본 근처에 잡혀 있을 수 있으므로, 드랍 액터 원점을 바닥에 그대로 두면 메시가 그 높이만큼 공중에 떠 보인다.
이건 메시가 잘못된 것이 아니라 모듈러 장착 기준으로 제작된 메시를 드랍 기준으로 재사용 하면서 생기는 자연스러운 현상이다.
해결 방법은 드랍 시 물리 대신 메시 바운드의 밑면을 바닥에 맞추는 오프셋을 한 번 적용 하는 것이다.
흐름은 다음과 같다.
Min.Z 를 이용해 메시 밑면 위치를 계산한다.예시:
const FBoxSphereBounds Bounds = Mesh->CalcBounds(FTransform::Identity);
const float BottomZ = Bounds.GetBox().Min.Z;
FVector SpawnLocation = DropLocation;
SpawnLocation.Z -= BottomZ;
이 방식의 장점은 명확하다.
ESpawnActorCollisionHandlingMethod 는 액터를 스폰할 때, 스폰 위치에서 다른 오브젝트와 겹침이 발생하면 어떻게 처리할지 결정하는 열거형이다.
자주 보는 옵션은 다음과 같다.
| 값 | 의미 |
|---|---|
AlwaysSpawn | 겹침을 무시하고 무조건 스폰 |
AdjustIfPossibleButAlwaysSpawn | 가능하면 위치를 조정하고, 그래도 스폰 |
AdjustIfPossibleButDontSpawnIfStillColliding | 조정 후에도 겹치면 스폰하지 않음 |
DontSpawnIfColliding | 겹치면 아예 스폰하지 않음 |
현재 드랍 액터처럼 “일단 생성은 되어야 하고, 위치는 후처리로 맞출 수 있다”는 상황에서는 AlwaysSpawn 가 단순하고 예측 가능하다.
SpawnActorDeferred 는 액터 스폰을 객체 생성 단계 와 최종 초기화 단계 로 나누는 방식이다.
일반 SpawnActor 는 생성 후 초기화가 즉시 진행되지만, SpawnActorDeferred 는 FinishSpawning() 을 호출하기 전까지 중간 초기화 구간을 확보할 수 있다.
즉, 흐름은 아래처럼 이해하면 된다.
SpawnActorDeferred 로 액터 객체를 만든다.
BeginPlay 전에 필요한 값을 세팅한다.
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 또는 초기화 로직이 먼저 돌고, 그 뒤에 값을 세팅하는 순서 문제가 생길 수 있다.
장착 데이터와 시각 표시를 분리한 현재 설계는 방향이 좋다.
| 컴포넌트 | 부착 위치 | 역할 |
|---|---|---|
UNSPartEquipComponent | PlayerState | 파츠 데이터, 슬롯 관리, GAS 연동 |
UNSPartVisualComponent | PlayerCharacter | 파츠 메시 표시 전용 |
핵심은 EquipComponent 가 파츠 하나당 붙는 구조가 아니라, 플레이어 하나당 1개 존재하면서 전 슬롯을 관리한다 는 점이다. 따라서 VisualComponent 는 EquipComponent 의 상태를 구독해서 렌더링만 담당하면 된다.
BindToEquipComponent(EquipComp, GetMesh())
├── GetMesh() = 캐릭터 본체 스켈레탈 메시(리더)
├── EnsureSlotComponents() = 슬롯별 메시 컴포넌트 최초 1회 생성
└── OnPartChanged 델리게이트 구독
이 구조의 장점은 초기화와 갱신 책임이 깔끔히 분리된다는 점이다. 슬롯용 메시 컴포넌트는 한 번 만들어 두고, 실제 파츠 변경은 이벤트 기반으로 처리하면 된다.
EnsureSlotComponents 는 슬롯별 비주얼 컴포넌트가 존재하는지 보장하는 초기화 함수 로 이해하면 된다. 보통 BindToEquipComponent 안에서 한 번만 호출되며, 하는 일은 크게 두 가지다.
BindToEquipComponent 가 중복되지 않게 준비한다.USkeletalMeshComponent 를 생성하고 등록한다.즉, 이 함수의 책임은 그릇 생성 이다. 아직 어떤 파츠를 보여줄지는 모르더라도, Body/Arm/Leg 슬롯이 사용할 메시 컴포넌트를 준비해 두는 단계다.
UpdateSlotVisual 은 실제 파츠 변경이 발생했을 때 해당 슬롯의 메시를 바꾸는 함수 다. 즉, EnsureSlotComponents 가 그릇을 만드는 단계라면, UpdateSlotVisual 은 그 그릇에 어떤 스켈레탈 메시를 담을지 결정하는 단계다.
파츠가 바뀔 때마다 새 컴포넌트를 생성할 필요는 없다. 이미 만들어 둔 슬롯 컴포넌트에 SetSkeletalMesh() 로 에셋만 교체하면 된다.
SetLeaderPoseComponent() 가 파츠 메시의 위치를 직접 계산해 주는 것은 아니다. 위치가 맞는 이유는 파츠 메시 자체가 캐릭터 전신 스켈레톤 기준 위치에 맞춰 모델링되어 있기 때문 이고, SetLeaderPoseComponent() 는 그 본 변형을 따라가게 해 주는 역할을 한다.
역할을 나누면 아래처럼 정리할 수 있다.
| 역할 | 담당 |
|---|---|
| 씬 계층상 부모-자식 관계 | SetupAttachment() |
| 본 애니메이션 동기화 | SetLeaderPoseComponent() |
| 처음부터 올바른 공간에 존재하는 버텍스 배치 | 파츠 스켈레탈 메시 에셋 자체 |
즉, 올바르게 모델링된 모듈러 메시 + 같은 스켈레톤 + Leader Pose 조합이 자연스러운 장착 비주얼을 만든다.
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 획득 타이밍이 다를 수 있기 때문이다.
UNSPartVisualComponent 신설파일 위치:
Source/NeoSanctum/Character/Component/NSPartVisualComponent.hSource/NeoSanctum/Character/Component/NSPartVisualComponent.cpp구성:
USkeletalMeshComponent 3개 보유BindToEquipComponent(UNSPartEquipComponent*, USkeletalMeshComponent* LeaderMesh)EnsureSlotComponents()UpdateSlotVisual(ENSPartSlot, const FNSPartData&)ClearVisual(ENSPartSlot)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 는 현재 월드 인스턴스가 어떤 네트워크 실행 모드인지 나타내는 값이다. 즉, “이 프로그램 인스턴스가 서버인지, 클라이언트인지, 혼자인지”를 구분하는 월드 단위 개념이다.
| 값 | 의미 |
|---|---|
NM_Standalone | 네트워크 연결 없이 단독 실행 |
NM_ListenServer | 서버이면서 동시에 로컬 플레이어도 존재 |
NM_DedicatedServer | 전용 서버, 플레이용 클라이언트 기능 없음 |
NM_Client | 서버에 접속한 클라이언트 |
정리하면 NetMode 는 월드 전체의 입장 이다. 같은 코드라도 어느 머신에서 실행하느냐에 따라 NM_Client, NM_ListenServer, NM_DedicatedServer 로 달라진다.
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 들은 AutonomousProxy 나 SimulatedProxy 처럼 서로 다른 Role 을 가질 수 있다.
HasAuthority() 또는 NetMode 를 본다.GetLocalRole() 과 소유 관계를 본다.SpawnActorDeferred 는 StoredInstance 같은 필수 데이터를 BeginPlay 전 주입해야 할 때 적합하다.EquipComponent(PlayerState) 와 VisualComponent(Character) 분리 구조가 맞고, 슬롯 메시 컴포넌트는 최초 1회 생성 후 에셋만 교체하면 된다.