
리슨 서버/클라이언트 환경에서 Replicate 해야하는 인스턴스가 클라이언트에서도 생성되는 문제가 발생했습니다. 특히 bReplicates 를 켜지 않았는데도 클라이언트에서 액터가 보이고, HasAuthority() 로 분기를 나눴다고 생각했는데도 여전히 중복 생성이 일어나는 상황이라, 네트워크 권한(Authority)와 NetMode 개념을 다시 정리할 필요가 있었습니다.
이번 글에서는 ACMProcedualMapGenerator + UCMMapGenerateLogicBaseComponent + ACMRoom 조합으로 작성된 맵 생성 로직에서 어떤 문제가 있었고, 이를 어떻게 디버깅하고 정리했는지 트러블슈팅 과정을 정리합니다.
ACMProcedualMapGenerator
GenerateMap(int32 InSeed, UCMRoomDataDefinition* InMapData) 에서 맵 생성 컴포넌트들을 동적으로 생성/교체MapGenerateLogicComponent : UCMMapGenerateLogicBaseComponent 기반SpawnPositionSelectorComponent : 스폰 위치 선택 담당TMap<FCMRoomPosition, TObjectPtr<ACMRoom>> RoomMap 에 저장UCMMapGenerateLogicBaseComponent
ExecuteMapGenerationLogic : 전체 생성 파이프라인GenerateRoom : 좌표 기반 그래프 생성 및 ACMRoom 스폰PlaceWallsAndEntrances : 각 방 주변에 Entrance/Wall 스폰ACMRoom
RoomBoundsBox, 4방향 Entrance 포인트, Entrance/Wall 클래스를 보유ExcuteSpawnRoom(int32 DirectionIndex, bool bIsEntrance) 에서 실제로 Entrance/Wall Actor 스폰void UCMMapGenerateLogicBaseComponent::PlaceWallsAndEntrances(TMap<FCMRoomPosition, TObjectPtr<ACMRoom>>& OutRoomMap)
{
ENetMode NetMode = GetWorld() ? GetWorld()->GetNetMode() : NM_Standalone;
for (const TPair<FCMRoomPosition, TObjectPtr<ACMRoom>>& Pair : OutRoomMap)
{
if (ACMRoom* Room = Pair.Value.Get())
{
for (int32 i = 0; i < 4; ++i)
{
FCMRoomPosition CurrentPos(Pair.Key.X + DirectionX[i], Pair.Key.Y + DirectionY[i]);
if (OutRoomMap.Contains(CurrentPos))
{
if (NetMode != NM_Client)
{
UE_LOG(LogTemp, Log, TEXT("HasAuthority: %d, NetMode: %d"),
GetOwner()->HasAuthority(),
static_cast<int32>(NetMode));
Room->ExcuteSpawnRoom(i, true); // Entrance
}
}
else
{
Room->ExcuteSpawnRoom(i, false); // Wall
}
}
}
}
}

bReplicates 를 블루프린트에서 켜지 않았는데도, 클라이언트에서 방/Entrance/Wall 이 모두 보임GetOwner()->HasAuthority() 로 조건을 걸어도, 여전히 호스트와 클라이언트 양쪽에서 Entrance/Wall 이 각각 생성됨HasAuthority: 1 이 서버와 클라이언트 양쪽에서 모두 찍히는 듯한 결과AActor::HasAuthority() 는 내부적으로 Role == ROLE_Authority 를 의미HasAuthority() == trueHasAuthority() == falseHasAuthority() == trueRole = ROLE_Authority → HasAuthority() == trueUWorld::GetNetMode() 로 현재 월드의 네트워크 모드를 알 수 있음NM_Standalone (0) : 싱글 플레이NM_DedicatedServer (1) : 디디케이티드 서버NM_ListenServer (2) : 리슨 서버(호스트)NM_Client (3) : 클라이언트World->GetNetMode() == NM_Client 이면 클라이언트 → 서버 전용 로직은 실행하지 않도록 Early ReturnWorld->GetNetMode() != NM_Client 을 서버/호스트 쪽으로 간주해도 됨ACMProcedualMapGenerator 는 단순 AActor 이고, bReplicates 세팅이나 서버 전용 제약 없이 월드에 배치/생성되어 있었음ACMProcedualMapGenerator 한 개ACMProcedualMapGenerator 한 개HasAuthority() == trueGenerateMap() 호출 또한 서버/클라 각각에서 트리거되는 구조였기 때문에,UCMMapGenerateLogicBaseComponent::ExecuteMapGenerationLogicUCMMapGenerateLogicBaseComponent::PlaceWallsAndEntrancesACMRoom::ExcuteSpawnRoomGetOwner()->HasAuthority() 만 보고 Entrance 를 생성ENetMode NetMode = GetWorld()->GetNetMode() 를 도입해 NetMode != NM_Client 인 경우에만 Entrance 를 생성하도록 변경else 분기 (OutRoomMap 에 인접 방이 없을 때)에서 Wall 스폰은 권한/NetMode 체크 없이 항상 실행ExcuteSpawnRoom 내부에서도 별도의 HasAuthority() 체크가 없으면, 양쪽에서 모두 스폰ACMRoom 이 비복제라면, 서버/클라 각각의 ACMRoom 인스턴스도 자기 기준에서는 HasAuthority() == trueUE_LOG(LogTemp, Log, TEXT("HasAuthority: %d, NetMode: %d"), GetOwner()->HasAuthority(), (int32)NetMode);HasAuthority = 1, NetMode = 2 (ListenServer) 또는 1 (DedicatedServer)HasAuthority = 1, NetMode = 3 (Client) 인 케이스가 가능HasAuthority 칼럼만 보면 둘 다 1로 찍히기 때문에,UWorld::GetNetMode() 가 NM_Client 이면 → 맵 생성 로직/스폰 로직은 아예 실행하지 않음HasAuthority() 는 보조적으로 사용하되, 비복제 Actor 도 Authority 일 수 있다는 점을 항상 염두에 둠아래는 Entrance 인스턴스가 정상적으로 Replication On/Off 되는 결과물입니다.


언리얼 네트워크에서 HasAuthority() 와 NetMode 는 비슷해 보이지만, 서로 다른 레이어의 개념이라는 점을 다시 상기
HasAuthority() : 이 Actor 인스턴스가 자기 월드에서 서버 역할인지NetMode : 이 월드가 DedicatedServer / ListenServer / Client / Standalone 인지비복제 Actor 는 서버와 클라이언트에서 각각 스폰되면, 둘 다 HasAuthority() == true 라는 점이 핵심 포인트
따라서, 서버/클라이언트를 분기하고 싶을 때는 다음 우선순위를 지키는 것이 좋다고 정리
우선순위
UWorld::GetNetMode() 로 프로세스 레벨(서버/클라) 구분NM_Client 인 경우 서버 전용 로직을 실행하지 않음AActor::HasAuthority() 는 복제된 Actor 의 서버/클라 인스턴스 구분에 사용ExcuteSpawnRoom) 내부에도 HasAuthority() 체크를 두어 이중 방어이번 트러블슈팅을 통해 얻은 교훈
HasAuthority 뿐 아니라 NetMode 값도 함께 로그로 남겨야, "어느 월드에서 무슨 역할로" 실행 중인지 명확히 볼 수 있다.