听课笔记
Weapon类初步主体
实现了该类的基础设置
建立一个继承自Actor
的C++类,放在相应的文件夹中。按项目结构梳理一下,应当放在与GameMode
、HUD
同级的地方,建立一个新的文件夹,以备后续派生出各种具体的武器,都放在这个文件夹里。
这个Weapon类也不是最终的Weapon类,后续会派生出射弹武器类和扫描武器类。此处仅实现通用的功能即可。
Weapon类基础代码
随后蓝图实例化配置一下网格体和碰撞检测球体就好了。主要代码如下
// header file
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Weapon.generated.h"
UENUM(BlueprintType)
enum class EWeaponState : uint8
{
EWS_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 properties
AWeapon();
virtual void Tick(float DeltaTime) override;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
private:
// visiable anywhere so can be seen in blueprint
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
USkeletalMeshComponent* WeaponMesh;
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
class USphereComponent* AreaSphere;
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
EWeaponState WeaponState;
};
// cpp file
// Fill out your copyright notice in the Description page of Project Settings.
#include "Weapon.h"
#include "Components/SphereComponent.h"
#include "Components/WidgetComponent.h"
#include "Blaster/Character/BlasterCharacter.h"
// Sets default values
AWeapon::AWeapon()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
bReplicates = true;
WeaponMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WeaponMeesh"));
WeaponMesh->SetupAttachment(RootComponent);
//SetRootComponent(WeaponMesh);
WeaponMesh->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Block);
WeaponMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);
WeaponMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// this component is what we use to detected overlap with players
// Once they overlap, we want to pickup weapon
AreaSphere = CreateDefaultSubobject<USphereComponent>(TEXT("AreaSphere"));
AreaSphere->SetupAttachment(RootComponent);
AreaSphere->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
// only enabled on server
AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
// Called when the game starts or when spawned
void AWeapon::BeginPlay()
{
Super::BeginPlay();
// same as if (GetLocalRole() == ENetRole::ROLE_Authority)
if (HasAuthority())
{
AreaSphere->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
AreaSphere->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
}
}
// Called every frame
void AWeapon::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
关于bCanEverTick
PrimaryActorTick.bCanEverTick = false;
If false, this tick function will never be registered and will never tick. Only settable in defaults.
关于replicating actor
不太相关的参考资料ReplicateActor学习笔记
同步Actor是网络游戏要考虑的一个问题,因为其状态和相关操作应当以服务器的权威版本为主。
为了将武器设为同步的,使用了下方代码
// in constructor
bReplicates = true;
该部分内容亦在官方文档中有讨论,文档连接中的“Actor复制”小节即是,由于其内容详尽,此处不再多言。
关于SetRootComponent
我不知道教程为什么前半截讲weapon.cpp
时出现了这个SetRootComponent(WeaponMesh);
,但是最后这节课github提交中并未出现。事实上代码中存在这行语句会导致编辑器无法启动:
最直观的原因便是该函数要求传入的参数类型是USceneComponent
,而WeaponMesh
的类型是USkeletalMeshComponent
,显然不符合。
bool SetRootComponent
(
USceneComponent * NewRootComponent
)
关于RootComponent
,它是定义在Actor.h
的变量(所以不用重新声明)
The component that defines the transform (location, rotation, scale) of this Actor in the world, all other components must be attached to this one somehow
一般对RootComponent
的使用方式就是在.h
文件中定义:
UPROPERTY(EditAnywhere)
USceneComponent* OurVisibleComponent;
该变量标记为 UPROPERTY
,因此其将对 虚幻引擎 可见。此设置可防止启动游戏时,或项目/关卡关闭后重新载入时重设该变量。
然后在相应的.cpp
文件中使用:
// 创建可附加内容的虚拟根组件。
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));
// 创建相机和可见对象
UCameraComponent* OurCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("OurCamera"));
OurVisibleComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("OurVisibleComponent"));
// 将相机和可见对象附加到根组件。偏移并旋转相机。
OurCamera->SetupAttachment(RootComponent);
OurCamera->SetRelativeLocation(FVector(-250.0f, 0.0f, 250.0f));
OurCamera->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
OurVisibleComponent->SetupAttachment(RootComponent);
关于CollisionResponse
参考官方文档碰撞响应参考
WeaponMesh->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Block);
WeaponMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);
WeaponMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
注意第三条语句,只有在启用碰撞(Collision Enabled)设为无碰撞(No Collision)以外的值才能进一步决定碰撞响应(Collision Responses)的策略。也就是只有“能碰撞”后才能决定“如何碰撞”
并且碰撞事件分为两种:重叠(Overlap)和阻挡(Block)。
为了让武器丢在地上后能够被角色越过去,所以才有的前两条设置,但是一开始武器不是丢在地上的状态,所以干脆直接都设为ECollisionEnabled::NoCollision
,待后续依据武器状态再改变。
武器两个主要部分,一个是描述武器样子的网格体,另一个是用于和人物碰撞(产生重叠事件意味着可以拾起)
Overlap Event与服务器
为了安全性,武器仅在服务端启用Overlap Event。
AreaSphere->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
在随后的BeginPlay
中鉴定是否是服务端,如果是则启用。
// same as if (GetLocalRole() == ENetRole::ROLE_Authority)
if (HasAuthority())
{
AreaSphere->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
AreaSphere->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
}
服务器掌管所有武器对象,并且为其处理重叠事件。
关于UENUM
官方文档:元数据说明符
声明类、接口、结构体、列举、列举值、函数,或属性时,可添加元数据说明符来控制其与引擎和编辑器各方面的相处方式。每一种类型的数据结构或成员都有自己的元数据说明符列表。
要添加元数据说明符,需使用单词meta
,后接说明符列表。如有必要,可以将它们的值添加到 UCLASS
、UENUM
、UINTERFACE
、USTRUCT
、UFUNCTION
或UPROPERTY
宏,如下所示:
{UCLASS/UENUM/UINTERFACE/USTRUCT/UFUNCTION/UPROPERTY}(SpecifierX, meta=(MetaTag1="Value1", MetaTag2, ..), SpecifierY)
要添加元数据说明符到列举值,可将UMETA
标签添加到值本身。如果存在用于分隔的逗号,则要添加到逗号之前,如下所示:
UENUM()
enum class EMyEnum : uint8
{
// Default Value Tooltip
DefaultValue = 0 UMETA(MetaTag1="Value1", MetaTag2, ..),
// ValueWithoutMetaSpecifiers Tooltip
ValueWithoutMetaSpecifiers,
// ValueWithMetaSpecifiers Tooltip
ValueWithMetaSpecifiers UMETA((MetaTag1="Value1", MetaTag2, ..),
// FinalValue Tooltip
FinalValue (MetaTag1="Value1", MetaTag2, ..)
};
对比自己代码中的内容不难理解其含义
UENUM(BlueprintType)
enum class EWeaponState : uint8
{
EWS_Initial UMETA(DisplayName = "Initial State"),
EWS_Equipped UMETA(DisplayName = "Equipped"),
EWS_Dropped UMETA(DisplayName = "Dropped"),
EWS_MAX UMETA(DisplayName = "DefaultMAX")
};
武器的拾取:服务端部分
提示控件的基本设置
一方面是控件的制作,本身很简单,主要就是一个text block
。
因为这个控件是外部的,需要为Weapon添加控件,一个指向它的指针,代码如下:
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
class UWidgetComponent* PickupWidget;
另一方面是委托的绑定,可见下文重叠事件部分。
基础代码
如上图所示,在原先代码基础上,增加了一些内容,主要是一个UWidgetComponent
// Weapon.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Weapon.generated.h"
UENUM(BlueprintType)
enum class EWeaponState : uint8
{
EWS_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 properties
AWeapon();
virtual void Tick(float DeltaTime) override;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
UFUNCTION()
virtual void OnSphereOverlap(
UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult
);
private:
// visiable anywhere so can be seen in blueprint
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
USkeletalMeshComponent* WeaponMesh;
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
class USphereComponent* AreaSphere;
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
EWeaponState WeaponState;
UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
class UWidgetComponent* PickupWidget;
};
// Weapon.cpp
#include "Weapon.h"
// Fill out your copyright notice in the Description page of Project Settings.
#include "Weapon.h"
#include "Components/SphereComponent.h"
#include "Components/WidgetComponent.h"
#include "Blaster/Character/BlasterCharacter.h"
// Sets default values
AWeapon::AWeapon()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
bReplicates = true;
WeaponMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WeaponMeesh"));
//WeaponMesh->SetupAttachment(RootComponent);
SetRootComponent(WeaponMesh);
WeaponMesh->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Block);
WeaponMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);
WeaponMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// this component is what we use to detected overlap with players
// Once they overlap, we want to pickup weapon
AreaSphere = CreateDefaultSubobject<USphereComponent>(TEXT("AreaSphere"));
AreaSphere->SetupAttachment(RootComponent);
AreaSphere->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
// only enabled on server
AreaSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
PickupWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("PickupWidget"));
PickupWidget->SetupAttachment(RootComponent);
}
// Called when the game starts or when spawned
void AWeapon::BeginPlay()
{
Super::BeginPlay();
// same as if (GetLocalRole() == ENetRole::ROLE_Authority)
if (HasAuthority())
{
AreaSphere->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
AreaSphere->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
}
}
void AWeapon::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
}
// Called every frame
void AWeapon::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
重叠事件
有关碰撞,可先参阅如下官方文档:
两个Actor都需要设置为阻挡彼此相应的对象类型。如果不这样设置,就不会发生碰撞。
(上述内容与正文无关)
重点来看OnSphereOverlap
这个函数,我们可在这个函数中设置控件可见性:
// in Weapon.h
UFUNCTION()
virtual void OnSphereOverlap(
UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult
);
该函数应当只在服务端调用,故而添加在HasAuthority()
的if检验语句内下列内容:
// Weapon.cpp中的HasAuthority的if语句体内
AreaSphere->OnComponentBeginOverlap.AddDynamic(this, &AWeapon::OnSphereOverlap);
另一方面,我们要把它绑定到AeraSphere
的OnComponentBeginOverlap
委托上。这里就要解释了为什么该函数必须有这些参数:
查看OnComponentBeginOverlap
的定义,得知它是FComponentBeginOverlapSignature
类型的委托,再看他的定义
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_SixParams(
FComponentBeginOverlapSignature,
UPrimitiveComponent, OnComponentBeginOverlap,
UPrimitiveComponent*, OverlappedComponent,
AActor*, OtherActor,
UPrimitiveComponent*, OtherComp,
int32, OtherBodyIndex,
bool, bFromSweep,
const FHitResult &, SweepResult);
对于其实现,使用了如下代码:
ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);
if (BlasterCharacter && PickupWidget)
{
PickupWidget->SetVisibility(true);
}
意思是,如果重叠的那个物体,能够转换为ABlasterCharacter
类型,换言之重叠的物体是人物类(在本文即ABlasterCharacter
,这个人物类名称只是这个项目的叫法,在别的项目可能有别的名字)的实例,或者说人物与之发生了重叠,才可能有后续操作。
当然,一开始这个控件应当是不显示的,所以BeginPlay
函数中应当加上下面语句,让它不具有可见性
if (PickupWidget)
{
PickupWidget->SetVisibility(false);
}
接下来需要解决客户端上的控件显示问题,因为重叠事件只在服务端产生,客户端是不知道的。因此这就涉及到复制(Replication),我们需要一个状态,客户端可以从服务端同步这个状态,以进行显示控件等操作。
武器的拾取:客户端部分
变量复制
C++:使用UPROPERTY宏进行标记。仅客户端执行,服务端也要执行一些功能可以考虑使用RPC
蓝图:细节面板中指定。服务端和客户端都可以执行。
格式:
UPROPERTY(ReplicatedUsing=xxx)
Rep Notify
- 某些变量(如生命值)由于安全性,应当仅在服务端进行修改,客户端进行同步
- 更新值之后可指定一个调用的函数,该函数就是一个
Rep Notify
Rep Notify
仅在变量更新时本地触发Rep Notify
比RPC节约带宽(显而易见地…)- 用途举例:UI的修改(比如在更新生命值之后修改血量UI)
当然,接上文,ReplicatedUsing=xx
这里的函数xxx
应当使用UFUNCTION()
修饰,哪怕该宏内部没有任何东西。
蓝图变量设为RepNotify后自动产生对应的
Rep Notify
,而C++需要手动设置
局部代码逻辑
- 声明复制的变量
- 将之注册为要复制的
- 声明更新时要调用的
RepNotify
- 实现该
RepNotify
的具体功能
这里代码的逻辑也很明确了,由于overlap的主动发起者是玩家,所以在玩家的头文件中声明那个要复制的变量:
// .h private section
UPROPERTY(ReplicatedUsing = OnRep_OverlappingWeapon)
class AWeapon* OverlappingWeapon;
// .cpp defination
void ABlasterCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ABlasterCharacter, OverlappingWeapon);
}
其逻辑是:有一个重叠的武器,如果重叠就更新这个类型为AWeapon*
的指针,当它不为NULL时则意味着人物与某个武器重叠,此时应当显示控件。当然,这里没考虑重叠多个武器的情况。
我们在GetLifetimeReplicatedProps
函数中将变量OverlappingWeapon
通过DOREPLIFETIME
宏注册为replicated
,该宏的第一个参数是该变量所在类的名称。
为了使用该宏,应当引入头文件#include "Net/UnrealNetwork.h"
ShowPickupWidget
是在玩家的Tick()
中调用的,这不是高效的,后面会进行修改。
为什么是玩家调用武器的该函数,而非武器直接调用,是因为主被动关系。如果武器是主动的,那么会出现别的玩家重叠之后在本机显示控件的情况。而我们期望的只是我这个客户端,在服务端发生了overlap事件,然后玩家因为Weapon
的Overlap事件
修复1:仅在当前客户端有效
上面的问题是所有的客户端都能看见这个控件,我们应当确保参与Overlap事件的客户端是本机所拥有的才显示控件。
但是这样的复制不完全有意义:复制给其他没有Overlap的客户端,它们用不到,还占带宽。
所以只复制给拥有该角色(指参与Overlap的角色)的客户端是最理想的。
因此DOREPLIFETIME
宏就得被修改成另一种宏,以确保该属性仅发送至 actor 的所有者。
DOREPLIFETIME_CONDITION(ABlasterCharacter, OverlappingWeapon, COND_OwnerOnly);
也就是你这台机器有该pawn的所有权,或者说你控制着这个pawn,再换句话说就是you are the owner of the pawn,在这种情况下服务器应当复制给作为客户端的你这台机器。这里的宏观个体是机器,因为要从网络通信层面考虑。
更进一步地,相关内容可以参阅官方文档:条件属性复制
修复2:服务端只显示自己的
上方修复完之后,客户端确实只在自己控制的角色和武器重叠之后才显示控件。但是服务端是无论如何都会显示的。我们希望服务端也只当自己控制的pawn。所以这个显示不应当在Tick中进行。这时候就用到了上面提到了RepNotify。
RepNotify函数的惯例是以OnRep_
开头后接要复制的变量的名称。
这类函数不具有参数,因为是自动调用的。且应当用UFUNCTION
修饰。另外要为复制变量指明其RepNotify
// in BlasterCharacter.h's private section
UPROPERTY(ReplicatedUsing = OnRep_OverlappingWeapon)
class AWeapon* OverlappingWeapon;
UFUNCTION()
void OnRep_OverlappingWeapon(AWeapon* LastWeapon);
并且在BlasterCharacter.cpp中进行OnRep_OverlappingWeapon
的定义:
void ABlasterCharacter::OnRep_OverlappingWeapon(AWeapon* LastWeapon)
{
if (OverlappingWeapon)
{
OverlappingWeapon->ShowPickupWidget(true);
}
}
很好,在上述进行完后,服务器成功不显示任何控件了
由于复制是单向的,即服务端到客户端,所以RepNotify只发生在客户端。
服务端由于不会调用RepNotify,故而无法显示自己的控件。所以我们需要处理我们操作的机器是服务端的情况。
所以我们要为OverlappingWeapon
的Setter
添加新功能,先修改其声明:
//FORCEINLINE void SetOverlappingWeapon(AWeapon* Weapon) { OverlappingWeapon = Weapon; }
void SetOverlappingWeapon(AWeapon* Weapon);
并且为服务端这种情况增加新的设置控件可见性的代码
void ABlasterCharacter::SetOverlappingWeapon(AWeapon* Weapon)
{
OverlappingWeapon = Weapon;
if (IsLocallyControlled())
{
if (OverlappingWeapon)
{
OverlappingWeapon->ShowPickupWidget(true);
}
}
}
此处要说一下IsLocallyControlled()
,引用一下(网络同步)不用RPC实现客户端/服务端/双端逻辑的代码。
void AThirdPersonCharacter::OnHealthUpdate()
{
//客户端特定的功能
if (IsLocallyControlled())
{...}
//服务器特定的功能
if (GetLocalRole() == ROLE_Authority)
{...}
//在所有机器上都执行的功能
...
}
修复3:隐藏控件
虽然解决了服务端可以显示,但是离开碰撞体范围后并不能隐藏,因此为Setter
构建flipflop(即增添第一个if
语句)
void ABlasterCharacter::SetOverlappingWeapon(AWeapon* Weapon)
{
if (OverlappingWeapon)
{
OverlappingWeapon->ShowPickupWidget(false);
}
OverlappingWeapon = Weapon;
if (IsLocallyControlled())
{
if (OverlappingWeapon)
{
OverlappingWeapon->ShowPickupWidget(true);
}
}
}
在Weapon.h
中的private
部分添加结束重叠的事件
FUNCTION()
void OnSphereEndOverlap(
UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex
);
以及相应的实现代码:
void AWeapon::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
ABlasterCharacter* BlasterCharacter = Cast<ABlasterCharacter>(OtherActor);
if (BlasterCharacter)
{
BlasterCharacter->SetOverlappingWeapon(nullptr);
}
}
以及相应地在Weapon.cpp
的BeginPlay()
中的注册:
// in the body of if (HasAuthority())
AreaSphere->OnComponentEndOverlap.AddDynamic(this, &AWeapon::OnSphereEndOverlap);
声明委托调用的函数是一回事,把它绑定到相应的委托上是另一回事。虽然这个回调长得像委托。但是委托来自于这个SphereComponent,也就是AreaSphere
,找到它的OnComponentEndOverlap
才对。
此时已经可以做到重叠时设置OverlappingWeapon
为相关物体,结束重叠置为nullptr
,相应地,RepNotify
的定义也得修改,修改为如下:
void ABlasterCharacter::OnRep_OverlappingWeapon(AWeapon* LastWeapon)
{
if (OverlappingWeapon)
{
OverlappingWeapon->ShowPickupWidget(true);
}
if (LastWeapon)
{
LastWeapon->ShowPickupWidget(false);
}
}
虽说一般情况下RepNotify
没参数,但是可以有,此时参数只能有一个,且必定是RepNotify
执行前它对应的变量的值。
比如拥有RepNotify
的变量A由1变为2,那么它的RepNotify
传入的就是1。不需主动调用,自动传入变化之前的值。
当然,进入RepNotify
中,A已经变成了2,但是你仍可以通过RepNotify
参数中声明的局部变量来访问到值等同于A先前的值的副本。
其他
题外话,可以看看这个B站教程: 彻底掌握UE4网络-05 Rep_Notify以及这篇文章:Ue4广域网(3)----Replication和Rep_Notify