ShooterGame是虚幻引擎开源的多人在线第一人称射击游戏项目,它的源码可以从Epic公司的商店里免费下载。里面包含了目前多人在线第一人称射击游戏常见功能,原本官方也有一个页面专门做了讲解,但是实在太过简单,对学习没有太多的帮助,所以我想花点时间在源代码层面自己分析一下,并把分析的过程记录在这里。今天分析一下代码中Session的创建和加入逻辑。
Session一般翻译成会话,这个概念对于做过服务器开发的人来说不会陌生。你可以简单认为Session是一个存在于服务器端的对象,它用来记录玩家的身份、连接状态等信息,比如你平时常遇到网站登录,当你登录成功后网页服务器就会创建一个Session来记录你的登录状态,身份等信息。ShooterGame作为一个多人在线的游戏,所以理所当然也会使用到Session这个概念。
ShooterGame进程启动后,当前的游戏实例可以有两个选择:一是当服务器主机,等待其他客户端加入;二是作为客户端直接加入一个已经运行的主机。所谓当服务器主机,从引擎的角度来说就是创建一个Session等待其他的客户端加入。我们先看主菜单的Host菜单项的响应方法:
void FShooterMainMenu::HostGame(const FString& GameType)
{
if (ensure(GameInstance.IsValid()) && GetPlayerOwner() != NULL)
{
FString const StartURL = FString::Printf(TEXT("/Game/Maps/%s?game=%s%s%s?%s=%d%s"), *GetMapName(), *GameType, GameInstance->GetOnlineMode() != EOnlineMode::Offline ? TEXT("?listen") : TEXT(""), GameInstance->GetOnlineMode() == EOnlineMode::LAN ? TEXT("?bIsLanMatch") : TEXT(""), *AShooterGameMode::GetBotsCountOptionName(), BotsCountOpt, bIsRecordingDemo ? TEXT("?DemoRec") : TEXT("") );
// Game instance will handle success, failure and dialogs
GameInstance->HostGame(GetPlayerOwner(), GameType, StartURL);
}
}
这个方法实现很简单,即根据玩家在界面上选择的关卡、游戏类型、Online Mode以及机器人数量等配置生产一个URL,然后调用UShooterGameInstance的HostGame方法。
// starts playing a game as the host
bool UShooterGameInstance::HostGame(ULocalPlayer* LocalPlayer, const FString& GameType, const FString& InTravelURL)
{
// 非Online game 略
AShooterGameSession* const GameSession = GetGameSession();
if (GameSession)
{
// add callback delegate for completion
OnCreatePresenceSessionCompleteDelegateHandle = GameSession->OnCreatePresenceSessionComplete().AddUObject(this, &UShooterGameInstance::OnCreatePresenceSessionComplete);
TravelURL = InTravelURL;
bool const bIsLanMatch = InTravelURL.Contains(TEXT("?bIsLanMatch"));
//determine the map name from the travelURL
const FString& MapNameSubStr = "/Game/Maps/";
const FString& ChoppedMapName = TravelURL.RightChop(MapNameSubStr.Len());
const FString& MapName = ChoppedMapName.LeftChop(ChoppedMapName.Len() - ChoppedMapName.Find("?game"));
if (GameSession->HostSession(LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, GameType, MapName, bIsLanMatch, true, AShooterGameSession::DEFAULT_NUM_PLAYERS))
{
if ( (PendingState == CurrentState) || (PendingState == ShooterGameInstanceState::None) )
{
ShowLoadingScreen();
GotoState(ShooterGameInstanceState::Playing);
return true;
}
}
}
return false;
}
在UShooterGameInstance的HostGame方法中,首先通过从当前GameMode实例里获得AGameSession实例。然后往AGameSession实例上注册创建Session完成事件的监听代理。最后就是根据玩家选择的游戏类型、关卡等设置调用AGameSession的HostSession来创建Session。Session创建完之后,接下来就是等待客户端加入Session。
要作为一个客户端加入一个已经运行的游戏主机上,玩家只需要在主菜单上点击JOINT按钮,然后在弹出的服务器列表界面选择一个服务器即可。在服务器界面选择服务器会调用到以下的方法:
void SShooterServerList::ConnectToServer()
{
if (SelectedItem.IsValid())
{
int ServerToJoin = SelectedItem->SearchResultsIndex;
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->RemoveAllViewportWidgets();
}
UShooterGameInstance* const GI = Cast<UShooterGameInstance>(PlayerOwner->GetGameInstance());
if (GI)
{
GI->JoinSession(PlayerOwner.Get(), ServerToJoin);
}
}
}
SShooterServerList::ConnectToServer方法会尝试去获得UShooterGameInstance实例,然后以玩家选择的服务器索引为参数调用UShooterGameInstance的JoinSession方法:
bool UShooterGameInstance::JoinSession(ULocalPlayer* LocalPlayer, int32 SessionIndexInSearchResults)
{
AShooterGameSession* const GameSession = GetGameSession();
if (GameSession)
{
AddNetworkFailureHandlers();
OnJoinSessionCompleteDelegateHandle = GameSession->OnJoinSessionComplete().AddUObject(this, &UShooterGameInstance::OnJoinSessionComplete);
if (GameSession->JoinSession(LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, SessionIndexInSearchResults))
{
if ( (PendingState == CurrentState) || (PendingState == ShooterGameInstanceState::None) )
{
ShowLoadingScreen();
GotoState(ShooterGameInstanceState::Playing);
return true;
}
}
}
return false;
}
JoinSession顾名思义就是加入会话,它的实现主要依赖AGameSession类的JoinSession方法,在调用完该方法之后剩下的就是等待AGameSession的回调来获得加入会话的执行结果。
从上面的源代码我们看到,session的创建和加入逻辑实现其实都是被引擎封装好了,作为使用者并不需要接触底层的网络代码,不需要自己去维护像网络连接创建和维护等这些很底层的代码,实际上引擎做的不仅仅是这样,还包括登录验证等安全相关的逻辑,有时间的话可以继续深入研读这部分代码。