跳至主要內容

NT-2.2|网络权威

Mr.Si大约 7 分钟unreal

书接上文

头像
前文已经实践了RPC的调用、Actor的复制、变量的复制等。

问题

头像
实际开发中,往往不可避免的使用UI显示一些数据,我偷懒用了子系统,却发现扣血后所有端都扣血了!
GIF

void AExorcistHeroCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);
	InitAbilityActorInfo();
	InitAbilities();
	
}

void AExorcistHeroCharacter::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();
	InitAbilityActorInfo();
}
void AExorcistHeroCharacter::InitAbilityActorInfo()
{
	AExorcistPlayerState* ExorcistPlayerState =GetPlayerState<AExorcistPlayerState>();
	
	if(!ExorcistPlayerState)
	{
		return;
	}
	
	//利用PlayerState初始化组件
	ExorcistPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(ExorcistPlayerState, this);

	//赋值指针
	AbilitySystemComponent = ExorcistPlayerState->GetAbilitySystemComponent();
	AttributeSet = ExorcistPlayerState->GetAttributeSet();
	
	UUIManagerSubsystem * UIManagerSubsystem = UGameplayStatics::GetGameInstance(this)->GetSubsystem<UUIManagerSubsystem>();
	if(UIManagerSubsystem)
	{
		UIManagerSubsystem->ExecuteBindAttributeInfo(AbilitySystemComponent);
	}
}
头像
这与子系统本身无关,子系统和普通UObject一样并不是什么潘多拉。看来咱们对网络权威必须做一次强化认知

预热

Lyra的UI框架

头像
Lyra中使用UI子系统维护UI,其大致框架是这样的,具体的讨论在Lyra篇中再做讨论。

PossessedBy

一切从PlayerJoin后的PossessedBy开始说起

头像
Character继承自Pawn,Pawn继承自Actor,玩家控制器获取某个Pawn的控制权时,会调用PossessedBy。
头像
通过源码追溯,PossessedBy接口最早出现在Pawn类中。记住这里的注释 Only called on the server,意味着这个函数 只会在服务器上执行。
	/** 
	 * Called when this Pawn is possessed. Only called on the server (or in standalone).
	 * @param NewController The controller possessing this pawn
	 */
	virtual void PossessedBy(AController* NewController);
头像
换句话说,这意味着所有能有效调用PossessedBy的都会在服务器上执行。

AIController

头像
可是为什么我在场景中放入Pawn也会打印PossessedBy?
头像

事实上所有放入场景的Pawn,即便没人去控制也会由AI控制器代理执行。这里如果不清楚控制器和Pawn关系的童鞋,可以回去看看

Gameplay框架导言

头像
通过禁用控制器也可以验证这一点。

推荐用IDE直接打断点,不过这种方法也不是不可取。

void AExorcistHeroCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);
	UE_LOG(LogTemp, Warning, TEXT("PossessedBy %d"), count);
	InitAbilityActorInfo();
}

InitAbilityActorInfo()中写了计数函数主要负责++处理,以及获取玩家PlayerState

头像
如果不出意外,运行后只会打印一次。
头像
现在我们在场景中丢个Pawn进去(非玩家角色),AI控制器会自动接管一个,所以会打印两次。

PlayerState

头像
所以PossessedBy处理获取玩家PlayerState的时要额外小心,因为AI是没有PlayerState的,会导致内存异常。
头像
现在我们考虑客户端加入的情况。
头像
可以看到主机和客户端各自都会执行一次自己的PossessBy

OnRep_PlayerState

头像
有意思的来了,OnRep_PlayerState被执行了多少次?
void AExorcistHeroCharacter::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();
	UE_LOG(LogTemp, Warning, TEXT("OnRep_PlayerState %d"), count);
	InitAbilityActorInfo();
}
头像
看到我头皮发麻,所以到底执行了几次?
头像
公开计数变量,看看各自的Character中都执行了几次。
	UPROPERTY(EditAnywhere,BlueprintReadWrite)
	int32 count = 0
头像
无论是客户端还是服务器,各自拥有的Character都执行了一次OnRep_PlayerState();
GIF
头像

网路同步02就提过, 变量的回调RepNotify|OnRepC++中会在变量本身改变时触发。 每次有新的PlayerJoin都会执行一次OnRep_PlayerState。

头像
问题来了,我在子系统中写了一些UI委托转发逻辑,一个客户端触发了所有客户端都跟着触发了怎么解决?
GIF
头像
还是先看看你的子系统中函数被调用了几次再说吧!
GIF
头像
好家伙!重复调用了!
头像
而且你咋总是不认真听课呢!都说了OnRep_PlayerState变化通知回调函数,一旦有变化就会执行,一旦有新PlayerJoin就会执行。
头像
我明白了!这不就是咱之前打印的流程嘛!

强化认知

RunOnServer

  1. 先去掉OnRep_PlayerState回调函数,并在子系统中加入一个测试函数。
void AExorcistHeroCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);
	InitAbilityActorInfo();
	
}

void AExorcistHeroCharacter::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();
	//InitAbilityActorInfo();
}
void AExorcistHeroCharacter::InitAbilityActorInfo()
{
	AExorcistPlayerState* ExorcistPlayerState =GetPlayerState<AExorcistPlayerState>();
	
	if(!ExorcistPlayerState)
	{
		return;
	}
	
	//利用PlayerState初始化组件
	ExorcistPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(ExorcistPlayerState, this);

	//赋值指针
	AbilitySystemComponent = ExorcistPlayerState->GetAbilitySystemComponent();
	AttributeSet = ExorcistPlayerState->GetAttributeSet();
	
	UUIManagerSubsystem * UIManagerSubsystem = UGameplayStatics::GetGameInstance(this)->GetSubsystem<UUIManagerSubsystem>();
	if(UIManagerSubsystem)
	{
		UIManagerSubsystem->SetTest();
	}
}
//子系统
public:
UPROPERTY(VisibleAnywhere,BlueprintReadWrite)
	int32 count = 0;

