I have an Actor class (AWorldItem) that loads its StaticMesh dynamically. Rather than create large PostProcess volumes, I decided to go with smaller movable PostProcessComponents attached to the Items. Each item is wrapped (using CalcBounds) by a translucent StaticMesh volume with a PostProcess material that defines the bounds for a PostProcessComponent. I got it working fairly well, but I’ve noticed errors in the logs regarding RegisterComponent:
[2020.06.07-15.52.53:237][ 0]LogOutputDevice: Error: [Callstack] 0x00007ffae442c119 UE4Editor-Engine.dll!::operator()() [d:\build++ue4\sync\engine\source\runtime\engine\private\components\actorcomponent.cpp:1153]
[2020.06.07-15.52.53:237][ 0]LogOutputDevice: Error: [Callstack] 0x00007ffae2fce1d1 UE4Editor-Engine.dll!UActorComponent::RegisterComponent() [d:\build++ue4\sync\engine\source\runtime\engine\private\components\actorcomponent.cpp:1153]
[2020.06.07-15.52.53:237][ 0]LogOutputDevice: Error: [Callstack] 0x00007ffad0d3bf0f UE4Editor-MSR-0002.dll!AWorldItem::AWorldItem() [d:\unreal projects\msr\source\msr\private\worlditem.cpp:34]
AWorldItem.h:
UCLASS(Blueprintable)
class AWorldItem : public AActor
{
GENERATED_BODY()
AWorldItem(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
// The BaseItem data for this item, including the Mesh
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
UBaseItem* BaseItem;
// The instanced data for this Item
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
UItemInstance* ItemInstance;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* MeshComponent;
// This volume surrounds the Item with a PostProcess Material
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* OutlineVolume;
// Allows for localized Post Process effects
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
UPostProcessComponent* PostProcessor;
// The Item Outline Effect to use when looking at this Item
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
UMaterialInstance* OutlineMaterialInstance;
//AActor Interface
virtual void BeginPlay() override;
// Load the mesh from BaseItem DataAsset into memory, if invalid
UFUNCTION()
void LoadMesh();
// Called once the Static Mesh asset is loaded
UFUNCTION()
void MeshLoaded();
};
AWorldItem.cpp:
// Sets up the components, but most features are done on BeginPlay()
AWorldItem::AWorldItem(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Most item actions are called through Events and do not need to Tick
PrimaryActorTick.bCanEverTick = false;
// World-visible Items need replication
bReplicates = true;
// Create the Component, but don't load the Mesh just yet
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>("WorldItemMesh");
MeshComponent->SetupAttachment(RootComponent, "ItemSocket");
RootComponent = MeshComponent;
// Attach the Outline Volume, but don't initialize it until Mesh is loaded
OutlineVolume = CreateDefaultSubobject<UStaticMeshComponent>("OutlineVolume");
OutlineVolume->SetupAttachment(MeshComponent);
OutlineVolume->RegisterComponent(); // Required to modify components later
// Post Process bounds are defined by the OutlineVolume above
PostProcessor = CreateDefaultSubobject<UPostProcessComponent>("OutlinePostProcess");
PostProcessor->bUseAttachParentBound = true;
PostProcessor->SetupAttachment(OutlineVolume);
PostProcessor->RegisterComponent(); // Required to modify components later
}
void AWorldItem::BeginPlay()
{
Super::BeginPlay();
// BaseItem cannot be null. If it's null, delete the item.
if (BaseItem == nullptr)
{
GetWorld()->DestroyActor(this, true);
}
return;
}
// OutlineVolume is set in the Editor and should be valid
if (OutlineVolume == nullptr || OutlineVolume->GetStaticMesh() == nullptr)
{
return;
}
// No Item Instance here means this item was placed or spawned directly
if (!ItemInstance)
{
// Create new ItemInstance, pair it with this, and check for validity
ItemInstance = NewObject<UItemInstance>();
if (!ItemInstance || !ItemInstance->PairWorldItem(this))
{
GetWorld()->DestroyActor(this, true);
return;
}
ItemInstance->Init();
}
LoadMesh();
}
// If needed, Async load the BaseItem's mesh asset into memory
void AWorldItem::LoadMesh()
{
// If Null, Item's Mesh has not been loaded yet. Perform Async Load.
if (BaseItem->GetMesh() == nullptr)
{
FPrimaryAssetId ItemID = BaseItem->GetPrimaryAssetId();
if (ItemID.IsValid())
{
TArray<FName> ItemMesh = { "ItemMesh" };
GEngine->AssetManager->LoadPrimaryAsset(ItemID, ItemMesh, FStreamableDelegate::CreateUObject(this, &AWorldItem::MeshLoaded));
}
return;
}
else
{
MeshLoaded();
}
}
// Called after we're sure the Item's Mesh is ready for use
void AWorldItem::MeshLoaded()
{
// Mesh SHOULD be valid at this point
if (BaseItem->GetMesh() == nullptr)
{
return;
}
MeshComponent->SetStaticMesh(BaseItem->GetMesh());
// Enable stencil for the Mesh's outline
MeshComponent->SetRenderCustomDepth(true);
MeshComponent->SetCustomDepthStencilValue(1); // TODO: Update based on Item Instance features
// Needed for the Editor (won't render Mesh otherwise)
MeshComponent->RegisterComponent();
// NOTE: PIE crashes here if I change any of the Attach or RegisterComponent lines.
// Now that the Mesh is loaded, update the Outline Volume transform
FBoxSphereBounds MeshBounds = MeshComponent->CalcBounds(FTransform());
FBoxSphereBounds OutlineBounds = OutlineVolume->CalcBounds(FTransform());
// Center Outline Volume on the Mesh
OutlineVolume->SetRelativeLocation(FVector(MeshBounds.Origin.X, MeshBounds.Origin.Y, MeshBounds.Origin.Z));
// Outline Volume needs to be larger than the Mesh (thus x1.0625)
// All Divisors also need to be non-zero (thus add 1 to each)
OutlineVolume->SetRelativeScale3D(FVector(
MeshBounds.BoxExtent.X/(OutlineBounds.BoxExtent.X + 1),
MeshBounds.BoxExtent.Y/(OutlineBounds.BoxExtent.Y + 1),
MeshBounds.BoxExtent.Z/(OutlineBounds.BoxExtent.Z + 1))*1.0625);
// Should have been set in the Editor
if (OutlineMaterialInstance == nullptr)
{
return;
}
// Why do I need these calls again? Is it because the Constructor is not being called in PIE?
OutlineVolume->RegisterComponent();
PostProcessor->RegisterComponent();
}
It works, but I’m pretty sure I’m setting up the components wrong. What is the proper way to use RegisterComponent and SetupAttachment (or AttachToComponent)?