UE4 C++联网RPC教程笔记(二)(第5~7集)
5. 联网变量
在前面的课程里,我们都是通过 Actor 的生成来看服务端与客户端是否同步。接下来我们研究下 Actor 的变量复制来实现变量同步。
通过 Replicated 说明符来复制变量
下面文本截取自梁迪老师的 RPC 联网文档。
变量复制只有在服务端修改才会更新到服务端和所有客户端,在客户端修改只会更新所在客户端,对服务端和其他客户端没有影响。
(1)首先用 UPROPERTY(Replicated, ......) 定义变量,如:
	UPROPERTY(Replicated)
	FString Inventory;
 
(2)必须在 .cpp 文件重写 GetLifetimeReplicatedProps() 方法,如:
void ARPCProjectCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> & OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	......
}
 
注意,重写这个方法不需要在 .h 文件进行声明。
(3)添加头文件 #include "Net/UnrealNetwork.h"
在上面的重写方法里用宏注册变量到联网系统:
	DOREPLIFETIME(ARPCProjectCharacter, Inventory);		// 所有端的对象都更新
	DOREPLIFETIME_CONDITION(ARPCProjectCharacter, Inventory, COND_OwnerOnly);	// 该属性仅发送至 Actor 的所有者
	DOREPLIFETIME_CONDITION(ARPCProjectCharacter, Inventory, COND_SkipOwner);	// 该属性将发送至除所有者之外的每个连接
	DOREPLIFETIME_CONDITION(ARPCProjectCharacter, Inventory, COND_Custom);		// 该属性没有特定条件,但需要通过 SetCustomIsActiveOverride 得到开启/关闭能力
 
更多条件参考官方文档:条件属性复制 >>【】
接下来我们通过实操来探究一下。在默认路径创建一个 C++ 的 Actor 类,命名为 FireEffectActor。
我们打算让这个 Actor 在场景里显示一个复制的 int32 数字,然后通过 Timer 来让这个数字递减。同时添加一个火焰粒子方便观察。
FireEffectActor.h
// 提前声明
class UParticleSystemComponent;
class UTextRenderComponent;
UCLASS()
class RPCCOURSE_API AFireEffectActor : public AActor
{
protected:
	void UpdateTimer();
protected:
	UPROPERTY(EditAnywhere)
	UParticleSystemComponent* FireEffect;
	UPROPERTY(EditAnywhere)
	UTextRenderComponent* TextRender;
	// 应用 变量复制 的说明符
	UPROPERTY(Replicated)
	int32 CountDownTimer;
	// 用于执行逐秒递减操作的计时器句柄
	FTimerHandle UpdateTimerHandle;
};
 
FireEffectActor.cpp
// 引入头文件
#include "Particles/ParticleSystemComponent.h"
#include "Components/TextRenderComponent.h"
#include "TimerManager.h"
#include "Net/UnrealNetwork.h"	// 变量复制需要用到
AFireEffectActor::AFireEffectActor()
{
	PrimaryActorTick.bCanEverTick = true;
	SetReplicates(true);
	RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
	FireEffect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("FireEffect"));
	FireEffect->SetupAttachment(RootComponent);
	TextRender = CreateDefaultSubobject<UTextRenderComponent>(TEXT("TextRender"));
	TextRender->SetupAttachment(RootComponent);
}
void AFireEffectActor::BeginPlay()
{
	Super::BeginPlay();
	
	CountDownTimer = 20;
	// 在服务端执行
	if (GetWorld()->IsServer()) {
		// 循环运行
		FTimerDelegate UpdateTimerDele = FTimerDelegate::CreateUObject(this, &AFireEffectActor::UpdateTimer);
		GetWorld()->GetTimerManager().SetTimer(UpdateTimerHandle, UpdateTimerDele, 1.f, true);
	}
}
void AFireEffectActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	// 渲染数字
	//TextRender->SetText(FString::FromInt(CountDownTimer));
	// 4.26 版本推荐使用以 FText 为参数的 SetText() 方法
	TextRender->SetText(FText::AsNumber(CountDownTimer));
}
void AFireEffectActor::UpdateTimer()
{
	if (CountDownTimer > 0)
		CountDownTimer -= 1;
}
// 需要把用 Replicated 定义的变量在这个方法的重写下进行注册
void AFireEffectActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	// 有一个复制变量就要调用一次这样的宏
	DOREPLIFETIME(AFireEffectActor, CountDownTimer);
}
 
