武器交互——捡起武器

写在前面

捡起武器,如果是在单机游戏中表现会简单很多。非常Naive的逻辑是:

  1. 给角色的骨骼设定好插槽并命名插槽。
  2. 在玩家操作角色捡起时,直接把武器Actor固定给那个插槽就行。

然而,考虑到我们要开发一个维护性高的项目,那为何我们不能把这个功能封装进一个Componnent里面,这样一来还能提高代码的复用性。

其次,我们在开发的是一个联机游戏,我们该如何确保所有所有Clients看到的东西和Server是一致的,这说明我们依然要用到UE的Replication技术,只不过这一次不止是Property Replication了。

接下来, 我们围绕上面高亮的三点,来逐步展开说说。

设定F键为捡起武器Action

首先,我们需要让角色接受一个输入,让其可以知道自己什么时候执行捡枪操作。

打开Project Setting->Engine->Input。绑定好Action,我绑的是F键

之后在角色类里面建立好函数映射:

c++ 复制代码
// Called to bind functionality to input
void ABlasterCharacter::SetupPlayerInputComponent(UInputComponent *PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	// 绑定输入事件
	PlayerInputComponent->BindAxis("MoveForward", this, &ABlasterCharacter::MoveForward);
	PlayerInputComponent->BindAxis("MoveRight", this, &ABlasterCharacter::MoveRight);
	PlayerInputComponent->BindAxis("Turn", this, &ABlasterCharacter::Turn);
	PlayerInputComponent->BindAxis("LookUp", this, &ABlasterCharacter::LookUp);

	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
        
  // 添加上这一行
	PlayerInputComponent->BindAction("Equip", IE_Pressed, this, &ThisClass::EquipWeapon);
}

设定角色骨骼枪械插槽

打开你之前用来做动画的角色骨骼,一般以SKEL...开头,在我项目里是:

进入之后,在r_hand下面右键AddSocket

再Preview下放把枪看下效果,如果不对及时调整。

这里可以打开Animation Sequence来查看下持枪效果,摆的大致对就行了。Socket可以直接调整,就和普通的组件一样。

这样一来,编辑器的方面的准备就做好了。

为装备武器新建组件

我们已经知道组件是Actor的功能配件,我们还需思考下,我们的功能涉及到transformation(SceneComponent)吗?涉及到Geometry吗?UPrimitiveComponent。我们目前只是要把枪给装备到身上去罢了,所以好像并不需要这些特性,那就UActorComponent就行了吧。

c++ 复制代码
// EquipWeaponComponent header
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class BLASTER__API UEquipWeaponComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	// Sets default values for this component's properties
	UEquipWeaponComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
	void EquipWeapon(AWeapon * UnequippedWeapon);

private:
	// 装备的武器
	UPROPERTY(VisibleAnyWhere)
	AWeapon *EquippedWeapon;
};

// cpp
void UEquipWeaponComponent::EquipWeapon(AWeapon *UnequippedWeapon)
{
	auto BlasterCharacter = Cast<ABlasterCharacter>(this->GetOwner());
	if (UnequippedWeapon)
		UE_LOG(LogTemp, Warning, TEXT("UnequippedWeapon is not null"));
	if (!UnequippedWeapon || !BlasterCharacter)
		return;

	// 修改Weapon的状态
	UnequippedWeapon->SetStatus(EWeaponStatus::EWS_Equipped);
	this->EquippedWeapon = UnequippedWeapon;
	auto RightHandSocket = BlasterCharacter->GetMesh()->GetSocketByName("RightHandSocket");

	if (RightHandSocket)
	{
          // 将武器给插入到之前设定好的插槽中
		RightHandSocket->AttachActor(this->EquippedWeapon, BlasterCharacter->GetMesh());
	}
	// 设置Owner为当前的BlasterCharacter
	this->EquippedWeapon->SetOwner(BlasterCharacter);
	// 在服务器层次停止overlapping的碰撞检查
	this->EquippedWeapon->GetSphereArea()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	// 在服务器层次将Pick Up字样隐藏
	this->EquippedWeapon->SetPickUpVisibility(false);
}

然后在我们的Blaster里我们组装这个组件

c++ 复制代码
// header
UPROPERTY(VisibleAnyWhere, Category = Weapon)
class UEquipWeaponComponent *EquipWeaponModule;

virtual void EquipWeapon();

// cpp
void ABlasterCharacter::EquipWeapon()
{
	if (!EquipWeaponModule)
            return;

	UE_LOG(LogTemp, Display, TEXT("Pick Up"));
	this->EquipWeaponModule->EquipWeapon(OverlappingWeapon);

}

