赞
踩
前面我们通过蓝图节点实现了局域网连接的功能,实际上我们还可以给项目打包后生成的 .exe 文件创建一个快捷方式,然后修改这个快捷方式的属性中的目标就可以实现简易的联网功能。
下面内容截取自梁迪老师准备的 RPC 联网文档:
使用 .exe 后缀输入和 open IP 地址联网
注意:这里只讨论 NM_Standalone、NM_ListenServer 以及 NM_Client 的情况
GetNetMode()
返回 NM_Standalone 类型,在这个状态下打印 GetWorld()->IsServer()
以及 HasAuthority()
都是 true,如果在存在监听服务器的情况下,运行命令行 open 127.0.0.1
,该单独端就会链接上监听服务器,执行 GetNetMode()
返回 NM_Client 类型,打印 GetWorld()->IsServer()
以及 HasAuthority()
都是 false,如果再次运行命令行 open 127.0.0.1
,该端会先断开服务器,然后再链接一次 (空格)?listen
,如:RPCProject.exe ?listen
,运行该快捷方式就会运行监听服务器端,执行 GetNetMode()
返回 NM_ListenServer 类型,打印 GetWorld()->IsServer()
以及 HasAuthority()
都是true,如果运行命令行 open 127.0.0.1
,该监听服务器端就会变成单独端 NM_Standalone(空格)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
,服务器会关闭。
前面我们用蓝图和快捷方式实现聆听服务器联机,接下来我们尝试下用 C++ 来实现同样的效果。下面内容截取自梁迪老师准备的 RPC 联网文档:
创建寻找加入会话 C++ 模式(这里只实现局域网)
C++ 联网步骤和蓝图基本相同,但是中间多了一个 StartSession()
方法需要调用,主要可以参考
这些类的实现,具体实现参考项目里的 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 课程到这里就结束了,衷心感谢梁迪老师提供的优质课程 : )
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。