Como projetar a arquitetura de um protótipo de shooter co-op local na Unreal Engine (Passo a passo)
Criar o protótipo de um jogo multiplayer co-op local é uma das maneiras mais rápidas de validar o seu core gameplay loop. Quando você tem dois jogadores no mesmo sofá, compartilhando a mesma tela, você sabe imediatamente se suas mecânicas de tiro têm impacto e se o seu level design incentiva o trabalho em equipe.
No entanto, construir um shooter multiplayer local na Unreal Engine é repleto de armadilhas arquitetônicas ocultas. Se você fixar (hardcode) seus inputs, acoplar sua UI ao "Player 0" ou ignorar os princípios de replicação desde o primeiro dia, seu rápido protótipo de fim de semana se tornará uma bagunça inescalável que exigirá centenas de horas de refatoração quando você eventualmente migrar para o multiplayer online.
Inspirado por um tutorial recente da comunidade sobre como construir um protótipo de shooter co-op em poucas horas, este guia detalha os passos técnicos exatos para projetar uma base robusta de multiplayer local na Unreal Engine. Abordaremos o spawning programático de jogadores, câmeras compartilhadas dinâmicas e como estruturar seus dados para que você possa escalar de forma limpa do co-op de sofá para um multiplayer online persistente.
Step 1: Entendendo a Arquitetura de Multiplayer Local da Unreal Engine
Antes de escrever qualquer código, você deve entender como a Unreal Engine lida com múltiplos jogadores em uma única máquina.
Em um jogo single-player padrão, você tem um UGameInstance, que contém um UWorld, que contém um ULocalPlayer. Esse jogador local é possuído por um APlayerController, que por sua vez possui o seu personagem APawn.
No multiplayer local, a hierarquia muda. O UGameInstance permanece um singleton, mas agora gerencia um array de objetos ULocalPlayer. Cada ULocalPlayer recebe seu próprio APlayerController.
O maior erro que os desenvolvedores cometem é assumir que GetWorld()->GetFirstPlayerController() funcionará para a lógica do jogo. No co-op local, depender do índice 0 significa que o Player 2 será completamente ignorado pelo estado do seu jogo, atualizações de UI e gatilhos de ambiente.
Step 2: Spawning Programático de Jogadores Locais
Embora você possa habilitar o split-screen nas Project Settings da Unreal e deixar a engine gerar jogadores automaticamente ao conectar um segundo gamepad, depender desse comportamento lhe dá zero controle sobre o processo de spawn, seleção de personagens ou atribuição de loadout.
Em vez disso, você deve lidar com a instanciação de jogadores manualmente dentro do seu AGameModeBase.
Aqui está uma implementação robusta em C++ para gerar um segundo jogador local dinamicamente quando ele pressiona o botão "Start" em um segundo gamepad:
void ACoopGameMode::SpawnSecondPlayer()
{
// Ensure we are running on the server/authority
if (!HasAuthority())
{
return;
}
UGameInstance* GameInstance = GetWorld()->GetGameInstance();
if (!GameInstance)
{
return;
}
FString ErrorMessage;
// Create a new local player at index 1 (Player 2)
// The 'true' boolean tells the engine to spawn a PlayerController automatically
ULocalPlayer* NewLocalPlayer = GameInstance->CreateLocalPlayer(1, ErrorMessage, true);
if (NewLocalPlayer)
{
UE_LOG(LogTemp, Log, TEXT("Successfully spawned Player 2. Controller ID: %d"), NewLocalPlayer->GetControllerId());
// Optional: Force a specific spawn point for Player 2
APlayerController* PC = NewLocalPlayer->GetPlayerController(GetWorld());
if (PC && PC->GetPawn())
{
FVector P2SpawnLocation = FVector(100.0f, -100.0f, 50.0f);
PC->GetPawn()->SetActorLocation(P2SpawnLocation);
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("Failed to spawn Player 2: %s"), *ErrorMessage);
}
}
Ao controlar a instanciação via CreateLocalPlayer, você pode interceptar o processo de spawn para atribuir meshes de personagens únicos ou armas iniciais com base em uma tela de seleção de personagens.
Step 3: Dominando a Matemática da Câmera de Tela Compartilhada
Para um shooter co-op top-down ou isométrico, o split-screen muitas vezes arruína a fidelidade visual e restringe a área de jogo. Uma câmera compartilhada dinâmica — popularizada por jogos como Helldivers ou Diablo — mantém todos os jogadores em uma única tela calculando sua posição média e afastando o zoom dinamicamente.
Para construir isso, você precisa de um ACameraActor dedicado que não esteja anexado a nenhum jogador específico. Em vez disso, esta câmera atualiza (ticks) a cada frame, encontrando a bounding box de todos os jogadores ativos.
Aqui está como você calcula o ponto central e o comprimento do zoom dinâmico:
void ASharedCameraController::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
FVector AverageLocation = FVector::ZeroVector;
float MaxDistance = 0.0f;
int32 PlayerCount = 0;
// Iterate through all active player controllers
for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator)
{
APlayerController* PC = Iterator->Get();
if (PC && PC->GetPawn())
{
FVector PlayerLoc = PC->GetPawn()->GetActorLocation();
AverageLocation += PlayerLoc;
PlayerCount++;
// Calculate distance to find the farthest player from the center
// (Requires a second pass in a real scenario, but simplified here for distance from origin)
float DistFromOrigin = PlayerLoc.Size();
if (DistFromOrigin > MaxDistance)
{
MaxDistance = DistFromOrigin;
}
}
}
if (PlayerCount > 0)
{
// Find the midpoint
AverageLocation /= PlayerCount;
// Smoothly interpolate the camera's target location
FVector NewLocation = FMath::VInterpTo(GetActorLocation(), AverageLocation, DeltaTime, 5.0f);
SetActorLocation(NewLocation);
// Dynamically adjust the SpringArm length based on player spread
// Assuming 'CameraSpringArm' is a valid USpringArmComponent pointer
float TargetZoom = FMath::Clamp(MaxDistance * 1.5f, 1000.0f, 3000.0f);
CameraSpringArm->TargetArmLength = FMath::FInterpTo(CameraSpringArm->TargetArmLength, TargetZoom, DeltaTime, 3.0f);
}
}
Essa lógica garante que a câmera acompanhe a ação suavemente. As funções VInterpTo e FInterpTo são críticas aqui; sem elas, a câmera se ajustará agressivamente quando um jogador morrer ou renascer, causando forte enjoo (motion sickness) aos seus jogadores.
Step 4: Sobrevivendo à Armadilha de UI do "Player 0"
Um dos bugs mais frustrantes no desenvolvimento de multiplayer local envolve as Interfaces de Usuário.
Quando você cria um widget usando o node padrão de Blueprint Create Widget (ou CreateWidget<UUserWidget>(GetWorld(), WidgetClass) em C++), a Unreal atribui por padrão a propriedade ao primeiro jogador local (Índice 0).
Se o Player 2 pegar munição, e sua lógica de UI atualizar o HUD de propriedade do Player 0, o contador de munição errado piscará. Pior ainda, se você usar AddToViewport(), o widget é renderizado globalmente, muitas vezes sobrepondo ou ignorando os limites do split-screen.
Para corrigir isso, sempre passe o Player Controller específico como o objeto proprietário ao criar widgets:
// CORRECT: Assigning ownership to the specific player
UUserWidget* PlayerHUD = CreateWidget<UUserWidget>(SpecificPlayerController, HUDWidgetClass);
// Use AddToPlayerScreen instead of AddToViewport for local multiplayer
PlayerHUD->AddToPlayerScreen();
AddToPlayerScreen() garante que, se você mudar de uma câmera compartilhada para split-screen, a UI se restringirá corretamente ao quadrante daquele jogador específico no monitor.
Step 5: O Ponto de Dor — Escalando o Estado Local para a Persistência Online
Protótipos de multiplayer local são incrivelmente enganosos. Como ambos os jogadores existem no mesmo espaço de memória na mesma máquina, você não precisa se preocupar com latência de rede, perda de pacotes ou autoridade do servidor. Você pode modificar diretamente a saúde do Player 2 a partir do projétil do Player 1.
No entanto, no momento em que você decide levar esse protótipo online, ou simplesmente quer salvar a progressão do jogador (como armas desbloqueadas ou pontuações altas) em diferentes sessões de jogo, a arquitetura desmorona.
Se você salvar os dados do jogador localmente usando objetos USaveGame, esses dados ficam vinculados à máquina física. Se o Player 2 for para casa e comprar o seu jogo, a progressão dele terá desaparecido. Para resolver isso, você precisa desacoplar o estado do seu jogador da máquina local e movê-lo para um backend na nuvem.
Construir isso por conta própria requer a configuração de balanceadores de carga, sharding de banco de dados e gerenciamento de certificados SSL — facilmente 4-6 semanas de trabalho apenas para ter um login seguro de jogador e um sistema de inventário funcionando. Com a horizOn, esses serviços Backend-as-a-Service vêm pré-configurados, permitindo que você lance o seu jogo em vez da sua infraestrutura.
Ao rotear seus perfis de jogador, loadouts e dados de sessão através de uma API backend no início do desenvolvimento, você garante que o "Player 2" seja um usuário autenticado com dados persistentes, em vez de apenas um convidado local transitório. Quando você estiver pronto para implementar o matchmaking online, a horizOn fornece sistemas de lobby prontos para uso que fazem a transição perfeita dos seus jogadores co-op locais para sessões online mais amplas.
Melhores Práticas para Prototipagem Co-Op
Para garantir que seu protótipo permaneça escalável e com bom desempenho, siga estas regras arquitetônicas desde o primeiro dia:
- Finja que é Online: Sempre use o framework de replicação da Unreal Engine (
HasAuthority(), RPCsServer_eUPROPERTY(Replicated)), mesmo se você estiver construindo apenas um protótipo local. Tratar a máquina local como um Listen Server desde o primeiro dia reduz o tempo de refatoração multiplayer em até 80% mais tarde. - Isole as Input Actions: Usando o Enhanced Input System, mapeie seus assets
UInputActionpara intenções lógicas de gameplay (ex: "FireWeapon"), não para botões de hardware. Isso permite que você remapeie dinamicamente Teclado/Mouse para o Player 1 e Gamepad para o Player 2 sem fixar índices. - Lide com Desconexões de Controle com Elegância: Sempre vincule a
FCoreDelegates::OnControllerConnectionChange. Se o controle do Player 2 desligar, seu jogo deve pausar automaticamente e solicitar a reconexão, em vez de deixar o personagem dele parado ocioso em um tiroteio. - Use Instanced Static Meshes para Projéteis: Em um shooter co-op, dois jogadores disparando armas de alta cadência podem gerar centenas de projéteis por segundo. Substitua os projéteis padrão baseados em Actor por
UInstancedStaticMeshComponentou sistemas de partículas Niagara para reduzir as draw calls de ~2000 para ~400 em cenas de combate pesado.
Construir um shooter co-op local é um desafio técnico incrivelmente gratificante. Ao estruturar corretamente o spawn de jogadores, a matemática da câmera e a persistência de dados desde o início, você garante que seu protótipo esteja pronto para escalar para um lançamento completo.
Source: Community Tutorial: Coop Local Multiplayer Shooter Prototype