Kembali ke Blog

Cara Merancang Arsitektur Prototipe Shooter Co-Op Lokal di Unreal Engine (Langkah demi Langkah)

Diterbitkan pada 20 Februari 2026
Cara Merancang Arsitektur Prototipe Shooter Co-Op Lokal di Unreal Engine (Langkah demi Langkah)

Membuat prototipe game multiplayer co-op lokal adalah salah satu cara tercepat untuk memvalidasi core gameplay loop Anda. Ketika Anda memiliki dua pemain di sofa yang sama, berbagi layar yang sama, Anda segera tahu apakah mekanik menembak Anda terasa berdampak dan apakah desain level Anda mendorong kerja sama tim.

Namun, membangun shooter multiplayer lokal di Unreal Engine penuh dengan jebakan arsitektur yang tersembunyi. Jika Anda melakukan hardcode pada input Anda, menggabungkan UI Anda ke "Player 0," atau mengabaikan prinsip-prinsip replikasi sejak hari pertama, prototipe akhir pekan Anda yang cepat akan menjadi kekacauan yang tidak dapat diskalakan yang membutuhkan ratusan jam untuk refactoring ketika Anda akhirnya beralih ke multiplayer online.

Terinspirasi oleh tutorial komunitas baru-baru ini tentang membangun prototipe shooter co-op dalam beberapa jam, panduan ini merinci langkah-langkah teknis yang tepat untuk merancang fondasi multiplayer lokal yang kuat di Unreal Engine. Kita akan membahas spawning pemain secara terprogram, kamera bersama yang dinamis, dan bagaimana menyusun data Anda sehingga Anda dapat menskalakan dengan bersih dari couch co-op ke multiplayer online yang persisten.

Step 1: Memahami Arsitektur Multiplayer Lokal Unreal Engine

Sebelum menulis kode apa pun, Anda harus memahami bagaimana Unreal Engine menangani banyak pemain di satu mesin.

Dalam game single-player standar, Anda memiliki satu UGameInstance, yang menampung satu UWorld, yang berisi satu ULocalPlayer. Pemain lokal tersebut dikendalikan (possessed) oleh APlayerController, yang pada gilirannya mengendalikan karakter Anda APawn.

Dalam multiplayer lokal, hierarkinya berubah. UGameInstance tetap menjadi singleton, tetapi sekarang mengelola array objek ULocalPlayer. Setiap ULocalPlayer mendapatkan APlayerController-nya sendiri.

Kesalahan terbesar yang dilakukan pengembang adalah mengasumsikan bahwa GetWorld()->GetFirstPlayerController() akan berfungsi untuk logika game. Dalam co-op lokal, mengandalkan indeks 0 berarti Player 2 akan sepenuhnya diabaikan oleh game state Anda, pembaruan UI, dan pemicu lingkungan.

Step 2: Spawning Pemain Lokal Secara Terprogram

Meskipun Anda dapat mengaktifkan split-screen di Project Settings Unreal dan membiarkan engine melakukan auto-spawn pemain saat menghubungkan gamepad kedua, mengandalkan perilaku ini memberi Anda nol kendali atas proses spawn, pemilihan karakter, atau penugasan loadout.

Sebaliknya, Anda harus menangani instansiasi pemain secara manual di dalam AGameModeBase Anda.

Berikut adalah implementasi C++ yang kuat untuk men-spawn pemain lokal kedua secara dinamis ketika mereka menekan tombol "Start" pada gamepad kedua:

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

Dengan mengontrol instansiasi melalui CreateLocalPlayer, Anda dapat mencegat proses spawn untuk menetapkan mesh karakter unik atau senjata awal berdasarkan layar pemilihan karakter.

Step 3: Menguasai Matematika Kamera Layar Bersama

Untuk shooter co-op top-down atau isometrik, split-screen sering kali merusak ketepatan visual dan membatasi area bermain. Kamera bersama yang dinamis—dipopulerkan oleh game seperti Helldivers atau Diablo—menjaga semua pemain di satu layar dengan menghitung posisi rata-rata mereka dan melakukan zoom out secara dinamis.

Untuk membangun ini, Anda memerlukan ACameraActor khusus yang tidak terpasang pada pemain tertentu. Sebaliknya, kamera ini melakukan tick setiap frame, menemukan bounding box dari semua pemain aktif.

Berikut adalah cara Anda menghitung titik tengah dan panjang zoom dinamis:

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

Logika ini memastikan kamera melacak aksi dengan mulus. Fungsi VInterpTo dan FInterpTo sangat penting di sini; tanpanya, kamera akan tersentak secara agresif ketika seorang pemain mati atau respawn, menyebabkan mabuk perjalanan (motion sickness) yang parah bagi pemain Anda.

Step 4: Bertahan dari Jebakan UI "Player 0"

Salah satu bug yang paling membuat frustrasi dalam pengembangan multiplayer lokal melibatkan User Interface.

Ketika Anda membuat widget menggunakan node Blueprint standar Create Widget (atau CreateWidget<UUserWidget>(GetWorld(), WidgetClass) di C++), Unreal secara default menetapkan kepemilikan ke pemain lokal pertama (Indeks 0).

