How do I get the MassStationaryVisualizationTrait to work?

For visual entities that should be stationary (e.g., not expected to move), it seemed logical to apply the MassStationaryVisualizationTrait using the MassRepresentation system. However, trying every trick in the book, I could not get it to work. The entity is spawning with the correct static mesh (e.g., also the StaticMeshIndexDesc to the RepresentationFragment) but it is not displayed in the world.

I enabled the StationaryISMSwitcher processor in the project settings. I also don’t know exactly what this processor does. You would expect the stationary to have to do less than the Movable, no? It worked when changing the code to use the MassMovableVisualizationTrait.

You can see me in this live coding session to get an idea of what I tried.

Any insight would be appreciated.

1 Like

Hey,

I just spent three days on this and got to the root of the problem and I’m able to get it working by just adding an extra processor:


#pragma once

#include "MassProcessor.h"
#include "MassStationaryProcessor.generated.h"

/**
 * Processor that ensures stationary ISM entities are maintained in the ISM system
 * by marking them as processed each frame to prevent auto-removal
 */
UCLASS()
class UMassStationaryProcessor : public UMassProcessor
{
    GENERATED_BODY()

public:
    UMassStationaryProcessor();

protected:
    virtual void ConfigureQueries() override;
    virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;

    FMassEntityQuery EntityQuery;
};

//////////////////////////////////////////

#include "MassStationaryProcessor.h"
#include "MassEntityView.h"
#include "MassRepresentationFragments.h"
#include "MassCommonFragments.h"
#include "MassCommonTypes.h"
#include "MassStationaryISMSwitcherProcessor.h"
#include "MassExecutionContext.h"
#include "MassRepresentationSubsystem.h"

UMassStationaryProcessor::UMassStationaryProcessor()
    : EntityQuery(*this)
{
    ExecutionOrder.ExecuteInGroup = UE::Mass::ProcessorGroupNames::Representation;

    // Make sure this processor runs after the ISM switcher processor
    ExecutionOrder.ExecuteAfter.Add(UMassStationaryISMSwitcherProcessor::StaticClass()->GetFName());

    bAutoRegisterWithProcessingPhases = true;
    ExecutionFlags = static_cast<int32>(EProcessorExecutionFlags::AllNetModes);
}

void UMassStationaryProcessor::ConfigureQueries()
{
    // Target entities with:
    // 1. Static representation tag (stationary entities)
    // 2. StaticMeshInstance representation
    // 3. Required components for visualization

    EntityQuery.AddTagRequirement<FMassStaticRepresentationTag>(EMassFragmentPresence::All);
    EntityQuery.AddTagRequirement<FMassStationaryISMSwitcherProcessorTag>(EMassFragmentPresence::All);

    EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadOnly);
    EntityQuery.AddRequirement<FMassRepresentationLODFragment>(EMassFragmentAccess::ReadOnly);
    EntityQuery.AddRequirement<FMassRepresentationFragment>(EMassFragmentAccess::ReadWrite);

    EntityQuery.AddSharedRequirement<FMassRepresentationSubsystemSharedFragment>(EMassFragmentAccess::ReadWrite);
}

void UMassStationaryProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
    EntityQuery.ForEachEntityChunk(EntityManager, Context, [](FMassExecutionContext& Context)
        {
            UMassRepresentationSubsystem* RepresentationSubsystem = Context.GetMutableSharedFragment<FMassRepresentationSubsystemSharedFragment>().RepresentationSubsystem;
            check(RepresentationSubsystem);

            FMassInstancedStaticMeshInfoArrayView ISMInfos = RepresentationSubsystem->GetMutableInstancedStaticMeshInfos();

            const TConstArrayView<FTransformFragment> TransformList = Context.GetFragmentView<FTransformFragment>();
            const TConstArrayView<FMassRepresentationLODFragment> RepresentationLODList = Context.GetFragmentView<FMassRepresentationLODFragment>();
            const TArrayView<FMassRepresentationFragment> RepresentationList = Context.GetMutableFragmentView<FMassRepresentationFragment>();

            const int32 NumEntities = Context.GetNumEntities();
            for (int32 EntityIdx = 0; EntityIdx < NumEntities; EntityIdx++)
            {
                const FMassEntityHandle EntityHandle = Context.GetEntity(EntityIdx);
                const FTransformFragment& TransformFragment = TransformList[EntityIdx];
                const FMassRepresentationLODFragment& RepresentationLOD = RepresentationLODList[EntityIdx];
                FMassRepresentationFragment& Representation = RepresentationList[EntityIdx];

                // Only process entities with StaticMeshInstance representation type
                if (Representation.CurrentRepresentation == EMassRepresentationType::StaticMeshInstance &&
                    Representation.StaticMeshDescHandle.IsValid())
                {
                    // The key part - add this entity to the update list to prevent it from being auto-removed
                    // We pass the same transform for both current and previous transform, since it's stationary
                    ISMInfos[Representation.StaticMeshDescHandle.ToIndex()].AddBatchedTransform(
                        EntityHandle,
                        TransformFragment.GetTransform(),
                        TransformFragment.GetTransform(),
                        RepresentationLOD.LODSignificance,
                        Representation.PrevLODSignificance
                    );

                    // Update the PrevLODSignificance to match current
                    Representation.PrevLODSignificance = RepresentationLOD.LODSignificance;
                }
            }
        });
}

I believe it’s due to this part of the code in UMassVisualizationComponent::EndVisualChanges - without some updates the instances are considered unprocessed and removed - I did notice when I simulated that I could see some meshes for a frame sometimes and there’s a comment just below “note that we’re not clearing the dirty flag on purpose - these components require constant updates” that made me suspicious.

TArray<FPrimitiveInstanceId> RemovedISMInstanceIds;
RemovedISMInstanceIds.Reserve(Unprocessed.Num());
{
    for(TConstSetBitIterator<> BitIt(Unprocessed); BitIt; ++BitIt)
    {
        Experimental::FHashElementId ElementId(BitIt.GetIndex());
        if (SharedIdMap.ContainsElementId(ElementId))
        {
            FPrimitiveInstanceId InstanceId = SharedIdMap.GetByElementId(ElementId).Value;
            check(InstanceId.IsValid());
            SharedIdMap.RemoveByElementId(ElementId);
            RemovedISMInstanceIds.Add(InstanceId);
        }
    }       
    ISMComponent->RemoveInstancesById(RemovedISMInstanceIds);
}

I will roll with this for the moment but I suspect I may overhaul this, it’s kind of a pointless system and should just be fixed within UMassVisualizationComponent.