
크로스헤어 스프레드, 색깔 조정 생략
HitReact 애니메이션 과정 생략
그리고 위 사진은 잘 작동하는 모습이다.
void AProjectile::OnHit(UPrimitiveComponent* HitComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
FVector NormalImpulse,
const FHitResult& Hit)
{
if (ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(OtherActor))
{
BlasterCharacter->PlayHitReactMontage();
}
Destroy();
}
Hit 이벤트에 캐릭터를 Cast해 가져와 HitReact 몽타주 재생 함수를 호출했다.
하지만 나의 진행 상황과 강의의 진행 상황은 다르다.
강의에선 서버가 Hit에 성공했을 때 클라이언트는 몽타주를 재생하지 않는 문제가 있어서 RPC 함수를 만들어 호출했다.
그리고 나는 RPC를 하지 않았음에도 클라이언트가 몽타주를 재생했다.
아마 강의에선 총알 스폰에 대한 권한을 서버가 갖고 있고, 나는 조작감 개선을 위해 클라이언트가 갖게 했기 때문인 것 같다.
Hit 이벤트를 클라이언트 모두가 각자 발생시키는 상황인 거다.
처음엔 'RPC 안 썼는데 다 재생하네? 대박이다.' 라고 생각했지만, 조금만 더 생각해봤더니 내가 훨씬 더 거대한 문제를 갖고 있다는 걸 알게 되었다.

내가 저번 포스트에서 이런 말을 적은 적이 있다.