UFUNCTION(BlueprintCallable, Category = "UIManagerSubsystem")
void SetTest();
  1. 打开UE,监听服务器模式,玩家设置为2.
头像
借助UE引擎调试,可以看到服务器上UIManagerSubsystem执行了2次,cout++后变成2.
头像
注意这里指的是服务器上的UIManagerSubsystem被执行了2次!
头像
UIManagerSubsystem本身不能复制,所以其他端口上调试可以看到count变量依然是0.
头像
咱们还可以康康服务器上是不是真的将对应的APlayerController加入进去了
UFUNCTION(BlueprintCallable, Category = "UIManagerSubsystem")
void SetTest(APlayerController * LocalPlayerController);

UPROPERTY(VisibleAnywhere,BlueprintReadWrite)
TArray<APlayerController*> PlayerControllers;
头像
现在咱们手动测试一下服务端是不是真的拥有生杀大权——让客户端解除控制。
void UUIManagerSubsystem::Kill()
{
	PlayerControllers[1]->UnPossess();
}
GIF
头像
可以可以!我好像明白了一切!

OnRep_PlayerState

头像
目前服务器上对每个玩家初始化了对应的GAS组件,可是这些组件对于服务器来说毫无价值。所以,应该放权复制到对应的 客户端上。一个很好的媒介就是每个玩家加入时PlayerState会刷新。
void AExorcistHeroCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);
	InitAbilityActorInfo();

}

void AExorcistHeroCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
InitAbilityActorInfo();
}
void AExorcistHeroCharacter::InitAbilityActorInfo()
{
AExorcistPlayerState* ExorcistPlayerState =GetPlayerState<AExorcistPlayerState>();

	if(!ExorcistPlayerState)
	{
		return;
	}
	
	//利用PlayerState初始化组件
	ExorcistPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(ExorcistPlayerState, this);

	//赋值指针
	AbilitySystemComponent = ExorcistPlayerState->GetAbilitySystemComponent();
	AttributeSet = ExorcistPlayerState->GetAttributeSet();
	if(IsLocallyControlled())
	{	
        UUIManagerSubsystem * UIManagerSubsystem = UGameplayStatics::GetGameInstance(this)->GetSubsystem<UUIManagerSubsystem>();
        if(UIManagerSubsystem)
        {
          //UI绑定函数
        }
	}
}
头像
是的,通过OnRep_PlayerState可以将每个玩家对应的GAS组件进行初始化。大致流程如下:
  1. 服务器上PossessedByA
  2. 服务器上执行InitAbilityActorInfo初始化A的ASC组件
  3. 看到IsLocallyControlled =true ,所以调用子系统UIManagerSubsystem绑定对应的AS委托
  4. 接着join New Player B
  5. 服务器上PossessedByB
  6. 服务器上执行InitAbilityActorInfo初始化B的ASC组件
  7. 服务器上B看到IsLocallyControlled为False,所以没有执行子系统绑定
  8. PossessedByB导致PlayerState变量更新,通过OnRep_PlayerState告诉对应的客户端。
  9. 对于客户端B来说OnRep_PlayerState的通知函数告诉他执行InitAbilityActorInfo, 此时他看到IsLocallyControlled = true, 所以成功调用了子系统将ASC的数据委托绑定到了本地的UI上。
头像
妙啊!这样对应端口的UI都绑定了对应的数据。
头像
绑定函数就好写多了
UUIManagerSubsystem * UIManagerSubsystem = UGameplayStatics::GetGameInstance(this)->GetSubsystem<UUIManagerSubsystem>();
if(UIManagerSubsystem && IsLocallyControlled())
{
		UIManagerSubsystem->InitAsc(AbilitySystemComponent);
		UIManagerSubsystem->BindAttributeInfo();
	    UIManagerSubsystem->NotifyUIShowAttributeInfo();
}
void UUIManagerSubsystem::InitAsc(
	UAbilitySystemComponent* TargetASC)
{
	if (!TargetASC) return;
	AbilitySystemComponent = TargetASC;
}
void UUIManagerSubsystem::BindAttributeInfo()
{
	if (!AbilitySystemComponent) return;
	AbilitySystemComponent->GetAllAttributes(OutAttributes);
	if (OutAttributes.Num() > 0)
	{
		for (auto const Attribute : OutAttributes)
		{
			GEngine->AddOnScreenDebugMessage(-1, 2.f, FColor::Red, Attribute.GetName());
			AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddLambda(
				[this](const FOnAttributeChangeData& OnAttributeChangeData)
				{
					OnAttributeChanged.Broadcast(OnAttributeChangeData.Attribute, OnAttributeChangeData.NewValue);
				}
			);
		}
	}
}

void UUIManagerSubsystem::NotifyUIShowAttributeInfo()
{
	if (OutAttributes.Num() > 0 && AbilitySystemComponent)
	{
		for (auto Attribute : OutAttributes)
		{
			OnAttributeChanged.Broadcast(Attribute, AbilitySystemComponent->GetNumericAttributeBase(Attribute));
		}
	}
}

UI过滤

IsLocallyControlled

头像
有了上面的强化认知后,本期的问题已经解决,核心思路就是IsLocallyControlled 过滤非本地玩家的指针