UE4 C++联网RPC教程笔记(三)(第8~9集)完结
- 8. exe 后缀实现监听服务器
- 9. C++ 实现监听服务器
8. exe 后缀实现监听服务器
前面我们通过蓝图节点实现了局域网连接的功能,实际上我们还可以给项目打包后生成的 .exe 文件创建一个快捷方式,然后修改这个快捷方式的属性中的目标就可以实现简易的联网功能。
下面内容截取自梁迪老师准备的 RPC 联网文档:
使用 .exe 后缀输入和 open IP 地址联网
注意:这里只讨论 NM_Standalone、NM_ListenServer 以及 NM_Client 的情况
- NM_Standalone:打包出 exe 后,不创建快捷方式直接运行 exe,执行
GetNetMode()
返回 NM_Standalone 类型,在这个状态下打印GetWorld()->IsServer()
以及HasAuthority()
都是 true,如果在存在监听服务器的情况下,运行命令行open 127.0.0.1
,该单独端就会链接上监听服务器,执行GetNetMode()
返回 NM_Client 类型,打印GetWorld()->IsServer()
以及HasAuthority()
都是 false,如果再次运行命令行open 127.0.0.1
,该端会先断开服务器,然后再链接一次 - NM_ListenServer:打包出来的 exe 生成快捷方式,并且在快捷方式的属性下在 exe 结尾添加
(空格)?listen
,如:RPCProject.exe ?listen
,运行该快捷方式就会运行监听服务器端,执行GetNetMode()
返回 NM_ListenServer 类型,打印GetWorld()->IsServer()
以及HasAuthority()
都是true,如果运行命令行open 127.0.0.1
,该监听服务器端就会变成单独端 NM_Standalone - NM_Client:打包出来的 exe 生成快捷方式,并且在快捷方式的属性下在 exe 结尾添加
(空格)127.0.0.1 -game
,如:RPCProject.exe 127.0.0.1 -game
,运行该快捷方式,如果存在监听端,就会链接上监听服务器,成为客户端,执行GetNetMode()
返回 NM_Client 类型,打印GetWorld()->IsServer()
以及HasAuthority()
都是 false
接下来我们在 GameMap 里用到的玩家控制器中添加一些打印当前端的逻辑。
RPCController.h
protected:.
void EchoNetMode();
RPCController.cpp
// 引入头文件
#include "RPCHelper.h"
void ARPCController::BeginPlay()
{
Super::BeginPlay();
// 限定打开的窗口尺寸。此处老师将变量名拼写错成 “Src”
FString ScreenCommand = FString("r.setres 1280x720w");
ConsoleCommand(ScreenCommand);
bShowMouseCursor = false;
FInputModeGameOnly InputMode;
SetInputMode(InputMode);
// 打印当前的端
EchoNetMode();
}
void ARPCController::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;
}
}
编译后,将默认关卡设置为 GameMap。随后开始打包。
打包成功后,运行 .exe 文件,可以看到左上角打印了当前的端名以及控制器名。
创建 .exe 文件的一个快捷方式,命名为 RPCCourseServer (保留 .exe 后缀),随后修改该文件的属性。
运行 RPCCourseServer.exe,可以看到左上角已经变成了聆听服务器。
再创建一个快捷方式,命名为 RPCCourseClient,这次给属性里的目标添加后缀 (空格)127.0.0.1 -game
。先运行 RPCCourseServer,然后再运行 RPCCourseClient.exe,可以看到后者的窗口里左上角显示是客户端。
此时在服务端按下 J 键,只有在客户端能看到角色处生成了红色的数字 1。说明联网方法和变量都是可以用的。
关掉客户端,服务端左上角会显示客户端的控制器登出了。
保持服务端开启,运行 RPCCourse.exe,按 ~
键(波浪符)呼出控制台,输入 open 127.0.0.1
。可以看到独立端加入了服务端,并且原独立端左上角输出了当前为客户端。
此时在服务端按下 J 键,也是只有原独立端可以看到自己角色位置生成了一个红色的数字 1。
在服务端呼出控制台然后输入 open 127.0.0.1
,服务器会关闭。
9. C++ 实现监听服务器
前面我们用蓝图和快捷方式实现聆听服务器联机,接下来我们尝试下用 C++ 来实现同样的效果。下面内容截取自梁迪老师准备的 RPC 联网文档:
创建寻找加入会话 C++ 模式(这里只实现局域网)
C++ 联网步骤和蓝图基本相同,但是中间多了一个 StartSession()
方法需要调用,主要可以参考
- UCreateSessionCallbackProxy
- UStartSessionCallbackProxy
- UFindSessionsCallbackProxy
- UJoinSessionCallbackProxy
- UDestroySessionCallbackProxy
这些类的实现,具体实现参考项目里的 URPCInstance 类。
UE4 官方推荐将联网的逻辑放在 GameInstance 下处理,GameInstance 在整个游戏所有关卡中都存在,用来传递关卡数据,在保存数据方面起作用,联网数据放在 GameInstance 下方便在任何关卡去操作联网
接下来开始实操。在默认路径下创建一个 C++ 的 Instance 类,命名为 RPCInstance。
将默认地图设置成 MenuMap。
来到主界面 UI 类,声明一个 URPCInstance 的指针和相应的注册方法,用来保存对 GameInstance 的引用,并且声明两个蓝图可调用的方法用于接入主界面的按钮点击事件。
MenuWidget.h
// 提前声明
class URPCInstance;
UCLASS()
class RPCCOURSE_API UMenuWidget : public UUserWidget
{
GENERATED_BODY()
public:
void AssignRPCInstance(URPCInstance* InInstance);
UFUNCTION(BlueprintCallable)
void LANServerEvent();
UFUNCTION(BlueprintCallable)
void LANClientEvent();
public:
URPCInstance* RPCInstance;
};
MenuWidget.cpp
// 引入头文件
#include "RPCInstance.h"
void UMenuWidget::AssignRPCInstance(URPCInstance* InInstance)
{
RPCInstance = InInstance;
}
void UMenuWidget::LANServerEvent()
{
RPCInstance->HostSession();
}
void UMenuWidget::LANClientEvent()
{
RPCInstance->ClientSession();
}
RPCInstance 里承载着联网相关的逻辑,主要都是调用网络模块的 API 和利用委托绑定回调函数。
RPCInstance.h
// 引入头文件
#include "Interfaces/OnlineSessionInterface.h" // 如果这个不行就用下面这句
//#include "../Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineSessionInterface.h"
#include "Delegates/IDelegateInstance.h"
#include "RPCInstance.generated.h"
// 提前声明
class IOnlineSubsystem;
class APlayerController;
UCLASS()
class RPCCOURSE_API URPCInstance : public UGameInstance
{
GENERATED_BODY()
public:
URPCInstance();
// 注册玩家控制器,并且获取联网系统和端 ID
void AssignPlayerController(APlayerController* InController);
// 创建会话
void HostSession();
// 寻找会话
void ClientSession();
// 销毁会话
void DestroySession();
protected:
// 开启服务器回调函数
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
void OnStartOnlineGameComplete(FName SessionName, bool bWasSuccessful);
// 加入服务器回调函数
void OnFindSessionsComplete(bool bWasSuccessful);
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
// 销毁会话回调函数
void OnDestroySessionComplete(FName SessionName, bool bWAsSuccessful);
protected:
APlayerController* PlayerController;
// 开启服务器委托与句柄
FOnCreateSessionCompleteDelegate OnCreateSessionCompleteDelegate;
FOnStartSessionCompleteDelegate OnStartSessionCompleteDelegate;
FDelegateHandle OnCreateSessionCompleteDelegateHandle;
FDelegateHandle OnStartSessionCompleteDelegateHandle;
// 加入服务器委托与句柄
FOnFindSessionsCompleteDelegate OnFindSessionsCompleteDelegate;
FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate;
FDelegateHandle OnFindSessionsCompleteDelegateHandle;
FDelegateHandle OnJoinSessionCompleteDelegateHandle;
// 销毁会话委托与句柄
FOnDestroySessionCompleteDelegate OnDestroySessionCompleteDelegate;
FDelegateHandle OnDestroySessionCompleteDelegateHandle;
// 联网系统
IOnlineSubsystem* OnlineSub;
// 端的 ID
TSharedPtr<const FUniqueNetId> UserID;
// 保存寻找到的 Sessions
TSharedPtr<FOnlineSessionSearch> SearchObject;
};
RPCInstance.cpp
// 引入头文件
#include "../Plugins/Online/OnlineSubsystem/Source/Public/OnlineSubsystem.h"
#include "../Plugins/Online/OnlineSubsystem/Source/Public/OnlineSessionSettings.h"
#include "../Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineSessionInterface.h"
#include "../Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/OnlineSubsystemUtils.h"
#include "GameFramework/PlayerController.h"
#include "RPCHelper.h"
#include "Kismet/GameplayStatics.h"
URPCInstance::URPCInstance()
{
// 绑定回调函数
OnCreateSessionCompleteDelegate = FOnCreateSessionCompleteDelegate::CreateUObject(this, &URPCInstance::OnCreateSessionComplete);
OnStartSessionCompleteDelegate = FOnStartSessionCompleteDelegate::CreateUObject(this, &URPCInstance::OnStartOnlineGameComplete);
OnFindSessionsCompleteDelegate = FOnFindSessionsCompleteDelegate::CreateUObject(this, &URPCInstance::OnFindSessionsComplete);
OnJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate::CreateUObject(this, &URPCInstance::OnJoinSessionComplete);
OnDestroySessionCompleteDelegate = FOnDestroySessionCompleteDelegate::CreateUObject(this, &URPCInstance::OnDestroySessionComplete);
}
void URPCInstance::AssignPlayerController(APlayerController* InController)
{
PlayerController = InController;
// 获取 OnlineSub
// 获取方式一:Online::GetSubsystem(GetWorld(), NAME_None),推荐方式
// 获取方式二:使用 IOnlineSubsystem::Get(),直接获取可以 CreateSession 但是 JoinSession 后客户端没有跳转场景
OnlineSub = Online::GetSubsystem(PlayerController->GetWorld(), NAME_None);
// 获取 UserID
// 获取方式一:UGameplayStatics::GetGameInstance(GetWorld())->GetLocalPlayers()[0]->GetPreferredUniqueNetId()
if (GetLocalPlayers().Num() == 0) {
DDH::Debug() << "No LocalPlayer Exists, Can't Get UserID" << DDH::Endl();
}
else {
UserID = (*GetLocalPlayers()[0]->GetPreferredUniqueNetId()).AsShared();
}
#if 0
// 获取方式二:使用 PlayerState 获取,该方式在打包成 exe 运行无问题,但是在编辑器模式下运行多个窗口,就会找不到 PlayerState
if (PlayerController->PlayerState)
UserID = PlayerController->PlayerState->UniqueId.GetUniqueNetId();
else
DDH::Debug() << "No PlayerState Exists, Can't Get UserID" << DDH::Endl();
#endif
// 如果在这里直接获取 Session 运行时会报错,生命周期的问题
}
void URPCInstance::HostSession()
{
if (OnlineSub) {
IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if (Session.IsValid()) {
// 会话设置
FOnlineSessionSettings Settings;
// 连接数
Settings.NumPublicConnections = 10;
Settings.bShouldAdvertise = true;
Settings.bAllowJoinInProgress = true;
// 使用局域网
Settings.bIsLANMatch = true;
Settings.bUsesPresence = true;
Settings.bAllowJoinViaPresence = true;
// 绑定委托
OnCreateSessionCompleteDelegateHandle = Session->AddOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegate);
// 创建会话
Session->CreateSession(*UserID, NAME_GameSession, Settings);
}
}
}
void URPCInstance::ClientSession()
{
if (OnlineSub) {
IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if (Session.IsValid()) {
// 实例化搜索结果指针并且设定参数
SearchObject = MakeShareable(new FOnlineSessionSearch);
// 返回结果数
SearchObject->MaxSearchResults = 10;
// 是否是局域网,就是 IsLAN
SearchObject->bIsLanQuery = true;
SearchObject->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
// 绑定寻找会话委托
OnFindSessionsCompleteDelegateHandle = Session->AddOnFindSessionsCompleteDelegate_Handle(OnFindSessionsCompleteDelegate);
// 进行会话寻找
Session->FindSessions(*UserID, SearchObject.ToSharedRef());
}
}
}
void URPCInstance::DestroySession()
{
if (OnlineSub) {
IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if (Session.IsValid()) {
// 绑定销毁会话委托
OnDestroySessionCompleteDelegateHandle = Session->AddOnDestroySessionCompleteDelegate_Handle(OnDestroySessionCompleteDelegate);
// 执行销毁会话
Session->DestroySession(NAME_GameSession);
}
}
}
void URPCInstance::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
if (OnlineSub) {
IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if (Session.IsValid()) {
// 解绑创建会话完成回调函数
Session->ClearOnCreateSessionCompleteDelegate_Handle(OnCreateSessionCompleteDelegateHandle);
// 判断创建会话是否成功
if (bWasSuccessful) {
DDH::Debug() << "CreateSession Succeed" << DDH::Endl();
// 绑定开启会话委托
OnStartSessionCompleteDelegateHandle = Session->AddOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegate);
// 执行开启会话
Session->StartSession(NAME_GameSession);
}
else
DDH::Debug() << "CreateSession Failed" << DDH::Endl();
}
}
}
void URPCInstance::OnStartOnlineGameComplete(FName SessionName, bool bWasSuccessful)
{
if (OnlineSub) {
IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if (Session.IsValid()) {
// 注销开启会话委托绑定
Session->ClearOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegateHandle);
if (bWasSuccessful) {
DDH::Debug() << "StartSession Succeed" << DDH::Endl();
// 服务端跳转场景
UGameplayStatics::OpenLevel(PlayerController->GetWorld(), FName("GameMap"), true, FString("listen"));
}
else
DDH::Debug() << "StartSession Failed" << DDH::Endl();
}
}
}
void URPCInstance::OnFindSessionsComplete(bool bWasSuccessful)
{
if (OnlineSub) {
IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if (Session.IsValid()) {
// 取消寻找会话委托绑定
Session->ClearOnStartSessionCompleteDelegate_Handle(OnStartSessionCompleteDelegateHandle);
if (bWasSuccessful) {
// 如果收集的结果存在并且大于 1
if (SearchObject.IsValid() && SearchObject->SearchResults.Num() > 0) {
DDH::Debug() << "Find Sessions Succeed" << DDH::Endl();
// 绑定加入 Session 委托
OnJoinSessionCompleteDelegateHandle = Session->AddOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegate);
// 执行加入 Session
Session->JoinSession(*UserID, NAME_GameSession, SearchObject->SearchResults[0]);
}
else
DDH::Debug() << "Find Sessions Succeed But Num == 0" << DDH::Endl();
}
else
DDH::Debug() << "Find Sessions Failed" << DDH::Endl();
}
}
}
void URPCInstance::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
if (OnlineSub) {
IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if (Session.IsValid()) {
// 取消加入会话委托绑定
Session->ClearOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegateHandle);
// 如果加入成功
if (Result == EOnJoinSessionCompleteResult::Success) {
// 传送玩家到新地图
FString ConnectString;
if (Session->GetResolvedConnectString(NAME_GameSession, ConnectString)) {
DDH::Debug() << "Join Sessions Succeed" << DDH::Endl();
// 客户端切换到服务器的关卡
PlayerController->ClientTravel(ConnectString, TRAVEL_Absolute);
}
}
else
DDH::Debug() << "Join Sessions Failed" << DDH::Endl();
}
}
}
void URPCInstance::OnDestroySessionComplete(FName SessionName, bool bWAsSuccessful)
{
if (OnlineSub) {
IOnlineSessionPtr Session = OnlineSub->GetSessionInterface();
if (Session.IsValid()) {
// 注销销毁会话委托
Session->ClearOnDestroySessionCompleteDelegate_Handle(OnDestroySessionCompleteDelegateHandle);
// 其他逻辑...
}
}
}
读者可能会发现上面的代码中,绑定委托后直接就通过 Session 执行相应逻辑了,然后在接下来的逻辑里解绑委托。这里笔者倾向于将这一过程理解为 装弹 —> 发射 —> 退弹壳。UE4 已经将网络模块的细枝末节都为我们封装好了,我们只需要知道如何使用就够了,当然,喜欢探索的读者也可以查阅源码去深入理解。
在主界面控制器里注册自己到 RPCInstance,并且将 RPCInstance 注册到主界面 UI。
MenuController.cpp
// 引入头文件
#include "Kismet/GameplayStatics.h"
#include "RPCInstance.h"
void AMenuController::BeginPlay()
{
// 获取 GameInstance
URPCInstance* RPCInstance = Cast<URPCInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
RPCInstance->AssignPlayerController(this);
UClass* MenuWidgetClass = LoadClass<UMenuWidget>(NULL, TEXT("WidgetBlueprint'/Game/Blueprint/MenuWidget_BP.MenuWidget_BP_C'"));
UMenuWidget* MenuWidget = CreateWidget<UMenuWidget>(GetWorld(), MenuWidgetClass);
MenuWidget->AddToViewport();
MenuWidget->AssignRPCInstance(RPCInstance); // 注册 RPCInstance 到 MenuWidget
}
最后添加一些网络模块相关的依赖。
RPCCourse.Build.cs
public RPCCourse(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "Slate", "UMG",
"HeadMountedDisplay", "OnlineSubsystem", "OnlineSubsystemUtils" }); // 添加这三个模块依赖
// 添加动态加载模组
DynamicallyLoadedModuleNames.AddRange(
new string[] {
"OnlineSubsystemNull",
}
);
}
编译后,在项目设置里将默认的 GameInstance 设置为 RPCInstance。
来到 MenuWidget_BP 的图表,重新调整两个按钮点击事件的连接节点如下:
此时运行玩家数应该是 3。运行后,在服务端创建服务器,创建成功;让另外两个客户端加入服务器,也能进入成功。并且在服务端按 J 键,另外两个客户端各自能看到自己角色处生成红色数字。
不过如果在客户端创建服务器,另外一个客户端可以加入,但是服务端加入会显示找到会话和加入成功,但不会跳转到 GameMap。所以必须要让服务端创建服务器才能正常运作。
至此,梁迪老师的 RPC 课程到这里就结束了,衷心感谢梁迪老师提供的优质课程 : )