写在前面
捡起武器,如果是在单机游戏中表现会简单很多。非常Naive的逻辑
是:
- 给角色的骨骼设定好插槽并命名插槽。
- 在玩家操作角色捡起时,直接把武器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,然后根据武器状态在服务端和客户端同步处理。