Powrót do Bloga

Dlaczego modyfikowanie domyślnych wartości Blueprint w runtime kończy się niepowodzeniem: Projektowanie bezpiecznego sklepu z ulepszeniami w Unreal Engine

Opublikowano 13 lipca 2026
Dlaczego modyfikowanie domyślnych wartości Blueprint w runtime kończy się niepowodzeniem: Projektowanie bezpiecznego sklepu z ulepszeniami w Unreal Engine

W skrócie

Artykuł wyjaśnia przyczyny niepowodzeń przy modyfikowaniu domyślnych wartości Blueprint (CDO) w runtime i pokazuje, jak rozwiązać ten problem w Unreal Engine. Kluczem do trwałego systemu ulepszeń broni jest oddzielenie danych o jej stanie od wizualnego actora i zapisanie ich w klasie typu `PlayerState`. Autor przedstawia kompletną implementację w C++ z obsługą replikacji sieciowej oraz zabezpieczeniem transakcji przed cheatami. Na koniec omówiono integrację z chmurową bazą danych backendu za pomocą platformy horizOn w celu zachowania progresu między sesjami.

Spędzasz tygodnie na budowaniu modułowego systemu broni w Unreal Engine, tworzysz child blueprint dla swojego karabinu, konfigurujesz eleganckie menu sklepu UMG, aby ulepszyć szybkość przeładowania, po czym okazuje się, że ulepszenia znikają w momencie, gdy gracz zmienia broń lub restartuje poziom. Co gorsza, kiedy próbujesz wykonać cast na klasę Blueprint w swoim widget menu i zmodyfikować zmienne, nic się nie dzieje. To dobrze znany bottleneck dla deweloperów budujących systemy progresji.

W tym poradniku szczegółowo omówimy, dlaczego modyfikacje Blueprint w runtime kończą się niepowodzeniem, jak zaprojektować trwały (persistent) system broni oraz jak bezpiecznie przechowywać te ulepszenia w bazie danych.

Główny problem: Dlaczego modyfikowanie domyślnych wartości klasy Blueprint w runtime kończy się niepowodzeniem

Edytując zmienne w Blueprint w Unreal Editor, modyfikujesz Class Default Object (CDO). CDO działa jak główny szablon (master template) dla każdej instancji tej klasy zespawnowanej w świecie gry. Jednak w runtime bezpośrednie modyfikowanie CDO podlega silnym restrykcjom – i to z dobrego powodu. Jeśli zmienisz zmienną w CDO, ryzykujesz modyfikację wartości domyślnej dla wszystkich przyszłych zespawnowanych obiektów, co może prowadzić do problemów z serializacją i popsuć replikację.

Najczęstszym błędem jest próba wykonania cast referencji klasy (TSubclassOf<AActor>) na instancję klasy. Jeśli Twój widget menu zawiera zmienną typu „Weapon Class” (np. BP_Rifle_Child) i próbujesz ustawić jej zmienne bezpośrednio, modyfikujesz szablon klasy, a nie aktywnego actora w rękach gracza. Nawet jeśli pomyślnie wykonasz cast na instancję aktywnego actora (np. aktualnie wyposażony karabin) i zmienisz jego BaseDamage z 25 na 50, zmiana ta będzie tymczasowa.

W momencie, gdy gracz schowa karabin, przełączy się na pistolet i ponownie wyposaży karabin, gra niszczy starego actora karabinu i spawnuje nowego. Nowy actor jest spawnowany na nowo z szablonu CDO, co resetuje ulepszone BaseDamage z powrotem do domyślnej wartości 25. Aby ulepszenia były trwałe, musisz odseparować (decouple) statystyki broni od wizualnego actora zespawnowanego w świecie.

Architektura trwałego (persistent) systemy broni

Aby to rozwiązać, musimy oddzielić Stan (statystyki broni) od Prezentacji (actor renderujący broń i obsługujący spawn pocisków). Zamiast przechowywać autorytatywne wartości obrażeń, szybkości przeładowania i pojemności magazynka wewnątrz samego actora broni, zapisujemy je w trwałej strukturze danych. Struktura ta powinna znajdować się w klasie, która istnieje niezależnie od niszczenia actorów, jak np. APlayerState, AGameState lub dedykowany komponent ekwipunku.

W przypadku gier multiplayer przechowywanie tych zmiennych w niestandardowym komponencie wymaga ostrożnego zarządzania własnością (ownership). Jeśli przechowujesz stany broni w customowym ActorComponent, upewnij się, że nie napotkasz koszmarów z ekwipunkiem w multiplayer wywołanych przez błędne przypisanie własności komponentu actora podczas replikacji. Umieszczając autorytatywne dane w APlayerState, masz pewność, że zostaną one zachowane nawet wtedy, gdy gracz zginie, zmieni poziom lub zmieni broń.

Kiedy gracz otwiera sklep z ulepszeniami, widget UI wchodzi w bezpośrednią interakcję ze stanem danych gracza. Zakup ulepszenia modyfikuje trwałą strukturę (struct), a nie actora broni. Kiedy gracz wyposaża broń, klasa postaci spawnuje actora broni i natychmiast inicjalizuje go przy użyciu struktury danych z poziomu stanu gracza. Gwarantuje to, że każda nowo zespawnowana broń odziedziczy poprawne, ulepszone statystyki.

