Kembali ke Blog

Mengapa Memodifikasi Blueprint Defaults saat Runtime Gagal: Merancang Toko Upgrade Unreal Engine yang Aman

Diterbitkan pada 13 Juli 2026
Mengapa Memodifikasi Blueprint Defaults saat Runtime Gagal: Merancang Toko Upgrade Unreal Engine yang Aman

Ringkasnya

Artikel ini membahas mengapa modifikasi Blueprint Class Defaults secara langsung saat runtime di Unreal Engine akan gagal dan mengakibatkan hilangnya data upgrade. Solusi terbaik adalah memisahkan data statistik senjata (state) dari visual actor (presentation) menggunakan struktur data persisten di `PlayerState`. Implementasi C++ disajikan untuk mengilustrasikan cara aman menginisialisasi aktor senjata menggunakan struct `FWeaponStats` yang direplikasi secara authoritative oleh server. Terakhir, artikel ini menyarankan penyimpanan data progresi secara aman menggunakan backend database cloud seperti horizOn untuk mencegah eksploitasi di sisi client.

Anda menghabiskan waktu berminggu-minggu untuk membangun sistem senjata modular di Unreal Engine, menginstansiasi child blueprint untuk senapan Anda, menyiapkan menu UMG shop yang apik untuk meng-upgrade reload speed-nya, hanya untuk menemukan bahwa upgrade tersebut hilang seketika saat pemain menukar senjata atau me-restart level. Lebih buruk lagi, ketika Anda mencoba melakukan cast ke Blueprint class di menu widget Anda dan memodifikasi variabelnya, tidak ada yang terjadi. Ini adalah bottleneck klasik yang sering dihadapi developer saat membangun sistem progresi.

Dalam tutorial ini, kita akan membahas mengapa modifikasi Blueprint saat runtime gagal, bagaimana merancang sistem senjata yang persisten, dan bagaimana menyimpan upgrade tersebut secara aman di database.

Masalah Utama: Mengapa Memodifikasi Blueprint Class Defaults saat Runtime Gagal

Ketika Anda mengedit variabel di dalam Blueprint pada Unreal Editor, Anda sebenarnya memodifikasi Class Default Object (CDO). CDO bertindak sebagai master template untuk setiap instance dari class tersebut yang di-spawn ke dalam game world. Namun, saat runtime, memodifikasi CDO secara langsung sangatlah dibatasi, dan tentu saja ada alasan kuat di baliknya. Jika Anda mengubah variabel CDO, Anda berisiko memodifikasi nilai default untuk semua spawn di masa mendatang, menyebabkan masalah serialisasi, dan merusak replication.

Kesalahan paling umum adalah mencoba melakukan cast dari class reference (TSubclassOf<AActor>) ke instance dari class tersebut. Jika menu widget Anda berisi variabel dengan tipe "Weapon Class" (misalnya, BP_Rifle_Child) dan Anda mencoba mengatur variabelnya secara langsung, Anda sebenarnya menargetkan class template, bukan actor aktif yang sedang dipegang oleh pemain. Bahkan jika Anda berhasil melakukan cast ke instance actor aktif (misalnya, senapan yang sedang digunakan) dan mengubah BaseDamage-nya dari 25 menjadi 50, perubahan tersebut bersifat transient.

Saat pemain menyarungkan senapan, beralih ke pistol, dan menggunakan senapan itu kembali, game akan men-destroy actor senapan yang lama dan men-spawn actor yang baru. Actor baru ini di-spawn langsung dari template CDO, sehingga me-reset BaseDamage yang telah di-upgrade kembali ke nilai default yaitu 25. Agar upgrade tetap persist, Anda harus memisahkan (decouple) statistik senjata dari visual actor yang di-spawn di world.

Arsitektur Sistem Senjata yang Persisten

Untuk mengatasi masalah ini, kita harus memisahkan State (statistik senjata) dari Presentation (actor yang me-render senjata dan menangani spawn projectile). Alih-alih menyimpan data damage, reload speed, dan kapasitas amunisi yang authoritative di dalam actor senjata itu sendiri, kita menyimpannya dalam struktur data yang persisten. Struktur ini sebaiknya berada di dalam class yang tetap bertahan meskipun actor di-destroy, seperti APlayerState, AGameState, atau komponen inventory kustom.

Untuk game multiplayer, menyimpan variabel-variabel ini dalam komponen kustom memerlukan manajemen ownership yang hati-hati. Jika Anda menyimpan weapon state dalam ActorComponent kustom, pastikan Anda tidak mengalami multiplayer inventory nightmares yang disebabkan oleh mismatch ownership pada actor component selama replication. Dengan menempatkan data authoritative di APlayerState, Anda memastikan data tetap terjaga bahkan ketika pemain mati, berpindah level, atau menukar senjata mereka.

