Blueprint Node: Sort Array of Actors By Field
Overview Author () Dear Community, This snippet is to show you how to make a utility function for blueprint arrays that allows you to sort the array by a given field, in ascending or descending ord...
Overview
Author ()
Dear Community,
This snippet is to show you how to make a utility function for blueprint arrays that allows you to sort the array by a given field, in ascending or descending order.
The Setup
Make yourselves a new Blueprint Function Library in C++.
The Header File (.h)
Ordering
We'll need to know in which order to sort the array: Ascending or Descending. I made an enumeration for this purpose and stuck it in the header file, before the "UXXXBlueprintFunctionLibrary" declaration:
UENUM(BlueprintType)
enum class ESortDirection: uint8
{
ASCENDING UMETA(DisplayName = "Ascending"),
DESCENDING UMETA(DisplayName = "Descending")
};
The Method Declaration
Let's go ahead and declare our method(s) in the header. We're going to follow the naming conventions of the other TArray utilities and name our methods: "Array_Sort" and "GenericArray_Sort".
We'll need four arguments:
-A pointer to the array that we'll be sorting
-A pointer to the array property (I'm not sure why, I was just following convention here).
-A string that will represent the field that we'll be sorting the array by.
-The sort direction.
So, here's what that looks like in code:
UFUNCTION(BlueprintCallable, CustomThunk, meta = (DisplayName = "Sort Array", CompactNodeTitle = "SORTARRAY", ArrayParm = "TargetArray|ArrayProperty"), Category = "Utilities|Array")
static void Array_Sort(const TArray& TargetArray, const UArrayProperty* ArrayProperty, const FString &FieldName, ESortDirection SortDirection);
static void GenericArray_Sort(void* TargetArray, const UArrayProperty* ArrayProp, const FString &FieldName, ESortDirection SortDirection);
The first method, "Array_Sort", is a dummy method. It should never be called. It's there solely to prevent compilation errors. "GenericArray_Sort" is where our sorting will actually be happening. The reason for this is that Unreal Engine 4's templated function support is limited (at the moment). More info here.
The Intercept
This next snippet is a bit of call stack magic. I believe this intercepts any call to our dummy method, "Array_Sort", and redirects it to "GenericArray_Sort".
DECLARE_FUNCTION(execArray_Sort)
{
Stack.StepCompiledIn(NULL);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
P_GET_OBJECT(UArrayProperty, ArrayProperty);
PARAM_PASSED_BY_REF(FieldName, UStrProperty, FString);
PARAM_PASSED_BY_VAL(SortDirection, UByteProperty, ESortDirection);
P_FINISH;
GenericArray_Sort(ArrayAddr, ArrayProperty, FieldName, SortDirection);
}
Custom Thunk
Next up is our Custom Thunk, which is also part of the pseudo-templating stuff. To be honest, I'm not 100% sure what it does/is used for, EXACTLY. Again, just following convention.
struct FXXXCustomThunkTemplates
{
template
static void Array_Sort(TArray& TargetArray, const UArrayProperty* ArrayProperty, const FString &FieldName, ESortDirection SortDirection)
{
UXXXBlueprintFunctionLibrary::GenericArray_Sort(&TargetArray, ArrayProperty, FieldName, SortDirection);
}
};
Source
#pragma once
#include "Kismet/BlueprintFunctionLibrary.h"
#include "XXXBlueprintFunctionLibrary.generated.h"
/**
*
*/
UCLASS()
class XXX_API UXXXBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, CustomThunk, meta = (DisplayName = "Sort Array", CompactNodeTitle = "SORTARRAY", ArrayParm = "TargetArray|ArrayProperty"), Category = "Utilities|Array")
static void Array_Sort(const TArray& TargetArray, const UArrayProperty* ArrayProperty, const FString &FieldName, ESortDirection SortDirection);
static void GenericArray_Sort(void* TargetArray, const UArrayProperty* ArrayProp, const FString &FieldName, ESortDirection SortDirection);
DECLARE_FUNCTION(execArray_Sort)
{
Stack.StepCompiledIn(NULL);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
P_GET_OBJECT(UArrayProperty, ArrayProperty);
PARAM_PASSED_BY_REF(FieldName, UStrProperty, FString);
PARAM_PASSED_BY_VAL(SortDirection, UByteProperty, ESortDirection);
P_FINISH;
GenericArray_Sort(ArrayAddr, ArrayProperty, FieldName, SortDirection);
}
};
struct FXXXCustomThunkTemplates
{
template
static void Array_Sort(TArray& TargetArray, const UArrayProperty* ArrayProperty, const FString &FieldName, ESortDirection SortDirection)
{
UXXXBlueprintFunctionLibrary::GenericArray_Sort(&TargetArray, ArrayProperty, FieldName, SortDirection);
}
};
The Sorting Predicate
Next up is our sorting predicate. This will be passed to the "Sort" method of our array of Actors. I put this in the header with all of the other stuff; You can put it wherever you want.
This does some UProperty type checking, casting, and comparisons.
struct FArraySortByFieldPredicate
{
FArraySortByFieldPredicate(const FString &InFieldName, ESortDirection InSortDirection)
: FieldName(InFieldName), SortDirection(InSortDirection)
{
}
bool operator ()(const AActor& A, const AActor& B) const
{
UClass *ourClass = A.GetClass();
if (ourClass != B.GetClass())
return false;
UProperty *targetProperty = FindField(ourClass, *FieldName);
if (targetProperty == nullptr)
return false;
const void *Aa = (SortDirection == ESortDirection::ASCENDING) ? &A : &B;
const void *Bb = (SortDirection == ESortDirection::ASCENDING) ? &B : &A;
if (targetProperty->IsA())
{
return Cast(targetProperty)->GetPropertyValue_InContainer(Aa) < Cast(targetProperty)->GetPropertyValue_InContainer(Bb);
}
else if (targetProperty->IsA())
{
return
Cast(targetProperty)->GetPropertyValue_InContainer(Aa) < Cast(targetProperty)->GetPropertyValue_InContainer(Bb);
}
else if (targetProperty->IsA())
{
return
Cast(targetProperty)->GetPropertyValue_InContainer(Aa) < Cast(targetProperty)->GetPropertyValue_InContainer(Bb);
}
else if (targetProperty->IsA())
{
return
Cast(targetProperty)->GetPropertyValue_InContainer(Aa) < Cast(targetProperty)->GetPropertyValue_InContainer(Bb);
}
else if (targetProperty->IsA())
{
return
Cast(targetProperty)->GetPropertyValue_InContainer(Aa) < Cast(targetProperty)->GetPropertyValue_InContainer(Bb);
}
else if (targetProperty->IsA())
{
return
Cast(targetProperty)->GetPropertyValue_InContainer(Aa) < Cast(targetProperty)->GetPropertyValue_InContainer(Bb);
}
else if (targetProperty->IsA())
{
return
Cast(targetProperty)->GetPropertyValue_InContainer(Aa) < Cast(targetProperty)->GetPropertyValue_InContainer(Bb);
}
else if (targetProperty->IsA())
{
return
Cast(targetProperty)->GetPropertyValue_InContainer(Aa).ToString() < Cast(targetProperty)->GetPropertyValue_InContainer(Bb).ToString();
}
// fall back, just let diff type win:
else
return false;
}
FString FieldName;
ESortDirection SortDirection;
};
The Source File (.cpp)
The Source File is fairly straightforward. It contains the body of our dummy method, "Array_Sort" and the body of "GenericArray_Sort".
#include "YourGame.h"
#include "XXXBlueprintFunctionLibrary.h"
void UXXXBlueprintFunctionLibrary::Array_Sort(const TArray& TargetArray, const UArrayProperty* ArrayProp, const FString &FieldName, ESortDirection SortDirection)
{
// We should never hit these! They're stubs to avoid NoExport on the class. Call the Generic* equivalent instead
check(0);
}
void UXXXBlueprintFunctionLibrary::GenericArray_Sort(void* TargetArray, const UArrayProperty* ArrayProp, const FString &FieldName, ESortDirection SortDirection)
{
if (TargetArray)
{
TArray *actorArray = (TArray *)TargetArray;
if (actorArray != nullptr)
{
actorArray->Sort(FArraySortByFieldPredicate(FieldName, SortDirection));
}
}
}
Notes/Concerns/TODO/Pitfalls
-This is not a truly templated sort method. I believe that you can send an array of objects that do not derive from AActor. In this case, the sort method will fail silently. With that being said:
-This could probably use better logging. Or any, really.
-If the class of the objects that you pass in does NOT contain the field in question, you may get weird results.
-The blueprint node does NOT contain field names. You'll have to infer which field is which (it isn't hard, considering that there are two of them, and one is an enumeration).
-The FArraySortByFieldPredicate structure is limited to basic types.
-I don't think sorting strings actually works right now. Perhaps calling "Compare()" would be better than the "\