当前位置: 首页 > news >正文

《UE5_C++多人TPS完整教程》学习笔记39 ——《P40 远程过程调用(Remote Procedure Calls)》


本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 —— 《P40 远程过程调用(Remote Procedure Calls)》 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author) Stephen Ulibarri 发布在 Udemy 上的课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
在这里插入图片描述


文章目录

  • P40 装备武器(Equipping Weapons)
  • 40.1 创建装备武器的 RPC 函数
  • 40.2 武器状态变量复制
  • 40.3 Summary


P40 装备武器(Equipping Weapons)

在上节课 《UE5_C++多人TPS完整教程》学习笔记38 ——《P39 装备武器(Equipping Weapons)》 中我们实现了枪战功能组件,使得人物角色可以装备武器,但目前只有在服务器上的人物角色才可以拾取并装备与之重叠的武器。本节课我们想进一步让客户端上的人物角色也能装备武器,但也希望服务器在这一方面仍拥有(管理的)权限。在 《UE5_C++多人TPS完整教程》学习笔记37 ——《P38 变量复制(Variable Replication)》 中我们已经学习了从服务器到客户端的复制变量方法,现在我们需要另一种方法让客户端执行一些事件时可以通知服务器,比如装备武器,这种方法就是 “远程过程调用”(Remote Procedure Calls, RPC)。

在这里插入图片描述


40.1 创建装备武器的 RPC 函数

  1. 远程过程调用(下文均表示为 “RPC”)是在本地调用但在其他机器(不同于执行调用的机器)上远程执行的函数。RPC 函数可允许客户端或服务器通过网络连接相互发送消息。我们将创建一个 RPC 函数,它将在客户端被调用,并在服务器上执行以允许客户端的物角色也能装备武器。

  2. 在创建 RPC 函数时,我们还需要指定 RPC 函数是否可靠(Reliable),可靠的 RPC 将保证会被执行(Be guaranteed to execute),不可靠的 PRC 可能(Potentially)在我们将信息从客户端发送至服务器时被丢弃(Dropped)。信息以数据包(Packets)的形式发送,数据包时通过网络发送的信息单元,而数据包是可以被丢弃的。如果 RPC 函数是可靠的,当服务器收到 RPC 时会发送确认消息,客户端将在底层或在后台(Under the hood)接收服务器确认消息,如果没收到服务器发送的确认消息,RPC 将会再次发送。可靠 RPC 应被谨慎(有节制)地使用(Be used relatively sparingly),例如,最好不要在 ”Tick()” 函数中进行 RPC 函数的收发,但对于类似于装备武器这种单一动作来说是可以使用 RPC 函数的。

    要求和注意事项
    您必须满足一些要求才能充分发挥 RPC 的作用:

    • 它们必须从 Actor 上调用。
    • Actor 必须被复制。
    • 如果 RPC 是从服务器调用并在客户端上执行,则只有实际拥有这个 Actor 的客户端才会执行函数。
    • 如果 RPC 是从客户端调用并在服务器上执行,客户端就必须拥有调用 RPC 的 Actor。

    多播 RPC 则是个例外:

    • 如果它们是从服务器调用,服务器将在本地和所有已连接的客户端上执行它们。
    • 如果它们是从客户端调用,则只在本地而非服务器上执行。

    现在,我们有了一个简单的多播事件限制机制:在特定 Actor 的网络更新期内,多播函数将不会复制两次以上。按长期计划,我们会对此进行改善,同时更好的支持跨通道流量管理与限制。

    下面的表格根据执行调用的 actor 的所有权(最左边的一列),总结了特定类型的 RPC 将在哪里执行。

    从服务器调用的 RPC

    Actor 所有权未复制NetMulticastServerClient
    Client-owned actor在服务器上运行在服务器和所有客户端上运行在服务器上运行在 actor 的所属客户端上运行
    Server-owned actor在服务器上运行在服务器和所有客户端上运行在服务器上运行在服务器上运行
    Unowned actor在服务器上运行在服务器和所有客户端上运行在服务器上运行在服务器上运行

    从客户端调用的 RPC

    Actor 所有权未复制NetMulticastServerClient
    Owned by invoking client在执行调用的客户端上运行在执行调用的客户端上运行在服务器上运行在执行调用的客户端上运行
    Owned by a different client在执行调用的客户端上运行在执行调用的客户端上运行丢弃在执行调用的客户端上运行
    Server-owned actor在执行调用的客户端上运行在执行调用的客户端上运行丢弃在执行调用的客户端上运行
    Unowned actor在执行调用的客户端上运行在执行调用的客户端上运行丢弃在执行调用的客户端上运行

    —— 虚幻引擎官方文档《虚幻引擎中的 RPC》

  3. 默认情况下,RPC 并不可靠。要确保在远程机器上执行 RPC 调用,需要指定 “Reliable” 关键字。因此,在 “BlasterCharacter.h” 中使用带有 “Server” 和 “Reliable” 关键字的 UFUNCTION() 宏声明 RPC 函数 “ServerEquipButtonPressed()”。RPC 函数有 “实施函数”(Implementation function) 和 “验证函数”(Validation function)两个概念,我们在 “BlasterCharacter.cpp” 的中添加 “ServerEquipButtonPressed()” 函数的定义,并在函数名后添加 “_Implementation” 使之成为 “实施函数”,虚幻引擎将为我们在底层或在后台创建这个函数的实际定义,它用来定义 RPC 函数将会执行什么。

    /*** BlasterCharacter.h ***/...UCLASS()
    class BLASTER_API ABlasterCharacter : public ACharacter
    {GENERATED_BODY()...protected:// Called when the game starts or when spawnedvirtual void BeginPlay() override;// 与轴映射相对应的回调函数void MoveForward(float Value);	// 人物角色前进或后退void MoveRight(float Value);	// 人物角色左移或右移void Turn(float Value);			// 人物角色视角左转或右转void LookUp(float Value);		// 人物角色俯视或仰视// 与动作映射相对应的回调函数void EquipButtonPressed();		// 人物角色装备武器private:...UPROPERTY(VisibleAnyWhere)class UCombatComponent* Combat;							// 添加枪战功能组件类/* P40 远程过程调用(Remote Procedure Calls)*/UFUNCTION(Server, Reliable)void ServerEquipButtonPressed();						// 人物角色装备武器的 RPC 函数/* P40 远程过程调用(Remote Procedure Calls)*/...};
    
    /*** BlasterCharacter.cpp ***/.../* P40 远程过程调用(Remote Procedure Calls)*/
    // 按下 E 键装备与人物角色重叠的武器
    void ABlasterCharacter::EquipButtonPressed()
    {// Combat 不为空,设置与人物角色重叠的武器为要装备的武器if (Combat) {if (HasAuthority()) {Combat->EquipWeapon(OverlappingWeapon);	// 服务器设置要装备的武器为与人物角色重叠的武器}else {ServerEquipButtonPressed();				// 客户端设置与人物角色重叠的武器为要装备的武器}}
    }void ABlasterCharacter::ServerEquipButtonPressed_Implementation()
    {// Combat 不为空,客户端设置与人物角色重叠的武器为要装备的武器if (Combat) {Combat->EquipWeapon(OverlappingWeapon);	// 设置要装备的武器为与人物角色重叠的武器}
    }
    /* P40 远程过程调用(Remote Procedure Calls)*/...
    

    验证
    我们在不久前加入了为 RPC 增加验证函数的功能,以此作为检测错误数据/输入的一个手段。其主要思路是:如果 RPC 的验证函数检测到任何 参数存在问题,就会通知系统将发起 RPC 调用的客户端/服务器断开。
    要为 RPC 声明一个验证函数,只需将 “WithValidation” 关键字添加到 “UFUNCTION” 声明语句:

    UFUNCTION( Server, WithValidation )
    void SomeRPCFunction( int32 AddHealth );
    

    然后在实施函数旁边加入验证函数:

    bool SomeRPCFunction_Validate( int32 AddHealth )
    {if ( AddHealth > MAX_ADD_HEALTH ){return false; // This will disconnect the caller}
    return true; // This will allow the RPC to be called
    }void SomeRPCFunction_Implementation( int32 AddHealth )
    {Health += AddHealth;
    }
    

    最近的一次更改被添加到 UHT,以便要求客户端 -> 服务器 RPC 具有一个 “_Validate” 函数。这样是为了鼓励使用安全的服务器 RPC 函数,同时尽可能方便其他人 添加代码以检查所有参数,确保其符合所有已知的输入限制。


    —— 虚幻引擎官方文档《虚幻引擎中的 RPC》

  4. 编译后进行测试,可以看到我们能操控其中一个客户端上的人物角色去拾取并装备武器,但是拾取组件仍可见,并且当我们控制它靠近另一个客户端或服务器上的人物角色时,手上的武器若与另一个客户端或服务器上的人物角色发生重叠,我们操控的客户端以及另一个客户端或服务器上仍会显示拾取组件。这意味着我们需要在武器被拾取后隐藏拾取部件,并禁用武器球体重叠事件。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述


40.2 武器状态变量复制

  1. 仿照 《UE5_C++多人TPS完整教程》学习笔记37 ——《P38 变量复制(Variable Replication)》 中复制变量的流程,在 “Weapon” 中将武器状态枚举类型 “WeaponState” 变为可复制变量,我们可以这么做是因为武器类 “AWeapon” 本身就是可复制的 Actor,如果武器类本身不可复制,那就不能进行变量复制。接着,重写 “GetLifetimeReplicatedProps()” 函数,在函数中我们调用了 “DOREPLIFETIME” 宏,用于指定需要被复制的属性为 “WeaponState”,然后定义 Repnotify 函数 “OnRep_WeaponState”,这样就可以保证 “WeaponState” 发生复制时被调用隐藏拾取组件。
    我们无需复制 “CombatComponent.cpp” 的 “EquipWeapon()” 函数中设置被装备的武器的所属者 “SetOwner()” 这个操作,因为通过查看 “SetOwner()” 的定义后进一步查看定义中 “Owner” 的声明可知,“Owner” 不仅为可复制的变量,还拥有相应的 Repnotify 函数 “OnRep_Owner”,因此该操作会被复制。

    /*** Weapon.h ***/...// UENUM():参阅《元数据说明符》https://dev.epicgames.com/documentation/zh-cn/unreal-engine/metadata-specifiers-in-unreal-engine?application_version=5.0
    // BlueprintType:将此类公开为可用于蓝图中的变量的类型,参阅《类说明符》https://dev.epicgames.com/documentation/zh-cn/unreal-engine/class-specifiers?application_version=5.0
    UENUM(BlueprintType)									
    enum class EWeaponState : uint8							// 武器状态枚举类型,枚举常量为无符号8位整型						
    {	// UMETA():参阅《元数据说明符》https://dev.epicgames.com/documentation/zh-cn/unreal-engine/metadata-specifiers-in-unreal-engine?application_version=5.0EWS_Initial UMETA(DisplayName = "Initial State"),	// 初始状态,武器可以被人物捡起EWS_Equipped UMETA(DisplayName = "Equipped"),		// 已装备状态,武器被人物捡起并装备使用EWS_Dropped UMETA(DisplayName = "Dropped"),			// 已丢弃状态,武器被人物丢弃EWS_Max UMETA(DisplayName = "DefaultMax")			// 大多数枚举常量都会有一个默认的最大常量,我们通过检查这个最大常量的值,就能知道枚举类型中有多少个常量
    };UCLASS()
    class BLASTER_API AWeapon : public AActor
    {GENERATED_BODY()public:	// Sets default values for this actor's propertiesAWeapon();/* P40 远程过程调用(Remote Procedure Calls)*/// 重写复制属性函数virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;/* P40 远程过程调用(Remote Procedure Calls)*/// 显示拾取组件void ShowPickupWidget(bool bShowWidget);...private:UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")	// 添加所有地方可见的骨骼网格组件,这样就可以通过蓝图进行编辑武器,归类为 Weapon Propertiesclass USkeletalMeshComponent* WeaponMesh;				UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")	// 添加一个重叠体积(Overlap Volume),这里使用球体组件 “包裹” 武器骨骼体组件,用于判定人物是否碰到球体,若碰到人物将会拾取该武器class USphereComponent* AreaSphere;/* P40 远程过程调用(Remote Procedure Calls)*///UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")									// 添加新声明的武器状态枚举类型,归类为 Weapon Properties//EWeaponState WeaponState;UPROPERTY(ReplicatedUsing = OnRep_WeponState, VisibleAnywhere, Category = "Weapon Properties")	// 指定 WeaponState 的 Repnotify 函数为 OnRep_WeaponState()EWeaponState WeaponState;									UFUNCTION()void OnRep_WeponState();																		// WeponState 的 Repnotify 函数/* P40 远程过程调用(Remote Procedure Calls)*/...}
    
    /*** Weapon.cpp ***/// Fill out your copyright notice in the Description page of Project Settings.#include "Weapon.h"	// 原来自动生成的代码是 #include "HUD/OverheadWidget.h",这里需要把 "GameMode/" 去掉,否则找不到文件 "LobbyGameMode.h"
    #include "Components/SphereComponent.h"
    #include "Components/WidgetComponent.h"
    #include "Blaster/Character/BlasterCharacter.h"/* P40 远程过程调用(Remote Procedure Calls)*/
    #include "Net/UnrealNetwork.h"
    /* P40 远程过程调用(Remote Procedure Calls)*/.../* P40 远程过程调用(Remote Procedure Calls)*/
    void AWeapon::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {// 调用 SuperSuper::GetLifetimeReplicatedProps(OutLifetimeProps);// 添加要为派生的类 AWeapon 复制的属性,需要添加头文件 "Net/UnrealNetwork.h"// DOREPLIFETIME 宏用于指定哪些属性需要被复制,以及复制的条件。DOREPLIFETIME(AWeapon, WeaponState);
    }
    /* P40 远程过程调用(Remote Procedure Calls)*/.../* P40 远程过程调用(Remote Procedure Calls)*/
    void AWeapon::OnRep_WeponState()
    {switch (WeaponState){case EWeaponState::EWS_Equipped:ShowPickupWidget(false);											// 隐藏拾取组件//AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);	//禁用武器球体碰撞break;}
    }
    /* P40 远程过程调用(Remote Procedure Calls)*/...
    
    /*** CombatComponent.cpp ***/...void UCombatComponent::EquipWeapon(AWeapon* WeaponToEquip)
    {if (Character == nullptr || WeaponToEquip == nullptr) return;// 设置要装备的武器以及武器状态EquippedWeapon = WeaponToEquip;										// 设置装备的武器EquippedWeapon->SetWeaponState(EWeaponState::EWS_Equipped);			// 设置武器状态为已装备// 获取武器插槽并附加武器在插槽上const USkeletalMeshSocket* HandSocket = Character->GetMesh()->GetSocketByName(FName("RightHandSocket"));	// 根据插槽名称搜索并获取右手插槽,需要添加头文件 "Blaster/Character/BlasterCharacter.h"if (HandSocket)	{													HandSocket->AttachActor(EquippedWeapon, Character->GetMesh());	// 将武器附加在右手插槽上,需要添加头文件 "Engine/SkeletalMeshSocket.h"}/* P40 远程过程调用(Remote Procedure Calls)*/// 设置武器所属并保证武器不能被拾取EquippedWeapon->SetOwner(Character);								// 设置武器所属,通过查看 SetOwner() 的定义,然后查看定义中 Owner 的声明,可知该操作会被复制EquippedWeapon->ShowPickupWidget(false);							// 设置拾取组件不可见,被装备的武器将不能再被拾取,该操作不会被复制/* P40 远程过程调用(Remote Procedure Calls)*/
    }
    
  2. 编译后进行测试,可以看到我们操控一个客户端上的人物角色装备武器后,拾取组件已被隐藏,并且当我们控制它靠近另一个客户端或服务器上的人物角色时,手上的武器若与另一个客户端或服务器上的人物角色发生重叠,拾取组件在我们操控的客户端上已被隐藏,但在另一个客户端或服务器上仍能显示拾取组件,这是因为武器球体 “AreaSphere” 的重叠事件只能在服务器上生成,仍能在服务器上执行,所以解决问题的方式就是禁用服务器上的碰撞即可。
    在这里插入图片描述
    在这里插入图片描述

    注意:这里可能会出现操控一个客户端上的人物角色与武器发生重叠时不显示拾取组件的问题,这里不要用实时编译,采用离线编译即可解决。
    在这里插入图片描述

  3. 在 “CombatComponent.cpp” 中修改 “EquipWeapon()” 函数的定义,删去隐藏拾取组件的代码;然后在 “Weapon.h” 和 “Weapon.cpp” 中重新定义 “SetWeaponState()” 函数,实现武器被装备后隐藏拾取组件和禁用武器球体碰撞事件功能。

    /*** CombatComponent.cpp ***/...void UCombatComponent::EquipWeapon(AWeapon* WeaponToEquip)
    {if (Character == nullptr || WeaponToEquip == nullptr) return;// 设置要装备的武器以及武器状态EquippedWeapon = WeaponToEquip;										// 设置装备的武器EquippedWeapon->SetWeaponState(EWeaponState::EWS_Equipped);			// 设置武器状态为已装备// 获取武器插槽并附加武器在插槽上const USkeletalMeshSocket* HandSocket = Character->GetMesh()->GetSocketByName(FName("RightHandSocket"));	// 根据插槽名称搜索并获取右手插槽,需要添加头文件 "Blaster/Character/BlasterCharacter.h"if (HandSocket)	{													HandSocket->AttachActor(EquippedWeapon, Character->GetMesh());	// 将武器附加在右手插槽上,需要添加头文件 "Engine/SkeletalMeshSocket.h"}/* P40 远程过程调用(Remote Procedure Calls)*/// 设置武器所属并保证武器不能被拾取EquippedWeapon->SetOwner(Character);														// 设置武器所属,通过查看 SetOwner() 的定义,然后查看定义中 Owner 的声明,可知该操作会被复制// EquippedWeapon->ShowPickupWidget(false);													// 设置拾取组件不可见,被装备的武器将不能再被拾取,该操作不会被复制// EquippedWeapon->GetAreaSphere()->SetCollisionEnabled(ECollisionEnabled::NoCollision);	// 禁用武器球体碰撞/* P40 远程过程调用(Remote Procedure Calls)*/
    }...
    
    /*** Weapon.h ***/...UCLASS()
    class BLASTER_API AWeapon : public AActor
    {GENERATED_BODY()...public:/* P40 远程过程调用(Remote Procedure Calls)*/void SetWeaponState(EWeaponState State); 	// 设置武器状态// ‌forceinline 是编程中用于强制内联函数的关键字或注解‌,主要用于减少函数调用开销,但需谨慎使用以避免代码膨胀或性能下降。// FORCEINLINE USphereComponent* GetAreaSphere() const  { return AreaSphere; }	// 获取武器球体/* P40 远程过程调用(Remote Procedure Calls)*/
    };...
    /*** Weapon.cpp ***/.../* P40 远程过程调用(Remote Procedure Calls)*/
    void AWeapon::OnRep_WeponState()
    {switch (WeaponState){case EWeaponState::EWS_Equipped:										// 隐藏拾取组件ShowPickupWidget(false);//AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);	// 禁用武器球体碰撞	break;}
    }void AWeapon::SetWeaponState(EWeaponState State)
    {WeaponState = State;switch (WeaponState){case EWeaponState::EWS_Equipped:ShowPickupWidget(false);											// 隐藏拾取组件AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);	// 禁用武器球体碰撞	break;}
    }
    /* P40 远程过程调用(Remote Procedure Calls)*/...
    
  4. 编译后进行测试,可以看到我们操控其中一个客户端上的人物角色去拾取并装备武器,手上的武器若与另一个客户端或服务器上的人物角色发生重叠,我们操控的客户端、另一个客户端或服务器上拾取组件均不可见,预期效果达成。
    在这里插入图片描述


40.3 Summary

本节课借助 RPC 函数完善了客户端装备武器的功能。首先我们创建创建了一个可靠的服务器 RPC 函数“ServerEquipButtonPressed”,在该 RPC 的实施函数中 “ServerEquipButtonPressed_Implementation()” 中执行实际的装备武器操作,确保服务器始终拥有装备决策权。当客户端按下装备键时调用该 RPC 函数,服务器收到后执行装备逻辑;接着,我们重构 “EquipButtonPressed()” 函数逻辑:服务器直接调用装备逻辑,客户端则通过调用 RPC 将请求发送至服务器。
为了解决已被装备的武器仍会在操控端和其他机器上显示拾取组件的问题,我们在武器类 “AWeapon” 中将武器变量枚举类型 “WeaponState” 修改为可复制变量,仿照之前课程的步骤流程,重写 “GetLifetimeReplicatedProps” 函数以注册 “WeaponState” 为复制属性,实现 “WeaponState” 的 Repnotify 函数 “OnRep_WeaponState”,这样当武器状态复制到客户端时,若为 “EWS_Equipped” 则自动隐藏拾取组件;然后,重新定义 “SetWeaponState()” 函数:在服务器端设置状态时,同步执行隐藏拾取组件和禁用碰撞球体的逻辑,确保即时生效;最后将碰撞禁用操作加入到 “SetWeaponState()” 函数的定义中,确保武器装备后,其碰撞球体在服务器和所有客户端上均被禁用,彻底解决多角色重叠时异常显示拾取提示的问题。
最终的测试结果表明,客户端上已被装备的武器若与另一个客户端或服务器上的人物角色发生重叠时,拾取组件在任何机器上均不可见,预期效果达成。
在这里插入图片描述

40.2 武器状态变量复制步骤 2 的测试中可能会出现操控一个客户端上的人物角色与武器发生重叠时不显示拾取组件的问题,这里不要用实时编译,采用离线编译即可解决。


http://www.lqws.cn/news/592615.html

相关文章:

  • LabVIEW自动扶梯振动监测
  • RabbitMQ简单消息发送
  • Node.js与Express框架的深度整合
  • beego打包发布到Centos系统及国产麒麟系统完整教程
  • react-数据Mock实现——json-server
  • 飞算 JavaAI 开发助手:深度学习驱动下的 Java 全链路智能开发新范式
  • 发票PDF处理工具,智能识别合并一步到位
  • Foundation 5 安装使用教程
  • 【Unity实战】UI按钮回调管理:职责分离与持久化策略
  • 基于 Vue + RuoYi 架构设计的商城Web/小程序实训课程
  • 网络基础知识与代理配置
  • Java 大视界 -- Java 大数据在智能交通共享单车智能调度与停放管理中的应用(329)
  • 数字雨动画背景
  • 深入剖析AI大模型:TensorFlow
  • 浅谈「线性代数的本质」 - 系列合集
  • 系统思考力量与实践
  • 从数据资产识别与防泄密看零信任产品
  • 什么叫单通
  • .Net 使用OpenAI开源离线语音识别模型Whisper
  • CppCon 2018 学习:EFFECTIVE REPLACEMENT OF DYNAMIC POLYMORPHISM WITH std::variant
  • Helix Toolkit 在 WPF 中加载带贴图素材的模型
  • 《全程软件测试》第1章
  • 坚石ET ARM加密狗复制模拟介绍
  • 23.安卓逆向2-r0capture搭配Wireshark方式抓包
  • Nuxt 3 中实现跨组件通信方式总结:使用 Pinia、Provide/Inject 或 Props
  • 设计模式-命令模式
  • 昆泰芯3D霍尔磁传感器芯片在汽车零部件中的应用
  • OpenCV-Python Tutorial : A Candy from Official Main Page(二)
  • 使用FFmpeg+SDL2读取本地摄像头并渲染
  • 07 Springboot+netty+mqtt服务端实现【重构】