Sparse Class Data

The Sparse Class Data system eliminates wasted memory from redudant properties.

Choose your operating system:

Windows

macOS

Linux

The Sparse Class Data system saves memory by eliminating redundant data on commonly-used Actor types. While developing a game, Blueprint-exposed properties provide a great way for designers to iterate on Actor behavior. However, by the time the game ships, many of these properties may effectively be constants if their values do not vary across Actor instances or change during gameplay. With Sparse Class Data, you can transfer those properties into a single shared struct instance, keeping only one copy of the property in memory, while retaining the ability for designers to access its value in Blueprint graphs and edit it in the class defaults. To determine if a property is a good candidate for Sparse Class Data, use the following three-prong test. The property is a good candidate if:

  1. It is a member of an Actor class that has many instances in-game at the same time, meaning that a significant amount of memory is tied up in redundant copies.

  2. It does not change on placed Actor instances. In other words, the property does not need the EditInstanceOnly or EditAnywhere UProperty Specifiers, because no instance of the Actor overrides or changes its base value.

  3. C++ code does not modify it. Any C++ code that directly accesses the variable must be replaced with calls to an accessor function.

Implementing the Sparse Class Data feature requires native (C++) code. Any Blueprint-declared variables must move to C++ code to become eligible for this process.

In Unreal Engine 4.24, this feature is incompatible with Blueprint Nativization , meaning that you cannot nativize Blueprint classes that reference or are children of a class that implements Sparse Class Data. Attempting to cook a project with an ineligible Blueprint will generate warning messages to this effect.

Implementation Example

Once you have decided to use Sparse Class Data for one of your classes, you must identify the candidate properties. Any property tagged with EditAnywhere , EditInstanceOnly , or BlueprintReadWrite is not a candidate for Sparse Class Data. Similarly, any property that changes in native C++ code is ineligible to participate in the Sparse Class Data system. This is because the property could have a different value on one Actor instance from another, either through per-instance editing in the Level Editor, or by Blueprint Scripting or native code altering its value on a single Actor instance during a game session. The following example class contains several properties, some of which are candidates for Sparse Class Data:

// The properties in this class can all be changed in the editor, but on a per-class basis, not a per-instance basis.
UCLASS(BlueprintType)
class AMyActor : public AActor
{
    GENERATED_UCLASS_BODY()

public:
    // This property can be changed in the editor, but only on a per-class basis.
    // Blueprint graphs cannot access or change it.
    // It is a potential candidate for Sparse Class Data.
    UPROPERTY(EditDefaultsOnly)
    float MyFloatProperty;

    // This property cannot be changed in the editor.
    // Blueprint graphs can access it, but cannot change it.
    // It is a potential candidate for Sparse Class Data.
    UPROPERTY(BlueprintReadOnly)
    int32 MyIntProperty;

    // This property can be edited on a per-class basis in the editor.
    // Blueprint graphs can access it, but cannot change it.
    // It is a potential candidate for Sparse Class Data.
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    FName MyNameProperty;

    // This property can be edited on placed MyActor instances.
    // It is not a potential candidate for Sparse Class Data.
    UPROPERTY(EditAnywhere)
    FVector MyVectorProperty;

    // This property can be changed in Blueprint graphs.
    // It is not a potential candidate for Sparse Class Data.
    UPROPERTY(BlueprintReadWrite)
    FVector2D MyVector2DProperty;
};

After identifying candidate variables, create a struct to contain them, and mark the struct with the BlueprintType UStruct Specifier . Each property in the struct must include the EditDefaultsOnly Specifier.

USTRUCT(BlueprintType)
struct FMySparseClassData
{
    GENERATED_BODY()

    FMySparseClassData() 
    : MyFloatProperty(0.f)
    , MyIntProperty(0)
    , MyNameProperty(NAME_None)
    { }

    // You can set this property's default value in the editor.
    // Blueprint graphs cannot access it.
    UPROPERTY(EditDefaultsOnly)
    float MyFloatProperty;

    // This property's value will be set in C++ code.
    // You can access it (but not change it) in Blueprint graphs.
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    int32 MyIntProperty;

    // You can set this property's default value in the editor.
    // You can access it (but not change it) in Blueprint graphs.
    // "GetByRef" means that Blueprint graphs access a const ref instead of a copy.
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta=(GetByRef))
    FName MyNameProperty;
};

FMySparseClassData

The original class needs some modification to use this struct. There are three specific changes that the process requires:

  • Tell the class to use this struct as its Sparse Class Data type.

  • Add #if WITH_EDITORONLY_DATA precompiler directive blocks around the properties that are moving to the new struct, and mark them with _DEPRECATED suffixes in the original class. Additionally, remove all UProperty Specifiers and set the properties to private access. Do not change other portions of your code to use the _DEPRECATED names; those lines will be replaced with accessor calls into the Sparse Class Data struct.

  • In editor builds ( #if WITH_EDITOR ), override the MoveDataToSparseClassDataStruct function. This function will preserve the existing data values by performing a one-time copy from the original class to the Sparse Class Data struct.

After making these changes, your class should look like this:

UCLASS(BlueprintType, SparseClassDataTypes = MySparseClassData)
class AMyActor : public AActor
{
    GENERATED_BODY()

#if WITH_EDITOR
public:
    // ~ This function transfers existing data into FMySparseClassData.
    virtual void MoveDataToSparseClassDataStruct() const override;
#endif // WITH_EDITOR

#if WITH_EDITORONLY_DATA
    //~ These properties are moving out to the FMySparseClassData struct:
private:
    // This property can be changed in the editor, but only on a per-class basis.
    // Blueprint graphs cannot access or change it.
    // It is a potential candidate for Sparse Class Data.
    UPROPERTY()
    float MyFloatProperty_DEPRECATED;

    // This property cannot be changed in the editor.
    // Blueprint graphs can access it, but cannot change it.
    // It is a potential candidate for Sparse Class Data.
    UPROPERTY()
    int32 MyIntProperty_DEPRECATED;

    // This property can be edited on a per-class basis in the editor.
    // Blueprint graphs can access it, but cannot change it.
    // It is a potential candidate for Sparse Class Data.
    UPROPERTY()
    FName MyNameProperty_DEPRECATED;
#endif // WITH_EDITORONLY_DATA

    //~ The remaining properties can change on a per-instance basis and
    //~ are therefore not involved in the Sparse Class Data implementation.
public:
    // This property can be edited on placed MyActor instances.
    // It is not a potential candidate for Sparse Class Data.
    UPROPERTY(EditAnywhere)
    FVector MyVectorProperty;

    // This property can be changed in Blueprint graphs.
    // It is not a potential candidate for Sparse Class Data.
    UPROPERTY(BlueprintReadWrite)
    FVector2D MyVector2DProperty;
};

The following function copies the existing values of all shared properties:

#if WITH_EDITOR
void AMyActor::MoveDataToSparseClassDataStruct() const
{
    // make sure we don't overwrite the sparse data if it has been saved already
    UBlueprintGeneratedClass* BPClass = Cast<UBlueprintGeneratedClass>(GetClass());
    if (BPClass == nullptr || BPClass->bIsSparseClassDataSerializable == true)
    {
        return;
    }

    Super::MoveDataToSparseClassDataStruct();
    #if WITH_EDITORONLY_DATA
    // Unreal Header Tool (UHT) will create GetMySparseClassData automatically.
    FMySparseClassData* SparseClassData = GetMySparseClassData();

    // Modify these lines to include all Sparse Class Data properties.
    SparseClassData->MyFloatProperty = MyFloatProperty_DEPRECATED;
    SparseClassData->MyIntProperty = MyIntProperty_DEPRECATED;
    SparseClassData->MyNameProperty = MyNameProperty_DEPRECATED;
    #endif // WITH_EDITORONLY_DATA
}
#endif // WITH_EDITOR

Engine/BlueprintGeneratedClass.h

At this point, Sparse Class Data is in place. Users editing or accessing the affected properties in the editor will not notice any difference in behavior, and memory usage in shipping builds will be reduced. If the properties are referenced in C++ code, replace any attempts to access the variable with a call to a getter function. For example, anywhere that the code used to access the MyFloatProperty variable should now call GetMyFloatProperty instead. UHT will automatically generate this function for you. If you have a non-trivial getter function and need to keep its behaviors, the NoGetter UProperty Metadata Specifier will instruct UHT not to generate its own getter functions. For variables that you want to access by const reference instead of by value, use the GetByRef UProperty Metadata Specifier.

Help shape the future of Unreal Engine documentation! Tell us how we're doing so we can serve you better.
Take our survey
Dismiss