武器交互——捡起武器

写在前面

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

相关推荐
王哈哈^_^1 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie2 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic2 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿3 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具3 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161774 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test4 小时前
js下载excel示例demo
前端·javascript·excel
Yaml44 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事4 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶4 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json