Kembali ke Blog

Mimpi Buruk Inventory Multiplayer: Memperbaiki ActorComponent Owner yang Tertukar di Unreal Engine

Diterbitkan pada 1 Maret 2026
Mimpi Buruk Inventory Multiplayer: Memperbaiki ActorComponent Owner yang Tertukar di Unreal Engine

Setiap developer game multiplayer pada akhirnya akan membentur dinding sistem replication Unreal Engine. Anda membangun sistem inventory, mengujinya secara lokal, dan semuanya berjalan mulus. Kemudian Anda menjalankan dedicated server dengan dua client, mengambil senjata, dan mimpi buruk pun dimulai.

Server tahu Anda mengambil item tersebut. Namun, client Anda berperilaku seolah-olah tidak terjadi apa-apa. Saat Anda melakukan debug pada ActorComponent dengan mencetak GetOwner(), Anda menemukan sesuatu yang membingungkan: Karakter 0 mengira pemiliknya adalah Karakter 1, dan Karakter 1 mengira pemiliknya adalah Karakter 0.

Component Anda tampaknya telah bertukar ownership melalui jaringan.

Desync spesifik ini—di mana GetOwner() mengembalikan karakter yang salah pada client—adalah jebakan terkenal dalam pengembangan multiplayer Unreal Engine. Hal ini merusak RPC (Remote Procedure Calls), menghancurkan logika UI Anda, dan membuka pintu bagi exploit yang merusak game.

Dalam pembahasan teknis mendalam ini, kita akan mengupas tuntas mengapa unreal engine actorcomponent getowner multiplayer fix ini begitu sering disalahpahami, bagaimana Play-In-Editor (PIE) secara aktif membohongi Anda, dan arsitektur C++ langkah demi langkah yang diperlukan untuk menyelesaikan replication inventory secara permanen.

Anatomi Bug: UActorComponent vs. AActor Ownership

Untuk memahami mengapa component Anda bertukar pemilik, pertama-tama kita harus memperjelas salah satu konsep yang paling sering disalahpahami di Unreal Engine: perbedaan mendasar antara network ownership Actor dan outer ownership Component.

UActorComponent::GetOwner() Bukanlah Fungsi Jaringan

Ketika developer memanggil SetOwner() pada AActor, mereka sedang berinteraksi dengan arsitektur jaringan Unreal. Network ownership menentukan koneksi client mana yang diizinkan untuk mengirim Server RPC untuk Actor spesifik tersebut.

Namun, UActorComponent tidak memiliki pemilik yang direplikasi secara jaringan dengan cara yang sama. Jika Anda melihat source code untuk UActorComponent::GetOwner(), Anda akan melihat sesuatu yang sangat sederhana:

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

Pemilik ActorComponent secara ketat ditentukan oleh Outer-nya—objek yang menampungnya di memori. Anda tidak dapat secara dinamis "menukar" network owner dari sebuah component melalui jaringan tanpa mengubah pemilik dari Actor induknya, atau menghancurkan dan membuat ulang component tersebut dengan Outer yang baru.

Jika GetOwner() mengembalikan karakter yang salah pada client, itu berarti salah satu dari dua hal ini telah terjadi:

  1. Jebakan Indeks Lokal PIE: Kode Anda mengandalkan indeks player lokal (seperti GetPlayerCharacter(0)) untuk menyelesaikan referensi, yang benar-benar hancur dalam pengujian multiplayer.
  2. Replication Race Conditions: Anda melakukan spawn component secara dinamis dan memberikan Outer yang salah selama instansiasi di sisi client, atau UI Anda menanyakan component tersebut sebelum server mereplikasi referensi yang benar.

Akar Masalah 1: Jebakan Indeks Lokal Play-In-Editor (PIE)

Saat Anda menguji multiplayer di Unreal Engine menggunakan mode "Play In Editor" (PIE) dengan opsi "Run Under One Process" dicentang (pengaturan default), semua client berjalan dalam ruang memori yang sama.

Banyak developer menginisialisasi UI atau widget inventory mereka menggunakan node Blueprint seperti Get Player Character (Index 0) atau padanan C++ seperti UGameplayStatics::GetPlayerCharacter(GetWorld(), 0).

Ini fatal dalam multiplayer.

Dalam game standalone, Index 0 selalu merupakan player lokal. Namun dalam sesi PIE dengan proses bersama, Unreal Engine harus mengelola beberapa player lokal sekaligus. Tergantung kapan dan di mana GetPlayerCharacter(0) dipanggil (terutama di dalam inisialisasi ActorComponent yang direplikasi), Client A mungkin secara tidak sengaja mengambil referensi controller milik Client B.

Akibatnya, ketika widget inventory Client A bertanya kepada component "Siapa pemilikmu?", widget tersebut sebenarnya menanyakan component yang terpasang pada Client B. Pemilik tampak "tertukar" karena UI Anda melihat alamat memori yang salah.

Solusinya: Menyelesaikan Local Viewing Player

Jangan pernah menggunakan indeks player yang di-hardcode dalam component atau UI multiplayer. Sebaliknya, selesaikan player controller melalui owning player milik widget atau hierarki aktual dari component tersebut.