Jika Player 2 mengambil amunisi, dan logika UI Anda memperbarui HUD yang dimiliki oleh Player 0, penghitung amunisi yang salah akan berkedip. Lebih buruk lagi, jika Anda menggunakan AddToViewport(), widget di-render secara global, sering kali tumpang tindih atau mengabaikan batas split-screen.

Untuk memperbaikinya, selalu teruskan Player Controller tertentu sebagai objek pemilik saat membuat widget:

// CORRECT: Assigning ownership to the specific player
UUserWidget* PlayerHUD = CreateWidget<UUserWidget>(SpecificPlayerController, HUDWidgetClass);

// Use AddToPlayerScreen instead of AddToViewport for local multiplayer
PlayerHUD->AddToPlayerScreen();

AddToPlayerScreen() memastikan bahwa jika Anda pernah beralih dari kamera bersama ke split-screen, UI akan dengan benar membatasi dirinya ke kuadran pemain tertentu di monitor.

Step 5: Titik Masalah — Menskalakan State Lokal ke Persistensi Online

Prototipe multiplayer lokal sangat menipu. Karena kedua pemain ada di ruang memori yang sama di mesin yang sama, Anda tidak perlu khawatir tentang latensi jaringan, packet loss, atau server authority. Anda dapat secara langsung memodifikasi kesehatan Player 2 dari proyektil Player 1.

Namun, saat Anda memutuskan untuk membawa prototipe ini online, atau sekadar ingin menyimpan progresi pemain (seperti senjata yang terbuka atau skor tinggi) di berbagai sesi permainan, arsitekturnya rusak.

Jika Anda menyimpan data pemain secara lokal menggunakan objek USaveGame, data tersebut terikat ke mesin fisik. Jika Player 2 pulang dan membeli game Anda, progresi mereka hilang. Untuk mengatasi ini, Anda perlu memisahkan player state Anda dari mesin lokal dan memindahkannya ke cloud backend.

Membangun ini sendiri membutuhkan pengaturan load balancer, database sharding, dan manajemen sertifikat SSL — dengan mudah 4-6 minggu kerja hanya untuk membuat login pemain yang aman dan sistem inventaris berjalan. Dengan horizOn, layanan Backend-as-a-Service ini sudah dikonfigurasi sebelumnya, memungkinkan Anda merilis game Anda alih-alih infrastruktur Anda.

Dengan merutekan profil pemain, loadout, dan data sesi Anda melalui backend API di awal pengembangan, Anda memastikan bahwa "Player 2" adalah pengguna yang diautentikasi dengan data persisten, bukan sekadar tamu lokal sementara. Saat Anda siap untuk mengimplementasikan matchmaking online, horizOn menyediakan sistem lobi out-of-the-box yang mentransisikan pemain co-op lokal Anda dengan mulus ke sesi online yang lebih luas.

Praktik Terbaik untuk Prototyping Co-Op

Untuk memastikan prototipe Anda tetap dapat diskalakan dan berkinerja baik, patuhi aturan arsitektur ini sejak hari pertama:

  1. Berpura-pura Ini Online: Selalu gunakan framework replikasi Unreal Engine (HasAuthority(), RPC Server_, dan UPROPERTY(Replicated)), bahkan jika Anda hanya membangun prototipe lokal. Memperlakukan mesin lokal sebagai Listen Server sejak hari pertama mengurangi waktu refactoring multiplayer hingga 80% di kemudian hari.
  2. Isolasi Input Actions: Menggunakan Enhanced Input System, petakan aset UInputAction Anda ke niat gameplay logis (misalnya, "FireWeapon"), bukan tombol perangkat keras. Ini memungkinkan Anda untuk secara dinamis memetakan ulang Keyboard/Mouse ke Player 1 dan Gamepad ke Player 2 tanpa melakukan hardcode pada indeks.
  3. Tangani Pemutusan Kontroler dengan Anggun: Selalu ikat ke FCoreDelegates::OnControllerConnectionChange. Jika kontroler Player 2 mati, game Anda harus secara otomatis menjeda dan meminta penyambungan kembali, daripada membiarkan karakter mereka berdiri diam dalam baku tembak.
  4. Gunakan Instanced Static Meshes untuk Proyektil: Dalam shooter co-op, dua pemain yang menembakkan senjata dengan rate-of-fire tinggi dapat men-spawn ratusan proyektil per detik. Ganti proyektil berbasis Actor standar dengan UInstancedStaticMeshComponent atau sistem partikel Niagara untuk mengurangi draw calls dari ~2000 menjadi ~400 dalam adegan pertempuran berat.

Membangun shooter co-op lokal adalah tantangan teknis yang sangat bermanfaat. Dengan menyusun spawning pemain, matematika kamera, dan persistensi data dengan benar sejak awal, Anda memastikan prototipe Anda siap untuk diskalakan menjadi rilis penuh.


Source: Community Tutorial: Coop Local Multiplayer Shooter Prototype