照着油管上的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); } } } } } }