《UE5_C++多人TPS完整教程》学习笔记37 ——《P38 变量复制(Variable Replication)》


本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 ------ 《P38 变量复制(Variable Replication)》 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author)Stephen Ulibarri 发布在 Udemy 上的课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么


文章目录

  • [P38 变量复制(Variable Replication)](#P38 变量复制(Variable Replication))
  • [38.1 创建武器重叠属性](#38.1 创建武器重叠属性)
  • [38.2 创建 Repnotify 函数](#38.2 创建 Repnotify 函数)
  • [38.3 创建拾取组件结束重叠事件](#38.3 创建拾取组件结束重叠事件)
  • [38.4 Summary](#38.4 Summary)

P38 变量复制(Variable Replication)

本节课的目标是显示拾取组件在客户端上,这涉及到复制变量(Replicating variables);我们将学习创建一个函数,当变量被复制时它会被客户端调用,这样的的函数被被称为 "RepNotify" 函数(更新通知 "复制" 函数)。


38.1 创建武器重叠属性

  1. 我们需要创建一个可以存储与人物角色重叠的武器的变量,因此需要在 "BlasterCharacter.h" 中使用添加了元数据说明符 "Replicated" 的 "UPROPERTY" 宏声明武器类变量实例 "OverlappingWeapon" 作为 Actor 类 "BlasterCharacter" 的属性,当这个属性在服务器上发生变化时,服务器会在复制的属性每次更改其值时向每个连接的客户端发送更新,每个客户端会将更新的值应用到其本地版本的 "BlasterCharacter"。此外,我们还需声明重写 "GetLifetimeReplicatedProps()" 函数并添加宏调用,以在派生的 "BlasterCharacter" 实例的生命周期内复制属性;使用 forceinline 宏声明"SetOverlappingWeapon()" 函数,用以设置重叠的武器实例,之后将在武器类 Weapon 的 OnSphereOverlap() 函数中调用。

    cpp 复制代码
    /*** BlasterCharacter.h ***/
    ...
    
    UCLASS()
    class BLASTER_API ABlasterCharacter : public ACharacter
    {
    	GENERATED_BODY()
    
    public:
    	// Sets default values for this character's properties
    	ABlasterCharacter();
    
    	// Called every frame
    	virtual void Tick(float DeltaTime) override;
    
    	// Called to bind functionality to input
    	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    
    	/* P38 变量复制(Variable Replication)*/
    	// 重写复制属性函数
    	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    	/* P38 变量复制(Variable Replication)*/
    	
    	...
    
    private:
    	// VisibleAnywhere:表示该变量在蓝图编辑器中显示在变量列表中,并且可以在蓝图中进行读取操作。常用于定义可读变量。
    	UPROPERTY(VisibleAnywhere, Category = Camera)	
    	class USpringArmComponent* CameraBoom;			// 添加弹簧臂组件,归类为 "Camera"
    
    	UPROPERTY(VisibleAnywhere, Category = Camera)
    	class UCameraComponent* FollowCamera;			// 添加摄像机组件,归类为 "Camera"
    
    	// BlueprintReadOnly:表示该变量只能在蓝图中进行读取操作,不能在蓝图中进行写入操作。常用于定义只读变量。
    	// 我们不能在私有变量中使用关键字 BlueprintReadOnly和 BlueprintReadWrite,除非使用了 meta = (AllowPrivateAccess = "true") 进行指定
    	// UE4中用于定义蓝图变量的元数据(metadata)的所有关键字及其解释和作用可以参见:https://blog.csdn.net/u013007305/article/details/130450354
    	UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))	
    	class UWidgetComponent* OverheadWidget;												// 添加头部组件
    
    	/* P38 变量复制(Variable Replication)*/
    	UPROPERTY(Replicated)
    	class AWeapon* OverlappingWeapon;
    	
    public:	
    	// 设置重叠的武器实例,以便在武器类 Weapon 的 OnSphereOverlap() 函数中调用 
    	FORCEINLINE void SetOverlappingWeapon(AWeapon* Weapon) { OverlappingWeapon = Weapon; }
    };
    	/* P38 变量复制(Variable Replication)*/

    forceinline 是编程中用于强制内联函数的关键字或注解‌,主要用于减少函数调用开销,但需谨慎使用以避免代码膨胀或性能下降。


    核心作用

    • 强制内联展开‌:与普通inline不同,forceinline会跳过编译器的成本/收益分析,直接依据程序员指定展开函数体到调用处。‌‌

    ‌ - 性能优化‌:消除函数调用时的栈帧开销,适用于频繁调用的小型函数。‌‌

    ‌- 潜在风险‌:过度使用可能导致代码体积膨胀、缓存命中率下降,甚至引发性能倒退。

    使用建议‌ :

    ‌ - 适用场景‌:高频调用的简单函数(如getter/setter)、性能关键路径代码。‌‌

    ‌- 禁用情况‌:递归函数、虚函数调用或含复杂控制流的函数。‌‌

    • 调试兼容性‌:需关闭内联(如C++的/Ob0选项)时,forceinline可能失效。

    ------ 百度 AI

    在 "BlasterCharacter.cpp" 中完成 "GetLifetimeReplicatedProps()" 的定义 "OverlappingWeapon"。这样,属性 "OverlappingWeapon" 就只会在服务器上发生改变时进行复制,而不会在每一帧(Frame)或每一次网络更新(Net update)中被复制。

    cpp 复制代码
    /*** BlasterCharacter.cpp ***/
    
    /* P38 变量复制(Variable Replication)*/
    #include "BlasterCharacter.h"
    #include "GameFramework/SpringArmComponent.h"
    #include "Camera/CameraComponent.h"
    #include "GameFramework/CharacterMovementComponent.h"
    #include "Components/WidgetComponent.h"
    
    /* P38 变量复制(Variable Replication)*/
    #include "Net/UnrealNetwork.h"
    /* P38 变量复制(Variable Replication)*/
    
    ...
    
    void ABlasterCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	// 调用Super
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	// 添加要为派生的类 ABlasterCharacter 复制的属性,需要添加头文件 "Net/UnrealNetwork.h" 
    	DOREPLIFETIME(ABlasterCharacter, OverlappingWeapon);
    }
    /* P38 变量复制(Variable Replication)*/

    属性复制

    每个 Actor 都维护一个全属性列表,其中包含 "Replicated" 说明符。每当复制的属性值发生变化时,服务器会向所有客户端发送更新。客户端会将其应用到 Actor 的本地版本上。这些更新只会来自服务器,客户端永远不会向服务器或其他客户端发送属性更新。

    我们不推荐在客户端上更改复制的变量值。该值将始终与服务器端的值不一致,直到服务器下一次侦测到变更并发送更新为止。如果服务器版本的属性不是经常更新,那客户端就需要等待很长时间才能被纠正。

    Actor 属性复制可靠。这意味着,Actor的客户端版本的属性最终将反映服务器上的值,但客户端不必接受服务器上某个属性的每一个单独变更。例如,如果一个整数属性的值快速从 100 变成 200,然后又变成了 300,客户端将最终接受一个值为 300 的变更,但客户端不一定会知道这个值曾经变成过 200。


    ------ 虚幻引擎官方文档《属性复制》
    复制Actor属性

    一般来说,复制Actor属性的途径主要有两个:

    • "Replicated"
    • "ReplicatedUsing"

    这些是虚幻引擎的反射系统使用的两个属性元数据说明符。"Replicated" 属性为属性复制提供了指定特定条件的选项,将属性复制限制在特定连接上。你也可以设置自定义复制条件,为属性复制定义自己的逻辑。"ReplicatedUsing" 属性需要你提供 "RepNotify" 函数,当相关属性被复制时,客户端就会调用该函数。

    你还可以使用 "NotReplicated" 说明符指定 不 复制的属性。此说明符可能一开始看起来没什么用,但在将要复制的结构体中某个属性设置为不复制时非常有用。


    ------ 虚幻引擎官方文档《复制Actor属性》

  2. 在 "Weapon.cpp" 的 "OnSphereOverlap()" 函数中调用 "SetOverlappingWeapon(),接着在 "Weapon.h" 中声明 "ShowPickupWidget()" 函数,并在 "Weapon.cpp" 中完成 "ShowPickupWidget()" 函数的定义,该函数用于设置拾取组件可见;然后在尝试在 "BlasterCharacter.cpp" 的 "Tick()" 函数中调用 "ShowPickupWidget()" 函数。

    cpp 复制代码
    /*** Weapon.cpp ***/
    
    ...
    
    void AWeapon::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OhterComponent, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    	// 需要先添加头文件 "Blaster/Character/BlasterCharacter.h"
    	ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);	// 类型转换为 OtherActor
    	if (BlasterCharacter && PickupWidget) {										// 确保 BlasterCharacter 和 PickupWidget 都不是空指针
    		/* P38 变量复制(Variable Replication)*/
    		// PickupWidget->SetVisibility(true);									// 设置拾取组件可见
    		BlasterCharacter->SetOverlappingWeapon(this);							// 设置重叠武器类
    		/* P38 变量复制(Variable Replication)*/
    	}
    }
    
    /* P38 变量复制(Variable Replication)*/
    void AWeapon::ShowPickupWidget(bool bShowWidget)
    {
    	if (PickupWidget) {
    		PickupWidget->SetVisibility(bShowWidget);								// 设置拾取组件可见
    	}
    }
    /* P38 变量复制(Variable Replication)*/
    cpp 复制代码
    /*** BlasterCharacter.cpp ***/
    
    /* P38 变量复制(Variable Replication)*/
    #include "Net/UnrealNetwork.h"
    #include "Blaster/Weapon/Weapon.h"
    /* P38 变量复制(Variable Replication)*/
    
    ...
    
    // Called every frame
    void ABlasterCharacter::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	/* P38 变量复制(Variable Replication)*/
    	// 如果有与人物角色重叠的武器,则在每个 tick 都显示拾取组件,需要添加头文件 "Blaster/Weapon/Weapon.h"
    	if (OverlappingWeapon) OverlappingWeapon->ShowPickupWidget(true);
    	/* P38 变量复制(Variable Replication)*/
    }
    
    ...
  3. 编译后在虚幻引擎中打开关卡 "BlasterMap" 进行测试,当我们控制其中一个客户端与武器发生重叠,可以看到拾取组件会显示在服务器和所有客户端上,这是因为在 "Tick()" 函数中,我们已标记可复制的属性 "OverlappingWeapon" 会被复制到所有客户端,但是我们只想要在这个客户端上显示拾取组件,而不想在服务器和其他客户端上上显示,说明当前方法行不通。

  4. 我们尝试在 "BlasterCharacter.cpp" 中将 "GetLifetimeReplicatedProps()" 函数里的 "DOREPLIFETIME()" 改为 "DOREPLIFETIME_Condition()",设置仅拥有者可复制属性 "OverlappingWeapon"。

    cpp 复制代码
    /*** BlasterCharacter.cpp ***/
    
    ...
    
    /* P38 变量复制(Variable Replication)*/
    void ABlasterCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	// 调用Super
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	// 添加要为派生的类 ABlasterCharacter 复制的属性,需要添加头文件 "Net/UnrealNetwork.h"
    	// DOREPLIFETIME(ABlasterCharacter, OverlappingWeapon);
    	DOREPLIFETIME_CONDITION(ABlasterCharacter, OverlappingWeapon, COND_OwnerOnly);	// 仅拥有者可复制
    }
    /* P38 变量复制(Variable Replication)*/
    
    ...

复制条件引用
COND_None:没有条件,一旦更改即复制。
COND_InitialOnly:仅尝试在初始系列上复制。
COND_OwnerOnly:仅复制到 Actor 的所有者。
COND_SkipOwner:复制到除 Actor 的所有者之外的每个连接。
COND_SimulatedOnly:复制到模拟的 Actor。
COND_AutonomousOnly:仅复制到自主 Actor。
COND_SimulatedOrPhysics:复制到模拟的或 bRepPhysics Actor。
COND_InitialOrOwner:在初始系列上复制,或复制到Actor的所有者。
COND_Custom:没有特定条件,但能够打开或关闭。请参阅自定义属性复制了解更多信息。
COND_ReplayOrOwner:仅复制到重播连接或Actor的所有者。
COND_ReplayOnly:仅复制到重播连接。
COND_SimulatedOnlyNoReplay:仅复制到模拟的Actor,但不复制到重播连接。
COND_SimulatedOrPhysicsNoReplay:复制到模拟的或 bRepPhysics Actor,但不复制到重播连接。
COND_SkipReplay:不复制到重播连接。
COND_Dynamic:在运行时重载条件。默认为总是复制,除非你将其重载为新条件。
COND_Never:从不复制。


自定义属性复制

要更精细地控制 Actor 属性何时复制,你可以将 "COND_Custom" 复制条件与 "DOREPLIFETIME_ACTIVE_OVERRIDE" 宏一起使用。此宏可帮助你创建自定义条件来规定属性何时复制。此复制按 Actor 规定,而不是按连接规定。因此,在自定义条件中使用可能因连接而异的状态是不安全的。


------ 虚幻引擎官方文档《复制Actor属性》

  1. 编译后再次进行测试,可以看到当我们控制其中一个客户端人物角色移动至与武器发生重叠时,可以看到拾取组件在另一个客户端上不会显示,但还是会在服务器上显示,不符合我们的要求,这说明在 "Tick()" 函数中设置拾取组件可见的方法是行不通的。

38.2 创建 Repnotify 函数

  1. 在控制一个客户端上的人物角色移动至与武器发生重叠时,我们只想要在这个客户端上显示拾取组件,而不想在服务器上和其他客户端上显示,这里就需要使用到 "Repnotify" 函数,它可以在指定的属性发生复制时被调用以执行特定操作。

    添加 "Replicated Using" 属性

    你可以使用 "UPROPERTY" 宏中的 "Replicated" 说明符复制 Actor 属性。你可以使用 "ReplicatedUsing" 说明符在每次复制变量时执行特定操作。要在每次复制你的属性时执行操作,你可以使用 "ReplicatedUsing" 说明符和相关联的 "RepNotify"。"RepNotify" 是复制带 "ReplicatedUsing" 说明符的属性时对客户端调用的 "OnRep_" 函数。


    ------ 虚幻引擎官方文档《复制 Actor 属性》

  2. 在 "BlasterCharacter.h" 中使用 "UPROPERTY" 宏中的 "ReplicatedUsing" 说明符声明属性 "OverlappingWeapon",接着声明与之相关联的 "Repnotify" 函数 "OnRep_OverlappingWeapon()";然后,在 "BlasterCharacter.cpp" 中完成"OnRep_OverlappingWeapon()" 函数的定义,删除或注释 "Tick()" 函数中设置显示拾取组件的代码。

    cpp 复制代码
    /*** BlasterCharacter.h ***/
    
    ...
    
    UCLASS()
    class BLASTER_API ABlasterCharacter : public ACharacter
    {
    	GENERATED_BODY()
    
    	...
    
    private:
    
    	...
    
    	/* P38 变量复制(Variable Replication)*/
    	// UPROPERTY(Replicated)
    	// class AWeapon* OverlappingWeapon;
    	UPROPERTY(ReplicatedUsing = OnRep_OverlappingWeapon)	// 指定 OverlappingWeapon 的 Repnotify 函数为 OnRep_OverlappingWeapon()
    	class AWeapon* OverlappingWeapon;
    
    	UFUNCTION()
    	void OnRep_OverlappingWeapon();							// OverlappingWeapon 的 Repnotify 函数
    	/* P38 变量复制(Variable Replication)*/
    
    	...
    }
    cpp 复制代码
    /*** BlasterCharacter.cpp ***/
    
    ...
    /* P38 变量复制(Variable Replication)*/
    // 武器重叠属性 Repnotify 函数
    void ABlasterCharacter::OnRep_OverlappingWeapon()
    {
    	
    	// 如果有与人物角色重叠的武器,则显示拾取组件
    	if (OverlappingWeapon) {
    		OverlappingWeapon->ShowPickupWidget(true);
    	}
    	
    }
    /* P38 变量复制(Variable Replication)*/
    
    // Called every frame
    void ABlasterCharacter::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	// /* P38 变量复制(Variable Replication)*/
    	// // 如果有与人物角色重叠的武器,则在每个 tick 都显示拾取组件
    	// if (OverlappingWeapon) OverlappingWeapon->ShowPickupWidget(true);
    	// /* P38 变量复制(Variable Replication)*/
    }
    
    ...
  3. 编译后进行测试,可以发现拾取组件只显示在我们控制的客户端上,在服务器上和其他客户端上都不显示。但是这里又出现一个值得注意的细节(A import detail to take note of):当我们控制服务器上的人物角色移动至与武器发生重叠时,服务器上无法显示拾取组件,显然我们在服务器上没有调用到 "Repnotify" 函数,这是因为属性复制是单向的,只能以从服务器复制到客户端 ,而不能从客户端复制到服务器上,因此服务器不会调用 "Repnotify" 函数。

  4. 为解决这个问题,我们不能只在 "Repnotify" 函数中设置拾取组件可见,还需要添加单独处理服务器上设置重叠武器和显示拾取组件的代码,于是在 "BlasterCharacter.h" 和 "BlasterCharacter.cpp" 中修改 "SetOverlappingWeapon()" 的定义,这里我们可以借助 "IsLocallyControlled()" 函数判断当前 Pawn 是否为在本地控制的人物(而不是镜像人物)。

    cpp 复制代码
    /*** BlasterCharacter.h ***/
    
    ...
    
    UCLASS()
    class BLASTER_API ABlasterCharacter : public ACharacter
    {
    	GENERATED_BODY()
    
    	...
    
    private:
    
    	...
    
    	/* P38 变量复制(Variable Replication)*/
    	// UPROPERTY(Replicated)
    	// class AWeapon* OverlappingWeapon;
    	UPROPERTY(ReplicatedUsing = OnRep_OverlappingWeapon)	// 指定 OverlappingWeapon 的 Repnotify 函数为 OnRep_OverlappingWeapon()
    	class AWeapon* OverlappingWeapon;
    
    	UFUNCTION()
    	void OnRep_OverlappingWeapon();							// OverlappingWeapon 的 Repnotify 函数
    
    public:
    	// 设置重叠的武器实例,以便在武器类 Weapon 的 OnSphereOverlap() 函数中调用 
    	// FORCEINLINE void SetOverlappingWeapon(AWeapon* Weapon) { OverlappingWeapon = Weapon; }
    	void SetOverlappingWeapon(AWeapon* Weapon);
    
    	/* P38 变量复制(Variable Replication)*/
    }
    cpp 复制代码
    /*** BlasterCharacter.cpp ***/
    
    ...
    /* P38 变量复制(Variable Replication)*/
    // 武器重叠属性 Repnotify 函数
    void ABlasterCharacter::OnRep_OverlappingWeapon()
    {
    	
    	// 如果有与人物角色重叠的武器,则显示拾取组件
    	if (OverlappingWeapon) {
    		OverlappingWeapon->ShowPickupWidget(true);
    	}
    	
    }
    
    // 设置与人物角色重叠的武器实例类到属性 OverlappingWeapon 中,并设置拾取组件可见
    void ABlasterCharacter::SetOverlappingWeapon(AWeapon* Weapon)
    {
    	OverlappingWeapon = Weapon;
    
    	// 使用 IsLocallyControlled() 可以快速判断当前 Pawn 自己是否为主控端,即本地控制的人物(而不是镜像人物)
    	if (IsLocallyControlled) {
    		if (OverlappingWeapon) {
    			OverlappingWeapon->ShowPickupWidget(true);
    		}
    	}
    }
    /* P38 变量复制(Variable Replication)*/
    ...
  5. 编译后再次进行测试,可以发现在控制服务器上的人物角色移动至与武器发生重叠时,拾取组件也能显示在服务器上了,而在所有客户端上均不显示。


