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 的作用:
-
它們必須從 Actor 上調用。
-
Actor 必須被復制。
-
如果 RPC 是從服務器調用並在客戶端上執行,則只有實際擁有這個 Actor 的客戶端才會執行函數。
-
如果 RPC 是從客戶端調用並在服務器上執行,客戶端就必須擁有調用 RPC 的 Actor。
-
多播 RPC 則是個例外:
-
如果它們是從服務器調用,服務器將在本地和所有已連接的客戶端上執行它們。
-
如果它們是從客戶端調用,則只在本地而非服務器上執行。
-
現在,我們有了一個簡單的多播事件限制機制:在特定 Actor 的網絡更新期內,多播函數將不會復制兩次以上。按長期計划,我們會對此進行改善,同時更好的支持跨通道流量管理與限制
-
6. 關於可靠性(Reliable)
8.討論:
1.在一個可復制的(Replicated)且一開始就在地圖里的物體中,調用廣播:
結果:客戶端和服務器都打印了。
2. 服務器生成一個可復制的物體,客戶端會存在這個物體嗎?
實驗1.在服務器生成一個物體,物體設置為可復制的(Replicated),然后把這個物體廣播出去。(在關卡藍圖中)
結果:只有服務器存在石頭,客戶端不存在。
3. 服務器生成一個可復制的物體,且調用此物體的多播函數:
結果:客戶端和服務器都打印了。