Wdrożenie krok po kroku: Rozdzielenie (decoupling) danych i logiki actora

Napiszmy czystą implementację tej odsprzęglonej architektury w C++. Zdefiniujemy strukturę FWeaponStats, która przechowuje ulepszane wartości, oraz bazową klasę broni, którą można dynamicznie konfigurować.

1. Definiowanie struktury statystyk broni

Najpierw definiujemy naszą strukturę danych. Struktura ta jest dostępna z poziomu Blueprint, co pozwala Twoim widgetom UI i tworzonym przez projektantów child blueprintom na łatwy odczyt i zapis statystyk.

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

Dzięki enkapsulacji statystyk broni w pojedynczej strukturze FWeaponStats, jej serializacja, replikacja i przekazywanie stają się banalnie proste. Zamiast zarządzać pięcioma osobnymi replikowanymi zmiennymi typu float, replikujemy jedną strukturę, co zmniejsza narzut replikacyjny (replication overhead).

2. Tworzenie bazowej klasy broni

Następnie tworzymy bazową klasę broni AWeaponBase. Klasa ta reprezentuje fizycznego actora w świecie i zawiera funkcję do inicjalizacji samego siebie przy użyciu nowych statystyk.

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

W funkcji inicjalizacji przypisujemy przekazane statystyki do naszej replikowanej zmiennej WeaponStats. W realnym scenariuszu wywoływałbyś w tym miejscu również zmiany wizualne lub funkcjonalne – takie jak zmiana rozmiaru mesha magazynka, modyfikacja timerów szybkostrzelności (fire rate) czy skalowanie systemów cząsteczkowych na podstawie nowych parametrów. Zamiast przeładowywać asset Blueprint o rozmiarze 20 MB za każdym razem, gdy zmienia się jakaś statystyka, przekazanie 48-bajtowej struktury C++ drastycznie zmniejsza narzut pamięciowy.

3. Integracja widgetu sklepu z ulepszeniami z Player State

Aby autorytatywnie zarządzać ulepszeniami, menu sklepu w UI powinno komunikować się z klasą PlayerState, zamiast próbować wykonywać cast bezpośrednio na tymczasowe instancje actorów. Zaprojektujmy klasę AShooterPlayerState, która będzie zarządzać statystykami gracza i walidować ulepszenia.

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

Klasa AShooterPlayerState zarządza walutą gracza oraz jego ekwipunkiem zawierającym statystyki broni. Dzięki wykorzystaniu funkcji RPC (Server_UpgradeWeapon) oraz replikacji, serwer pozostaje jedynym źródłem prawdy (single source of truth) dla statystyk gracza.

Zwróć uwagę na funkcję Server_UpgradeWeapon_Validate. System walidacji RPC w Unreal Engine automatycznie rozłącza (kickuje) klientów, którzy wysyłają nieprawidłowe żądania, np. próbując ulepszyć broń, nie posiadając wystarczającej ilości złota.

Gdy serwer przetworzy ulepszenie, odejmuje złoto, aktualizuje statystyki i replikuje zmiany z powrotem do klienta. Ta replikacja automatycznie wyzwala aktualizację UI po stronie klienta za pomocą callbacków replikacji (replication callbacks).

Zapobieganie exploitom po stronie klienta w Twoim sklepie z ulepszeniami

Jeśli Twój tytuł to gra multiplayer lub chcesz chronić ekonomię w grze jednoosobowej (single-player) przed edytorami pamięci, nie możesz ufać klientowi w kwestii zarządzania ulepszeniami. Jeśli widget UI po stronie klienta będzie mógł wywołać Server_UpgradeWeapon(int32 NewDamage), haker z łatwością przechwyci pakiety sieciowe lub użyje narzędzi takich jak Cheat Engine, aby wysłać pakiet z informacją, że obrażenia broni wynoszą 999 999.

Bez ścisłej walidacji opartej na autorytecie serwera (server-authoritative validation), Twoja gra będzie narażona na krytyczne desynchronizacje stanu w grze multiplayer, w których lokalne UI klienta uważa, że broń została ulepszona, podczas gdy serwer przetwarza obrażenia na podstawie początkowych statystyk z 1. poziomu. Aby temu zapobiec, klient powinien wysyłać jedynie zamiar ulepszenia, np. Server_RequestUpgrade(FName WeaponID). Następnie serwer autorytatywnie realizuje transakcję.

Proces walidacji po stronie serwera musi przebiegać według następujących kroków:

  1. Weryfikacja zasobów: Sprawdzenie, czy gracz faktycznie ma wystarczającą ilość złota lub surowców na zakup ulepszenia.
  2. Walidacja ścieżki ulepszeń: Potwierdzenie, czy żądane ulepszenie jest kolejne w kolejce (np. przejście z poziomu 2 na 3, a nie natychmiastowy skok na poziom 10).
  3. Pobranie kosztu i aplikacja zmian: Odliczenie waluty na serwerze i zaktualizowanie trwałej struktury statystyk gracza.
  4. Replikacja i synchronizacja: Zreplikowanie zaktualizowanej struktury z powrotem do klienta, co automatycznie zaktualizuje wyposażoną broń.

