Edit class array values in editor

I have a class which is going to store an array of classes called Elements. I would like to be able to add to the array a class, but not actually reference one by creating it. What I am trying to do is have it so that when you click the “+” icon when adding an element to the array, it will add a class to it, and I will be able to change the class’s variables from there. So, for example, when I add an element, it will come up with a list of variables like Name, Texture, Id, etc. I’ve seen it done in Unity (I know much more about Unity than I do about Unreal Engine), so I am unsure how to do it in Unreal.

What is your goal with this array? Are you planning to spawn objects from your class, and then have each object have the stored values you’ve applied in the array? Are you trying to create different versions of your class?

I’m not sure I understand what you mean by “change the class’s variables” - a class is not something you modify during runtime. A class is the rules for building a type of object. Different classes are different from each other, but the same class is always the same. Do you want to change the variables of different objects spawned of that class; but don’t want to spawn the objects yet?

Will you be storing multiple, different classes within the same array, or are all the elements in the array going to be the same class?

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Any)
TArray< TSubclassOf > ArrayOfClasses;

reference: https://answers.unrealengine.com/questions/133244/creating-an-array-of-classes.html

This only allows you to hold classes that are or are derived from the YourClass class, though; and it doesn’t allow you to modify any values within the class, or store individual settings for each instance.

You could also do:

TArray<UClass*> ArrayOfClasses;

Which allows you to store any type of class, regardless of inheritance; but it still doesn’t allow you to modify the values or store instance data.

What I want to do is create an array, then later on, I will assign each object an element. This will determine its material, density, etc. I was going to do this with a static class but the array could not be easily manipulated via the editor. The question:
“Do you want to change the variables of different objects spawned of that class; but don’t want to spawn the objects yet?”
My answer is Yes.

I want them all to be derived from a class called Element, then the objects (correct me if i’m wrong - I’m not an expert) will have different values when spawned/referenced.

I apologise if I am confusing

There are two ways I would suggest doing this:

  1. Create subclasses of your Element class that have the different values you want defined in them.

  2. Create a struct that holds the different values you want defined; and assign those values to new Element objects when they are spawned.

The details of these two methods, and which is more appropriate, depends on exactly what you’re doing and what you need; but I’ll provide some generic examples below.

Method one would look something like this:

//Element.h

UCLASS(Blueprintable, BlueprintType)
class MYGAME_API UElement : public UObject
{
    GENERATED_BODY()
    
public:

    /** The machine-friendly, unique label of this element */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Element Data")
    FName ElementName;

    /** The amount of mass (grams) held in a unit of volume of this element (cubic meter) */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Element Data")
    int32 Density;

    /** The material used to texture objects of this element */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Element Data")
    UMaterialInstance* ElementMaterial;

    UElement();

};


//Element.cpp

UElement::UElement()
    :
    UObject(),
    ElementName(NAME_None),
    Density(0),
    ElementMaterial(NULL)
{}


//GoldElement.h

UCLASS(Blueprintable, BlueprintType)
class MYGAME_API UGoldElement : public UElement
{
    GENERATED_BODY()

    UGoldElement();

};


//GoldElement.cpp

UGoldElement::UGoldElement()
    :
    UElement()
{
    static ConstructorHelpers::FObjectFinder<UMaterialInstance> ElementMaterialLoader(TEXT("Material'/Game/Materials/Elements/GoldElementMaterial_MAT.GoldElementMaterial_MAT'"));
    if (ElementMaterialLoader.Succeeded()) {
        ElementMaterial = ElementMaterialLoader.Object;

    }
    
    
    Density = 100;
    
    ElementName = TEXT("Gold");

}


//CarbonElement.h

UCLASS(Blueprintable, BlueprintType)
class MYGAME_API UCarbonElement : public UElement
{
    GENERATED_BODY()

    UCarbonElement();

};


//CarbonElement.cpp

UCarbonElement::UCarbonElement()
    :
    UElement()
{
    static ConstructorHelpers::FObjectFinder<UMaterialInstance> ElementMaterialLoader(TEXT("Material'/Game/Materials/Elements/CarbonElementMaterial_MAT.CarbonElementMaterial_MAT'"));
    if (ElementMaterialLoader.Succeeded()) {
        ElementMaterial = ElementMaterialLoader.Object;

    }
    
    
    Density = 25;
    
    ElementName = TEXT("Carbon");

}

Using method one doesn’t actually require you to use the array at all; you can just define the classes however you like and then spawn them as needed. You can also create new elements in the editor by creating a Blueprint based on the Element class.

You can then populate an array with your TSubclassOf classes. Alternatively, you could create a TMap that associates the classes with an FName identifier for easy retrieval. TMap, however, is not exposed to Blueprints, so you wouldn’t be able to populate the TMap directly in the blueprint; but since you know all the available classes beforehand in this method you can hard code it; and/or you can create methods that provide an interface to the TMap.

//.h

//including the UPROPERTY macro allows the engine to perform garage collection, but it's still not exposed to blueprints
UPROPERTY()
TMap<FName, TSubclassOf<UElement>> ElementClasses;

UFUNCTION(BlueprintCallable, Category = "Elements")
void PopulateElementClasses();

UFUNCTION(BlueprintCallable, Category = "Elements")
void AddElementClass(const FName& ElementName, TSubclassOf<UElement> ElementClass);

UFUNCTION(BlueprintCallable, Category = "Elements")
TSubclassOf<UElement> GetElementClass(const FName& ElementName);

