OnComponentHit Triggering Twice From One Impact

I’ve got a Target and Projectile class in my cpp project, with the Target handling collisions with Projectiles by detracting health based on the damage provided by the Projectile, and the Projectile deleting itself whenever it hits anything. These interactions work, but for whatever reason, the Target is calling it’s OnComponentHit callback twice whenever hit by a Projectile.

Target.cpp

#include "Target.h"

// Sets default values
ATarget::ATarget()
{
 	// 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;

	// create the visual mesh, set it to simulate physics, and attach the OnHit function to collisions
	VisualMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	VisualMesh->SetupAttachment(RootComponent);

	// set a default mesh of a target
	static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Cube.Shape_Cube"));
	if (CubeVisualAsset.Succeeded())
	{
		VisualMesh->SetStaticMesh(CubeVisualAsset.Object);
		VisualMesh->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
	}

	// enable physics and overlap events
	VisualMesh->SetSimulatePhysics(true);
	VisualMesh->SetGenerateOverlapEvents(true);
}

// Called when the game starts or when spawned
void ATarget::BeginPlay()
{
	// this is needed for destroy calls to work
	Super::BeginPlay();

	// set up collision handling
	VisualMesh->OnComponentHit.AddDynamic(this, &ATarget::OnHit);
	VisualMesh->OnComponentBeginOverlap.AddDynamic(this, &ATarget::OnOverlap);
}

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

void ATarget::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit)
{
	// check for collision with Projectiles
	if (OtherActor->GetClass()->IsChildOf(AProjectile::StaticClass())) {
		// debug logging Projectile hit
		if (GEngine) {
			// Display a debug message for five seconds
			// The -1 "Key" value argument prevents the message from being updated or refreshed
			GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("Target hit by Projectile"));
		}
		// cast to Projectile
		AProjectile* HitProjectile = static_cast<AProjectile*>(OtherActor);
		if (GEngine) {
			// Display a debug message for five seconds
			// The -1 "Key" value argument prevents the message from being updated or refreshed
			GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, FString::Printf(TEXT("Projectile Damage: %d"), HitProjectile->Damage));
		}
		// reduce health by ammount of damage the Projectile provides
		Health -= FMath::Min(HitProjectile->Damage, Health);
		if (GEngine) {
			// Display a debug message for five seconds
			// The -1 "Key" value argument prevents the message from being updated or refreshed
			GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, FString::Printf(TEXT("New current health: %d"), Health));
		}
	}
	// destroy this object if it's health has reached 0
	if (Health == 0) {
		// debug logging Target destruction
		if (GEngine) {
			// Display a debug message for five seconds
			// The -1 "Key" value argument prevents the message from being updated or refreshed
			GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("Target destroyed"));
		}
		// destroy the Target
		Destroy();
	}
}

void ATarget::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor->GetClass()->IsChildOf(AExplosion::StaticClass())) {
		// debug logging Explosion hit
		if (GEngine) {
			// Display a debug message for five seconds
			// The -1 "Key" value argument prevents the message from being updated or refreshed
			GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Orange, TEXT("Target hit by Explosion"));
		}
		AExplosion* HitExplosion = static_cast<AExplosion*>(OtherActor);
		if (GEngine) {
			// Display a debug message for five seconds
			// The -1 "Key" value argument prevents the message from being updated or refreshed
			GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Orange, FString::Printf(TEXT("Explosion Damage: %d"), HitExplosion->Damage));
		}
		Health -= FMath::Min(HitExplosion->Damage, Health);
		if (GEngine) {
			// Display a debug message for five seconds
			// The -1 "Key" value argument prevents the message from being updated or refreshed
			GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Orange, FString::Printf(TEXT("New current health: %d"), Health));
		}
	}
	// destroy this object if it's health has reached 0
	if (Health == 0) {
		// debug logging Target destruction
		if (GEngine) {
			// Display a debug message for five seconds
			// The -1 "Key" value argument prevents the message from being updated or refreshed
			GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Orange, TEXT("Target destroyed"));
		}
		// destroy the Target
		Destroy();
	}
}


Projectile.cpp

