A Better Unreal DataTable Row Picker

· Read in about 7 min · (1451 Words)

It’s always better to drive your game’s systems from data that’s easily editable. Games require a lot of iteration, and if you can just play with settings on the fly instead of having to change code, you can try things out much faster, and everyone on the team can experiment, not just programmers.

A key asset type for this in Unreal is the DataTable. You make a row struct, then fill it with whatever data you like. Great!

Referencing a DataTable Row

But, what if you want to refer to a particular row of that table in another asset? Anyone who has worked on databases knows that you always need more than one table, and you use “foreign keys” to locate rows in other tables.

In Unreal, the unique key of a row is the Row Name, which is an FName. It’s always the first column when you’re editing a DataTable, and is there regardless of what struct you use to define the rows.

But how do we refer to this Row Name in other tables, or from another general class?

Unreal provides one built-in way, but it’s not very usable. It is the FDataTableRowHandle. If you create a UPROPERTY of this type, it appears like this:

UPROPERTY(EditAnywhere)
FDataTableRowHandle RowHandle;
FDataTableRowHandle property

Notice how it requires you to pick both the DataTable and the row. That’s “fine” I guess, but it’s really tedious to have to pick that datatable. When you’re designing systems like this, you almost always already know which data table you’re referencing, you just want to pick the row. Having to pick the table as well introduces potential human error, takes more time - especially if this is itself in a table or some other collection, where you’re having to pick the DataTable every damn time for each entry.

What I really wanted to do was pre-define the datatable a property is supposed to use, and to only have to pick the row from a drop-down. Seems obvious, but there is no in-built solution for this, despite many people asking about it on the forum over the years.

So, as usual I had to figure it out myself. But thanks to this blog post, you won’t have to. 🙂

A Table-specific Row Handle

So here’s the new way of defining a UPROPERTY that references a table row, with the DataTable pre-defined:

UPROPERTY(EditAnywhere, meta=(DataTable="/Game/Data/DT_YourData"))
FStevesFixedDataTableRowHandle DataRow;

A couple of notes:

  • I’m using a new type called FSsFixedDataTableRowHandle instead of FDataTableRowHandle
  • I provide the location of the DataTable in a meta tag, using the standard asset ref format. It’s pointing to a regular DataTable asset.

When you use that type, the property editor shows this instead:

New property edit experience

Notice how I just have a single property row with a simple drop-down. I don’t have to tell it what DataTable to use, I just get a simple fast interface to reference a row of the table that I already knew I wanted to use. The table itself looks like this:

DataTable view

The Juicy Code Bits

If you’d rather just have all this setup done for you, you can download my open source plugin “StevesUEHelpers”, which includes this code, among other things.

If you’d rather incorporate it into your own code manually, read on.

But how does all this work, Steve? Pull up a chair…

Game Module Code

In your game module, you’ll need the new type FStevesFixedDataTableRowHandle. This type lives in your game module, because the references are part of your game data. It’s just one header file:

// Filename: StevesFixedDataTableRowHandle.h
#pragma once
#include "Engine/DataTable.h"

#include "StevesFixedDataTableRowHandle.generated.h"

/// Just a type to denote that this table row handle should be edited differently
USTRUCT(BlueprintType)
struct STEVESUEHELPERS_API FStevesFixedDataTableRowHandle : public FDataTableRowHandle
{
	GENERATED_USTRUCT_BODY()
};

You’ll note that all this does is subclass FDataTableRowHandle, and does nothing else. So why do we need it? Well, it’s because we want to customise the property editor, and we need a unique type to associate the code with. Under the hood, the row reference is still going to know which datatable it came from - this is important, because only the editor will auto-select the DataTable, so at runtime we still need the direct reference. But our new type will allow us to make this look different in the property editor and remove the need to select the table.

Editor Module Code

You need a C++ Editor Module in your project for this part. If you don’t have one already, check out the tutorial on the wiki on how to create one, I’m not going to cover that here. All the code in this section must be placed in your Editor module. If you put it in your Game module it will still work, but it will break your standalone game builds. You don’t want that. 😉

We need to create an implementation of IPropertyTypeCustomization to change how properties of type FStevesFixedDataTableRowHandle are edited.

Header File: (Link)

/// Filename: StevesFixedDataTableCustomisationLayout.h
#pragma once

#include "Containers/Array.h"
#include "Containers/UnrealString.h"
#include "IPropertyTypeCustomization.h"
#include "Templates/SharedPointer.h"
#include "UObject/NameTypes.h"

class IPropertyHandle;
class SToolTip;
class UDataTable;
class UScriptStruct;
struct FAssetData;

