【UE4C++-ActionRougelike-22】问题修复——约束客户端的权限
这一部分是对先前各部分游戏功能模块在多人游戏中的一些Bug修复,很多问题是客户端和服务器端不同步,本质上是客户端的权限过大,很多行为操作应当只放在服务器端上执行。
一、攻击动作不能同步到客户端
1、问题情况
问题:这里将先前编写的Action_MagicProjectile添加到Player的ActionComp组件中的Default Actions数组后,会发现执行攻击后,魔法攻击抛射物能够正常在服务器和客户端生成,但是攻击动画不会同步到客户端。
原因:
- 先前的执行Action回调函数写的有问题,StartAction方法传入的是空,客户端在执行MyAction_ProjectileAttack的StartAction时由于传入的Instigator为空,所以无法正常播放攻击动画。
- 在多人游戏网络同步时我们就已经在生成抛射物MyMagicProjectile的父类MyProjectileBase的构造函数中设置了SetReplicates(true),所以魔法攻击抛射物能够同步给客户端。
void UMyAction::OnRep_IsRunning()
{
//如果bIsRunning为真,则会调用该MyAction的StartAction从而更新本地客户端这个MyAction的动作
if (bIsRunning)
{
StartAction(nullptr);
}
else
{
StopAction(nullptr);
}
}
//服务器端执行这个函数时会触发定时器,从而调用AttackDelay_Elapsed生成抛射物类
//但是其他客户端在执行该函数时,传入的Instigator是null,所以不能够正常播放动画
void UMyAction_ProjectileAttack::StartAction_Implementation(AActor* Instigator)
{
Super::StartAction_Implementation(Instigator);
// 如果 Instigator 是 ACharacter 类型,则进行攻击
ACharacter* Character = Cast<ACharacter>(Instigator);
if (Character)
{
//播放攻击动画
Character->PlayAnimMontage(AttackAnim);
// 在手部 socket 上播放粒子特效
UGameplayStatics::SpawnEmitterAttached(CastingEffect, Character->GetMesh(), HandSocketName, FVector::ZeroVector, FRotator::ZeroRotator, EAttachLocation::SnapToTarget);
// 创建一个定时器,在攻击动画延迟之后调用 AttackDelay_Elapsed 函数
FTimerHandle TimerHandle_AttackDelay;
FTimerDelegate Delegate;
Delegate.BindUFunction(this, "AttackDelay_Elapsed", Character);
GetWorld()->GetTimerManager().SetTimer(TimerHandle_AttackDelay, Delegate, AttackAnimDelay, false);
}
}
2、解决方案
在MyAction.h中添加结构体FActionRepData来表示服务器发回客户端的Action数据,包含该Action是否运行,以及该Action的发起者Instigator。
USTRUCT()
struct FActionRepData
{
GENERATED_BODY()
public:
UPROPERTY()
bool bIsRunning;
UPROPERTY()
AActor* Instigator;
};
UCLASS(Blueprintable)
class ACTIONROGUELIKE_API UMyAction : public UObject
{
//RepData属性将进行网络复制,当该属性发生变化时(由服务器接收到更新),将调用名为OnRep_RepData的函数
UPROPERTY(ReplicatedUsing="OnRep_RepData")
FActionRepData RepData;
UFUNCTION()
void OnRep_RepData();
}
在MyAction.cpp中的StartAction和StopAction除了要更新bIsRunning,也要更新Instigator。这样在回调函数执行OnRep_RepData时,可以将RepData.Instigator作为参数传入。
void UMyAction::StartAction_Implementation(AActor* Instigator)
{
//...
// 将RepData.bIsRunning设置为true,表示UMyAction正在运行
RepData.bIsRunning = true;
// 将RepData.Instigator设置为Instigator
RepData.Instigator = Instigator;
}
void UMyAction::StopAction_Implementation(AActor* Instigator)
{
//...
// 将RepData.bIsRunning设置为false,表示UMyAction停止运行
RepData.bIsRunning = false;
// 将RepData.Instigator设置为Instigator
RepData.Instigator = Instigator;
}
//被服务器告知RepData属性变化后的回调函数,根据RepData.bIsRunning来更新是否执行该MyAction动作
void UMyAction::OnRep_RepData()
{
//如果RepData.bIsRunning为真,则会调用该MyAction的StartAction从而更新本地客户端这个MyAction的动作
if (RepData.bIsRunning)
{
StartAction(RepData.Instigator);
}
else
{
StopAction(RepData.Instigator);
}
}
3、效果演示
二、客户端生成两次抛射物
1、问题情况
我们修复了上面的问题后,会产生一个新的问题:客户端会生成两次抛射物。当服务器端攻击时,可以看到客户端产生了两个抛射物。
原因:
- 当服务器端告知客户端RepData变化时,会调用客户端的StartAction,本地客户端在执行完播放动画后,会触发一个定时委托调用 AttackDelay_Elapsed,产生一个抛射物。
- 我们先前又已经将抛射物复制,所以服务器也会同步生成一个抛射物到客户端。
2、解决方案
在MyAction_ProjectileAttack的StartAction中只允许服务器端调用AttackDelay_Elapsed产生抛射物
//实现攻击能力的逻辑
void UMyAction_ProjectileAttack::StartAction_Implementation(AActor* Instigator)
{
Super::StartAction_Implementation(Instigator);
// 如果 Instigator 是 ACharacter 类型,则进行攻击
ACharacter* Character = Cast<ACharacter>(Instigator);
if (Character)
{
//播放攻击动画
Character->PlayAnimMontage(AttackAnim);
// 在手部 socket 上播放粒子特效
UGameplayStatics::SpawnEmitterAttached(CastingEffect, Character->GetMesh(), HandSocketName, FVector::ZeroVector, FRotator::ZeroRotator, EAttachLocation::SnapToTarget);
//只在服务器端调用AttackDelay_Elapsed产生抛射物,通过复制同步到客户端
if (Character->HasAuthority())
{
// 创建一个定时器,在攻击动画延迟之后调用 AttackDelay_Elapsed 函数
FTimerHandle TimerHandle_AttackDelay;
FTimerDelegate Delegate;
Delegate.BindUFunction(this, "AttackDelay_Elapsed", Character);
GetWorld()->GetTimerManager().SetTimer(TimerHandle_AttackDelay, Delegate, AttackAnimDelay, false);
}
}
}
三、限制客户端伤害计算
1、问题情况
问题:在问题二未被修复情况下,服务器攻击后,其中客户端产生的抛射物命中客户端角色后,本地客户端角色依然会造成伤害,但服务器端的客户端角色并不会收到伤害。可以看到客户端本地显示掉血,服务器端上的的客户端角色并不掉血。
原因:
- 因为我们在属性组件中的ApplyHealthChange方法中,健康值计算都是在客户端和服务器端都会执行
- 本地客户端产生的抛射物与本地客户端角色碰撞后进行伤害计算,所以在本地造成了伤害。
- 在服务器端是没有这个抛射物的,因此服务器端,客户端角色并未受到伤害
2、解决方案
使伤害计算仅在服务器端执行
bool UMyAttributeComponent::ApplyHealthChange(AActor* InstigatorActor, float Delta)
{
if (!GetOwner()->CanBeDamaged() && Delta < 0.0f)
{
return false;
}
// 如果 Delta 值小于 0,即表示要对角色造成伤害
if (Delta < 0.0f)
{
// 获取 CVarDamageMultiplier 控制台变量在游戏线程上的值
float DamageMultiplier = CVarDamageMultiplier.GetValueOnGameThread();
// 根据全局伤害乘数调整 Delta 值
Delta *= DamageMultiplier;
}
float OldHealth = Health;
float NewHealth = FMath::Clamp(Health + Delta, 0.0f, MaxHealth);
float ActualDelta = NewHealth - OldHealth;
//只在服务器端进行伤害计算
if (GetOwner()->HasAuthority())
{
Health = NewHealth;
if (ActualDelta != 0.0f)
{
//如果实际变化量不为0,则通知客户端
MulticastHealthChanged(InstigatorActor, Health, ActualDelta);
}
// 死亡
if (ActualDelta < 0.0f && Health == 0.0f)
{
AMyGameModeBase* GM = GetWorld()->GetAuthGameMode<AMyGameModeBase>();
if (GM)
{
GM->OnActorKilled(GetOwner(), InstigatorActor);
}
}
}
return ActualDelta != 0;
}
3、效果演示
此时客户端不会再进行伤害计算,我们将所有伤害计算放在了服务器端。
四、加速能力Bug
注:先前添加Action行为是服务器和客户端都允许执行的,但添加Action应当只让服务器端进行,要限制客户端添加行为Action
void UMyActionComponent::AddAction(AActor* Instigator, TSubclassOf<UMyAction> ActionClass)
{
if (!ensure(ActionClass))
{
return;
}
//只允许服务器端进行Action的添加
if (!GetOwner()->HasAuthority())
{
UE_LOG(LogTemp, Warning, TEXT("Client attempting to AddAction. [Class: %s]"), *GetNameSafe(ActionClass));
return;
}
// 创建一个新的动作对象添加到 Actions 数组中
//...
}
void AMyMagicProjectile::OnActorOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// 如果重叠的物体不是发射该魔法弹道的角色本身
if (OtherActor && OtherActor != GetInstigator())
{
//...
// 对重叠的物体施加方向性伤害,如果该物体受到伤害,则执行 Explode() 函数
if (UMyGameplayFunctionLibrary::ApplyDirectionalDamage(GetInstigator(), OtherActor, DamageAmount, SweepResult))
{
Explode();
//只有服务器端才能施加燃烧效果
if (ActionComp && BurningActionClass && HasAuthority())
{
ActionComp->AddAction(GetInstigator(), BurningActionClass);
}
}
}
}
1、问题情况
问题:客户端角色按Shift键进行加速后,松下Shift键后,服务器端客户端角色仍会以加速状态移动
原因:在停止Action时,没有向服务器端发送请求
//修改前
bool UMyActionComponent::StopActionByName(AActor* Instigator, FName ActionName)
{
// 遍历 Actions 数组,查找并停止指定名称的动作
for (UMyAction* Action : Actions)
{
if (Action && Action->ActionName == ActionName)
{
if (Action->IsRunning())
{
Action->StopAction(Instigator);
return true;
}
}
}
return false;
}
2、解决方案
在停止Action时也要通过一个服务端RPC函数告诉服务器停止Action
//MyActionComponent.h
UFUNCTION(Server, Reliable)
void ServerStopAction(AActor* Instigator, FName ActionName);
//MyActionComponent.cpp
// 实现服务器 RPC 函数 ServerStopAction,调用 StopActionByName 启动动作
void UMyActionComponent::ServerStopAction_Implementation(AActor* Instigator, FName ActionName)
{
StopActionByName(Instigator, ActionName);
}
//根据名称停止一个动作
bool UMyActionComponent::StopActionByName(AActor* Instigator, FName ActionName)
{
// 遍历 Actions 数组,查找并停止指定名称的动作
for (UMyAction* Action : Actions)
{
if (Action && Action->ActionName == ActionName)
{
if (Action->IsRunning())
{
// 如果当前组件不在服务器上,向服务器发送 RPC 启动动作
if (!GetOwner()->HasAuthority())
{
ServerStopAction(Instigator, ActionName);
}
Action->StopAction(Instigator);
return true;
}
}
}
return false;
}
五、死亡后血量条UI不刷新问题
1、问题情况
问题:当玩家死亡后,血量条UI不会更新,仍然是死亡时的空白状态
原因:血量UI控件PlayerHealth_Widget是与我们的PlayerPawn绑定的,当玩家死亡重生后,会产生一个新的Pawn,但先前的PlayerHealth_Widget并没有绑定到新的Pawn上进行血量UI变化,所以血量条会一直显示死亡的空白状态
2、解决方案
解决思路:当玩家控制器切换Pawn时,我们更新玩家的PlayerHealth_Widget
2.1 创建玩家控制器类
以PlayerController为父类创建游戏玩家控制类MyPlayerController,声明一个动态多播委托用于玩家Pawn改变时触发委托。
// 声明一个具有一个参数的动态多播委托类型FOnPawnChanged,当玩家Pawn变化时触发
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPawnChanged, APawn*, NewPawn);
UCLASS()
class ACTIONROGUELIKE_API AMyPlayerController : public APlayerController
{
GENERATED_BODY()
protected:
UPROPERTY(BlueprintAssignable)
FOnPawnChanged OnPawnChanged;
virtual void SetPawn(APawn* InPawn) override;
};
void AMyPlayerController::SetPawn(APawn* InPawn)
{
Super::SetPawn(InPawn);
// 广播OnPawnChanged事件,将新设置的角色作为参数
OnPawnChanged.Broadcast(InPawn);
}
还要再PlayerController_BP的Class Setting中将父类设置为MyPlayerController
2.2 玩家切换Pawn后更新UI
在PlayerHealth_Widget蓝图中设置当Pawn修改时,将新的Pawn用来为PlayerHealth_Widget更新血量UI