(UE)接入Steam平台的主要步骤

本文基于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,顺利的话会有类似下图的输出:

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注