#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;

	// set up collision component
	if (!CollisionComponent)
	{
		// Use a sphere as a simple collision representation
		CollisionComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
		// Set the sphere's collision profile name to "Projectile"
		CollisionComponent->BodyInstance.SetCollisionProfileName(TEXT("Projectile"));
		// ignore other projectiles and explosions
		CollisionComponent->SetCollisionResponseToChannel(ECollisionChannel::ECC_GameTraceChannel1, ECollisionResponse::ECR_Ignore);
		CollisionComponent->SetCollisionResponseToChannel(ECollisionChannel::ECC_GameTraceChannel2, ECollisionResponse::ECR_Ignore);
		// Set the sphere's initial collision radius
		CollisionComponent->InitSphereRadius(15.0);
	}
	// Set the root component to be the collision component
	RootComponent = CollisionComponent;
	// set up projectile movement component
	if (!ProjectileMovementComponent) {
		// Use this component ot drive this projectile's movement
		ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovementComponent"));
		ProjectileMovementComponent->SetUpdatedComponent(CollisionComponent);
		ProjectileMovementComponent->InitialSpeed = 10000.0f;
		ProjectileMovementComponent->MaxSpeed = 10000.0f;
		ProjectileMovementComponent->bRotationFollowsVelocity = true;
		ProjectileMovementComponent->bShouldBounce = true;
		ProjectileMovementComponent->Bounciness = 0.3f;
		ProjectileMovementComponent->ProjectileGravityScale = 0.0f;
	}
	// set up mesh component
	if (!ProjectileMeshComponent)
	{
		ProjectileMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ProjectileMeshComponent"));
		static ConstructorHelpers::FObjectFinder<UStaticMesh> ProjectileMesh(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
		if (ProjectileMesh.Succeeded())
		{
			ProjectileMeshComponent->SetStaticMesh(ProjectileMesh.Object);
		}
	}

	// set up material
	static ConstructorHelpers::FObjectFinder<UMaterial> ProjectileMaterial(TEXT("'/Game/StarterContent/Materials/M_Metal_Steel.M_Metal_Steel'"));
	if (ProjectileMaterial.Succeeded())
	{
		ProjectileMaterialInstance = UMaterialInstanceDynamic::Create(ProjectileMaterial.Object, ProjectileMeshComponent);
	}

	// attach material to mesh, scale mesh, and set mesh as root component
	ProjectileMeshComponent->SetMaterial(0, ProjectileMaterialInstance);
	ProjectileMeshComponent->SetRelativeScale3D(FVector(0.1f, 0.1f, 0.1f));
	ProjectileMeshComponent->SetupAttachment(RootComponent);

	InitialLifeSpan = 3.0f;
}

// Called when the game starts or when spawned
void AProjectile::BeginPlay()
{
	// this is needed for destroy calls to work
	Super::BeginPlay();	

	// Event called when component hits something
	CollisionComponent->OnComponentHit.AddDynamic(this, &AProjectile::OnHit);
}

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

}

void AProjectile::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit)
{
	// add impulse to physicis simulating objects
	if (OtherActor != this && OtherComponent->IsSimulatingPhysics())
	{
		OtherComponent->AddImpulseAtLocation(ProjectileMovementComponent->Velocity * 100.0f, Hit.ImpactPoint);
	}
	if (GEngine) {
		// Display a debug message for five seconds
		// The -1 "Key" value argument prevents the message from being updated or refreshed
		GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, FString::Printf(TEXT("Destroying Projectile")));
	}
	// destroy this projectile
	Destroy();
	// spawn explosion if one has been attached to this object
	if (ProjectileExplosion) {
		if (GEngine) {
			// Display a debug message for five seconds
			// The -1 "Key" value argument prevents the message from being updated or refreshed
			GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Orange, FString::Printf(TEXT("Spawn X: %f, Spawn Y: %f, Spawn Z: %f"), CollisionComponent->GetComponentLocation().X, CollisionComponent->GetComponentLocation().Y, CollisionComponent->GetComponentLocation().Z));
		}
		GetWorld()->SpawnActor<AExplosion>(ProjectileExplosion, CollisionComponent->GetComponentLocation(), CollisionComponent->GetComponentRotation());
	}
}

void AProjectile::FireInDirection(const FVector& ShootDirection)
{
	// set the velocity based on the passed direction
	ProjectileMovementComponent->Velocity = ShootDirection * ProjectileMovementComponent->InitialSpeed;
}


Might be worth noting that I’ve also incorporated an Explosion class that damages Targets via an OnComponentBeginOverlap callback, and when testing that functionality it only fires once per explosion. So I’m at a bit of a loss as to why the OnComponentHit doesn’t work the same…

I’ve encountered this with overlaps. Usually the easiest solution is to add a boolean to track if it was hit, set it the first time OnHit is called and just return on any subsequent calls.

@Dante5050 If it’s a character you are hitting it could be hitting the outer capsule component first and then the inner mesh. Try printing the name of the components to screen.

If the internal mesh is triggering it too then maybe decide if only one part of the character should receive the collision (maybe through a custom collision channel for the projectile) and turn that off for the mesh or capsule collider reaction (ignore) for projectiles.

@SolidGasStudios has the right approach how to stop it. Without many extra steps.

agree. Log the HitComponent and the OtherComponent, and I think you’ll find that it’s either two different components hitting Target, or two different components in Target being hit.

Logging the components yields the SphereComponent for other (Projectile) and Mesh for hit (Target) both times. Targets (as of right now) only have a StaticMesh, no separate collision capsule component or anything, but for the sake of testing I also tried removing the mesh component from Projectile so that it only had it’s sphere collision component, but even then it was still triggering Target’s OnHit twice. For the time being I’ve implemented @SolidGasStudios solution with the modification that Targets are checking the name of the OtherActor each OnHit instead of just using a bool, as I plan on having some Targets taking multiple hits from multiple sources before going down, and for now it does the trick. However, I worry that if two Projectiles hit a Target at the same time, the OnHits may get layered such that P1 Hits for the first time, then P2 Hits for the first time, resetting the tracking string to P2 before P1 Hits the second time, resurrecting this issue down the road…