블로그로 돌아가기

Multiplayer 인벤토리의 악몽: Unreal Engine에서 뒤바뀐 ActorComponent Owner 수정하기

게시일 2026년 3월 1일
Multiplayer 인벤토리의 악몽: Unreal Engine에서 뒤바뀐 ActorComponent Owner 수정하기

모든 Multiplayer 게임 개발자는 결국 Unreal Engine의 Replication 시스템이라는 벽에 부딪히게 됩니다. 인벤토리 시스템을 구축하고 로컬에서 테스트하면 완벽하게 작동합니다. 하지만 두 개의 클라이언트로 Dedicated Server를 실행하고 무기를 줍는 순간, 악몽이 시작됩니다.

서버는 당신이 아이템을 주웠다는 것을 알고 있습니다. 하지만 클라이언트는 아무 일도 일어나지 않은 것처럼 행동합니다. ActorComponent를 디버깅하기 위해 GetOwner()를 출력해 보면 당혹스러운 사실을 발견하게 됩니다. 캐릭터 0은 자신의 Owner가 캐릭터 1이라고 생각하고, 캐릭터 1은 자신의 Owner가 캐릭터 0이라고 생각하는 것입니다.

컴포넌트의 Ownership이 네트워크상에서 서로 뒤바뀐 것처럼 보입니다.

클라이언트에서 GetOwner()가 잘못된 캐릭터를 반환하는 이 특정 Desync 현상은 Unreal Engine Multiplayer 개발에서 악명 높은 함정입니다. 이는 RPC (Remote Procedure Calls)를 깨뜨리고, UI 로직을 파괴하며, 게임을 망치는 악용 사례(exploits)의 빌미를 제공합니다.

이 기술 심층 분석에서는 왜 이 unreal engine actorcomponent getowner multiplayer fix가 그토록 오해받는지, Play-In-Editor (PIE)가 어떻게 당신을 속이는지, 그리고 인벤토리 Replication 문제를 영구적으로 해결하기 위해 필요한 단계별 C++ 아키텍처를 파헤쳐 보겠습니다.

버그의 해부학: UActorComponent vs. AActor Ownership

컴포넌트의 Owner가 바뀌는 이유를 이해하려면 먼저 Unreal Engine에서 가장 오해받는 개념 중 하나인 Actor의 Network Ownership과 Component의 Outer Ownership 사이의 근본적인 차이점을 명확히 해야 합니다.

UActorComponent::GetOwner()는 네트워크 함수가 아닙니다

개발자가 AActor에서 SetOwner()를 호출할 때, 그들은 Unreal의 네트워크 아키텍처와 상호작용하는 것입니다. Network Ownership은 특정 Actor에 대해 어떤 클라이언트 연결이 Server RPC를 보낼 수 있는지 결정합니다.

하지만 UActorComponent는 동일한 방식으로 네트워크 복제된 Owner를 갖지 않습니다. UActorComponent::GetOwner()의 소스 코드를 보면 매우 단순한 구조를 확인할 수 있습니다.

AActor* UActorComponent::GetOwner() const
{
    return Cast<AActor>(GetOuter());
}

ActorComponent의 Owner는 메모리에서 이를 포함하고 있는 객체인 Outer에 의해 엄격하게 정의됩니다. 부모 Actor의 Owner를 변경하거나, 컴포넌트를 파괴하고 새로운 Outer로 다시 생성하지 않는 한, 네트워크를 통해 컴포넌트의 Network Owner를 동적으로 "교체"할 수 없습니다.

클라이언트에서 GetOwner()가 잘못된 캐릭터를 반환한다면, 다음 두 가지 중 하나가 발생한 것입니다.

  1. PIE 로컬 인덱스 함정: 코드에서 참조를 해결하기 위해 로컬 플레이어 인덱스(GetPlayerCharacter(0) 등)에 의존하고 있으며, 이는 Multiplayer 테스트에서 완전히 깨집니다.
  2. Replication 레이스 컨디션: 컴포넌트를 동적으로 생성하면서 클라이언트 측 인스턴스화 중에 잘못된 Outer를 전달했거나, 서버가 올바른 참조를 복제하기 전에 UI가 컴포넌트를 쿼리하고 있는 경우입니다.

근본 원인 1: Play-In-Editor (PIE) 로컬 인덱스 함정

