강의에서도 발생했던 문제 수정!

김지윤·2025년 4월 7일

UE5_MultiplayerShooter

목록 보기
22/25

정신 나가는 줄 알았다.
버그 발생 과정은 이렇다.

  1. 다른 클라이언트가 총을 사격한 뒤 장전한다.
  2. 해당 클라이언트의 캐릭터가 사망한 뒤 총을 드랍한다.
  3. 그 총을 주우면 장전이 반영되지 않아있다.

왼쪽이 서버, 오른쪽이 클라이언트다.
15발에서 장전, 30발이 되었는데 주워보면 15발이다.
디버그를 찍어보니 Client Side Prediction Ammo를 구현하다가 발생한 버그였다.
사격 즉시 클라이언트는 Ammo에서 1을 빼고, Sequence를 1 더한다.
그 뒤 서버에 사격을 요청, 서버도 Ammo에서 1을 빼고 해당 정보를 클라이언트들에게 뿌린다.
사격을 요청한 클라이언트 또한 그 정보를 받는다.
그럼 매개변수로 온 서버측의 Ammo를 자신의 Ammo에 반영, Sequence에서 1을 뺀 뒤 Ammo에서 Sequence를 뺀다.

말로 풀어놓으니 복잡하다. 코드로 보자.

void AWeapon::SpendRound()
{
	// 클라이언트와 서버 모두가 여기로 들어와 Ammo에 대한 관리를 시작
	Ammo = FMath::Clamp(Ammo - 1, 0, MagCapacity);
	SetHUDAmmo();
	if (HasAuthority())
	{
    	// 서버는 모든 클라이언트에게 Ammo를 뿌린다
		ClientUpdateAmmo(Ammo);
	}
	else
	{
    	// 사격을 요청한 클라이언트는 Sequence를 +1
		++Sequence;
	}
}

void AWeapon::ClientUpdateAmmo_Implementation(int32 ServerAmmo)
{
	// 서버가 보내준 Ammo를 받는 함수
	if (HasAuthority()) return;
	
	Ammo = ServerAmmo;
	--Sequence;
	Ammo -= Sequence;
	SetHUDAmmo();
}

위처럼 클라이언트 예측 탄창을 구현했다.
문제가 여기서 발생한 거다!
SpendRound는 LocalFire를 통해 들어온다.

  • 클라이언트A: LocalFire 후 ServerFire로 서버에 요청
  • 서버: ServerFire 후 MulticastFire로 모든 클라이언트에게 뿌림
  • 클라이언트B: MulticastFire로 받아 LocalFire를 호출

사격을 요청한 A뿐 아니라 B도 LocalFire를 호출한다!
그럼 B도 SpendRound를 통해 Ammo 관리를 시작, 당연히 Sequence가 1 늘어난다.
그런데 서버는 사격을 요청한 클라이언트에게만 Ammo를 알려준다.
이런! 다른 클라이언트가 총을 쏘면 Sequence가 영원히 늘어나게 된다.
총을 줍고 아무리 장전해봐도 30발이 채워지지 않게 된다.

내가 강의를 잘못 따라했나 싶어서 강의를 돌려보기도 하고, 머리를 싸매쥐고 4시간 동안 이것만 고치고 있었다.
여러 방법을 시도해보다가 아무리 봐도 강의랑 다른 부분이 없어서 '혹시 강의가 잘못된 거 아냐?' 싶었는데, 진짜 그랬다.
GitHub에서 원본 프로젝트를 받아봤는데 같은 현상이 있었다.
강사도 사람이니까 그럴 수 있다고 생각한다.(내가 구버전을 잘못 받았을 수도 있지만)
중요한 건 내 디버깅 실력이 늘었다는 거다.

어떻게 고쳤냐 하면..
잤다.
4시간 동안 머리 싸매쥐고 고민하다가 새벽 2시가 됐길래 그냥 잤는데..
아침에 양치하면서 떠올렸다.
'Sequence가 LocalFire 때문에 늘어났는데, 그게 줄어들질 않는구나!'
어쨌든 다른 클라이언트도 Ammo에 대한 정보를 알아야 하니까 SpendRound를 호출하지 않도록 할 순 없으니..
Sequence를 늘리지 않도록 해주고, 장전 시 모든 클라이언트가 알 수 있게 했다.

void AWeapon::SpendRound()
{
	Ammo = FMath::Clamp(Ammo - 1, 0, MagCapacity);
	SetHUDAmmo();
	if (HasAuthority())
	{
		ClientUpdateAmmo(Ammo);
	}
	else
	{
    	// 사격을 요청한 클라이언트만 Sequence 증가
		BlasterOwnerCharacter = BlasterOwnerCharacter == nullptr ? Cast<ABlasterCharacter>(GetOwner()) : BlasterOwnerCharacter;
		if (BlasterOwnerCharacter->IsLocallyControlled())
		{
			++Sequence;
		}
	}
}

void AWeapon::ClientUpdateAmmo_Implementation(int32 ServerAmmo)
{
	if (HasAuthority()) return;
	
	Ammo = ServerAmmo;
	--Sequence;
	Ammo -= Sequence;
	SetHUDAmmo();
}

void AWeapon::AddAmmo(int32 AmmoToAdd)
{
	// 장전 시 서버가 들어오는 함수
	Ammo = FMath::Clamp(Ammo + AmmoToAdd, 0, MagCapacity);
	SetHUDAmmo();
    // Client -> NetMulticast, 장전 요청한 클라이언트만이 아닌 모두에게 알려주도록 변경
	MulticastAddAmmo(AmmoToAdd);
}

void AWeapon::MulticastAddAmmo_Implementation(int32 AmmoToAdd)
{
	// 장전에 대한 처리는 그대로
	if (HasAuthority()) return;
	
	Ammo = FMath::Clamp(Ammo + AmmoToAdd, 0, MagCapacity);
	BlasterOwnerCharacter = BlasterOwnerCharacter == nullptr ? Cast<ABlasterCharacter>(GetOwner()) : BlasterOwnerCharacter;
	if (WeaponType == EWeaponType::EWT_Shotgun && BlasterOwnerCharacter && BlasterOwnerCharacter->GetCombat() && IsFull())
	{
		BlasterOwnerCharacter->GetCombat()->JumpToShotgunMoreReload(false);
	}
	SetHUDAmmo();
}

이걸로 고쳐졌다.

잘 안 될 땐 푹 쉬고 하는 것도 중요하다.
하지만 아침에 눈 뜨자마자 어제 못 고친 버그가 생각날 정도로 집요해지자.

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

0개의 댓글