UE5多人游戏开发:C++实现会话查找与加入的完整指南 1. 项目概述构建多人游戏会话的基石在UE5的多人游戏开发中让玩家能够找到并加入一个正在进行的游戏会话是构建在线体验最核心、也最激动人心的第一步。想象一下你开发了一个精彩的第三人称射击TPS游戏玩家创建了房间但其他人却找不到入口这无疑是灾难性的。今天要拆解的正是《P11 设置加入游戏会话Setup for Joining Sessions》这一关键环节。这不仅仅是调用一个API那么简单它涉及到客户端如何发现服务器、如何理解会话信息、以及如何建立连接这一整套逻辑的初始化与配置。对于刚接触UE5网络模块的开发者来说这里充满了陷阱比如会话查询失败、连接超时或者即使找到了会话却无法加入。本文将基于一个典型的UE5 C TPS项目深入剖析设置加入游戏会话的完整流程从底层原理到每一行代码的意图并分享我在实际项目中趟过的坑和总结出的最佳实践目标是让你不仅能复现功能更能透彻理解其背后的网络架构思想。2. 核心需求与架构设计解析2.1 为什么需要专门的“加入会话”设置在单机或本地多人游戏里玩家直接进入游戏世界即可。但在网络游戏中玩家客户端需要先定位到主机服务器创建的一个虚拟“房间”即游戏会话Session。UE5的在线子系统Online Subsystem抽象了这部分功能但要让其工作客户端必须进行正确的配置。这个“设置”过程本质上是初始化客户端的会话接口并为其配备“寻找房间”和“敲门进入”的能力。如果没有这个设置客户端就像一个没有地图和通讯设备的探险家根本不知道服务器世界存在于网络的哪个角落。2.2 会话加入流程的宏观蓝图整个加入流程可以简化为三个主要阶段理解这个蓝图对后续代码分析至关重要初始化与查询客户端初始化在线会话接口并向在线服务如Steam、Epic Online Services或NULL开发接口发送查询请求查找所有符合条件如特定地图、游戏模式的可用会话。选择与请求客户端从查询结果列表中选择一个目标会话然后向该会话的“所有者”通常是创建该会话的服务器或客户端发送加入请求。旅行与连接如果请求被批准客户端将执行一次“网络旅行”Network Travel到会话指定的地图并建立与主机的稳定网络连接最终完成加入过程。我们的“设置”工作主要聚焦在完美地实现第一阶段并为第二、三阶段铺平道路。2.3 关键组件与类职责分析在UE5 C中以下几个类是完成此任务的核心APlayerController玩家控制器是客户端逻辑的枢纽。通常我们会在一个专属的玩家控制器如AMyPlayerController或游戏实例UGameInstance中编写会话查找和加入的逻辑。IOnlineSessionPtr在线会话接口的核心指针。通过它我们可以调用FindSessions、JoinSession等所有与会话相关的函数。获取它的方式是Online::GetSessionInterface()。FOnlineSessionSearch会话搜索请求的载体。我们需要创建它的一个实例并设置搜索条件比如最大搜索数量MaxSearchResults、查询状态QuerySettings.SearchState等。FOnFindSessionsCompleteDelegate一个多播委托。当会话查询完成无论成功与否时会触发此委托。我们必须将一个自定义的回调函数如OnFindSessionsComplete绑定到它以处理查询结果。注意很多新手会混淆Session会话和Connection连接。会话是逻辑上的“房间”概念由在线服务管理连接是网络底层的Socket链路。加入会话成功后引擎会自动处理连接的建立。3. 核心代码实现与逐行解读接下来我们将在自定义的PlayerController中实现加入游戏会话的功能。假设我们有一个名为AMyTPSPlayerController的类。3.1 第一步声明委托回调函数与成员变量首先在头文件.h中声明必要的成员和函数。// MyTPSPlayerController.h #pragma once #include “GameFramework/PlayerController.h” #include “Interfaces/OnlineSessionInterface.h” #include “MyTPSPlayerController.generated.h” // 前向声明用于智能指针 class FOnlineSessionSearch; UCLASS() class MYTPS_API AMyTPSPlayerController : public APlayerController { GENERATED_BODY() public: AMyTPSPlayerController(); // 用于触发搜索会话的函数可以被蓝图调用 UFUNCTION(BlueprintCallable, Category “Multiplayer|Sessions”) void FindGameSessions(); // 用于加入指定索引会话的函数 UFUNCTION(BlueprintCallable, Category “Multiplayer|Sessions”) void JoinGameSession(int32 SessionIndex); protected: virtual void BeginPlay() override; private: // 指向在线会话接口的智能指针 IOnlineSessionPtr OnlineSessionInterface; // 会话搜索请求对象用TSharedPtr管理生命周期 TSharedPtrFOnlineSessionSearch SessionSearch; // 委托回调函数当查找会话完成时被调用 void OnFindSessionsComplete(bool bWasSuccessful); // 委托回调函数当加入会话完成时被调用 void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); };关键点解读IOnlineSessionPtr这是一个TSharedPtrIOnlineSession的别名。使用智能指针可以避免手动内存管理更安全。TSharedPtrFOnlineSessionSearch同样使用智能指针来管理搜索对象确保其在回调期间一直有效。两个回调函数都是私有的因为它们属于内部逻辑通常不需要蓝图直接调用。使用UFUNCTION(BlueprintCallable)暴露FindGameSessions和JoinGameSession给蓝图便于在UI按钮上调用这是非常实用的设计模式。3.2 第二步初始化会话接口与绑定委托在源文件.cpp的BeginPlay或构造函数中我们需要获取会话接口并绑定委托。// MyTPSPlayerController.cpp #include “MyTPSPlayerController.h” #include “OnlineSessionSettings.h” #include “Online/OnlineSessionNames.h” AMyTPSPlayerController::AMyTPSPlayerController() { // 通常建议在BeginPlay中初始化但构造函数中也行 } void AMyTPSPlayerController::BeginPlay() { Super::BeginPlay(); // 1. 获取在线会话接口 IOnlineSubsystem* OnlineSub IOnlineSubsystem::Get(); if (OnlineSub) { OnlineSessionInterface OnlineSub-GetSessionInterface(); if (OnlineSessionInterface.IsValid()) { // 2. 将成员函数绑定到委托 // 注意AddUObject用于绑定UObject成员函数确保正确的生命周期管理 OnFindSessionsCompleteDelegateHandle OnlineSessionInterface-AddOnFindSessionsCompleteDelegate_Handle( FOnFindSessionsCompleteDelegate::CreateUObject(this, AMyTPSPlayerController::OnFindSessionsComplete) ); OnJoinSessionCompleteDelegateHandle OnlineSessionInterface-AddOnJoinSessionCompleteDelegate_Handle( FOnJoinSessionCompleteDelegate::CreateUObject(this, AMyTPSPlayerController::OnJoinSessionComplete) ); } else { UE_LOG(LogTemp, Error, TEXT(“Failed to get valid OnlineSessionInterface!”)); } } else { // 如果使用NULL子系统用于局域网开发这里也能获取到 UE_LOG(LogTemp, Warning, TEXT(“No OnlineSubsystem found. Using NULL subsystem for local testing.”)); // 即使没有在线服务接口也可能存在NULL接口所以不要直接返回。 OnlineSessionInterface IOnlineSubsystem::Get()-GetSessionInterface(); // ... 同样需要绑定委托 } }实操心得IOnlineSubsystem::Get()这是获取在线子系统实例的入口。在开发期未配置任何在线服务如Steam时它会返回一个“NULL”子系统这对于局域网测试至关重要。委托绑定时机必须在调用FindSessions之前绑定好OnFindSessionsComplete委托否则你永远收不到查询结果的通知。BeginPlay是一个安全的位置。CreateUObjectvsCreateLambda对于UObject类成员函数务必使用CreateUObject。它内部使用了弱引用能防止因Controller提前被销毁而导致的崩溃。如果是在非UObject类中则使用CreateLambda或CreateRaw但要格外小心生命周期管理。3.3 第三步实现会话查找功能这是核心功能我们来实现FindGameSessions和其回调函数。void AMyTPSPlayerController::FindGameSessions() { if (!OnlineSessionInterface.IsValid()) { UE_LOG(LogTemp, Warning, TEXT(“OnlineSessionInterface is not valid.”)); return; } // 1. 创建并配置会话搜索对象 SessionSearch MakeSharedFOnlineSessionSearch(); if (SessionSearch.IsValid()) { // 设置最大搜索结果数100是个合理的上限 SessionSearch-MaxSearchResults 100; // 设置查询状态为所有可加入的会话 SessionSearch-QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); // 2. 可选添加更多搜索过滤器 // 例如只搜索特定地图的会话 // SessionSearch-QuerySettings.Set(SETTING_MAPNAME, FString(“MyTPSMap”), EOnlineComparisonOp::Equals); UE_LOG(LogTemp, Log, TEXT(“Starting to find sessions…”)); // 3. 获取本地玩家的唯一网络ID ULocalPlayer* LocalPlayer GetLocalPlayer(); if (LocalPlayer) { // 4. 发起异步查找会话请求 OnlineSessionInterface-FindSessions( *LocalPlayer-GetPreferredUniqueNetId(), // 本地用户ID SessionSearch.ToSharedRef() // 搜索条件 // 注意我们没有传递第三个参数SearchSettings使用QuerySettings即可 ); } } } void AMyTPSPlayerController::OnFindSessionsComplete(bool bWasSuccessful) { if (bWasSuccessful SessionSearch.IsValid()) { UE_LOG(LogTemp, Log, TEXT(“FindSessions completed. Found %d sessions.”), SessionSearch-SearchResults.Num()); if (SessionSearch-SearchResults.Num() 0) { // 遍历并打印所有找到的会话信息 for (const FOnlineSessionSearchResult Result : SessionSearch-SearchResults) { FString SessionId Result.GetSessionIdStr(); FString OwnerName Result.Session.OwningUserName; int32 Ping Result.PingInMs; int32 CurrentPlayers Result.Session.NumOpenPublicConnections Result.Session.NumOpenPrivateConnections; int32 MaxPlayers Result.Session.SessionSettings.NumPublicConnections; UE_LOG(LogTemp, Log, TEXT(“- Session: %s, Owner: %s, Ping: %dms, Players: %d/%d”), *SessionId, *OwnerName, Ping, CurrentPlayers, MaxPlayers); // 通常这里会更新UI将会话列表显示给玩家 // 例如BroadcastOnSessionsFound(SessionSearch-SearchResults); } } else { UE_LOG(LogTemp, Warning, TEXT(“No sessions found.”)); } } else { UE_LOG(LogTemp, Error, TEXT(“FindSessions failed!”)); } }深度解析FOnlineSessionSearch配置SEARCH_PRESENCE是一个关键设置。它告诉在线服务我们只查找那些设置了“在线状态”的会话这通常意味着它们是公开的、可供搜索的。对于局域网NULL子系统这个设置可能被忽略但仍应保留。FindSessions调用这是一个异步操作。函数调用后会立即返回真正的查询工作在后台进行。查询完成后会自动调用我们绑定的OnFindSessionsComplete回调。绝对不要在调用FindSessions后立即访问SessionSearch-SearchResults因为那时结果还没返回结果解析FOnlineSessionSearchResult结构体包含了会话的详细信息如创建者、Ping值、当前玩家数、最大玩家数以及自定义的SessionSettings。这些信息是更新UI列表的基础。3.4 第四步实现会话加入功能当玩家从UI列表中选择一个会话后调用JoinGameSession。void AMyTPSPlayerController::JoinGameSession(int32 SessionIndex) { if (!OnlineSessionInterface.IsValid() || !SessionSearch.IsValid()) { return; } if (SessionSearch-SearchResults.IsValidIndex(SessionIndex)) { const FOnlineSessionSearchResult SelectedSession SessionSearch-SearchResults[SessionIndex]; ULocalPlayer* LocalPlayer GetLocalPlayer(); if (LocalPlayer) { // 发起异步加入会话请求 OnlineSessionInterface-JoinSession( *LocalPlayer-GetPreferredUniqueNetId(), // 本地用户ID NAME_GameSession, // 会话名称通常使用NAME_GameSession SelectedSession // 选中的会话结果 ); UE_LOG(LogTemp, Log, TEXT(“Attempting to join session: %s”), *SelectedSession.GetSessionIdStr()); } } else { UE_LOG(LogTemp, Warning, TEXT(“Invalid session index: %d”), SessionIndex); } } void AMyTPSPlayerController::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (Result EOnJoinSessionCompleteResult::Success) { UE_LOG(LogTemp, Log, TEXT(“JoinSession succeeded for session: %s”), *SessionName.ToString()); // 加入会话成功现在需要旅行到服务器地图 FString TravelURL; if (OnlineSessionInterface.IsValid() OnlineSessionInterface-GetResolvedConnectString(SessionName, TravelURL)) { APlayerController* PC this; // 当前PlayerController if (PC) { // 这是最关键的一步客户端旅行到服务器 PC-ClientTravel(TravelURL, TRAVEL_Absolute); UE_LOG(LogTemp, Log, TEXT(“ClientTravel to: %s”), *TravelURL); } } else { UE_LOG(LogTemp, Error, TEXT(“Failed to get connect string for session: %s”), *SessionName.ToString()); } } else { // 处理各种失败情况 FString FailureReason; switch (Result) { case EOnJoinSessionCompleteResult::SessionIsFull: FailureReason TEXT(“Session is full.”); break; case EOnJoinSessionCompleteResult::SessionDoesNotExist: FailureReason TEXT(“Session does not exist.”); break; case EOnJoinSessionCompleteResult::CouldNotRetrieveAddress: FailureReason TEXT(“Could not retrieve server address.”); break; case EOnJoinSessionCompleteResult::AlreadyInSession: FailureReason TEXT(“Already in this session.”); break; case EOnJoinSessionCompleteResult::UnknownError: default: FailureReason TEXT(“Unknown error.”); break; } UE_LOG(LogTemp, Error, TEXT(“JoinSession failed: %s”), *FailureReason); // 通知UI显示错误信息 } }核心要点与避坑指南JoinSession也是异步的和FindSessions一样它不会阻塞线程结果通过OnJoinSessionComplete委托返回。ClientTravel是灵魂这是整个流程中最容易遗漏也最关键的一步。JoinSession成功只意味着在线服务记录了你的加入意向实际的网络连接和地图加载是由ClientTravel触发的。TravelURL包含了服务器的IP地址、端口和地图信息由GetResolvedConnectString从会话信息中解析得到。TRAVEL_Absolute参数使用TRAVEL_Absolute意味着使用完整的URL包含服务器地址进行旅行这是加入远程服务器的标准方式。如果是本地监听服务器可能会使用TRAVEL_Relative。错误处理至关重要OnJoinSessionComplete提供了详细的失败原因枚举。务必根据不同的原因给玩家清晰的反馈例如“房间已满”、“房间不存在”等这能极大提升用户体验。4. 常见问题排查与实战技巧即使代码看起来正确在实际运行中你仍可能遇到各种问题。下面是我在多个项目中总结的排查清单和技巧。4.1 问题一FindSessions返回成功但结果列表为空可能原因及解决方案可能原因排查步骤与解决方案网络环境/防火墙确保开发机客户端和服务器的UDP端口默认7777在防火墙中已开放。对于Steam等在线服务还需要开放额外端口。会话未正确广告检查创建会话的服务器代码。确保在创建会话CreateSession时将SessionSettings.bShouldAdvertise设置为true并且bIsLANMatch与客户端的搜索设置匹配都是局域网或都是在线。搜索条件不匹配检查服务器会话设置SessionSettings和客户端搜索条件QuerySettings。例如服务器设置了自定义属性SETTING_GAMEMODE为“Deathmatch”而客户端在搜索时过滤了“TeamDeathmatch”就会找不到。开发初期建议在客户端减少过滤条件。在线子系统配置检查DefaultEngine.ini中的OnlineSubsystem配置。客户端和服务器必须使用相同的子系统如Steam或都使用NULL进行局域网测试。时机问题确保客户端在服务器成功创建并广告会话之后才发起搜索。可以添加简单的日志或等待几秒钟再搜索。实操心得在开发初期我强烈建议先使用NULL子系统进行局域网测试。在同一台电脑上运行一个编辑器实例作为服务器Play As Listen Server另一个编辑器实例作为客户端Play As Client这样可以排除Steam API配置等复杂因素快速验证核心逻辑是否正确。4.2 问题二JoinSession成功但ClientTravel后卡住或失败可能原因及解决方案可能原因排查步骤与解决方案Connect String解析失败GetResolvedConnectString返回false。检查服务器端创建会话时是否正确设置了SessionSettings.bUsesPresence和SessionSettings.bAllowJoinInProgress。对于NULL子系统确保服务器IP地址正确。地图名称或路径错误TravelURL中的地图路径必须与服务器当前加载的地图完全匹配包括在项目中的路径。服务器在创建会话时其SessionSettings.Settings里应该包含地图信息。网络兼容性确保客户端和服务器使用的是完全相同的项目构建版本包括所有蓝图和资产。任何差异都可能导致旅行后连接不兼容而断开。服务器未正确监听确认服务器进程确实在运行并监听指定端口。可以用命令行工具如netstat -anClientTravel调用对象错误确保调用ClientTravel的是客户端的PlayerController而不是服务器端的。在OnJoinSessionComplete回调中this指针就是客户端的PlayerController这是安全的。4.3 问题三委托回调函数从未被调用这是最令人头疼的问题之一代码看似在运行但没有任何反应。检查委托绑定确认在调用FindSessions或JoinSession之前已经成功绑定了委托。在绑定后加一句日志UE_LOG(LogTemp, Log, TEXT(“Delegate bound.”))。检查对象生命周期如果你的PlayerController被过早销毁例如在关卡切换时那么绑定在其上的委托回调将不会执行。确保持有这些委托的对象在回调发生期间是存活的。在BeginPlay中绑定在EndPlay中移除是良好实践。移除委托为了避免重复绑定导致回调多次执行可以在EndPlay函数中移除委托。void AMyTPSPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (OnlineSessionInterface.IsValid()) { OnlineSessionInterface-ClearOnFindSessionsCompleteDelegate_Handle(OnFindSessionsCompleteDelegateHandle); OnlineSessionInterface-ClearOnJoinSessionCompleteDelegate_Handle(OnJoinSessionCompleteDelegateHandle); } Super::EndPlay(EndPlayReason); }4.4 性能与体验优化技巧搜索节流不要允许玩家无限频繁地点击“刷新”按钮。可以在FindGameSessions函数开始时添加一个冷却时间检查防止向在线服务发送过多请求。异步UI更新会话搜索是异步的UI更新也应该是异步的。不要在FindGameSessions函数中直接阻塞线程等待结果。使用委托/事件分发器DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam将搜索结果列表传递给UI组件。Ping排序FOnlineSessionSearchResult中的PingInMs字段反映了网络延迟。在将列表展示给玩家前按Ping值从低到高排序可以显著提升体验让玩家优先加入延迟低的房间。自定义会话属性充分利用SessionSettings.Set来设置自定义属性如游戏模式SETTING_GAMEMODE、地图名称SETTING_MAPNAME、回合数等。客户端在搜索时可以通过QuerySettings.Set进行精确过滤让玩家快速找到想要的房间。设置加入游戏会话是打开UE5多人游戏世界大门的钥匙。这个过程初看有些繁琐涉及异步委托、在线接口和网络旅行等多个概念但一旦理清其脉络——初始化接口、配置搜索、发起查询、处理结果、请求加入、最终旅行——就会发现它是一套设计精良、逻辑清晰的流程。最重要的经验是永远不要假设网络操作是即时或必然成功的每一个步骤都需要健壮的错误处理和清晰的用户反馈。从NULL子系统下的局域网测试开始逐步过渡到完整的在线服务是学习这条路径最平稳的方式。当你看到自己的客户端成功搜索并加入另一个进程运行的服务器时那种成就感无疑是驱动你继续深入UE5网络编程的强大动力。