Download

Having issues dynamically loading a DataTable from a blueprint-exposed TAssetPtr

Hey everyone!

I’ve been trying (in conjunction with some friends) to create a dialogue system similar to the ones found in Dragon Age and Mass Effect. I’ve created some container classes, an interactible Dialogue actor, and some external tools to author dialogue trees. These trees are stored as CSV files, and loaded into the engine as UDataTables. So far so good.

However, when trying to access/load these dynamically from the engine (i.e, selecting an asset from the browser and setting it in the actor’s defaults,) a number of issues crop up.

First of all, this is my header:


// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "UseableSkeletalActor.h"
#include "DialogueDataTable.h"
#include "DialogueInterface.h"
#include "DialogueActor.generated.h"

/**
 * The DialogueActor derives from the UseableSkeletalActor, which gives it basic use functionality (outline and interaction)
 */
UCLASS()
class MYGAME_API ADialogueActor : public AUseableSkeletalActor, public IDialogueInterface
{
	GENERATED_BODY()

public:
	ADialogueActor(const class FObjectInitializer& ObjectInitializer);

	

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue", meta = (DisplayName = "Dialogue Tree Asset-Ptr"))
		TAssetPtr<UDataTable> DataTable;
	
	UFUNCTION(BlueprintCallable, Category = "Dialogue")
		virtual void LoadDialogueData(UDataTable* Table) override;

	UFUNCTION(BlueprintCallable, Category = "Dialogue")
		virtual UDataTable* GetLoadedDialogueData() override;

	UFUNCTION(BlueprintCallable, Category = "Dialogue")
		virtual TArray<UDialogueMenu*> GetDialogueMenus() override;

	UFUNCTION(BlueprintCallable, Category = "Dialogue")
		virtual UDialogueMenu* GetDialogueMenu(int32 TargetMenu) override;

	UFUNCTION(BlueprintCallable, Category = "Dialogue")
		virtual void BeginConversation(AActor* TargetActor) override;
};

This code allows me to boot up the editor, but does not give me a useable reference to the table. Instead, it returns null.


ADialogueActor::ADialogueActor(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	//load table from asset and call LoadDialogueData
	static ConstructorHelpers::FObjectFinder<UDataTable> DataTable_BP(*DataTable.ToStringReference().ToString());

	if (GEngine)
	{
		//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("DataTable'/Game/Blueprints/DialogueObjects/Testing/DwarfInBarrel-en_GB.DwarfInBarrel-en_GB'"));
		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, *DataTable.ToStringReference().ToString());
	}

	if (DataTable_BP.Object != NULL)
	{
		UDataTable* Table = DataTable_BP.Object;
		LoadDialogueData(Table);
	}			
}

void ADialogueActor::LoadDialogueData(UDataTable* Table)
{
	IDialogueInterface* Interface = Cast<IDialogueInterface>(this);
    if (Interface)
	{
		if (GEngine)
		{
			GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("Interface OK."));
		}
		if (Table != NULL)
		{
			if (GEngine)
			{
				GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("Loading Table."));
			}
			Interface->LoadDialogueData(Table);
		}		
	}
}

If I instead hardcode the asset path for testing purposes, like this:


ADialogueActor::ADialogueActor(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	//load table from asset and call LoadDialogueData
	static ConstructorHelpers::FObjectFinder<UDataTable> DataTable_BP(TEXT("DataTable'/Game/Blueprints/DialogueObjects/Testing/DwarfInBarrel-en_GB.DwarfInBarrel-en_GB'"));

	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("DataTable'/Game/Blueprints/DialogueObjects/Testing/DwarfInBarrel-en_GB.DwarfInBarrel-en_GB'"));
	}

	if (DataTable_BP.Object != NULL)
	{
		UDataTable* Table = DataTable_BP.Object;
		LoadDialogueData(Table);
	}			
}

void ADialogueActor::LoadDialogueData(UDataTable* Table)
{
	IDialogueInterface* Interface = Cast<IDialogueInterface>(this);
    if (Interface)
	{
		if (GEngine)
		{
			GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("Interface OK."));
		}
		if (Table != NULL)
		{
			if (GEngine)
			{
				GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("Loading Table."));
			}
			Interface->LoadDialogueData(Table);
		}		
	}
}

The editor crashes on startup with this error message at the first line of LoadDialogueData:
8b9818efd3df7623b871f0beb8196c35cecc987b.png

I’m stumped by this. I’ve tried using a direct UDataTable* pointer for my blueprint-exposed variable, hardcoding and dynamically loading, with AssetLongPathname instead of ToString() and convoluted asset loading with StaticLoadObject.

Any help will be greatly appreciated. This is a major (if not the most major feature) in our game, and I’m personally going figuratively insane from what would seem like a small problem :stuck_out_tongue:

For the TAssetPtr case, TAssetPtr doesn’t automatically load its asset on demand. That is left to the game code so you can decide how you want to load the asset. (i.e.: through a streaming system of your own, in bulk for certain objects, directly through StaticLoadObject, etc.).

Next up,



static ConstructorHelpers::FObjectFinder<UDataTable> DataTable_BP(*DataTable.ToStringReference().ToString());


This is guaranteed to fail. FObjectFinders are static for a reason – the asset reference is stored as a compile time constant (and added to the CDO for asset reference purposes). Since your DataTable asset ptr is uninitialized at this point, FObjectFinder will have an empty string reference and not load anything. FObjectFinder has a very rigid use case: If you have an asset member property you want to initialize natively, instead of leaving it up to blueprint to set. This is not the case anywhere here.

In all cases, native classes’s CDO are initialized early in loading module code by running the constructor. At this point, not everything is guaranteed to be ready for use or for loading. Namely, it seems like the class/interface hierarchy isn’t ready for runtime type checks at that time. Make sure this sort of thing is not run by the CDO by adding extra checks for template objects:


if( !HasAnyFlags( RF_ClassDefaultObject|RF_ArchetypeObject ) )
{
// do loading stuff here
}

But if you’re using TAssetPtr, then you’re saying you want to load this asset on demand. Then you shouldn’t even be putting this stuff in the constructor, but rather loading it whenever you need it. If you want it to be loaded alongside the actor, use a hard UDataTable pointer instead and let the engine’s loader take care of it.

Lastly, why are you interface casting this? If you’re referring to your own class, you already know it implements that interface. Just nix it, interfaces are meant to call specific methods on an object from the outside.

-Camille

Hi Camille,

Thanks a lot for your help :slight_smile:
I’m still fairly green with C++ and as such, I’m not all too clever with what to do and where to do it.
I’ve changed the TAssetPtr to a hard pointer instead



UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue", meta = (DisplayName = "Dialogue Tree Asset-Ptr"))
		UDataTable* DataTable;


and got rid of the interface casting. However, DataTable (when I set it to an imported asset in my actor’s defaults) is always null. Must I load it somewhere first, or have I misunderstood something?

That’s not really C++ as much as UE4-specific details.

A hard pointer reference should ensure the asset is loaded automatically, yes. But it won’t actually be loaded in the constructor, that’s much too early. If you have any processing to do that depends on subobjects, try doing it in PostInitializeComponents. As it stands, I’m not sure what LoadDialogue is supposed to do beyond call itself infinitely. :slight_smile:

The data table should be loaded with the actor’s init process, you should be able to just use FindRow, etc. on it past that point.

It all works like a charm now. Thank you so much for your help!

I have this build error , what this happen! like this wiki page: