Bloga Dön

Unreal Engine'de Yerel Co-Op Nişancı Prototipi Mimarisi Nasıl Tasarlanır (Adım Adım)

Yayınlanma tarihi 20 Şubat 2026
Unreal Engine'de Yerel Co-Op Nişancı Prototipi Mimarisi Nasıl Tasarlanır (Adım Adım)

Yerel bir co-op çok oyunculu oyunun prototipini oluşturmak, temel oynanış döngünüzü (core gameplay loop) doğrulamanın en hızlı yollarından biridir. İki oyuncu aynı koltukta oturup aynı ekranı paylaştığında, atış mekaniklerinizin etkili hissettirip hissettirmediğini ve bölüm tasarımınızın takım çalışmasını teşvik edip etmediğini anında anlarsınız.

Ancak, Unreal Engine'de yerel bir çok oyunculu nişancı oyunu geliştirmek gizli mimari tuzaklarla doludur. Girdilerinizi (inputs) sabit kodlarsanız (hardcode), arayüzünüzü (UI) "Player 0"a bağlarsanız veya ilk günden itibaren replikasyon (replication) ilkelerini göz ardı ederseniz, o hızlı hafta sonu prototipiniz, eninde sonunda çevrimiçi çok oyunculu moda geçtiğinizde yüzlerce saatlik yeniden düzenleme (refactoring) gerektiren, ölçeklenemez bir karmaşaya dönüşecektir.

Birkaç saat içinde bir co-op nişancı prototipi oluşturmaya yönelik yakın tarihli bir topluluk eğitiminden ilham alan bu rehber, Unreal Engine'de sağlam bir yerel çok oyunculu temel tasarlamak için gereken kesin teknik adımları ayrıntılarıyla anlatıyor. Programatik oyuncu oluşturma (spawning), dinamik paylaşımlı kameralar ve verilerinizi nasıl yapılandıracağınızı ele alacağız, böylece koltuk co-op'undan kalıcı çevrimiçi çok oyunculu moda temiz bir şekilde ölçeklenebilirsiniz.

Step 1: Unreal Engine'in Yerel Çok Oyunculu Mimarisini Anlamak

Herhangi bir kod yazmadan önce, Unreal Engine'in tek bir makinede birden fazla oyuncuyu nasıl idare ettiğini anlamalısınız.

Standart bir tek oyunculu oyunda, bir UWorld tutan bir UGameInstanceınız vardır ve bu da bir ULocalPlayer içerir. Bu yerel oyuncu bir APlayerController tarafından kontrol edilir (possessed), o da sırasıyla karakteriniz olan APawnı kontrol eder.

Yerel çok oyunculu modda hiyerarşi değişir. UGameInstance bir singleton olarak kalır, ancak artık bir ULocalPlayer nesneleri dizisini (array) yönetir. Her ULocalPlayer kendi APlayerControllerını alır.

Geliştiricilerin yaptığı en büyük hata, GetWorld()->GetFirstPlayerController() fonksiyonunun oyun mantığı için çalışacağını varsaymaktır. Yerel co-op'ta 0 indeksine güvenmek, Player 2'nin oyun durumunuz (game state), UI güncellemeleriniz ve çevresel tetikleyicileriniz tarafından tamamen görmezden gelineceği anlamına gelir.

Step 2: Yerel Oyuncuları Programatik Olarak Oluşturmak (Spawning)

Unreal'ın Project Settings bölümünden bölünmüş ekranı (split-screen) etkinleştirebilir ve ikinci bir gamepad bağlandığında motorun oyuncuları otomatik olarak oluşturmasına izin verebilirsiniz, ancak bu davranışa güvenmek size oluşturma süreci, karakter seçimi veya ekipman (loadout) ataması üzerinde sıfır kontrol sağlar.

Bunun yerine, oyuncu örneklendirmesini (instantiation) AGameModeBase sınıfınız içinde manuel olarak ele almalısınız.

