Asynchronous Loading of Primary Data Assets gets different result in C++

Here is what I’m trying to do:

I have a squad of 4 units (AUnit) in my Level. One of them is defined as the Leader. For each squad I want my reference to the Leader be saved as a soft reference TSoftObjectPtr and the references to the other Members as a TArray of TSoftObjectPtr. I store them in a custom primary data asset class. I’ve made a Data Asset_BP, which derives from my data asset class. I have assigned all the references to my objects in the level and saved them.

The squad units also have a soft reference to the Data Asset BP, which holds all information about the squad.

In Blueprints I have no problem loading the references to my objects in the level. Since it’s asynchronous, I’ve set a delay before trying to access the data. I can compare the reference I get from my Data Asset with the reference to my own Unit Actor.

I tried doing the same thing in C++ with the StreamableManager and loaded up all the data with RequestAsyncLoad() using FStreamableDelegate and assigning a Callback to it. The Data seemed to have loaded correctly, but the pointer to my AUnit does not seem to be the exact object from my level, but rather a copy of all the default properties my AUnit class has to offer. This is a huge problem for me, since I cannot rely on the references anymore I get back from my Data Asset, but have to additionally go through all the AUnit objects in my level and compare their names with the object references I get back from my Data Asset in order to figure out if this is a unit member from my squad.

Has anyone encountered a similar problem or is there a better way to solve this?

I think I know what my issue is. Calling Get() on my SoftObjectPtr Reference gives back a const ref of referred object. Since const ref is not the same as using this pointer from within my actor class, my code thinks that this is not the same object in the level.

const ref cannot be compared to this pointer, since this pointers aren’t const!

that still does not resolve my issue. The object I’m accessing now is from the Level Editor, not the actual object instantiated in the game. When writing changes to it, the object from the Level Editor has changed. I can see the changes when I exit Game Mode and look at the Outliner.

In other words, your soft reference is wrong.

Are you sure you are passing the reference the correct way?
A code snippet of this area would probably show the issue/allow someone to tell you how to fix it.

Regadless if what you assigned is not what you want, the problem has to be the assignment process.
Be it as you imagine, an issue with the loading priority, or as i imagine, a simple missing & or similar.

I assign the references just the same way, as I did in my Blueprint only project - via drag and drop.

unfortunately due to security reasons I cannot seem to upload images…

but I simply assign my Units as SoftObjects to my DataAsset. Then after loading them I do get the references from the Editor. But I don’t want the editor objects, I want my initialized object from the game. Do I need to search through my actor iterator and compare names additionally?

here’s how I’m loading the data currently:

FSoftObjectPath Datapath = PlatoonData.ToSoftObjectPath();

StreamableManager.RequestAsyncLoad(
	Datapath, [&]()
	{
		PlatoonDataInternal = PlatoonData.Get();

		TArray<FSoftObjectPath> MembersToStream;
		MembersToStream.AddUnique(PlatoonDataInternal->Leader.ToSoftObjectPath());

		for (int32 i = 0; i < PlatoonDataInternal->Members.Num(); ++i)
		{
			MembersToStream.AddUnique(PlatoonDataInternal->Members[i].ToSoftObjectPath());
		}

		StreamableManager.RequestAsyncLoad(MembersToStream, FStreamableDelegate::CreateUObject(this, &ACGF::AssignComponents));
		
	});

void ACGF::AssignComponents()
{
// Get Leader Information and create Components
if (PlatoonDataInternal->Leader.Get()->GetName().Equals(this->GetName()))
{
bIsLeader = true;

	auto comp = NewObject<ULeaderPlatoonLogic>(this, ULeaderPlatoonLogic::StaticClass(), FName("LeaderPlatoonLogicComponent"));
	comp->RegisterComponent();
	PlatoonLogicComponent = comp;
}
else
{
	bIsLeader = false;

	auto comp = NewObject<UUnitPlatoonLogic>(this, UUnitPlatoonLogic::StaticClass(), FName("UnitPlatoonLogicComponent"));
	comp->RegisterComponent();
	// TODO: fix actual leader ref with asset manager?
	comp->SetLeader(PlatoonDataInternal->Leader.Get());
	PlatoonLogicComponent = comp;
}

for (int32 i = 0; i < PlatoonDataInternal->Members.Num(); ++i)
{
	if (PlatoonDataInternal->Members[i].Get()->GetName().Equals(this->GetName()))
	{
		MemberIndex = i;
	}

	PlatoonDataInternal->Members[i].Get()->FillMembersArray(OtherMembers);
}

}

Completely out the right field…
But what if you were to re-work the whole thing to just use Gamplay Tags?

I think you can scrap the whole reference idea alltogeter.

You assign a gameplay tag to the leader.
Then fetch all actors with that gameplay tag.

The references are done for you by the getallbytag function and resolve to the real entities @ gameplay…

As far as the code goes.
Scrap the loop. Change it to a forach.
Yhis shpuld be fed to you by the class itself, probably as private. Somethig like

private List<AActor>? Members

Which is managed/updated on the construction event to fill up.

Noting inherintly wrong with for loops, but the engine may have issues when you get upward of 1000 loop iterations. It never will with a foreach.

Anyway, most of the stuff is dependent on your class.
Ans how you defined the Leader->Get() is probably the matter.

if (PlatoonDataInternal->Leader.Get()->GetName().Equals(this->GetName()))

Start outputting to the log the values of that at each step… so you can compare it in the log…

1 Like

the idea of using a tag for the leader isn’t bad, if it was just one platoon squad on the map. The goal is to be able to have multiple platoon squads on the map, where the user can define anyone as the leader before (and later on also while simulation runs). I think using Data Assets is a good way to easily configure the platoons.

I did want to use a for each loop, but my visual studio didn’t want to compile. (my pc at work acting up every now and then) I’ll try to fix that as well.

I’m currently thinking of making a Subsystem, which will load all the data from all platoons and then feed the members with the necessary data. But I think I’ll do that tomorrow. (Been reading through docs all day most of the time). I’ll let you know how it goes.

Thank you so much for helping out. I Hope you’ll have a great time!

1 Like

You can have multiple gameplay tags assigned.
So flag it as platoon leader, then look up other tags in it that define whatever else you may need (such as the platoon number)…

1 Like

Thank you for the suggestion, but it’d like to mendle with game tags as less as possible. They can easily get out of hand. I’ll just keep playing around with the different ways of loading up things and keep you updated upon the results here.

1 Like

@MostHost_LA Ok, I got a working solution for now.

In my Subsystem, which inherits from GameInstanceSubsystem, I’ve added a Timer with a small delay and a Callback, to ensure that all Instances have been initialized in the game, before I’m trying to access them.

Subsystem.cpp:

void UNamelessSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);

GetWorld()->GetTimerManager().SetTimer(MyTimerHandle, this, &UNamelessSubsystem::StartGettingReferences, 0.3f, false);

UE_LOG(LogTemp, Log, TEXT("Hello, I'm the nameless Subsystem"));

}

then in my callback I’m getting all hard references to the objects within my game world and let the Asset Manager get the references to my PlatoonDataAssets from the directory. Then again I’m using the Streamable Manager with async loading, (which worked pretty well last time, so I’m doing it again).

void UNamelessSubsystem::StartGettingReferences()
{
// iterate through cgf actors and store elements in an array
for (TActorIterator ActorItr((GetWorld())); ActorItr; ++ActorItr)
{
CGFArray.Add(*ActorItr);
}

UAssetManager* Manager = UAssetManager::GetIfValid();

if (Manager)
{
	FPrimaryAssetType PlatoonDataType = FPrimaryAssetType(FName("PlatoonInfo"));

	TArray<FPrimaryAssetId> PlatoonIdList;
	Manager->GetPrimaryAssetIdList(PlatoonDataType, PlatoonIdList);

	for (const FPrimaryAssetId& PlatoonID : PlatoonIdList)
	{
		FAssetData AssetDataToParse;
		Manager->GetPrimaryAssetData(PlatoonID, AssetDataToParse);

		if (AssetDataToParse.IsValid())
		{
			auto DataAsset = AssetDataToParse.GetAsset();

			// load data asynchronously
			UPlatoonInfo* platoonInfo = Cast<UPlatoonInfo>(DataAsset);
			PlatoonDataInternal.Push(platoonInfo);

			TArray<FSoftObjectPath> MembersToStream;
			MembersToStream.AddUnique(platoonInfo->Leader.ToSoftObjectPath());

			for (int32 i = 0; i < platoonInfo->Members.Num(); ++i)
			{
				MembersToStream.AddUnique(platoonInfo->Members[i].ToSoftObjectPath());
			}

			StreamableManager.RequestAsyncLoad(MembersToStream, FStreamableDelegate::CreateUObject(this, &UNamelessSubsystem::OnReferencesLoaded));
		}
	}
}

}

Once all soft references have been resolved, the callback from the streamable manager will handle the rest.

void UNamelessSubsystem::OnReferencesLoaded()
{
// assign necessary data to each platoon
// Get Leader Information and create Components
ACGF* LeaderUnit = nullptr;
TArray<ACGF*> PlatoonMembers;
UPlatoonInfo* platoonInfo = PlatoonDataInternal.Pop();

// determine leader
for (auto unit : CGFArray)
{
	if (platoonInfo->Leader.Get()->GetName().Equals(unit->GetName()))
	{
		LeaderUnit = unit;
	}

}

// go through object array and set information
for (auto unit : CGFArray)
{
	for (int32 i = 0; i < platoonInfo->Members.Num(); ++i)
	{
		if (platoonInfo->Members[i].Get()->GetName().Equals(unit->GetName()))
		{
			int MemberIndex = i;
			PlatoonMembers.Add(unit);
			unit->SetPlatoonData(MemberIndex, LeaderUnit, platoonInfo);
		}
	}
}

LeaderUnit->OtherMembers = PlatoonMembers;

}

What I don’t like about this yet is that I have to loop through my cgf object array once to figure out the leader and once again to tell everyone else about who the leader is. Also I could remove already resolved cgf units from the object array, to make the loops for other platoons shorter.

I’m not 100% happy with this, but it works for now. Isn’t there a way to get my hard object references without having to compare names?? Maybe it’s best to just spawn the actors on the map and create a hard reference.

platoonInfo->Members[i]

For Each that instead of loop.

Foreach(AACtor pm in platoonInfo->Members) {

should do the trick.

MemberIndex++;

inside with a definition for it to 0 before the for each.

then instead of

platoonInfo->Members[i].Get()

you have
pm.Get()

What I don’t like about this yet is that I have to loop through my cgf object array once to figure out the leader and once again to tell everyone else about who the leader is

Technically that’s always the better option. Also you can’t solve their reference until that has happened anyway, so it should already be happening / why you need the delay.

You can probably change HOW these are spawned in, and assign the variables correctly during their OnBeginPlay event.

1 Like

Technically that’s always the better option. Also you can’t solve their reference until that has happened anyway, so it should already be happening / why you need the delay.

I just noticed that my subsytem is being initialized before my unit instances, so I’ve added the delay to ensure that they will be initialized.

MemberIndex++;

great tip! For this it’s better to loop through the members of my platoon asset data first to ensure that the index order will be correct.

The final code therefore looks like this now:

int MemberIndex = 0;

// compare member list with object array and set information
for (auto member : platoonInfo->Members)
{
	for (auto unit : CGFArray)
	{
		if (member.Get()->GetName().Equals(unit->GetName()))
		{
			unit->SetPlatoonData(MemberIndex, LeaderUnit, platoonInfo);
			MemberIndex++;

			if (unit != LeaderUnit)
			{
				PlatoonMembers.Add(unit);
			}
		}
	}
}

LeaderUnit->OtherMembers = PlatoonMembers;

I left out the leader from my OtherMembers array, which makes sorting out easier for me later.

If I had to do this again, I would definitely make sure to save the world position of my unit actors in my data asset as well and spawn them where the user has placed them on the map, but that’s for another patch xD

1 Like

Only thing is this is a loop within a loop, which vould not be optimal if it isnt required.

You loop the members, then for each member you loop the CGFArray to find if one entry matches.

Instead of doing that with a loop, you can use the array Find or any other similar operation to directly select the correct entry without having to compare things manually one by one.

This ofc is assuming that the type of the 2 things in the array is identical.
In other words:
AppleArray.find(Pear) will always be false.
But
AppleArray.find(GrannySmith) could be correct.

I think you should give it a go.
If you arent sure about types, just print string GetType() of the memeber, and each cfg during the loop to see if they match up.

If they do, you can save a lot of overhead.

If you have 20 members, and each member had to loop 20cgf+whatever else is in the scene, you can quickly reach situations where you get CPU hangups.

1 Like

yeah, I get where you’re coming from.

Since this is just a prototype I probably wouldn’t leave it like that, but just to ensure a little bit of performance here is my optimization:

int MemberIndex = 0;

// compare member list with object array and set information
for (auto member : platoonInfo->Members)
{
	const FString unitName = member.Get()->GetName();

	ACGF* foundItem = *CGFArray.FindByPredicate([&unitName](const ACGF* item) { return unitName.Equals(item->GetName()); });

	if (!foundItem)
	{
		UE_LOG(LogTemp, Error, TEXT("Unit member could not be found!"));
		continue;
	}

	foundItem->SetPlatoonData(MemberIndex, LeaderUnit, platoonInfo);
	Members.Add(foundItem);

	MemberIndex++;
}

LeaderUnit->PlatoonMembers = Members;

Decided to add the Leader back in into the Array, as I would end up rewriting more stuff :')

I’m pretty happy with how it looks like right now. Also Lambdas are still confusing. Could really use a Lambda Bootcamp xD

1 Like