Torna al Blog

Perché modificare i default dei Blueprint a runtime fallisce: progettare un Upgrade Shop sicuro in Unreal Engine

Pubblicato il 13 luglio 2026
Perché modificare i default dei Blueprint a runtime fallisce: progettare un Upgrade Shop sicuro in Unreal Engine

In breve

Questa guida spiega perché la modifica a runtime del Class Default Object (CDO) nei Blueprint di Unreal Engine causa la perdita dei dati al momento del cambio di arma o del riavvio del livello. Viene illustrata un'architettura che disaccoppia lo stato dei dati (statistiche dell'arma) dalla presentazione visiva (l'actor dell'arma) sfruttando struct persistenti memorizzate nel `PlayerState`. Attraverso un'implementazione pratica in C++, l'articolo mostra come proteggere l'economia di gioco con la convalida server-authoritative. Infine, viene descritto come salvare in modo persistente i progressi dei giocatori nel cloud tramite l'SDK di [horizOn](https://horizon.pm).

Passi settimane a costruire un sistema di armi modulare in Unreal Engine, istanzi un child Blueprint per il tuo fucile, configuri un elegante menu shop in UMG per potenziarne la velocità di ricarica, solo per scoprire che gli upgrade svaniscono nel momento stesso in cui il giocatore cambia arma o riavvia il livello. Ancora peggio, quando provi a fare il cast alla classe Blueprint nel menu del tuo widget e a modificarne le variabili, non succede nulla. Questo è un noto collo di bottiglia per gli sviluppatori che creano sistemi di progressione.

In questo tutorial analizzeremo perché le modifiche ai Blueprint a runtime falliscono, come progettare un sistema di armi persistente e come memorizzare questi upgrade in modo sicuro in un database.

Il problema principale: perché la modifica dei Class Defaults dei Blueprint a runtime fallisce

Quando modifichi le variabili in un Blueprint all'interno dell'Unreal Editor, stai modificando il Class Default Object (CDO). Il CDO funge da template principale per ogni istanza di quella classe spawnata nel mondo di gioco. A runtime, tuttavia, la modifica diretta del CDO è fortemente limitata, e per ottime ragioni. Se modifichi una variabile del CDO, corri il rischio di alterare il valore predefinito per tutti gli spawn futuri, causando problemi di serializzazione e interrompendo la replication.

L'errore più comune è tentare di fare il cast di un riferimento di classe (TSubclassOf<AActor>) a un'istanza della classe stessa. Se il menu del tuo widget contiene una variabile di tipo "Weapon Class" (ad esempio, BP_Rifle_Child) e provi a impostare le sue variabili direttamente, stai puntando al template della classe, non all'actor attivo tra le mani del giocatore. Anche se riesci a eseguire con successo il cast all'istanza dell'actor attivo (ad esempio, il fucile attualmente equipaggiato) e a cambiare il suo BaseDamage da 25 a 50, tale modifica rimarrà transitoria.

Nel momento in cui il giocatore rinfodera il fucile, passa a una pistola ed equipaggia nuovamente il fucile, il gioco distrugge il vecchio actor del fucile e ne spawna uno nuovo. Il nuovo actor viene spawnato da zero partendo dal template del CDO, ripristinando il tuo BaseDamage potenziato al valore predefinito di 25. Per fare in modo che gli upgrade persistano, devi disaccoppiare le statistiche dell'arma dall'actor visivo spawnato nel mondo di gioco.

L'architettura di un sistema di armi persistente

Per risolvere questo problema, dobbiamo separare lo Stato (le statistiche dell'arma) dalla Presentazione (l'actor che renderizza l'arma e gestisce lo spawn dei proiettili). Invece di memorizzare il danno autorevole, la velocità di ricarica e la capacità delle munizioni all'interno dell'actor dell'arma stesso, li memorizziamo in una struttura dati persistente. Questa struttura dovrebbe risiedere in una classe che persiste oltre la distruzione dell'actor, come APlayerState, AGameState o un componente di inventario personalizzato.

Per i giochi multiplayer, la memorizzazione di queste variabili in un componente personalizzato richiede un'attenta gestione dell'ownership. Se memorizzi gli stati delle armi in un ActorComponent personalizzato, assicurati di non imbatterti negli incubi dell'inventario multiplayer causati da una errata ownership dei componenti dell'actor durante la replication. Custodendo i dati autorevoli nel APlayerState, ti assicuri che i dati vengano preservati anche quando il giocatore muore, cambia livello o scambia le proprie armi.

Quando il giocatore apre l'upgrade shop, il widget dell'interfaccia utente (UI widget) interagisce direttamente con lo stato dei dati del giocatore. L'acquisto di un upgrade modifica la struct persistente, non l'actor dell'arma. Quando il giocatore equipaggia un'arma, la classe del personaggio (character class) spawna l'actor dell'arma e lo inizializza immediatamente utilizzando la struct dei dati dallo stato del giocatore. Questo garantisce che ogni nuova arma spawnata erediti le statistiche corrette e potenziate.

Implementazione passo dopo passo: disaccoppiare i dati e la logica dell'actor

Scriviamo un'implementazione C++ pulita di questa architettura disaccoppiata. Definiremo una struct FWeaponStats che contiene i valori potenziabili e una classe d'arma base che può essere configurata dinamicamente.

1. Definizione della struct Weapon Stats

Per prima cosa, definiamo la nostra struttura dati. Questa struttura è accessibile da Blueprint, consentendo ai tuoi UI widget e ai child Blueprint creati dai designer di leggere e scrivere facilmente le statistiche.

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

Incapsulando le statistiche dell'arma in una singola struct FWeaponStats, rendiamo banale serializzarla, replicarla e passarla in giro. Invece di gestire cinque variabili float replicate separate, replichiamo una singola struct, riducendo l'overhead di replication.

2. Creazione della classe d'arma base

Successivamente, creiamo la classe d'arma base AWeaponBase. Questa classe rappresenta l'actor fisico nel mondo e contiene una funzione per inizializzare se stessa con le nuove statistiche.

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

Nella funzione di inizializzazione, assegniamo le statistiche passate alla nostra variabile replicata WeaponStats. In uno scenario reale, qui potresti anche attivare regolazioni visive o funzionali, come il ridimensionamento della mesh del caricatore, la modifica dei timer del rateo di fuoco o la variazione delle scale del sistema particellare in base alle nuove statistiche. Invece di ricaricare un asset Blueprint da 20 MB ogni volta che una statistica cambia, il passaggio di una struct C++ da 48 byte consente di risparmiare un enorme overhead di memoria.

3. Integrazione del widget dell'Upgrade Shop con il Player State

Per gestire gli upgrade in modo autorevole, il menu UI dello shop dovrebbe interagire con il PlayerState invece di tentare di eseguire il cast direttamente su istanze temporanee di actor. Progettiamo la classe AShooterPlayerState per gestire le statistiche del giocatore e convalidare gli upgrade.

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

La classe AShooterPlayerState gestisce la valuta del giocatore e il suo inventario di statistiche delle armi. Utilizzando le RPC (Server_UpgradeWeapon) e la replication, the server rimane l'unica fonte di verità (single source of truth) per le statistiche del giocatore.

Nota la funzione Server_UpgradeWeapon_Validate. Il sistema di convalida delle RPC di Unreal disconnette automaticamente i client che inviano richieste non valide, come il tentativo di potenziare un'arma senza possedere abbastanza oro.

Una volta che il server elabora l'upgrade, detrae l'oro, aggiorna le statistiche e replica le modifiche al client. Questa replication attiva automaticamente gli aggiornamenti dell'interfaccia utente (UI) lato client tramite le callback di replication.

Prevenire gli exploit lato client nel tuo Upgrade Shop

Se il tuo gioco è multiplayer, o se vuoi proteggere la tua economia single-player dai memory editor, non puoi fidarti del client per la gestione dei propri upgrade. Se al widget UI lato client è consentito chiamare Server_UpgradeWeapon(int32 NewDamage), un hacker può facilmente intercettare i pacchetti di rete o utilizzare strumenti come Cheat Engine to inviare un pacchetto dichiarando che il danno della propria arma è 999.999.

Senza una rigorosa convalida server-authoritative, il tuo gioco subirà gravi desincronizzazioni dello stato multiplayer in cui l'interfaccia utente locale del client pensa di avere un'arma potenziata, ma il server sta elaborando il danno in base alle statistiche originali di livello 1. Per evitare ciò, il client deve inviare solo un intento di potenziamento, come Server_RequestUpgrade(FName WeaponID). Il server esegue quindi la transazione in modo autorevole.

Il flusso di convalida del server deve seguire questi passaggi:

  1. Verifica delle risorse (Verify Resources): Controlla se il giocatore ha effettivamente abbastanza oro o materiali per acquistare l'upgrade.
  2. Convalida del percorso di upgrade (Validate Upgrade Path): Conferma che l'upgrade richiesto sia il successivo nella sequenza (ad esempio, dal livello 2 al livello 3, non saltando direttamente al livello 10).
  3. Detrazione del costo e applicazione (Deduct Cost and Apply): Detrai la valuta sul server e aggiorna la struct delle statistiche persistenti del giocatore.
  4. Replica e sincronizzazione (Replicate and Sync): Replica la struct aggiornata al client, che aggiornerà automaticamente l'arma equipaggiata.

Persistenza degli upgrade nel database di Backend

Sebbene mantenere le statistiche nel PlayerState funzioni durante una singola partita, queste statistiche vengono cancellate nel momento in cui il giocatore chiude il gioco o il server si riavvia. Per creare un vero loop di progressione, devi salvare queste modifiche dinamiche delle variabili in un database di Backend persistente.

Costruire tutto questo manualmente da soli è un'impresa enorme. Dovresti predisporre un database SQL o NoSQL, configurare un API gateway con autenticazione OAuth2, implementare una logica server personalizzata per analizzare i payload JSON e gestire casi limite come i timeout di connessione e il database sharding. La configurazione di questa infrastruttura può richiedere facilmente da 4 a 6 settimane di sviluppo dedicato, allontanando l'attenzione dal perfezionamento del core gameplay loop.

È qui che entra in gioco horizOn, che ti consente di memorizzare i dati di progressione dei giocatori in modo sicuro nel cloud senza scrivere codice per il database di Backend. Puoi utilizzare l'SDK del Backend per serializzare la tua struct FWeaponStats in JSON e salvarla direttamente nel profilo cloud del giocatore con regole di sicurezza server-authoritative. Un tipico payload JSON delle statistiche di un'arma è inferiore a 500 byte (circa 320 byte per un loadout standard), il che significa che le operazioni sul database vengono eseguite in meno di 15 ms. Questa velocità mantiene le transizioni dell'interfaccia utente (UI) scattanti e garantisce che i giocatori non subiscano rallentamenti o stuttering quando aprono i menu dello shop o finalizzano le transazioni.

Ad esempio, puoi scrivere una funzione di Cloud Code sicura nel Backend che convalida l'acquisto dell'upgrade. Quando il giocatore fa clic su "Buy Upgrade" nel menu del tuo widget, il gioco invia una richiesta API sicura. Il database verifica l'inventario del giocatore, detrae la valuta, scrive il nuovo livello dell'arma e restituisce il payload delle statistiche aggiornato. Questo garantisce che, anche se un giocatore altera la propria memoria locale, il database cloud autorevole rimanga sicuro.

Best Practice per i sistemi di upgrade in Unreal Engine

Per garantire che il tuo Upgrade Shop sia stabile e performante, segui questi principi fondamentali:

  1. Non modificare mai i Class Default Objects (CDO) a runtime: Tratta i CDO come Blueprint di sola lettura. Utilizzali esclusivamente per lo spawn delle mesh visive predefinite e dei template iniziali.
  2. Disaccoppia le statistiche dagli Actor: Memorizza le statistiche autorevoli in una classe persistente come APlayerState o GameInstance, e passale all'actor al momento dello spawn.
  3. Imponi la Server Authority: Non permettere mai ai client di stabilire direttamente le modifiche alle statistiche. I client richiedono gli upgrade; the server convalida le risorse e applica la modifica.
  4. Utilizza le struct per la serializzazione: Raggruppa le statistiche potenziabili in USTRUCT. Questo rende fluida la serializzazione per i salvataggi locali o per le chiamate al database di Backend.
  5. Ottimizza i payload del database: Mantieni i profili dei giocatori leggeri. Memorizza solo i livelli di upgrade (ad esempio, WeaponLevel: 3) nel database, e ricostruisci i valori reali in float (ad esempio, Damage: 45.0f) sul server di gioco.

Riepilogo e prossimi passi

Risolvere gli aggiornamenti dinamici delle variabili sui child Blueprint richiede il passaggio da un design incentrato sull'actor a un design incentrato sui dati. Memorizzando le statistiche in struct persistenti su PlayerState e inizializzando le armi in modo dinamico, eviti che gli upgrade scompaiano al cambio d'arma. Quando sarai pronto a portare questo sistema di progressione in multiplayer e a metterlo al sicuro nel cloud, l'integrazione con un database di Backend sarà essenziale.

Pronto a scalare i tuoi sistemi di progressione e a proteggere gli inventari dei giocatori? Prova horizOn gratuitamente o consulta la documentazione delle API per iniziare a costruire il tuo Backend oggi stesso.


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