编译后,在 Blueprint 下创建一个 FireEffectActor 的蓝图,命名为 FireEffectActor_BP,调整蓝图如下:

 将这个蓝图拖拽至场景内,运行游戏(记得要开2个玩家的聆听服务器),可以看到服务端和客户端的 FireEffectActor_BP 的 TextRender 上的数字都在同步递减。

修改一下,让递减的 Timer 只在客户端运作。
FireEffectActor.cpp
void AFireEffectActor::BeginPlay()
{
	// 在客户端执行该操作
	if (!GetWorld()->IsServer()) {
		FTimerDelegate UpdateTimerDele = FTimerDelegate::CreateUObject(this, &AFireEffectActor::UpdateTimer);
		GetWorld()->GetTimerManager().SetTimer(UpdateTimerHandle, UpdateTimerDele, 1.f, true);
	}
}
 
编译后运行游戏,可以看到客户端的 TextRender 上的数字在递减,而服务端的则没有变化。

通过 RepNotify 和 ReplicatedUsing 来同步变量
(4)RepNotify 和 ReplicatedUsing
参考文档:
- [UE4] RepNotify,更新通知
 - UE4 c++ 变量RepNotify
 
复制通知的功能是在可复制的 Actor 下定义某个值,这个值绑定某个方法,当在服务端修改这个值时,绑定的方法会在客户端执行,如果想要在服务端也执行绑定的方法,需要再调用一次;当在客户端修改这个值或者执行这个值绑定的方法时,只会对所在的客户端有效果,对服务端和其他客户端没有效果。
使用例子如下:
.h文件
	UPROPERTY(ReplicatedUsing = OnRep_变量名)
	bool 变量名;
	
	UFUNCTION()
	void OnRep_变量名();
 
.cpp文件
	// 修改变量值,修改后客户端会执行 OnRep_变量名() 方法,但不会执行服务端的方法
	变量名 = true;
	// 执行变量方法,会在服务端执行 OnRep_变量名() 方法,但是不会在客户端执行
	OnRep_变量名();
	// 所以如果要在服务端和客户端都执行,必须两个语句一起写
 
接下来试一下复制通知的功能。我们打算声明一个应用复制通知的 bool 变量,通知方法的作用是将火焰特效关闭。然后在 CountDownTimer 递减为 0 时改变一下它的值使客户端自动运行通知方法,再主动调用通知方法使服务端的火焰特效也关闭。
FireEffectActor.h
protected:
	UFUNCTION()
	void OnRep_Deactivate();
protected:
	UPROPERTY(ReplicatedUsing = OnRep_Deactivate)
	bool Deactivate;
 
FireEffectActor.cpp
void AFireEffectActor::BeginPlay()
{
	// 将递减 Timer 的逻辑改回在服务端运行
	if (GetWorld()->IsServer()) {
		FTimerDelegate UpdateTimerDele = FTimerDelegate::CreateUObject(this, &AFireEffectActor::UpdateTimer);
		GetWorld()->GetTimerManager().SetTimer(UpdateTimerHandle, UpdateTimerDele, 1.f, true);
	}
}
void AFireEffectActor::UpdateTimer()
{
	if (CountDownTimer > 0)
		CountDownTimer -= 1;
	else {
		// 改变一下以便能够触发客户端执行通知方法
		Deactivate = !Deactivate;
		// 主动调用通知方法以便让服务端也执行对应逻辑
		OnRep_Deactivate();
	}
}
void AFireEffectActor::OnRep_Deactivate()
{
	// 关闭火焰特效
	FireEffect->Deactivate();
}
void AFireEffectActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	// 注册复制变量
	DOREPLIFETIME(AFireEffectActor, Deactivate);
}
 
