Назад к блогу

Почему изменение Blueprint Defaults в runtime не работает: архитектура безопасного магазина улучшений в Unreal Engine

Опубликовано 13 июля 2026 г.
Почему изменение Blueprint Defaults в runtime не работает: архитектура безопасного магазина улучшений в Unreal Engine

Коротко о главном

В данном руководстве разбираются причины, по которым прямое изменение Blueprint Class Defaults в runtime приводит к ошибкам при реализации систем прогрессии в Unreal Engine. Описывается архитектура, разделяющая состояние данных оружия и его визуальное представление на сцене с использованием persistent-структур в `APlayerState`. Представлена реализация на C++ с репликацией данных на сервер для предотвращения читерства и эксплойтов на стороне клиента. Также показано, как интегрировать эту систему с Backend-решением от horizOn для надежного сохранения прогресса игроков в облаке.

Вы тратите недели на создание модульной системы оружия в Unreal Engine, создаете дочерний Blueprint для своей винтовки, настраиваете стильное меню магазина на UMG для улучшения скорости перезарядки, но обнаруживаете, что все улучшения исчезают, как только игрок меняет оружие или перезапускает уровень. Хуже того, при попытке выполнить cast к классу Blueprint в вашем меню Widget и изменить переменные ничего не происходит. Это классический bottleneck для разработчиков, создающих системы прогрессии.

В этом туториале мы разберем, почему модификация Blueprint в runtime не работает, как спроектировать persistent-систему оружия и как безопасно сохранять эти улучшения в базе данных.

Основная проблема: почему изменение Blueprint Class Defaults в runtime не работает

Когда вы редактируете переменные в Blueprint внутри Unreal Editor, вы изменяете Class Default Object (CDO). CDO служит главным шаблоном для каждого инстанса этого класса, спавнящегося в игровом мире. Однако в runtime прямое изменение CDO сильно ограничено, и на то есть веские причины. Если вы измените переменную CDO, вы рискуете изменить дефолтное значение для всех будущих спавнов, что вызовет проблемы с сериализацией и сломает replication.

Самая распространенная ошибка — это попытка выполнить cast ссылки на класс (TSubclassOf<AActor>) к инстансу этого класса. Если ваше меню Widget содержит переменную типа "Weapon Class" (например, BP_Rifle_Child) и вы пытаетесь задать ее переменные напрямую, вы обращаетесь к шаблону класса, а не к активному actor в руках игрока. Даже если вы успешно выполните cast к инстансу активного actor (например, к экипированной в данный момент винтовке) и измените ее BaseDamage с 25 до 50, это изменение будет временным (transient).

Как только игрок убирает винтовку, переключается на пистолет и снова экипирует винтовку, игра уничтожает старый actor винтовки и спавнит новый. Новый actor создается с нуля на основе шаблона CDO, сбрасывая улучшенный BaseDamage обратно к дефолтному значению 25. Чтобы сделать улучшения постоянными, вам нужно отвязать характеристики оружия от визуального actor, спавнящегося в мире.

Архитектура persistent-системы оружия

Чтобы решить эту проблему, мы должны разделить State (характеристики оружия) и Presentation (actor, визуализирующий оружие и обрабатывающий спавн снарядов). Вместо того чтобы хранить авторитетные значения урона, скорости перезарядки и емкости магазина внутри самого actor оружия, мы сохраняем их в persistent-структуре данных. Эта структура должна находиться в классе, который продолжает существовать после уничтожения actor, например в APlayerState, AGameState или в кастомном компоненте инвентаря.

Для multiplayer-игр хранение этих переменных в кастомном компоненте требует тщательного управления ownership. Если вы храните состояния оружия в кастомном ActorComponent, убедитесь, что вы не столкнетесь с multiplayer inventory nightmares, вызванными некорректным ownership компонентов actor во время replication. Размещая авторитетные данные в APlayerState, вы гарантируете их сохранность, даже когда игрок погибает, меняет уровни или переключает оружие.

Когда игрок открывает магазин улучшений, UI Widget взаимодействует напрямую с состоянием данных игрока. Покупка улучшения модифицирует persistent-структуру, а не actor оружия. Когда игрок экипирует оружие, класс персонажа спавнит actor оружия и сразу же инициализирует его, используя структуру данных из состояния игрока. Это гарантирует, что каждое вновь созданное оружие наследует правильные, улучшенные характеристики.

Пошаговая реализация: разделение логики данных и Actor

Давайте напишем чистую реализацию этой независимой архитектуры на C++. Мы определим структуру FWeaponStats, которая будет содержать обновляемые значения, и базовый класс оружия, который можно настраивать динамически.

1. Определение структуры Weapon Stats

Сначала определим нашу структуру данных. Эта структура доступна из Blueprint, что позволяет вашим UI Widget и дочерним Blueprint для дизайнеров легко читать и записывать характеристики.

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

Инкапсулируя характеристики оружия в одну структуру FWeaponStats, мы делаем ее сериализацию, replication и передачу тривиальными. Вместо управления пятью отдельными реплицируемыми float-переменными, мы выполняем replication одной структуры, снижая накладные расходы на сетевую передачу.

2. Создание базового класса оружия

Затем мы создаем базовый класс оружия AWeaponBase. Этот класс представляет собой физический actor в мире и содержит функцию для собственной инициализации новыми характеристиками.

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

В функции инициализации мы присваиваем переданные характеристики нашей реплицируемой переменной WeaponStats. В реальном сценарии вы также можете запускать здесь визуальные или функциональные изменения — например, изменять размер mesh магазина, корректировать таймеры скорострельности или масштабировать системы частиц на основе новых характеристик. Вместо перезагрузки ассета Blueprint размером 20 МБ при каждом изменении характеристик, передача 48-байтовой структуры C++ экономит огромный объем памяти.

3. Интеграция Widget магазина улучшений с Player State

Для авторитетного управления улучшениями UI-меню магазина должно взаимодействовать с PlayerState вместо попыток выполнить cast напрямую к временным инстансам actor. Давайте спроектируем класс AShooterPlayerState для управления характеристиками игрока и валидации улучшений.

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

Класс AShooterPlayerState управляет валютой игрока и его инвентарем с характеристиками оружия. Благодаря использованию RPC (Server_UpgradeWeapon) and replication, сервер остается единственным источником правды для характеристик игрока.

Обратите внимание на функцию Server_UpgradeWeapon_Validate. Встроенная в Unreal система валидации RPC автоматически отключает клиентов, отправляющих невалидные запросы, например попытку улучшить оружие при нехватке золота.

Как только сервер обрабатывает улучшение, он списывает золото, обновляет характеристики и выполняет replication изменений обратно на клиент. Эта replication автоматически запускает обновления UI на стороне клиента через коллбеки репликации (replication callbacks).

Предотвращение эксплойтов на стороне клиента в вашем магазине улучшений

Если ваша игра поддерживает Multiplayer или если вы хотите защитить экономику одиночной игры от редакторов памяти, вы не можете доверять клиенту управление собственными улучшениями. Если разрешить UI Widget на стороне клиента вызывать Server_UpgradeWeapon(int32 NewDamage), хакер сможет легко перехватить сетевые пакеты или использовать такие инструменты, как Cheat Engine, чтобы отправить пакет, утверждающий, что урон его оружия равен 999 999.

Без строгой server-authoritative валидации ваша игра столкнется с критическими multiplayer state desyncs, при которых локальный UI клиента считает, что оружие улучшено, а сервер рассчитывает урон на основе исходных характеристик 1-го уровня. Чтобы предотвратить это, клиент должен отправлять только намерение (intent) сделать улучшение, например Server_RequestUpgrade(FName WeaponID). Затем сервер авторитарно выполняет транзакцию.

Процесс валидации на сервере должен состоять из следующих шагов:

  1. Verify Resources: Проверить, действительно ли у игрока достаточно золота или ресурсов для покупки улучшения.
  2. Validate Upgrade Path: Подтвердить, что запрошенное улучшение является следующим по порядку (например, с уровня 2 на уровень 3, а не прыжок на уровень 10).
  3. Deduct Cost and Apply: Списать валюту на сервере и обновить persistent-структуру характеристик игрока.
  4. Replicate and Sync: Выполнить replication обновленной структуры обратно на клиент, что автоматически обновит экипированное оружие.

Сохранение улучшений в Backend-базе данных

Хотя хранение характеристик в PlayerState работает в течение одного матча, эти данные стираются, как только игрок закрывает игру или перезапускается сервер. Чтобы создать полноценный цикл прогрессии, вам необходимо сохранять эти динамические изменения переменных в persistent-базу данных на Backend.

Самостоятельная реализация такой системы с нуля — это огромная задача. Вам потребуется развернуть SQL- или NoSQL-базу данных, настроить API Gateway с аутентификацией OAuth2, написать кастомную серверную логику для парсинга JSON-payloads, а также обработать такие проблемы, как тайм-ауты соединения и шардинг базы данных. Настройка этой инфраструктуры может легко занять от 4 до 6 недель чистой разработки, отвлекая вас от полировки основного gameplay loop.

Здесь на помощь приходит horizOn, позволяющий безопасно хранить данные о прогрессе игроков в облаке без необходимости писать код для базы данных на Backend. Вы можете использовать Backend SDK, чтобы сериализовать вашу структуру FWeaponStats в JSON и сохранить ее напрямую в облачный профиль игрока с помощью server-authoritative правил безопасности. Типичный JSON-payload для характеристик оружия весит менее 500 байт (около 320 байт для стандартного набора снаряжения), что позволяет выполнять операции с базой данных менее чем за 15 мс. Такая скорость обеспечивает отзывчивость UI и гарантирует, что игроки не столкнутся с фризами при открытии меню магазина или завершении транзакций.

Например, вы можете написать безопасную функцию Cloud Code на Backend, которая проверяет покупку улучшения. Когда игрок нажимает "Buy Upgrade" в вашем меню Widget, игра отправляет безопасный API-запрос. База данных проверяет инвентарь игрока, списывает валюту, записывает новый уровень оружия и возвращает обновленный payload характеристик. Это гарантирует, что даже если игрок взломает локальную память на своем устройстве, авторитетная облачная база данных останется в безопасности.

Best Practices для систем улучшений в Unreal Engine

Чтобы ваш магазин улучшений работал стабильно и производительно, следуйте этим основным принципам:

  1. Никогда не редактируйте Class Default Objects (CDO) в runtime: Относитесь к CDO как к Blueprints только для чтения. Используйте их исключительно для спавна дефолтных визуальных meshes и начальных шаблонов.
  2. Отвяжите характеристики от Actors: Храните авторитетные характеристики в persistent-классе, таком как APlayerState или GameInstance, и передавайте их в actor при спавне.
  3. Обеспечьте Server Authority: Никогда не позволяйте клиентам напрямую менять характеристики. Клиенты запрашивают улучшения, а сервер валидирует ресурсы и применяет изменения.
  4. Используйте структуры для сериализации: Группируйте улучшаемые характеристики в USTRUCT. Это сделает сериализацию для локальных сейвов или запросов к Backend-базе данных бесшовной.
  5. Оптимизируйте payloads базы данных: Держите профили игроков компактными. Сохраняйте в базе данных только уровни улучшений (например, WeaponLevel: 3), а реальные float-значения (например, Damage: 45.0f) реконструируйте на игровом сервере.

Заключение и следующие шаги

Решение проблемы динамического обновления переменных в дочерних Blueprints требует перехода от actor-ориентированного дизайна к data-ориентированному. Храня характеристики в persistent-структурах в PlayerState и инициализируя оружие динамически, вы предотвращаете исчезновение улучшений при смене оружия. Когда вы будете готовы перевести эту систему прогрессии в Multiplayer и защитить ее в облаке, интеграция с Backend-базой данных станет обязательным шагом.

Готовы масштабировать свои системы прогрессии и защитить инвентарь игроков? Попробуйте horizOn бесплатно или изучите API-документацию, чтобы начать разработку своего Backend уже сегодня.


Источник: How to edit the values of a blueprint class that is a child and that is already in use by the player?