UE4的联网系统研究

1. 物体复制

  具体细节可参考官网内容:http://api.unrealengine.com/CHN/Gameplay/Networking/index.htmlhtml

  这里只挑部分点来展开。c++

  首先,分为服务端和客户端。api

  而后,先看在c++中的两个参数:bNetLoadOnClient和SetReplicates(true), 对应蓝图的参数以下图所示:服务器

  

  Replicate的意思为复制。网络

  假设如今要生成一个物体(若是执行者是服务端时),若是这个物体的Replicate为true,则服务端和客户端都会生成;若是这个物体的Replicate为false,则只会在服务端生成,客户端不会生成。编辑器

  但若是执行者是客户端时,则不管物体的Replicate是否为true,服务端都不会生成这个物体。ide

  PS:函数

  1. 判断执行者是否为服务端可经过 if (GetWorld()->IsServer())来进行。学习

  2. 判断执行者是否为服务端经过HasAuthority()有时候是不许确的,例如当一个物体是由客户端生成的,此物体的HasAuthority()就会返回true。ui

  Net Load on Client意思大概是在加载地图时,这个物体是否在客户端中加载出来。

  若是地图上放置了一个物体,且这个物体的Net Load on Client为false,则客户端不会加载这个物体;反之则会。

 

2. 变量复制

  所谓变量复制就是,服务端的变量进行修改时,客户端的变量也跟着修改。

  实现很简单,只需在变量上加UPROPERTY(Replicated);或者在蓝图中,勾选Replicated,以下图所示:

  

  若是想实现,服务端修改某个变量后,自动触发某个事件,则须要在此变量上添加特别的东西,如:

//注意,OnRep_XXX中的XXX是要监测的变量。
UPROPERTY(ReplicatedUsing = OnRep_Deactivate) bool Deactivate; //一旦变量Deactivate在服务端中进行修改,则会触发这个函数
UFUNCTION() void OnRep_Deactivate(); //在此例子中,变量Deactivate一旦在服务端被修改,客户端的OnRep_Deactivate()就会被调用,但服务端的这个函数不会被调用,须要特地去手动调用一下,如:
void AFireEffectActor::UpdateTimer() { //更新数字
    if (CountDownTimer > 0) CountDownTimer -= 1; else { //修改变量,且通知修改事件
        Deactivate = !Deactivate; //修改事件函数只会在客户端运行,而服务端的则须要特地调用一下(如这里)
 OnRep_Deactivate(); } }

3. 服务端与客户端的信息交流

  学习资料:http://api.unrealengine.com/CHN/Gameplay/Networking/Actors/RPCs/index.html

  信息交流有3种方法:

  a. NetMulticast: 服务端广播,全部客户端能收到;客户端广播,只有该客户端能收到。

  b. Client:服务端发出通知,拥有这我的物的客户端都会调用此方法;客户端调用,则只有该客户端能调用。

  c. Server:客户端传递信息给服务端的方法;若是服务端调用则服务端能收到。

  如下将逐一讨论:

a. NetMulticast

.h: UFUNCTION(NetMulticast, Reliable) void SpaceBarNetMulticast(); .cpp: void ARPCCourseCharacter::SpaceBarNetMulticast_Implementation() { //获取蓝图
    UClass* FireEffectClass = LoadClass<AActor> (NULL, TEXT("Blueprint'/Game/BP/UnReplicateFire.UnReplicateFire_C'")); //在玩家那生成物体
    GetWorld()->SpawnActor<AActor>(FireEffectClass, GetActorTransform()); }

 

  注意:

  1. Reliable是可靠的意思,意味着服务端发出的信息,客户端绝对能收到。

  2. 方法的实现要加后缀_Implementation。

 

b.Client

.h: //Client联网方法,服务端发出通知,拥有这我的物的客户端都会调用此方法。
 UFUNCTION(Client, Reliable) void KeyJClient(int32 InInt); .cpp:
void ARPCCourseCharacter::KeyJEvent() { if (GetWorld()->IsServer()) { //获取全部ARPCCourseCharacter
        TArray<AActor*> ActArray; UGameplayStatics::GetAllActorsOfClass( GetWorld(), ARPCCourseCharacter::StaticClass(), ActArray); //呼叫全部ARPCCourseCharacter(除了本身)
        for (int i = 0; i < ActArray.Num(); ++i) { if (ActArray[i] != this) { Cast<ARPCCourseCharacter>(ActArray[i])->KeyJClient(i); } } } }