编译后运行,可以看到服务端和客户端的 TextRender 的数字都在递减,当数字为 0 后,两个端的火焰特效都一起被关闭了。

再试一下不主动调用通知方法,看看服务端是否不会执行。
FireEffectActor.cpp
void AFireEffectActor::UpdateTimer()
{
	if (CountDownTimer > 0)
		CountDownTimer -= 1;
	else {
		Deactivate = !Deactivate;
		// 这次测试完毕后记得取消注释
		//OnRep_Deactivate();
	}
}
 
编译后运行,这次数字为 0 后,客户端的火焰特效被关闭了,但服务端的火焰特效还在。

 接下来笔者又试了另外两种情况:
恢复上面对复制通知方法的调用,同时让 FireEffectActor.cpp 的 BeginPlay() 里面的计时操作在客户端运行,则只有客户端的 TextRender 会递减;数字为 0 后客户端的火焰特效消失。服务端没有任何变化。
但是如果注释掉对复制通知方法的调用,让计时操作在客户端运行,则只有客户端的 TextRender 会递减;但是数字为 0 后客户端的火焰特效不会消失。服务端没有任何变化。很显然,变量在客户端上变化的时候不会自动调用通知方法,并且此时调用通知方法只对客户端的对象有效。
6. 联网方法
前面讲到了复制变量,这节课来讲复制方法。下面文字截取自梁迪老师的 RPC 联网文档。
参考文档:UE4 RPC在C++中的使用简例
Reliable和Unreliable:前者一定会执行;后者在网络不好的时候可能会丢弃。Client、Server、NetMulticast:
Client:如果在服务器运行,会在拥有该 Actor 的客户端上调用;如果在客户端调用,只会在当前客户端上执行
Server:客户端调用时,在服务器运行,通常用于客户端给服务器传递数据,服务端调用只在服务端运行
NetMulticast:在服务端调用,会广播到所有客户端;如果在客户端调用,只会在当前客户端上执行WithValidation进行安全检查,如果是使用 Server 函数,一定要在UFUNCTION()内添加这个声明,并且在 .h 文件声明void 方法名_Implementation(var)与bool 方法名_Validate(var),并且在 .cpp 文件实现,而方法名本身的方法不需要实现,其中_Implementation实现逻辑,_Validate内实现安全检验,防止作弊。但是有些类型不能作为参数,比如FString,编译会报错
案例:UE4 的 NetWork 简单原理(这篇文章目前是 VIP 文章,读者如果没开会员也可以不用看)
示例如下:
.h文件
	// 使用了 RPC 说明符的函数
	UFUNCTION(Server, Reliable, WithValidation) 
	void ServerMove(FVector Velocity, bool bSweep); 
	// 安全检验方法
	virtual bool ServerMove_Validate(FVector Velocity, bool bSweep); 
	// 执行方法
	virtual void ServerMove_Implementation(FVector Velocity, bool bSweep);
 
.cpp文件
	bool ASWeapon::ServerHandleFiring_Validate() 
	{ 
		return true;	// 默认返回为true,返回 false 则服务器会把该客户端踢掉
	} 
	void ASWeapon::ServerHandleFiring_Implementation() 
	{ 
		// 具体的函数逻辑 
	} 
 
测试多播方法
接下来开始实操。我们打算创建一个不可复制的、带火焰粒子组件的蓝图 Actor;给项目的默认角色添加一个多播(NetMulticast,也可以叫广播)的方法,该方法的逻辑是生成一个火焰 Actor 对象在当前角色的位置,并且在角色跳跃的时候调用这个方法。
在 Blueprint 下创建一个 Actor 蓝图,命名为 UnReplicateFire。然后修改如下:

