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

  

 

 

   

 

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

  

相關文章
相關標籤/搜索