그렇다! 벌써 왔다! 골치 아픈 거!
A는 B를 맞혔고, 피격 애니메이션이 재생되는 걸 확인까지 했다.
하지만 B는? 옆으로 피했다!
이런 경우 대부분의 게임은 어떻게 진행되는가 하면, 그냥 맞았다고 처리한다.
핑이 높은 환경에서 슈터 게임을 한 경험이 있는 사람은 알 것이다.
난 분명 벽 뒤로 피했고, 내 화면에서 더이상 적이 보이지 않게 된 지 한참이 지났음에도 내가 죽어버린 경험.
이럴 때 그냥 서버가 맞았다고 처리해버려서 그렇다.
보통은 억울하게 생각하겠지만, 개발 지식과 슈터 게임에 대한 지식이 둘 다 있는 사람이라면 이게 공평하단 걸 알 것이다.
하지만 이건 레이캐스트(언리얼에선 라인트레이스) 방식으로 피격 판정을 검사하는 '히트스캔' 방식에서나 먹히는 얘기다.
'투사체' 무기의 경우 서버가 권한을 갖는 게 맞다.
정말 원래대로라면, 배틀 그라운드가 이런 방식인데, 투사체 자체는 발사한 플레이어에게만 보이고 다른 클라이언트들은 사운드와 피격 효과만 보면 된다 하지만..
강의가 나중에 이 Projectile 클래스를 상속받는 '수류탄'을 만든다면 강의를 그대로 따라가기가 더욱 어려워질 것 같다.
왜냐면 수류탄 등의 투척무기는 정말 서버가 모든 권한을 가져야 하기 때문이다.
현재는 게임을 만드는 게 아니라 수강을 제대로 마치는 게 목적이므로, 여기부턴 적당히 강의를 따라가야겠다..
애니메이션과 사운드 재생은 로컬에서 먼저 수행한다는 점이 강의와는 다른 점이다.
void UCombatComponent::LocalFire(const FVector_NetQuantize& TraceHitTarget)
{
// 착용 중인 무기가 없는 경우 바로 리턴
if (EquippedWeapon == nullptr)
{
return;
}
if (Character && Character->IsLocallyControlled())
{
// 캐릭터 사격 애니메이션 재생
Character->PlayFireMontage(bAiming);
// Projectile에 대한 권한은 서버에게 있어야 하므로 스폰 없이 애니메이션만 재생
Character->PlayFireMontage(bAiming);
EquippedWeapon->PlayFireMontage();
// 서버에 사격 요청
ServerFire(TraceHitTarget);
}
}
기존 Projectile을 스폰하도록 함수를 호출한 뒤 서버에 요청한 것과 달리, 지금은 애니메이션만 재생한 뒤 서버에 Projectile을 스폰하도록 요청하고 있다.
void UCombatComponent::ServerFire_Implementation(const FVector_NetQuantize& TraceHitTarget)
{
// 해당 함수는 ServerFire는 클라이언트가 서버에 요청하는 함수
MulticastFire(TraceHitTarget);
}
void UCombatComponent::MulticastFire_Implementation(const FVector_NetQuantize& TraceHitTarget)
{
// 서버가 클라이언트들에게 함수를 원격 호출해주는 함수
if (EquippedWeapon == nullptr || Character == nullptr)
{
return;
}
// 자신의 캐릭터는 이미 사격 함수를 호출했으므로, 아닌 경우만 호출
if (!Character->IsLocallyControlled())
{
// 애니메이션만 재생
Character->PlayFireMontage(bAiming);
EquippedWeapon->PlayFireMontage();
}
// 모두에게 Projectile 스폰
EquippedWeapon->Fire(TraceHitTarget);
}
서버는 클라이언트들에게 해당 사항을 뿌려준다.
클라이언트들은 자신이 아닌 플레이어가 사격한 경우 애니메이션을 재생, Projectile 스폰은 사격한 플레이어를 포함 모든 클라이언트에게 스폰된다.
void AProjectile::OnHit(UPrimitiveComponent* HitComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
FVector NormalImpulse,
const FHitResult& Hit)
{
if (ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(OtherActor))
{
BlasterCharacter->MulticastHit();
}
Destroy();
}
void ABlasterCharacter::MulticastHit_Implementation()
{
PlayHitReactMontage();
}
void ABlasterCharacter::PlayHitReactMontage()
{
if (!IsWeaponEquipped()) return;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && HitReactMontage)
{
AnimInstance->Montage_Play(HitReactMontage);
const FName SectionName("FromFront");
AnimInstance->Montage_JumpToSection(SectionName);
}
}
이제 Projectile에 대한 모든 권한은 서버가 갖고 있다.
즉, 히트 이벤트는 서버만 알 수 있게 됐다.
Multicast를 바로 호출할 수 있으며, 모두가 HitReact 애니메이션을 재생할 수 있다.
Destroy도 호출 시 모든 클라이언트에게 전파될 것이다.

위 사진은 오른쪽의 사격하는 클라이언트가 1500의 핑을 갖고 있다.
사격 시 애니메이션은 바로 재생하나 Projectile 스폰은 한참 후에 된다.
이것이 의미하는 바는, 클라이언트마다 피격에 대한 정보가 다를 일이 없단 얘기가 되었다.
피했으면 피한 거고, 맞았으면 맞은 거다.
사실 뚝딱 만들어낸 건 아니고, 하루 정도 뻘짓을 했다.
클라이언트가 권한을 갖고 있는 Projectile에서 서버에 요청하는 함수를 호출해보기도 하고(당연히 작동 안 됐다), 앞서 말한 사격한 클라이언트에서만 Projectile이 보이는 방식도 구현해보려고 했다.
그래도 권한에 대한 확실한 이해를 갖게 되었으니 좋은 경험이었다.
이제 HasAuthority를 적절히 사용할 수 있는 지식을 갖게 되었다.
클라이언트에서 스폰된 액터에서 HasAuthority를 호출하면 서버와 다른 클라이언트는 false를 갖고, 서버에서 스폰된 액터에서 HasAuthority를 호출하면 모든 클라이언트가 false를 갖게 된다.
Multicast를 할 때 적절히 사용해주면 Character의 IsLocallyControlled처럼 좋은 필터링 함수로 쓸 수 있다.