Retour au Blog

Comment concevoir l'architecture d'un prototype de jeu de tir en coop locale dans Unreal Engine (Étape par étape)

Publié le 20 février 2026
Comment concevoir l'architecture d'un prototype de jeu de tir en coop locale dans Unreal Engine (Étape par étape)

Le prototypage d'un jeu multijoueur en coop locale est l'un des moyens les plus rapides de valider votre boucle de gameplay principale. Lorsque vous avez deux joueurs sur le même canapé, partageant le même écran, vous savez immédiatement si vos mécaniques de tir ont de l'impact et si votre level design encourage le travail d'équipe.

Cependant, créer un jeu de tir multijoueur local dans Unreal Engine est truffé de pièges architecturaux cachés. Si vous codez vos inputs en dur, liez votre UI au "Player 0", ou ignorez les principes de réplication dès le premier jour, votre petit prototype du week-end deviendra un désordre non évolutif qui nécessitera des centaines d'heures de refactoring lorsque vous passerez finalement au multijoueur en ligne.

Inspiré par un récent tutoriel de la communauté sur la création d'un prototype de jeu de tir en coop en quelques heures, ce guide détaille les étapes techniques exactes pour concevoir une base multijoueur locale robuste dans Unreal Engine. Nous aborderons le spawn programmatique des joueurs, les caméras partagées dynamiques, et la manière de structurer vos données pour pouvoir passer proprement de la coop de canapé à un multijoueur en ligne persistant.

Step 1: Comprendre l'architecture multijoueur locale d'Unreal Engine

Avant d'écrire le moindre code, vous devez comprendre comment Unreal Engine gère plusieurs joueurs sur une seule machine.

Dans un jeu solo standard, vous avez un UGameInstance, qui contient un UWorld, qui lui-même contient un ULocalPlayer. Ce joueur local est possédé par un APlayerController, qui à son tour possède votre personnage APawn.

En multijoueur local, la hiérarchie change. Le UGameInstance reste un singleton, mais il gère désormais un tableau d'objets ULocalPlayer. Chaque ULocalPlayer obtient son propre APlayerController.

La plus grande erreur que font les développeurs est de supposer que GetWorld()->GetFirstPlayerController() fonctionnera pour la logique du jeu. En coop locale, s'appuyer sur l'index 0 signifie que le Player 2 sera complètement ignoré par l'état de votre jeu, les mises à jour de l'UI et les déclencheurs environnementaux.

Step 2: Spawner les joueurs locaux de manière programmatique

Bien que vous puissiez activer le split-screen dans les Project Settings d'Unreal et laisser le moteur spawner automatiquement les joueurs lors de la connexion d'une deuxième manette, s'appuyer sur ce comportement ne vous donne aucun contrôle sur le processus de spawn, la sélection des personnages ou l'attribution de l'équipement.

Au lieu de cela, vous devriez gérer l'instanciation des joueurs manuellement dans votre AGameModeBase.

Voici une implémentation C++ robuste pour spawner dynamiquement un deuxième joueur local lorsqu'il appuie sur le bouton "Start" d'une deuxième manette :

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);
    }
}

En contrôlant l'instanciation via CreateLocalPlayer, vous pouvez intercepter le processus de spawn pour attribuer des meshes de personnages uniques ou des armes de départ basées sur un écran de sélection de personnages.

Step 3: Maîtriser les mathématiques de la caméra à écran partagé

Pour un jeu de tir en coop vu de haut ou isométrique, le split-screen ruine souvent la fidélité visuelle et restreint la zone de jeu. Une caméra partagée dynamique — popularisée par des jeux comme Helldivers ou Diablo — garde tous les joueurs sur un seul écran en calculant leur position moyenne et en dézoomant dynamiquement.

Pour construire cela, vous avez besoin d'un ACameraActor dédié qui n'est attaché à aucun joueur spécifique. Au lieu de cela, cette caméra s'actualise (tick) à chaque frame, trouvant la bounding box de tous les joueurs actifs.

Voici comment calculer le point central et la longueur du zoom dynamique :

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);
    }
}

Cette logique garantit que la caméra suit l'action en douceur. Les fonctions VInterpTo et FInterpTo sont critiques ici ; sans elles, la caméra se déplacera de manière agressive lorsqu'un joueur meurt ou respawn, provoquant un grave mal des transports chez vos joueurs.

Step 4: Survivre au piège de l'UI du "Player 0"

L'un des bugs les plus frustrants dans le développement multijoueur local concerne les interfaces utilisateur.