İkinci bir gamepad'de "Start" düğmesine basıldığında ikinci bir yerel oyuncuyu dinamik olarak oluşturmak için sağlam bir C++ uygulaması şöyledir:

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

Örneklendirmeyi CreateLocalPlayer aracılığıyla kontrol ederek, bir karakter seçim ekranına dayalı olarak benzersiz karakter mesh'leri veya başlangıç silahları atamak için oluşturma sürecine müdahale edebilirsiniz.

Step 3: Paylaşımlı Ekran Kamera Matematiğinde Ustalaşmak

Yukarıdan aşağıya (top-down) veya izometrik bir co-op nişancı oyunu için bölünmüş ekran genellikle görsel kaliteyi bozar ve oyun alanını kısıtlar. Helldivers veya Diablo gibi oyunlarla popülerleşen dinamik paylaşımlı kamera, tüm aktif oyuncuların ortalama konumunu hesaplayıp dinamik olarak uzaklaşarak herkesi tek bir ekranda tutar.

Bunu oluşturmak için, belirli bir oyuncuya bağlı olmayan özel bir ACameraActora ihtiyacınız vardır. Bunun yerine, bu kamera her karede (frame) güncellenerek (ticks) tüm aktif oyuncuların sınırlayıcı kutusunu (bounding box) bulur.

Merkez noktasını ve dinamik yakınlaştırma uzunluğunu şu şekilde hesaplarsınız:

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

Bu mantık, kameranın aksiyonu pürüzsüz bir şekilde takip etmesini sağlar. VInterpTo ve FInterpTo fonksiyonları burada kritiktir; onlar olmadan, bir oyuncu öldüğünde veya yeniden doğduğunda kamera agresif bir şekilde sıçrayacak ve oyuncularınızda ciddi hareket hastalığına (motion sickness) neden olacaktır.

Step 4: "Player 0" UI Tuzağından Kurtulmak

Yerel çok oyunculu geliştirmedeki en sinir bozucu hatalardan biri Kullanıcı Arayüzlerini (UI) içerir.

