
FVector_NetQuantize UCombatComponent::TraceUnderCrosshairs(FHitResult& TraceHitResult)
{
// 뷰포트 사이즈를 통해 정가운데 위치 계산
FVector2D ViewportSize;
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->GetViewportSize(ViewportSize);
}
FVector2D CrosshairLocation(ViewportSize.X / 2.f, ViewportSize.Y / 2.f);
// 2D 화면 좌표를 3D 월드 좌표로 변환
FVector CrosshairWorldPosition; // 크로스헤어 시작 위치
FVector CrosshairWorldDirection; // 크로스헤어가 향하는 방향
bool bScreenToWorld = UGameplayStatics::DeprojectScreenToWorld(
UGameplayStatics::GetPlayerController(this, 0),
CrosshairLocation,
CrosshairWorldPosition,
CrosshairWorldDirection
);
// 시작점과 끝점 계산
FVector Start = CrosshairWorldPosition;
FVector End = Start + CrosshairWorldDirection * TRACE_LENGTH;
// HitTarget 추적 시작
if (bScreenToWorld)
{
// 두 벡터 중간에 Visibility 채널로 충돌 검출
GetWorld()->LineTraceSingleByChannel(
TraceHitResult,
Start,
End,
ECollisionChannel::ECC_Visibility
);
}
if (TraceHitResult.bBlockingHit)
{
return TraceHitResult.ImpactPoint;
}
else
{
return End;
}
}
클라이언트는 자신이 어딜 바라보고 있는지 계속해서 업데이트할 필요가 없다.
사격하기 직전, 딱 그 때만 한 번 업데이트하면 된다.
기존 멤버 변수에 있던 FVector HitTrace를 지우고, LineTrace를 하는 함수가 FVector_NetQuantize를 직접 반환하도록 했다.
void UCombatComponent::FireButtonPressed(bool bPressed)
{
// 일단 바로 변경, 아직 다른 클라이언트는 반영되지 않음
bFireButtonPressed = bPressed;
if (bFireButtonPressed)
{
// 사격 시작
FHitResult HitResult;
LocalFire(TraceUnderCrosshairs(HitResult));
}
}
그 뒤 LocalFire의 매개변수로 TraceUnderCrosshairs(HitResult)를 넣는다.
당연히 LocalFire, ServerFire(클라이언트가 서버에 호출을 요청하는 함수), MulticastFire(RPC)의 매개변수를 FVector_NetQuantize로 변경했다.
void AProjectileWeapon::Fire(const FVector& HitTarget)
{
// 격발 애니메이션 재생
Super::Fire(HitTarget);
APawn* InstigatorPawn = Cast<APawn>(GetOwner());
// 총구쪽 Socket
const USkeletalMeshSocket* MuzzleFlashSocket = GetWeaponMesh()->GetSocketByName(FName("MuzzleFlash"));
if (MuzzleFlashSocket)
{
// LineTrace 시작 Transform(== 총구쪽 Socket의 Transform)
FTransform SocketTransform = MuzzleFlashSocket->GetSocketTransform(GetWeaponMesh());
// 총구에서 HitTarget으로의 방향 계산
FVector ToTarget = HitTarget - SocketTransform.GetLocation();
FRotator TargetRotation = ToTarget.Rotation();
if (ProjectileClass && InstigatorPawn)
{
// ProjectileClass를 기반으로 한 액터 스폰
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = GetOwner();
SpawnParams.Instigator = InstigatorPawn;
UWorld* World = GetWorld();
if (World)
{
World->SpawnActor<AProjectile>(
ProjectileClass,
SocketTransform.GetLocation(),
TargetRotation,
SpawnParams
);
}
}
}
}
Weapon을 상속받은 ProjectileWeapon의 Fire 함수 구문이다.
Super는 단순 애니메이션만 재생한다.
그 다음 총구쪽 Socket의 Transform을 가져와 Projectile 클래스를 기반으로 하는 액터를 스폰, 화면 방향으로 발사한다.
void AProjectile::BeginPlay()
{
Super::BeginPlay();
if (Tracer)
{
TracerComponent = UGameplayStatics::SpawnEmitterAttached(
Tracer,
CollisionBox,
FName(),
GetActorLocation(),
GetActorRotation(),
EAttachLocation::KeepWorldPosition
);
}
CollisionBox->OnComponentHit.AddDynamic(this, &ThisClass::OnHit);
}
Projectile은 시작과 동시에 이미터를 스폰하고, OnCopmonentHit 이벤트에 함수를 바인딩한다.
Tracer는 ParticleSystem으로, 파티클 효과를 정의하는 클래스다.
이 Tracer를 기반으로 CollisionBox에 Attach시켜 이미터를 스폰한다.

이렇게 생긴 파티클이 Projectile을 따라 함께 날아갈 것이다.
void AProjectile::OnHit(UPrimitiveComponent* HitComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
FVector NormalImpulse,
const FHitResult& Hit)
{
// TODO: 부딪힌 대상에게 데미지
Destroy();
}
void AProjectile::Destroyed()
{
Super::Destroyed();
if (ImpactParticle)
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactParticle, GetActorTransform());
}
if (ImpactSound)
{
UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation());
}
}
아직은 부딪혔을 때 단순히 Destroy되기만 한다.
그리고 Destroy되는 순간 부딪히는 파티클을 해당 자리에 스폰하며 사운드를 재생하는 코드를 작성해놨다.

Sound Cue는 미리 만들어진 에셋을 사용했다.
잠시 내부를 살펴보면

재생 시마다 6개의 사운드 에셋 중 랜덤하게 플레이하는 모습이다.

이건 어테뉴에이션 세팅이라는데 솔직히 봐도 잘 모르겠다.

어쨌든 잘 작동하는 모습이다.
총알마다 속도가 다를 수 있기 때문에 Projectile 생성자에서 직접 속도를 설정하지 않고 해당 클래스에서 파생되는 블루프린트가 속도를 정하게 했다.
현재 속도는 15000이다.

이건 net pktlag=300 으로 핑이 300인 상황을 만들어 테스트하는 모습이다.
기존까지 했던대로 '클라이언트가 일단 사격, 서버로 송신해 다른 클라이언트에 반영'을 고수했다.
타격 판정을 히트스캔이 아닌 투사체로 보는 데다가, 강의가 이끄는 방향(모든 걸 서버에서 처리)이 아닌 내 입맛대로 수정하고 있기 때문에 나중에 상당히 골치 아플 것 같긴 하지만..
좋은 경험이 되지 않을까 싶다.