LineTraceSingle fails at certain angles when called from Tick()

Hello,

I’m developing something between a Diablo clone and a twin stick shooter and I’m stuck on this headscratcher:

I have a character class that has a weapon actor class. The character turns towards the mouse location and the weapon is aligned with the character’s x axis. When the weapon fires, it spawns a particle beam from its origin to a line trace collision or its max range if the trace fails to hit anything. The character has a left click input that calls the weapon’s FireHold() when down and FireRelease() when up. FireHold() not only fires the weapon but also turns a boolean, autoFireOn, to true, and FireRelease() makes it false. While it is true, the weapon’s Tick() will repeatedly call FireHold(), firing a shot on a timer. Basic stuff, right?

Here’s the bug: at a certain roughly 90 degree arc, traces from FireHold() will never hit anything, but ONLY if FireHold() was called from Tick(). If it was called from the character’s input, it works at all angles, otherwise it works at any angle outside of that arc. I’ve attached a simple diagram to show what I mean.

Anyone know what could be causing this or how to fix it? I feel like I’m missing some fundamental understanding of how the engine handles these things.

Thanks!

Here are some relevant code snippets:

The player character’s rotate to mouse, called on its Tick():

bool AControllableCharacter::findMouseRotation(FRotator& rotation)
{
	
	if (GetWorld()) {
		FVector mousePos = FVector();
		FVector mouseDir = FVector();

		APlayerController* control = UGameplayStatics::GetPlayerController(ValidWorld, 0);
		bool mouseFound = control->DeprojectMousePositionToWorld(mousePos, mouseDir);
		if (!mouseFound) return false; // conversion failed
		
		FVector location = this->GetActorLocation();
		FVector worldUp = FVector(0, 0, 1);
		mouseDir.Normalize();
		FVector x = FVector(0, 0, location.Z) - mousePos;
		//x.Normalize();
		float num = FVector::DotProduct(x, worldUp);
		float denom = FVector::DotProduct(mouseDir, worldUp);

		FVector targetOnPlane = mouseDir * num / denom + mousePos;
		rotation = FRotationMatrix::MakeFromX(targetOnPlane - location).Rotator();
		rotation = FRotator(0, rotation.Yaw, 0);

		return true;
	} 
	
	return false; // should never happen in game, but just in case
}

The player character’s inputs:

void AControllableCharacter::PrimaryFireHold()
{
	if (activeWeapon != NULL) {
		activeWeapon->FireHold();
		if (equipDebug) GEngine->AddOnScreenDebugMessage(-1, 5.f, 
			FColor::Blue,
			TEXT("Fired a weapon"));
	}
	else {
		if (equipDebug) GEngine->AddOnScreenDebugMessage(-1, 5.f,
			FColor::Blue,
			TEXT("Active weapon is NULL"));
	}
}

void AControllableCharacter::PrimaryFireRelease()
{
	if (activeWeapon != NULL) {
		activeWeapon->FireRelease();
	}
}

FireHold() and FireRelease():

void ABaseWeapon::FireHold()
{
	autoFireOn = true;

	// only fire when timer allows
	if (remainingShotDelay > 0.01) {
		return;
	}

	FVector origin = validRotation.RotateVector(barrelLocation) + GetActorLocation();
	FVector direction = validRotation.Vector();
	FVector end = origin + range * direction;

	// traces against level and characters
	ECollisionChannel channel = ECC_WorldDynamic;

	// do not request any additional details from collision
	FCollisionQueryParams params(false);

	if (hitscanWeapon) {
		// trace a ray from weapon to range
		FHitResult collision = FHitResult(ForceInit);
		validWorld->LineTraceSingleByChannel(collision, origin, end, channel, params);

		if (collision.IsValidBlockingHit()) {
			// need to get damageable interface working first to inflict damage
			end = collision.ImpactPoint;
			if (debug) {
				GEngine->AddOnScreenDebugMessage(-1, 5.f,
					FColor::Blue,
					TEXT("Weapon confirmed collision"));
			}
			// Damage is NYI
		}
		else {
			// to be disabled, but shows expected values despite bug!
			GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue,
				FString::Printf(TEXT("Missing rotator: (%f, %f, %f) at translation (%f, %f, %f"),
					validRotation.Roll,
					validRotation.Yaw,
					validRotation.Pitch,
					origin.X,
					origin.Y,
					origin.Z));
		}

		if (beam) {
			// spawn a particle beam
			UParticleSystemComponent* spawnedBeam = 
				UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), beam, 
					GetActorTransform(), false);

			if (spawnedBeam) {
				spawnedBeam->SetBeamSourcePoint(0, origin, 0);
				spawnedBeam->SetBeamEndPoint(0, end);
			}
		}

		if (impact && collision.IsValidBlockingHit()) {
			UParticleSystemComponent* spawnedImpact = 
				UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), impact,
					FTransform(FRotator(), end), false);
		}

		if (muzzleFlash) { 
			UParticleSystemComponent* spawnedFlash =
				UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), muzzleFlash,
					FTransform(GetActorRotation(), origin), false);
		}

	}
	else {
		// projectile weapon is NYI
	}

	remainingShotDelay = shotDelay;
}