Standart Blueprint düğümü Create Widget (veya C++'ta CreateWidget<UUserWidget>(GetWorld(), WidgetClass)) kullanarak bir widget oluşturduğunuzda, Unreal varsayılan olarak sahipliği ilk yerel oyuncuya (İndeks 0) atar.

Eğer Player 2 cephane alırsa ve UI mantığınız Player 0'a ait HUD'ı güncellerse, yanlış cephane sayacı yanıp sönecektir. Daha da kötüsü, AddToViewport() kullanırsanız, widget global olarak işlenir (rendered), genellikle bölünmüş ekran sınırlarıyla örtüşür veya onları görmezden gelir.

Bunu düzeltmek için, widget oluştururken her zaman belirli Player Controller'ı sahip nesne olarak geçirin:

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

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

AddToPlayerScreen(), paylaşımlı kameradan bölünmüş ekrana geçerseniz, UI'ın monitörde o belirli oyuncunun çeyreğine kendini doğru bir şekilde kısıtlamasını sağlar.

Step 5: Sıkıntılı Nokta — Yerel Durumu Çevrimiçi Kalıcılığa Ölçeklendirmek

Yerel çok oyunculu prototipler inanılmaz derecede aldatıcıdır. Her iki oyuncu da aynı makinede aynı bellek alanında var olduğundan, ağ gecikmesi (network latency), paket kaybı veya sunucu yetkisi (server authority) hakkında endişelenmenize gerek yoktur. Player 2'nin sağlığını doğrudan Player 1'in mermisinden değiştirebilirsiniz.

Ancak, bu prototipi çevrimiçi yapmaya karar verdiğiniz an veya sadece oyuncu ilerlemesini (açılan silahlar veya yüksek skorlar gibi) farklı oyun oturumları arasında kaydetmek istediğinizde mimari çöker.

Oyuncu verilerini USaveGame nesneleri kullanarak yerel olarak kaydederseniz, bu veriler fiziksel makineye bağlanır. Player 2 eve gidip oyununuzu satın alırsa, ilerlemesi kaybolur. Bunu çözmek için, oyuncu durumunuzu (player state) yerel makineden ayırmanız ve bir bulut arka ucuna (cloud backend) taşımanız gerekir.

Bunu kendiniz oluşturmak, yük dengeleyiciler (load balancers), veritabanı parçalama (database sharding) ve SSL sertifika yönetimi kurmayı gerektirir — sadece güvenli bir oyuncu girişi ve envanter sistemini çalıştırmak bile kolayca 4-6 haftalık bir iştir. horizOn ile bu Backend-as-a-Service hizmetleri önceden yapılandırılmış olarak gelir ve altyapınız yerine oyununuzu piyasaya sürmenize olanak tanır.

Oyuncu profillerinizi, ekipmanlarınızı ve oturum verilerinizi geliştirmenin erken aşamalarında bir backend API üzerinden yönlendirerek, "Player 2"nin sadece geçici bir yerel misafir değil, kalıcı verilere sahip kimliği doğrulanmış bir kullanıcı olmasını sağlarsınız. Çevrimiçi eşleştirmeyi (online matchmaking) uygulamaya hazır olduğunuzda, horizOn yerel co-op oyuncularınızı sorunsuz bir şekilde daha geniş çevrimiçi oturumlara geçiren kullanıma hazır lobi sistemleri sunar.

Co-Op Prototipleme İçin En İyi Uygulamalar

Prototipinizin ölçeklenebilir ve performanslı kalmasını sağlamak için ilk günden itibaren şu mimari kurallara uyun:

  1. Çevrimiçiymiş Gibi Davranın: Sadece yerel bir prototip oluşturuyor olsanız bile her zaman Unreal Engine'in replikasyon çerçevesini (HasAuthority(), Server_ RPC'leri ve UPROPERTY(Replicated)) kullanın. Yerel makineye ilk günden itibaren bir Listen Server gibi davranmak, daha sonraki çok oyunculu refactoring süresini %80'e kadar azaltır.
  2. Input Actions'ı İzole Edin: Enhanced Input System'i kullanarak, UInputAction varlıklarınızı donanım düğmelerine değil, mantıksal oynanış niyetlerine (örn. "FireWeapon") eşleyin. Bu, indeksleri sabit kodlamadan Klavye/Fareyi dinamik olarak Player 1'e ve Gamepad'i Player 2'ye yeniden eşlemenize olanak tanır.
  3. Kontrolcü Bağlantı Kopmalarını Zarifçe Yönetin: Her zaman FCoreDelegates::OnControllerConnectionChangee bağlanın. Player 2'nin kontrolcüsü kapanırsa, oyununuz karakterlerini bir çatışmanın ortasında boşta bırakmak yerine otomatik olarak duraklamalı ve yeniden bağlantı istemelidir.
  4. Mermiler İçin Instanced Static Meshes Kullanın: Bir co-op nişancı oyununda, yüksek atış hızlı silahlar ateşleyen iki oyuncu saniyede yüzlerce mermi oluşturabilir. Ağır çatışma sahnelerinde çizim çağrılarını (draw calls) ~2000'den ~400'e düşürmek için standart Actor tabanlı mermileri UInstancedStaticMeshComponent veya Niagara parçacık sistemleriyle değiştirin.

Yerel bir co-op nişancı oyunu geliştirmek inanılmaz derecede tatmin edici bir teknik zorluktur. Oyuncu oluşturmayı, kamera matematiğini ve veri kalıcılığını en başından doğru bir şekilde yapılandırarak, prototipinizin tam teşekküllü bir sürüme ölçeklenmeye hazır olmasını sağlarsınız.


Source: Community Tutorial: Coop Local Multiplayer Shooter Prototype