Add Dynamic not working for On Component Hit for a component that is created as a default subobject

I’m trying to create a projectile parent class for my game that can then be repurposed with different data assets to create different behavior patterns. Currently, the object is composed of a Capsule Component and a child Static Mesh Component (along with some other extra things like a projectile movement component and a custom damage handler).

I’m trying to bind On Component Hit for the Capsule Component using Add Dynamic, but either the hit is not being registered, or the event is not being bound. Initially, I was trying to bind a function in a different actor to the On Component Hit for the projectile’s capsule, and so I figured that that was the issue, but I tested doing this same thing with a function that’s native to the projectile class and the logic still doesn’t fire.

Here are the header and cpp files for the projectile:

#pragma once

#include "Components/CapsuleComponent.h"
#include "CoreMinimal.h"
#include "DamageOutbound.h"
#include "GameFramework/Actor.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Projectile.generated.h"

UCLASS(BlueprintType)
class PROJECTSPIRE_API UProjectileData : public UDataAsset
{
	//Information on the projectile to be used
};

UCLASS()
class PROJECTSPIRE_API AProjectile : public AActor
{
	GENERATED_BODY()
	
	public:	
		// Sets default values for this actor's properties
		AProjectile();
		~AProjectile();

	protected:
		// Called when the game starts or when spawned
		virtual void BeginPlay() override;

	public:	
		// Called every frame
		virtual void Tick(float DeltaTime) override;

		//initialization
		bool InitializeWithDataAsset(UProjectileData* dataAsset, FVector fireDirection, AActor* inHandler);

		//collision
		void OnCanisterHit(UPrimitiveComponent* hitComp, AActor* otherActor, UPrimitiveComponent* otherComp, FVector normalImpulse, const FHitResult& hit);

	private:
		AActor* handler;

		//projectile components to add by default
		UCapsuleComponent* capsuleComponent;
		UDamageOutbound* damageOut;
		UProjectileMovementComponent* projectileMovement;
		UStaticMeshComponent* staticMesh;
};
#include "Projectile.h"

// Sets default values
AProjectile::AProjectile()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	//spawn in and set up hierarchy of components
	capsuleComponent = CreateDefaultSubobject<UCapsuleComponent>("Capsule", true);
	SetRootComponent(capsuleComponent);
	capsuleComponent->SetCollisionResponseToAllChannels(ECR_Block);
	capsuleComponent->SetCollisionObjectType(ECC_WorldDynamic);
	capsuleComponent->SetGenerateOverlapEvents(false);
	staticMesh = CreateDefaultSubobject<UStaticMeshComponent>("Mesh", true);
	staticMesh->SetupAttachment(capsuleComponent);
	staticMesh->SetCollisionResponseToAllChannels(ECR_Ignore);

	//add in additional components
	damageOut = CreateDefaultSubobject<UDamageOutbound>("Damage Out", true);
	projectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>("Projectile", true);
	projectileMovement->Velocity = FVector(0,0,0);
	projectileMovement->ProjectileGravityScale = 0;
}

AProjectile::~AProjectile()
{
	//...
}

// Called when the game starts or when spawned
void AProjectile::BeginPlay()
{
	Super::BeginPlay();
	
	capsuleComponent->OnComponentHit.AddDynamic(this, &AProjectile::OnCanisterHit);
}

// Called every frame
void AProjectile::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

//Pre: data to use in initializing this projectile
//Post: whether or not it could be initialized
//Purpose: setup a projectile to be used in the world with a data asset
bool AProjectile::InitializeWithDataAsset(UProjectileData* dataAsset, FVector fireDirection, AActor* inHandler)
{
	//...
}

void AProjectile::OnCanisterHit(UPrimitiveComponent* hitComp, AActor* otherActor, UPrimitiveComponent* otherComp, FVector normalImpulse, const FHitResult& hit)
{
	GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Green, TEXT("HIT!"));
}

For testing purposes, I’m just binding this event in the AProjectile class in BeginPlay for the moment. The projectile collides with objects in the way that’s expected (i.e. it’s blocked by all other objects). However, hitting any other objects does not produce the “HIT!” text. Any help would be greatly appreciated, thanks in advance!

You need to mark your pointer variables in the .h with UPROPERY. Otherwise garbage collector just destroys them.

Tried making all the pointers for components UPROPERTY but still no luck.
For reference:

UPROPERTY(Instanced, VisibleDefaultsOnly, Category = "COMPONENTS")
UCapsuleComponent* capsuleComponent;

Also tried making the pointer that references the projectile in the script which creates it a UPROPERTY in the class, as opposed to a pointer that exists only in the function which creates the projectile.

Here’s the code responsible for spawning the projectile:
.h

UPROPERTY(BlueprintReadOnly, Category = "RUNTIME")
AProjectile* projectileReference;

.cpp

void AToolActor::SpawnProjectile()
{
	//spawn in the projectile
	FTransform transform;
	FActorSpawnParameters spawnParams;
	spawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

	projectileReference = Cast<AProjectile>(GetWorld()->SpawnActor(AProjectile::StaticClass(), &transform, spawnParams));

	projectileReference->SetActorLocation(GetActorLocation() + (facingDirection * toolParts.toolMedium->hitboxRadius * 2));

	projectileReference->InitializeWithDataAsset(toolParts.toolMedium->projectileData, facingDirection, this);

	//bind canister logic
	projectileReference->GetComponentByClass<UCapsuleComponent>()->OnComponentHit.AddDynamic(this, &AToolActor::OnCanisterHit);
}

Okay, everything is finally working! Seems like there were two major issues with my code which prevented the events from firing properly on collision:

  1. As STRiFE pointed out, any components added via c++ seemingly must be flagged as UPROPERTY or they will be garbage collected by Unreal and thus will not be able to be used for anything (collisions, in this case)
  2. Furthermore, it seems that any function that you want to bind via AddDynamic() must be flagged as a UFUNCTION. Not sure as to why this is the case, but adding this before all of the functions I wanted to bind fixed the issues. No specifiers are required either, it can literally just be UFUNCTION()