DEV Community

Tiny
Tiny

Posted on • Originally published at tinygame.dev on

Better third person projectiles

The problem

In third person games you tend to fire a projectile from the muzzle of a weapon, the hand of the player character, or some similar location. Imagine tracing a line from that point in the game world in the direction of the weapon/hand/etc. This is possibly the path you'd like your projectile to follow. When the player aims however they most likely look at a 2D sprite of a crosshair in the center of the screen. Now imagine tracing a line from the center of the screen (which would be the location of your camera) forward along the view direction. This line will be different from the line the projectile will follow, which means the projectile may or may not hit what the player thought they were aiming at.

The fix

In order to get perfect aiming you would really need to spawn your projectile at the location of the camera and have it move along the line traced through your crosshair into the game world. This isn't great for third person games when you see the character in front of the camera. If you don't care about perfect, there's a simple improvement that can be applied.

  1. Trace a line from the camera forward for some distance, say 50m.
  2. If the line intersects an object, use the location of the intersection as the target point for the projectile. If there's no intersection, keep the point 50m ahead as the target.
  3. Spawn a projectile from the muzzle/hand/etc. moving towards the target point from above.

Keep in mind there's edge cases where this will break, for example when tracing a line from the character forward would intersect an obstacle and the line from the camera wouldn't. Still, most of the time this will work as expected.

See example code for UE5 below.

// get the pawn that spawns the projectile and use it as Instigator/Owner, you may want to know who caused damage later
APawn* Pawn = Cast<APawn>(GetMesh()->GetOwner());
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParams.Instigator = Pawn;
SpawnParams.Owner = Pawn;

// collision shape for the projectile
FCollisionShape Shape;
Shape.SetSphere(20.f);

// make sure projectile doesn't collide with the pawn that fires it
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(Pawn);

// types of geometry to collide with
FCollisionObjectQueryParams ObjectQueryParams;
ObjectQueryParams.AddObjectTypesToQuery(ECC_WorldDynamic);
ObjectQueryParams.AddObjectTypesToQuery(ECC_WorldStatic);
ObjectQueryParams.AddObjectTypesToQuery(ECC_Pawn);

// trace line from camera 50m forward and try to find what the player is aiming at
APlayerController* PlayerController = Pawn->GetController<APlayerController>();
FVector TraceStart = PlayerController->PlayerCameraManager->GetCameraLocation();
FVector TraceEnd = TraceStart + (Pawn->GetControlRotation().Vector() * 5000);

// if we hit something, update the end point to the location of the hit
FHitResult Hit;
if (Pawn->GetWorld()->SweepSingleByObjectType(Hit, TraceStart, TraceEnd, FQuat::Identity, ObjectQueryParams, Shape, QueryParams))
{
    TraceEnd = Hit.ImpactPoint;
}

// use a socket for spawn location (muzzle, hand, etc.)
FVector SocketLocation = GetMesh()->GetSocketLocation(SocketName);
// create a vector going from socket to the trace hit location (or 50m ahead) and use as direction
FRotator ProjectileRotation = FRotationMatrix::MakeFromX(TraceEnd - SocketLocation).Rotator();
FTransform SpawnTransform = FTransform(ProjectileRotation, SocketLocation);

// spawn projectile
GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTransform, SpawnParams);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)