List MyBlueprint variables to CSV
- Tom Mooney
- 6 minutes ago
- 9 min read
This is a brief record of a cool utility that grabs the MyBlueprint list of Variables (via UProperty information obtained with the cunning use of flags).
The utility composes a CSV from everything it finds for a given Blueprint reference. The CSV can be shown in a spreadsheet, for example to inspect the Character or PlayerController variables' Name, Type, and Value, even with colour coding! It's really only an example -- unfortunately it doesn't let subsequent changes in the spreadsheet affect the BP. This exploration came from wanting to shift-select an entire screen of variables and Categorize them, which so far as I know, can only be done one by one, which is super tedious. So, while this doesn't provide editing in bulk, it does give oversight of information that takes a while to drill into within the MyBlueprint panel in Unreal.
Step 1 I tried to avoid using C++ for this, but I had to add a function to my blueprint function library. I started with only python, but couldn't figure out how to get the constituent variables in arrays or maps, and I wanted to skip the very long string results from getting the value of certain Struct variables. I also wanted to skip Engine and Inherited variable info. This goes in the blueprint function library FunFunctionLibrary (my custom thingy with all my nodes), but it's just a function. I figure if you have a C++ project, you'll know where this might go.
Expander: Header declaration of GetClassPropertiesDetailed function
UFUNCTION(BlueprintCallable, Category = "CSV",
meta = (
DisplayName = "Get Class Properties Detailed",
Tooltip = "This function retrieves editable UClass properties with details (name, type, value).",
Keywords = "properties class reflection detailed CSV"
)
)
static void GetClassPropertiesDetailed(
UClass* Class,
TArray<FString>& OutPropertyNames,
TArray<FString>& OutPropertyTypes,
TArray<bool>& OutIsDeclaredOnClass,
TArray<bool>& OutIsStructProperty
);
There are some specific includes needed for the .cpp : #include "FunFunctionLibrary.h" //or whatever unreal class you are using
#include "CoreMinimal.h"
#include "Templates/Casts.h"
#include "UObject/Class.h" // for TFieldIterator
#include "UObject/UnrealType.h" // for FProperty, FStructProperty, FArrayProperty, FMapProperty, FSetProperty
Expander: CPP definition of GetClassPropertiesDetailed function
void UFunFunctionLibrary::GetClassPropertiesDetailed(
UClass* Class,
TArray<FString>& OutPropertyNames,
TArray<FString>& OutPropertyTypes,
TArray<bool>& OutIsDeclaredOnClass,
TArray<bool>& OutIsStructProperty
)
{
OutPropertyNames.Empty();
OutPropertyTypes.Empty();
OutIsDeclaredOnClass.Empty();
OutIsStructProperty.Empty();
if (!IsValid(Class))
{
return;
}
for (TFieldIterator<FProperty> It(Class); It; ++It)
{
FProperty* Prop = *It;
if (!Prop->HasAnyPropertyFlags(CPF_Edit))
{
continue;
}
// Add property name
OutPropertyNames.Add(Prop->GetName());
// Determine full C++ type, including inner types for TArray, TMap, TSet
FString CppType;
if (FArrayProperty* ArrayProp = CastField<FArrayProperty>(Prop))
{
// Array inner type
FString InnerType = ArrayProp->Inner->GetCPPType(nullptr, 0);
CppType = FString::Printf(TEXT("TArray<%s>"), *InnerType);
}
else if (FMapProperty* MapProp = CastField<FMapProperty>(Prop))
{
// Map key/value types
FString KeyType = MapProp->KeyProp->GetCPPType(nullptr, 0);
FString ValueType = MapProp->ValueProp->GetCPPType(nullptr, 0);
CppType = FString::Printf(TEXT("TMap<%s,%s>"), *KeyType, *ValueType);
}
else if (FSetProperty* SetProp = CastField<FSetProperty>(Prop))
{
// Set element type
FString ElemType = SetProp->ElementProp->GetCPPType(nullptr, 0);
CppType = FString::Printf(TEXT("TSet<%s>"), *ElemType);
}
else
{
// Default primitive or object type
CppType = Prop->GetCPPType(nullptr, 0);
}
OutPropertyTypes.Add(CppType);
// Declared on this class?
OutIsDeclaredOnClass.Add(Prop->GetOwnerClass() == Class);
// Is a struct property?
OutIsStructProperty.Add(Prop->IsA(FStructProperty::StaticClass()));
}
}
Before going on, it's worth noting that the function returns parent variables of the class also. If only the user added variables are wanted, the function has to filter out super properties.
Expander: CPP defintion of same class but ignoring Super variables in the class
void UFunFunctionLibrary::GetClassPropertiesDetailed(
UClass* Class,
TArray<FString>& OutPropertyNames,
TArray<FString>& OutPropertyTypes,
TArray<bool>& OutIsDeclaredOnClass,
TArray<bool>& OutIsStructProperty
)
{
OutPropertyNames.Empty();
OutPropertyTypes.Empty();
OutIsDeclaredOnClass.Empty();
OutIsStructProperty.Empty();
if (!IsValid(Class))
{
return;
}
// Only properties *declared on this class* (no supers)
// => pass `None` so IncludeSuper is not set
for (TFieldIterator<FProperty> It(Class, EFieldIterationFlags::None); It; ++It)
{
FProperty* Prop = *It;
// Only editable props
if (!Prop->HasAnyPropertyFlags(CPF_Edit))
{
continue;
}
// Skip anything coming from /Script/Engine
const FString OwnerPkg = Prop->GetOwnerClass()->GetOutermost()->GetName();
if (OwnerPkg.StartsWith(TEXT("/Script/Engine")))
{
continue;
}
// ---- now guaranteed user-declared on this class ----
// Name
OutPropertyNames.Add(Prop->GetName());
// Resolve full C++ type
FString CppType;
if (const FArrayProperty* A = CastField<FArrayProperty>(Prop))
{
CppType = FString::Printf(
TEXT("TArray<%s>"),
*A->Inner->GetCPPType(nullptr, 0)
);
}
else if (const FMapProperty* M = CastField<FMapProperty>(Prop))
{
CppType = FString::Printf(
TEXT("TMap<%s,%s>"),
*M->KeyProp->GetCPPType(nullptr, 0),
*M->ValueProp->GetCPPType(nullptr, 0)
);
}
else if (const FSetProperty* S = CastField<FSetProperty>(Prop))
{
CppType = FString::Printf(
TEXT("TSet<%s>"),
*S->ElementProp->GetCPPType(nullptr, 0)
);
}
else
{
CppType = Prop->GetCPPType(nullptr, 0);
}
OutPropertyTypes.Add(CppType);
// This is always true here
OutIsDeclaredOnClass.Add(true);
// Struct?
OutIsStructProperty.Add(Prop->IsA<FStructProperty>());
}
}
Step 2 The C++ function will be used by a pythonscript in editor. Before setting up the script, we need to make a button to call the script. In my weird way, I made things hard for myself by trying to make it so that a user only needs the blueprint Copy Reference string that Unreal puts to clipboard in the content browser, RMB command Copy Reference, to populate as an input for each run of the python script.


