Bloga Dön

Neden Runtime Sırasında Blueprint Defaults Değiştirmek Başarısız Olur? Güvenli Bir Unreal Engine Upgrade Shop Mimarisi Tasarlamak

Yayınlanma tarihi 13 Temmuz 2026
Neden Runtime Sırasında Blueprint Defaults Değiştirmek Başarısız Olur? Güvenli Bir Unreal Engine Upgrade Shop Mimarisi Tasarlamak

Özet olarak

Unreal Engine projelerinde runtime sırasında Blueprint varsayılanlarını (CDO) değiştirmek, verilerin sıfırlanmasına ve replikasyon sorunlarına yol açar. Bu rehberde, silah istatistiklerini görsel actor'den ayırarak PlayerState gibi kalıcı sınıflarda saklamayı sağlayan veri odaklı (decoupled) bir mimari ele alınmaktadır. Güvenli bir multiplayer yapısı için server-authoritative doğrulamanın önemi açıklanmakta ve verilerin backend veritabanına kalıcı olarak nasıl kaydedileceği gösterilmektedir.

Unreal Engine'de modüler bir silah sistemi oluşturmak için haftalarınızı harcar, tüfeğiniz için bir child blueprint instantiate eder ve reload speed'i upgrade etmek için şık bir UMG shop menüsü kurarsınız; ancak oyuncu silah değiştirdiği veya level'ı yeniden başlattığı anda upgrade'lerin yok olduğunu görürsünüz. Daha da kötüsü, widget menünüzde ilgili Blueprint class'ına cast edip değişkenleri değiştirmeye çalıştığınızda hiçbir şey gerçekleşmez. Bu, progression sistemleri inşa eden geliştiriciler için oldukça meşhur bir bottleneck'tir.

Bu rehberde, runtime sırasında Blueprint modifikasyonlarının neden başarısız olduğunu inceleyecek, kalıcı (persistent) bir silah sisteminin nasıl tasarlanacağını ele alacak ve bu upgrade'lerin bir veritabanında (database) nasıl güvenle saklanacağını adım adım açıklayacağız.

Temel Sorun: Runtime Sırasında Blueprint Class Varsayılanlarını Değiştirmek Neden Başarısız Olur?

Unreal Editor içinde bir Blueprint'teki değişkenleri düzenlediğinizde, Class Default Object (CDO) yapısını değiştirmiş olursunuz. CDO, oyun dünyasında o class'tan spawn edilen her instance için ana şablon (master template) görevi görür. Ancak runtime sırasında CDO'yu doğrudan değiştirmek son derece kısıtlanmıştır ve bunun iyi bir sebebi vardır. Bir CDO değişkenini değiştirirseniz, gelecekte spawn edilecek tüm nesnelerin varsayılan değerlerini değiştirme riskini alır, serialization sorunlarına yol açar ve replication yapısını bozarsınız.

En sık yapılan hata, bir class referansını (TSubclassOf<AActor>) o class'ın bir instance'ına cast etmeye çalışmaktır. Eğer widget menünüz "Weapon Class" (örneğin BP_Rifle_Child) tipinde bir değişken içeriyorsa ve siz bunun değişkenlerini doğrudan set etmeye çalışıyorsanız, oyuncunun elindeki aktif actor'ü değil, class şablonunu hedefliyorsunuz demektir. Aktif actor instance'ına (örneğin kuşanılmış olan tüfeğe) başarıyla cast edip BaseDamage değerini 25'ten 50'ye çıkarsanız bile bu değişiklik geçicidir (transient).

Oyuncu tüfeği kılıfına koyup tabancaya geçtiğinde ve ardından tekrar tüfeği kuşandığında, oyun eski tüfek actor'ünü destroy eder ve yenisini spawn eder. Yeni actor, CDO şablonundan sıfırdan spawn edildiği için upgrade edilmiş BaseDamage değeriniz tekrar varsayılan olan 25'e sıfırlanır. Upgrade'lerin kalıcı (persistent) olmasını sağlamak için silahın istatistiklerini (statistics), dünyada spawn edilen görsel actor'den ayırmanız (decouple) gerekir.

