• UE4的联网系统研究


    1. 物体复制

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

      这里只挑部分点来展开。

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

      然后,先看在c++中的两个参数:bNetLoadOnClient和SetReplicates(true), 对应蓝图的参数如下图所示:

      

      Replicate的意思为复制。

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

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

      PS:

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

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

      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. 服务器生成一个可复制的物体,且调用此物体的多播函数:

      

       

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

      

  • 相关阅读:
    set集合
    作业(2)
    字典
    列表功能
    字符串
    while循环
    pb加密转换成C#
    NET在64位系統使用32位oracle客户端访问数据库
    SQL Server中查找包含某个文本的存储过程
    SQLServer 错误: 15404,无法获取有关 Windows NT 组/用户
  • 原文地址:https://www.cnblogs.com/mcomco/p/11189725.html
Copyright © 2020-2023  润新知