Zapisywanie ulepszeń w bazie danych backendu

Choć przechowywanie statystyk w PlayerState sprawdza się podczas pojedynczego meczu, dane te są usuwane w momencie wyłączenia gry przez gracza lub restartu serwera. Aby stworzyć prawdziwą pętlę progresji (progression loop), musisz zapisywać te dynamiczne zmiany zmiennych w trwałej bazie danych backendu.

Samodzielne zbudowanie takiego rozwiązania od zera to ogromne przedsięwzięcie. Musiałbyś wdrożyć bazę danych SQL lub NoSQL, skonfigurować bramę API (API gateway) z uwierzytelnianiem OAuth2, zaimplementować własną logikę serwera do parsowania payloadów JSON oraz obsłużyć skrajne przypadki (edge cases), takie jak limity czasu połączenia (connection timeouts) czy sharding bazy danych. Konfiguracja tej infrastruktury może z łatwością zająć od 4 do 6 tygodni dedykowanego czasu pracy dewelopera, odciągając Twoją uwagę od szlifowania głównej pętli rozgrywki (gameplay loop).

W tym miejscu z pomocą przychodzi horizOn, który pozwala na bezpieczne przechowywanie danych o progresji gracza w chmurze, bez konieczności pisania kodu bazy danych backendu. Możesz użyć SDK backendu do zserializowania struktury FWeaponStats do formatu JSON i zapisać ją bezpośrednio w profilu gracza w chmurze przy użyciu reguł bezpieczeństwa opartych na autorytecie serwera. Typowy payload JSON ze statystykami broni zajmuje mniej niż 500 bajtów (około 320 bajtów dla standardowego wyposażenia), co oznacza, że operacje na bazie danych wykonują się w czasie krótszym niż 15 ms. Taka prędkość pozwala zachować płynność przejść UI i gwarantuje, że gracze nie doświadczą zacięć (stutteringu) podczas otwierania menu sklepu czy finalizowania transakcji.

Na przykład możesz napisać bezpieczną funkcję Cloud Code na backendzie, która waliduje zakup ulepszenia. Kiedy gracz klika „Kup ulepszenie” w widget menu, gra wysyła bezpieczne zapytanie API. Baza danych weryfikuje ekwipunek gracza, pobiera walutę, zapisuje nowy poziom broni i zwraca zaktualizowany payload ze statystykami. Dzięki temu, nawet jeśli gracz zmodyfikuje lokalną pamięć gry, autorytatywna baza danych w chmurze pozostaje w pełni bezpieczna.

Dobre praktyki przy projektowaniu systemów ulepszeń w Unreal Engine

Aby upewnić się, że Twój sklep z ulepszeniami jest stabilny i wydajny, przestrzegaj następujących kluczowych zasad:

  1. Nigdy nie edytuj obiektów Class Default Objects (CDO) w runtime: Traktuj CDO jako Blueprinty wyłącznie do odczytu. Używaj ich jedynie do spawnowania domyślnych meshy wizualnych i szablonów startowych.
  2. Odseparuj (decouple) statystyki od actorów: Przechowuj autorytatywne parametry w klasie trwałej (np. APlayerState lub GameInstance) i przekazuj je do actora w momencie spawnowania.
  3. Wymuszaj autorytet serwera: Nigdy nie pozwalaj klientom bezpośrednio dyktować zmian statystyk. Klienci zgłaszają chęć zakupu ulepszenia; serwer waliduje zasoby i aplikuje zmiany.
  4. Używaj struktur (structs) do serializacji: Zgrupuj modyfikowalne parametry w struktury USTRUCT. Dzięki temu serializacja na potrzeby lokalnych zapisów lub zapytań do bazy danych backendu staje się bezproblemowa.
  5. Optymalizuj payloady bazy danych: Utrzymuj profile graczy w kompaktowej formie. Zapisuj w bazie wyłącznie poziomy ulepszeń (np. WeaponLevel: 3), a konkretne wartości float (np. Damage: 45.0f) odtwarzaj na serwerze gry.

Podsumowanie i kolejne kroki

Rozwiązanie problemu dynamicznych zmian zmiennych w child blueprintach wymaga przejścia z projektowania zorientowanego na actora (actor-centric) na projektowanie zorientowane na dane (data-centric). Przechowując statystyki w trwałych strukturach w PlayerState i dynamicznie inicjalizując broń, zapobiegasz znikaniu ulepszeń przy zmianie wyposażenia. Kiedy będziesz gotowy przenieść ten system progresji do trybu multiplayer i zabezpieczyć go w chmurze, integracja z bazą danych backendu stanie się niezbędna.

Gotowy na skalowanie systemów progresji i zabezpieczenie ekwipunku graczy? Wypróbuj horizOn za darmo lub zapoznaj się z dokumentacją API, aby już dziś zacząć budować swój backend.


Źródło: How to edit the values of a blueprint class that is a child and that is already in use by the player?