Workarounds for referencing TArray of pointers to UStruct

I have a situation where I have a “graph” of different nodes contained in a UPolygonGraph class. Specifically, I have FCenter (accessing the center of a polygon), FVertex (accessing the vertex of a polygon) and FEdge (connecting neighboring centers or neighboring vertices) classes.

All 3 of these are stored in 3 different TArray variables in my UPolygonGraph class. Each struct also contains information about its neighbors – the FCenter struct contains information about all neighboring FCenter structures, as well as information about the FVertex structures that make up the vertices in the polygon and all the FEdge structures connecting vertex to vertex and center to center. The FVertex struct has information about neighboring vertices, what centers use this vertex as a polygon, all edges connected to the vertex, and so on.

The logical way to access the first vertex of a FCenter structure would be to do something like:

FVertex FirstVertex = FCenter.Vertices[0];
FirstVertex.Location = FVector::ZeroVector;

However, in reality, I can't do that -- while I can make as many copies of these structs as I want, because the TArrayis aUPROPERTY, it won't work properly (since, IIRC, pointers to UStructs in general aren't permitted, let alone pointers to TArrays of UStructs). I can't modify that vertex and have my changes "automatically" propagate to the larger UPolygonGraph` class and all neighboring structures.

As it is right now, neighboring structures are actually being referred to by integers (which correspond to the index of that structure in its respective array). Here’s that above example, where I try to fetch the first vertex of a FCenter object:

int VertexIndex = FCenter.Vertices[0];
FVertex FirstVertex = UPolygonGraph.GetVertexByIndex(VertexIndex);
FirstVertex.Location = FVector::ZeroVector;
UPolygonGraph.SetVertexByIndex(VertexIndex, FirstVertex);

As you can see, about 0% of that was intuitive. It doesn’t make sense that accessing the first element in the vertices array would return an integer. It doesn’t make sense that getting a vertex by index would return a copy and not a reference. It doesn’t make sense that you’d need to “manually” update the graph after you make changes to something.

I’m not sure exactly what a good workaround would be. I’d like to leave as much exposed to Blueprints as possible. I’ve been trying to use TArray and UPROPERTY instead of just using a native C++ array with pointers to everything. As mentioned, I can’t make a pointer to a UStruct accessible to Blueprints, so I can’t use pointers to the neighboring structures. I can’t call any functions declared within the UStruct within Blueprints, so I haven’t really written any functions for them. Changing them all to be UClass instead of UStruct is out of the question; I tried it once and took a major performance hit.

Any ideas? Or will this continue to just be super-confusing?

This is a bit cludgy, and I’m not sure that it will actually streamline anything for you; but this might help.

You say, “It doesn’t make sense that getting a vertex by index would return a copy and not a reference.” and I’m not sure why you’re choosing not to return by reference. I may have missed something in your explanation, but as far as I know that can be done in the situation you’re describing.

You also say, “I can’t call any functions declared within the UStruct within Blueprints, so I haven’t really written any functions for them.”

And this is correct, to a point. However, you can expose methods within structs to being called within a Blueprint by creating an intermediary library of static methods.

For use in C++, you can build your structs as you would without regard to BP, and use pointers to neighbors etc. You can build methods within your structs to perform the graph updates automatically; and you can build accessor methods that traverse the internal pointer chain to get the desired data.

Once you have that complete, you can build a BP Static Library of methods that call the methods within your structs for you.

A simple example might make this more clear. Let’s say you have a struct:

USTRUCT(BlueprintType)
struct FExampleStruct
{

    GENERATED_USTRUCT_BODY()

    /** Some BP exposed data */
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Example")
    FVector Location;

    /** Some non-BP exposed data */
    FVector HiddenLocation;

    //default constructor
    FExampleStruct(
        FVector NewLocation = FVector(0.0f),
        FVector NewHiddenLocation = FVector(0.0f)
    )
        :
        Location(NewLocation),
        HiddenLocation(NewHiddenLocation)
    {}

    //internal methods
    void AdjustLocation(FVector& Movement)
    {
        Location += Movement;
    }

    void AdjustHiddenLocation(FVector& Movement)
    {
        HiddenLocation += Movement;
    }

    float& ReferenceHiddenX()
    {
        return HiddenLocation.X;
    }

};

Normally, the internal methods and the HiddenLocation variable would be inaccessible from BP. But if we create a UBlueprintFunctionLibrary of static methods, we can access and modify the values:

HEADER:::

#pragma once

#include "Kismet/BlueprintFunctionLibrary.h"

UCLASS(Blueprintable, BlueprintType)
class EXAMPLE_API UExampleStructLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
public:

    /** Retrieves HiddenLocation value from referenced struct */
    UFUNCTION(BlueprintCallable, Category = "Example Struct")
    static FVector GetHiddenLocation(FExampleStruct& TargetStruct);
    
    /** Retrieves reference to HiddenLocation value from referenced struct */
    UFUNCTION(BlueprintCallable, Category = "Example Struct")
    static FVector& ReferenceHiddenLocation(FExampleStruct& TargetStruct);
    
    /** Retrieves reference to HiddenLocation.X value from referenced struct */
    UFUNCTION(BlueprintCallable, Category = "Example Struct")
    static float& ReferenceHiddenX(FExampleStruct& TargetStruct);

    /** Assigns referenced FVector value HiddenLocation within referenced struct */
    UFUNCTION(BlueprintCallable, Category = "Example Struct")
    static FVector SetHiddenLocation(FExampleStruct& TargetStruct, FVector& NewHiddenLocation);

    /** Adjusts Location within referenced struct by referenced FVector */
    UFUNCTION(BlueprintCallable, Category = "Example Struct")
    static void AdjustLocation(FExampleStruct& TargetStruct, FVector& Movement);

    /** Adjusts HiddenLocation within referenced struct by referenced FVector */
    UFUNCTION(BlueprintCallable, Category = "Example Struct")
    static void AdjustHiddenLocation(FExampleStruct& TargetStruct, FVector& Movement);

};


SOURCE:::

#include "ExampleGame.h"

#include "ExampleStructLibrary.h"

FVector UExampleStructLibrary::GetHiddenLocation(FExampleStruct& TargetStruct)
{
    return TargetStruct.HiddenLocation;
}

FVector& UExampleStructLibrary::ReferenceHiddenLocation(FExampleStruct& TargetStruct)
{
    return TargetStruct.HiddenLocation;
}

//note this implementation doesn't call the internal reference method
float& UExampleStructLibrary::ReferenceHiddenX(FExampleStruct& TargetStruct)
{
    return TargetStruct.HiddenLocation.X;
}

FVector UExampleStructLibrary::SetHiddenLocation(FExampleStruct& TargetStruct, FVector& NewHiddenLocation)
{
    TargetStruct.HiddenLocation = NewHiddenLocation;
}

void UExampleStructLibrary::AdjustLocation(FExampleStruct& TargetStruct, FVector& Movement)
{
    TargetStruct.AdjustLocation(Movement);
}

void UExampleStructLibrary::AdjustHiddenLocation(FExampleStruct& TargetStruct, FVector& Movement)
{
    TargetStruct.AdjustHiddenLocation(Movement);
}