// BURUK: Akan menyebabkan pemilik "tertukar" dalam pengujian multiplayer PIE
AActor* BadOwner = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);

// BAIK: Menyelesaikan melalui hierarki Outer aktual dari component
AActor* TrueOwner = GetOwner();
APawn* OwningPawn = Cast<APawn>(TrueOwner);
if (OwningPawn && OwningPawn->IsLocallyControlled())
{
    // Kita sekarang tahu dengan aman bahwa component ini milik client lokal
    APlayerController* PC = Cast<APlayerController>(OwningPawn->GetController());
}

Jika Anda berurusan dengan masalah sinkronisasi yang lebih dalam di mana status player benar-benar tidak sejajar antara server dan client, Anda mungkin menghadapi bug engine yang lebih luas. Untuk konteks lebih lanjut tentang menangani desync status, baca panduan kami di The Unreal Engine Multiplayer Sync Bug Ruining Your World States And How To Fix It.

Akar Masalah 2: Attachment vs. Network Ownership

Alasan utama lainnya mengapa component inventory gagal pada client adalah membingungkan antara Attachment dengan Ownership.

Ketika seorang player mengambil senjata atau item inventory (yang sering kali merupakan AActor yang berisi berbagai ActorComponents), developer sering kali menempelkan (attach) item tersebut ke mesh karakter.

// Menempelkan mesh TIDAK memberikan network ownership!
ItemActor->AttachToComponent(CharacterMesh, FAttachmentTransformRules::SnapToTargetNotIncludingScale, "WeaponSocket");

Menempelkan actor hanya memperbarui hierarki transform-nya. Hal ini tidak memperbarui NetOwner. Jika Anda tidak secara eksplisit memanggil SetOwner() di server, client tidak akan pernah mendapatkan authority untuk mengeksekusi RPC pada component item tersebut. Lebih buruk lagi, jika item tersebut mereplikasi statusnya, client mungkin menerima replication attachment tetapi tetap membaca GetOwner() == nullptr atau pemilik sebelumnya.

Ketika client mencoba melengkapi senjata atau memindahkannya di inventory, RPC Server akan dibatalkan karena client tidak memiliki network authority, yang mengakibatkan gejala klasik "client berperilaku seolah-olah item tersebut tidak pernah diambil".

Arsitektur Langkah-demi-Langkah: Pickup yang Server-Authoritative

Untuk menyelesaikan pertukaran ownership dan desync ini secara permanen, Anda harus merancang pengambilan inventory agar benar-benar server-authoritative, dengan penetapan ownership yang eksplisit dan hook replication sisi client yang aman.

Berikut adalah pendekatan C++ yang telah teruji untuk mentransfer ownership item secara aman ke component inventory player.

Langkah 1: Eksekusi di Sisi Server

Semua transaksi inventory harus terjadi di server. Ketika player memicu pengambilan, server memproses permintaan, menetapkan ownership, dan memperbarui array inventory yang direplikasi.

// Di dalam Component Character atau Inventory Manager Anda
void UInventoryComponent::Server_PickupItem_Implementation(AItemBase* ItemToPickup)
{
    if (!GetOwner()->HasAuthority())
    {
        return; // Periksa kembali apakah kita berada di server
    }

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

    // 1. Tetapkan Network Ownership ke Karakter
    // Ini KRITIS untuk perutean RPC dan memperbarui konteks GetOwner()
    ItemToPickup->SetOwner(GetOwner());

    // 2. Atur Instigator untuk atribusi damage/event
    ItemToPickup->SetInstigator(Cast<APawn>(GetOwner()));

    // 3. Sembunyikan item di dunia (jika pindah ke inventory tersembunyi)
    ItemToPickup->SetActorHiddenInGame(true);
    ItemToPickup->SetActorEnableCollision(false);

    // 4. Tambahkan ke array inventory yang direplikasi
    ReplicatedInventory.Add(ItemToPickup);

    // 5. Paksa net update agar client segera mendapatkan perubahan
    ItemToPickup->ForceNetUpdate();
    GetOwner()->ForceNetUpdate();
}

Langkah 2: Pembaruan UI Sisi Client yang Aman dengan OnRep

Jika UI Anda segera mencoba membaca inventory setelah player menekan tombol "Pickup", ia akan membaca data lama. Client harus menunggu server mereplikasi array ReplicatedInventory yang telah diperbarui dan referensi Owner yang baru.

Alih-alih memperbarui UI pada tick atau segera setelah input, gunakan fungsi RepNotify (OnRep). Ini memastikan client hanya bertindak setelah kebenaran dari server tiba.

// Di file header Anda
UPROPERTY(ReplicatedUsing = OnRep_InventoryUpdated)
TArray<AItemBase*> ReplicatedInventory;

UFUNCTION()
void OnRep_InventoryUpdated();
// Di file cpp Anda
void UInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    // Replikasi array inventory hanya ke client pemilik untuk menghemat bandwidth
    DOREPLIFETIME_CONDITION(UInventoryComponent, ReplicatedInventory, COND_OwnerOnly);
}