这样一来,在我们的Server上和Client上,你会发现已经可以捡起武器了。

适应联机

如果现在是单机游戏,我们的工作已经结束了,但是我们是联机游戏,我们的游戏会有好几个游戏实例,我们必须保证所有游戏的内容是同步和一致的。

在现阶段,开启一个Server和两个Client,我们可以进行观察:

  • 如果Server端,我们操纵本地角色去捡起武器,在两个Client上都观察到了武器被捡起(同步正常)。
  • 随便在一个Client p1端,我们操纵本地角色去捡起武器,在Server上可以观察到武器被捡起,但是在p2端,武器没被捡起(同步错误)。

这说明了,在Server上做的本地行为可以很好地被同步到所有的Client中去,但是在Client上的做的本地行为,似乎能够被同步到Server,但是Server也省略了再同步所有Clients的行为。

那自然而然,我们想到,Server本身就是权威,那我们什么事情都让Server做就得了,我们Client就享受Server的同步就行了。

RPC(Remote Procedure Call)

顾名思义,远程过程调用,就是在本地(Local)发起调用请求,但是在远端(Remote)进行过程调用的过程。Client p1在低处,Server在高处,你一嗓子只能让Server听到,那你干脆发起远程调用让Server吼一嗓子,这样大家都听到了。

在UE中如何理解这一过程呢,首先这个函数是由客户端调用的,否则没啥意义。我们之前说过,3个游戏运行实例,一共有9个Character,每个Client的本地操控的Character和Server对应的Character有一条同步的纽带连接着,我在Client操控着我的Character调用了RPC后,UE顺着这条纽带找到Server里面我的Character实例,然后响应了这个方法,此时这个方法涉及到的对象都不再是Client游戏实例的对象了,而是Server的。前面说过,Server捡起武器的操作是可以被两台Client看到的,所以同步就达成了。

了解了逻辑之后,我们开始写代码:

c++ 复制代码
// Blaster Header
// 远程在Server调用 由本地发起请求
UFUNCTION(Server, Reliable)
virtual void EquipWeaponOnServer();

// cpp
void ABlasterCharacter::EquipWeapon()
{
	if (!EquipWeaponModule)
		return;

	// 在服务器上调用
	if (HasAuthority())
	{
		UE_LOG(LogTemp, Display, TEXT("Pick Up"));
		this->EquipWeaponModule->EquipWeapon(OverlappingWeapon);
	}
	else
	{
		this->EquipWeaponOnServer();
	}
}

// 该函数由client invock 但是由Server调用 因此无需在此之内查看是否在服务器
void ABlasterCharacter::EquipWeaponOnServer_Implementation()
{
	if (!EquipWeaponModule)
		return;
	this->EquipWeaponModule->EquipWeapon(OverlappingWeapon);
}

首先我们先判断了是否在服务器端,在服务器的话直接走逻辑,否则的话,走Server远程调用。这样一来,效果就正常了。

但是,我们还有工作,那就是在武器被捡走后,就不会再出现PickUp字样了。因为我们是在Server做工作,所以武器的Status也是改在Server(没错,即使overlapping weapon的内部变量被更改了,但是并不会Replicated,感觉只有整个变量变化才会通知Replicated),所以比较简单的逻辑就是,把武器的status也改成Property Replicated,然后根据武器状态在服务端和客户端同步处理。

相关推荐
程序员鱼皮3 分钟前
Gemini 3.0 发布!
前端·ai编程·gemini
程序员鱼皮5 分钟前
Gemini 3.0 炸裂发布!前端又死了???
前端·ai·程序员·互联网·代码
xiangxiongfly9157 分钟前
CSS svg
前端·css·svg
山依尽18 分钟前
如何将一个 React SPA 项目迁移到 Next.js 服务端渲染
前端·next.js
22 分钟前
使用 svgfmt 优化 SVG 图标
前端·svg·icon
Watermelo61723 分钟前
href 和 src 有什么区别,它们对性能有什么影响?
前端·javascript·vue.js·性能优化·html·html5·用户体验
hqk31 分钟前
鸿蒙零基础语法入门:开启你的开发之旅
android·前端·harmonyos
AAA阿giao34 分钟前
大厂面试之反转字符串:深入解析与实战演练
前端·javascript·数据结构·面试·职场和发展·编程技巧
专业抄代码选手41 分钟前
告别“屎山”:用 Husky + Prettier + ESLint 打造前端项目的代码基石
前端
想进字节冲啊冲1 小时前
Vibe Coding 实战指南:从“手写代码”到“意图设计”的前端范式转移
前端·ai编程