Lorsque vous créez un widget à l'aide du nœud Blueprint standard Create Widget (ou CreateWidget<UUserWidget>(GetWorld(), WidgetClass) en C++), Unreal attribue par défaut la propriété au premier joueur local (Index 0).

Si le Player 2 ramasse des munitions, et que votre logique d'UI met à jour le HUD appartenant au Player 0, le mauvais compteur de munitions clignotera. Pire encore, si vous utilisez AddToViewport(), le widget est rendu globalement, chevauchant souvent ou ignorant les limites du split-screen.

Pour corriger cela, passez toujours le Player Controller spécifique comme objet propriétaire lors de la création de 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() garantit que si vous passez un jour d'une caméra partagée à un split-screen, l'UI se contraindra correctement au quadrant de ce joueur spécifique sur le moniteur.

Step 5: Le point de douleur — Passer de l'état local à la persistance en ligne

Les prototypes multijoueurs locaux sont incroyablement trompeurs. Parce que les deux joueurs existent dans le même espace mémoire sur la même machine, vous n'avez pas à vous soucier de la latence du réseau, de la perte de paquets ou de l'autorité du serveur. Vous pouvez modifier directement la santé du Player 2 à partir du projectile du Player 1.

Cependant, au moment où vous décidez de mettre ce prototype en ligne, ou si vous souhaitez simplement sauvegarder la progression du joueur (comme les armes débloquées ou les meilleurs scores) à travers différentes sessions de jeu, l'architecture s'effondre.

Si vous sauvegardez les données des joueurs localement à l'aide d'objets USaveGame, ces données sont liées à la machine physique. Si le Player 2 rentre chez lui et achète votre jeu, sa progression est perdue. Pour résoudre ce problème, vous devez découpler l'état de votre joueur de la machine locale et le déplacer vers un backend cloud.

Construire cela vous-même nécessite la configuration de load balancers, le sharding de bases de données et la gestion des certificats SSL — facilement 4 à 6 semaines de travail juste pour obtenir un système de connexion et d'inventaire sécurisé. Avec horizOn, ces services Backend-as-a-Service sont préconfigurés, vous permettant de livrer votre jeu plutôt que votre infrastructure.

En routant vos profils de joueurs, vos loadouts et vos données de session via une API backend tôt dans le développement, vous vous assurez que le "Player 2" est un utilisateur authentifié avec des données persistantes, plutôt qu'un simple invité local éphémère. Lorsque vous êtes prêt à implémenter le matchmaking en ligne, horizOn fournit des systèmes de lobby prêts à l'emploi qui font passer de manière transparente vos joueurs en coop locale vers des sessions en ligne plus vastes.

Bonnes pratiques pour le prototypage en coop

Pour vous assurer que votre prototype reste évolutif et performant, respectez ces règles architecturales dès le premier jour :

  1. Faites comme si c'était en ligne : Utilisez toujours le framework de réplication d'Unreal Engine (HasAuthority(), RPCs Server_, et UPROPERTY(Replicated)), même si vous ne construisez qu'un prototype local. Traiter la machine locale comme un Listen Server dès le premier jour réduit le temps de refactoring multijoueur jusqu'à 80 % par la suite.
  2. Isolez les Input Actions : En utilisant l'Enhanced Input System, mappez vos assets UInputAction à des intentions de gameplay logiques (ex. "FireWeapon"), et non à des boutons matériels. Cela vous permet de remapper dynamiquement le Clavier/Souris au Player 1 et la Manette au Player 2 sans coder les index en dur.
  3. Gérez les déconnexions de manettes avec élégance : Liez toujours à FCoreDelegates::OnControllerConnectionChange. Si la manette du Player 2 s'éteint, votre jeu devrait automatiquement se mettre en pause et demander une reconnexion, plutôt que de laisser son personnage inactif dans une fusillade.
  4. Utilisez des Instanced Static Meshes pour les projectiles : Dans un jeu de tir en coop, deux joueurs tirant avec des armes à cadence élevée peuvent spawner des centaines de projectiles par seconde. Remplacez les projectiles standard basés sur des Actor par des UInstancedStaticMeshComponent ou des systèmes de particules Niagara pour réduire les draw calls d'environ 2000 à environ 400 dans les scènes de combat intenses.

Construire un jeu de tir en coop locale est un défi technique incroyablement gratifiant. En structurant correctement le spawn de vos joueurs, les mathématiques de la caméra et la persistance des données dès le départ, vous vous assurez que votre prototype est prêt à évoluer vers une version complète.


Source: Community Tutorial: Coop Local Multiplayer Shooter Prototype