同步系列4-拾取武器的同步


项目仓库地址: https://github.com/Backfire935/BlasterAgain

今天来纪录一下我是如何实现拾取武器并在服务端和客户端上同步消息的。

首先是角色类中输入组件函数中的输入绑定事件:

1
2
//按F拾取武器
EnhancedInputComponent->BindAction(IA_Equip, ETriggerEvent::Triggered, this, &ABlasterCharacter::EquipButtonPressed);

这里绑定到的是EquipButtonPressed()函数,我们看一下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void ABlasterCharacter::EquipButtonPressed(const FInputActionValue& InputValue)
{
if(Combat)
{
if(HasAuthority())
{
Combat->EquipWeapon(OverlappingWeapon);//如果就在Server上就直接执行
}
else
{
ServerEquipButtonPressed();//不然就执行RPC
}
}
}

很明显这里的意思是,如果是在Server的玩家按F就直接执行Combat->EquipWeapon()这个函数,如果是客户端的玩家按F,则执行ServerEquipButtonPressed()函数,再由ServerEquipButtonPressed()函数调用Combat->EquipWeapon()装备武器,相当于装备武器这个过程必须先在服务端执行过,这样来确保安全。

我们先看ServerEquipButtonPressed()函数,Combat->EquipWeapon()一会讨论。在角色类的.h文件中,有:

1
2
UFUNCTION(Server, Reliable)
void ServerEquipButtonPressed();

其实现如下:

1
2
3
4
5
6
7
8
void ABlasterCharacter::ServerEquipButtonPressed_Implementation()
{
//反正只在服务端调用,不用写HasAuthority
if (Combat)
{
Combat->EquipWeapon(OverlappingWeapon);
}
}

可以看出这个时候都跳到了Combat->EquipWeapon函数上。那么这个Combat是什么呢。

1
2
UPROPERTY(VisibleAnywhere)
class UCombatComponent* Combat;

Combat是一个Actor组件,在角色类的构造函数中创建,同时开启了复制:

1
2
Combat = CreateDefaultSubobject<UCombatComponent>(TEXT("CombatComponent"));
Combat->SetIsReplicated(true);

说实话我不知道为什么装备武器不用ClientRPC就能同步武器的附加信息到其它客户端上,啊我在写这句话的时候知道了,我们后面再说,我们先说Combat干嘛的,Combat是个组件,用来附加在角色身上,这个组件的作用是将跟战斗有关的业务函数都写到这,这样Character想用直接调用就行,甚至如果Character在某个模式中不用战斗,比如种田,那么甚至可以不加载Combat组件,从而直接废弃相关的功能。这样组件化,模块化的设计是值得我这样的初学者学习的。不然所有的业务逻辑代码都写在Character里肯定是一件很令人崩溃的事情。(Combat的.h中设置了friend class ABlasterCharacter为友元,这样就可以访问角色类中的属性和函数,同时BlasterCharacter中初始化了Combat的Character属性)

1
2
3
4
5
6
7
8
void ABlasterCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
if(Combat)
{
Combat->Character = this;//在请求初始化组件函数中设定了 CombatComponent组件中Character变量的值是谁.
}
}

说了这么多,我们来看下Combat->EquipWeapon()函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void UCombatComponent::EquipWeapon(AWeapon* WeaponToEquip)
{
if (Character == nullptr || WeaponToEquip == nullptr) return;
EquippedWeapon = WeaponToEquip;
EquippedWeapon->SetWeaponState(EWeaponState::EWS_Equipped);
//关闭模拟物理
EquippedWeapon->GetWeaponMesh()->SetSimulatePhysics(false);
//关闭重力
EquippedWeapon->GetWeaponMesh()->SetEnableGravity(false);
EquippedWeapon->GetWeaponMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 关闭碰撞检测
const USkeletalMeshSocket * HandSocket = Character->GetMesh()->GetSocketByName(FName("RightHandSocket"));
if(HandSocket)
{
HandSocket->AttachActor(EquippedWeapon, Character->GetMesh()); //在插槽上将武器附加到身体上
}
EquippedWeapon->SetOwner(Character);
}

这样就成功在服务端上附加武器了!但是此时如果使用客户端拾取武器,从服务端观察的话,武器应该是成功的附加到了客户端角色的身上。但是从客户端自身或者其他的客户端来观察的话,可能武器的位置没动,即使武器被设为replicate,这是因为武器附加到角色身上的代码并没有在客户端执行。

那么怎么在客户端也执行拾取代码呢?我们先给武器添加一个属性,叫武器状态

1
2
3
4
5
6
7
8
UENUM(BlueprintType)
enum class EWeaponState : uint8
{
EWS_Initial UMETA(DisplayName = "Initial State"),
EWS_Equipped UMETA(DisplayName = "Equipped State"),
EWS_Dropped UMETA(DisplayName = "Dropped State"),
EWS_Max UMETA(DisplayName = "Max"),
};

那么剩下的事情其实就简单了,给武器状态属性添加属性复制,每次武器状态发生改变的时候调用RepNotify回调函数,在回调函数上执行武器相关的操作,而且RepNotify是只能在Server发出,在Client上执行的,且在Server上不执行,所以之前要在Combat->EquipWeapon函数中执行一次拾取武器的操作。接下来看一下如何设置OnRep,先给WeaponState属性注册一下复制

1
2
3
4
5
void AWeapon::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AWeapon, WeaponState);
}
1
2
3
4
5
6
private
UPROPERTY(ReplicatedUsing= OnRep_WeaponState, VisibleAnywhere, Category = "Weapon Properties")
EWeaponState WeaponState;

UFUNCTION()
void OnRep_WeaponState();

下面是OnRep_WeaponState()的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void AWeapon::OnRep_WeaponState()//处理武器不同状态时客户端武器的物理效果
{
ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(GetOwner());
const USkeletalMeshSocket* HandSocket = BlasterCharacter ? BlasterCharacter->GetMesh()->GetSocketByName(FName("RightHandSocket")) : nullptr;
switch (WeaponState)
{
case EWeaponState::EWS_Initial:
ShowPickupWidget(false);
break;
case EWeaponState::EWS_Equipped :
ShowPickupWidget(false);
//关闭模拟物理
WeaponMesh->SetSimulatePhysics(false);
//关闭重力
WeaponMesh->SetEnableGravity(false);
WeaponMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 关闭碰撞检测
if (HandSocket && BlasterCharacter)//!!!踩了一天的坑,服务端附加了客户端也得附加
{
HandSocket->AttachActor(this, BlasterCharacter->GetMesh()); //在插槽上将武器附加到身体上
}
break;
case EWeaponState::EWS_Dropped :
//相关业务逻辑
break;
}
}

那么到此,关于拾取武器和同步的部分就完成了,希望以后看到这段代码我能轻点骂自己。

最后的实现效果:

image-20230418234250851


文章作者: John Doe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 John Doe !
  目录