接下来在项目自带角色类里添加多播方法,并将多播方法绑定到跳跃按键上;多播方法的逻辑就是在角色当前位置生成火焰特效 Actor。
RPCCourseCharacter.h
protected:
	// 广播事件
	UFUNCTION(NetMulticast, Reliable)
	void SpaceBarNetMulticast();
 
RPCCourseCharacter.cpp
void ARPCCourseCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
	check(PlayerInputComponent);
	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
	PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
	// 添加这个按键绑定方法
	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ARPCCourseCharacter::SpaceBarNetMulticast);
	
}
void ARPCCourseCharacter::SpaceBarNetMulticast_Implementation()
{
	UClass* FireEffectClass = LoadClass<AActor>(NULL, TEXT("Blueprint'/Game/Blueprint/UnReplicateFire.UnReplicateFire_C'"));
	GetWorld()->SpawnActor<AActor>(FireEffectClass, GetActorTransform());
}
 
编译后,将运行的玩家人数改为 3(确保有聆听服务器)。运行游戏,让服务端的角色跳一下,可以看到服务端和两个客户端都能看到生成了火焰;但是让其中一个客户端跳一下,只有该客户端能看到生成火焰,服务端和另一个客户端都看不到。
GIF 动图录制出来超过 5MB,这里就不放图片了,后面如果有不放图片的也是同样原因。
测试服务端发消息给客户端的 Client 方法
接下来测试 Client 方法。我们打算新建一个不可复制的、只带有数字的 Actor;在角色类声明一个 Client 方法,其作用是生成数字 Actor 并传入值赋给这个数字。最后让角色类绑定一个按键到这个方法。
在默认路径新建一个 C++ 的 Actor 类,命名为 NumPad。
NumPad.h
class UTextRenderComponent;
UCLASS()
class RPCCOURSE_API ANumPad : public AActor
{
	GENERATED_BODY()
	
public:
	// 由于 URenderTextComponent::SetText() 在 4.26 提倡传入 FText 类型的参数,所以笔者更改了形参类型,原本是 FString
	void AssignRenderText(FText InText);
	
public:
	UPROPERTY(EditAnywhere)
	UTextRenderComponent* TextRender;
};
 
NumPad.cpp
// 引入头文件
#include "Components/TextRenderComponent.h"
ANumPad::ANumPad()
{
	PrimaryActorTick.bCanEverTick = false;	// 关闭 Tick
	// 设置不可复制
	SetReplicates(false);
	RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
	TextRender = CreateDefaultSubobject<UTextRenderComponent>(TEXT("TextRender"));
	TextRender->SetupAttachment(RootComponent);
	TextRender->SetWorldSize(80.f);
	TextRender->SetTextRenderColor(FColor::Red);
	TextRender->SetRelativeLocation(FVector(0.f, 0.f, 50.f));
}
void ANumPad::AssignRenderText(FText InText)
{
	TextRender->SetText(InText);
}
 
RPCCourseCharacter.h
protected:
	// J 键绑定
	void KeyJEvent();
	// Client 联网方法
	UFUNCTION(Client, Reliable)
	void KeyJClient(int32 InInt);
 
RPCCourseCharacter.cpp
// 引入头文件
#include "NumPad.h"
void ARPCCourseCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
	// 添加按键绑定方法
	PlayerInputComponent->BindKey(EKeys::J, IE_Pressed, this, &ARPCCourseCharacter::KeyJEvent);
}
void ARPCCourseCharacter::KeyJEvent()
{
	// 只在服务端运行
	if (GetWorld()->IsServer()) {
		TArray<AActor*> ActArray;	// 老师多输入了一个 a
		UGameplayStatics::GetAllActorsOfClass(GetWorld(), ARPCCourseCharacter::StaticClass(), ActArray);
		for (int i = 0; i < ActArray.Num(); ++i) {
			if (ActArray[i] != this) {
				Cast<ARPCCourseCharacter>(ActArray[i])->KeyJClient(i);
			}
		}
	}
}
void ARPCCourseCharacter::KeyJClient_Implementation(int32 InInt)
{
	ANumPad* NumPad = GetWorld()->SpawnActor<ANumPad>(ANumPad::StaticClass(), GetActorTransform());
	NumPad->AssignRenderText(FText::AsNumber(InInt));
}
 
最后添加对 Slate 的依赖(因为设置了 TextRender 的参数),否则编译无法通过。
RPCCourse.Build.cs
public RPCCourse(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "Slate" });	// 添加 Slate 模块依赖
	}
 
编译后运行,在服务端按下 J 键,可以看到两个客户端角色所在位置分别生成了一个红色的数字,只有对应的客户端才能看到自己的数字。

测试客户端给服务端发消息的 Server 方法(带检验)
因为客户端的状况千奇百怪,所以给服务端发消息时必须要带安全检验,如果不符合检验条件则将其踢出会话。
这次我们打算让客户端按下按键后,在服务器端能看到客户端角色的位置生成一个数字 Actor。
RPCCourseCharacter.h
protected:
	// H 键绑定
	void KeyHEvent();
	// Server 方法
	UFUNCTION(Server, Reliable, WithValidation)
	void KeyHServer(int32 InInt);
	// Server 方法逻辑
	void KeyHServer_Implementation(int32 InInt);
	// Server 方法数据验证
	bool KeyHServer_Validate(int32 InInt);
 
RPCCourseCharacter.cpp
void ARPCCourseCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
	PlayerInputComponent->BindKey(EKeys::H, IE_Pressed, this, &ARPCCourseCharacter::KeyHEvent);
}
void ARPCCourseCharacter::KeyHEvent()
{
	// 在客户端运行
	if (!GetWorld()->IsServer()) {
		KeyHServer(3);
	}
}
void ARPCCourseCharacter::KeyHServer_Implementation(int32 InInt)
{
	ANumPad* NumPad = GetWorld()->SpawnActor<ANumPad>(ANumPad::StaticClass(), GetActorTransform());
	NumPad->AssignRenderText(FText::AsNumber(InInt));
}
bool ARPCCourseCharacter::KeyHServer_Validate(int32 InInt)
{
	if (InInt > 0)
		return true;
	return false;
}
 
编译后运行,在任意一个客户端按下 H 键,只有服务端会看到该客户端的角色处生成一个红色的 3。

7. 蓝图实现监听服务器
这节课我们会利用蓝图节点来实现局域网联机功能,其中包括创建会话和加入会话这两个功能。之前我们在聆听服务器模式下运行游戏则省略了 “服务器创建会话” 和 “客户端加入会话” 这两个过程。
我们打算在另一个空白地图里放上一个主界面 UI,包括创建会话和加入会话的按钮,进入会话的端则会来到有可操控小白人的地图。
在默认路径下新建以下 C++ 类:
一个 GameModeBase,命名为 MenuGameMode。作为游戏主界面的游戏模式。
 一个 PlayerController,命名为 MenuController。作为游戏主界面的玩家控制器。
 一个 UserWidget,命名为 MenuWidget。作为游戏主界面的 UI。
新建一个 Map 文件夹,在里面创建一个新的空白 Level,命名为 MenuMap。然后打开该地图。
在编辑器界面 Edit -> Project Settings -> Maps & Modes 里,将 MenuMap 设置为编辑器初始地图和默认地图。

将 ThirdPersonCPP/Maps 路径下的 ThirdPersonExampleMap 移到 Map 文件夹。随后将其改名为 GameMap。
右侧 WorldSettings,将 MenuMap 默认的 GameMode 设置为 MenuGameMode。

 来到 MenuGameMode,给主界面游戏模式设定 PlayerController,并设置默认 Pawn 为 NULL。
MenuGameMode.h
public:
	AMenuGameMode();
 
MenuGameMode.cpp
// 引入头文件
#include "MenuController.h"
AMenuGameMode::AMenuGameMode()
{
	PlayerControllerClass = AMenuController::StaticClass();
	DefaultPawnClass = NULL;
}
 