38.3 创建拾取组件结束重叠事件

  1. 在测试中当我们控制人物角色移动至与武器结束重叠时,拾取组件还依然显示,接下来要在 "Weapon.h" 和 "Weapon.cpp" 中重写拾取组件结束重叠事件 "OnSphereEndOverlap()",使得拾取组件不显示,"OnSphereEndOverlap()" 需要动态绑定到武器球体组件 "AreaSphere" 的 "`OnComponentEndOverlap``" 的委托上。

    cpp 复制代码
    /*** Weapon.h ***/
    
    ...
    
    UCLASS()
    class BLASTER_API AWeapon : public AActor
    {
    	GENERATED_BODY()
    	
    	...
    
    public:	
    	// Called every frame
    	virtual void Tick(float DeltaTime) override;
    
    	UFUNCTION()					// 需要动态绑定到 OnComponentBeginOverlap 的委托上,因此需要将函数设置为 UFunction
    	virtual void OnSphereOverlap(
    		UPrimitiveComponent*  OverlappedComponent,
    		AActor* OtherActor,
    		UPrimitiveComponent* OhterComponent,
    		int32 OtherBodyIndex,
    		bool bFromSweep,
    		const FHitResult& SweepResult
    	);
    
    	/* P38 变量复制(Variable Replication)*/
    	UFUNCTION()					
    	virtual void OnSphereEndOverlap(
    			UPrimitiveComponent* OverlappedComponent,
    			AActor* OtherActor,
    			UPrimitiveComponent* OhterComponent,
    			int32 OtherBodyIndex
    	);
    	/* P38 变量复制(Variable Replication)*/
    
    	...
    }
    cpp 复制代码
    /*** Weapon.cpp ***/
    
    ...
    
    // Called when the game starts or when spawned
    void AWeapon::BeginPlay()
    {
    	Super::BeginPlay();
    
    	// 服务器将负责掌管(in charge of)所有武器权限
    	if (HasAuthority()) {															// 判断是否拥有权威角色,等同于 if (GetLocalRole() == ENetRole::ROLE_Authority) 
    		AreaSphere->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);		// 既进行碰撞检测,也进行碰撞响应。物体之间会检测是否发生碰撞,同时会产生物理效果
    		AreaSphere->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
    		AreaSphere->OnComponentBeginOverlap.AddDynamic(this, &AWeapon::OnSphereOverlap);
    		/** Delegate for notification of start of overlap with a specific component */
    		/* DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_SixParams(FComponentBeginOverlapSignature, 
    																UPrimitiveComponent, 
    																OnComponentBeginOverlap, 
    																UPrimitiveComponent*, 
    																OverlappedComponent, 
    																AActor*, OtherActor, 
    																UPrimitiveComponent*, 
    																OtherComp, int32, 
    																OtherBodyIndex, 
    																bool,
    																bFromSweep, 
    																FHitResult&, 
    																SweepResult);
    		*/
    
    
    		/* P38 变量复制(Variable Replication)*/
    		AreaSphere->OnComponentEndOverlap.AddDynamic(this, &AWeapon::OnSphereEndOverlap);	// 动态绑定到 OnComponentEndOverlap 的委托上
    		/* P38 变量复制(Variable Replication)*/
    	}
    
    	
    	if (PickupWidget) {									// 确保 PickupWidget 不是空指针
    		PickupWidget->SetVisibility(false);				// 先设置拾取组件不可见
    	}
    }
    
    ...
    
    void AWeapon::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OhterComponent, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
    {
    	
    	// 需要先添加头文件 "Blaster/Character/BlasterCharacter.h"
    	ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);	// 类型转换为 OtherActor
    	if (BlasterCharacter && PickupWidget) {										// 确保 BlasterCharacter 和 PickupWidget 都不是空指针
    		/* P38 变量复制(Variable Replication)*/
    		// PickupWidget->SetVisibility(true);									// 设置拾取组件可见
    		BlasterCharacter->SetOverlappingWeapon(this);							// 设置重叠武器
    		/* P38 变量复制(Variable Replication)*/
    	}
    }
    
    /* P38 变量复制(Variable Replication)*/
    void AWeapon::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OhterComponent, int32 OtherBodyIndex)
    {
    	ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);	// 类型转换为 OtherActor
    	if (BlasterCharacter && PickupWidget) {										// 确保 BlasterCharacter 和 PickupWidget 都不是空指针
    		BlasterCharacter->SetOverlappingWeapon(nullptr);						// 设置重叠武器为空指针
    	}
    }
    
    void AWeapon::ShowPickupWidget(bool bShowWidget)
    {
    	if (PickupWidget) {
    		PickupWidget->SetVisibility(bShowWidget);								// 设置拾取组件可见
    	}
    }
    /* P38 变量复制(Variable Replication)*/
  2. 在 "OnSphereEndOverlap()" 中我们通过设置重叠武器为空指针 "nullptr" 来隐藏拾取组件,为了使得人物角色移动至与武器再次发生重叠时能够显示拾取组件,可以为 "Repnotify" 函数 "OnRep_OverlappingWeapon()" 添加一个输入参数(只能添加一个),用来表示上一个与人物角色发生重叠的武器类实例,这样就可以在复制发生之前保存在 "BlasterCharacter" 的 "OverlappingWeapon" 属性中,这里的思路非常巧妙,详情可以参阅官方文档的说明以及代码中的注释。

    "RepNotify" 中参数的用法

    虚幻引擎的复制系统支持在属性的 "RepNotify" 中传递类型与复制的属性相同的参数。 如果你在 "RepNotify" 中传递参数,复制系统会自动将复制属性的上一个值传递 到 "RepNotify" 调用中。


    ------ 虚幻引擎官方文档《复制 Actor 属性》

    cpp 复制代码
    /*** BlasterCharacter.h ***/
    UCLASS()
    class BLASTER_API ABlasterCharacter : public ACharacter
    {
    	GENERATED_BODY()
    
    	...
    
    private:
    	
    	...
    
    	/* P38 变量复制(Variable Replication)*/
    	// UPROPERTY(Replicated)
    	// class AWeapon* OverlappingWeapon;
    	UPROPERTY(ReplicatedUsing = OnRep_OverlappingWeapon)	// 指定 OverlappingWeapon 的 Repnotify 函数为 OnRep_OverlappingWeapon()
    	class AWeapon* OverlappingWeapon;
    
    	UFUNCTION()
    	void OnRep_OverlappingWeapon(AWeapon* LastWeapon);		// OverlappingWeapon 的 Repnotify 函数
    	/* P38 变量复制(Variable Replication)*/
    CPP 复制代码
    /*** BlasterCharacter.cpp ***/
    
    ...
    
    // 武器重叠属性 Repnotify 函数
    /* P38 变量复制(Variable Replication)*/
    void ABlasterCharacter::OnRep_OverlappingWeapon(AWeapon* LastWeapon)
    {
    	// 如果人物角色与武器发生重叠,则显示拾取组件
    	// 如果人物角色与重叠的武器重叠结束,OverlappingWeapon 将会被设置为空指针,下面的语句将不会执行 
    	if (OverlappingWeapon) {
    		OverlappingWeapon->ShowPickupWidget(true);	// 设置拾取组件可见
    	}
    
    	// 传递参数 LastWeapon 与属性 OverlappingWeapon 类型相同,复制系统会自动将属性 OverlappingWeapon 发生变化前的上一个值保存在 LastWeapon 中
    	// 也就是说如果人物角色与重叠的武器重叠结束,OverlappingWeapon 的值在被设置为空指针之前,LastWeapon 中就已经保存了发生变化前 OverlappingWeapon 的值(不为空指针),下面的语句将被执行
    	// 而当人物角色与再次与武器重叠时,OverlappingWeapon 的值在被设置为武器类实例之前,LastWeapon 中就已经保存了发生变化前 OverlappingWeapon 的值(空指针),下面的语句将不被执行
    	if (LastWeapon) {
    		LastWeapon->ShowPickupWidget(false);		// 设置拾取组件不可见
    	}
    }
    /* P38 变量复制(Variable Replication)*/
  3. 38.2 创建 Repnotify 函数 步骤 3 ,由于属性复制是单向的,只能以从服务器复制到客户端,而不能从客户端复制到服务器上,因此服务器不会调用 "Repnotify" 函数。由于我们修改了 "Repnotify" 函数的代码,因此还需重新单独处理服务器上设置重叠武器和显示拾取组件的代码;否则,当控制服务器上的人物角色移动至与武器结束重叠时,拾取组件仍然可见。

    cpp 复制代码
    /*** BlasterCharacter.cpp ***/
    
    ...
    
    // 设置与人物角色重叠的武器实例类到属性 OverlappingWeapon 中,并设置拾取组件可见
    void ABlasterCharacter::SetOverlappingWeapon(AWeapon* Weapon)
    {
    	// 在服务器上,当人物角色与武器结束重叠时调用 SetOverlappingWeapon(nullptr),此时 OverlappingWeapon 的值为发生重叠的武器类实例,先执行下面的 if 语句块隐藏拾取组件,然后再设置其值为空指针
    	// 在服务器上,当人物角色再次与武器开始重叠时调用 SetOverlappingWeapon(Weapon),此时 OverlappingWeapon 的值为空指针,下面的 if 语句块将不被执行,直接设置其值为重叠的武器类实例
    	if (OverlappingWeapon) {						
    		OverlappingWeapon->ShowPickupWidget(false);
    	}
    
    	OverlappingWeapon = Weapon;
    
    	// 使用 IsLocallyControlled() 可以快速判断当前 Pawn 是否为自己是否为主控端,即本地控制的人物(而不是镜像人物)
    	if (IsLocallyControlled()) {
    		if (OverlappingWeapon) {
    			OverlappingWeapon->ShowPickupWidget(true);	// 还是需要先判断 OverlappingWeapon 是否为空指针,如果不判断人物角色与武器结束重叠时 UE 会闪退
    		}
    	}
    }
    ...
  4. 编译后进行测试,可以看到我们无论是控制客户端还是服务器上的人物角色移动至与武器结束重叠,拾取组件都会变得不可见。



38.4 Summary

本节课为了在客户端上实现人物角色与武器开始重叠时显示拾取组件的功能,我们学习了属性复制与 "RepNotify" 函数、网络同步实现的基本原理。我们通过为 "BlasterCharacter" 添加带有 "Replicated" 说明符的 "OverlappingWeapon" 属性,来表示与人物角色重叠的武器类实例,接着重写 "GetLifetimeReplicatedProps()" 函数,在函数中我们调用了 "DOREPLIFETIME" 宏,用于指定需要被复制的属性;然后直接在 "BlasterCharacter" 的 "Tick()" 函数中直接调用显示拾取组件的函数,但这会导致所有客户端和服务器同步显示拾取组件,而我们只想在人物角色与武器重叠的这个客户端上显示。我们尝试将 "DOREPLIFETIME()" 宏替换至 "DOREPLIFETIME_CONDITION()" 宏,虽然可以其他客户端不显示拾取组件,但服务器端仍会显示。

此时,就需要使用到 "Repnotify" 函数,它可以在指定的属性发生复制时被调用以执行特定操作,我们首先创建了 "OnRep_OverlappingWeapon()" 这个不带任何输入参数的 "RepNotify" 函数:该函数在属性复制时自动触发,配合 "IsLocallyControlled()" 检测当前是否为主控端可精准控制提示显示范围:当客户端角色与武器重叠时,仅当前控制客户端显示提示;因为属性复制是单向的,只能以从服务器复制到客户端,而不能从客户端复制到服务器上,因此服务器不会调用 "Repnotify" 函数,因此我们还通过在 "SetOverlappingWeapon()" 函数中添加本地控制检测来单独处理服务器显示逻辑。

为解决结束重叠时的拾取组件隐藏的功能,我们在武器类中定义了重叠结束函数,并将其动态绑定了 "OnComponentEndOverlap()" 的委托上,在函数中我们调用 "SetOverlappingWeapon(nullptr)",即设置重叠武器为空指针。接下来就是本节课最耐人寻味的地方:我们巧妙地利用 "RepNotify" 函数的特性:为 "OnRep_OverlappingWeapon()" 函数加入与 "OverlappingWeapon" 类型相同的输入参数 "LastWeapon",虚幻引擎的复制系统会自动将复制属性的上一个值传递到 "RepNotify" 函数调用中,这让我们能同时处理新旧重叠武器的显隐状态切换,完美解决服务器端结束重叠时的拾取组件无法隐藏的问题。


相关推荐
凤年徐3 小时前
【数据结构初阶】单链表
c语言·开发语言·数据结构·c++·经验分享·笔记·链表
oioihoii3 小时前
C++11 右值引用:从入门到精通
开发语言·c++
阿阳微客5 小时前
Steam 搬砖项目深度拆解:从抵触到真香的转型之路
前端·笔记·学习·游戏
木子.李3477 小时前
排序算法总结(C++)
c++·算法·排序算法
freyazzr8 小时前
C++八股 | Day2 | atom/函数指针/指针函数/struct、Class/静态局部变量、局部变量、全局变量/强制类型转换
c++
fpcc9 小时前
跟我学c++中级篇——理解类型推导和C++不同版本的支持
开发语言·c++
终焉代码9 小时前
STL解析——list的使用
开发语言·c++
DevangLic10 小时前
【 *p取出内容 &a得到地址】
c++
鑫鑫向栄10 小时前
[蓝桥杯]修改数组
数据结构·c++·算法·蓝桥杯·动态规划
鑫鑫向栄10 小时前
[蓝桥杯]带分数
数据结构·c++·算法·职场和发展·蓝桥杯