I have an Editor Utility Widget that I have in my viewport to run things like python scripts or simple graphs that do things like setting multiple properties at once on selected level actors -- so this python that calls the function library function can go nicely in there. It's just a text labeled button, in the editor utility widget.

When the editor utility widget is Run, it shows up in the editor and can be docked to suit. On clicked, the added button processes the reference string of the blueprint we're interested in. We could add a textfield in which to paste the reference string, so as not to have to open the editor utility widget graph each time a new blueprint is processed. But for now...

Alright, I decided to do the textfield paste method for passing the CopyReference string to the TargetBP string. One could grab the clipboard, being a little faster, but it's probably better to see what string will be passed in.

Hmm, I notice the node Validate Path returns true with partial paths ; weird. Oh well, there's a better way, shown next. Well, if that node worked, it'd be the better way.

This editabletext changed string simply sets TargetBP string in the graph and sets a feedback in the utility UI (so you know); this is pretty optional. Anyway, we hope to see 'INFO" change to the CopyReference string.

This happens, but note that there's not much by way of failsafes in this setup. If you paste in anything but a valid blueprint reference string, it'll just say it's not valid in the INFO text instead.

To check that validity, an easy, but slightly expensive way is to poll the Asset Registry for the CopyReference string, once it's shortened to an actual path that the GetAssetsByPath node will accept:

Step 3 Next, comes the On Clicked event of the CSV_Vars button in our editor utility widget. This is an overview image, but clearly hard to read, so the full BP can be found here at UE Pastebin. Note: It's a fragment, so won't do anything if run by itself.

It's in four parts: 1 Ingest the CopyReference blueprint path 2 String ops to inject the class path into the python script 3 The python execute command (executes the C++ function and sets the CSV to disk) 4 The result feedback, based on success or failure Step 4 The pythonscript is called using the node Execute Pythonscript. Normally, for a script like this, it's easier to have an external python script path, but I wanted to be able to vary the class reference (blueprint, widget, etc) in editor. So, the script is shoe-horned into appends that feed the ExecutePythonscript string input.
Expander: Python to process the C++ function GetClassPropertiesDetailed
import unreal
import os
import csv
# ——— CONFIGURATION ——— # Input string of bp_asset
# ——— END CONFIGURATION ———
# 1. Load the Blueprint asset
bp_asset = unreal.load_asset(BP_PATH, unreal.Blueprint)
if not bp_asset:
unreal.log_error(f"[ExportProps] Could not load Blueprint at {BP_PATH}")
raise SystemExit
# 2. Get its generated UClass
bp_class = bp_asset.generated_class()
# 3. Call the detailed helper and unpack names & types
names, types, _, _ = unreal.FunFunctionLibrary.get_class_properties_detailed(bp_class)
# 4. Grab the Class Default Object for default values
cdo = unreal.get_default_object(bp_class)
# 5. Collect entries into a list and sort by name
entries = []
for name, ptype in zip(names, types):
default_val = cdo.get_editor_property(name)
entries.append((name, default_val, ptype))
entries.sort(key=lambda e: e[0].lower())
# 6. Write the sorted CSV
os.makedirs(os.path.dirname(CSV_PATH), exist_ok=True)
with open(CSV_PATH, "w", newline="") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["Name", "DefaultValue", "Type"])
for name, default_val, ptype in entries:
writer.writerow([name, default_val, ptype])
unreal.log(f"[ExportProps] Wrote {len(entries)} properties sorted by name to {CSV_PATH}")
Note that I'm using an append so that the input blueprint reference string slots into the python. This could be done other ways, but for some reason building with Appends was what got me there...