Unreal Engine에서 "Run Under One Process"가 체크된 상태(기본 설정)로 "Play In Editor" (PIE) 모드를 사용하여 Multiplayer를 테스트할 때, 모든 클라이언트는 동일한 메모리 공간 내에서 실행됩니다.

많은 개발자가 Blueprint의 Get Player Character (Index 0) 노드나 C++의 UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)와 같은 함수를 사용하여 UI나 인벤토리 위젯을 초기화합니다.

이는 Multiplayer에서 치명적입니다.

Standalone 게임에서 Index 0은 항상 로컬 플레이어입니다. 하지만 프로세스를 공유하는 PIE 세션에서 Unreal Engine은 여러 로컬 플레이어를 동시에 관리해야 합니다. GetPlayerCharacter(0)가 호출되는 정확한 시점과 위치(특히 복제된 ActorComponent 초기화 내부)에 따라, 클라이언트 A가 실수로 클라이언트 B의 Controller 참조를 가져올 수 있습니다.

결과적으로 클라이언트 A의 인벤토리 위젯이 컴포넌트에게 "당신의 Owner는 누구인가요?"라고 물으면, 위젯은 실제로 클라이언트 B에 부착된 컴포넌트를 쿼리하게 됩니다. UI가 잘못된 메모리 주소를 보고 있기 때문에 Owner가 "뒤바뀐" 것처럼 보이는 것입니다.

해결책: Local Viewing Player 해결하기

Multiplayer 컴포넌트나 UI에서 하드코딩된 플레이어 인덱스를 절대 사용하지 마십시오. 대신 위젯의 Owning Player나 컴포넌트의 실제 계층 구조를 통해 Player Controller를 해결하십시오.

// 나쁜 예: PIE Multiplayer 테스트에서 Owner가 "뒤바뀌는" 원인이 됨
AActor* BadOwner = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);

// 좋은 예: 컴포넌트의 실제 Outer 계층 구조를 통해 해결
AActor* TrueOwner = GetOwner();
APawn* OwningPawn = Cast<APawn>(TrueOwner);
if (OwningPawn && OwningPawn->IsLocallyControlled())
{
    // 이제 이 컴포넌트가 로컬 클라이언트에 속해 있음을 안전하게 알 수 있습니다.
    APlayerController* PC = Cast<APlayerController>(OwningPawn->GetController());
}

서버와 클라이언트 간에 플레이어 상태가 완전히 어긋나는 더 깊은 동기화 문제를 겪고 있다면, 더 광범위한 엔진 버그에 직면했을 수 있습니다. 상태 Desync 처리에 대한 자세한 내용은 The Unreal Engine Multiplayer Sync Bug Ruining Your World States And How To Fix It 가이드를 참조하십시오.

근본 원인 2: Attachment vs. Network Ownership

클라이언트에서 인벤토리 컴포넌트가 실패하는 또 다른 주요 이유는 Attachment(부착)와 Ownership(소유권)을 혼동하기 때문입니다.

플레이어가 무기나 인벤토리 아이템(대개 다양한 ActorComponents를 포함하는 AActor)을 주울 때, 개발자들은 종종 아이템을 캐릭터의 Mesh에 부착합니다.

// Mesh를 부착한다고 해서 Network Ownership이 부여되는 것은 아닙니다!
ItemActor->AttachToComponent(CharacterMesh, FAttachmentTransformRules::SnapToTargetNotIncludingScale, "WeaponSocket");

Actor를 부착하는 것은 Transform 계층 구조만 업데이트할 뿐입니다. 이는 NetOwner를 업데이트하지 않습니다. 서버에서 명시적으로 SetOwner()를 호출하지 않으면, 클라이언트는 해당 아이템의 컴포넌트에서 RPC를 실행할 권한을 영원히 얻지 못합니다. 설상가상으로 아이템이 상태를 복제하는 경우, 클라이언트는 부착 Replication은 받지만 여전히 GetOwner() == nullptr 또는 이전 Owner를 읽게 될 수 있습니다.

클라이언트가 무기를 장착하거나 인벤토리에서 이동하려고 할 때, 클라이언트에 네트워크 권한이 없기 때문에 Server RPC가 드롭되며, 결과적으로 "클라이언트가 아이템을 줍지 않은 것처럼 행동하는" 전형적인 증상이 나타납니다.

단계별 아키텍처: Server-Authoritative 줍기

이러한 Ownership 뒤바뀜과 Desync를 영구적으로 해결하려면 인벤토리 줍기 과정을 엄격하게 Server-Authoritative(서버 권위) 방식으로 설계해야 하며, 명시적인 Ownership 할당과 안전한 클라이언트 측 Replication 훅을 갖춰야 합니다.

