前言

今天完成了教程的 38 - 41 小节,对应 GitHub 上 2022-2-20 这一天的提交,主要涉及角色与武器的网络状态同步,关于网络部分知识点的初步整理放在了 UE 入坑系列(三):UE 网络入门 中,本文将会结合目前的项目分析一下这些网络相关功能的使用。

OnRep_Notify

在目前的项目中,设置武器的状态为 Relipcated,并且在发生改变时调用 OnRep_WeaponState,主要用于关闭提示信息的显示。而 SetWeaponState 则是在捡起武器时调用,负责关闭碰撞以及关闭 UI 显示。需要注意的是,由于在项目中设置了 “拾取范围碰撞仅在服务器端开启” ,因此这个函数仅会在服务器端调用,同时也仅会关闭服务器端的 UI 显示。因此,服务器端的显示不会有问题,是直接通过碰撞检测进行函数调用的;而客户端的 UI 显示取决于 OnRep_WeaponState,毕竟客户端不会有真正的(拾取)碰撞。

1
2
UPROPERTY(ReplicatedUsing = OnRep_WeaponState, VisibleAnywhere, Category = "Weapon Properties")
EWeaponState WeaponState;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void AWeapon::SetWeaponState(EWeaponState State)
{
WeaponState = State;
switch (WeaponState)
{
case EWeaponState::EWS_Equipped:
ShowPickupWidget(false);
// AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
break;
}
}

/// @brief 武器状态同步到客户端
void AWeapon::OnRep_WeaponState()
{
switch (WeaponState)
{
case EWeaponState::EWS_Equipped:
// ShowPickupWidget(false);
break;
}
}

为了验证这一点,我们将 OnRep_WeaponState 中的 ShowPickupWidget(false); 注释掉,就会发现:客户端的 UI 在捡起武器后依然在显示,而服务器端无论是自己捡起武器还是客户端控制的角色捡起武器,UI 显示都不会有问题,这也验证了 OnRepNotify 一般都是由服务器发送到客户端这一特性。

Replicate

为了实现角色捡起武器就切换到另一个造型,需要设定一个变量用于维护这一状态,即 bWeaponEquipped,动画机根据这一状态来进行动画的切换。但同时,如果想将这一功能进行网络同步该如何实现?答案是将 EquippedWeapon 标记为同步(因为 bWeaponEquipped 会根据 EquippedWeapon 来返回值),这样就能在服务器端与客户端进行动画的同步了。

1
2
3
4
// 声明一个 Replicated 属性,就必须注册在 GetLifetimeReplicatedProps 中
// UPROPERTY(Replicated)
UPROPERTY()
class AWeapon* EquippedWeapon;
1
2
// BlasterAnimInstance.cpp
bWeaponEquipped = BlasterCharacter->IsWeaponEquipped();
1
2
3
4
5
bool ABlasterCharacter::IsWeaponEquipped() const
{
// 必须设置了 EuqippedWeapon 为 replicated 才能在客户端也同步信息
return (Combat && Combat->EquippedWeapon);
}

为了验证,我们同样将 EquippedWeapon 取消标记,并且要在 GetLifetimeReplicatedProps 取消注册,之后运行游戏会发现:仅有服务器端的角色会切换动画状态而客户端则不会,这说明了:拾取的碰撞检测确实只发生在了服务器上,在一个客户端的角色拾取了武器之后这个数据只会存在于服务器端的角色上。只有将 EquippedWeapon 这一变量同步至客户端,客户端才能正确地知道 “拾取到了武器”这一信息,否则即使在模型上已经显示了武器(因为武器已经设置了同步),但动画依旧不会变化。

RPCs

在目前的项目中,拾取武器这一事件只会在服务器上发生,因此在下面的函数中进行判断,如果是客户端按下了 “E” 键拾取武器,则会发送 RPC 到服务器,由服务器实现这一功能再同步到客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void ABlasterCharacter::EquipButtonPressed()
{
if (Combat)
{
// 服务器上直接调用 EquipWeapon
if (HasAuthority())
{
Combat->EquipWeapon(OverlappingWeapon);
GEngine->AddOnScreenDebugMessage(-1, 20.f, FColor::Green, TEXT("Server Equip"));
}
// 客户端上调用 ServerEquipButtonPressed
else
{
ServerEquipButtonPressed();
GEngine->AddOnScreenDebugMessage(-1, 20.f, FColor::Red, TEXT("Client Equip"));
}
}
}

void ABlasterCharacter::ServerEquipButtonPressed_Implementation()
{
if (Combat)
{
Combat->EquipWeapon(OverlappingWeapon);
}
}

// .h
UFUNCTION(Server, Reliable)
void ServerEquipButtonPressed();

总结

UE 作为 C/S 架构的网络模型,在进行网络游戏的设计时,一般只给客户端模拟的权限,检测与状态变化相关的都放在服务器进行,只是将结果同步到客户端,客户端再根据服务器的结果进行模拟,便有了客户端自己能自由操控的错觉;这一功能通常由 ReplicatedOnRepNotify 来实现;客户端如果想要向服务器发送请求,通常需要使用 RPCs 来实现。