Voltar ao Blog

Pesadelos de Inventário Multiplayer: Corrigindo ActorComponent Owners trocados na Unreal Engine

Publicado em 1 de março de 2026
Pesadelos de Inventário Multiplayer: Corrigindo ActorComponent Owners trocados na Unreal Engine

Todo desenvolvedor de jogos multiplayer eventualmente bate de frente com o sistema de replication da Unreal Engine. Você cria um sistema de inventário, testa localmente e ele funciona perfeitamente. Então, você inicia um dedicated server com dois clientes, pega uma arma e o pesadelo começa.

O servidor sabe que você pegou o item. Seu cliente, no entanto, se comporta como se nada tivesse acontecido. Quando você faz o debug do ActorComponent imprimindo GetOwner(), descobre algo bizarro: o Character 0 pensa que seu dono é o Character 1, e o Character 1 pensa que seu dono é o Character 0.

Seus componentes parecem ter trocado de ownership pela rede.

Este desync específico — onde GetOwner() retorna o personagem errado nos clientes — é uma armadilha notória no desenvolvimento multiplayer da Unreal Engine. Ele quebra RPCs (Remote Procedure Calls), destrói a lógica da sua UI e abre as portas para exploits que acabam com o jogo.

Neste mergulho técnico, vamos desvendar exatamente por que este unreal engine actorcomponent getowner multiplayer fix é tão incompreendido, como o Play-In-Editor (PIE) mente ativamente para você e a arquitetura C++ passo a passo necessária para resolver permanentemente a replication de inventário.

A Anatomia do Bug: UActorComponent vs. AActor Ownership

Para entender por que seus componentes estão trocando de donos, primeiro temos que esclarecer um dos conceitos mais mal compreendidos na Unreal Engine: a diferença fundamental entre o network ownership de um Actor e o outer ownership de um Component.

UActorComponent::GetOwner() não é uma função de rede

Quando os desenvolvedores chamam SetOwner() em um AActor, eles estão interagindo com a arquitetura de rede da Unreal. O network ownership determina qual conexão de cliente tem permissão para enviar RPCs do tipo Server para aquele Actor específico.

No entanto, o UActorComponent não possui um dono replicado pela rede da mesma forma. Se você olhar o código-fonte de UActorComponent::GetOwner(), verá algo incrivelmente simples:

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

O dono de um ActorComponent é estritamente definido pelo seu Outer — o objeto que o contém na memória. Você não pode "trocar" dinamicamente o network owner de um componente pela rede sem alterar o dono do seu Actor pai, ou destruir e recriar o componente com um novo Outer.

Se o GetOwner() está retornando o personagem errado em um cliente, isso significa que uma de duas coisas aconteceu:

  1. A Armadilha do Índice Local do PIE: Seu código está dependendo de índices de jogadores locais (como GetPlayerCharacter(0)) para resolver referências, o que quebra completamente em testes multiplayer.
  2. Replication Race Conditions: Você está spawnando componentes dinamicamente e passando o Outer errado durante a instanciação no lado do cliente, ou sua UI está consultando o componente antes que o servidor tenha replicado as referências corretas.

Causa Raiz 1: A Armadilha do Índice Local do Play-In-Editor (PIE)

Quando você testa o multiplayer na Unreal Engine usando o modo "Play In Editor" (PIE) com a opção "Run Under One Process" marcada (a configuração padrão), todos os clientes rodam dentro do mesmo espaço de memória.

Muitos desenvolvedores inicializam sua UI ou widgets de inventário usando nós de Blueprint como Get Player Character (Index 0) ou equivalentes em C++ como UGameplayStatics::GetPlayerCharacter(GetWorld(), 0).

Isso é fatal no multiplayer.

Em um jogo standalone, o Index 0 é sempre o jogador local. Mas em uma sessão PIE de processo compartilhado, a Unreal Engine precisa lidar com múltiplos jogadores locais. Dependendo de exatamente quando e onde o GetPlayerCharacter(0) é chamado (especialmente dentro da inicialização de um ActorComponent replicado), o Cliente A pode acidentalmente pegar a referência do controller do Cliente B.

Consequentemente, quando o widget de inventário do Cliente A pergunta ao componente "Quem é o seu dono?", o widget está, na verdade, consultando o componente anexado ao Cliente B. Os donos parecem "trocados" porque sua UI está olhando para o endereço de memória errado.

A Correção: Resolvendo o Local Viewing Player

Nunca use índices de jogadores hardcoded em componentes multiplayer ou UI. Em vez disso, resolva o player controller através do owning player do widget ou da hierarquia real do componente.

// RUIM: Causará donos "trocados" em testes multiplayer no PIE
AActor* BadOwner = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);

