写好之后,不管是服务器玩家,还是客户端玩家都调用SetCurrentAmmo()/SetAiming来设置预测的变量。
一、预测Int32变量,不需要复制到模拟代理的情况。
客户端预测当前子弹数量。每次射击,装填子弹都会对这个变量产生影响,我不希望客户端需要等待一段延迟再更新这个变量,所以对他进行客户端预测。
/**
* Client Predict CurrentAmmo
*/
void SetCurrentAmmo(int32 NewCurrentAmmo);
void PredictSetCurrentAmmo(int32 NewCurrentAmmo);
UFUNCTION(Server,Reliable)
void ServerSetCurrentAmmo(int32 NewCurrentAmmo);
UFUNCTION(Client,Reliable)
void ClientSetCurrentAmmo(int32 NewCurrentAmmo);
void ConsilateCurrentAmmo();
UFUNCTION()
void OnRep_CurrentAmmo(int32 OldNum);
void SetCurrentAmmoInternal(int32 NewCurrentAmmo);
UPROPERTY(BlueprintReadOnly)
int32 CurrentAmmo=20;
TArray<int32> PredictCurrentAmmoArray;
由于CurrentAmmo不需要复制到模拟代理上,而本地机器通过客户端预测进行复制,所以这里不把CurrentAmmo设置为ReplicatedUsing,下一个例子预测Aiming变量时会需要复制。
SetCurrentAmmo 在本地控制机器调用,会根据是服务器玩家还是客户端玩家选择直接设置变量还是预测地设置变量。
void AShooterGun::SetCurrentAmmo(int32 NewCurrentAmmo)
{
if(HasAuthority())
{
SetCurrentAmmoInternal(NewCurrentAmmo);
}else
{
PredictSetCurrentAmmo(NewCurrentAmmo);
}
}
PredictSetCurrentAmmo用于预测地设置CurrentAmmo变量,具体过程如下:
使用一个TArray<int32> PredictCurrentAmmoArray,每次预测,记录下新值与当前值的差(对存的是差,服务器协调步骤就是加上这个差)
void AShooterGun::PredictSetCurrentAmmo(int32 NewCurrentAmmo)
{
//Record Delta For Reconsilation
int32 Delta = NewCurrentAmmo - CurrentAmmo;
SetCurrentAmmoInternal(NewCurrentAmmo);
PredictCurrentAmmoArray.Add(Delta);
ServerSetCurrentAmmo(NewCurrentAmmo);
}
然后调用ServerSetCurrentAmmo,作用有两个,让服务器将值复制到模拟代理上(如果需要的话),并且把控最终值(进行数据验证)。服务器通过ClientSetCurrentAmmo函数将变量复制回OwnerClient.
void AShooterGun::ServerSetCurrentAmmo_Implementation(int32 NewCurrentAmmo)
{
//Validate Ammo Here
checkf(NewCurrentAmmo<=Capacity&&NewCurrentAmmo>=0,TEXT("Try Set Invalid NewCurrentAmmo!"));
SetCurrentAmmoInternal(NewCurrentAmmo);
ClientSetCurrentAmmo(NewCurrentAmmo);
}
OwnerClient收到RPC后,首先设置服务器复制过来的值,并且移除掉预测数组的第一个索引(使用RemoveAt()而不是Remove,他是Remove(Item)!)表示这个预测数据已经被复制到了。然后进行服务器协调
void AShooterGun::ClientSetCurrentAmmo_Implementation(int32 NewCurrentAmmo)
{
CurrentAmmo = NewCurrentAmmo;
//Remove(Item);
PredictCurrentAmmoArray.RemoveAt(0);
ConsilateCurrentAmmo();
}
服务器协调就是计算剩下的预测数据的总的差,然后加上。
void AShooterGun::ConsilateCurrentAmmo()
{
for (int32 Delta : PredictCurrentAmmoArray)
{
CurrentAmmo += Delta;
}
//Set To Self
SetCurrentAmmoInternal(CurrentAmmo);
}
然后在客户端直接设置CurrentAmmo的值,并且调用OnRep函数对修改作出反应(比如更新UI,修改变量值并作出反应被封装到SetCurrentAmmoInternal里面)
void AShooterGun::SetCurrentAmmoInternal(int32 NewCurrentAmmo)
{
int32 OldCurrentAmmo = CurrentAmmo;
CurrentAmmo = NewCurrentAmmo;
OnRep_CurrentAmmo(OldCurrentAmmo);
}
void AShooterGun::OnRep_CurrentAmmo(int32 OldNum)
{
ACharacter* OwnerCharacter = GetOwnerCharacter();
checkf(OwnerCharacter,TEXT("Empty OwnerCharacter"));
if(OwnerCharacter->IsLocallyControlled())
{
if(OnCurrentAmmoChanged.IsBound())
{
OnCurrentAmmoChanged.Execute();
}else
{
UE_LOG(LogTemp, Warning, TEXT("%s,CurrentAmmoChange Not Bound!"), *FString(__FUNCTION__))
}
}
}
完工
二、bool变量的客户端预测,需要复制到模拟代理的情况
/**
*Aim
*/
void AimPressed();
void AimReleased();
UPROPERTY(BlueprintReadOnly,ReplicatedUsing=OnRep_Aiming,Category = "Gun|Aim")
bool bAiming = false;
void SetAim(bool NewAim);
void PredictSetAim(bool NewAim);
UFUNCTION(Server,Reliable)
void ServerSetAiming(bool NewAim);
UFUNCTION(Client,Reliable)
void ClientRepAiming(bool NewAim);
void ConsilateAim();
UFUNCTION()
void OnRep_Aiming(bool OldAim);
bool SetAimingInternal(bool NewAim);
int PredictAimCount = 0;
bool CanAim();
UPROPERTY(EditAnywhere,Category = "Gun|Aim")
float AimWalkSpeed = 150.f;
同理,SetAim...
void AShooterCharacter::SetAim(bool NewAim)
{
if(HasAuthority())
{
SetAimingInternal(NewAim);
}else
{
PredictSetAim(NewAim);
}
//下面可以去掉
if(bAiming)
{
ZoomTimeLine->Play();
}else
{
ZoomTimeLine->Reverse();
}
}
另一个不同的是,由于布尔变量的差只有对自己取反,所以这里使用一个int32记录下取反了多少次就行,小小的优化。Int32 PredictAimCount = 0;在预测时对他进行++。
void AShooterCharacter::PredictSetAim(bool NewAim)
{
if(SetAimingInternal(NewAim))
{
PredictAimCount++;
ServerSetAiming(bAiming);
}
}
服务器检查,由于不是本地控制,所以不用SetAimingInternal,而是直接bAimng = NewAim
void AShooterCharacter::ServerSetAiming_Implementation(bool NewAim)
{
SetAimingInternal(NewAim);
ClientRepAiming(bAiming);
}
客户端收到后,就设置然后--,然后协调
void AShooterCharacter::ClientRepAiming_Implementation(bool NewAim)
{
bAiming = NewAim;
PredictAimCount--;
ConsilateAim();
}
协调也变成了取反若干次,然后SetAimingInternal;
void AShooterCharacter::ConsilateAim()
{
for(int i = 0;i<PredictAimCount;i++)
{
bAiming = !bAiming;
}
SetAimingInternal(bAiming);
}
void AShooterCharacter::OnRep_Aiming(bool OldAim)
{
if(bAiming)
{
GetCharacterMovement()->MaxWalkSpeed = AimWalkSpeed;
}else
{
GetCharacterMovement()->MaxWalkSpeed = DefaultWalkSpeed;
}
}
bool AShooterCharacter::CanAim()
{
return EquippedGun!=nullptr&&!bReloading;
}
bool AShooterCharacter::SetAimingInternal(bool NewAim)
{
if(NewAim&&!CanAim()) return false;
bool OldAim = bAiming;
bAiming = NewAim;
OnRep_Aiming(OldAim);
return true;
}
PredictAim...->ServerSetAim...->ClientRepAim...->OnRep_Aim...->ConsilateAim...
不同的是由于要将变量复制到模拟代理,用于驱动动画状态机,所以设置bAiming变量为ReplicatedUsing,条件设置为跳过拥有者,即将变量复制,不同的是,本地控制的机器,我自己进行客户端预测。模拟代理照常复制就行。
三、客户端预测播放动画,调用PredictPlayReloadMontage
void PredictPlayReloadMontage(bool bInAiming);
//Montage Driven Event RPC Set To Reliable
UFUNCTION(Server,Reliable)
void ServerPlayReloadMontage(bool bInAiming);
UFUNCTION(NetMulticast,Reliable)
void MultiPlayReloadMontageSkipOwner(bool bInAiming);
void PlayReloadMontageInternal(bool bInAiming);
void AShooterGun::PredictPlayReloadMontage(bool bInAiming)
{
PlayReloadMontageInternal(bInAiming);
ServerPlayReloadMontage(bInAiming);
}
void AShooterGun::ServerPlayReloadMontage_Implementation(bool bInAiming)
{
MultiPlayReloadMontageSkipOwner(bInAiming);
}
void AShooterGun::MultiPlayReloadMontageSkipOwner_Implementation(bool bInAiming)
{
if(ACharacter* OwnerCharacter = GetOwnerCharacter())
{
if(!OwnerCharacter->IsLocallyControlled())
{
PlayReloadMontageInternal(bInAiming);
}
}
}
void AShooterGun::PlayReloadMontageInternal(bool bInAiming)
{
ACharacter* OwnerCharacter = GetOwnerCharacter();
checkf(OwnerCharacter,TEXT("Try Play Reload Montage On Empty Owner!"));
if(OwnerReloadMontage_Aim&&OwnerReloadMontage_Hip&&GunReloadAnim)
{
if(UAnimMontage* ChoosenReloadMontage = bInAiming?OwnerReloadMontage_Aim:OwnerReloadMontage_Hip)
{
OwnerCharacter->GetMesh()->GetAnimInstance()->Montage_Play(ChoosenReloadMontage);
SkeletalMesh->PlayAnimation(GunReloadAnim,false);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("%s,Empty Reload Anim!"), *FString(__FUNCTION__))
}
}