top of page
Search

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.



Note the /Script/Engine.Blueprint' format prefix...
Note the /Script/Engine.Blueprint' format prefix...

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.

Editor Utility Widget ... with a button!
Editor Utility Widget ... with a button!

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...

A string variable 'TargetBP' that has the Copy Reference value of the blueprint in question.
A string variable 'TargetBP' that has the Copy Reference value of the blueprint in question.

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.

EditorUtilityEditableText (added to the editor utility widget)
EditorUtilityEditableText (added to the editor utility widget)

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.

Once populated, the user has to press Enter to update this into the system, so to speak.
Once populated, the user has to press Enter to update this into the system, so to speak.

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:

String functions anyone?
String functions anyone?

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}")


Diff - shows that cunning use of 'declared flags' skips any variable not declared in the TargetBP class (left).
Diff - shows that cunning use of 'declared flags' skips any variable not declared in the TargetBP class (left).

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.

I think these can only be edited one by one. Tell me if I'm wrong!
I think these can only be edited one by one. Tell me if I'm wrong!

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.]


Fixed array of class prefixes allows more CSVs to be made with less editing of the EUW. QOL FTW IMO!
Fixed array of class prefixes allows more CSVs to be made with less editing of the EUW. QOL FTW IMO!

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


CSV doesn't care what application it opens in; Notepad++ or Googlesheets or Excel or Unreal. All are welcome.
CSV doesn't care what application it opens in; Notepad++ or Googlesheets or Excel or Unreal. All are welcome.

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.

Why do this?  It is worthwhile, if only to learn a small tip in Googlesheets on your day off!
Why do this? It is worthwhile, if only to learn a small tip in Googlesheets on your day off!

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.


Create filter view.  You know you want to.
Create filter view. You know you want to.
Isn't it great we can now filter by text colour Red to show only bools (coz bools are red)?  This is great, especially if you want to compare similar classes to match their variables, like if you decided to manually recreate an entire blueprint class in another project in another editor version and have different solution _API names for all your classes and functions.
Isn't it great we can now filter by text colour Red to show only bools (coz bools are red)? This is great, especially if you want to compare similar classes to match their variables, like if you decided to manually recreate an entire blueprint class in another project in another editor version and have different solution _API names for all your classes and functions.

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.

So many ways to sort things. 老いて死ね
So many ways to sort things. 老いて死ね

 
 
 

Comments


Featured Posts
Recent Posts
Search By Tags
Follow Us
  • Facebook Basic Square
  • Twitter Basic Square
  • Google+ Basic Square

Follow Tom

  • gfds.de-bluesky-butterfly-logo
  • facebook-square

​Copyright 2014 and beyond and beforehand

bottom of page