Zurück zum Blog

Warum das Ändern von Blueprint-Defaults zur Laufzeit fehlschlägt: Die Architektur eines sicheren Unreal Engine Upgrade-Shops

Veröffentlicht am 13. Juli 2026
Warum das Ändern von Blueprint-Defaults zur Laufzeit fehlschlägt: Die Architektur eines sicheren Unreal Engine Upgrade-Shops

Kurz und knapp

Dieses Tutorial erklärt, warum direkte Änderungen an Blueprint Class Defaults (CDOs) zur Laufzeit in Unreal Engine fehlschlagen und zeigt, wie sich dieses Problem durch Entkopplung von Daten (Waffen-Stats) und Präsentation (Waffen-Actor) lösen lässt. Durch das Speichern der Statistiken in persistenten Datenstrukturen wie dem `PlayerState` und die serverseitige Validierung von Upgrades werden clientseitige Exploits und Multiplayer-Desynchronisationen effektiv verhindert. Zudem wird erläutert, wie Entwickler diese persistenten Daten über ein Backend-SDK wie horizOn in einer Cloud-Datenbank speichern können. Das Ergebnis ist eine performante und sichere Architektur für Progressionssysteme in Multiplayer-Spielen.

Man verbringt Wochen damit, ein modulares Waffensystem in Unreal Engine aufzubauen, instanziiert ein Child-Blueprint für das Gewehr, richtet ein elegantes UMG-Shop-Menü ein, um dessen Nachladegeschwindigkeit zu verbessern, nur um dann festzustellen, dass die Upgrades in dem Moment verschwinden, in dem der Spieler die Waffe wechselt oder das Level neu startet. Noch schlimmer: Wenn man versucht, im Widget-Menü auf die Blueprint-Klasse zu casten und die Variablen zu ändern, passiert überhaupt nichts. Dies ist ein berüchtigter Bottleneck für Entwickler, die Progression-Systeme implementieren.

In diesem Tutorial erklären wir, warum Blueprint-Modifikationen zur Laufzeit fehlschlagen, wie man ein persistentes Waffensystem konzipiert und wie man diese Upgrades sicher in einer Datenbank speichert.

Das Kernproblem: Warum das Ändern von Blueprint Class Defaults zur Laufzeit fehlschlägt

Wenn man Variablen in einem Blueprint im Unreal Editor bearbeitet, modifiziert man das Class Default Object (CDO). Das CDO fungiert als Master-Template für jede Instanz dieser Klasse, die in der Spielwelt gespawnt wird. Zur Laufzeit ist das direkte Ändern des CDO jedoch stark eingeschränkt – und das aus gutem Grund. Wenn man eine CDO-Variable ändert, läuft man Gefahr, den Standardwert für alle zukünftigen Spawns zu manipulieren, was zu Serialisierungsproblemen führt und die Replication zunichtemacht.

Der häufigste Fehler ist der Versuch, eine Klassenreferenz (TSubclassOf<AActor>) auf eine Instanz der Klasse zu casten. Wenn das Widget-Menü eine Variable vom Typ „Weapon Class“ (z. B. BP_Rifle_Child) enthält und man versucht, deren Variablen direkt zu setzen, zielt man auf das Klassentemplate ab und nicht auf den aktiven Actor in den Händen des Spielers. Selbst wenn der Cast auf die aktive Actor-Instanz (z. B. das aktuell ausgerüstete Gewehr) gelingt und man dessen BaseDamage von 25 auf 50 ändert, ist diese Änderung flüchtig.

In dem Moment, in dem der Spieler das Gewehr weggesteckt, zu einer Pistole wechselt und das Gewehr erneut ausrüstet, zerstört das Spiel den alten Gewehr-Actor und spawnt einen neuen. Der neue Actor wird frisch aus dem CDO-Template gespawnt, wodurch das verbesserte BaseDamage wieder auf den Standardwert 25 zurückgesetzt wird. Damit Upgrades persistent bleiben, muss man die Statistiken der Waffe vom visuellen Actor, der in der Welt gespawnt wird, entkoppeln.

