Wie man die Architektur für einen lokalen Co-Op-Shooter-Prototyp in Unreal Engine entwirft (Schritt für Schritt)
Das Prototyping eines lokalen Co-Op-Multiplayer-Spiels ist einer der schnellsten Wege, um Ihren Core Gameplay Loop zu validieren. Wenn zwei Spieler auf derselben Couch sitzen und sich denselben Bildschirm teilen, wissen Sie sofort, ob sich Ihre Schussmechanik wuchtig anfühlt und ob Ihr Leveldesign Teamwork fördert.
Der Aufbau eines lokalen Multiplayer-Shooters in Unreal Engine ist jedoch voller versteckter architektonischer Fallen. Wenn Sie Ihre Inputs hartcodieren, Ihre UI an "Player 0" koppeln oder Replikationsprinzipien vom ersten Tag an ignorieren, wird Ihr schnelles Wochenend-Prototyp zu einem unskalierbaren Chaos, das Hunderte von Stunden für Refactoring erfordert, wenn Sie schließlich zum Online-Multiplayer übergehen.
Inspiriert von einem aktuellen Community-Tutorial über den Bau eines Co-Op-Shooter-Prototyps in wenigen Stunden, schlüsselt dieser Leitfaden die genauen technischen Schritte auf, um ein robustes lokales Multiplayer-Fundament in Unreal Engine zu entwerfen. Wir behandeln das programmatische Spawnen von Spielern, dynamische Shared Cameras und wie Sie Ihre Daten strukturieren, damit Sie sauber vom Couch-Co-Op zum persistenten Online-Multiplayer skalieren können.
Step 1: Die lokale Multiplayer-Architektur der Unreal Engine verstehen
Bevor Sie Code schreiben, müssen Sie verstehen, wie Unreal Engine mit mehreren Spielern auf einer einzigen Maschine umgeht.
In einem Standard-Einzelspieler-Spiel haben Sie eine UGameInstance, die ein UWorld hält, welches einen ULocalPlayer enthält. Dieser lokale Spieler wird von einem APlayerController besessen, der wiederum Ihren Charakter APawn besitzt.
Im lokalen Multiplayer ändert sich die Hierarchie. Die UGameInstance bleibt ein Singleton, verwaltet nun aber ein Array von ULocalPlayer-Objekten. Jeder ULocalPlayer erhält seinen eigenen APlayerController.
Der größte Fehler, den Entwickler machen, ist die Annahme, dass GetWorld()->GetFirstPlayerController() für die Spiellogik funktioniert. Im lokalen Co-Op bedeutet das Verlassen auf Index 0, dass Player 2 von Ihrem Game State, UI-Updates und Umgebungs-Triggern vollständig ignoriert wird.
Step 2: Lokale Spieler programmatisch spawnen
Obwohl Sie Split-Screen in den Project Settings von Unreal aktivieren und die Engine Spieler beim Anschließen eines zweiten Gamepads automatisch spawnen lassen können, gibt Ihnen dieses Verhalten null Kontrolle über den Spawn-Prozess, die Charakterauswahl oder die Loadout-Zuweisung.
Stattdessen sollten Sie die Spieler-Instanziierung manuell in Ihrer AGameModeBase handhaben.
Hier ist eine robuste C++-Implementierung, um einen zweiten lokalen Spieler dynamisch zu spawnen, wenn dieser die "Start"-Taste auf einem zweiten Gamepad drückt:
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);
}
}
Indem Sie die Instanziierung über CreateLocalPlayer steuern, können Sie den Spawn-Prozess abfangen, um basierend auf einem Charakterauswahlbildschirm einzigartige Charakter-Meshes oder Startwaffen zuzuweisen.
Step 3: Die Mathematik der Shared Screen Camera meistern
Für einen Top-Down- oder isometrischen Co-Op-Shooter ruiniert Split-Screen oft die visuelle Qualität und schränkt den Spielbereich ein. Eine dynamische Shared Camera – populär gemacht durch Spiele wie Helldivers oder Diablo – hält alle Spieler auf einem einzigen Bildschirm, indem sie ihre durchschnittliche Position berechnet und dynamisch herauszoomt.
Um dies zu bauen, benötigen Sie einen dedizierten ACameraActor, der an keinen spezifischen Spieler angehängt ist. Stattdessen tickt diese Kamera jeden Frame und findet die Bounding Box aller aktiven Spieler.
Hier ist, wie Sie den Mittelpunkt und die dynamische Zoom-Länge berechnen:
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);
}
}
Diese Logik stellt sicher, dass die Kamera der Action reibungslos folgt. Die Funktionen VInterpTo und FInterpTo sind hier kritisch; ohne sie wird die Kamera aggressiv springen, wenn ein Spieler stirbt oder respawnt, was bei Ihren Spielern schwere Motion Sickness verursachen kann.
Step 4: Die "Player 0" UI-Falle überleben
Einer der frustrierendsten Bugs in der lokalen Multiplayer-Entwicklung betrifft User Interfaces.
Wenn Sie ein Widget mit der Standard-Blueprint-Node Create Widget (oder CreateWidget<UUserWidget>(GetWorld(), WidgetClass) in C++) erstellen, weist Unreal standardmäßig dem ersten lokalen Spieler (Index 0) die Eigentümerschaft zu.
Wenn Player 2 Munition aufhebt und Ihre UI-Logik das HUD aktualisiert, das Player 0 gehört, blinkt der falsche Munitionszähler auf. Noch schlimmer: Wenn Sie AddToViewport() verwenden, wird das Widget global gerendert und überlappt oder ignoriert oft Split-Screen-Grenzen.
Um dies zu beheben, übergeben Sie immer den spezifischen Player Controller als besitzendes Objekt, wenn Sie Widgets erstellen:
// CORRECT: Assigning ownership to the specific player
UUserWidget* PlayerHUD = CreateWidget<UUserWidget>(SpecificPlayerController, HUDWidgetClass);
// Use AddToPlayerScreen instead of AddToViewport for local multiplayer
PlayerHUD->AddToPlayerScreen();
AddToPlayerScreen() stellt sicher, dass sich die UI, falls Sie jemals von einer Shared Camera zu Split-Screen wechseln, korrekt auf den Quadranten dieses spezifischen Spielers auf dem Monitor beschränkt.
Step 5: Der Schmerzpunkt — Lokalen State auf Online-Persistenz skalieren
Lokale Multiplayer-Prototypen sind unglaublich trügerisch. Da beide Spieler im selben Speicherbereich auf derselben Maschine existieren, müssen Sie sich keine Sorgen um Netzwerklatenz, Paketverlust oder Server Authority machen. Sie können die Gesundheit von Player 2 direkt durch das Projektil von Player 1 modifizieren.
In dem Moment jedoch, in dem Sie sich entscheiden, diesen Prototyp online zu bringen, oder einfach nur den Spielerfortschritt (wie freigeschaltete Waffen oder Highscores) über verschiedene Spielsitzungen hinweg speichern wollen, bricht die Architektur zusammen.
Wenn Sie Spielerdaten lokal mit USaveGame-Objekten speichern, sind diese Daten an die physische Maschine gebunden. Wenn Player 2 nach Hause geht und Ihr Spiel kauft, ist sein Fortschritt weg. Um dies zu lösen, müssen Sie Ihren Player State von der lokalen Maschine entkoppeln und in ein Cloud-Backend verschieben.
Dies selbst zu bauen, erfordert die Einrichtung von Load Balancern, Database Sharding und SSL-Zertifikatsmanagement — leicht 4-6 Wochen Arbeit, nur um ein sicheres Spieler-Login- und Inventarsystem zum Laufen zu bringen. Mit horizOn kommen diese Backend-as-a-Service-Dienste vorkonfiguriert, sodass Sie Ihr Spiel ausliefern können anstatt Ihrer Infrastruktur.
Indem Sie Ihre Spielerprofile, Loadouts und Session-Daten früh in der Entwicklung über eine Backend-API leiten, stellen Sie sicher, dass "Player 2" ein authentifizierter Benutzer mit persistenten Daten ist und nicht nur ein flüchtiger lokaler Gast. Wenn Sie bereit sind, Online-Matchmaking zu implementieren, bietet horizOn Out-of-the-box-Lobby-Systeme, die Ihre lokalen Co-Op-Spieler nahtlos in größere Online-Sessions überführen.
Best Practices für Co-Op-Prototyping
Um sicherzustellen, dass Ihr Prototyp skalierbar und performant bleibt, halten Sie sich vom ersten Tag an an diese architektonischen Regeln:
- Tun Sie so, als wäre es online: Verwenden Sie immer das Replikations-Framework der Unreal Engine (
HasAuthority(),Server_RPCs undUPROPERTY(Replicated)), auch wenn Sie nur einen lokalen Prototyp bauen. Die lokale Maschine vom ersten Tag an als Listen Server zu behandeln, reduziert die Refactoring-Zeit für Multiplayer später um bis zu 80%. - Isolieren Sie Input Actions: Verwenden Sie das Enhanced Input System und mappen Sie Ihre
UInputAction-Assets auf logische Gameplay-Absichten (z.B. "FireWeapon"), nicht auf Hardware-Tasten. Dies ermöglicht es Ihnen, Keyboard/Mouse dynamisch auf Player 1 und Gamepad auf Player 2 umzumappen, ohne Indizes hart zu codieren. - Behandeln Sie Controller-Disconnects elegant: Binden Sie immer an
FCoreDelegates::OnControllerConnectionChange. Wenn der Controller von Player 2 stirbt, sollte Ihr Spiel automatisch pausieren und zur Wiederverbindung auffordern, anstatt den Charakter in einem Feuergefecht untätig stehen zu lassen. - Verwenden Sie Instanced Static Meshes für Projektile: In einem Co-Op-Shooter können zwei Spieler, die Waffen mit hoher Feuerrate abfeuern, Hunderte von Projektilen pro Sekunde spawnen. Ersetzen Sie standardmäßige Actor-basierte Projektile durch
UInstancedStaticMeshComponentoder Niagara-Partikelsysteme, um Draw Calls in schweren Kampfszenen von ~2000 auf ~400 zu reduzieren.
Der Bau eines lokalen Co-Op-Shooters ist eine unglaublich lohnende technische Herausforderung. Indem Sie Ihr Player Spawning, die Kamera-Mathematik und die Datenpersistenz von Anfang an richtig strukturieren, stellen Sie sicher, dass Ihr Prototyp bereit ist, zu einem vollwertigen Release zu skalieren.
Source: Community Tutorial: Coop Local Multiplayer Shooter Prototype