void ARPCCourseCharacter::KeyJClient_Implementation(int32 InInt) { ANumPad* NumPad = GetWorld()->SpawnActor<ANumPad>(ANumPad::StaticClass(), GetActorTransform()); NumPad->AssignRenderText(FString::FromInt(InInt)); }

   注意,方法的实现要加后缀_Implementation。

 

c.Server

.h: //H键绑定
    void KeyHEvent(); //Server方法
 UFUNCTION(Server, Reliable, WithValidation) void KeyHServer(int32 InInt); //Serve方法逻辑
    void KeyHServer_Implementation(int32 InInt); //Serve方法数据验证(若是验证后的结果为true,KeyHServer能够正常运行;若是为false,则发出此信息的客户端被踢出房间,此客户端从新开了一局单机游戏)
    bool KeyHServer_Validate(int32 InInt); .cpp: void ARPCCourseCharacter::KeyHEvent() { //客户端执行
    if (!GetWorld()->IsServer()) KeyHServer(3); } void ARPCCourseCharacter::KeyHServer_Implementation(int32 InInt) { //生成数字
    ANumPad* NumPad = GetWorld()->SpawnActor<ANumPad>(ANumPad::StaticClass(), GetActorTransform()); NumPad->AssignRenderText(FString::FromInt(InInt)); } bool ARPCCourseCharacter::KeyHServer_Validate(int32 InInt) { if (InInt > 0) return true; return false; }

   注意:

  1. 方法的实现要加后缀_Implementation。

  2. 为预防玩家做弊,客户端传过来的信息要先进行验证,经过了才能被服务端接收。方法的验证要加后缀_Validate。

 

4.建立会话、登入与登出

  UE4的建立会话(Create Session),至关于建立房间。而后等客户端寻找房间并加入便可。如:

  

  

  GameMode只存在于服务端,不存在于客户端,所以登入与登出的行为在GameMode上作比较好。

.h: UCLASS(minimalapi) class ARPCCourseGameMode : public AGameModeBase { GENERATED_BODY() public: ARPCCourseGameMode(); //GameMode只存在于服务端! //用户登入
    virtual void PostLogin(APlayerController* NewPlayer) override; //用户登出
    virtual void Logout(AController* Exiting) override; protected: //计算有多少我的加入了游戏
 int32 PlayerCount; }; .cpp: ARPCCourseGameMode::ARPCCourseGameMode() { PlayerControllerClass = ARPCController::StaticClass(); //若是不给WorldSetting指定GameMode,游戏运行时会自动把建立项目时生成的项目名GameMode这个类给设置上去 //若是建立的GameMode不指定PawnClass的话,会自动设定为ADefaultPawn类,因此这里必须设置为NULL
    DefaultPawnClass = NULL; PlayerCount = 0; } void ARPCCourseGameMode::PostLogin(APlayerController* NewPlayer) { Super::PostLogin(NewPlayer); //若是这个控制器自带了一个Pawn,则摧毁它
    if (NewPlayer->GetPawn()) { GetWorld()->DestroyActor(NewPlayer->GetPawn()); } TArray<AActor*> ActArray; UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), ActArray); if (ActArray.Num() > 0) { //人数+1
        PlayerCount++; //读取角色蓝图
        UClass* CharacterClass = LoadClass<ARPCCourseCharacter> (NULL, TEXT("Blueprint'/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter.ThirdPersonCharacter_C'")); //生成角色,位置是PlayerStart或者它的右边
        ARPCCourseCharacter* NewCharacter = GetWorld()->SpawnActor<ARPCCourseCharacter> (CharacterClass, ActArray[0]->GetActorLocation() + FVector(0.f, PlayerCount*200.f, 0.f), ActArray[0]->GetActorRotation()); //把玩家交给他对应的控制器
        NewPlayer->Possess(NewCharacter); DDH::Debug() << NewPlayer->GetName() << "Login" << DDH::Endl(); } } void ARPCCourseGameMode::Logout(AController* Exiting) { Super::Logout(Exiting); PlayerCount--; DDH::Debug() << Exiting->GetName() << "Logout" << DDH::Endl(); }

 

