插件
UE中的插件就相当于一个模块,在引擎界面点击创建新的插件后,会在项目文件夹中生成插件的文件夹,在该文件夹内,只需要像游戏项目一样编写插件逻辑,最后在插件选择界面开启该插件即可
当新建插件后,UE会自动生成继承于IModuleInterface的类,说明该文件夹的内容为插件。
在InsideUE4中对于Subsystem的介绍可以了解到,Subsystem的生命周期分别与其父类相同。在对于实现网络连接的插件,由于其是需要在整个游戏运行时存在所以选择了UGameInstanceSubsystem。
Lan连接
同一局域网下的连接,只要该局域网下有一个主机充当了服务器,其余电脑就可以作为客户端连接。UE没有区分客户端与服务器,所以客户端和服务器的代码都写在一个文件中。如果是网络属性赋值和RPC,会对函数添加UFUNCTION(Server/Client)说明该函数是在服务器还是客户端运行。
对于Lan连接,一般只需要两个函数,一个对应服务器,一个对应客户端
服务器
作为服务器的主机,主要实现打开地图,开启监听,当有其他主机请求连接时,进行请求。
在UE中创建一个地图,利用AWorld里面提供的ServerTravel函数对地图添加?listen。然后可以绑定一个按键用于调用这个函数,当某个主机按下该按键,该主机执行这个函数,成为服务器并且切换地图
void AMPGameDemoCharacter::OpenLobby_Map()
{
UWorld* World = GetWorld();
if (World) {
World->ServerTravel("/Game/ThirdPersonCPP/Maps/Lobby_Map?listen");
}
}
客户端
客户端主要就是实现地图的切换,通过IP地址来获取服务器,进行地图切换。有两种方法实现,可以是在单机游戏中OpenLevel的方法
void AMPGameDemoCharacter::CallOpenLobby_Map(const FString& Addr)
{
UGameplayStatics::OpenLevel(this, *Addr);
}
使用按键控制调用,在调用时传入表示IP地址的字符串即可
第二种方法利用APlayerController中的ClientServer,传入IP地址和传送方式
void AMPGameDemoCharacter::CallClientTravel(const FString& Addr)
{
//需要获取客户端上的角色控制器
APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController();
if (PlayerController) {
PlayerController->ClientTravel(Addr, ETravelType::TRAVEL_Absolute);
}
}
利用Steam提供的接口连接
插件类UMultiplayerSessionSubsystem
基础成员变量
//Session接口
IOnlineSessionPtr SessionInterface;
TSharedPtr<FOnlineSessionSettings> SessionSettings;
TSharedPtr<FOnlineSessionSearch> SessionSearch;
SessionInterface为一个接口指针,其作用就是维护与steam平台的连接,使用该指针可以实现session的创建,加入离开等等功能。是整个插件的核心
SessionSettings设置了连接session的方式,该session的最大容量等等信息
SessionSearch设置了对session进行搜索时的参数,用于寻找session时的判断配置等,其中会存储寻找的结果
对项目的配置
按照官方教程进行设置即可
参考连接
创建OnlineSubsystem
OnlineSubsystem - Series of interfaces to support communicating with various web/platform layer services
OnlineSubsystem是UE准备好的一系列支持各种平台网络服务的类,可以直接使用该类来获取与steam服务的连接。
通过在构造函数中调用Get函数获得OnlineSubsystem的实例对象,当获得了实例化对象,就可以利用GetSessionInterface()来对SessionInterface进行赋值,从而开始准备维护一个session
//获得onlinesubsystem的变量
IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get();
if (Subsystem)
{
//将会话接口实例化
SessionInterface = Subsystem->GetSessionInterface();
}
功能
该插件是为了提供一个网络连接功能,即需要一个主机依靠steam创建一个session,然后其他主机通过对该session的搜索进行加入或者离开操作。
所以该插件主要实现以下几个功能,以供其他类进行调用
//通过传入参数方便对Session的设置,一个是能够连接的玩家,一个是等待客户端连接时需要匹配的键值对值
void CreateSession(int32 NumPublicConnections, FString MatchType);
//客户端寻找Session时调用,最大搜寻数
void FindSessions(int32 MaxSearchResults);
//客户端加入Session时调用传入正确的Session会话
void JoinSession(const FOnlineSessionSearchResult& SessionResult);
//销毁会话和开始会话
void DestroySession();
void StartSession();
同时还需要创建委托和回调函数来控制,以及DelegateHandles来控制Delegate当这些Delegate结束时。
OnlineSessionInterface委托
OnlineSessionInterface中提供了上述五种功能完成时的委托声明,为了使用需要定义对应的成员变量
FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate;
FDelegateHandle CreateSessionCompleteDelegateHandle;
FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate;
FDelegateHandle FindSessionsCompleteDelegateHandle;
FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate;
FDelegateHandle JoinSessionCompleteDelegateHandle;
FOnDestroySessionCompleteDelegate DestroySessionCompleteDelegate;
FDelegateHandle DestroySessionCompleteDelegateHandle;
FOnStartSessionCompleteDelegate StartSessionCompleteDelegate;
FDelegateHandle StartSessionCompleteDelegateHandle;
对于上面五个Delegate,还需要定义5个回调函数来进行控制,这5个函数只会在该类中调用,用于将Delegate List中的对应的Delegate清除掉
void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful);
void OnFindSessionsComplete(bool bWasSuccessful);
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful);
void OnStartSessionComplete(FName SessionName, bool bWasSuccessful);
然后就需要对Delegate进行绑定上述5个回调函数,可以在构造函数中实现,此时构造函数就变为了
UMultiplayerSessionSubsystem::UMultiplayerSessionSubsystem():
CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this,&UMultiplayerSessionSubsystem::OnCreateSessionComplete)),
FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this,&UMultiplayerSessionSubsystem::OnFindSessionsComplete)),
JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this,&UMultiplayerSessionSubsystem::OnJoinSessionComplete)),
DestroySessionCompleteDelegate(FOnDestroySessionCompleteDelegate::CreateUObject(this,&UMultiplayerSessionSubsystem::OnDestroySessionComplete)),
StartSessionCompleteDelegate(FOnStartSessionCompleteDelegate::CreateUObject(this,&UMultiplayerSessionSubsystem::OnStartSessionComplete))
{
//获得onlinesubsystem的变量
IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get();
if (Subsystem)
{
//将会话接口实例化
SessionInterface = Subsystem->GetSessionInterface();
}
}
上面对与UMultiplayerSessionSubsystem类的功能已经基本完成了,接下来就需要对5个功能函数进行编写了。
首先来看看在其他类如何实现对UMultiplayerSessionSubsystem类的功能调用,下面以一个主界面两个按钮为例,一个按钮用于创建session,一个按钮用于加入session。
Menu类调用UMultiplayerSessionSubsystem类的功能
首先要在Menu类中声明一个UMultiplayerSessionSubsystem对象,当按钮按下后,将通过对该对象中函数的调用实现功能。
声明后就需要对这个对象赋值,由于UMultiplayerSessionSubsystem类是继承的UGameInstance,所以当引擎一启动,UMultiplayerSessionSubsystem就会被创建,可以通过获取GameInstance实例对象,然后从GameInstance存储的列表中获得UMultiplayerSessionSubsystem对象,所以在Menu类的构造函数中就有
//实例化子系统变量
UGameInstance* GameInstance = GetGameInstance();
if (GameInstance)
{
MultiplayerSessionSubsystem = GameInstance->GetSubsystem<UMultiplayerSessionSubsystem>();
}
之后就是实现点击事件的响应,以创建session为例,当点击创建按钮后,将调用MultiplayerSessionSubsystem对象中的Create功能函数,在Create功能函数中实现Session的创建,然后如果创建成功后,将利用自定义委托(不是上面5个)进行广播,而该广播的回调函数是在Menu类中(为了分离)实现了地图的转换
MultiplayerSessionSubsystem类中的委托
基本上是十个委托,5个为OnlineSessionInterface提供,其可以使用DelegateHandle进行控制,一般用来表明5个功能是否成功的执行,对应的回调函数中,如果顺利执行即CompleteSuccessful那么就可以广播自定义的5个委托,这5个委托用于在Menu类中调用绑定回调函数,当得到Successful后可以执行进入地图或者离开地图等功能。
//自定义的5个委托
//绑定自己创建的动态多播用于响应菜单UI界面的响应回调
//委托名字,参数类型,参数形参
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiPlayerOnCreateSessionComplete, bool, bWasSuccessful);
//动态多播的返回类型必须是UClass或者UStruct,并且与蓝图兼容
//寻找委托,返回一个数组结果,FOnlineSessionSearchResult不是一个UClass,所以在蓝图中也无法调用
//如果想在蓝图中调用,那么可以自己创建一个继承的UClass,然后采用动态多播
DECLARE_MULTICAST_DELEGATE_TwoParams(FMultiPlayerOnFindSessionComplete, const TArray<FOnlineSessionSearchResult>& SessionResults, bool bWasSuccessful);
//加入会话的委托传入加入的结果
DECLARE_MULTICAST_DELEGATE_OneParam(FMultiPlayerOnJoinSessionComplete, EOnJoinSessionCompleteResult::Type Result);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiPlayerOnDestroySessionComplete, bool, bWasSuccessful);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMultiPlayerOnStartSessionComplete, bool, bWasSuccessful);
五个功能函数的实现
这些函数都将在Menu类响应事件后被调用
CreateSession
创建Session,即通过传入的参数新建一个Session不涉及主机在创建Session后的具体操作,具体操作应该与该类分离,在Menu类实现比如地图的切换等等。
参数: 该Session最大容许加入的主机数,该会话的表示(用于其他主机搜寻,字符串即可)
void CreateSession(int32 NumPublicConnections, FString MatchType);
- 检测SessionInterface是否有效,如果无效说明没有获取到Steam服务,则无法继续CreateSession
- 检测之前是否存在Session,如果存在则先断开再重新创建
利用SessionInterface中的GetNamedSession,来判断是否已经有Session存在,UE会将已经存在的Session存放在系统中,由宏定义Name_GameSession保存REGISTER_NAME(287,GameSession)
,如果可以通过Name_GameSession获得一个Session说明已经存在一个Session需要先断开再创建。
可以看到在搜索之前先上锁,然后在Session列表中以O(n)的复杂度搜索匹配项,如果没有返回NULL
auto ExistingSession = SessionInterface->GetNamedSession(NAME_GameSession);
if(ExistingSession != nullptr)
{
bCreateSessionOnDestroy = true;
LastNumPublicConnections = NumPublicConnections;
LastMatchType = MatchType;
//使用自己创建的销毁会话函数
DestroySession();
}
- 绑定委托以及添加DelegateHandle
利用SessionInterface中的AddOnCreateSessionCompleteDelegate_Handle函数将OnCreateSessionCompleteDelegate添加到SessionInterface的Delegate列表中,该函数会返回一个DelegateHandle,可以利用该Handle来控制该Delegate
CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate);
- 配置Session参数
//创建的会话设置
SessionSettings = MakeShareable(new FOnlineSessionSettings());
//设置是否为LAN连接
SessionSettings->bIsLANMatch = IOnlineSubsystem::Get()->GetSubsystemName() == "NULL" ? true : false;
SessionSettings->NumPublicConnections = NumPublicConnections;
SessionSettings->bAllowJoinInProgress = true;
SessionSettings->bAllowJoinViaPresence = true;
SessionSettings->bShouldAdvertise = true;
SessionSettings->bUsesPresence = true;
//设置一些键值对,使得我们可以区分其他的session连接
SessionSettings->Set(FName("MatchType"), MatchType, EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
SessionSettings->BuildUniqueId = 1;
SessionSettings->bUseLobbiesIfAvailable = true;
- CreateSession
利用SessionInterface中的CreateSession函数
可以看到Session在这个函数被调用之后仍然不是完全创建的,只有在OnCreateSessionCompleteDelegate被广播后才能说完成创建,以Steam平台为例,
bool FOnlineSessionSteam::CreateSession(int32 HostingPlayerNum, FName SessionName, const FOnlineSessionSettings& NewSessionSettings)
需要传入主机即创建该Session的Player,以及Name_GameSession,和配置的Session信息
该函数会首先定义一个局部变量,来判断是否创建成功uint32 Result = ONLINE_FAIL;
,通过之前配置的参数进行Session的创建,然后调用OnCreateSessionCompleteDelegate进行广播完成创建
在子编写函数中可以利用 GetWorld()->GetFirstLocalPlayerFromController();
获取当前Player,然后判断上述函数的返回值即可
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
//当会话创建失败,删除这个Handle
if (!SessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings))
{
SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle);
//广播自定义委托,传入的值会被回调函数接受
MultiPlayerOnCreateSessionComplete.Broadcast(false);
}
MultiPlayerOnCreateSessionComplete委托将广播false导致Menu类中的回调函数不会成功执行。
6. 清理Delegate
当CreateSession执行成功后,需要从Session的Delegate列表中清除CreateSessionCompleteDelegate,上面是可以在没有创建成功后清除,为了能在成功后清除,就需要之前创建的对应的回调函数
CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this,&UMultiplayerSessionSubsystem::OnCreateSessionComplete))
在回调函数OnCreateSessionComplete中利用ClearOnCreateSessionCompleteDelegate_Handle清除Delegate并且广播MultiPlayerOnCreateSessionComplete.Broadcast(bWasSuccessful)
FindSession
另一个主要的功能就是加入Session功能,要加入首先就要搜索到对应的Session,用于其他主机在点击按钮后搜索Session。
参数: 搜索的最大范围,用于确定回去搜索多少个Session,如果过少可能造成无法寻找到的情况
当FindSession成功后,利用MultiPlayerOnFindSessionComplete委托进行广播到Menu类中的回调函数,由于会传入一个带有结果的数据,所以在回调函数中对数组遍历,判断Session的标识是不是设置的标识,如果是调用JoinSession()
函数实现加入
还是以Steam平台为例,在调用FindSession后,寻找到的结果会存储在SessionResults内,然后根据是否是局域网联机,进行不同的寻找
会通过寻找当前网络中存在的SteamSession任务,然后加入到列表中。
当得到结果数组后,在Menu类中的回调函数中,通过对数组的遍历,进行JoinSession操作
for (auto Result : SessionResults)
{
FString SettingsValue;
Result.Session.SessionSettings.Get(FName("MatchType"), SettingsValue);
if (SettingsValue == MatchType)
{
if (MultiplayerSessionSubsystem)
{
MultiplayerSessionSubsystem->JoinSession(Result);
return;
}
}
}
JoinSession
void UMultiplayerSessionSubsystem::JoinSession(const FOnlineSessionSearchResult& SessionResult)
{
if (!SessionInterface.IsValid())
{
MultiPlayerOnJoinSessionComplete.Broadcast(EOnJoinSessionCompleteResult::UnknownError);
return;
}
//调用会话接口加入会话,绑定委托,添加委托列表
JoinSessionCompleteDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate);
const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController();
//给JoinSession添加客户端信息,session信息,session的名字和服务器信息
if (!SessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, SessionResult))
{
SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle);
MultiPlayerOnJoinSessionComplete.Broadcast(EOnJoinSessionCompleteResult::UnknownError);
}
}
在JoinSession函数中,会通过判断SessionSettings里面的bUserPresence(主机是否显示用户信息)来选择Join方式和SessionInfo
如果bUserPresence为false说明是Client,会在加入时进行信息验证。
当成功的JoinSession后,MultiPlayerOnJoinSessionComplete委托调用Menu类中的回调函数,回调函数主要实现的功能就是客户端上地图的转换等等功能实现,而通过在LAN模式下地图的切换需要知道地图的地址,那么在非LAN模式下,地图的地址存放在SessionInterface中,为了使得插件MultiplayerSessionSubsystem类和Menu类的分离,所以需要在回调函数中定义一个局部变量获取SessionInterface。
void UMenuWidget::OnJoinSession(EOnJoinSessionCompleteResult::Type Result)
{
//为了使得菜单与插件相互独立,所以我们需要单独获取SessionInterface
//获得onlinesubsystem的变量
IOnlineSubsystem* Subsystem = IOnlineSubsystem::Get();
if (Subsystem)
{
//将会话接口实例化
IOnlineSessionPtr SessionInterface = Subsystem->GetSessionInterface();
if (SessionInterface.IsValid())
{
FString Addr;
SessionInterface->GetResolvedConnectString(NAME_GameSession, Addr);
//客户端利用ClientTrave进行地图转换
APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController();
if (PlayerController)
{
PlayerController->ClientTravel(Addr, ETravelType::TRAVEL_Absolute);
}
}
}
DestroySession
UE提供的DestroySession函数中,首先通过传入的SessionName寻找到需要Destroy的Session,然后会先停止当前正在进行的所有进程
其次通过判断bUsesPresence,来确定是主机还是客户端想要DestroySession
如果是主机,会直接将该Session状态修改为Destroy,然后关闭所有连接
如果是客户端,会先清空与Session的会话,然后从Server LogOff,最后Destroy
主要区别就是LogOff的类不一样
FOnlineAsyncTaskSteamLeaveLobby: Async task for leaving a single lobby
FOnlineAsyncTaskSteamLogoffServer:Async task for shutting down an advertised game erver