The injected blueprint reference also informs the CSV file output, so each different blueprint writes to a different file on the desktop: # ——— CONFIGURATION ———
BP_PATH = "/Game/4P/Blueprints/4P_Char.4P_Char"
CSV_PATH = os.path.join(os.path.expanduser("~"), "Desktop", "4P_Char_Properties_SortedByName.csv")
# ——— END CONFIGURATION ———
The part after the End Configuration comment essentially populates the CSV based on what the C++ function found in the blueprint; so it's like an intermediary data shuffler.
Earlier, I mentioned the C++ function can be altered so it filters out Super variables (ones from the class parent that the user didn't add, or probably didn't add); well, we can do the same in the pythonscript that processes the CSV results:
Pythonscript (after -End Configuration- comment) that favours user variables only.
# ——— END CONFIGURATION ———
# 1. Load the Blueprint asset
bp_asset = unreal.load_asset(BP_PATH, unreal.Blueprint)
if not bp_asset:
unreal.log_error(f"[ExportProps] Could not load Blueprint at {BP_PATH}")
raise SystemExit
# 2. Get its generated UClass
bp_class = bp_asset.generated_class()
# 3. Call the detailed helper and unpack names & types
names, types, declared_flags, struct_flags = unreal.FunFunctionLibrary.get_class_properties_detailed(bp_class)
# 4. Grab the Class Default Object for default values
cdo = unreal.get_default_object(bp_class)
# 5. Collect entries into a list and sort by name
entries = []
for name, ptype, declared, is_struct in zip(names, types, declared_flags, struct_flags):
# Skip anything not declared on this class
if not declared:
continue
# Skip engine-native classes
owner_pkg = bp_class.get_outermost().get_name()
if owner_pkg.startswith("/Script/Engine"):
continue
default_val = cdo.get_editor_property(name)
entries.append((name, default_val, ptype))
entries.sort(key=lambda e: e[0].lower())
# 6. Write the sorted CSV
os.makedirs(os.path.dirname(CSV_PATH), exist_ok=True)
with open(CSV_PATH, "w", newline="") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["Name", "DefaultValue", "Type"])
for name, default_val, ptype in entries:
writer.writerow([name, default_val, ptype])
unreal.log(f"[ExportProps] Wrote {len(entries)} properties sorted by name to {CSV_PATH}")

Sometimes you may write your own nested/heavily inherited blueprints, so it just depends which is wanted. The use of either approach could be regulated by a bool in the editor utility widget, so you could have a tickbox for 'Only User Variables' and let it roll either function (and either pythonscript) accordingly. The output CSV is sorted by variable name, but that could be skipped to return the list just like it is in the MyBlueprint panel. So, here's a sample of the infamous MyBlueprint variables that you can only edit one by one in unreal.

Notice that I assumed that it's a Blueprint that is being inspected; in the graph, it's possible to change the string Split nodes to have different types, such as the very editor utility widget discussed in this article: /Script/Blutility.EditorUtilityWidgetBlueprint'/Game/4P/Blueprints/Utility/FunBPHelperUtility.FunBPHelperUtility' It's not hard to swap the values, though perhaps an asset picker would be ideal. "Use Selected in Content Browser" is certainly the best thing in a game editor. [Edit: I went over the idea of supporting more class types from the pasted CopyReference string, and indeed opted to make a string array to compare the input string against. More entries could be added as needed. The class prefixes can be compared against the Split : LeftS result of /Game in the string.]

Anyway, for today, that's it for the unreal part. Here's the resulting CSV, filtered by type in colourful cells.

Step 5
Use File > Import to insert the data from the CSV to any Sheet. I ignore the CSV import settings for numbers and formulas, then align the B column left (the values). The Type Column (C) has colours set up using Conditional Formatting. This is a fancy way to apply a set of formulas to a set of ranges in a grouped way. You only need to add Conditional Format Rules. Follow the style and cell property steps to set colours and variable types (or whatever you want really) using Add Another Rule. This is for people who like a lot of rules.

The rules for all the variable types are a little tedious to add, but luckily can be copied from sheet to sheet using Copy > Paste Special > Conditional Formatting Only.

If you want to sort the table, without the Sort command changing the spreadsheet for all users, you can use Data > Create Filter, which is a temporary thing (that persists ... and is quite flexible). You can even filter (in the sense of filtering out what you don't want to sort) by properties like Colour.


The Filter View can be named, such as SortByType, then just use the 'funnel' icon that is appending in the relevant columns.

“Filter” (or “Funnel”) icon used to depict Filterable content
Guaranteed you will only ever need to know this if you work in a very large financial firm, or if you are some kind of wannabe game maker.

Comments