// BOM: Resolvendo através da hierarquia Outer real do componente
AActor* TrueOwner = GetOwner();
APawn* OwningPawn = Cast<APawn>(TrueOwner);
if (OwningPawn && OwningPawn->IsLocallyControlled())
{
    // Agora sabemos com segurança que este componente pertence ao cliente local
    APlayerController* PC = Cast<APlayerController>(OwningPawn->GetController());
}

Se você estiver lidando com problemas de sincronização mais profundos, onde os estados dos jogadores estão completamente desalinhados entre o servidor e o cliente, você pode estar enfrentando um bug mais amplo da engine. Para mais contexto sobre como lidar com desyncs de estado, leia nosso guia sobre The Unreal Engine Multiplayer Sync Bug Ruining Your World States And How To Fix It.

Causa Raiz 2: Attachment vs. Network Ownership

Outra razão importante pela qual os componentes de inventário falham nos clientes é confundir Attachment com Ownership.

Quando um jogador pega uma arma ou um item de inventário (que geralmente é um AActor contendo vários ActorComponents), os desenvolvedores frequentemente anexam o item à mesh do personagem.

// Anexar a mesh NÃO concede network ownership!
ItemActor->AttachToComponent(CharacterMesh, FAttachmentTransformRules::SnapToTargetNotIncludingScale, "WeaponSocket");

Anexar um actor apenas atualiza sua hierarquia de transform. Isso não atualiza o NetOwner. Se você não chamar explicitamente SetOwner() no servidor, o cliente nunca ganhará autoridade para executar RPCs nos componentes desse item. Pior ainda, se o item replicar seu estado, o cliente pode receber a replicação do attachment, mas ainda ler GetOwner() == nullptr ou o dono anterior.

Quando um cliente tenta equipar a arma ou movê-la no inventário, o RPC do tipo Server é descartado porque o cliente não tem autoridade de rede, resultando no sintoma clássico de "o cliente se comporta como se o item nunca tivesse sido pego".

Arquitetura Passo a Passo: O Pickup com Autoridade do Servidor

Para resolver permanentemente essas trocas de ownership e desyncs, você deve arquitetar suas coletas de inventário para serem estritamente autoritativas no servidor, com atribuição explícita de ownership e hooks de replication seguros no lado do cliente.

Aqui está a abordagem C++ testada em batalha para transferir com segurança a ownership de um item para o componente de inventário de um jogador.

Passo 1: A Execução no Lado do Servidor

Todas as transações de inventário devem ocorrer no servidor. Quando o jogador aciona uma coleta, o servidor processa a solicitação, atribui a ownership e atualiza o array de inventário replicado.

// Dentro do seu Character ou Inventory Manager Component
void UInventoryComponent::Server_PickupItem_Implementation(AItemBase* ItemToPickup)
{
    if (!GetOwner()->HasAuthority())
    {
        return; // Verificação dupla de que estamos no servidor
    }

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

    // 1. Atribuir Network Ownership ao Personagem
    // Isso é CRÍTICO para o roteamento de RPC e atualização de contextos GetOwner()
    ItemToPickup->SetOwner(GetOwner());

    // 2. Definir o Instigator para atribuição de dano/eventos
    ItemToPickup->SetInstigator(Cast<APawn>(GetOwner()));

    // 3. Esconder o item no mundo (se estiver movendo para um inventário oculto)
    ItemToPickup->SetActorHiddenInGame(true);
    ItemToPickup->SetActorEnableCollision(false);

    // 4. Adicionar ao array de inventario replicado
    ReplicatedInventory.Add(ItemToPickup);

    // 5. Forçar um net update para que os clientes recebam as mudanças imediatamente
    ItemToPickup->ForceNetUpdate();
    GetOwner()->ForceNetUpdate();
}

Passo 2: Atualizações de UI Seguras no Cliente com OnRep

Se sua UI tentar ler o inventário imediatamente após o jogador pressionar o botão "Pickup", ela lerá dados obsoletos. O cliente deve esperar que o servidor replique o array ReplicatedInventory atualizado e a nova referência de Owner.

Em vez de atualizar a UI em um tick ou imediatamente após o input, use uma função RepNotify (OnRep). Isso garante que o cliente só aja depois que a verdade do servidor chegar.

// No seu arquivo de cabeçalho (.h)
UPROPERTY(ReplicatedUsing = OnRep_InventoryUpdated)
TArray<AItemBase*> ReplicatedInventory;

UFUNCTION()
void OnRep_InventoryUpdated();
// No seu arquivo .cpp
void UInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    // Replicar o array de inventário apenas para o cliente dono para economizar largura de banda
    DOREPLIFETIME_CONDITION(UInventoryComponent, ReplicatedInventory, COND_OwnerOnly);
}

