【UE4C++-ActionRougelike-22】问题修复——约束客户端的权限


【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执行情况

原因:在停止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问题

原因:血量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

血量UI蓝图设置

3、效果演示

血量UI问题修复

评论
  目录