Die Architektur eines persistenten Waffensystems

Um dies zu lösen, müssen wir den State (die Statistiken der Waffe) von der Presentation (dem Actor, der die Waffe rendert und das Spawnen von Projektilen steuert) trennen. Anstatt den autoritativen Schaden, die Nachladegeschwindigkeit und die Munitionskapazität im Waffenelement selbst zu speichern, legen wir sie in einer persistenten Datenstruktur ab. Diese Struktur sollte in einer Klasse liegen, die die Zerstörung des Actors überdauert, wie z. B. im APlayerState, AGameState oder einer benutzerdefinierten Inventarkomponente.

Bei Multiplayer-Spielen erfordert das Speichern dieser Variablen in einer Custom-Komponente ein sorgfältiges Ownership-Management. Wenn man den Waffen-State in einem benutzerdefinierten ActorComponent speichert, muss man darauf achten, dass man keine Multiplayer-Inventar-Alpträume durch fehlerhaftes ActorComponent-Ownership bei der Replication riskiert. Indem man die autoritativen Daten im APlayerState unterbringt, stellt man sicher, dass die Daten erhalten bleiben, selbst wenn der Spieler stirbt, das Level wechselt oder seine Waffen tauscht.

Wenn der Spieler den Upgrade-Shop öffnet, interagiert das UI-Widget direkt mit dem Data-State des Spielers. Der Kauf eines Upgrades modifiziert das persistente Struct, nicht den Waffen-Actor. Rüstet der Spieler eine Waffe aus, spawnt die Character-Klasse den Waffen-Actor und initialisiert ihn sofort mit dem Daten-Struct aus dem State des Spielers. Dies garantiert, dass jede neu gespawnte Waffe die korrekten, verbesserten Werte erbt.

Schritt-für-Schritt-Implementierung: Entkopplung von Daten- und Actor-Logik

Schreiben wir eine saubere C++-Implementierung dieser entkoppelten Architektur. Wir definieren ein FWeaponStats-Struct, das die upgradebaren Werte enthält, sowie eine Basis-Waffenklasse, die dynamisch konfiguriert werden kann.

1. Definition des Weapon Stats Structs

Zuerst definieren wir unsere Datenstruktur. Dieses Struct ist Blueprint-accessible, sodass Ihre UI-Widgets und die für Designer gedachten Child-Blueprints Statistiken problemlos lesen und schreiben können.

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

Durch das Kapseln der Waffen-Stats in einem einzigen FWeaponStats-Struct lässt es sich kinderleicht serialisieren, replizieren und weitergeben. Anstatt fünf separate, replizierte Float-Variablen zu verwalten, replizieren wir ein einziges Struct, was den Replication-Overhead reduziert.

2. Erstellung der Basis-Waffenklasse

Als Nächstes erstellen wir die Basis-Waffenklasse AWeaponBase. Diese Klasse repräsentiert den physischen Actor in der Welt und enthält eine Funktion zur Initialisierung mit neuen Werten.

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

In der Initialisierungsfunktion weisen wir der replizierten WeaponStats-Variable die übergebenen Stats zu. In der Praxis würde man hier auch visuelle oder funktionale Anpassungen triggern – wie das Anpassen der Größe eines Magazin-Meshs, das Ändern von Feuerraten-Timern oder das Modifizieren von Partikelsystem-Skalierungen basierend auf den neuen Werten. Anstatt bei jeder Stat-Änderung ein 20 MB großes Blueprint-Asset neu zu laden, spart das Übergeben eines 48 Byte großen C++-Structs massiven Speicher-Overhead.

3. Integration des Upgrade-Shop-Widgets in den PlayerState

