저번에 구현한 발사 및 총알 동기화에 이어서 재장전 또한 동기화를 해보도록 하겠습니다.
재장전 또한 Fire 로직과 비슷하게 lastReloadStartTick 만으로 처리하기에는 부족한 부분이 많습니다.
특히, Reload의 경우에는 중간에 끊기는 경우 또한 고려를 해야하기 때문에, Tick과 더불어 현재 Reload 중인지 또한 기록을 해주어야 합니다.
우선, 기존 WeaponState 구조체에 두 필드를 추가해줍니다.
public int reloadStartTick; // Reconcile
public bool isReloading; // Reconcile
이후에 WeaponSystem의 Simulate 함수에서 Reload가 가능한지 판단을 하고, Fire는 되지 않되 recoil된 aim은 제자리로 돌아올 수 있도록 만들어 보겠습니다.
우선, 기존에 사용하던 tick 방식을 수정하겠습니다.
private int IncreaseTick()
{
currentTick = (currentTick + 1) % Constants.BUFFER_SIZE;
return currentTick;
}
기존에 사용하던 방식은 위와 같이 int 값을 BUFFER_SIZE 보다 작게 유지하는 방식으로 구현하였습니다.
하지만 이러한 방식은 tick간의 정확한 차이를 알기 어렵기 때문에, 인덱스로 사용할 때에만 모듈러 함수를 사용하도록 수정했습니다.
public static int ToIndex(this int tick)
{
return tick % Constants.BUFFER_SIZE;
}
이제 SimualteWeapon 함수에 재장전 로직을 추가해보겠습니다.
/* Reload */
bool canReload = input.isReload
&& state.ammoInMagazine < currentWeapon.Spec.MagazineSize
&& state.reserveAmmo > 0
&& !state.isReloading;
if (canReload)
{
state.reloadStartTick = input.tick;
state.isReloading = true;
}
if (state.isReloading && input.tick - state.reloadStartTick >= currentWeapon.Spec.ReloadTick)
{
int reloadAmount = Mathf.Min(currentWeapon.Spec.MagazineSize - state.ammoInMagazine, state.reserveAmmo);
state.ammoInMagazine += reloadAmount;
state.reserveAmmo -= reloadAmount;
state.isReloading = false;
}
CSP이기 때문에 총알과 리로드 틱에 관련한 코드만 작성하였습니다.
우선 여기까지 테스트를 해보도록 하겠습니다.

제대로 작동되는 모습을 확인했으니, 이제 서버에서 동일하게 시뮬레이션을 하고 Snapshot을 받아오도록 하겠습니다.
public static NetworkWeaponState SimulateWeaponSimple(
GunBase currentWeapon,
NetworkWeaponState state,
PlayerInput input)
{
int tickBetweenShots = Mathf.RoundToInt(Constants.TICK_RATE / currentWeapon.Spec.FireRate);
/* Reload */
bool canReload = input.isReload
&& state.ammoInMagazine < currentWeapon.Spec.MagazineSize
&& state.reserveAmmo > 0
&& !state.isReloading;
if (canReload)
{
state.reloadStartTick = input.tick;
state.isReloading = true;
}
if (state.isReloading && input.tick - state.reloadStartTick >= currentWeapon.Spec.ReloadTick)
{
int reloadAmount = Mathf.Min(currentWeapon.Spec.MagazineSize - state.ammoInMagazine, state.reserveAmmo);
state.ammoInMagazine += reloadAmount;
state.reserveAmmo -= reloadAmount;
state.isReloading = false;
}
bool canFire = input.isFired
&& (input.tick - state.lastFiredTick) >= tickBetweenShots
&& state.ammoInMagazine > 0
&& !state.isReloading;
if (canFire)
{
state.ammoInMagazine--;
state.lastFiredTick = input.tick;
}
return state;
}
클라이언트의 SimulateWeapon 함수에서 Recoil 관련 로직을 제거한 SimulateWeaponSimple 함수입니다.
서버에서는 해당 함수를 통해 각 클라이언트들의 Weapon 상태를 유지합니다.
해당 함수로 서버에서 시뮬레이션을 돌린 뒤, 클라이언트에 snapshot을 보내서 항상 서버에서의 상태와 동일하도록 만들어 줍니다.
아래는 테스트 결과입니다.
서버와 클라이언트에서 재장전의 시작과 끝이 동일한 시점에 호출되는 것을 확인할 수 있습니다.


또한, 클라이언트에서 재장전 시간을 임의로 조작했을 경우를 테스트해보겠습니다.

예측과 서버 결과간의 충돌이 일어날 줄 알았지만, 별 충돌 없이 항상 권위적인 상태로만 플레이되는 것을 확인하실 수 있습니다.
재장전 로직을 서버권위적으로 구현해 보았습니다.
특히 발사와 같이 Event 로직이 아닌, 지속되는 상태를 동기화하려다 보니 약간 다르게 생각해야하는 부분이 있었습니다.
다음은 재장전 애니메이션을 추가하고,
Lag Compensation을 통해 네트워크 지연이 있는 상태에서 클라이언트간 실시간 이벤트를 어떻게 처리하는지에 대해서 알아보겠습니다.