5.特殊的联机方法

  项目打包后,直接打开exe文件,此时游戏视为单机游戏(Standalone)。

  若是在exe的快捷方式后缀加上" ?listen"。则此时游戏视为监听模式(NM_ListenServer),如:

  

  若是在exe的快捷方式后缀加上"  127.0.0.1 -game",而且处于监听模式的游戏存在时(即已经打开了上面的RPCCourseServer),则此时游戏视为客户端,自动加入该游戏(NM_Client)。(重复打开,则重复添加客户端)如:

  

  若是直接打开原exe文件,按"~"调出控制面板后,输入“open 127.0.0.1”。则会加入已处于监听模式的游戏中,此时,此游戏成为客户端。

  

  若是在已处于监听模式的游戏中,按"~"调出控制面板后,输入“open 127.0.0.1”,则会关闭联网模式,全部游戏变为单机游戏。

  

6. 用C++建立、加入或摧毁会话

  首先要在build.cs中添加组件:

  

  因为GameInstance在游戏中一直存在,故建立会话等操做都在GameInstance中进行:

.h: #include "CoreMinimal.h" #include "Engine/GameInstance.h" #include "../Plugins/Online/OnlineSubsystem/Source/Public/Interfaces/OnlineSessionInterface.h" #include "IDelegateInstance.h" #include "RPCInstance.generated.h"

