I’ve been seeing some unexpected behavior recently when attempting to use FMassCommandAddFragmentInstances
to add the same type of fragment to multiple entities at once, where each instance of the fragment contains different data. When I do this, the entities get assigned the correct fragment types, but the fragment data gets scrambled between the different entities.
I would very much appreciate either:
A) Is anybody able to confirm that this is a bug (and ideally help me find a workaround until it’s fixed); or
B) Help me understand what I’m doing incorrectly with FMassCommandAddFragmentInstances
and what I should be doing instead.
Thank you!
The below snippet is a much simplified example of what I’m attempting to do:
struct FFooFragment : public FMassFragment
{
// For the sake of this example, this should always be the entity
// that has this fragment.
UPROPERTY()
FMassEntityHandle SelfEntity;
}
// First processor
for (FMassEntityHandle Entity : some set of entities)
{
FFooFragment Foo.
Foo.SelfEntity = Entity;
Context.Defer().PushCommand<FMassCommandAddFragmentInstances>(Entity, Foo);
}
// deferred command buffer gets flushed...
// Second processor
FooQuery.ForEachEntityChunk(...)
{
auto FooView = Context.GetFragmentView<FFoo>();
for (int EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
// This check fails!
check(FooView[EntityIndex].SelfEntity == Context.GetEntity(EntityIndex));
}
}
The behavior I see is that the check near the bottom fails! Even though I populated each Foo fragment with a reference to the entity to which the Foo fragment was to be added, when I fetch that data later the Foo associated with each entity is not the correct one.
This behavior manifests when the first processor operates on multiple non-contiguous entities in the same archetype. For example, say we have an archetype with four entities [ A, B, C, D ] and the first processor operates on A, C, and D. Because A, C, and D are not contiguous, the entity collection passed into these functions will be split into two ranges:
{
{ SubchunkStart = 0, Length = 1 } // [A]
{ SubchunkStart = 2, Length = 2 } // [C, D]
}
Since we’re adding a Foo component, BatchAddFragmentInstancesForEntities
needs to move those entities to another archetype before it can call BatchSetFragmentValues
. BatchMoveEntitiesToAnotherArchetype
wants to operate on these back-to-front, as described in the following snippet at MassArchetypeData.cpp line 946-952:
// Sorting the subchunks info so that subchunks of a given chunk are processed "from the back". Otherwise removing
// a subchunk from the front of the chunk would inevitably invalidate following subchunks' information.
TArray<FMassArchetypeEntityCollection::FArchetypeEntityRange> Subchunks(EntityCollection.GetRanges());
Subchunks.Sort([](const FMassArchetypeEntityCollection::FArchetypeEntityRange& A, const FMassArchetypeEntityCollection::FArchetypeEntityRange& B)
{
return A.ChunkIndex < B.ChunkIndex || (A.ChunkIndex == B.ChunkIndex && A.SubchunkStart > B.SubchunkStart);
});
So this sort will cause the range containing [C, D] to be handled before the one containing [A]. When handling each range, it calls PrepareNextEntitiesSpanInternal
on the new archetype. This adds those entities to the new archetype’s entity map in a front-to-back manner. This means that the order of those entities in the new archetype will be [C, D, A]. This isn’t a problem in and of itself, since order of entities within a chunk is arbitrary.
BUT… after returning from this function, it calls BatchSetFragmentValues
passing in EntityRangesWithPayload
. EntityRangesWithPayload
contains the fragment data in the order that the entities were present in the original archetype, and this data was not reordered when the entities were moved to a new archetype. That snippet at MassEntityManager.cpp line 1063-1066 looks like this:
// at this point all the entities are in the target archetype, we can set the values
// note that even though the "subchunk" information could have changed the order of entities is the same and
// corresponds to the order in FMassArchetypeEntityCollectionWithPayload's payload
TargetArchetypeHandle.DataPtr->BatchSetFragmentValues(TargetArchetypeEntityRanges, EntityRangesWithPayload.GetPayload());
That assumption seems incorrect to me. The order of entities is not necessarily the same; as shown above, the order of entities in the new archetype is [C, D, A] but the fragment data in EntityRangesWithPayload is still [A, C, D]. This means that when we go into that function, we copy the data such that we wind up with the following:
Entity FooFragment
C A
D C
A D
As far as I have been able to tell, this is what is causing my entities to have incorrect data associated with them in the example above. As far as I can tell, the incorrect logic is entirely inside BatchAddFragmentInstancesForEntities
, which makes me feel like it’s probably an engine bug. However if this were the case, I would have expected it to affect many more people than just me, so I don’t want to write off the possibility that the error is on my end. I’d appreciate any help understanding what the problem is and what I can do to address it.
Thanks in advance!