Ketika pemain membuka menu upgrade shop, UI widget akan berinteraksi langsung dengan data state milik pemain. Pembelian upgrade akan memodifikasi struct persisten, bukan actor senjata. Ketika pemain menggunakan senjata, character class akan men-spawn actor senjata dan langsung menginisialisasinya menggunakan data struct dari player state. Ini menjamin bahwa setiap senjata yang baru di-spawn akan mewarisi statistik yang benar dan telah di-upgrade.

Implementasi Langkah-demi-Langkah: Memisahkan Data dan Logika Actor

Mari kita buat implementasi C++ yang bersih dari arsitektur yang terpisah (decoupled) ini. Kita akan mendefinisikan struct FWeaponStats yang menyimpan nilai yang dapat di-upgrade, dan base weapon class yang dapat dikonfigurasi secara dinamis.

1. Mendefinisikan Weapon Stats Struct

Pertama, kita mendefinisikan struktur data kita. Struktur ini dapat diakses melalui Blueprint, memungkinkan UI widget dan child blueprint yang digunakan oleh desainer untuk membaca dan menulis statistik dengan mudah.

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

Dengan mengenkapsulasi weapon stats ke dalam satu struct FWeaponStats, kita membuatnya sangat mudah untuk diserialisasi, direplikasi (replicate), dan dikirim ke mana saja. Alih-alih mengelola lima variabel float terpisah yang direplikasi, kita cukup mereplikasi satu struct tunggal, sehingga mengurangi overhead replication.

2. Membuat Base Weapon Class

Selanjutnya, kita membuat base weapon class AWeaponBase. Class ini mewakili physical actor di world dan memiliki fungsi untuk menginisialisasi dirinya sendiri dengan statistik baru.

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

Dalam fungsi inisialisasi, kita memasukkan statistik yang dikirim ke variabel WeaponStats yang kita replikasi. Di skenario dunia nyata, Anda juga akan men-trigger penyesuaian visual atau fungsional di sini—seperti mengubah ukuran magazine mesh, mengubah fire rate timer, atau memodifikasi skala particle system berdasarkan statistik baru tersebut. Alih-alih me-reload asset blueprint berukuran 20MB setiap kali ada statistik yang berubah, mengirimkan struct C++ berukuran 48-byte akan menghemat overhead memori dalam jumlah besar.

3. Mengintegrasikan Upgrade Shop Widget dengan Player State

Untuk mengelola upgrade secara authoritative, UI shop menu harus berinteraksi dengan PlayerState alih-alih mencoba melakukan cast secara langsung ke instance actor yang bersifat transient. Mari kita rancang class AShooterPlayerState untuk mengelola statistik pemain dan memvalidasi upgrade tersebut.

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

Class AShooterPlayerState mengelola currency pemain dan inventory weapon stats mereka. Dengan menggunakan RPC (Server_UpgradeWeapon) and replication, server tetap menjadi single source of truth untuk statistik pemain.

Perhatikan fungsi Server_UpgradeWeapon_Validate. Sistem validasi RPC bawaan Unreal secara otomatis akan menendang (kick) client yang mengirimkan request tidak valid, seperti mencoba meng-upgrade senjata tanpa gold yang cukup.

Setelah server memproses upgrade, server akan memotong gold, memperbarui statistik, dan mereplikasi perubahan tersebut kembali ke client. Proses replication ini men-trigger pembaruan UI di sisi client secara otomatis melalui callback replication.

Mencegah Exploit Sisi Client pada Upgrade Shop Anda

Jika game Anda adalah game multiplayer, atau jika Anda ingin melindungi ekonomi game single-player Anda dari memory editor, Anda tidak boleh memercayai client untuk mengelola upgrade mereka sendiri. Jika UI widget di sisi client diizinkan untuk memanggil Server_UpgradeWeapon(int32 NewDamage), hacker dapat dengan mudah mengintersepsi network packet atau menggunakan tools seperti Cheat Engine untuk mengirimkan packet yang mengeklaim damage senjata mereka adalah 999.999.

Tanpa validasi server-authoritative yang ketat, game Anda akan mengalami masalah fatal berupa multiplayer state desyncs, di mana UI lokal pada client menganggap mereka memiliki senjata yang telah di-upgrade, padahal server memproses damage berdasarkan statistik level 1 yang asli. Untuk mencegah hal ini, client hanya boleh mengirimkan intent untuk melakukan upgrade, misalnya Server_RequestUpgrade(FName WeaponID). Server kemudian mengeksekusi transaksi tersebut secara authoritative.

Alur validasi server harus mengikuti langkah-langkah berikut:

  1. Verify Resources: Periksa apakah pemain benar-benar memiliki cukup gold atau scrap untuk membeli upgrade.
  2. Validate Upgrade Path: Pastikan upgrade yang diminta berada dalam urutan yang benar (misalnya, Level 2 ke Level 3, bukan langsung melompat ke Level 10).
  3. Deduct Cost and Apply: Potong currency di server dan perbarui struct statistik persisten milik pemain.
  4. Replicate and Sync: Replikasi struct yang telah diperbarui kembali ke client, yang secara otomatis akan memperbarui senjata yang sedang digunakan.

Menyimpan Upgrade Secara Persisten ke Backend Database

Meskipun menyimpan statistik di PlayerState berfungsi dengan baik selama satu match, statistik tersebut akan hilang saat pemain menutup game atau server melakukan restart. Untuk membuat loop progresi yang sesungguhnya, Anda harus menyimpan perubahan variabel dinamis ini ke backend database yang persisten.

Membangun sistem ini sendiri secara manual adalah pekerjaan yang sangat besar. Anda harus menyediakan (provision) database SQL atau NoSQL, menyiapkan API gateway dengan autentikasi OAuth2, mengimplementasikan logika server kustom untuk melakukan parsing payload JSON, serta menangani edge cases seperti connection timeout dan database sharding. Penyiapan infrastruktur ini dapat dengan mudah memakan waktu 4 hingga 6 minggu waktu pengembangan khusus, sehingga mengalihkan fokus Anda dari memoles core gameplay loop.

Di sinilah horizOn hadir, memungkinkan Anda menyimpan data progresi pemain secara aman di cloud tanpa perlu menulis kode backend database. Anda dapat menggunakan SDK backend untuk melakukan serialisasi struct FWeaponStats menjadi JSON dan menyimpannya langsung ke profil cloud pemain dengan security rules yang server-authoritative. Payload JSON statistik senjata pada umumnya berukuran kurang dari 500 byte (sekitar 320 byte untuk standard loadout), yang berarti operasi database dapat dieksekusi dalam waktu kurang dari 15 ms. Kecepatan ini menjaga transisi UI tetap responsif (snappy) dan memastikan pemain tidak mengalami stuttering saat membuka menu shop atau menyelesaikan transaksi.

Sebagai contoh, Anda dapat menulis fungsi Cloud Code yang aman di backend untuk memvalidasi pembelian upgrade. Ketika pemain mengeklik "Buy Upgrade" di menu widget Anda, game akan mengirimkan secure API request. Database kemudian akan memverifikasi inventory pemain, memotong currency, menulis level senjata yang baru, dan mengembalikan payload statistik yang telah diperbarui. Ini memastikan bahwa meskipun pemain meretas (hack) memori lokal mereka, cloud database yang authoritative akan tetap aman.

Best Practices untuk Sistem Upgrade Unreal Engine

Untuk memastikan upgrade shop Anda stabil dan memiliki performa tinggi, ikuti prinsip-prinsip utama berikut:

  1. Jangan Pernah Mengedit Class Default Objects (CDOs) saat Runtime: Perlakukan CDO sebagai blueprint read-only. Gunakan CDO hanya untuk men-spawn visual mesh default dan template awal.
  2. Decouple Statistik dari Actor: Simpan statistik authoritative di dalam class yang persisten seperti APlayerState or GameInstance, lalu kirimkan statistik tersebut ke actor saat di-spawn.
  3. Enforce Server Authority: Jangan biarkan client menentukan perubahan statistik secara langsung. Client hanya meminta upgrade; server memvalidasi resources dan menerapkan perubahannya.
  4. Gunakan Struct untuk Serialisasi: Kelompokkan statistik yang dapat di-upgrade ke dalam USTRUCT. Hal ini membuat serialisasi untuk penyimpanan lokal atau pemanggilan backend database menjadi seamless.
  5. Optimalkan Payload Database: Jaga agar profil pemain tetap ramping. Simpan level upgrade saja (misalnya, WeaponLevel: 3) di database, lalu rekonstruksi nilai float aslinya (misalnya, Damage: 45.0f) di server game.

Ringkasan dan Langkah Selanjutnya

Mengatasi pembaruan variabel dinamis pada child blueprint memerlukan pergeseran dari desain actor-centric ke desain data-centric. Dengan menyimpan statistik dalam struct persisten pada PlayerState dan menginisialisasi senjata Anda secara dinamis, Anda mencegah upgrade hilang saat senjata ditukar. Ketika Anda siap untuk membawa sistem progresi ini ke ranah multiplayer dan mengamankannya di cloud, mendukungnya dengan backend database sangatlah penting.

Siap untuk mengembangkan sistem progresi Anda dan mengamankan inventory pemain? Coba horizOn secara gratis atau pelajari dokumentasi API untuk mulai membangun backend Anda hari ini.


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