本文基于Unreal Engine 5.3.1,参考Udemy课程《Unreal Engine 5 C++ Multiplayer Shooter》,记录游戏开发接入Steam平台主要步骤。
UE提供了在线子系统(OnlineSubsystem)方便我们在开发游戏时,以同样的网络代码接入常见的游戏平台,如Steam、Epic、XBox Live等。其中,接入Steam平台对应的是OnlineSubsystemSteam,UE官网提供了相关教程:https://docs.unrealengine.com/5.3/zh-CN/online-subsystem-steam-interface-in-unreal-engine/
本文的实例代码:https://github.com/tilongzs/UE_NetworkDemo
1 基本的网络游戏步骤
例如创建一个基于C++的第三人称游戏项目(UE_NetworkDemo),默认关卡是ThirdPersonMap,我们再创建一个新空白Basic关卡,例如命名为Lobby,在服务端创建游戏或客户端加入游戏成功后跳转至这个关卡。
一般的,服务端使用ServerTravel()
跳转至一个关卡并等待客户端加入,如:
UWorld* world = GetWorld();
if (world)
{
world->ServerTravel("/Game/Maps/Lobby?listen");
Log("OpenLobby sucess");
}
else
{
Log("OpenLobby failed");
}
客户端使用ClientTravel()
连接服务端,如:
APlayerController* playerController = GetGameInstance()->GetFirstLocalPlayerController();
if (playerController)
{
FString address("127.0.0.1");
playerController->ClientTravel(address, ETravelType::TRAVEL_Absolute);
}
else
{
Log("CallClientTravel failed");
}
OnlineSubsystem并不是对它们进行封装,而只是提供了额外的游戏会话、用户管理等功能。
2 项目配置和启用插件
UE内置有Online Subsystem Steam插件,需要先启用它。
编辑文件Source/UE_NetworkDemo/UE_NetworkDemo.Build.cs,在PublicDependencyModuleNames中增加"OnlineSubsystemSteam", "OnlineSubsystem"
。
如果仅开发局域网游戏,则只需要增加
"OnlineSubsystem"
即可。下一步骤的Config/DefaultEngine.ini也不需要,
编辑文件Config/DefaultEngine.ini,增加以下代码:
[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
[OnlineSubsystem]
DefaultPlatformService=Steam
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480
bInitServerOnClient=true
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"
注:上面代码中的SteamDevAppId为开发者共享的测试应用ID,将来发布正式游戏需要在Steam上获取正式ID。
我们需要OnlineSubsystem提供这几个游戏会话流程接口:创建游戏会话CreateSession()、查找游戏会话FindSessions()、加入游戏会话JoinSession()、开始游戏会话StartSession()、销毁游戏会话DestroySession()。
接下先来测试一下相关模块是否正常启用,例如创建一个游戏实例子系统类MyGameInstanceSubsystem,然后创建成员变量:
IOnlineSessionPtr _onlineSession;
创建函数InitOnlineSession()
,增加一下代码,然后在关卡蓝图的按键1事件中调用:
void UMyGameInstanceSubsystem::InitOnlineSession()
{
if (_onlineSession)
{
// 在线子系统已经初始化
return;
}
IOnlineSubsystem* onlineSubsystem = IOnlineSubsystem::Get();
if (onlineSubsystem)
{
_onlineSession = onlineSubsystem->GetSessionInterface();
if (GEngine)
{
Log(FString::Printf(TEXT("当前网络子系统为:%s"), *onlineSubsystem->GetSubsystemName().ToString()));
}
}
else
{
Log(TEXT("获取在线子系统失败"));
}
}
启动Steam软件并登录。然后以”选中的视口“运行,会输出”当前网络子系统为:NULL“;以”独立进程游戏“运行,会输出”当前网络子系统为:STEAM“。
如果在以”独立进程游戏“运行能弹出Steam窗口并输出STEAM,说明配置一切正常,准备工作完成。
3 游戏会话
3.1 创建游戏会话
创建当游戏会话创建完成的回调函数OnCreateSessionComplete()
,增加以下代码:
void UMyGameInstanceSubsystem::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
if (bWasSuccessful)
{
Log(FString::Printf(TEXT("创建游戏会话%s成功"), *SessionName.ToString()));
}
else
{
LogWarning(TEXT("创建游戏会话失败"));
}
}
在头文件中创建相应的委托变量:
FOnCreateSessionCompleteDelegate _dlgOnCreateSessionComplete = FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete);
创建函数CreateGameSession()
,增加以下代码,然后在角色蓝图中的按键2事件中调用:
void UMyGameInstanceSubsystem::CreateGameSession()
{
if (!_onlineSession.IsValid())
{
return;
}
Log(TEXT("开始创建游戏会话"));
// 检查游戏会话是否已存在
auto* namedSession = _onlineSession->GetNamedSession(NAME_GameSession);
if (namedSession != nullptr)
{
// 销毁游戏会话。但不能销毁的过程需要时间,因此不能立即创建游戏会话。
_onlineSession->DestroySession(NAME_GameSession);
return;
}
// 创建游戏会话
_onlineSession->AddOnCreateSessionCompleteDelegate_Handle(_dlgOnCreateSessionComplete);
TSharedPtr<FOnlineSessionSettings> sessionSetting = MakeShared<FOnlineSessionSettings>();
sessionSetting->bIsLANMatch = true; // 使用局域网,方便本地测试
sessionSetting->NumPublicConnections = 4;
sessionSetting->bAllowJoinInProgress = true;
sessionSetting->bAllowJoinViaPresence = true;
sessionSetting->bShouldAdvertise = true;
sessionSetting->bUsesPresence = true;
sessionSetting->bUseLobbiesIfAvailable = true;
sessionSetting->Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); // 自定义参数
sessionSetting->BuildUniqueId = rand(); // 生成唯一会话ID,以保证其他用户能搜索到
const ULocalPlayer* localPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if (!_onlineSession->CreateSession(*localPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *sessionSetting))
{
LogError(TEXT("执行创建游戏会话失败"));
return;
}
}
以”独立进程游戏“运行,先后按下按键1、2,顺利的话会有类似下图的输出:
3.2 查找游戏会话
在头文件中创建相应的委托变量_dlgFindSessionsComplete
,以及保存搜索结果的变量_onlineSessionSearch
:
FOnFindSessionsCompleteDelegate _dlgOnFindSessionsComplete = FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete);
TSharedPtr<FOnlineSessionSearch> _onlineSessionSearch;
创建当查找游戏会话完成的回调函数OnFindSessionsComplete()
,增加以下代码:
void UMyGameInstanceSubsystem::OnFindSessionsComplete(bool bWasSuccessful)
{
if (!_onlineSession.IsValid())
{
return;
}
if (bWasSuccessful)
{
Log(FString::Printf(TEXT("查找到%d个游戏会话"), _onlineSessionSearch->SearchResults.Num()));
for (auto& result : _onlineSessionSearch->SearchResults)
{
FString sessionID = result.GetSessionIdStr();
Log(FString::Printf(TEXT("查找到游戏会话 sessionID:%s 创建者userName:%s"), *sessionID, *result.Session.OwningUserName));
// 可检查MatchType是否一致
FString matchType;
result.Session.SessionSettings.Get("MatchType", matchType);
if (matchType == "FreeForAll")
{
Log(FString::Printf(TEXT("--游戏类型:%s"), *matchType));
}
}
}
else
{
Log(TEXT("查找游戏会话失败"));
}
}
创建函数FindGameSessions()
,增加以下代码,然后在角色蓝图中的按键3事件中调用:
void UMyGameInstanceSubsystem::FindGameSessions()
{
if (!_onlineSession.IsValid())
{
return;
}
Log(TEXT("开始查找游戏会话"));
// 查找游戏会话
_onlineSession->AddOnFindSessionsCompleteDelegate_Handle(_dlgOnFindSessionsComplete);
_onlineSessionSearch = MakeShared<FOnlineSessionSearch>();
_onlineSessionSearch->MaxSearchResults = 100;
_onlineSessionSearch->bIsLanQuery = true; // 使用局域网,方便本地测试
_onlineSessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
const ULocalPlayer* localPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if (!_onlineSession->FindSessions(*localPlayer->GetPreferredUniqueNetId(), _onlineSessionSearch.ToSharedRef()))
{
LogError(TEXT("执行查找游戏会话失败"));
}
}
打包项目,拷贝到另一台运行Steam的电脑上,运行并按下按键1、2以创建游戏会话。本机以”独立进程游戏“运行,先后按下按键1、3,顺利的话会有类似下图的输出:
可以看到查找到1个游戏会话,并且输出了Steam用户名。
3.3 加入游戏会话
服务端:修改游戏会话创建完成的回调函数OnCreateSessionComplete()
,增加创建游戏会话后立即跳转至游戏大厅地图的代码,如下:
void UMyGameInstanceSubsystem::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
if (bWasSuccessful)
{
Log(FString::Printf(TEXT("创建游戏会话%s成功"), *SessionName.ToString()));
// 跳转至游戏大厅地图
UWorld* world = GetWorld();
if (world)
{
if (!world->ServerTravel("/Game/Maps/Lobby?listen"))
{
LogError(TEXT("跳转至游戏大厅地图失败"));
}
}
else
{
LogError(TEXT("跳转至游戏大厅地图 获取world失败"));
}
}
else
{
LogWarning(TEXT("创建游戏会话失败"));
}
}
客户端:修改查找游戏会话完成的函数OnFindSessionsComplete()
,增加当查找到需要的游戏会话时就加入游戏会话的代码,如下:
void UMyGameInstanceSubsystem::OnFindSessionsComplete(bool bWasSuccessful)
{
if (!_onlineSession.IsValid())
{
return;
}
if (bWasSuccessful)
{
Log(FString::Printf(TEXT("查找到%d个游戏会话"), _onlineSessionSearch->SearchResults.Num()));
for (auto& result : _onlineSessionSearch->SearchResults)
{
FString sessionID = result.GetSessionIdStr();
Log(FString::Printf(TEXT("查找到游戏会话 sessionID:%s 创建者userName:%s"), *sessionID, *result.Session.OwningUserName));
// 可检查MatchType是否一致
FString matchType;
result.Session.SessionSettings.Get("MatchType", matchType);
if (matchType == "FreeForAll")
{
Log(FString::Printf(TEXT("--游戏类型:%s"), *matchType));
// 加入游戏会话
_onlineSession->AddOnJoinSessionCompleteDelegate_Handle(_dlgOnJoinSessionComplete);
const ULocalPlayer* localPlayer = GetWorld()->GetFirstLocalPlayerFromController();
if (!_onlineSession->JoinSession(*localPlayer->GetPreferredUniqueNetId(), NAME_GameSession, result))
{
LogError(TEXT("执行加入游戏会话失败"));
}
break;
}
}
}
else
{
Log(TEXT("查找游戏会话失败"));
}
}
接着在头文件中创建加入游戏会话完成的委托变量:
FOnJoinSessionCompleteDelegate _dlgOnJoinSessionComplete = FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete);
创建当加入游戏会话完成的回调函数OnJoinSessionComplete()
,增加以下代码:
void UMyGameInstanceSubsystem::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type result)
{
if (!_onlineSession.IsValid())
{
return;
}
if (result == EOnJoinSessionCompleteResult::Success)
{
Log(TEXT("加入游戏会话成功"));
// 获取该会话的连接信息
FString connectInfo;
if (_onlineSession->GetResolvedConnectString(NAME_GameSession, connectInfo))
{
// connectInfo是包含服务端IP与端口的字符串,例如:192.168.1.10:7777
Log(FString::Printf(TEXT("游戏会话连接信息: %s"), *connectInfo));
// 连接服务端
APlayerController* playerController = GetFirstLocalPlayerController();
if (playerController)
{
playerController->ClientTravel(connectInfo, ETravelType::TRAVEL_Absolute);
}
else
{
LogWarning(TEXT("跳转至大厅地图失败"));
}
}
else
{
LogWarning(TEXT("获取游戏会话的连接信息失败"));
}
}
else if (result == EOnJoinSessionCompleteResult::AlreadyInSession)
{
LogWarning(TEXT("已经在游戏会话中"));
}
else
{
LogWarning(TEXT("加入游戏会话失败"));
}
}
最后打包项目,拷贝到另一台运行Steam的电脑上,运行并按下按键1、2以创建游戏会话。本机以”独立进程游戏“运行,先后按下按键1、3,顺利的话会有类似下图的输出: