How do I show a component's details in the actor details?

Boomer64
Mon, 17 Feb 2025 07:10:14 +0000
C++

I’m encountering a challenging problem.

I am trying to create a designer friendly camera system. Currently, the goal is to create a system where a developer can just add a component to a camera that allows them to create custom camera sequences that trigger when the player enters specified volumes. In order to clearly illustrate the link between a sequence and a trigger every sequence has a colour that is automatically applied to every trigger associated with that sequence.

The problem is that when a component is added in a blueprint it does not show up in the actor details when in a level and when selecting a specific component on an actor every volume loses it’s colour and becomes grey so you can’t see the colour you are changing the triggers to. Here is what I want:

Here is what I’ve done so far:

Firstly, I have a struct called sequence data that I have contained inside an array on my component:

// The sequence data relating to 1 camera sequence a cine camera sequence does
USTRUCT(BlueprintType)
struct FSequenceData
{
public:
	GENERATED_BODY()

	FSequenceData()
	{
		SequenceColour = FColor::MakeRandomColor();
		BlendFunction = EViewTargetBlendFunction::VTBlend_Linear;
		bLockOutgoing = false;
	}

	static FSequenceData Empty;

	// The colour signifying the sequence. It will be applied to volumes
	UPROPERTY(EditAnywhere, meta = (DisplayPriority = 0))
	FColor SequenceColour = FColor();

	//UPROPERTY(EditAnywhere, meta = (DisplayPriority = 2))
	//TObjectPtr<UAkAudioEvent> SequenceAudioEvent = nullptr;

	// The array of trigger volumes that activate this sequence
	UPROPERTY(EditAnywhere, meta = (DisplayPriority = 3))
	TArray< TObjectPtr<AVolume>> TriggerVolumes;

	UPROPERTY(EditAnywhere, Category = "Camera Blend Options", meta = (DisplayPriority = 4))
	float BlendTime = 0.f;

	UPROPERTY(EditAnywhere, Category = "Camera Blend Options", meta = (DisplayPriority = 4))
	TEnumAsByte<EViewTargetBlendFunction> BlendFunction;

	UPROPERTY(EditAnywhere, Category = "Camera Blend Options", meta = (DisplayPriority = 4))
	float BlendExp = 0.f;

	UPROPERTY(EditAnywhere, Category = "Camera Blend Options", meta = (DisplayPriority = 4))
	uint8 bLockOutgoing : 1;

protected:

	UPROPERTY(VisibleAnywhere, meta = (DisplayPriority = 1))
	int32 SequenceID = -1;

	UPROPERTY()
	int32 SequenceAudioEventID = -1;

public:


	FORCEINLINE int32 GetSequenceID() const { return SequenceID; }

	FORCEINLINE void SetSequenceID(int32 InSequenceID) { SequenceID = InSequenceID; }
};

Inside the actor component’s protected section:

	// The array of sequences this camera has
	UPROPERTY(EditAnywhere)
	TArray<FSequenceData> Sequences;

Then, so I can respond to changes to the struct from the editor I create a struct inheriting from IPropertyTypeCustomization that simply goes through the properties inside of the sequence data struct and binds delegates to their property change and child property change events in order to update the volumes’ colour in real time:

.h file

#if WITH_EDITOR

#include "IPropertyTypeCustomization.h"
#include "Components/Cinematics/BaseSequenceComponent.h"

class AVolume;

class FSequenceDataCustomisation : public IPropertyTypeCustomization
{
public:
	// Sets default values for this actor's properties
	FSequenceDataCustomisation();
	~FSequenceDataCustomisation();

	static TSharedRef<IPropertyTypeCustomization> MakeInstance();

	/**
	 * Called when the header of the property (the row in the details panel where the property is shown)
	 * If nothing is added to the row, the header is not displayed
	 *
	 * @param PropertyHandle			Handle to the property being customized
	 * @param HeaderRow					A row that widgets can be added to
	 * @param StructCustomizationUtils	Utilities for customization
	 */
	virtual void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) override;

	/**
	 * Called when the children of the property should be customized or extra rows added
	 *
	 * @param PropertyHandle			Handle to the property being customized
	 * @param StructBuilder				A builder for adding children
	 * @param StructCustomizationUtils	Utilities for customization
	 */
	virtual void CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils) override;

	void UpdateTriggers(TSharedPtr<FColor> SequenceColour, TSharedPtr<TArray<AVolume*>> Triggers);

private:

	FSequenceData SequenceData;

	TArray<TSharedRef<FSimpleDelegate>> Delegates;
};

#endif // WITH_EDITOR

.cpp file:

#include "DetailsCustomisation/SequenceDataCustomisation.h"

#if WITH_EDITOR

#include "Editor/PropertyEditor/Public/IDetailChildrenBuilder.h"
#include "Editor/PropertyEditor/Public/PropertyHandle.h"
#include "Editor/PropertyEditor/Public/PropertyCustomizationHelpers.h"

FSequenceDataCustomisation::FSequenceDataCustomisation()
{
}

