Projectile을 통한 총알 발사

김지윤·2025년 1월 31일

UE5_MultiplayerShooter

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

profile
공부한 거 시간 날 때 작성하는 곳

0개의 댓글