UE4 C++联网RPC教程笔记(一)(第1~4集)
- 前言
- 1. 教程介绍与资源
- 2. 自定义 Debug 功能
- 3. Actor 的复制
- 4. 联网状态判断
前言
本系列笔记将会对梁迪老师的《UE4C++联网RPC框架开发吃鸡》教程进行个人的知识点梳理与总结,此课程也像全反射零耦合框架的课程那样,已经超过报名截止时间了,无法通过正常方法观看。
笔者依旧是采取神奇的方法,通过手机浏览器(不同浏览器的效果有差别,有的会直接要求你登录,遇到这样的就换一个;还有可能点开网页会发现没有播放按钮,遇到这样的就换一个网页)搜索该课程后可以在课程预览界面观看,也可以在目录进行跳转,不过没有字幕。建议是在 PC 端的手机模拟器观看。
本课程集数不多,可以通过目录跳转看完,就不需要复制一串数字到 URL 来切换集数了。
笔者用的引擎版本是 4.26.2,老师推荐的引擎版本是 4.20,不同的版本可能在代码上有所区别,笔者会通过注释标明。
本系列文章不允许转载。
本系列笔记可供读者学习后用于复习回顾或参考代码来解决一些敲错了代码导致的 Bug。并且笔者只会贴出对应集数修改的代码内容,已经有了的部分代码基本都不会贴出来,以免笔记篇幅过长。
1. 教程介绍与资源
此处列出本课程需要翻阅的网址:虚幻文档关于 RPC 的讲解 >>【】
RPC 的全称是 Remote Procedure Calls 远程过程调用。
本课程篇幅较短,分两步走:1. RPC 基础 2. 分别用蓝图和 C++ 实现监听服务器。
2. 自定义 Debug 功能
打开 UE4,创建一个新的 C++ 第三人称游戏项目,需带有初学者内容包,命名为 RPCCourse。
如果学过梁迪老师另一个课程《UE4全反射零耦合框架开发坦克游戏》的读者可能会有印象,因为这个自定义 Debug 功能也在那个课程里面实现了,学过的读者可自行决定是否再看一遍。
创建以下 C++ 类:
创建一个 Object,命名为 RPCHelper,路径为默认。
要实现自定义 Debug 功能,我们需要用到单例模式
RPCHelper.h
#include "CoreMinimal.h"
// 引入头文件
#include "Engine/GameEngine.h"
class RPCCOURSE_API DDRecord
{
private:
// 自身单例
static TSharedPtr<DDRecord> RecordInst;
// 最终输出的字符串
FString RecordInfo;
// 显示时长
float ShowTime;
// 显示的颜色
FColor ShowColor;
public:
// 构造和析构函数不写内容,并且由于可能会被大量调用所以写成内联函数
inline DDRecord() {}
~DDRecord() {}
static TSharedPtr<DDRecord> Get();
// 初始化显示时长和颜色
inline void InitParam(float InTime, FColor InColor)
{
ShowTime = InTime;
ShowColor = InColor;
}
// 实际依赖引擎自带输出逻辑
inline void Output()
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, ShowTime, ShowColor, RecordInfo);
// 清空最终输出字符串
RecordInfo.Empty();
}
// 移位操作符重写,将传入的各种类型数据都转换成 FString 然后加入最终输出字符串
inline DDRecord &operator<<(FName Info) { RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord &operator<<(FText Info) { RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord &operator<<(const char* Info) { RecordInfo += Info; return *this; }
inline DDRecord &operator<<(const char Info) { RecordInfo.AppendChar(Info); return *this; }
inline DDRecord &operator<<(int32 Info) { RecordInfo.Append(FString::FromInt(Info)); return *this; }
inline DDRecord &operator<<(float Info) { RecordInfo.Append(FString::SanitizeFloat(Info)); return *this; }
inline DDRecord &operator<<(double Info) { RecordInfo.Append(FString::SanitizeFloat(Info)); return *this; }
inline DDRecord &operator<<(bool Info) { RecordInfo.Append(Info ? FString("true") : FString("false")); return *this; }
inline DDRecord &operator<<(FVector2D Info) { RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord &operator<<(FVector Info) { RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord &operator<<(FRotator Info) { RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord &operator<<(FQuat Info) { RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord &operator<<(FTransform Info) { RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord &operator<<(FMatrix Info) { RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord &operator<<(FColor Info) { RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord &operator<<(FLinearColor Info) { RecordInfo.Append(Info.ToString()); return *this; }
// 在遇到 DDRecord 对象时(即下文的 Endl)输出,即调用 Output()
inline void operator<<(DDRecord& Record) { Record.Output(); }
};
namespace DDH
{
FORCEINLINE DDRecord& Debug(float InTime = 3000.f, FColor InColor = FColor::Yellow)
{
DDRecord::Get()->InitParam(InTime, InColor);
return *DDRecord::Get();
}
FORCEINLINE DDRecord& Endl()
{
return *DDRecord::Get();
}
}
RPCHelper.cpp
TSharedPtr<DDRecord> DDRecord::RecordInst = NULL;
TSharedPtr<DDRecord> DDRecord::Get()
{
if (!RecordInst.IsValid())
RecordInst = MakeShareable(new DDRecord());
return RecordInst;
}
接下来到第三人称项目自带的这个 RPCCourseCharacter,重写它的 BeginPlay()
方法来测试一下我们的自定义 Debug 功能。
RPCCourseCharacter.h
protected:
void BeginPlay() override;
RPCCourseCharacter.cpp
// 引入头文件
#include "RPCHelper.h"
void ARPCCourseCharacter::BeginPlay()
{
Super::BeginPlay();
// 输出 Debug
DDH::Debug(20.f, FColor::Red) << "Hello UE4 " << 123 << 0.888 << FVector(30, 40, 50) << FColor::Red << DDH::Endl();
}
编译后运行游戏,左上角输出红色的 Debug 语句,20 秒后消失。
继续改进下,让 Debug 支持更多输出方式,比如输出到 Output Log 控制台里,并且有日志记录、警告、报错模式。
RPCHelper.h
class RPCCOURSE_API DDRecord
{
public:
// 状态模式,0:Debug,1:Log,2:Warning,3:Error
uint8 PatternID;
public:
inline void Output()
{
switch (PatternID) {
case 0:
{
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, ShowTime, ShowColor, RecordInfo);
}
break;
case 1:
{
UE_LOG(LogTemp, Log, TEXT("%s"), *RecordInfo);
}
break;
case 2:
{
UE_LOG(LogTemp, Warning, TEXT("%s"), *RecordInfo);
}
break;
case 3:
{
UE_LOG(LogTemp, Error, TEXT("%s"), *RecordInfo);
}
break;
}
RecordInfo.Empty();
}
};
namespace DDH
{
FORCEINLINE DDRecord& Debug(float InTime = 3000.f, FColor InColor = FColor::Yellow)
{
DDRecord::Get()->PatternID = 0; // 初始化
DDRecord::Get()->InitParam(InTime, InColor);
return *DDRecord::Get();
}
// 只改变输出颜色,不管显示时间
FORCEINLINE DDRecord& Debug(FColor InColor)
{
return Debug(3000.f, InColor);
}
FORCEINLINE DDRecord& Log()
{
DDRecord::Get()->PatternID = 1;
return *DDRecord::Get();
}
FORCEINLINE DDRecord& Warning()
{
DDRecord::Get()->PatternID = 2;
return *DDRecord::Get();
}
FORCEINLINE DDRecord& Error()
{
DDRecord::Get()->PatternID = 3;
return *DDRecord::Get();
}
}
最后测试一下日志记录模式。
RPCCourseCharacter.cpp
void ARPCCourseCharacter::BeginPlay()
{
Super::BeginPlay();
DDH::Log() << "Hello UE4 " << 123 << 0.888 << FVector(30, 40, 50) << FColor::Red << DDH::Endl();
}
编译后,打开 Window -> Develop Tools -> Output Log,运行游戏,可以看到日志输出了 Debug 语句。
最后将 BeginPlay()
里的 Debug 语句删除掉。
3. Actor 的复制
以下知识点内容截取自梁迪老师准备的 RPC 联网文档:
(1)bool 变量 bNetLoadOnClient
这个变量是给一开始就放置在场景中的对象使用的。
如果bNetLoadOnClient
设置为 true
,当客户端连接上服务端时,客户端也会存在这个对象。
如果 bNetLoadOnClient
设置为 false
,当客户端连接上服务端时,客户端不会存在这个对象。
SetReplicates
无论是否为 true
都不会影响这个变量的作用。(关于 SetReplicates
下面会讲解)
在默认路径下新建 3 个 C++ 的 Actor 类,分别命名为 RPCActor、CubeReplicate 和 CubeNoReplicate。
我们先用 RPCActor 来测试 bNetLoadOnClient
。
RPCActor.cpp
ARPCActor::ARPCActor()
{
bNetLoadOnClient = false; // 设置为 不网络同步到客户端
}
来到角色类,在 BeginPlay()
里输出场上 RPCActor 实例的数量。
RPCCourseCharacter.cpp
// 引入头文件
#include "Kismet/GameplayStatics.h"
#include "RPCActor.h"
void ARPCCourseCharacter::BeginPlay()
{
Super::BeginPlay();
// 寻找场景中的 RPCActor
TArray<AActor*> ActArray;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ARPCActor::StaticClass(), ActArray);
DDH::Debug() << "RPCActor Num --> " << ActArray.Num() << DDH::Endl();
}
编译后,将 C++ 类的 RPCActor 拖进场景,随后作以下设置(对于 4.26 版本会多出第 2 步)。运行后结果如图所示。
输出场景内 RPCActor 的数量为 1 的语句是服务端发出来的,输出数量为 0 的语句则是客户端发出来的。至于为何会分别输出了两遍,是因为在编辑器中,调用引擎自带的屏幕输出方法,会使语句在每个端都输出一次。
同时因为 RPCActor 设置了 bNetLoadOnClient
为 false
,所以 RPCActor 只存在于服务端。
RPCActor.cpp
ARPCActor::ARPCActor()
{
// 重新设置为 网络同步到客户端
bNetLoadOnClient = true;
}
编译后,将原本场景里的 RPCActor 删除,重新放置一个 RPCActor。运行游戏,结果如图所示:
这时服务端和客户端的场景里都存在这个 RPCActor 的实例。
(2)SetReplicates(bool)
调用 SetReplicates(true)
设置 Actor 可以复制。
调用 SetReplicates(false)
设置 Actor 不可以复制。
当在服务端 Spawn 可复制的 Actor 时,客户端会生成。
当在客户端 Spawn 可复制的 Actor 时,其他端不会生成。
测试下 SetReplicates(bool)
和 bNetLoadOnClient
共同作用是什么样的效果。
RPCActor.cpp
ARPCActor::ARPCActor()
{
// bNetLoadOnClient 设置为 true 时,如果该对象是一开始就在场景中的对象,
// 客户端连接到服务端时该对象也会存在,与 SetReplicates 是否为 true 没有关系
SetReplicates(true);
bNetLoadOnClient = false;
}
编译后,将原本场景里的 RPCActor 删除,重新放置一个 RPCActor。运行游戏,结果如图所示:
又变成了客户端没有 RPCActor,服务端有。说明确实 bNetLoadOnClient
的优先级比 SetReplicates(bool)
更高。
接下来单独测试一下 SetReplicates(bool)
。我们给 CubeReplicate 和 CubeNoReplicate 添加一些组件方便观察,前者设置可复制,后者设置不可复制。
CubeReplicate.h
protected:
UStaticMeshComponent* CubeMesh;
CubeReplicate.cpp
// 引入头文件
#include "Components/StaticMeshComponent.h"
#include "UObject/ConstructorHelpers.h"
#include "RPCHelper.h"
ACubeReplicate::ACubeReplicate()
{
PrimaryActorTick.bCanEverTick = true;
// 设置复制
SetReplicates(true);
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
CubeMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("CubeMesh"));
CubeMesh->SetupAttachment(RootComponent);
// 附加模型
ConstructorHelpers::FObjectFinder<UStaticMesh> StaticCubeMesh(TEXT("StaticMesh'/Game/StarterContent/Shapes/Shape_Cylinder.Shape_Cylinder'"));
CubeMesh->SetStaticMesh(StaticCubeMesh.Object);
}
CubeNoReplicate.h
protected:
UStaticMeshComponent* CubeMesh;
CubeNoReplicate.cpp
// 引入头文件
#include "Components/StaticMeshComponent.h"
#include "UObject/ConstructorHelpers.h"
#include "RPCHelper.h"
ACubeNoReplicate::ACubeNoReplicate()
{
PrimaryActorTick.bCanEverTick = true;
// 设置不复制
SetReplicates(false);
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
CubeMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("CubeMesh"));
CubeMesh->SetupAttachment(RootComponent);
// 附加模型
ConstructorHelpers::FObjectFinder<UStaticMesh> StaticCubeMesh(TEXT("StaticMesh'/Game/StarterContent/Shapes/Shape_WideCapsule.Shape_WideCapsule'"));
CubeMesh->SetStaticMesh(StaticCubeMesh.Object);
}
来到 RPCActor,让前面的两个 Cube 只在服务端生成。
RPCActor.cpp
// 引入头文件
#include "CubeReplicate.h"
#include "CubeNoReplicate.h"
#include "RPCHelper.h"
ARPCActor::ARPCActor()
{
//SetReplicates(true);
bNetLoadOnClient = true; // 设置为 网络同步到客户端
}
void ARPCActor::BeginPlay()
{
Super::BeginPlay();
// 判断是不是服务端
if (GetWorld()->IsServer()) {
GetWorld()->SpawnActor<ACubeReplicate>(ACubeReplicate::StaticClass(), GetActorLocation() + FVector::RightVector * 300.f, FQuat::Identity.Rotator());
GetWorld()->SpawnActor<ACubeNoReplicate>(ACubeNoReplicate::StaticClass(), GetActorLocation() - FVector::RightVector * 300.f, FQuat::Identity.Rotator());
}
}
编译后,将场景内的 RPCActor 删除。
新建一个 Blueprint 文件夹,在里面创建一个基于 RPCActor 的蓝图,命名为 RPCActor_BP,然后将其拖进场景内。
运行游戏,得到效果如图。可以看到服务端出现了 CubeReplicate 和 CubeNoReplicate 的实例,但是客户端没有 CubeNoReplicate 的实例。说明 SetReplicates(bool)
生效了。
再试一下只在客户端生成,只需要在判断条件前面加个 !
取反就可以了。
RPCActor.cpp
void ARPCActor::BeginPlay()
{
Super::BeginPlay();
// 判断是不是客户端
if (!GetWorld()->IsServer()) {
GetWorld()->SpawnActor<ACubeReplicate>(ACubeReplicate::StaticClass(), GetActorLocation() + FVector::RightVector * 300.f, FQuat::Identity.Rotator());
GetWorld()->SpawnActor<ACubeNoReplicate>(ACubeNoReplicate::StaticClass(), GetActorLocation() - FVector::RightVector * 300.f, FQuat::Identity.Rotator());
}
}
编译后再次运行,可以看到这回服务端场景内没有生成两个 Actor 的实例,而客户端出现了 CubeReplicate 和 CubeNoReplicate 的实例。说明客户端内生成对象时,即便这个对象是可复制的,它也不会生成到服务端。
最后将 RPCCourseCharacter.cpp 的 BeginPlay()
方法里的 Debug 语句注释掉。
4. 联网状态判断
本节课需要看的官方参考文档:网络概述 >>【】
以下知识点内容截取自梁迪老师准备的 RPC 联网文档:
(1)AActor 的 HasAuthority()
,返回 true
是说明 Actor 是该端创建的角色。
在关卡蓝图或者是 GameMode 以及默认放在场景中的 Actor 等对象使用这个函数可以用来判断是否是服务端,因为关卡蓝图和 GameMode 与默认放在场景中的对象可以看做是由服务端生成的。
不推荐用 HasAuthority()
来判断当前端是不是服务端。梁迪老师推荐的用法是用来它来做下面这个 Actor 的角色判断。
(2)Actor 的角色判断
AActor 里的 ENetRole Role
枚举是用来识别角色的 Actor 的身份的。ENetRole
的几个值:
ROLE_None
:该 Actor 在网络游戏中无角色,不会复制。
ROLE_SimulatedProxy
:这个 Actor 是其他客户端在本机客户端的一个模拟代理
ROLE_AutonomousProxy
:这个 Actor 是本机客户端的自己控制的角色
ROLE_Authority
:这个 Actor 是服务器上的 Actor
ROLE_MAX
:官方没有解释,笔者个人猜测应该是代表该枚举的最大枚举值。
(3)是否是服务端判断,不推荐使用 HasAuthority()
来判断
推荐使用 GetWorld()->IsServer()
或者 GetNetMode()
判断
(4)端的判断
使用 GetNetMode()
函数可以获取端的属性 ENetMode
,分类如下:
NM_Standalone
:单独端,单机游戏
NM_DedicatedServer
:专用服务器
NM_ListenServer
:监听服务器
NM_Client
:客户端
NM_MAX
:官方没有解释,笔者个人猜测应该是代表该枚举的最大枚举值。
接下来我们打算测试一下 IsServer()
和 HasAuthority()
在 “判断当前端是不是服务端” 的需求上表现如何。
在复制 Cube 和不复制 Cube 的 BeginPlay()
函数输出一下调用上面两个方法后返回的结果。
CubeReplicate.cpp
void ACubeReplicate::BeginPlay()
{
Super::BeginPlay();
DDH::Debug() << "IsServer --> " << GetWorld()->IsServer() << " ; HasAuthority() --> " << HasAuthority() << " ACubeReplicate BeginPlay" << DDH::Endl();
}
CubeNoReplicate.cpp
void ACubeNoReplicate::BeginPlay()
{
Super::BeginPlay();
DDH::Debug() << "IsServer --> " << GetWorld()->IsServer() << " ; HasAuthority() --> " << HasAuthority() << " ACubeNoReplicate BeginPlay" << DDH::Endl();
}
接上一节课结尾的代码,此时两个 Cube 的生成逻辑是在客户端上运行的,所以只有在客户端才会生成这两个 Cube,服务端不会生成。
编译后运行,可以看到左上角客户端输出的两句 Debug 信息,IsServer()
返回的结果是 false,符合预期;而 HasAuthority()
返回的结果是 true
,说明这个方法不一定能判断当前端是服务端还是客户端。
重新调整下,让两个 Cube 在服务端生成。
RPCActor.cpp
void ARPCActor::BeginPlay()
{
Super::BeginPlay();
// 将判断表达式的 ! 去掉
if (GetWorld()->IsServer()) {
GetWorld()->SpawnActor<ACubeReplicate>(ACubeReplicate::StaticClass(), GetActorLocation() + FVector::RightVector * 300.f, FQuat::Identity.Rotator());
GetWorld()->SpawnActor<ACubeNoReplicate>(ACubeNoReplicate::StaticClass(), GetActorLocation() - FVector::RightVector * 300.f, FQuat::Identity.Rotator());
}
}
编译后运行,下面和中间的 Debug 语句是服务端打印的,上面的 Debug 语句是客户端打印的。此时 HasAuthority()
确实在服务端则输出了 true,在客户端输出了 false。两次测试结果相比之下还是 IsServer()
更适合用于判断当前端的性质。
随后将 CubeReplicate 和 CubeNoReplicate 的 BeginPlay()
内的 Debug 语句注释掉。
接下来我们测试一下 AActor 的 GetNetMode()
方法,用于获取当前端类型 ENetMode
的值。
RPCActor.h
protected:
// 获取端类型的枚举后以文本形式输出
void EchoNetMode();
RPCActor.cpp
void ARPCActor::BeginPlay()
{
// 测试完毕后记得注释掉
EchoNetMode();
}
void ARPCActor::EchoNetMode()
{
ENetMode NetMode = GetNetMode();
switch (NetMode)
{
case NM_Standalone:
DDH::Debug() << "NM_Standalone" << DDH::Endl();
break;
case NM_DedicatedServer:
DDH::Debug() << "NM_DedicatedServer" << DDH::Endl();
break;
case NM_ListenServer:
DDH::Debug() << "NM_ListenServer" << DDH::Endl();
break;
case NM_Client:
DDH::Debug() << "NM_Client" << DDH::Endl();
break;
case NM_MAX:
DDH::Debug() << "NM_MAX" << DDH::Endl();
break;
}
}
编译后运行游戏,很明显 Client 是客户端输出的,ListenServer 是服务端输出的。
如果将运行模式调整如下后运行游戏,则会显示 NM_Standalone。