照着油管上的UE4 C++ Network Multiplayer教程敲了一遍多人游戲的實現。嘗試着理解UE4的多人游戲C/S同步方式。
其中有幾個基本概念:
1.GameMode:只有一份且只存在於Server端。
關於Actor replication:
2.如果一個Actor為設置為replication,那么所有的客戶端Clients都能看到這個Actor:If an Actor replicates when it's spawned on the server that means it will be sent to all the clients all the remote machines. And they will be aware of that actors existence.!!!
3.There are more complex scenarios that come up where somtimes things will only replicate to the person who owns them.
4.因此Role == Role_Authority不一定都是在Server端上。
關於 Actor的Variable Replication:
Howerver it doesn't always make sense to replicate every simple piece of information on the actor。
這個多人游戲視頻教程的玩法大致如上:每個玩家定時減少血量,需要拾取從地圖上空掉下來電池的電池補充血量,被拾取的電池會消失同時伴有雷電效果。如果血量為0則會有倒下的效果。
於是思考如下幾個問題,基本上可以理解UE4同步的使用方式。以下的服務器方式是Listening Server
問題一:如何在地圖上掉落電池並讓所有玩家都看到
讓所有玩家都看到電池這個實現只需要將電池(BatteryActor)的bReplicates、bReplicateMovement設置為true,在Server端生成這些Actor,引擎就實現了同步給所有客戶端的操作了。
有一個SpawnVolume內啟動了一個定時器會產生這些Battery Actor,在GameMode下會調用這個函數;
void ASpawnVolume::SetSpawningActive(bool bShouldSpawn) { if (Role == ROLE_Authority) { if (bShouldSpawn) { SpawnDelay = FMath::FRandRange(SpawnDelayRangeLow, SpawnDelayRangeHigh); GetWorldTimerManager().SetTimer(SpawnTimer, this, &ASpawnVolume::SpawnPickup, SpawnDelay, false); } else { GetWorldTimerManager().ClearTimer(SpawnTimer); } } }
問題二:玩家(Client)如何拾取一塊Battery Actor
在Client端調用,通過網絡傳給Server端的RPC函數
//RPC服務器執行 UFUNCTION(Reliable, Server, WithValidation) void ServerCollectPickups();
在Server端會遍歷這個玩家所有的OverlapActors,然后拿到這些Battery Actor。
注意Client端的Character其實只是Server端的一個副本,因此在Server端執行獲取玩家身邊的OverlapActors理所應當是可行的。
void ANMPGameCharacter::ServerCollectPickups_Implementation() { if (Role == ROLE_Authority) { float TotalPower = 0.0f; TArray<AActor*> CollectedActors; CollectionSphere->GetOverlappingActors(CollectedActors); for (int i = 0; i < CollectedActors.Num(); ++i) { UE_LOG(LogClass, Log, TEXT("overlap num=%d"), CollectedActors.Num()); APickup* const TestPickup = Cast<APickup>(CollectedActors[i]); if (TestPickup != NULL /*&& TestPickup->IsPendingKill()*/ && TestPickup->IsActive()) { if (ABatteryPickup *const TestBattery = Cast<ABatteryPickup>(TestPickup)) { TotalPower += TestBattery->GetPower(); } TestPickup->PickUpBy(this); TestPickup->SetActive(false); } } if (!FMath::IsNearlyZero(TotalPower, 0.001f)) { updatePower(TotalPower); } } }
在updatePower()函數內修改了屬於這個Character的replicated variables,因此會自動通過網絡通知Client端調用OnRepNotify函數:
void ANMPGameCharacter::updatePower(float DeltaPower) { if (Role == ROLE_Authority) { CurrentPower += DeltaPower; GetCharacterMovement()->MaxWalkSpeed = BaseSpeed + SpeedFactor * CurrentPower; //listen server不會自動調用OnRep,這里是Server fake調用 OnRep_CurrentPower(); } }
Client端在CurrentPower變量發生改變的時候調用OnRep_CurrentPower()函數,就可以用來在Client端更新玩家的顏色變化之類的;PowerChangeEffect()可以用藍圖實現;
void ANMPGameCharacter::OnRep_CurrentPower() { PowerChangeEffect(); }
在這里這個Server端的Character的Replicated Variables CurrentPower的變化,同步給到Client端,應該是單向同步到指定的客戶端,而不是MultiCast的廣播同步。里面應該是涉及到了Server的指定同步功能。
問題三:如何讓對方看到我的顏色變化?(待驗證)
首先顏色是依據CurPower的值來設定的。也就是說當Server端的CurPower值發生變化的時候,會通知Client端。Client端調用OnRep_xxx函數來更新自己的顏色。注意這時候只是一個Client端更新了顏色其他客戶端是看不到這個Client顏色的變更。如果要讓所有的玩家的看到這個玩家的顏色變更,需要在Server端調用OnRep_xxx函數(Listen Server不會調用OnRep_xx函數)。由Server將這個Client的顏色同步給其他Client。(應該是Replicated Actor自動完成的);
疑惑:如果是這樣的話直接在Server端調用顏色變化的函數不就可以了嗎?
問題四:其他玩家Client是如何看到閃電生成的?
閃電的生成任務應該交由被拾取的Battery Actor而不是Character,因此Server端在拾取的時候,被拾取的電池存儲了Character的指針。這些都發送在Server端。
在Battery Actor的父類定義了一個廣播RPC函數用來模擬閃電的視覺效果:
//哪個玩家拿了電池,這個Pawn是Server端的Pawn UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pickup") APawn* PickupInstigator; private: UFUNCTION(NetMulticast, Unreliable) void ClientOnPickedUpBy(APawn *Pawn);
Server服務器端調用了BatteryActor的pickupBy(),在pickupBy()函數內部又通過RPC函數讓所有的客戶端的BatteryActor調用了clientOnPickupBy(),讓客戶端得知某個BatteryActor被拾取。由客戶端模擬聲音、粒子效果等等;
在所有的客戶端的BatteryActor調用ClientOnPickupBy()里面記錄了是哪個Pawn拾取的電池,因此每個客戶端的BatteryActor都在這個Pawn的附近模擬了閃電;
void APickup::ClientOnPickedUpBy_Implementation(APawn* Pawn) { PickupInstigator = Pawn; //WasCollected()的由藍圖實現; WasCollected(); }
問題五:如何讓所有的玩家的血量減少?
GameMode只有一個且在Server端上;每個玩家都會對應一個Controller,因此可以從GetWorld()里面拿到所有玩家的Controller,然后再拿到對應玩家操控的Pawn。
這些都是在Server端操作的。也就是說Server端自帶這能知道這些。看來UE服務端和客戶端不得不用同一套代碼。
問題六:如何做屬於自己HUD界面的
GameMode是屬於Server端的(真的是只有一份在Server端!!!)在Client端Cast To GameMode會失敗。
在GameMode里面指定了一個HUDClass,因此所有的客戶端Client都只有一份HUD和Widget,因此在HUD里面顯示和自己相關的數值界面,依靠的是通過GameState等其他方式拿到和玩家相關的數值的;
問題五:在GameMode里面設置GameState內的某個變量如果同步給Client的?(待確定)
如果客戶端想要獲得某個游戲的狀態,例如我的MyPower是否到達了上限,這個上限可以存在GameState里面,GameState里面的會Replicate同步給屬於他的客戶端。
在GameMode里面對MyGameState->setCurrentState();是怎么知道是哪個Server端玩家的GameState?好像是設置所有玩家共同的State的;對的,是設置所有的;
問題六:NetMuticast函數疑惑?(待確定)
玩家能量完全失去后會調用OnPlayerDeath()函數做模擬Ragdoll倒下的動作,這個函數是NetMulticast的會通過網絡發送給所有客戶端執行。
那么客戶端是怎么知道不是我這個Player Actor在執行Ragdoll倒下的動作,而是那個失去能量的Character在執行???
void ANMPGameGameMode::DrainPowerOverTime() { UWorld* World = GetWorld(); check(World); ANMPGameGameState* myGameState = Cast<ANMPGameGameState>(GameState); check(myGameState); for (FConstControllerIterator It = World->GetControllerIterator(); It; ++It) { if (APlayerController *PlayerController = Cast<APlayerController>(*It)) { if (ANMPGameCharacter *BatteryCharacter = Cast<ANMPGameCharacter>(PlayerController->GetPawn())) { if (BatteryCharacter->GetCurrentPower() > myGameState->PowerToWin) { myGameState->WinningPlayerName = BatteryCharacter->GetName(); //Todo:疑惑2:在Server端設置一個Player的GameState還不用做區分!?啥呀呀 HandleNewState(EBatteryPlayState::EWon); } else if (BatteryCharacter->GetCurrentPower() > 0) { BatteryCharacter->updatePower(-PowerDrainDelay * DecayRate * (BatteryCharacter->GetInitialPower())); } else { //完全失去能量的在Server端的Character會調用這個函數,而這個函數是MultiCast的。同步給所有客戶端; BatteryCharacter->OnPlayerDeath(); ++DeadPlayerCount; if (DeadPlayerCount >= GetNumPlayers()) { HandleNewState(EBatteryPlayState::EGameOver); } } } } } }