Um Upgrades autoritativ zu verwalten, sollte das UI-Shop-Menü mit dem PlayerState interagieren, anstatt zu versuchen, direkt auf flüchtige Actor-Instanzen zu casten. Entwerfen wir die Klasse AShooterPlayerState, um die Spielerstatistiken zu verwalten und die Upgrades zu validieren.

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

Die Klasse AShooterPlayerState verwaltet die Währung des Spielers und sein Inventar an Waffenstatistiken. Durch die Verwendung von RPCs (Server_UpgradeWeapon) and Replication bleibt der Server die Single Source of Truth für die Stats des Spielers.

Beachten Sie die Funktion Server_UpgradeWeapon_Validate. Das RPC-Validierungssystem von Unreal kickt automatisch Clients, die ungültige Anfragen senden – wie zum Beispiel den Versuch, ein Waffen-Upgrade ohne ausreichend Gold durchzuführen.

Sobald der Server das Upgrade verarbeitet hat, zieht er das Gold ab, aktualisiert die Statistiken und repliziert die Änderungen zurück an den Client. Diese Replication triggert über Replication-Callbacks automatisch die clientseitigen UI-Updates.

Clientseitige Exploits im Upgrade-Shop verhindern

Wenn es sich um ein Multiplayer-Spiel handelt oder Sie Ihre Singleplayer-Ökonomie vor Memory-Editoren schützen möchten, dürfen Sie dem Client bei der Verwaltung seiner eigenen Upgrades nicht vertrauen. Wenn das clientseitige UI-Widget direkt Server_UpgradeWeapon(int32 NewDamage) aufrufen darf, kann ein Hacker leicht die Netzwerkpakete abfangen oder Tools wie Cheat Engine verwenden, um ein Paket zu senden, das den Waffenschaden als 999.999 deklariert.

Ohne eine strikte, server-autoritative Validierung leidet Ihr Spiel unter schwerwiegenden Multiplayer-State-Desyncs, bei denen die lokale UI des Clients zwar ein verbessertes Gewehr anzeigt, der Server den Schaden aber basierend auf den ursprünglichen Level-1-Stats berechnet. Um dies zu verhindern, darf der Client lediglich die Absicht für ein Upgrade senden, z. B. Server_RequestUpgrade(FName WeaponID). Der Server führt die Transaktion dann autoritativ aus.

Der Server-Validierungsablauf muss folgenden Schritten folgen:

  1. Ressourcen überprüfen: Prüfen, ob der Spieler tatsächlich genug Gold oder Schrott besitzt, um das Upgrade zu kaufen.
  2. Upgrade-Pfad validieren: Bestätigen, dass das angeforderte Upgrade der nächste logische Schritt in der Sequenz ist (z. B. von Level 2 auf Level 3, nicht direkt auf Level 10).
  3. Kosten abziehen und anwenden: Die Währung auf dem Server abziehen und das persistente Stats-Struct des Spielers aktualisieren.
  4. Replizieren und synchronisieren: Das aktualisierte Struct an den Client replizieren, was die ausgerüstete Waffe automatisch aktualisiert.

Upgrades in der Backend-Datenbank persistieren

Obwohl das Halten der Stats im PlayerState während eines einzelnen Matches funktioniert, werden diese Werte zurückgesetzt, sobald der Spieler das Spiel schließt oder der Server neu startet. Um einen echten Progression-Loop zu schaffen, müssen Sie diese dynamischen Variablenänderungen in einer persistenten Backend-Datenbank speichern.

Dies manuell selbst aufzubauen, ist ein riesiges Unterfangen. Sie müssten eine SQL- oder NoSQL-Datenbank bereitstellen, ein API-Gateway mit OAuth2-Authentifizierung einrichten, eigene Server-Logik zum Parsen von JSON-Payloads implementieren sowie Edge-Cases wie Verbindungs-Timeouts und Datenbank-Sharding handhaben. Dieses Infrastruktur-Setup kann leicht 4 bis 6 Wochen reine Entwicklungszeit in Anspruch nehmen – Zeit, die Ihnen beim Polishing Ihres Core-Gameplay-Loops fehlt.

