Loading

UE4 -- 实现用于网络连接的插件

插件

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结束时。
image

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类中(为了分离)实现了地图的转换
image

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);

image

五个功能函数的实现

这些函数都将在Menu类响应事件后被调用

CreateSession

创建Session,即通过传入的参数新建一个Session不涉及主机在创建Session后的具体操作,具体操作应该与该类分离,在Menu类实现比如地图的切换等等。
参数: 该Session最大容许加入的主机数,该会话的表示(用于其他主机搜寻,字符串即可)

void CreateSession(int32 NumPublicConnections, FString MatchType);
  1. 检测SessionInterface是否有效,如果无效说明没有获取到Steam服务,则无法继续CreateSession
  2. 检测之前是否存在Session,如果存在则先断开再重新创建
    利用SessionInterface中的GetNamedSession,来判断是否已经有Session存在,UE会将已经存在的Session存放在系统中,由宏定义Name_GameSession保存REGISTER_NAME(287,GameSession),如果可以通过Name_GameSession获得一个Session说明已经存在一个Session需要先断开再创建。
    image
    可以看到在搜索之前先上锁,然后在Session列表中以O(n)的复杂度搜索匹配项,如果没有返回NULL
	auto ExistingSession = SessionInterface->GetNamedSession(NAME_GameSession);
	if(ExistingSession != nullptr)
	{
		bCreateSessionOnDestroy = true;
		LastNumPublicConnections = NumPublicConnections;
		LastMatchType = MatchType;
		//使用自己创建的销毁会话函数
		DestroySession();
	}
  1. 绑定委托以及添加DelegateHandle
    利用SessionInterface中的AddOnCreateSessionCompleteDelegate_Handle函数将OnCreateSessionCompleteDelegate添加到SessionInterface的Delegate列表中,该函数会返回一个DelegateHandle,可以利用该Handle来控制该Delegate
CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate);
  1. 配置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;
  1. CreateSession
    利用SessionInterface中的CreateSession函数
    image
    可以看到Session在这个函数被调用之后仍然不是完全创建的,只有在OnCreateSessionCompleteDelegate被广播后才能说完成创建,以Steam平台为例,
bool FOnlineSessionSteam::CreateSession(int32 HostingPlayerNum, FName SessionName, const FOnlineSessionSettings& NewSessionSettings)

需要传入主机即创建该Session的Player,以及Name_GameSession,和配置的Session信息
该函数会首先定义一个局部变量,来判断是否创建成功uint32 Result = ONLINE_FAIL;,通过之前配置的参数进行Session的创建,然后调用OnCreateSessionCompleteDelegate进行广播完成创建
image
在子编写函数中可以利用 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内,然后根据是否是局域网联机,进行不同的寻找
image
会通过寻找当前网络中存在的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
image
如果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,然后会先停止当前正在进行的所有进程
image
其次通过判断bUsesPresence,来确定是主机还是客户端想要DestroySession
如果是主机,会直接将该Session状态修改为Destroy,然后关闭所有连接
image
如果是客户端,会先清空与Session的会话,然后从Server LogOff,最后Destroy
image
主要区别就是LogOff的类不一样
FOnlineAsyncTaskSteamLeaveLobby: Async task for leaving a single lobby
FOnlineAsyncTaskSteamLogoffServer:Async task for shutting down an advertised game erver

posted @ 2024-05-05 15:12  XTG111  阅读(5)  评论(0编辑  收藏  举报