同步系列3-武器拾取HUD显示或隐藏的同步


玩家在靠近地上无主的武器一定范围内,武器上方会弹出一个提示,告知玩家按某个按钮后拾取武器。离开一定范围后提示消失。同时我们希望这个弹出的按钮只会在本地显示,也就是说一个玩家能否看到提示和其它玩家是无关的。

image-20230420210225319

我们需要一个UserWidget控件和一个Weapon武器类,在UserWidget类中自己加一下文本提示即可,然后在Weapon.h中添加控件组件的声明。

1
2
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
class UWidgetComponent* PickupWidget;

image-20230420210700424

之后我们在Weapon武器蓝图中设置空间为屏幕,添加上我们之前创建好的控件类,指定其大小,并勾上以所需大小绘制。这样就设置好了显示的格式。

武器类中添加了一个球形碰撞体,就是通过这个球形碰撞体积和玩家的碰撞来判断玩家进入和离开可拾取武器的范围的。

1
2
3
private:
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
class USphereComponent* AreaSphere;

之后通过两个函数来处理玩家进入和离开的逻辑。

1
2
3
4
5
protected:
UFUNCTION()
virtual void OnSphereOverlap(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

参数挺多,一个管玩家进,一个管玩家出,下面是实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void AWeapon::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);
if (BlasterCharacter)
{
BlasterCharacter->SetOverlappingWeapon(this);
}
}

void AWeapon::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);
if (BlasterCharacter)
{
ShowPickupWidget(false);//无主的仅在服务端执行,客户端靠OnRep_OverlappingWeapon
BlasterCharacter->SetOverlappingWeapon(nullptr);
}
}

这里的SetOverlappingWeapon函数用来传入武器给角色,就是告诉角色你已经进入了可拾取武器的范围并把武器自身的指针传给了角色。接下来回到角色类的.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
void ABlasterCharacter::SetOverlappingWeapon(AWeapon* Weapon)
{
OverlappingWeapon = Weapon;//如果是客户端,不会执行这个函数,因为是无主的weapon调用的这个函数,客户端触发碰撞后会在服务端执行,然后OnRep会把OverlappingWeapon传到其他客户端去,又因为被定义为COND_OwnerOnly,所以只有Owner对应的客户端拿到了值,OnRep由服务端调用,不在服务端执行只在客户端执行,所以Server不会触发OnRep函数
//因为Server不会触发OnRep函数,所以若是Server的玩家触发的碰撞,通过下面的方法进行显示。
//只有Server上的本地玩家执行,加上函数本身是由在服务端AWeapon::OnSphereOverlap调用的
if(IsLocallyControlled())//Returns true if controlled by a local (not network) Controller.实测注释这个函数没有影响,因为调用这个函数的对象的特殊性确保了代码运行到这的时候只可能是服务端在运行,客户端压根不会调用这个函数,这里参照教程保留这个函数。特殊性为,武器是无主的,无主的都归server
{
if (OverlappingWeapon)
{
OverlappingWeapon->ShowPickupWidget(true);
}
}
}

OverlappingWeapon是在角色类中一个开启了属性复制的变量,因此当这个变量发生变化的时候,可以通过一个RepNotify回调去其他客户端上执行相应的逻辑

1
2
3
4
5
void ABlasterCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION(ABlasterCharacter, OverlappingWeapon, COND_OwnerOnly);
}

此处开启条件复制,仅进入武器范围内的角色在服务端的Authority或(如果是服务端玩家进入范围) 和自主代理的那个客户端(某个客户端玩家进入范围)会被更新值。为什么客户端玩家进入范围,服务端也会被更新值?因为服务端永远是Authority的,换句话说服务端也是Owner。

知道了这些之后,我们就可以理解下面关于OverlappingWeapon的RepNotify回调了,它只会在服务端和 或某一个客户端上更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UFUNCTION()
void OnRep_OverlappingWeapon(AWeapon* LastWeapon);

void ABlasterCharacter::OnRep_OverlappingWeapon(AWeapon* LastWeapon)
{
if (OverlappingWeapon)
{
OverlappingWeapon->ShowPickupWidget(true);
}
if(LastWeapon)
{
LastWeapon->ShowPickupWidget(false);
}
}

你问搞这么麻烦干什么,一个提示而已,只在本地执行不就好了,为什么非得搞这么麻烦的同步。其实可以测试一下,如果只写玩家进入离开时的代码而不写同步的内容的话,对于客户端玩家而言确实没什么问题,但是如果是服务端玩家进入范围触发提示后,那么所有玩家的屏幕上都会被打开那个提示….

最后补充一下Weapon类中关于球形碰撞体积的设置,此处已省略其他无关代码:

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
31
32
AWeapon::AWeapon()
{
PrimaryActorTick.bCanEverTick = false;
bReplicates = true;
SetReplicateMovement(true);

AreaSphere = CreateDefaultSubobject<USphereComponent>(TEXT("AreaSphere"));
AreaSphere->SetupAttachment(RootComponent);
AreaSphere->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
AreaSphere->SetSphereRadius(72.f);

PickupWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("PickupWidget"));
PickupWidget->SetupAttachment(RootComponent);
}

void AWeapon::BeginPlay()
{
Super::BeginPlay();

if(HasAuthority()) //与HasAuthority()等效
{
AreaSphere->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
AreaSphere->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
AreaSphere->OnComponentBeginOverlap.AddDynamic(this, &AWeapon::OnSphereOverlap);
AreaSphere->OnComponentEndOverlap.AddDynamic(this, &AWeapon::OnSphereEndOverlap);
}
if (PickupWidget)
{
PickupWidget->SetVisibility(false);
}
}

在这里绑定了球形碰撞体积在被覆盖时候的处理函数。

那么到这里就大功告成了。


  目录