class IOnlineSubsystem; class APlayerController; /** * */ UCLASS() class RPCCOURSE_API URPCInstance : public UGameInstance { GENERATED_BODY() public: URPCInstance(); //指定玩家控制器
    void AssignPlayerController(APlayerController* InController); //建立会话
    void HostSession(); //加入会话
    void ClientSession(); //摧毁会话
    void DestroySession(); protected: //当建立会话结束后,调用这个函数
    void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful); //当开始会话结束后,调用这个函数
    void OnStartSessionComplete(FName SessionName, bool bWasSuccessful); //加入服务器(会话Session)回调函数
    void OnFindSessionComplete(bool bWasSuccessful); void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); //销毁会话回调函数
    void OnDestroySessionComplete(FName SessionName, bool bWasSuccessful); protected: APlayerController* PlayerController; //开启服务器委托
 FOnCreateSessionCompleteDelegate OnCreateSessionCompleteDelegate; FOnStartSessionCompleteDelegate OnStartSessionCompleteDelegate; //开启服务器委托句柄
 FDelegateHandle OnCreateSessionCompleteDelegateHandle; FDelegateHandle OnStartSessionCompleteDelegateHandle; //加入服务器委托
 FOnFindSessionsCompleteDelegate OnFindSessionsCompleteDelegate; FOnJoinSessionCompleteDelegate OnJoinSessionCompleteDelegate; //加入服务器委托句柄
 FDelegateHandle OnFindSessionsCompleteDelegateHandle; FDelegateHandle OnJoinSessionCompleteDelegateHandle; //销毁会话委托与句柄
 FOnDestroySessionCompleteDelegate OnDestroySessionCompleteDelegate; FDelegateHandle OnDestroySessionCompleteDelegateHandle; IOnlineSubsystem* OnlineSub; TSharedPtr<const FUniqueNetId> UserID; //保存寻找到的Sessions
    TSharedPtr<FOnlineSessionSearch> SearchObject; }; .cpp:
 #include "Public/RPCInstance.h" #include "GameFramework/PlayerController.h" #include "../Plugins/Online/OnlineSubsystem/Source/Public/Online.h" #include "../Plugins/Online/OnlineSubsystemUtils/Source/OnlineSubsystemUtils/Public/OnlineSubsystemUtils.h" #include "Public/RPCHelper.h" #include "Kismet/GameplayStatics.h"
 URPCInstance::URPCInstance() { //绑定回调函数
    OnCreateSessionCompleteDelegate = FOnCreateSessionCompleteDelegate:: CreateUObject(this, &URPCInstance::OnCreateSessionComplete); OnStartSessionCompleteDelegate = FOnStartSessionCompleteDelegate:: CreateUObject(this, &URPCInstance::OnStartSessionComplete); OnFindSessionsCompleteDelegate = FOnFindSessionsCompleteDelegate:: CreateUObject(this, &URPCInstance::OnFindSessionComplete); OnJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate:: CreateUObject(this, &URPCInstance::OnJoinSessionComplete); OnDestroySessionCompleteDelegate = FOnDestroySessionCompleteDelegate:: CreateUObject(this, &URPCInstance::OnDestroySessionComplete); } void URPCInstance::AssignPlayerController(APlayerController* InController) { PlayerController = InController; //获取OnlineSub //获取方式一:Online::GetSubsystem(GetWorld(), NAME_None),推荐使用这种 //获取方式二:使用IOnlineSubsystem::Get(),直接获取能够createSession,可是joinSession后,客户端没有跳转场景
    OnlineSub = Online::GetSubsystem(PlayerController->GetWorld(), NAME_None); //获取UserID //获取方式一:UGameplayStatics::GetGameInstance(GetWorld())->GetLocalPlayers()[0]->GetPreferredUniqueNetId()
    if (GetLocalPlayers().Num() == 0) DDH::Debug() << "No LocalPlayer Exist, Can't Get UserID" << DDH::Endl(); else UserID = (*GetLocalPlayers()[0]->GetPreferredUniqueNetId()).AsShared(); //用宏定义,使编译器不对下面这段代码编译
#if 0 
    //获取方式二:使用PlayerState获取,打包后运行没问题,但在编辑器多窗口模式下,PlayerState不存在
    if (PlayerController->PlayerState) UserID = PlayerController->PlayerState->UniqueId.GetUniqueNetId(); else DDH::Debug() << "No PlayerState Exist, Can't Get UserID" << DDH::Endl(); #endif

    //在这里直接获取Session运行时会报错,生命周期的问题
 } void URPCInstance::HostSession() { if (OnlineSub) { IOnlineSessionPtr Session = OnlineSub->GetSessionInterface(); if (Session.IsValid()) { //会话设置
 FOnlineSessionSettings Settings; //链接数
            Settings.NumPublicConnections = 10; Settings.bShouldAdvertise = true; Settings.bAllowJoinInProgress = true; //使用局域网
            Settings.bIsLANMatch = true; Settings.bUsesPresence = true; Settings.bAllowJoinViaPresence = true; //绑定委托
            OnCreateSessionCompleteDelegateHandle = Session ->AddOnCreateSessionCompleteDelegate_Handle (OnCreateSessionCompleteDelegate); //建立会话
            Session->CreateSession(*UserID, NAME_GameSession, Settings); } } } void URPCInstance::ClientSession() { if (OnlineSub) { IOnlineSessionPtr Session = OnlineSub->GetSessionInterface(); if (Session.IsValid()) { //实例化搜索结果指针而且设定参数
            SearchObject = MakeShareable(new FOnlineSessionSearch); //返回结果数
            SearchObject->MaxSearchResults = 10; //是不是局域网,就是IsLAN
            SearchObject->bIsLanQuery = true; SearchObject->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); //绑定寻找会话委托
            OnFindSessionsCompleteDelegateHandle = Session-> AddOnFindSessionsCompleteDelegate_Handle (OnFindSessionsCompleteDelegate); //进行会话寻找
            Session->FindSessions(*UserID, SearchObject.ToSharedRef()); } } } void URPCInstance::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful) { if (OnlineSub) { IOnlineSessionPtr Session = OnlineSub->GetSessionInterface(); if (Session.IsValid()) { //解绑建立会话完成回调函数
            Session-> ClearOnCreateSessionCompleteDelegate_Handle (OnCreateSessionCompleteDelegateHandle); //判断建立会话是否成功
            if (bWasSuccessful) { DDH::Debug() << "CreatSession Succeed" << DDH::Endl(); //绑定开启会话委托
                OnStartSessionCompleteDelegateHandle = Session-> AddOnStartSessionCompleteDelegate_Handle (OnStartSessionCompleteDelegate); Session->StartSession(NAME_GameSession); } else DDH::Debug() << "CreateSession Failed" << DDH::Endl(); } } } void URPCInstance::OnStartSessionComplete(FName SessionName, bool bWasSuccessful) { DDH::Debug() << "StartSession Start" << DDH::Endl(); if (OnlineSub) { IOnlineSessionPtr Session = OnlineSub->GetSessionInterface(); if (Session.IsValid()) { //注销开启会话委托绑定
            Session->ClearOnStartSessionCompleteDelegate_Handle (OnStartSessionCompleteDelegateHandle); if (bWasSuccessful) { DDH::Debug() << "StartSession Succeed" << DDH::Endl(); //服务端跳转场景
                UGameplayStatics::OpenLevel(PlayerController->GetWorld(), FName("GameMap"), true, FString("listen")); } else DDH::Debug() << "StartSession Failed" << DDH::Endl(); } } } void URPCInstance::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (OnlineSub) { IOnlineSessionPtr Session = OnlineSub->GetSessionInterface(); if (Session.IsValid()) { //取消加入对话委托绑定
            Session->ClearOnJoinSessionCompleteDelegate_Handle (OnJoinSessionCompleteDelegateHandle); //若是加入成功
            if (Result == EOnJoinSessionCompleteResult::Success) { //传送玩家到新地图
 FString ConnectString; if (Session->GetResolvedConnectString(NAME_GameSession, ConnectString)) { DDH::Debug() << "Join Sessions Succeed" << DDH::Endl(); //客户端切换到服务器的关卡
                    PlayerController->ClientTravel(ConnectString, TRAVEL_Absolute); } else DDH::Debug() << "Join Sessions Failed" << DDH::Endl(); } } } } void URPCInstance::OnFindSessionComplete(bool bWasSuccessful) { if (OnlineSub) { IOnlineSessionPtr Session = OnlineSub->GetSessionInterface(); if (Session.IsValid()) { //取消寻找会话委托绑定
            Session->ClearOnStartSessionCompleteDelegate_Handle (OnStartSessionCompleteDelegateHandle); //若是寻找会话成功
            if (bWasSuccessful) { //若是收集的结果存在且大于1
                if (SearchObject.IsValid() && SearchObject->SearchResults.Num() > 0) { DDH::Debug() << "Find Sessions Succeed" << DDH::Endl(); //绑定加入Session委托
                    OnJoinSessionCompleteDelegateHandle = Session ->AddOnJoinSessionCompleteDelegate_Handle (OnJoinSessionCompleteDelegate); //执行加入会话
                    Session->JoinSession(*UserID, NAME_GameSession, SearchObject->SearchResults[0]); } else DDH::Debug() << "Find Sessions Succeed But Num = 0" << DDH::Endl(); } else DDH::Debug() << "Find Sessions Failed" << DDH::Endl(); } } } void URPCInstance::OnDestroySessionComplete(FName SessionName, bool bWasSuccessful) { if (OnlineSub) { IOnlineSessionPtr Session = OnlineSub->GetSessionInterface(); if (Session.IsValid()) { //注销销毁会话委托
            Session->ClearOnDestroySessionCompleteDelegate_Handle (OnDestroySessionCompleteDelegateHandle); //其它逻辑。。。
 } } } void URPCInstance::DestroySession() { if (OnlineSub) { IOnlineSessionPtr Session = OnlineSub->GetSessionInterface(); if (Session.IsValid()) { //绑定销毁会话委托
            OnDestroySessionCompleteDelegateHandle = Session-> AddOnDestroySessionCompleteDelegate_Handle (OnDestroySessionCompleteDelegate); //执行销毁会话
            Session->DestroySession(NAME_GameSession); } } }

 7.注意事项:

  必须知足一些要求才能充分发挥 RPC 的做用:

  1. 它们必须从 Actor 上调用。

  2. Actor 必须被复制。

  3. 若是 RPC 是从服务器调用并在客户端上执行,则只有实际拥有这个 Actor 的客户端才会执行函数。

  4. 若是 RPC 是从客户端调用并在服务器上执行,客户端就必须拥有调用 RPC 的 Actor。

  5. 多播 RPC 则是个例外:

    • 若是它们是从服务器调用,服务器将在本地和全部已链接的客户端上执行它们。

    • 若是它们是从客户端调用,则只在本地而非服务器上执行。

    • 如今,咱们有了一个简单的多播事件限制机制:在特定 Actor 的网络更新期内,多播函数将不会复制两次以上。按长期计划,咱们会对此进行改善,同时更好的支持跨通道流量管理与限制

  6. 关于可靠性(Reliable)

  

 

 

8.讨论:

1.在一个可复制的(Replicated)且一开始就在地图里的物体中,调用广播:

 

 

 结果:客户端和服务器都打印了。

  

 

2. 服务器生成一个可复制的物体,客户端会存在这个物体吗?

实验1.在服务器生成一个物体,物体设置为可复制的(Replicated),而后把这个物体广播出去。(在关卡蓝图中)

 

 

 结果:只有服务器存在石头,客户端不存在。

  

3. 服务器生成一个可复制的物体,且调用此物体的多播函数:

  

 

 

   

 

 结果:客户端和服务器都打印了。

  

相关文章
相关标签/搜索