Unreal Engine 连接 Steam(Session 操作)
Unreal Engine 提供了一种只需使用一种方式,即可兼容各平台的代码库(Unreal Engine Code Base)。
游戏想要连接处于各地的玩家、查看好友列表等功能,可以使用_Stream_、Xbox Live 、Facebook Game 提供的服务。但是它们之间是存在差异的,使用它们的服务可能要用到不同的代码。
Unreal Engine 提供了一种只需使用一种方式,即可兼容各平台的代码库( Unreal Engine Code Base)。使用它连接到一个服务,并运用它提供的服务。不同平台的差异,由 Unreal Engine 去处理,开发者只需要写一次代码。这种用来连接到各种在线服务的系统,称为 在线子系统(Online Subsystem)。
会话接口是建立连接非常重要的接口,它负责创建、管理和销毁游戏会话、可以搜索 Session 也可以配对。
Session 可以看作一个游戏实例,运行在服务器上的一组属性和会话可以公开展示,以便其它玩家找到并加入。也可以是私人的 Session ,之后被邀请的人可以加入游戏。
Session 基本生命周期:
- 创建 Session 。
- 等待玩家加入。
- 玩家加入则注册进 Session 。
- 开始一个 Session 。
- 每个玩家都在这个 Session 中玩游戏。
- 结束 Session 。
- 取消玩家的注册。
- 更新 Session 或者销毁 Session 。
以第三人称模板创建一个新项目,名字为 MenuSystem ,以此项目体验子系统。
窗口顶部的 Edit -> Plugins 搜索 online subsystem steam ,安装这个插件。勾选后重启生效。