Kalıcı Bir Silah Sisteminin Mimarisi

Bu sorunu çözmek için State (silahın istatistikleri) ile Presentation (silahı render eden ve projectile spawn işlemlerini yöneten actor) yapılarını birbirinden ayırmalıyız. Asıl (authoritative) hasar, reload speed ve mermi kapasitesi değerlerini silah actor'ünün kendisinde saklamak yerine, kalıcı bir veri yapısında (data structure) depolarız. Bu yapı, actor destroy olsa bile yaşamaya devam eden APlayerState, AGameState veya özel bir envanter bileşeni (custom inventory component) gibi bir class içinde bulunmalıdır.

Multiplayer oyunlar için bu değişkenleri özel bir component içinde saklamak, sahiplik (ownership) durumunun dikkatli yönetilmesini gerektirir. Silah durumlarını (weapon states) özel bir ActorComponent içinde saklıyorsanız, replication sırasında hatalı eşleşen actor component ownership durumlarının neden olduğu multiplayer envanter kabusları ile karşılaşmadığınızdan emin olun. Authoritative verileri APlayerState içinde barındırarak, oyuncu öldüğünde, level değiştirdiğinde veya silahlarını değiştirdiğinde bile verilerin korunmasını sağlarsınız.

Oyuncu upgrade shop'u açtığında, UI widget'ı doğrudan oyuncunun veri durumuyla (data state) etkileşime girer. Bir upgrade satın almak, silah actor'ünü değil, kalıcı struct yapısını değiştirir. Oyuncu bir silah kuşandığında, character class silah actor'ünü spawn eder ve bunu oyuncunun state'indeki veri struct'ı ile hemen initialize eder. Bu sayede yeni spawn edilen her silahın doğru ve upgrade edilmiş istatistikleri miras alması garanti edilir.

Adım Adım Entegrasyon: Veri ve Actor Mantığını Ayırmak (Decoupling)

Şimdi bu decoupled mimarinin temiz bir C++ uygulamasını yazalım. Yükseltilebilir değerleri tutan bir FWeaponStats struct'ı ve dinamik olarak konfigüre edilebilen bir temel silah class'ı tanımlayacağız.

1. Silah İstatistikleri Struct Yapısını Tanımlamak

İlk olarak veri yapımızı tanımlıyoruz. Bu struct, Blueprint tarafından erişilebilir (Blueprint-accessible) olmalıdır; böylece UI widget'larınız ve tasarımcılara yönelik child blueprint'leriniz istatistikleri kolayca okuyup yazabilir.

#pragma once

#include "CoreMinimal.h"
#include "WeaponStats.generated.h"

USTRUCT(BlueprintType)
struct FWeaponStats
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float BaseDamage = 25.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    float ReloadSpeedModifier = 1.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    int32 MaxAmmo = 30;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
    int32 CurrentUpgradeLevel = 0;
};

Silah istatistiklerini tek bir FWeaponStats struct'ı içinde kapsülleyerek (encapsulating), serialize etmeyi, replicate etmeyi ve veri aktarımını son derece basit hale getiriyoruz. Beş farklı replike edilmiş float değişkenini yönetmek yerine tek bir struct replike ederek replication yükünü (replication overhead) azaltıyoruz.

2. Temel Silah Class'ını Oluşturmak

Sırada temel silah class'ı olan AWeaponBase var. Bu class, dünyadaki fiziksel actor'ü temsil eder ve kendisini yeni istatistiklerle initialize edecek bir fonksiyon içerir.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "WeaponStats.h"
#include "WeaponBase.generated.h"

UCLASS()
class SHOOTER_API AWeaponBase : public AActor
{
    GENERATED_BODY()
    
public:    
    AWeaponBase();

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon")
    USkeletalMeshComponent* WeaponMesh;

    UPROPERTY(Replicated, BlueprintReadOnly, Category = "Weapon")
    FWeaponStats WeaponStats;

