写在前面
在我们平常玩FPS游戏中,我们经常需要和武器产生交互,在UE中,我们会定义一个交互区域,只要玩家进入了那个区域,就可以与武器产生交互。
在此期间有两种方法判断是否进入了交互区域,一种比较笨,那就是在Tick函数中不断地查询是否已经进入了交互区,另外一种就是采用回调函数,只要进入了交互区,那么就调用回调就行,我们将采用这个方法。
设置PickupWidget和回调逻辑
在Weapon class中加入回调函数:
首先我们先按照之前的组件添加逻辑设置一下PickUpWidget,然后我们就开始加入回调逻辑
c++
// header
UFUNCTION()
virtual void OnSphereOverLap(UPrimitiveComponent *OverlappedComponent, AActor *OtherActor, UPrimitiveComponent *OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult &SweepResult);
UFUNCTION()
virtual void OnSphereEndOverLap(UPrimitiveComponent *OverlappedComponent, AActor *OtherActor, UPrimitiveComponent *OtherComp, int32 OtherBodyIndex);
// cpp
// Called when the game starts or when spawned
void AWeapon::BeginPlay()
{
Super::BeginPlay();
if (PickUpWidget)
{
PickUpWidget->SetVisibility(false);
}
// 如果是在服务器上, 才让SphereArea正常发挥碰撞作用
if (HasAuthority())
{
this->SphereArea->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
this->SphereArea->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
// this: 这个实例 // function: 调用这个函数
this->SphereArea->OnComponentBeginOverlap.AddDynamic(this, &ThisClass::OnSphereOverLap);
this->SphereArea->OnComponentEndOverlap.AddDynamic(this, &ThisClass::OnSphereEndOverLap);
}
}
// 回调函数 只有在组件被overlap时 才会被调用
void AWeapon::OnSphereOverLap(UPrimitiveComponent *OverlappedComponent, AActor *OtherActor, UPrimitiveComponent *OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult &SweepResult)
{
auto BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);
if (BlasterCharacter && PickUpWidget)
{
this->PickUpWidget->SetVisibility(true);
}
}
// 回调函数 只有在组件不再被overlap时 才会被调用
void AWeapon::OnSphereEndOverLap(UPrimitiveComponent *OverlappedComponent, AActor *OtherActor, UPrimitiveComponent *OtherComp, int32 OtherBodyIndex)
{
auto BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);
if (BlasterCharacter && PickUpWidget)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("End Overlap"));
this->PickUpWidget->SetVisibility(false);
}
}
需要注意的是,只有在服务端,我们的回调函数才会发挥作用,也就是说在添加了以上代码后,会有以下效果。
- 控制服务端的Blaster触碰Weapon后,会在服务端出现字样(正确效果),在客户端没有出现字样(正确效果);在这时,控制服务端的Blaster离开Weapon,服务端字样消失(正确效果),但是如果控制客户端的Blaster接触再离开Weapon,字样同样会在服务端消失(错误效果)。
- 至始至终,无论怎么操作客户端,我们都无法在客户端看到字样,但是操作客户端的Blaster与武器交互,在服务端都会看到效果。
Variable Replication
显然,上面的效果并非是我们想要的,但是按照我们的开发逻辑,因为我们的Server保存着游戏的权威副本,所以我们完全可以先做Server的逻辑,然后再将各自的效果分发出去到相应的Client。
这时就不得不提到我们的Variable Replication了。
Property Replication in Unreal Engine | Unreal Engine 5.0 Documentation
总的来说,就是当我们把一个变量设置成为Replicated变量时,只要这个变量在服务端发生改变了,那么这个这个变量在客户端就会跟着变。那么这样一来我们可以先在服务端使用回调函数改变Replicated变量,然后所有客户端监听这个变量是否改变,如果变量变了,那么就显示字样即可。
这是我按照自己观察结果构造出来的一个抽象模型,和正确的肯定有出路,但是方便自己现阶段的理解。
- 首先,在Server上和Client的machine上都运行着独立的GameInstance
- 作为Replication的单元,Actor以某种方式连接在一起。
- 虽然我们在Server上和Client上看到的世界效果都是同步的或是相同的,但是整个世界的一切都是不同的实例,比如说我在Client Machine p1上操纵我的角色奔跑,我在Server上看到了p1的奔跑,但是Server的p1角色并没有被Client直接控制,他们仍然是独立的,只是UE帮助我们使用Replication保持了同步罢了。
在写代码时,虽然我们都是在改同一个类,但是类中的代码却被分为了三部分:全局域
,Server域
,Client域
,我们以Server域为主线,然后对于Client域再额外处理,这就好像先把游戏当成一个单机游戏开发,然后逐渐扩充让它适应联机。
首先,我们先定义Replicate的变量和Rep_On回调函数,
c++
// BlasterCharacter.h
// 该函数只有在属性被重新Replicated时,才会调用 是一个回调函数
UFUNCTION()
void OnRep_OverlappingWeaponChanged(class AWeapon *LastWeapon);
// Replicate 属性,在Server中改变时,Client会跟着一起变
UPROPERTY(ReplicatedUsing = OnRep_OverlappingWeaponChanged)
class AWeapon *OverlappingWeapon;
// 在这个函数内部注册该Replicate Variable
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty> &OutLifetimeProps) const override;
// BlasterCharacter.cpp
void ABlasterCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> &OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 条件注册 比如当某个clientP1操纵角色在Server游戏实例中改变了OverLappingWeapon属性,那么只有p1 machine BlasterCharacter的OverLappingWeapon会跟着改变,p2的machine上的Blaster不会跟着改变的不会
DOREPLIFETIME_CONDITION(ABlasterCharacter, OverlappingWeapon, COND_OwnerOnly);
}
void ABlasterCharacter::OnRep_OverlappingWeaponChanged(AWeapon *LastWeapon)
{
if (OverlappingWeapon)
{
OverlappingWeapon->SetPickUpVisibility(true);
return;
}
if (LastWeapon)
{
LastWeapon->SetPickUpVisibility(false);
}
}
那么Rep_On
函数会在哪调用呢,想象一下。你连接了一个Server,然后操控着你的角色跑到武器那里,接着武器亮起press
提示,因此很明显,这个逻辑是在你的GameInstance上调用的,影响的是你的游戏而非Server的,但是这个过程却需要Server的帮助:
对于一个Server,两个Client p1, p2, 一共有三个GameInstance实例,9个BlasterCharactrer实例。你作为p1,操纵你的角色接触到Weapon的SphereArea时,Server上的p1的相应实例也接触到了Weapon的SphereArea从而触发了overlapping事件。
c++
// 回调函数 只有在组件被overlap时 才会被调用
void AWeapon::OnSphereOverLap(UPrimitiveComponent *OverlappedComponent, AActor *OtherActor, UPrimitiveComponent *OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult &SweepResult)
{
auto BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);
if (BlasterCharacter)
{
// 如果那个触碰到SphereArea的是本地玩家而非client玩家
if (BlasterCharacter->IsLocallyControlled())
{
// 直接展示字样
SetPickUpVisibility(true);
}
else
{
// 否则利用Replication机制让远程的字样显示
BlasterCharacter->OverlappingWeapon = this;
}
}
}
// 回调函数 只有在组件不再被overlap时 才会被调用
void AWeapon::OnSphereEndOverLap(UPrimitiveComponent *OverlappedComponent, AActor *OtherActor, UPrimitiveComponent *OtherComp, int32 OtherBodyIndex)
{
auto BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);
if (BlasterCharacter)
{
// 如果那个触碰到SphereArea的是本地玩家而非client玩家
if (BlasterCharacter->IsLocallyControlled())
{
// 直接取消展示字样
SetPickUpVisibility(false);
}
else
{
BlasterCharacter->OverlappingWeapon = nullptr;
}
}
}
触发了overlapping后,Server上的相应BlasterCharacter修改了OverlappingWeapon, 作为需要复制的属性,它顺着网路去同步了p1的相应角色的BlasterCharacter的OverlappingWeapon(注意并非对象同步,也就是说p1的BlasterCharacter的overlapping Weapon和Server的不是同一个,但是是对应的), 这时OnRep函数调用,将p1的weapon的PickUpWidget显示出来。
这是Client的逻辑,因为OnRep是在Client那边调用的,因此我们需要通过判断是否是localControlled来确定是否是Server操控的Blaster,不然Server的就无法显示了。