模块只是存在于引擎中的一个代码包。打开 Visual Studio ,找到 MenuSystem.Build.cs 这个 C# 文件,修改后内容如下:
public MenuSystem(ReadOnlyTargetRules Target) : base(Target){ PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; // 新增了"OnlineSubsystemSteam", "OnlineSubsystem" PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "OnlineSubsystemSteam", "OnlineSubsystem" });}保存修改后 Ctrl + Shift + B 给它 rebuild 一下,此时如果报错,回头看一下 Unreal Engine 中的 Enable Live Coding 是否取消勾选了。
找到项目文件夹,找到 Config/DefaultEngine.ini ,在最后面添加这样一段内容:
;设置网络驱动,通过 SteamNetDriver 连接到 Steam 服务[/Script/Engine.GameEngine]+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
;设置默认服务平台为Steam[OnlineSubsystem]DefaultPlatformService=Steam
;设置 SteamDevAppId 为 480(这是通用测试 ID);要用Session,就要设置 bInitServerOnClient=true[OnlineSubsystemSteam]bEnabled=trueSteamDevAppId=480bInitServerOnClient=true
; If using Sessions; bInitServerOnClient=true;设置连接类名[/Script/OnlineSubsystemSteam.SteamNetDriver]NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"关闭 Visual Studio 和 Unreal Engine ,删除项目目录下的一些目录( Saved、Intermediate、Binaries )。接着右击 .uproject 文件,选择 Generate Visual Studio project files 。刚刚删除的目录又出现了,接着点击 .uproject ,会弹窗提示 “Would you like to rebuild them now ? “,选择是,配置完成。
为了省事,处理 Session 相关的代码直接写在 MenuSystemCharacter 的类里。首先在 MenuSystemCharacter.h 中添加一个公共属性:
public:
// Pointer to online session interface // 这个成员变量用于存储 在线会话接口 // GetSessionInterface 返回的是一个 IOnlineSessionPtr。( MenuSystemCharacter.cpp 中用到) // IOnlineSessionPtr OnlineSessionInterface; 这是下面类型的别名,所以此处直接写原本的类型,否则需要头文件引入 TSharedPtr<class IOnlineSession, ESPMode::ThreadSafe> OnlineSessionInterface;};接着在 MenuSystemCharacter.cpp 的构造函数中,在最后面添加下面的代码:
AMenuSystemCharacter::AMenuSystemCharacter(){ // ......
// 获取在线子系统 IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get(); if (OnlineSubsystem) { OnlineSessionInterface = OnlineSubsystem->GetSessionInterface();
// GEngine 是 Unreal Engine 自带的一个指针,提供了一系列方法 // 格式化输出文字到屏幕 if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, // 不会替换之前打印的消息 15.f, // 打印的内容停留 15 秒 FColor::Blue, // 文字颜色为蓝色 FString::Printf(TEXT("Found subsystem %s"), *OnlineSubsystem->GetSubsystemName().ToString()) // 获取子系统名字 ); } }}因为是直接写在角色类的构造函数中,所以游戏启动就会执行上面的代码。在 Editor 中启动后,左上方屏幕输出的是 NULL, 这个 NULL 不是空指针,而是一个专为局域网( Lan )设计的子系统。打包后就可以看了,如果打包后目录中没有输出 exe 文件,则删除项目目录下的 Saved、Intermediate、Binaries 这三个目录。rebuild 项目后打包。接着确定 Steam 在后台运行,打开打包后的程序,可以看到打印出来的效果如下:

主要三个步骤,在 MenuSystemCharacter.h 中进行创建 Session 的函数声明。MenuSystemCharacter.cpp 中实现函数声明。在蓝图中绑定什么时候执行条件。首先是 MenuSystemCharacter.h :
// 后面代码中的 IOnlineSessionPtr FOnCreateSessionCompleteDelegate 的头文件为 OnlineSessionInterface.h#include "Interfaces/OnlineSessionInterface.h"
#include "MenuSystemCharacter.generated.h" // generated.h 要放在最后面
UCLASS(config=Game)class AMenuSystemCharacter : public ACharacter{ ......
public:
// Pointer to online session interface // TSharedPtr<class IOnlineSession, ESPMode::ThreadSafe> // 引入了 OnlineSessionInterface.h ,所以直接用 IOnlineSessionPtr IOnlineSessionPtr OnlineSessionInterface;
protected: // 这个函数执行创建 Session 的逻辑,设为蓝图可调用的 UFUNCTION(BlueprintCallable) void CreateGameSeesion();
// 成功创建 Session 后执行的函数 void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
private: // 委托变量,存放 OnCreateSessionComplete FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;};接着,在 MenuSystemCharacter.cpp 中给委托赋值、实现头文件中的函数声明。
// 构造函数中给 CreateSessionCompleteDelegate 绑定一个函数// FOnCreateSessionCompleteDelegate 来自 OnlineSessionSettings.h// &ThisClass 代表本类的名字,// 构造函数中给 CreateSessionCompleteDelegate 赋值为本类中的 OnCreateSessionComplete函数AMenuSystemCharacter::AMenuSystemCharacter(): CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)){ // ......}
// 实现创建 Session 的逻辑void AMenuSystemCharacter::CreateGameSeesion() { // OnlineSessionInterface 无效时直接返回 if (!OnlineSessionInterface.IsValid()) { return; }
// NAME_GameSession 是个常量,表示当前会话名 // 判断当前会话名判断是否存在于在线会话中 auto ExistingSession = OnlineSessionInterface->GetNamedSession(NAME_GameSession); if (ExistingSession != nullptr) { // 如果存在,则销毁 Session OnlineSessionInterface->DestroySession(NAME_GameSession); }
// 添加 CreateSessionCompleteDelegate 到委托列表中 // 当 Session 创建成功后,会调用 CreateSessionCompleteDelegate 绑定的函数 OnCreateSessionComplete OnlineSessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate);
// 创建一个存放 Session 设置的对象指针 TSharedPtr<FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings()); SessionSettings->bIsLANMatch = false; // 设置为 外部玩家可见的非局域网匹配 SessionSettings->NumPublicConnections = 4; // 可以有 4 个玩家连接 SessionSettings->bAllowJoinInProgress = true; // 允许加入正在运行的游戏 SessionSettings->bAllowJoinViaPresence = true; // 允许区域玩家加入 SessionSettings->bShouldAdvertise = true; // 在在线服务上公开发布 SessionSettings->bUsesPresence = true; // 显示用户状态信息 SessionSettings->bUseLobbiesIfAvailable = true; // 支持 Lobbies Api,不开启可能无法找到 Session // 获取玩家控制器,用于获取玩家信息 const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); // 创建 Session,传入网络ID 、Sesssion名、Session设置 OnlineSessionInterface -> CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings);}接着在蓝图中绑定触发事件:

此时打包后运行游戏,按 1 触发绑定的事件。可以看到成功创建了 Session ,效果如下图所示:

新增寻找 Session 的代码,步骤与创建类似。首先在 MenuSystemCharacter.h 中添加下面的内容:
protected: // 加入游戏 Session 的逻辑 UFUNCTION(BlueprintCallable) void JoinGameSession();
// 找到 Session 后执行的函数 void OnFindSessionsComplete(bool bWasSuccessful);
private: // 存放找到 OnFindSessionsComplete 的委托 FOnFindSessionsCompleteDelegate FindSessionCompleteDelege; // 存放搜索对象 TSharedPtr<FOnlineSessionSearch> SessionSearch;};接着是_MenuSystemCharacter.cpp_ :
// 构造函数中给 FindSessionCompleteDelege 赋值为本类中的 OnFindSessionsComplete 函数AMenuSystemCharacter::AMenuSystemCharacter(): CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)), FindSessionCompleteDelege(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)){ // ......}
// 加入游戏的逻辑void AMenuSystemCharacter::JoinGameSession() { // Find game session if (!OnlineSessionInterface.IsValid()) { return; }
// 将委托添加到 FindSessionsCompleteDelegate_Handle 委托列表 // 找到 Session 后执行 FindSessionCompleteDelege 绑定的函数 OnFindSessionsComplete OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionCompleteDelege);
SessionSearch = MakeShareable(new FOnlineSessionSearch()); // 创建 SessionSearch 对象 SessionSearch->MaxSearchResults = 10000; // 最大搜索结果为 10000 条 SessionSearch->bIsLanQuery = false; // 关闭局域网查询 SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); // 只查询 presence 值为 true 的
// 获取玩家控制器,以供后面获取网络 ID const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); // 通过 网络ID、SessionSearch 搜索参数 来查找 Session OnlineSessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SessionSearch.ToSharedRef());}
// 找到 Session 后执行的函数void AMenuSystemCharacter::OnFindSessionsComplete(bool bWasSuccessful) { // 打印搜索结果数量 GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Found SearchResults %d"), SessionSearch->SearchResults.Num()) );
// 遍历结果,取出每条结果的 Id、User,打印它们 for (auto Result : SessionSearch->SearchResults) { FString Id = Result.GetSessionIdStr(); FString User = Result.Session.OwningUserName; if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Id: %s, User: %s"), *Id, *User) ); } }}接着,在蓝图中绑定事件:

最后将游戏打包,打开两个客户端,一个创建,另一个查找。最终查找到的效果如下:

首先是 MenuSystemCharacter.h :
UCLASS(config=Game)class AMenuSystemCharacter : public ACharacter{ // ......protected: void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);private: // ...... FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate;
}然后是 MenuSystemCharacter.cpp :
// 构造函数中给 JoinSessionCompleteDelegate赋值为本类中的 OnJoinSessionComplete函数AMenuSystemCharacter::AMenuSystemCharacter(): CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)), FindSessionCompleteDelege(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)), JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete)){ // ......}
// CreateGameSeesion 中添加 SessionSettings 设置void AMenuSystemCharacter::CreateGameSeesion() { // ......
// 设置 MatchType 为 "FreeForAll" SessionSettings->Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
// ......}
// OnCreateSessionComplete 中添加成功创建 Session 后切换关卡的逻辑void AMenuSystemCharacter::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful) { if (bWasSuccessful) { // ......
// 切换到名为 Lobby 的关卡 UWorld* World = GetWorld(); if (World) { World->ServerTravel(FString("/Game/ThirdPerson/Maps/Lobby?listen")); } } else { // ...... }}
//void AMenuSystemCharacter::OnFindSessionsComplete(bool bWasSuccessful) {
if (!OnlineSessionInterface.IsValid()) { return; }
for (auto Result : SessionSearch->SearchResults) {
FString Id = Result.GetSessionIdStr(); FString User = Result.Session.OwningUserName;
// 获取 MatchType FString MatchType; Result.Session.SessionSettings.Get(FName("MatchType"), MatchType);
if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Id: %s, User: %s"), *Id, *User) ); }
// 如果 MatchType 是前面 CreateGameSeesion 中设置的 "FreeForAll",则打印并添加委托,同时加入 Session if (MatchType == FString("FreeForAll")) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Joining Match Type %s"), *MatchType) ); }
OnlineSessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, Result); } }}最后打包,将 .exe 在两台机器上运行,查看效果。