    UFUNCTION(BlueprintCallable, Category = "Weapon")
    void InitializeWeapon(const FWeaponStats& NewStats);

    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
#include "WeaponBase.h"
#include "Net/UnrealNetwork.h"

AWeaponBase::AWeaponBase()
{
    PrimaryActorTick.bCanEverTick = false;
    bReplicates = true;

    WeaponMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WeaponMesh"));
    RootComponent = WeaponMesh;
}

void AWeaponBase::InitializeWeapon(const FWeaponStats& NewStats)
{
    WeaponStats = NewStats;
    // Apply changes dynamically to the active actor
    // e.g., Adjust weapon mesh scale, update firing rate variables, or UI indicators
}

void AWeaponBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(AWeaponBase, WeaponStats);
}

Initialization fonksiyonunda, gönderilen istatistikleri replike edilen WeaponStats değişkenimize atıyoruz. Gerçek bir senaryoda, burada görsel veya işlevsel düzenlemeleri de tetiklersiniz; örneğin şarjör mesh boyutunu değiştirmek, atış hızı (fire rate) zamanlayıcılarını güncellemek veya yeni istatistiklere göre particle sistem ölçeklerini değiştirmek gibi. İstatistik her değiştiğinde 20 MB'lık bir blueprint asset'ini yeniden yüklemek yerine, 48 byte'lık bir C++ struct'ı geçirmek devasa bir bellek yükünden (memory overhead) tasarruf sağlar.

3. Upgrade Shop Widget'ını Player State ile Entegre Etmek

Upgrade'leri yetkili (authoritative) bir şekilde yönetmek için, UI shop menüsü geçici (transient) actor instance'larına doğrudan cast etmeye çalışmak yerine PlayerState ile etkileşime girmelidir. Oyuncu istatistiklerini yönetmek ve upgrade'leri doğrulamak için AShooterPlayerState class'ını tasarlayalım.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "WeaponStats.h"
#include "ShooterPlayerState.generated.h"

UCLASS()
class SHOOTER_API AShooterPlayerState : public APlayerState
{
    GENERATED_BODY()

public:
    AShooterPlayerState();

    UPROPERTY(ReplicatedUsing = OnRep_WeaponInventory, BlueprintReadOnly, Category = "Inventory")
    TArray<FWeaponStats> WeaponInventory;

    UFUNCTION(BlueprintCallable, Category = "Inventory")
    FWeaponStats GetWeaponStats(int32 WeaponIndex) const;

    UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = "Inventory")
    void Server_UpgradeWeapon(int32 WeaponIndex);

    UPROPERTY(Replicated, BlueprintReadOnly, Category = "Economy")
    int32 PlayerGold = 500;

    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

protected:
    UFUNCTION()
    void OnRep_WeaponInventory();
};
#include "ShooterPlayerState.h"
#include "Net/UnrealNetwork.h"

AShooterPlayerState::AShooterPlayerState()
{
    bReplicates = true;
}

FWeaponStats AShooterPlayerState::GetWeaponStats(int32 WeaponIndex) const
{
    if (WeaponInventory.IsValidIndex(WeaponIndex))
    {
        return WeaponInventory[WeaponIndex];
    }
    return FWeaponStats();
}

bool AShooterPlayerState::Server_UpgradeWeapon_Validate(int32 WeaponIndex)
{
    if (!WeaponInventory.IsValidIndex(WeaponIndex)) return false;

    int32 UpgradeCost = (WeaponInventory[WeaponIndex].CurrentUpgradeLevel + 1) * 100;
    return PlayerGold >= UpgradeCost;
}

void AShooterPlayerState::Server_UpgradeWeapon_Implementation(int32 WeaponIndex)
{
    int32 UpgradeCost = (WeaponInventory[WeaponIndex].CurrentUpgradeLevel + 1) * 100;
    PlayerGold -= UpgradeCost;

    FWeaponStats& Stats = WeaponInventory[WeaponIndex];
    Stats.CurrentUpgradeLevel++;
    Stats.BaseDamage += 10.0f;
    Stats.ReloadSpeedModifier *= 0.9f;
}

void AShooterPlayerState::OnRep_WeaponInventory()
{
    // Update local UI representation or bind to UI delegates
}

void AShooterPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(AShooterPlayerState, WeaponInventory);
    DOREPLIFETIME(AShooterPlayerState, PlayerGold);
}

AShooterPlayerState class'ı, oyuncunun parasını ve silah istatistikleri envanterini yönetir. RPC'ler (Server_UpgradeWeapon) ve replication kullanarak, sunucu (server) oyuncunun istatistikleri için tek gerçeklik kaynağı (single source of truth) olarak kalır.

Server_UpgradeWeapon_Validate fonksiyonuna dikkat edin. Unreal'ın RPC validation sistemi, yeterli altını olmadan bir silahı upgrade etmeye çalışmak gibi geçersiz istekler gönderen istemcileri (client'ları) otomatik olarak oyundan atar (kick).

Server upgrade işlemini onaylayıp işlediğinde, altını düşer, istatistikleri günceller ve değişiklikleri client tarafına replike eder. Bu replication, replication callback'leri aracılığıyla client tarafındaki UI güncellemelerini otomatik olarak tetikler.

Upgrade Shop Menünüzde Client Tarafındaki Açıkları (Exploits) Önlemek

Eğer oyununuz multiplayer ise veya single-player ekonominizi bellek düzenleyicilerden (memory editors) korumak istiyorsanız, upgrade'leri yönetme konusunda client'a güvenemezsiniz. Client tarafındaki UI widget'ının doğrudan Server_UpgradeWeapon(int32 NewDamage) çağırmasına izin verilirse, bir hacker ağ paketlerini (network packets) kolayca manipüle edebilir veya Cheat Engine gibi araçlar kullanarak silah hasarını 999.999 olarak bildiren bir paket gönderebilir.

Sıkı bir server-authoritative doğrulama olmadan oyununuz, client'ın yerel UI'ının upgrade edilmiş bir silaha sahip olduğunu düşündüğü, ancak server'ın hasarı orijinal seviye 1 istatistiklerine göre işlediği, oyunu bozan multiplayer state desyncs (durum senkronizasyon bozuklukları) sorunlarıyla karşılaşacaktır. Bunu önlemek için client sadece bir upgrade isteği göndermelidir (örneğin Server_RequestUpgrade(FName WeaponID)). Ardından server bu işlemi yetkili (authoritative) olarak gerçekleştirir.

Server doğrulama akışı şu adımları izlemelidir:

  1. Kaynakları Doğrula (Verify Resources): Oyuncunun upgrade'i satın almak için gerçekten yeterli altına veya hurdaya (scrap) sahip olup olmadığını kontrol edin.
  2. Upgrade Yolunu Doğrula (Validate Upgrade Path): İstenen upgrade'in sıralamada bir sonraki adım olduğunu onaylayın (örneğin, Seviye 10'a atlamak yerine Seviye 2'den Seviye 3'e geçmek).
  3. Maliyeti Düş ve Uygula (Deduct Cost and Apply): Server tarafında oyun içi para birimini düşüp oyuncunun kalıcı stats struct'ını güncelleyin.
  4. Replike Et ve Eşitle (Replicate and Sync): Güncellenmiş struct'ı client tarafına replike edin; bu işlem kuşanılmış silahı otomatik olarak günceller.

Upgrade'leri Backend Veritabanına (Database) Kaydetmek

İstatistikleri PlayerState içinde tutmak tek bir maç sırasında işe yarasa da, oyuncu oyunu kapattığı veya server yeniden başladığı anda bu istatistikler silinir. Gerçek bir progression döngüsü oluşturmak için, bu dinamik değişken değişikliklerini kalıcı bir backend veritabanına kaydetmelisiniz.

Bunu kendi başınıza manuel olarak inşa etmek devasa bir iştir. Bir SQL veya NoSQL veritabanı hazırlamanız, OAuth2 kimlik doğrulamasına sahip bir API gateway kurmanız, JSON yüklerini işlemek için özel sunucu mantığı uygulamanız ve bağlantı zaman aşımı ile database sharding gibi uç durumları (edge cases) yönetmeniz gerekir. Bu altyapı kurulumu, odak noktanızı temel oynanış döngünüzü (gameplay loop) geliştirmekten uzaklaştırarak 4 ila 6 haftalık yoğun bir geliştirme süresi alabilir.

İşte tam bu noktada, backend veritabanı kodu yazmakla uğraşmadan oyuncu progression verilerini bulutta güvenle saklamanızı sağlayan horizOn devreye giriyor. Backend SDK'sını kullanarak FWeaponStats struct'ınızı JSON formatına serialize edebilir ve server-authoritative güvenlik kuralları eşliğinde doğrudan oyuncunun bulut profiline kaydedebilirsiniz. Standart bir silah istatistiği JSON yükü 500 bayttan azdır (standart bir loadout için yaklaşık 320 bayt), bu da veritabanı işlemlerinin 15 ms'nin altında tamamlandığı anlamına gelir. Bu hız, UI geçişlerini akıcı tutar ve oyuncuların shop menülerini açarken veya işlemleri tamamlarken donma (stuttering) yaşamamasını sağlar.

Örneğin, backend tarafında upgrade satın alımını doğrulayan güvenli bir Cloud Code fonksiyonu yazabilirsiniz. Oyuncu widget menünüzde "Buy Upgrade" butonuna tıkladığında, oyun güvenli bir API isteği gönderir. Veritabanı oyuncunun envanterini doğrular, parayı düşer, yeni silah seviyesini yazar ve güncellenmiş stat verisini döndürür. Bu, bir oyuncu yerel belleğini (local memory) hacklese bile, yetkili (authoritative) bulut veritabanının güvende kalmasını sağlar.

Unreal Engine Upgrade Sistemleri İçin En İyi Pratikler (Best Practices)

Upgrade shop menünüzün hem kararlı hem de yüksek performanslı olmasını sağlamak için şu temel ilkelere uyun:

  1. Runtime Sırasında Asla Class Default Objects (CDO) Düzenlemeyin: CDO'lara salt okunur (read-only) blueprint'ler olarak yaklaşın. Bunları yalnızca varsayılan görsel mesh'leri ve ilk şablonları spawn etmek için kullanın.
  2. İstatistikleri Actor'lerden Ayırın (Decouple): Yetkili (authoritative) istatistikleri APlayerState veya GameInstance gibi kalıcı bir class içinde saklayın ve spawn edildiğinde bunları actor'e aktarın.
  3. Server Authority Uygulayın: İstemcilerin (client'ların) stat değişikliklerini doğrudan belirlemesine asla izin vermeyin. Client'lar upgrade isteği gönderir; server kaynakları doğrular ve değişikliği uygular.
  4. Serialization İçin Struct'ları Kullanın: Yükseltilebilir istatistikleri USTRUCT'lar içinde gruplayın. Bu, yerel kayıtlar veya backend veritabanı çağrıları için serialization işlemlerini sorunsuz hale getirir.
  5. Database Yüklerini Optimize Edin: Oyuncu profillerini hafif tutun. Veritabanında yalnızca upgrade seviyelerini saklayın (örneğin WeaponLevel: 3) ve gerçek float değerlerini (örneğin Damage: 45.0f) oyun sunucusunda (game server) yeniden oluşturun.

Özet ve Sonraki Adımlar

Child blueprint'lerdeki dinamik değişken güncellemelerini çözmek, actor odaklı bir tasarımdan veri odaklı bir tasarıma geçiş yapmayı gerektirir. İstatistikleri PlayerState üzerindeki kalıcı struct'larda saklayarak ve silahlarınızı dinamik olarak initialize ederek, silahlar değiştirildiğinde upgrade'lerin kaybolmasını önlersiniz. Bu progression sistemini multiplayer yapıya taşımaya ve bulutta güvence altına almaya hazır olduğunuzda, bunu bir backend veritabanı ile desteklemek büyük önem taşır.

Progression sistemlerinizi ölçeklendirmeye ve oyuncu envanterlerini güvence altına almaya hazır mısınız? horizOn'u ücretsiz deneyin veya bugün kendi backend'inizi oluşturmaya başlamak için API dokümantasyonuna göz atın.


Kaynak: How to edit the values of a blueprint class that is a child and that is already in use by the player?