Shotgun에서는 탄의 퍼짐을 계산하는게 제일 중요하다.
이를 구현하기 위해선 여러 방법이 있겠지만, 나는 Sphere형태의 구를 그리고 최대 Radius를 설정한 뒤 각각의 랜덤 벡터를 Trace할 수 있도록 설계하였다. 결과는 다음과 같다.

MuzzleSocket에서 총알이 나가는건 똑같되, 해당 총알의 방향을 설정하는게 주요 로직이다.
산탄총알의 갯수, 그러니까 Pellet의 갯수를 10개라고 한다면, 10개의 총알 모두 비슷한 방향으로 나아가되, 각각 다른 방향으로 나아가야만 한다.
FVector AHitScanWeapon::TraceEndWithScatter(const FVector& TraceStart, const FVector& HitTarget)
{
FVector ToTargetNormalized=(HitTarget-TraceStart).GetSafeNormal();
FVector SphereCenter=TraceStart+DistanceToSphere*ToTargetNormalized;
FVector RandomVector=UKismetMathLibrary::RandomUnitVector()*FMath::FRandRange(0.f,SphereRadius);
FVector EndLoc=SphereCenter+RandomVector;
FVector ToEndLoc=EndLoc-TraceStart;
DrawDebugSphere(GetWorld(),SphereCenter,SphereRadius,12,FColor::Red,true);
DrawDebugSphere(GetWorld(), EndLoc, 4.f, 12, FColor::Orange, true);
DrawDebugLine(
GetWorld(),
TraceStart,
FVector(TraceStart + ToEndLoc * 80000.f / ToEndLoc.Size()),
FColor::Cyan,
true);
return FVector(TraceStart + ToEndLoc * 80000.f / ToEndLoc.Size());

앞서 보이는것처럼 ToTarget을 정규화하고 해당방향에서 일정거리 떨어진 구를 생성한다. 이 Sphere에 최대 Radius만큼의 Vector를 구하고 각각의 점을 EndLoc라고 한다. 다시 이 EndLoc-MuzzleSocket을 하게 되면, 해당 구의 랜덤벡터 방향을 지나는 Pellet의 궤적이 그려진다.
void AShotgun::Fire(const FVector& HitTarget)
{
AWeapon::Fire(HitTarget);
const USkeletalMeshSocket* MuzzleFashSocket=GetWeaponMesh()->GetSocketByName("MuzzleFlash");
if (MuzzleFashSocket)
{
FTransform SocketTransform=MuzzleFashSocket->GetSocketTransform(GetWeaponMesh());
FVector Start=SocketTransform.GetLocation();
TMap<ACDCharacter*, uint32> HitMap;
for (uint32 i=0;i<NumberOfPellets;i++)
{
FVector End=TraceEndWithScatter(Start,HitTarget);
}
}
}
이를 상속받은 샷건에서 Fire()을 하여, pellet의 갯수만큼 반복문을 돌리면, 모두 각기다른 Endpoint를 지닌 궤적이 만들어진다.
다음은 Trace를 통한 Damage 부여이다.
궤적에 따라 HitMap을 그리고 이를 통과하였을 경우 데미지를 부여하였다.
for (uint32 i=0;i<NumberOfPellets;i++)
{
FVector End=TraceEndWithScatter(Start,HitTarget);
FHitResult FireHit;
// WeaponTraceHit(Start, HitTarget, FireHit);
ACDCharacter* CDCharacter=Cast<ACDCharacter>(FireHit.GetActor());
if (HasAuthority()&&CDCharacter)
{
if (HitMap.Contains(CDCharacter))
{
HitMap[CDCharacter]++;
}
else
{
HitMap.Emplace(CDCharacter,1);
}
}
if (ImpactParticles)
{
UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
ImpactParticles,
FireHit.ImpactPoint,
FireHit.ImpactNormal.Rotation());
}
for (auto HitPair:HitMap)
{
if (HitPair.Key&&HasAuthority())
{
UGameplayStatics::ApplyDamage(
HitPair.Key,
Damage*HitPair.Value,
InstigatorController,
this,
UDamageType::StaticClass());
}
}
}
HitMap이라는 구조체는 Character가 맞은 총알 갯수를 해당 캐릭터에 축적시켜서, 한번에 계산하는 함수이다.
TMap<ACDCharacter*, uint32> HitMap;는 맞은 캐릭터, Value는 맞은 총알의 갯수로 포문을 통해 여러개의 탄환이 발사되고, 이러한 HitMap을 통해 같은 캐릭터가 몇개의 탄환을 맞았는지 누적계산하여 적용시킨다.
만약 Pellet을 맞은 캐릭터가 기존 캐릭터일 경우 단순 value값을 1늘려주고, 새로운 캐릭터라면 1로 초기화 시킨뒤 위의 if작업을 이어한다. 이는 C++에서 지원하는 Map이 ++만 하면 자동으로 초기화, 추가해주는 것과는 사뭇 다른 점에서 기인한다. Unreal의 TMap 자료구조 특성상HitMap[CDCharacter]++만으로 인덱스 추가가 되지 않기에 else를 통한 emplace작업을 곁들어줘야한다.
이러한 함수를 따로 만든 이유는 계산 최적화와 더불어, 탄환 하나를 맞을때마다 피격 애니메이션이 재생되는 경우를 막기 위함이다.