무기 발사와 데미지를 서버 권한으로 처리하기
23강. 무기 발사와 데미지를 서버 권한으로 처리하기
1. 이번 강의에서 해결할 문제
클라이언트가 보낸 Hit Actor와 데미지를 서버가 그대로 적용하면 벽 너머 적, 무한 탄약과 연사 치팅이 가능합니다. 반대로 서버의 현재 ControlRotation만 사용하면 네트워크 지연 중 사용자가 본 조준과 판정이 어긋날 수 있습니다. 이 강의에서는 Lag Compensation까지 구현하지 않지만 신뢰할 값과 검증할 값을 명확히 나눕니다.
2. 학습 목표
3. 핵심 개념
클라이언트는 조준 방향과 Sequence를 보낼 수 있지만 명중 Actor, 데미지, 남은 탄약과 쿨다운 완료를 결정하지 않습니다. 서버는 권위 있는 Muzzle 위치, 현재 무기, 탄약과 서버 시간으로 요청을 검증합니다. 기본 과정에서는 서버 현재 World를 Trace하며 과거 위치 되감기는 32강의 Lag Compensation 심화 안내로 남깁니다.
4. 단계별 실습
UCLASS()
class CAPTUREARENA_API ACaptureWeapon : public AActor
{
GENERATED_BODY()
public:
ACaptureWeapon();
void RequestFire(FVector AimDirection);
virtual void GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const override;
protected:
UFUNCTION(Server, Unreliable, WithValidation)
void Server_RequestFire(
FVector_NetQuantizeNormal AimDirection,
uint16 Sequence);
UFUNCTION(NetMulticast, Unreliable)
void Multicast_PlayFireFx(
FVector_NetQuantize Muzzle,
FVector_NetQuantize Impact,
bool bHit);
UPROPERTY(ReplicatedUsing = OnRep_Ammo)
int32 CurrentAmmo = 12;
UPROPERTY(EditDefaultsOnly, Category = "Weapon")
float Damage = 25.0f;
UPROPERTY(EditDefaultsOnly, Category = "Weapon")
float Range = 5000.0f;
UPROPERTY(EditDefaultsOnly, Category = "Weapon")
float FireInterval = 0.25f;
UFUNCTION() void OnRep_Ammo();
private:
double LastServerFireTime = -DBL_MAX;
uint16 LocalSequence = 0;
};
void ACaptureWeapon::RequestFire(FVector AimDirection)
{
APawn* OwnerPawn = Cast<APawn>(GetOwner());
if (!OwnerPawn || !OwnerPawn->IsLocallyControlled()) return;
Server_RequestFire(AimDirection.GetSafeNormal(), ++LocalSequence);
}
bool ACaptureWeapon::Server_RequestFire_Validate(
FVector_NetQuantizeNormal AimDirection, uint16 Sequence)
{
return AimDirection.IsNormalized() && !AimDirection.ContainsNaN()
&& Sequence != 0;
}
void ACaptureWeapon::Server_RequestFire_Implementation(
FVector_NetQuantizeNormal AimDirection, uint16 Sequence)
{
APawn* OwnerPawn = Cast<APawn>(GetOwner());
UWorld* World = GetWorld();
if (!OwnerPawn || !World || CurrentAmmo <= 0) return;
const double Now = World->GetTimeSeconds();
if (Now - LastServerFireTime < FireInterval) return;
const FVector Muzzle = GetMuzzleLocation();
const FVector ServerForward = OwnerPawn->GetControlRotation().Vector();
if (FVector::DotProduct(ServerForward, AimDirection) < 0.85f) return;
FHitResult Hit;
FCollisionQueryParams Params(SCENE_QUERY_STAT(CaptureFire), true, OwnerPawn);
Params.AddIgnoredActor(this);
const FVector End = Muzzle + FVector(AimDirection) * Range;
World->LineTraceSingleByChannel(Hit, Muzzle, End, ECC_Visibility, Params);
--CurrentAmmo;
LastServerFireTime = Now;
OnRep_Ammo();
if (AActor* HitActor = Hit.GetActor())
{
UGameplayStatics::ApplyPointDamage(
HitActor, Damage, AimDirection, Hit,
OwnerPawn->GetController(), this, DamageTypeClass);
}
Multicast_PlayFireFx(Muzzle, Hit.bBlockingHit ? Hit.ImpactPoint : End,
Hit.bBlockingHit);
}
void ACaptureWeapon::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION(ACaptureWeapon, CurrentAmmo, COND_OwnerOnly);
}
무기는 서버에서 Spawn하고 Character를 Owner/Instigator로 설정해야 소유 클라이언트의 RPC와 데미지 Instigator가 올바르게 연결됩니다. 서버 Trace Debug는 서버 창 또는 서버 로그에서 확인합니다.
5. 코드가 동작하는 이유
소유 클라이언트는 조준 의도만 전송합니다. 서버는 자신의 Muzzle, ControlRotation, 탄약과 시간으로 유효한 한 발을 만들고 Trace 결과에만 데미지를 적용합니다. 탄약은 OwnerOnly RepNotify로 HUD에 수렴하고 효과 유실은 데미지 결과에 영향을 주지 않습니다.
6. 자주 하는 실수와 해결법
7. 직접 실습
8. 이해 점검 질문 3개
9. 핵심 요약
무기 발사와 데미지를 서버 권한으로 처리하기 미니 퀴즈
선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.
학습을 마쳤나요?
직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.