다음은 아이템의 Ownership을 플레이어의 인벤토리 컴포넌트로 안전하게 이전하기 위한, 실전에서 검증된 C++ 접근 방식입니다.

1단계: 서버 측 실행

모든 인벤토리 트랜잭션은 서버에서 발생해야 합니다. 플레이어가 줍기를 트리거하면 서버는 요청을 처리하고, Ownership을 할당하며, 복제된 인벤토리 배열을 업데이트합니다.

// Character 또는 Inventory Manager 컴포넌트 내부
void UInventoryComponent::Server_PickupItem_Implementation(AItemBase* ItemToPickup)
{
    if (!GetOwner()->HasAuthority())
    {
        return; // 서버에 있는지 다시 한번 확인
    }

    if (!IsValid(ItemToPickup) || ItemToPickup->IsPendingKillPending())
    {
        return;
    }

    // 1. 캐릭터에게 Network Ownership 할당
    // 이는 RPC 라우팅 및 GetOwner() 컨텍스트 업데이트에 필수적입니다.
    ItemToPickup->SetOwner(GetOwner());

    // 2. 데미지/이벤트 귀속을 위해 Instigator 설정
    ItemToPickup->SetInstigator(Cast<APawn>(GetOwner()));

    // 3. 월드에서 아이템 숨기기 (숨겨진 인벤토리로 이동하는 경우)
    ItemToPickup->SetActorHiddenInGame(true);
    ItemToPickup->SetActorEnableCollision(false);

    // 4. 복제된 인벤토리 배열에 추가
    ReplicatedInventory.Add(ItemToPickup);

    // 5. 클라이언트가 변경 사항을 즉시 받을 수 있도록 Net Update 강제 실행
    ItemToPickup->ForceNetUpdate();
    GetOwner()->ForceNetUpdate();
}

2단계: OnRep을 사용한 안전한 클라이언트 측 UI 업데이트

플레이어가 "줍기" 버튼을 누른 직후 UI가 인벤토리를 읽으려고 하면 오래된 데이터를 읽게 됩니다. 클라이언트는 서버가 업데이트된 ReplicatedInventory 배열과 새로운 Owner 참조를 복제할 때까지 기다려야 합니다.

Tick이나 입력 직후에 UI를 업데이트하는 대신 RepNotify (OnRep) 함수를 사용하십시오. 이를 통해 클라이언트가 서버의 "진실"이 도착한 후에만 동작하도록 보장할 수 있습니다.

// 헤더 파일 내부
UPROPERTY(ReplicatedUsing = OnRep_InventoryUpdated)
TArray<AItemBase*> ReplicatedInventory;

UFUNCTION()
void OnRep_InventoryUpdated();
// cpp 파일 내부
void UInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    // 대역폭 절약을 위해 소유 클라이언트에게만 인벤토리 배열 복제
    DOREPLIFETIME_CONDITION(UInventoryComponent, ReplicatedInventory, COND_OwnerOnly);
}

void UInventoryComponent::OnRep_InventoryUpdated()
{
    // 이 함수는 서버가 배열을 업데이트한 후에만 클라이언트에서 실행됩니다.
    // 이제 UI를 안전하게 업데이트할 수 있습니다.
    
    if (ACharacter* MyCharacter = Cast<ACharacter>(GetOwner()))
    {
        if (MyCharacter->IsLocallyControlled())
        {
            UpdateInventoryUI();
        }
    }
}

OnRep_InventoryUpdated를 기다림으로써 UI가 Item->GetOwner()를 호출할 때 Replication 레이어가 이미 포인터를 업데이트했음을 보장합니다. 캐릭터가 더 이상 뒤바뀌어 보이지 않을 것입니다.

빠른 속도의 Multiplayer 상호작용을 부드럽게 하고 줍기 중 시각적 끊김을 방지하는 고급 기술에 대해서는 How To Fix Player Location Desync In Uefn And Unreal Engine Multiplayer 튜토리얼을 확인하십시오.

엔진 레벨 Replication의 한계

GetOwner() 참조를 수정하고 OnRep 함수를 마스터하면 매치 내 인벤토리가 안정화됩니다. 하지만 Unreal Engine의 Replication 시스템은 Dedicated Server가 실행되는 동안에만 메모리에 존재합니다.

