Flexible Data with UStruct

In game prototyping or pre-production we usually don’t know what kind of data we are going to use, these data will probably change in each iteration as we are trying to figure out the mechanics of our game, and as our code base get larger these changes will be more and more expensive to make and harder to manage. What I am going to show you in the article is an easy and effective way to manage this kind of problem in Unreal Engine 4.

Case Study

For the purpose of explaining the method, I am going to use a simple problem as case study, let’s say that we are making an RPG game in Unreal Engine 4 using C++ and in this game we have combat stats that looks something like this:

  • Health
  • Mana
  • Attack
  • Defense

We decided that the final combat stats of a character is calculated using this formula:

CombatStats = CombatStatsBase + CombatStatsModifier

The CombatStatsModifier is the sum of all CombatStats from the many elements that the character has such as from equipments, from status effects, from abilities, etc. Each component may need some common Arithmetic operations such as addition, multiplication, substraction, etc for generating its CombatStats. For example, the Health of a specific armor is 0.2 times the CombatStatBase of its user.

We have also decided to have an in-game UI that we can toggle at runtime to show the values of each member in CombatStats.

Let’s say that at any point in the development we may expand or change the CombatStats, maybe to something like this:

  • Health
  • Stamina
  • Mana
  • Attack
  • Defense
  • Intelligence
  • Hit
  • Dodge
  • MovementSpeed
  • AttackSpeed

Now from the case study above, we can extract these requirements:

  • CombatStats member variables may change in the future.
  • The final number of members in CombatStats may be more than 10.
  • Two CombatStats can be multiplied, divided, added, or substracted.
  • Since there will be multiplication and division with a fraction (e.g armor adds 20% health), the member should be a floating point number instead of an integer.
  • The user interface should show each of the member’s name, base value, and modifier value.

You must have encounter similar thing in your own game development process especially if you are using pre-production step or using some kind of agile game development practice that embraces changes in order to make a fun game.

Data Structure

First of all, we need to create the data structure for combat stats:

USTRUCT(BlueprintType, meta=(HiddenByDefault))
struct FCombatStats
{
    GENERATED_USTRUCT_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="CombatStats")
    float Health;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="CombatStats")
    float Mana;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="CombatStats")
    float Attack;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="CombatStats")
    float Defense;

    // Arithmetic operators
    FCombatStats operator*(const FCombatStats& Other) const;
    FCombatStats operator/(const FCombatStats& Other) const;
    FCombatStats operator+(const FCombatStats& Other) const;
    FCombatStats operator-(const FCombatStats& Other) const;
    // End of Arithmetic operators
};

Now lets look at the unreal struct metadata, we specified BlueprintType because we want this struct to be exposed to blueprint for obvious reasons that I do not need to explain, and the second one is HiddenByDefault which means that we told unreal to not show the pin when we break or make the structure in blueprint, the reason is because we will have more than 10 members, exposing all of them will be quite messy and we’re not going to need all of them anyway in most of our blueprint graphs.

Arithmetic Operator

In the struct declaration, we declared several Arithmetical operators, this is one of them:

FCombatStats FCombatStats:operator*(const FCombatStats& Other) const
{
    const float * const SelfPtr = reinterpret_cast<const float * const>( this );
    const float * const OtherPtr = reinterpret_cast<const float * const>( &Other );

    FCombatStats Result;
    float* ResultPtr = reinterpret_cast<float*>( &Result );
    int32 ElementNum = sizeof(FCombatStats) / sizeof(float);
    for (int32 i = 0; i < ElementNum; i++)
    {
        ResultPtr[i] = SelfPtr[i] * OtherPtr[i];
    }

    return Result;  
}

This operator is flexible and does not need to be changed if we modify or extend the members of FCombatStats. We can also say that the operator is independent or decoupled from the data. Other operators in this struct are similar to this one.

Blueprint Function Library

The operators the we have made are for C++, they are not accessible from Blueprint, to do that we need to make a Blueprint function library, it is quite simple and looks something like this:

UCLASS()
class UCombatStatsStatics : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
public:

    UFUNCTION(BlueprintPure, Category="CombatStats", meta=(DisplayName="CombatStats * CombatStats", CompactNodeTitle="*", Keywords="* multiply", CommutativeAssociativeBinaryOperator = "True"))
    static FCombatStats Multiply_CombatStatsCombatStats( FCombatStats A, FCombatStats B ) { return A * B; }

    UFUNCTION(BlueprintPure, Category="CombatStats", meta=(DisplayName="CombatStats / CombatStats", CompactNodeTitle="*", Keywords="/ divide"))
    static FCombatStats Divide_CombatStatsCombatStats( FCombatStats A, FCombatStats B ) { return A / B; }

    UFUNCTION(BlueprintPure, Category="CombatStats", meta=(DisplayName="CombatStats + CombatStats", CompactNodeTitle="+", Keywords="+ add plus", CommutativeAssociativeBinaryOperator = "True"))
    static FCombatStats Add_CombatStatsCombatStats( FCombatStats A, FCombatStats B ) { return A + B; }

    UFUNCTION(BlueprintPure, Category="CombatStats", meta=(DisplayName="CombatStats - CombatStats", CompactNodeTitle="-", Keywords="- substract minus"))
    static FCombatStats Substract_CombatStatsCombatStats( FCombatStats A, FCombatStats B ) { return A - B; }

};

The functions have many meta data and almost all of their purpose are to make the blueprint graph much cleaner, and the other important purpose is to make them usable in one of the best feature in Blueprint, the Add Math Expression node that basically enables Blueprint user to type mathematical expression instead of creating dozens of haywire nodes that are hard to read and manage.

Since these functions are basically calling the C++ operators, they are automatically become as flexible as the operators.

User Interface

The last one is the User Interface, we are using UMG (Unreal Motion Graphics) and we make a widget, WB_CombatStatWidget, that displays a single member of FCombatStats and then we make another widget, WB_CombatStatsPanel, that creates as many WB_CombatStatWidget as the number of members in FCombatStats and arrange them in a vertical box.

I am not going to go further into UMG as it is not within the scope of this article, but I am going to show you one of the most important function in the c++ parent class of WB_CombatStatsPanel. But before that let’s have a look at what we are trying to display in WB_CombatStatWidget, it is something like this:

[NameText] : [BaseValueText] ( +/- ModifierValueText )

The ModifierValueText is optional, the text may not visible if the modifier value is 0 and it is also colored differently when it has negative value (red) as opposed to positive value (green). This is a common representation of a character stat in most RPG or Strategy games such as Diablo, Torchlight or Dota 2; The data for WB_CombatStatWidget is:

USTRUCT(BlueprintType)
struct FCombatStatWidgetData
{
    GENERATED_USTRUCT_BODY()

    UPROPERTY(BlueprintReadWrite, Category="UI")
    FText NameText;

    UPROPERTY(BlueprintReadWrite, Category="UI")
    FText BaseValueText;

    UPROPERTY(BlueprintReadWrite, Category="UI")
    FSlateVisibility ModifierValueTextVisibility;

    UPROPERTY(BlueprintReadWrite, Category="UI")
    FLinearColor ModifierValueTextColor;

    UPROPERTY(BlueprintReadWrite, Category="UI")
    FText ModifierValueText;
};

Now, this data will be fed to each WB_CombatStatWidget created by WB_CombatStatsPanel, and WB_CombatStatsPanel get these data from this function below:

void UCombatStatsPanel::GetCombatStatWidgetDataList( const FCombatStats& BaseStats, const FCombatStats& ModifierStats, TArray<FCombatStatWidgetData>& OutDataList ) const
{
    UScriptStruct* CombatStatsStruct = FindObject<UScriptStruct>( nullptr, TEXT("/Script/MyProject.CombatStats") ); 

    OutDataList.Empty();

    for ( TFieldIterator<UFloatProperty> It(CombatStatsStruct); It; ++It )
    {
        UFloatProperty* Prop = *It;
        float BaseStat = Prop->GetFloatingPointPropertyValue( (void const *)(&BaseStats) );
        float ModifierStat = Prop->GetFloatingPointPropertyValue( (void const *)(&ModifierStats) );

        FCombatStatWidgetData Data;
#if WITH_EDITOR
        Data.NameText = Prop->GetDisplayNameText();
#else
        // TODO: create a lookup for member name or resources, but for now just use the name of the property
        Data.NameText = FText::FromString( Prop->GetName() );
#endif
        Data.BaseValueText = FText::AsNumber( BaseStat );
        if ( ModifierStat == 0 )
        {
            Data.ModifierValueTextVisibility = ESlateVisibility::Hidden;
        }
        else
        {
            Data.ModifierVisibility = ESlateVisibility::Visible;

            if ( ModifierStat > 0 )
            {
                Data.ModifierValueTextColor = FColor::Green;
                Data.ModifierValueText = FText::FromString( FString::Printf( TEXT("+ %.0f"), ModifierStat ) );
            }
            else
            {
                Data.ModifierValueTextColor = FColor::Red;
                Data.ModifierValueText = FText::FromString( FString::Printf( TEXT("- %.0f"), FMath::Abs(ModifierStat) ) );
            }
        }

        OutDataList.Add( Data );
    }
}

As you can see in the code, we are not doing brute force by writing it like this:

// Health
Data.NameText = LOCTEXT("Health", "Health");
Data.BaseValueText = FText::AsNumber( BaseStats.Health );
Data.ModifierValueText = FText::AsNumber( FMath::Abs(ModifierValue.Health) );

...

// Defense
Data.NameText = LOCTEXT("Defense", "Defense");
Data.BaseValueText = FText::AsNumber( BaseStats.Defense );
Data.ModifierValueText = FText::AsNumber( FMath::Abs(ModifierValue.Defense) );

That would not be flexible and very tightly coupled to the data declaration, what we have written are codes that are using the Unreal Engine reflection system, we iterate each of the float properties in that struct and populate the widget data from each of them, this is flexible and we do not need to change it if we modify the FCombatStats as long as it conforms to the rule of having all of the members to be a float point type.

Conclusion

By using this method we can easily modify the FCombatStats as much as we want without needing to rewrite the Arithmetic operators, UI codes, and many other codes that uses FCombatStats.

I have used this method in my project, it works well in both the editor and shipped game, it has significantly increased the speed of the development iterations, and I can focus on more important stuff of actually making the game fun.

One thing what I am worried about is the reinterpret casts in the Arithmetic operators, but if Unreal Engine has managed the endianness of the underlying platform (and I believe it has) then it wouldn’t be a problem, and as far as I know the only platforms that uses big-endian are IBM servers, which I don’t think has a large market share for games, they are.. well.. servers, that are meant to run non-game applications. But if you want to be on the safe side, you can use similar techniques as the UI codes which is iterating the script struct properties.

Published by

Fathurahman

I am a game programmer interested in many aspects of game programming especially tools development and User Interface. I am currently using Unreal Engine 5 for both work and off-work projects. I love to do figure drawing and digital painting in my free time.

Leave a comment