void ABaseWeapon::FireRelease()
{
	autoFireOn = false;
	if (!chargeWeapon) return; // NYI
}

And the Weapon’s Tick():

void ABaseWeapon::Tick( float DeltaTime )
{
	Super::Tick( DeltaTime );
	remainingShotDelay -= DeltaTime;
	
	if (remainingShotDelay < 0) remainingShotDelay = 0;

	if (autoFireOn) FireHold();
}

Hey Dan M,

I am not seeing the same thing you are:

[.h]

#pragma once

#include "GameFramework/Character.h"
#include "TCharacter.generated.h"

UCLASS()
class AH500338_API ATCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ATCharacter();
	virtual void BeginPlay() override;
	virtual void Tick( float DeltaSeconds ) override;
	virtual void SetupPlayerInputComponent(class UInputComponent* IC) override;
    virtual void PossessedBy( AController * NewController ) override;
    
    void ToggleActiveClick( );
    bool bActiveClick;
};

[cpp]

#include "AH500338.h"
#include "TCharacter.h"

ATCharacter::ATCharacter()
{
    PrimaryActorTick.bCanEverTick = true;
}

void ATCharacter::BeginPlay()
{
	Super::BeginPlay();
	
}

void ATCharacter::Tick( float DeltaTime )
{
	Super::Tick( DeltaTime );

    if( bActiveClick )
    {            
         FHitResult HitResult;
         if( Cast<APlayerController>( GetController( ) )->GetHitResultUnderCursor(ECollisionChannel::ECC_WorldStatic, true, HitResult) )
         {
             DrawDebugLine( GetWorld( ), GetActorLocation( ), HitResult.ImpactPoint, FColor::Red, true, 1.5f, 0, 4.f );
         }
    }
}

void ATCharacter::PossessedBy( AController * NewController )
{
    Super::PossessedBy( NewController );
    Cast<APlayerController>( NewController )->bShowMouseCursor = true;
}

void ATCharacter::SetupPlayerInputComponent(class UInputComponent* IC)
{
	Super::SetupPlayerInputComponent(IC);

    IC->BindAction( "Jump", IE_Pressed, this, &ACharacter::Jump );
    IC->BindAction( "ActiveClick", IE_Pressed, this, &ATCharacter::ToggleActiveClick );
    IC->BindAction( "ActiveClick", IE_Released, this, &ATCharacter::ToggleActiveClick );
}

void ATCharacter::ToggleActiveClick( )
{
    bActiveClick = !bActiveClick;
}

I have never calculated rotations and angles like you are, so maybe that has more going on with what you are seeing than an issue Trace( ) .

Hi,
Doesn’t GetHitResultUnderCursor trace from the beginning of the view frustum to wherever your mouse is on screen space? I’m trying to use LineTraceSingleByChannel which goes from the weapon actor’s barrel in the direction of its forward vector (so the trace ends at whatever is in the way of the gunshot).

I’m also concerned with whether trying to encapsulate everything in the weapon actor parented to the character has anything to do with it, since I am eventually planning on making an inventory system.

Also, the rotation calculation gets a point from ray-plane intersection and looks toward it. My first instinct also was that I was doing this incorrectly but debug printing and the visual output suggest it’s working properly. Even when the trace fails, the particle beam is drawn at the correct location and angle.

I’m trying to use LineTraceSingleByChannel which goes from the weapon actor’s barrel in the direction of its forward vector (so the trace ends at whatever is in the way of the gunshot).

As in wanting to fire a projectile from the weapon to wherever the mouse is (in world space)?

Yes. That is the plan. Or at least towards the mouse in world space.

An update: everything works fine if I break encapsulation by only calling FireHold() and handling the automatic fire in the character class instead of from the weapon class. While I’m happy that I can move on with developing my game, I don’t think this is a good coding practice. Does anyone know what could be causing this?