I’ve been trying to create a dynamic data blueprint. It looks something like this:
Level1 is a Blueprintable UObject. It contains a number of UPROPERTIES, one of which is a TMap<FName, FLeve2> LevelTwos. The key for the map is generated using meta tag “GetKeyOptions”. The value is a struct similar to Level1. It, along with number of other properties, contains a TMap<FName, Level3> LevelThrees. This code demonstrates it so far:
UCLASS(Blueprintable)
class ULevel1 : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
int32 SomeOtherProperty = 0;
UPROPERTY(EditAnywhere, meta=(GetKeyOptions="GetKeyOptions", ShowOnlyInnerProperties))
TMap<FName, FLevel2> LevelTwos;
UFUNCTION()
TArray<FName> GetKeyOptions()
{
return {"A", "B", "C"};
}
};
USTRUCT(BlueprintType)
struct FLevel2
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
int32 SomeOtherProperty = 0;
UPROPERTY(EditAnywhere, meta=(ShowOnlyInnerProperties))
TMap<FName, FLevel3> LevelThrees;
};
USTRUCT(BlueprintType)
struct FLevel3
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
int32 SomeOtherProperty = 0;
};
With this method, I only need three steps to add an entry to LevelTwos:
- Add an entry to the map.
- Select something from the list as the key.
- Expand the value which shows data in the struct directly. If I did a little digging, I might find a way to trigger the expand with one of the PostEdit* functions, reducing the number of steps to only two.
What is not in this code is the fact that the key for LevelThrees is generated using the same function as LevelTwos (GetOptions will look through parents to find the appropriate function), except, the array that is returned depends on the key of the LevelTwos this FLevel2 struct is bound to. The problem is, meta tag GetKeyOptions (as well as GetValueOptions and GetOptions) has to be bound to function that takes no arguments and returns a TArray or TArray. This means that I need to know that I’m currently accessing LevelThrees map (instead of LevelTwos), and, if I’m accessing LevelThrees, I need to know which instance since the FNames I need to provide differ from instance to instance.
I’ve looked through the engine and there are several functions that can be overriden that get called when one is editing properties. Those are:
virtual void PreEditChange(FEditPropertyChain& PropertyAboutToChange);
virtual void PreEditChange(FProperty* PropertyAboutToChange);
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent);
virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent);
The problem with them is that they are called only after GetOptions and its variants are executed, so I can’t use them to setup the array that GetOptions would return. The easiest solution would be to just put GetOptions function to every FLevel2, dealing with the abiguity problem, however, the functions need to be UFUNCTIONS, which functions in USTRUCTS cannot be.
The next reasonable step would be to drop structs altogether and just use UObjects for all the layers:
UCLASS(Blueprintable)
class ULevel1 : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
int32 SomeOtherProperty = 0;
UPROPERTY(EditAnywhere, Instanced, meta=(GetKeyOptions="GetKeyOptions", ShowInnerProperties))
TMap<FName, ULevel2*> LevelTwos;
UFUNCTION()
TArray<FName> GetKeyOptions()
{
return {"A", "B", "C"};
}
};
UCLASS(BlueprintType, EditInlineNew)
class ULevel2 : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
int32 SomeOtherProperty = 0;
UPROPERTY(EditAnywhere, Instanced, meta=(GetKeyOptions="GetKeyOptions", ShowInnerProperties))
TMap<FName, ULevel3*> LevelThrees;
UFUNCTION()
TArray<FName> GetKeyOptions()
{
// PostEditProperty of ULevel1 could be used to store the
// parent key and return array dependant on it, the
// constant is just for the example
return {"D", "E", "F"};
}
};
UCLASS(BlueprintType, EditInlineNew)
class ULevel3 : public UObject
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
int32 SomeOtherProperty = 0;
};
This method does deal with all the previous problems, enabling me to use state of ULevel2 (and its own GetOptions function) to figure out which FNames I want to present to the user. The values for the maps are marked as Instanced and the UObjects which are used as parameters have EditInlineNew tag, enabling me to instance them within the blueprint (instead of the map values being references to other classes or blueprints defined somewhere else). These are the steps to add a new entry to the top level map, more complicated for user, unfortunately:
- Add an entry to the map.
- Select something from the list as the key.
- Select the class for the value (there should only be one choice).
- Expand the value, which just adds another row representing the class
- Expand the class to finally get to the values
I’ve also tried to use ShowInnerProperties (which is supposed to be version of ShowOnlyInnerProperties for uobjects), but it doesn’t seem to do anything. I’ve also made and attempt with TObjectPtr<> instead of raw pointer, but that also made no difference. Finally, I tried using ForceInlineRow meta tag for TMap, but I don’t think this works in this case.
You can see how it looks expanded in this picture:
The two main problems are the fact that I have to explicitly select ULevel2* as the value in the map and the fact that I have to expand it twice to get to all the data within it. Do you have any ideas as how I could have this work the way I want to, but to make is as simple as possible to the user?