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. 服務器生成一個可復制的物體,且調用此物體的多播函數:

  

 

 

   

 

 結果:客戶端和服務器都打印了。

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM