Back to Blog

Why Modifying Blueprint Defaults at Runtime Fails: Architecting a Secure Unreal Engine Upgrade Shop

Published on July 13, 2026
Why Modifying Blueprint Defaults at Runtime Fails: Architecting a Secure Unreal Engine Upgrade Shop

In a nutshell

Build a secure unreal engine blueprint upgrade shop database integration. Prevent client-side cheating and persist dynamic weapon stats at runtime.

You spend weeks building a modular weapon system in Unreal Engine, instantiate a child blueprint for your rifle, set up a sleek UMG shop menu to upgrade its reload speed, only to find the upgrades vanish the instant the player swaps weapons or restarts the level. Even worse, when you try to cast to the Blueprint class in your widget menu and modify the variables, nothing happens. This is a notorious bottleneck for developers building progression systems.

In this tutorial, we will unpack why runtime Blueprint modifications fail, how to architect a persistent weapon system, and how to store those upgrades securely in a database.

The Core Problem: Why Modifying Blueprint Class Defaults at Runtime Fails

When you edit variables in a Blueprint inside the Unreal Editor, you are modifying the Class Default Object (CDO). The CDO acts as the master template for every instance of that class spawned into the game world. At runtime, however, modifying the CDO directly is heavily restricted, and for good reason. If you change a CDO variable, you run the risk of modifying the default value for all future spawns, causing serialization issues, and breaking replication.

The most common mistake is attempting to cast a class reference (TSubclassOf<AActor>) to an instance of the class. If your widget menu contains a variable of type "Weapon Class" (e.g., BP_Rifle_Child) and you try to set its variables directly, you are targeting the class template, not the active actor in the player's hands. Even if you successfully cast to the active actor instance (e.g., the rifle currently equipped) and change its BaseDamage from 25 to 50, that change is transient.

The moment the player holsters the rifle, switches to a pistol, and equips the rifle again, the game destroys the old rifle actor and spawns a new one. The new actor is spawned fresh from the CDO template, resetting your upgraded BaseDamage back to the default 25. To make upgrades persist, you must decouple the weapon's statistics from the visual actor spawned in the world.

The Architecture of a Persistent Weapon System

To resolve this, we must separate State (the weapon's statistics) from Presentation (the actor rendering the weapon and handling projectile spawns). Instead of storing the authoritative damage, reload speed, and ammo capacity inside the weapon actor itself, we store them in a persistent data structure. This structure should live in a class that persists across actor destruction, such as the APlayerState, AGameState, or a custom inventory component.

For multiplayer games, storing these variables in a custom component requires careful management of ownership. If you store weapon states in a custom ActorComponent, make sure you don't run into multiplayer inventory nightmares caused by mismatched actor component ownership during replication. By housing the authoritative data in the APlayerState, you ensure the data is preserved even when the player dies, changes levels, or swaps their weapons.

When the player opens the upgrade shop, the UI widget interacts directly with the player's data state. Purchasing an upgrade modifies the persistent struct, not the weapon actor. When the player equips a weapon, the character class spawns the weapon actor and immediately initializes it using the data struct from the player's state. This guarantees that every newly spawned weapon inherits the correct, upgraded stats.

Step-by-Step Implementation: Decoupling Data and Actor Logic

Let's write a clean C++ implementation of this decoupled architecture. We will define a FWeaponStats struct that holds the upgradeable values, and a base weapon class that can be configured dynamically.

1. Defining the Weapon Stats Struct

First, we define our data structure. This structure is Blueprint-accessible, allowing your UI widgets and designer-facing child blueprints to read and write stats easily.

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

By encapsulating weapon stats in a single FWeaponStats struct, we make it trivial to serialize, replicate, and pass around. Instead of managing five separate replicated float variables, we replicate a single struct, reducing replication overhead.

2. Creating the Base Weapon Class

Next, we create the base weapon class AWeaponBase. This class represents the physical actor in the world and contains a function to initialize itself with new stats.

#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 the initialization function, we assign the passed stats to our replicated WeaponStats variable. In a real-world scenario, you would also trigger visual or functional adjustments here—such as resizing a magazine mesh, altering fire rate timers, or modifying particle system scales based on the new stats. Instead of reloading a 20MB blueprint asset every time a stat changes, passing a 48-byte C++ struct saves massive memory overhead.

3. Integrating the Upgrade Shop Widget with Player State

To manage upgrades authoritatively, the UI shop menu should interact with the PlayerState instead of trying to cast directly to transient actor instances. Let's design the AShooterPlayerState class to manage player stats and validate the upgrades.

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

The AShooterPlayerState class manages the player's currency and their inventory of weapon stats. By utilizing RPCs (Server_UpgradeWeapon) and replication, the server remains the single source of truth for the player's stats.

Notice the Server_UpgradeWeapon_Validate function. Unreal's RPC validation system automatically kicks clients who send invalid requests, such as attempting to upgrade a weapon without enough gold.

Once the server processes the upgrade, it deducts the gold, updates the stats, and replicates the changes back to the client. This replication triggers client-side UI updates automatically via replication callbacks.

Preventing Client-Side Exploits in Your Upgrade Shop

If your game is multiplayer, or if you want to protect your single-player economy from memory editors, you cannot trust the client to manage their own upgrades. If the client-side UI widget is allowed to call Server_UpgradeWeapon(int32 NewDamage), a hacker can easily intercept the network packets or use tools like Cheat Engine to send a packet claiming their weapon damage is 999,999.

Without strict server-authoritative validation, your game will suffer from game-breaking multiplayer state desyncs where the client's local UI thinks they have an upgraded weapon, but the server is processing damage based on the original level 1 stats. To prevent this, the client must only send an intent to upgrade, such as Server_RequestUpgrade(FName WeaponID). The server then executes the transaction authoritatively.

The server validation flow must follow these steps:

  1. Verify Resources: Check if the player actually has enough gold or scrap to purchase the upgrade.
  2. Validate Upgrade Path: Confirm the requested upgrade is next in the sequence (e.g., Level 2 to Level 3, not jumping to Level 10).
  3. Deduct Cost and Apply: Deduct the currency on the server and update the player's persistent stats struct.
  4. Replicate and Sync: Replicate the updated struct back to the client, which automatically updates the equipped weapon.

Persisting Upgrades to the Backend Database

While keeping stats in the PlayerState works during a single match, those stats are wiped out the moment the player closes the game or the server restarts. To create a true progression loop, you must save these dynamic variable changes to a persistent backend database.

Building this yourself manually is a massive undertaking. You would need to provision a SQL or NoSQL database, set up an API gateway with OAuth2 authentication, implement custom server logic to parse JSON payloads, and handle edge cases like connection timeouts and database sharding. This infrastructure setup can easily take 4 to 6 weeks of dedicated development time, taking focus away from polishing your core gameplay loop.

This is where horizOn comes in, allowing you to store player progression data securely in the cloud without writing backend database code. You can use the backend SDK to serialize your FWeaponStats struct into JSON and save it directly to the player's cloud profile with server-authoritative security rules. A typical weapon stats JSON payload is less than 500 bytes (around 320 bytes for a standard loadout), meaning database operations execute in under 15ms. This speed keeps UI transitions snappy and ensures players don't experience stuttering when opening shop menus or finalizing transactions.

For example, you can write a secure Cloud Code function in the backend that validates the upgrade purchase. When the player clicks "Buy Upgrade" in your widget menu, the game sends a secure API request. The database verifies the player's inventory, deducts the currency, writes the new weapon level, and returns the updated stat payload. This ensures that even if a player hacks their local memory, the authoritative cloud database remains secure.

Best Practices for Unreal Engine Upgrade Systems

To ensure your upgrade shop is both stable and performant, follow these core principles:

  1. Never Edit Class Default Objects (CDOs) at Runtime: Treat CDOs as read-only blueprints. Use them solely for spawning the default visual meshes and initial templates.
  2. Decouple Stats from Actors: Store authoritative stats in a persistent class like APlayerState or GameInstance, and pass them to the actor upon spawning.
  3. Enforce Server Authority: Never let clients dictate stat changes directly. Clients request upgrades; the server validates resources and applies the change.
  4. Use Structs for Serialization: Group upgradeable stats into USTRUCTs. This makes serialization for local saves or backend database calls seamless.
  5. Optimize Database Payloads: Keep player profiles lean. Store only the upgrade levels (e.g., WeaponLevel: 3) in the database, and reconstruct the actual floats (e.g., Damage: 45.0f) on the game server.

Summary and Next Steps

Resolving dynamic variable updates on child blueprints requires shifting from an actor-centric design to a data-centric design. By storing stats in persistent structs on the PlayerState and initializing your weapons dynamically, you prevent upgrades from disappearing when weapons are swapped. When you are ready to take this progression system multiplayer and secure it in the cloud, backing it with a backend database is essential.

Ready to scale your progression systems and secure player inventories? Try horizOn for free or check out the API documentation to start building your backend today.


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