void UInventoryComponent::OnRep_InventoryUpdated()
{
    // Fungsi ini hanya berjalan di client SETELAH server memperbarui array.
    // Sekarang aman untuk memperbarui UI.
    
    if (ACharacter* MyCharacter = Cast<ACharacter>(GetOwner()))
    {
        if (MyCharacter->IsLocallyControlled())
        {
            UpdateInventoryUI();
        }
    }
}

Dengan menunggu OnRep_InventoryUpdated, Anda menjamin bahwa ketika UI memanggil Item->GetOwner(), lapisan replication sudah memperbarui pointer-nya. Karakter tidak akan lagi tampak tertukar.

Untuk teknik yang lebih canggih dalam menghaluskan interaksi multiplayer yang cepat dan mencegah stutter visual selama pengambilan, lihat tutorial kami tentang How To Fix Player Location Desync In Uefn And Unreal Engine Multiplayer.

Batasan Replication Tingkat Engine

Memperbaiki referensi GetOwner() dan menguasai fungsi OnRep akan membuat inventory dalam pertandingan Anda stabil. Namun, sistem replication Unreal Engine hanya ada di memori selama dedicated server berjalan.

Apa yang terjadi saat pertandingan berakhir? Jika Anda membangun extraction shooter, MMO, atau game apa pun dengan progres yang persisten, Anda pada akhirnya harus mengambil array C++ yang direplikasi dengan sempurna itu dan menyimpannya ke database.

Secara historis, ini berarti menghentikan pengembangan game untuk membangun backend kustom. Anda perlu menyiapkan REST API, mengonfigurasi database PostgreSQL, mengelola sertifikat SSL, dan menulis logika validasi sisi server untuk memastikan player tidak memalsukan payload inventory mereka.

Di sinilah arsitektur game modern membutuhkan pendekatan yang berbeda. Alih-alih membangun infrastruktur dari nol, Anda dapat menggunakan horizOn.

Dengan mengintegrasikan Backend-as-a-Service, Anda dapat melewati fase infrastruktur sepenuhnya. Ketika kode server-authoritative Anda selesai memproses pengambilan, ia cukup memanggil endpoint backend yang telah dikonfigurasi sebelumnya untuk menyimpan status tersebut secara aman. Dengan horizOn, layanan seperti autentikasi player, data player persisten, dan penskalaan database real-time sudah tersedia secara langsung, membiarkan Anda fokus memperbaiki bug gameplay daripada mengelola shard database.

5 Praktik Terbaik untuk ActorComponents Multiplayer

Untuk memastikan Anda tidak pernah mengalami pertukaran ownership atau desync component lagi, patuhi aturan yang telah teruji ini saat membangun sistem multiplayer di Unreal:

  1. Jangan Pernah Gunakan Indeks Player yang Di-hardcode: Hapus GetPlayerCharacter(0) dari codebase multiplayer Anda. Selalu selesaikan player lokal dengan memeriksa IsLocallyControlled() pada Pawn atau melalui Player Controller.
  2. Tetapkan Network Owner Secara Eksplisit: Saat memindahkan Actor ke dalam inventory player, selalu panggil Item->SetOwner(PlayerCharacter). Jangan mengandalkan attachment untuk menangani perutean jaringan.
  3. Gunakan COND_OwnerOnly untuk Data Pribadi: Array inventory jarang perlu direplikasi ke semua orang dalam pertandingan. Gunakan DOREPLIFETIME_CONDITION(..., COND_OwnerOnly) untuk menghemat bandwidth jaringan dan mencegah pencurian memori oleh hacker.
  4. Andalkan RepNotify untuk Pembaruan UI: Jangan pernah menjalankan pembaruan UI dari prediksi input sisi client kecuali Anda memiliki sistem rollback yang kuat. Jalankan pembaruan UI Anda dari fungsi OnRep sehingga mereka secara ketat mencerminkan kebenaran server.
  5. Validasi di Server: Jangan pernah mempercayai referensi ItemToPickup dari client secara membabi buta. Server harus memverifikasi bahwa item tersebut ada, berada dalam jangkauan pengambilan, dan belum diambil oleh player lain dalam frame yang sama.

Melangkah Maju

Bug multiplayer seperti pertukaran GetOwner() sangat menjengkelkan karena mereka merusak aturan mendasar tentang bagaimana kita mengharapkan kode dieksekusi. Namun, hal itu hampir selalu bermuara pada kesalahpahaman tentang urutan eksekusi Unreal Engine dan ruang memori selama pengujian PIE.

Dengan menegakkan authority server yang ketat, mengelola network ownership secara eksplisit, dan menghormati waktu pembaruan replication, Anda dapat membangun sistem inventory yang tetap sinkron dengan sempurna, terlepas dari latensi jaringan.

Setelah netcode Anda kokoh dan Anda siap untuk menyimpan data inventory tersebut antar pertandingan, Anda tidak perlu menjadi administrator database untuk mewujudkannya. Coba horizOn secara gratis dan hubungkan dedicated server Unreal Engine Anda ke backend yang skalabel dan siap produksi dalam hitungan menit.


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