武器交互——显示按下

写在前面

在我们平常玩FPS游戏中,我们经常需要和武器产生交互,在UE中,我们会定义一个交互区域,只要玩家进入了那个区域,就可以与武器产生交互。

在此期间有两种方法判断是否进入了交互区域,一种比较笨,那就是在Tick函数中不断地查询是否已经进入了交互区,另外一种就是采用回调函数,只要进入了交互区,那么就调用回调就行,我们将采用这个方法。

设置PickupWidget和回调逻辑

在Weapon class中加入回调函数:

首先我们先按照之前的组件添加逻辑设置一下PickUpWidget,然后我们就开始加入回调逻辑

// 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回调函数,

// 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事件。

// 回调函数 只有在组件被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的就无法显示了。

武器交互——显示按下

武器交互——显示按下

原文链接:https://juejin.cn/post/7329329170541658152 作者:jjjoker

(0)
上一篇 2024年1月30日 上午10:37
下一篇 2024年1月30日 上午10:47

相关推荐

发表回复

登录后才能评论