Question about actor component dependencies and notifications

I work with runtime added components very often. I run into situations where component A is dependent on component B, however, component B is not available when component A executes begin play. This creates a situation where I need to resolve dependency at runtime. I would really like to see events at the actor level that notify Subscribers when components are added or removed. In the absence of these type of events, does anyone know how a component might be notified when a sibling component has been added or removed from the owning actor at runtime?

The simplest thing is to wrap the component creation/deletion functions with their own function, which also calls a delegate (located in the game instance or subsystem), notifying everyone who is subscribed.

I’m not 100% sure, but I think the engine had a notification about the creation of ANY object, but you’d be filtering out 99% of the triggers in that case. So, using your own wrapper is the best option, in my opinion.

That’s always an issue with BeginPlay, you don’t know when it fires. It’d be better to create you own initialisation function and call it on all components when the parent actor begins play since that happens post components or call it manually.

Thanks for the responses. I may need to create a wrapper or some kind of component tracker component to get the functionality I need.

Here is how I fixed the problem. Admittedly, it’s a janky approach but I wanted something decoupled and runtime-safe. This component will notify any subscribers of changes to it’s owner’s components. It should not be used too much for performance. Hope this helps anyone who has the same problem.

ComponentTrackerComponent.h

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ComponentTrackerComponent.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
	FOnComponentAdded,
	AActor*,
	Source,
	UActorComponent*,
	AddedComponent
);

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
	FOnComponentRemoved,
	AActor*,
	Source,
	UActorComponent*,
	RemovedComponent
);

UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class YOUR_API UComponentTrackerComponent : public UActorComponent
{
	GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, Category="Component Tracker")
	bool bUseQuickCheck = true;

    UPROPERTY(EditAnywhere, Category="Component Tracker")
	bool bUseLongCheck = true;

	UPROPERTY(BlueprintReadOnly, Category="Events")
	FOnComponentAdded OnComponentAdded;

	UPROPERTY(BlueprintReadOnly, Category="Events")
	FOnComponentRemoved OnComponentRemoved;
	
private:
	UPROPERTY(EditAnywhere, Category="Component Tracker", meta=(ClampMin="0.0"))
	float QuickCheckDelay = 0.f;

    UPROPERTY(EditAnywhere, Category="Component Tracker", meta=(ClampMin="0.0"))
	float LongCheckDelay = 0.f;

	UPROPERTY()
	float TimeSinceQuickCheck = 0;

	UPROPERTY()	
	float TimeSinceLongCheck = 0;

	UPROPERTY()
	int32 LastQuickCheckCount = -1;

	UPROPERTY()
	TSet<UActorComponent*> LastComponentCache;
	
public:
	UComponentTrackerComponent();

	virtual void TickComponent(
		float DeltaTime,
		ELevelTick TickType,
		FActorComponentTickFunction* ThisTickFunction
	) override;

	float GetQuickCheckDelay();

	void SetQuickCheckDelay(float NewQuickCheckDelay);

	float GetLongCheckDelay();

	void SetLongCheckDelay(float NewLongCheckDelay);
	
private:
	bool PerformQuickCheck();

	void PerformLongCheck();

	void RaiseOnComponentAdded(UActorComponent* AddedComponent);

	void RaiseOnComponentRemoved(UActorComponent* RemovedComponent);
	
protected:
	virtual void BeginPlay() override;
};

ComponentTrackerComponent.cpp

#include "ComponentTrackerComponent.h"

UComponentTrackerComponent::UComponentTrackerComponent()
{
	PrimaryComponentTick.bCanEverTick = true;
}

void UComponentTrackerComponent::BeginPlay()
{
	Super::BeginPlay();
	LastComponentCache = GetOwner()->GetComponents();
	LastQuickCheckCount = GetOwner()->GetComponents().Num();
}

void UComponentTrackerComponent::TickComponent(
	float DeltaTime,
	ELevelTick TickType,
	FActorComponentTickFunction* ThisTickFunction
)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	if (bUseQuickCheck)
	{
		TimeSinceQuickCheck += DeltaTime;
		if (TimeSinceQuickCheck >= QuickCheckDelay)
		{
			if (PerformQuickCheck())
			{
				PerformLongCheck();
			}
		}
	}
	
	if (bUseLongCheck)
	{
		TimeSinceLongCheck += DeltaTime;
		if (TimeSinceLongCheck >= LongCheckDelay)
		{
			PerformLongCheck();
		}
	}
}

float UComponentTrackerComponent::GetQuickCheckDelay()
{
	return QuickCheckDelay;
}

void UComponentTrackerComponent::SetQuickCheckDelay(float NewQuickCheckDelay)
{
	QuickCheckDelay = FMath::Max(0, NewQuickCheckDelay);
}

float UComponentTrackerComponent::GetLongCheckDelay()
{
	return LongCheckDelay;
}

void UComponentTrackerComponent::SetLongCheckDelay(float NewLongCheckDelay)
{
	LongCheckDelay = FMath::Max(0, NewLongCheckDelay);
}

bool UComponentTrackerComponent::PerformQuickCheck()
{
	if (LastQuickCheckCount == -1)
	{
		LastQuickCheckCount = GetOwner()->GetComponents().Num();
		return false;
	}

	int32 curCount = GetOwner()->GetComponents().Num();
	bool changed = curCount != LastQuickCheckCount;
	LastQuickCheckCount = curCount;
	
	TimeSinceQuickCheck = 0;
	return changed;
}

void UComponentTrackerComponent::PerformLongCheck()
{
	if (LastComponentCache.Num() == 0)
	{
		LastComponentCache = GetOwner()->GetComponents();
		return;
	}
	
	const TSet<UActorComponent*>& currentComponents = GetOwner()->GetComponents();
	TSet<UActorComponent*> added = currentComponents.Difference(LastComponentCache);
	TSet<UActorComponent*> removed = LastComponentCache.Difference(currentComponents);

	for (UActorComponent* comp : added)
	{
		RaiseOnComponentAdded(comp);
	}

	for (UActorComponent* comp : removed)
	{
		RaiseOnComponentRemoved(comp);
	}

	LastComponentCache = currentComponents;
	
	TimeSinceLongCheck = 0;
}

void UComponentTrackerComponent::RaiseOnComponentAdded(UActorComponent* AddedComponent)
{
	if (!AddedComponent || !GetOwner())
		return;
	OnComponentAdded.Broadcast(GetOwner(), AddedComponent);
}

void UComponentTrackerComponent::RaiseOnComponentRemoved(UActorComponent* RemovedComponent)
{
	if (!RemovedComponent || !GetOwner())
		return;
	OnComponentRemoved.Broadcast(GetOwner(), RemovedComponent);
}