FSequenceDataCustomisation::~FSequenceDataCustomisation()
{
	for (TSharedRef<FSimpleDelegate> Delegate : Delegates)
	{
		Delegate.Get().Unbind();
	}

	Delegates.Empty();
}

TSharedRef<IPropertyTypeCustomization> FSequenceDataCustomisation::MakeInstance()
{
	return MakeShareable(new FSequenceDataCustomisation);
}

void FSequenceDataCustomisation::CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{

}

void FSequenceDataCustomisation::CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
	IPropertyHandle& PropertyHandleReference = PropertyHandle.Get();

	uint32 NumChildren = 0;
	PropertyHandle->GetNumChildren(NumChildren);

	TArray<TSharedRef<IPropertyHandle>> BindProperties;

	TSharedPtr<FColor> SequenceColourPtr = nullptr;
	TSharedPtr<TArray<AVolume*>> TriggerVolumesPtr = nullptr;
	for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex)
	{
		TSharedRef<IPropertyHandle> ChildPropertyHandle = PropertyHandle->GetChildHandle(ChildIndex).ToSharedRef();

		ChildBuilder.AddProperty(ChildPropertyHandle);

		FProperty* ChildProperty = ChildPropertyHandle.Get().GetProperty();

		FName ChildPropertyName = (ChildProperty != nullptr) ? ChildProperty->GetFName() : NAME_None;

		bool bIsColour = ChildPropertyName == GET_MEMBER_NAME_CHECKED(FSequenceData, SequenceColour);
		bool bIsVolumes = ChildPropertyName == GET_MEMBER_NAME_CHECKED(FSequenceData, TriggerVolumes);
		if (bIsColour || bIsVolumes)
		{
			BindProperties.Add(ChildPropertyHandle);

			void* Data = nullptr;
			PropertyHandle.Get().GetValueData(Data);

			if (bIsColour)
			{
				SequenceColourPtr = MakeShareable<FColor>(ChildProperty->ContainerPtrToValuePtr<FColor>(Data));

			}

			if (bIsVolumes)
			{
				TriggerVolumesPtr = MakeShareable<TArray<AVolume*>>(ChildProperty->ContainerPtrToValuePtr<TArray<AVolume*>>(Data));
			}
		}
	}

	for (TSharedRef<IPropertyHandle> BindProperty : BindProperties)
	{
		TSharedRef<FSimpleDelegate> Delegate = MakeShared<FSimpleDelegate>();
		Delegate.Get().BindSP(this, &FSequenceDataCustomisation::UpdateTriggers, SequenceColourPtr, TriggerVolumesPtr);
		BindProperty.Get().SetOnPropertyValueChanged(Delegate.Get());
		BindProperty.Get().SetOnChildPropertyValueChanged(Delegate.Get());

		Delegates.Add(Delegate);
	}

	//PropertyHandle.Get();
}

void FSequenceDataCustomisation::UpdateTriggers(TSharedPtr<FColor, ESPMode::ThreadSafe> SequenceColour, TSharedPtr<TArray<AVolume*>, ESPMode::ThreadSafe> Triggers)
{
	if (!SequenceColour.IsValid() || !Triggers.IsValid()) return;

	// Loop through the trigger volumes
	for (AVolume* TriggerVolume : *Triggers)
	{
		// Check if the trigger volume is not valid
		if (!IsValid(TriggerVolume)) continue;

		// Set the trigger volume brush colour to the one for the sequence
		TriggerVolume->BrushColor = *SequenceColour;

		// Mark render state dirty so the colour of the volume changes immediately
		TriggerVolume->MarkComponentsRenderStateDirty();
	}
}

#endif

Afterwards, I override the OnRegister function inside of my actor component to grab the outer object’s class then use that to find the actor component as a FProperty where I change the property flags in an attempt to get it to display like I want:

void UBaseSequenceComponent::OnRegister()
{
	Super::OnRegister();

	if (UObject* OuterObject = GetOuter(); UClass * OuterClass = OuterObject->GetClass())
	{
		UE_LOG(LogTemp, Warning, TEXT("The FName is: %s"), *GetFName().ToString());

		FString NameAsString = GetFName().ToString();

		NameAsString.RemoveSpacesInline();

		FName SpacelessFName = FName(*NameAsString);

		FProperty* Property = OuterClass->FindPropertyByName(SpacelessFName);

		if (Property != nullptr)
		{
			UE_LOG(LogTemp, Warning, TEXT("Here we are!"));
			Property->SetPropertyFlags(
				EPropertyFlags::CPF_BlueprintVisible |
				EPropertyFlags::CPF_ExportObject |
				EPropertyFlags::CPF_Edit |
				EPropertyFlags::CPF_EditConst |
				EPropertyFlags::CPF_ExposeOnSpawn);
		}
	}
}

However that simply results in this:

I know it has something to do with the blueprint add component execution path because regardless of whether it is a blueprint or C++ component it ends up looking like this. I know there’s something the create default subobject path does that I need to replicate, I just don’t know what.

Any help would be extremely appreciated.

5 posts - 2 participants