武器交互——捡起武器

写在前面

捡起武器,如果是在单机游戏中表现会简单很多。非常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,然后根据武器状态在服务端和客户端同步处理。

相关推荐
程序员小王꧔ꦿ2 分钟前
python植物大战僵尸项目源码【免费】
python·游戏
让开,我要吃人了2 小时前
HarmonyOS开发实战(5.0)实现二楼上划进入首页效果详解
前端·华为·程序员·移动开发·harmonyos·鸿蒙·鸿蒙系统
everyStudy3 小时前
前端五种排序
前端·算法·排序算法
ZBzibing3 小时前
[游戏技术]L4D服务器报错解决
服务器·游戏
甜兒.4 小时前
鸿蒙小技巧
前端·华为·typescript·harmonyos
琪智科技7 小时前
秦时明月6.2魔改版+GM工具+虚拟机一键端
游戏
Jiaberrr8 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy8 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白8 小时前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、8 小时前
Web Worker 简单使用
前端