/// Drop-down for data table row name when the table itself is fixed in meta tags
class FStevesFixedDataTableCustomisationLayout : public IPropertyTypeCustomization
{
public:
	static TSharedRef<IPropertyTypeCustomization> MakeInstance() 
	{
		return MakeShareable( new FStevesFixedDataTableCustomisationLayout );
	}

	virtual void CustomizeHeader(TSharedRef<class IPropertyHandle> InStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override;
	// Not needed, but must be implemented
	virtual void CustomizeChildren(TSharedRef<class IPropertyHandle> InStructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override {}

protected:
	bool GetCurrentValue(UDataTable*& OutDataTable, FName& OutName) const;
	void OnSearchForReferences();
	void OnGetRowStrings(TArray< TSharedPtr<FString> >& OutStrings, TArray<TSharedPtr<SToolTip>>& OutToolTips, TArray<bool>& OutRestrictedItems) const;
	FString OnGetRowValueString() const;
	void BrowseTableButtonClicked();

	TSharedPtr<IPropertyHandle> DataTablePropertyHandle;
	TSharedPtr<IPropertyHandle> RowNamePropertyHandle;

};

Source File: (Link)

// Filename: StevesFixedDataTableCustomisationLayout.cpp
#include "StevesFixedDataTableCustomisationLayout.h"

#include "AssetRegistry/AssetData.h"
#include "Containers/Map.h"
#include "DataTableEditorUtils.h"
#include "Delegates/Delegate.h"
#include "DetailWidgetRow.h"
#include "Editor.h"
#include "Engine/DataTable.h"
#include "Fonts/SlateFontInfo.h"
#include "Framework/Commands/UIAction.h"
#include "HAL/Platform.h"
#include "HAL/PlatformCrt.h"
#include "Internationalization/Internationalization.h"
#include "Internationalization/Text.h"
#include "Misc/Attribute.h"
#include "PropertyCustomizationHelpers.h"
#include "PropertyEditorModule.h"
#include "PropertyHandle.h"
#include "Templates/Casts.h"
#include "UObject/Class.h"
#include "UObject/Object.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Text/STextBlock.h"

class SToolTip;

#define LOCTEXT_NAMESPACE "FSsFixedDataTableCustomisationLayout"

void FStevesFixedDataTableCustomisationLayout::CustomizeHeader(TSharedRef<class IPropertyHandle> InStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
	DataTablePropertyHandle = InStructPropertyHandle->GetChildHandle("DataTable");
	RowNamePropertyHandle = InStructPropertyHandle->GetChildHandle("RowName");

	if (InStructPropertyHandle->HasMetaData(TEXT("DataTable")))
	{
		// Find data table from asset ref
		const FString& DataTablePath = InStructPropertyHandle->GetMetaData(TEXT("DataTable"));
		if (UDataTable* DataTable = LoadObject<UDataTable>(nullptr, *DataTablePath, nullptr))
		{
			DataTablePropertyHandle->SetValue(DataTable);
		}
		else
		{
			UE_LOG(LogDataTable, Warning, TEXT("No Datatable found at %s"), *DataTablePath);
		}
	}
	else
	{
		UE_LOG(LogDataTable, Warning, TEXT("No Datatable meta tag present on property %s"), *InStructPropertyHandle->GetPropertyDisplayName().ToString());
	}

	

	FPropertyComboBoxArgs ComboArgs(RowNamePropertyHandle, 
			FOnGetPropertyComboBoxStrings::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::OnGetRowStrings), 
			FOnGetPropertyComboBoxValue::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::OnGetRowValueString));
	ComboArgs.ShowSearchForItemCount = 1;


	TSharedRef<SWidget> BrowseTableButton = PropertyCustomizationHelpers::MakeBrowseButton(
		FSimpleDelegate::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::BrowseTableButtonClicked),
		LOCTEXT("SsBrowseToDatatable", "Browse to DataTable in Content Browser"));
	HeaderRow
	.NameContent()
	[
		InStructPropertyHandle->CreatePropertyNameWidget()
	]
	.ValueContent()
	.MaxDesiredWidth(0.0f) // don't constrain the combo button width
	[
			SNew(SHorizontalBox)
			+SHorizontalBox::Slot()
			.FillWidth(1.0f)
			[
				PropertyCustomizationHelpers::MakePropertyComboBox(ComboArgs)
			]
			+SHorizontalBox::Slot()
			.Padding(2.0f)
			.HAlign(HAlign_Center)
			.VAlign(VAlign_Center)
			.AutoWidth()
			[
				BrowseTableButton
			]
	];	;

	FDataTableEditorUtils::AddSearchForReferencesContextMenu(HeaderRow, FExecuteAction::CreateSP(this, &FStevesFixedDataTableCustomisationLayout::OnSearchForReferences));
}

void FStevesFixedDataTableCustomisationLayout::BrowseTableButtonClicked()
{
	if (DataTablePropertyHandle.IsValid())
	{
		UObject* SourceDataTable = nullptr;
		if (DataTablePropertyHandle->GetValue(SourceDataTable) == FPropertyAccess::Success)
		{
			TArray<FAssetData> Assets;
			Assets.Add(SourceDataTable);
			GEditor->SyncBrowserToObjects(Assets);
		}
	}	
}

bool FStevesFixedDataTableCustomisationLayout::GetCurrentValue(UDataTable*& OutDataTable, FName& OutName) const
{
	if (RowNamePropertyHandle.IsValid() && RowNamePropertyHandle->IsValidHandle() && DataTablePropertyHandle.IsValid() && DataTablePropertyHandle->IsValidHandle())
	{
		// If either handle is multiple value or failure, fail
		UObject* SourceDataTable = nullptr;
		if (DataTablePropertyHandle->GetValue(SourceDataTable) == FPropertyAccess::Success)
		{
			OutDataTable = Cast<UDataTable>(SourceDataTable);

			if (RowNamePropertyHandle->GetValue(OutName) == FPropertyAccess::Success)
			{
				return true;
			}
		}
	}
	return false;
}

void FStevesFixedDataTableCustomisationLayout::OnSearchForReferences()
{
	UDataTable* DataTable;
	FName RowName;

	if (GetCurrentValue(DataTable, RowName) && DataTable)
	{
		TArray<FAssetIdentifier> AssetIdentifiers;
		AssetIdentifiers.Add(FAssetIdentifier(DataTable, RowName));

		FEditorDelegates::OnOpenReferenceViewer.Broadcast(AssetIdentifiers, FReferenceViewerParams());
	}
}

FString FStevesFixedDataTableCustomisationLayout::OnGetRowValueString() const
{
	if (!RowNamePropertyHandle.IsValid() || !RowNamePropertyHandle->IsValidHandle())
	{
		return FString();
	}

	FName RowNameValue;
	const FPropertyAccess::Result RowResult = RowNamePropertyHandle->GetValue(RowNameValue);
	if (RowResult == FPropertyAccess::Success)
	{
		if (RowNameValue.IsNone())
		{
			return LOCTEXT("DataTable_None", "None").ToString();
		}
		return RowNameValue.ToString();
	}
	else if (RowResult == FPropertyAccess::Fail)
	{
		return LOCTEXT("DataTable_None", "None").ToString();
	}
	else
	{
		return LOCTEXT("MultipleValues", "Multiple Values").ToString();
	}
}

void FStevesFixedDataTableCustomisationLayout::OnGetRowStrings(TArray< TSharedPtr<FString> >& OutStrings, TArray<TSharedPtr<SToolTip>>& OutToolTips, TArray<bool>& OutRestrictedItems) const
{
	UDataTable* DataTable = nullptr;
	FName IgnoredRowName;

	// Ignore return value as we will show rows if table is the same but row names are multiple values
	GetCurrentValue(DataTable, IgnoredRowName);

	TArray<FName> AllRowNames;
	if (DataTable != nullptr)
	{
		for (TMap<FName, uint8*>::TConstIterator Iterator(DataTable->GetRowMap()); Iterator; ++Iterator)
		{
			AllRowNames.Add(Iterator.Key());
		}

		// Sort the names alphabetically.
		AllRowNames.Sort(FNameLexicalLess());
	}

	for (const FName& RowName : AllRowNames)
	{
		OutStrings.Add(MakeShared<FString>(RowName.ToString()));
		OutRestrictedItems.Add(false);
	}
}


#undef LOCTEXT_NAMESPACE

You need to register this property customisation as well, which you do in your editor module startup code: (Link)

// in your module c++ code
void FStevesUEHelpersEdModule::StartupModule()
{

	FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
	PropertyModule.RegisterCustomPropertyTypeLayout("StevesFixedDataTableRowHandle", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FSsFixedDataTableCustomisationLayout::MakeInstance));
	
}

void FStevesUEHelpersEdModule::ShutdownModule()
{
	FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
	PropertyModule.UnregisterCustomPropertyTypeLayout("StevesFixedDataTableRowHandle");
}

Your module will need to reference a few libraries; in your YourEdModule.build.cs:

PrivateDependencyModuleNames.AddRange(
	new string[]
	{
		"UnrealEd",
		"PropertyEditor",
		"DataTableEditor"
	}
);

Conclusion

And that’s it! To repeat, once you have this code in your project you can provide an easy table-specific drop-down in your properties very simply:

UPROPERTY(EditAnywhere, meta=(DataTable="/Game/Data/DT_YourData"))
FSsFixedDataTableRowHandle DataRow;

I’ve seen a few people asking about this on the forums and never seemingly solving it, so hopefully this helps someone.