Hier kommt horizOn ins Spiel: Es ermöglicht Ihnen, die Progressionsdaten der Spieler sicher in der Cloud zu speichern, ohne eigenen Code für eine Backend-Datenbank schreiben zu müssen. Sie können das Backend-SDK nutzen, um Ihr FWeaponStats-Struct in JSON zu serialisieren und direkt im Cloud-Profil des Spielers unter Einhaltung server-autoritativer Sicherheitsregeln zu speichern. Eine typische JSON-Payload für Waffenstatistiken ist kleiner als 500 Byte (etwa 320 Byte für ein Standard-Loadout), was bedeutet, dass Datenbankoperationen in unter 15 ms ausgeführt werden. Diese Geschwindigkeit sorgt für flüssige UI-Übergänge und stellt sicher, dass Spieler beim Öffnen von Shop-Menüs oder beim Abschließen von Transaktionen keine Stotterer erleben.

Sie können beispielsweise eine sichere Cloud-Code-Funktion im Backend schreiben, die den Upgrade-Kauf validiert. Wenn der Spieler in Ihrem Widget-Menü auf „Upgrade kaufen“ klickt, sendet das Spiel einen sicheren API-Request. Die Datenbank überprüft das Inventar des Spielers, zieht die Währung ab, schreibt das neue Waffen-Level und gibt die aktualisierte Stat-Payload zurück. Dies stellt sicher, dass die autoritative Cloud-Datenbank selbst dann geschützt bleibt, wenn ein Spieler seinen lokalen Speicher manipuliert.

Best Practices für Unreal Engine Upgrade-Systeme

Um sicherzustellen, dass Ihr Upgrade-Shop sowohl stabil als auch performant ist, sollten Sie diese Kernprinzipien befolgen:

  1. Class Default Objects (CDOs) niemals zur Laufzeit bearbeiten: Behandeln Sie CDOs als Read-only-Blueprints. Nutzen Sie sie ausschließlich für das Spawnen der visuellen Standard-Meshes und initialen Templates.
  2. Stats von Actors entkoppeln: Speichern Sie autoritative Statistiken in einer persistenten Klasse wie APlayerState oder GameInstance und übergeben Sie diese beim Spawnen an den Actor.
  3. Server-Authority erzwingen: Lassen Sie Clients niemals Stat-Änderungen direkt diktieren. Clients fordern Upgrades an; der Server validiert die Ressourcen und wendet die Änderung an.
  4. Structs für die Serialisierung nutzen: Gruppieren Sie upgradebare Statistiken in USTRUCTs. Dies macht die Serialisierung für lokale Spielstände oder Backend-Datenbank-Aufrufe nahtlos.
  5. Datenbank-Payloads optimieren: Halten Sie Spielerprofile schlank. Speichern Sie nur die Upgrade-Level (z. B. WeaponLevel: 3) in der Datenbank und rekonstruieren Sie die tatsächlichen Floats (z. B. Damage: 45.0f) auf dem Game-Server.

Zusammenfassung und nächste Schritte

Das Lösen dynamischer Variablen-Updates bei Child-Blueprints erfordert den Wechsel von einem Actor-zentrierten zu einem daten-zentrierten Design. Indem man Statistiken in persistenten Structs im PlayerState speichert und Waffen dynamisch initialisiert, verhindert man, dass Upgrades beim Waffenwechsel verschwinden. Wenn Sie bereit sind, dieses Progression-System in den Multiplayer zu überführen und in der Cloud abzusichern, ist die Anbindung an eine Backend-Datenbank unerlässlich.

Bereit, Ihre Progression-Systeme zu skalieren und die Inventare der Spieler abzusichern? Testen Sie horizOn kostenlos oder werfen Sie einen Blick in die API-Dokumentation, um noch heute mit dem Aufbau Ihres Backends zu beginnen.


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