在 Blueprint 下创建一个 Widget Blueprint,命名为 MenuWidget_BP。打开蓝图,将其父类设置为 MenuWidget。随后修改其界面如下:(两个按钮的锚点都改成居中)

 在蓝图图表重写这两个按钮的点击事件,即创建会话和加入会话的逻辑:

 
其实还有一个 Destroy Session 的蓝图节点,可以用于关闭会话。
因为使用了 UMG 所以要添加 UMG 的依赖。
RPCCourse.Build.cs
public RPCCourse(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "Slate", "UMG" });	// 添加 UMG 模块依赖
	}
 
给主界面玩家控制器设置 UI 输入模式,并且显示主菜单 UI 到界面。
MenuController.h
protected:
	virtual void BeginPlay() override;
 
MenuController.cpp
// 引入头文件
#include "MenuWidget.h"
void AMenuController::BeginPlay()
{
	Super::BeginPlay();
	// 设定输入模式
	bShowMouseCursor = true;
	FInputModeUIOnly InputMode;
	InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
	SetInputMode(InputMode);
	// 创建 UI
	UClass* MenuWidgetClass = LoadClass<UMenuWidget>(NULL, TEXT("WidgetBlueprint'/Game/Blueprint/MenuWidget_BP.MenuWidget_BP_C'"));
	UMenuWidget* MenuWidget = CreateWidget<UMenuWidget>(GetWorld(), MenuWidgetClass);
	MenuWidget->AddToViewport();
}
 
编译后,将运行选项的玩家数量改成 2。运行游戏,在服务端点击创建服务端按钮,创建成功并进入了 GameMap;在客户端点击加入服务器,稍等片刻后加入成功,进入了 GameMap。(让客户端创建服务器,服务端加入也可以)
(动图裁剪了 6 秒,避免图片体积过大)

不过有个问题是我们因为前面设置了 UI 输入模式所以现在无法控制角色,所以我们需要创建一个额外的 PlayerController。
在默认路径新建一个 C++ 的 PlayerController,命名为 RPCController。作为游玩时的玩家控制器。
RPCController.h
protected:
	virtual void BeginPlay() override;
 
RPCController.cpp
void ARPCController::BeginPlay()
{
	Super::BeginPlay();
	// 设定输入模式
	bShowMouseCursor = false;
	FInputModeGameOnly InputMode;
	SetInputMode(InputMode);
}
 
会话中的 GameMode 与会话状态监听
下面内容截取自梁迪老师准备的 RPC 联网文档:
- GameMode 只在服务端存在,可以用来管理服务端对象和客户端对象
 - 创建项目时,会自动生成项目名 GameMode 类,如果没有在编辑器的 WorldSetting 中指定 GameMode,运行游戏后会自动使用这一个 GameMode 来作为游戏的 GameMode
 - GameMode 的 DefaultPawnClass 需要自己设置,如果不设置的话会默认设置为 DefaultPawn 类,如果要使用自己定义的角色类,需要把 DefaultPawnClass 设置为 NULL,然后在 