UFUNCTION(BlueprintCallable, Category = "Elements")
UElement* SpawnElement(const FName& ElementName);


//.cpp
void UMyObject::PopulateElementClasses() {
    ElementClasses.Empty();
    ElementClasses.Emplace(TEXT("Gold"), UGoldElement::StaticClass());
    ElementClasses.Emplace(TEXT("Carbon"), UCarbonElement::StaticClass());

}

void UMyObject::AddElementClass(const FName& ElementName, TSubclassOf<UElement> ElementClass) {
    ElementClasses.Emplace(ElementName, ElementClass);

}

TSubclassOf<UElement> UMyObject::GetElementClass(const FName& ElementName) {
    if (ElementClasses.Contains(ElementName)) {
        return *ElementClasses.Find(ElementName);

    }
    return NULL;

}

UElement* UMyObject::SpawnElementClass(const FName& ElementName) {
    TSubclassOf<UElement> ElementClass = GetElementClass(ElementName);
    if (ElementClass) {
        return NewObject<UElement>(ElementClass);

    }
    return NULL;

}

You could also create a TArray> that builds itself into the TMap, the advantage being that you can update the elements in the blueprint editor. You can leave this as a TArray and forget the TMap, but the disadvantage is that the retrieval method is a bit more convoluted:

//.h

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Elements")
TArray<TSubclassOf<UElement>> ElementClassArray;

UFUNCTION(BlueprintCallable, Category = "Elements|Initialization")
void PopulateClassTMap();

UFUNCTION(BlueprintCallable, Category = "Elements")
TSubclassOf<UElement> GetElementClassFromArray(const FName& ElementName);


//.cpp

void UMyObject::PopulateClassTMap() {
    for (auto& EachClass : ElementClassArray) {
        UObject* CDObject = EachClass::GetDefaultObject();
        if (CDObject) {
            UElement* EachElement = Cast<UElement>(CDObject);
            if (EachElement) {
                ElementClasses.Emplace(EachElement->ElementName, EachClass);

            }

        }

    }
    //optionally, clear the ElementClassArray from RAM if you're only going to use the TMap
    ElementClassArray.Empty();

}

TSubclassOf<UElement> UMyObject::GetElementClassFromArray(const FName& ElementName) {
    for (auto& EachClass : ElementClassArray) {
        UObject* CDObject = EachClass::GetDefaultObject();
        if (CDObject) {
            UElement* EachElement = Cast<UElement>(CDObject);
            if (EachElement && EachElement->ElementName == ElementName) {
                return EachClass;

            }

        }

    }
    return NULL;
    
}

Method two is very similar, but you use an FStruct to define the individual elements, and you always spawn from the same class. In my example below, we’re using the same UElement class defined in method one.

USTRUCT(BlueprintType, Category = "GMT Library|Custom Data Types|Structs|Components")
struct FElementData
{
    GENERATED_USTRUCT_BODY()

    /** The machine-friendly, unique label of this element */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Element Data")
    FName ElementName;

    /** The amount of mass (grams) held in a unit of volume of this element (cubic meter) */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Element Data")
    int32 Density;

    /** The material used to texture objects of this element */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Element Data")
    UMaterialInstance* ElementMaterial;
    
    FElementData(const FName NewElementName = NAME_None, int32 NewDensity = 0, UMaterialInstance* NewElementMaterial = NULL)
        :
        ElementName(NewElementName),
        Density(NewDensity),
        ElementMaterial(NewElementMaterial)
    {}

};

Using these structs will allow you to define new elements during runtime, provided you have an appropriate material for the new element.

//.h

UPROPERTY()
TArray<FElementData> Elements;

UFUNCTION(BlueprintCallable, Category = "Elements")
void AddElement(const FName& ElementName, const int32& Density, UMaterialInstance* Material);

UFUNCTION(BlueprintCallable, Category = "Elements")
void AddElementData(const FElementData& Element);

UFUNCTION(BlueprintCallable, Category = "Elements")
FElementData GetElementData(const FName& ElementName);

UFUNCTION(BlueprintCallable, Category = "Elements")
UElement* SpawnElement(const FName& ElementName);


//.cpp

void AddElement(const FName& ElementName, const int32& Density, UMaterialInstance* Material) {
    Elements.Emplace(FElementData(ElementName, Density, Material));

}

void AddElementData(const FElementData& Element) {
    Elements.Emplace(Element);

}

FElementData UMyObject::GetElementData(const FName& ElementName) {
    for (auto& EachElement : Elements) {
        if (EachElement.ElementName == ElementName) {
            return EachElement;

        }

    }
    return FElementData();
    
}

UElement* UMyObject::SpawnElement(const FName& ElementName) {
    if (ElementName == NAME_None) {
        return NULL;

    }
    FElementData& ElementData = GetElementData(ElementName);
    if (ElementData.ElementName == ElementName) {
        UElement* NewElement = NewObject<UElement>();
        if (NewElement) {
            NewElement->ElementName = ElementName;
            NewElement->Density = ElementData.Density;
            NewElement->ElementMaterial = ElementData.ElementMaterial;
            return NewElement;

        }

    }
    return NULL;
    
}

You can also build the array into a TMap as detailed in method one.

Thank you for your input, it is very helpful and informative. I think I will use the first option. It’s much simpler and seems to make more sense and is more logical in my mind than option 2.

It all depends on what you need it to do. :wink:

I’m glad this helped.

Please be sure to mark the question as resolved so others looking with similar questions can find the answer.