매치가 끝나면 어떻게 될까요? 익스트랙션 슈터, MMO 또는 영구적인 진행 요소가 있는 게임을 개발 중이라면 결국 완벽하게 복제된 C++ 배열을 데이터베이스에 저장해야 합니다.

과거에는 이를 위해 게임 개발을 중단하고 커스텀 백엔드를 구축해야 했습니다. REST API를 설정하고, PostgreSQL 데이터베이스를 구성하고, SSL 인증서를 관리하고, 플레이어가 인벤토리 데이터를 조작하지 못하도록 서버 측 검증 로직을 작성해야 했습니다.

여기서 현대적인 게임 아키텍처는 다른 접근 방식을 요구합니다. 인프라를 처음부터 구축하는 대신 horizOn을 사용할 수 있습니다.

Backend-as-a-Service를 통합하면 인프라 구축 단계를 완전히 건너뛸 수 있습니다. 서버 권위 코드가 줍기 처리를 완료하면 미리 구성된 백엔드 엔드포인트를 호출하여 해당 상태를 안전하게 커밋할 수 있습니다. horizOn을 사용하면 플레이어 인증, 영구 플레이어 데이터, 실시간 데이터베이스 확장과 같은 서비스가 기본으로 제공되므로 데이터베이스 샤드 관리 대신 게임플레이 버그 수정에 집중할 수 있습니다.

Multiplayer ActorComponents를 위한 5가지 베스트 프랙티스

Ownership 뒤바뀜이나 컴포넌트 Desync 문제를 다시는 겪지 않으려면 Unreal에서 Multiplayer 시스템을 구축할 때 다음의 검증된 규칙을 준수하십시오.

  1. 하드코딩된 플레이어 인덱스 사용 금지: Multiplayer 코드베이스에서 GetPlayerCharacter(0)를 제거하십시오. 항상 Pawn의 IsLocallyControlled()를 확인하거나 Player Controller를 통해 로컬 플레이어를 해결하십시오.
  2. Network Owner 명시적 설정: Actor를 플레이어 인벤토리로 이동할 때 항상 Item->SetOwner(PlayerCharacter)를 호출하십시오. 네트워크 라우팅을 Attachment에 의존하지 마십시오.
  3. 개인 데이터에 COND_OwnerOnly 사용: 인벤토리 배열이 매치의 모든 사람에게 복제될 필요는 거의 없습니다. DOREPLIFETIME_CONDITION(..., COND_OwnerOnly)를 사용하여 네트워크 대역폭을 절약하고 해커의 메모리 스누핑을 방지하십시오.
  4. UI 업데이트는 RepNotify에 의존: 강력한 롤백 시스템이 없는 한 클라이언트 측 입력 예측으로 UI 업데이트를 구동하지 마십시오. UI 업데이트는 서버의 진실을 엄격하게 반영하도록 OnRep 함수에서 구동하십시오.
  5. 서버에서 검증: 클라이언트의 ItemToPickup 참조를 맹목적으로 믿지 마십시오. 서버는 아이템이 존재하고, 줍기 범위 내에 있으며, 동일한 프레임에 다른 플레이어가 이미 줍지 않았는지 확인해야 합니다.

결론

GetOwner() 뒤바뀜과 같은 Multiplayer 버그는 코드 실행 방식에 대한 근본적인 기대를 깨뜨리기 때문에 매우 실망스럽습니다. 하지만 이러한 문제는 거의 항상 PIE 테스트 중 Unreal Engine의 실행 순서와 메모리 공간에 대한 오해에서 비롯됩니다.

엄격한 서버 권위를 강제하고, Network Ownership을 명시적으로 관리하며, Replication 업데이트 타이밍을 존중함으로써 네트워크 지연 시간에 관계없이 완벽하게 동기화된 인벤토리 시스템을 구축할 수 있습니다.

넷코드가 견고해지고 인벤토리 데이터를 매치 간에 유지할 준비가 되었다면, 이를 위해 데이터베이스 관리자가 될 필요는 없습니다. horizOn을 무료로 체험하고 Unreal Engine Dedicated Server를 확장 가능하고 즉시 프로덕션에 투입 가능한 백엔드에 몇 분 만에 연결해 보십시오.


출처: ActorComponent GetOwner() returns wrong character on clients (owners appear swapped)

이 대시보드는 다음에 의해 애정을 담아 만들어졌습니다 Projectmakers

© 2026 projectmakers.de

unknown-v1.91.1 / unknown-v--