PostLogin()以及Logout()等函数进行角色的生成 - 重写 GameMode 下的 
PostLogin()函数,在每次有新的端连接上服务端时(服务端创建时自己也会调用),会调用这个函数并且传入对应端的控制器 PlayerController,可以在这个方法里生成自定义的角色并且添加到对应控制器的 Process 让对应端进行控制,这样子的话角色就都是在服务端进行生成的,在服务端调用角色的HasAuthority()会返回 true,在客户端调用角色的HasAuthority()会返回 false。如果不重写这个PostLogin()方法,而是在 WorldSetting 下设定 DefaultPawnClass 为自定义的角色类,那么客户端的角色依然是由服务端生成的,在服务端调用角色的HasAuthority()会返回 true,客户端调用角色的HasAuthority()会返回 false。(推荐自己重写PostLogin()进行处理) - 重写 GameMode 下的 
Logout()函数,可以处理客户端登出时的逻辑 - GameMode 提供 
RestartPlayer()函数允许寻找可用的 PlayerStart 点并且重新生成角色,其最主要的目的就是寻找生成点。可以在PostLogin()中通过NewPlayer->GetPawn()判断角色是否存在,如果存在的话,可以销毁。重写RestartPlayer()函数,销毁角色后调用RestartPlayer()函数,把NewPlayer(类型为 PlayerController*)参数传入进行自定义的重新生成对象,类似函数还有RestartPlayerAtPlayerStart()以及RestartPlayerAtTransform(),如果自己书写生成点逻辑的话可以不用调用这个函数 
接下来我们编写一下 GameMode 在 “端进入会话” 与 “端退出会话” 时所运行的逻辑。
RPCCourseGameMode.h
UCLASS(minimalapi)
class ARPCCourseGameMode : public AGameModeBase
{
	GENERATED_BODY()
public:
	ARPCCourseGameMode();
	// 用户进入
	virtual void PostLogin(APlayerController* NewPlayer) override;
	// 用户退出
	virtual void Logout(AController* Exiting) override;
protected:
	// 统计当前加入会话的玩家数量
	int32 PlayerCount;
};
 
RPCCourseGameMode.h
// 引入头文件
#include "RPCController.h"
#include "GameFramework/PlayerStart.h"
#include "Kismet/GameplayStatics.h"
#include "RPCHelper.h"
ARPCCourseGameMode::ARPCCourseGameMode()
{
	PlayerControllerClass = ARPCController::StaticClass();
	// 如果不给 WorldSetting 指定 GameMode,游戏运行时会自动把创建项目时生成的 {项目名}GameMode 这个类给设置上去
	// 如果创建的 GameMode 不指定 PawnClass 的话,会自动设定为 ADefaultPawn 类,所以这里必须给其设置 NULL
	DefaultPawnClass = NULL;
	PlayerCount = 0;
}
void ARPCCourseGameMode::PostLogin(APlayerController* NewPlayer)
{
	Super::PostLogin(NewPlayer);
	// 销毁已经存在的角色对象
	if (NewPlayer->GetPawn()) {
		GetWorld()->DestroyActor(NewPlayer->GetPawn());
	}
	// 获取场上所有的玩家生成点
	TArray<AActor*> ActArray;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), ActArray);
	if (ActArray.Num() > 0) {
		PlayerCount++;
		// 创建角色后让玩家控制器接管角色
		UClass* CharacterClass = LoadClass<ARPCCourseCharacter>(NULL, TEXT("Blueprint'/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter.ThirdPersonCharacter_C'"));
		ARPCCourseCharacter* NewCharacter = GetWorld()->SpawnActor<ARPCCourseCharacter>(CharacterClass, ActArray[0]->GetActorLocation() + FVector(0.f, PlayerCount * 200.f, 0.f), ActArray[0]->GetActorRotation());
		NewPlayer->Possess(NewCharacter);
		DDH::Debug() << NewPlayer->GetName() << " Login" << DDH::Endl();
	}
}
void ARPCCourseGameMode::Logout(AController* Exiting)
{
	Super::Logout(Exiting);
	PlayerCount--;
	DDH::Debug() << Exiting->GetName() << " Logout" << DDH::Endl();
}
 
编译后,将 GameMap 的默认 GameMode 设置成 RPCCourseGameMode。

 随后在 MenuMap 运行游戏,在服务端点击创建服务端按钮,创建成功并进入了 GameMap;在客户端点击加入服务器,稍等片刻后加入成功,进入了 GameMap,并且此时可以自由控制角色移动了。(客户端创建服务器,服务端加入也可以)
如果将玩家数量设置为 3,重复上述操作,然后在创建服务端的那个窗口操控角色按 J 键,可以看到只有另外两个端能看到各自角色的位置生成了一个红色的数字,这说明即便角色的生成逻辑在 GameMode,角色的所属权还是归客户端所有的。