void UInventoryComponent::OnRep_InventoryUpdated()
{
    // Esta função só dispara no cliente DEPOIS que o servidor atualizou o array.
    // Agora é seguro atualizar a UI.
    
    if (ACharacter* MyCharacter = Cast<ACharacter>(GetOwner()))
    {
        if (MyCharacter->IsLocallyControlled())
        {
            UpdateInventoryUI();
        }
    }
}

Ao esperar por OnRep_InventoryUpdated, você garante que, quando a UI chamar Item->GetOwner(), a camada de replication já terá atualizado os ponteiros. Os personagens não aparecerão mais trocados.

Para técnicas mais avançadas sobre como suavizar interações multiplayer rápidas e evitar stutter visual durante as coletas, confira nosso tutorial sobre How To Fix Player Location Desync In Uefn And Unreal Engine Multiplayer.

Os Limites da Replication em Nível de Engine

Corrigir suas referências de GetOwner() e dominar as funções OnRep tornará seu inventário estável durante a partida. No entanto, o sistema de replication da Unreal Engine só existe na memória enquanto o dedicated server está rodando.

O que acontece quando a partida termina? Se você está construindo um extraction shooter, um MMO ou qualquer jogo com progressão persistente, eventualmente terá que pegar esse array de C++ perfeitamente replicado e salvá-lo em um banco de dados.

Historicamente, isso significava interromper o desenvolvimento do jogo para construir um backend personalizado. Você precisaria configurar APIs REST, configurar bancos de dados PostgreSQL, gerenciar certificados SSL e escrever lógica de validação no lado do servidor para garantir que os jogadores não estejam falsificando seus dados de inventário.

É aqui que a arquitetura de jogos moderna exige uma abordagem diferente. Em vez de construir a infraestrutura do zero, você pode usar horizOn.

Ao integrar um Backend-as-a-Service, você pode ignorar completamente a fase de infraestrutura. Quando seu código server-authoritative termina de processar uma coleta, ele pode simplesmente chamar um endpoint de backend pré-configurado para salvar esse estado com segurança. Com horizOn, serviços como autenticação de jogadores, dados persistentes e escalonamento de banco de dados em tempo real vêm prontos para uso, permitindo que você foque em corrigir bugs de gameplay em vez de gerenciar shards de banco de dados.

5 Boas Práticas para ActorComponents Multiplayer

Para garantir que você nunca mais encontre trocas de ownership ou desyncs de componentes, siga estas regras testadas em batalha ao construir sistemas multiplayer na Unreal:

  1. Nunca Use Índices de Jogadores Hardcoded: Erradique o GetPlayerCharacter(0) do seu código multiplayer. Sempre resolva jogadores locais verificando IsLocallyControlled() no Pawn ou roteando através do Player Controller.
  2. Defina Network Owners Explicitamente: Ao mover um Actor para o inventario de um jogador, sempre chame Item->SetOwner(PlayerCharacter). Não confie no attachment para lidar com o roteamento de rede.
  3. Use COND_OwnerOnly para Dados Privados: Arrays de inventário raramente devem ser replicados para todos na partida. Use DOREPLIFETIME_CONDITION(..., COND_OwnerOnly) para economizar largura de banda de rede e evitar que hackers bisbilhotem a memória.
  4. Confie em RepNotifies para Atualizações de UI: Nunca execute atualizações de UI a partir de previsões de input do lado do cliente, a menos que você tenha um sistema de rollback robusto. Execute suas atualizações de UI a partir de funções OnRep para que elas reflitam estritamente a verdade do servidor.
  5. Valide no Servidor: Nunca confie cegamente na referência ItemToPickup do cliente. O servidor deve verificar se o item existe, se está dentro do alcance de coleta e se ainda não foi pego por outro jogador no mesmo frame.

Seguindo em Frente

Bugs de multiplayer como a troca de GetOwner() são frustrantes porque quebram as regras fundamentais de como esperamos que o código seja executado. No entanto, eles quase sempre se resumem a um mal-entendido sobre a ordem de execução da Unreal Engine e os espaços de memória durante os testes no PIE.

Ao aplicar uma autoridade estrita do servidor, gerenciar explicitamente o network ownership e respeitar o tempo das atualizações de replication, você pode construir um sistema de inventário que permanece perfeitamente sincronizado, independentemente da latência da rede.

Assim que seu netcode estiver à prova de balas e você estiver pronto para persistir esses dados de inventário entre as partidas, você não precisa se tornar um administrador de banco de dados para fazer isso acontecer. Experimente o horizOn gratuitamente e conecte seus dedicated servers da Unreal Engine a um backend escalável e pronto para produção em minutos.


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