keywords:UE4, Replication, Relicate, Reliable, RPC, RTS Movement, Dedicated Server, 专用服务器

实例的完整工程下载地址见文章底部。

属性同步

属性同步基本用法

步骤:
1,对属性添加UPROPERTY(Replicated)宏:

//Player display name
UPROPERTY(Replicated)
    FString Alias_;

2,属性所在的class中重写函数GetLifetimeReplicatedProps
需要头文件:

#include "Net/UnrealNetwork.h"

重写函数:

void AReplTestCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(AReplTestCharacter, Alias_);
}
属性同步扩展用法

如何为同步属性添加回调函数?

用法:

UPROPERTY(ReplicatedUsing = OnHPChanged)
    float HealthPoint;
    
UFUNCTION()
    void OnHPChanged();

ReplicatedUsing只在客户端执行,服务端不执行。客户端相关表现逻辑可以放在ReplicatedUsing回调函数。另外函数需要添加UFUNCTION

蓝图对应的标记叫RepNotify
https://docs.unrealengine.com/en-US/Resources/ContentExamples/Networking/1_4/index.html

RPC(远程执行调用)

步骤:
1,对需要远程执行的函数添加宏UFUNCTION(Server, Reliable, WithValidation)或者UFUNCTION(Client, Reliable)。其中Server表示在客户端调用,在服务端执行;WithValidation表示是否需要验证函数,加上的画需要添加函数:bool MyFun_Validate(),函数体内容写在MyFun_Implementation函数内。cpp中不需要与函数名同名的函数体,只需要实现_Validate和_Implementation两个函数即可。 头文件:

//移动角色(只在服务端执行的函数)
UFUNCTION(Server, Reliable, WithValidation)
    void ServerMoveToDest(APawn* Panw, const FVector DestLocation);
bool ServerMoveToDest_Validate(APawn* Panw, const FVector DestLocation);
void ServerMoveToDest_Implementation(APawn* Panw, const FVector DestLocation);

CPP:

bool AReplTestPlayerController::ServerMoveToDest_Validate(APawn* Panw, const FVector DestLocation)
{
    return true;
}

void AReplTestPlayerController::ServerMoveToDest_Implementation(APawn* Pawn, const FVector DestLocation)
{
    //logic...
}
三种 RPC 函数区别
  • UFUNCTION(Server, Reliable, WithValidation) 客户端请求,服务端执行。
    使用场景:涉及到数据安全的行为,比如:砍一刀扣血,扣血条件判定以及血量修改,都应该放在服务端执行。
  • UFUNCTION(Client, Reliable) 服务端请求,客户端执行,也可能是服务端执行(详细描述见下文)。NM_Standalone模式下,Client函数也会执行。
    使用场景:只是表现相关,不涉及数据修改的行为,比如:装备升级,整备的属性修改发生在服务端,升级成功后的外观变化,服务端需要通知客户端替换武器 Mesh 和材质。
  • UFUNCTION(NetMulticast, Reliable) 服务端先执行,然后所有连接的客户端再执行。默认情况下只在服务端执行(详细描述见下文)。
    使用场景:当前客户端做的表现也希望其他客户端也看到,比如:播放攻击动作,客户端A控制的角色A播放攻击动作,希望所有其他客户端也能看见角色A播放了攻击动作。
    NetMulticast一般可以设置为 Unreliable ,表示如果网络不通畅,不重新发送 UDP 消息,比如上述装备升级,如果网络问题导致客户端未能更新装备外观,影响也不大,Unreliable 可以节省带宽。

    UFUNCTION(Client, Unreliable) 并不表示是一定在客户端执行!!!也可能是服务端执行。官方文档: Since the server can own actors itself, a “Run on Owning Client” event may actually run on the server, despite its name.

    官方文档:Multiplayer in Blueprints
    https://docs.unrealengine.com/latest/INT/Gameplay/Networking/Blueprints/index.html
角色身上需要设置的属性

角色蓝图上的这几个属性默认是勾选的。如果是C++,对应的属性名也是这几个。

登陆界面:

多个客户端连上服务端的最终情景:

同步相关的基础概念

NetMode

每个Actor有个接口:AActor::GetNetMode(),返回值意思如下:

  • NM_Standalone:表示当前Actor在独立服务器(单机模式),可以执行客户端、服务端的所有功能。
  • NM_DedicatedServer:表示当前Actor在专用服务器上,只能执行服务端相关的功能。
  • NM_ListenServer:官方文档上解释的很少,我在官方论坛上问了下得到的几个解释是,ListenServer可以被游戏内的某个玩家的机器当作服务器,该服务器拥有操作每个客户端角色的权利。这种模式下,更方便来创建服务器;缺点是承载人数较少。个人理解是ListenServer可能更适合做局域网游戏,因为这种既不需要考虑外挂也不需要考虑承载压力。
  • NM_Client:当客户端未连接 Dedicated Server 时,NetMode 为 NM_Standalone, 连接成功之后,客户端的 NetMode 就变成了 NM_Client 。 DedicatedServer没有客户端的相关功能,只接收远程客户端的网络请求,适合承载大规模玩家在线。另外连接DedicatedServer的客户端,是没有权限直接修改其他玩家的数据,因为客户端上的大部分Object的Role枚举都是ROLE_AutonomousProxy(如果你逻辑有bug,也是可以被客户端发送非法请求串改其他角色的数据,所以这里所说的没有权限修改,不要以为DedicatedServer可以帮你反外挂),但是在ListenServer的玩家主机上可以,因为他拥有其他所有玩家的实例(Role枚都是ROLE_Authority类型)。UE4的一些API内部有权限检测的逻辑:判断当前Role类型是否为ROLE_Authority,这样避免非法修改数据。

判断当前网络模式是否为Dedicated Server或者Client的API:
蓝图接口:

bool UKismetSystemLibrary::IsServer(const UObject* WorldContextObject)

bool UKismetSystemLibrary::IsDedicatedServer(const UObject* WorldContextObject)

bool UKismetSystemLibrary::IsStandalone(const UObject* WorldContextObject)

C++接口:

//CoreMisc.h下的全局函数
bool IsRunningDedicatedServer()
bool IsRunningClientOnly()

如果通过editor启动游戏:

  • IsRunningDedicatedServer()返回true的前提是命令行包含参数-server
  • IsRunningClientOnly()返回true的前提是命令行参数包含-game -clientonly
  • 打包版本下,BuildTarget必须为ClientIsRunningClientOnly()才会返回true

Role

每个Actor有个公开属性:AActor::Role。表示当前Actor的作用权限,枚举值有:

  • ROLE_SimulatedProxy:表示当前Actor是一个模拟服务端的Actor状态Object,无法修改服务端上的数据,也没有权限执行远程函数(Reliable标识的UFUNCTION)。DedicatedServer服务器中创建的对象,在服务端都是ROLE_Authority,这些对象映射在客户端上的对象则都是ROLE_SimulatedProxy,当然也有办法让这些对象在客户端变成非ROLE_SimulatedProxy类型,具体方法见下文。该类型的对象,如果去执行自身的UFUNCTION(Server)标识的函数,该函数不会执行。
  • ROLE_AutonomousProxy:与ROLE_SimulatedProxy相似,区别是ROLE_AutonomousProxy有权限执行远程函数,但是远程函数必须是该Object身上的,其他Object上的远程函数没有权限执行。客户端的ROLE_AutonomousProxy类型对象,其函数若未加UFUNCTION(Server)标识,执行时仍然是在是在客户端。执行UFUNCTION(Server)标识的函数时,在服务端触发执行,Role类型为ROLE_Authority,NetMode类型为NM_DedicatedServer。ROLE_AutonomousProxy的优点之一是:当对角色执行转向或者位移控制时,会自动请求服务端处理,而ROLE_SimulatedProxy是做不到的,需要手动请求服务端。
  • ROLE_Authority:最高权限,即可以修改Server上的属性,又可以执行任何Objecct身上的远程函数。Standalone服务器中,所有对象都是ROLE_Authority。DedicatedServer服务器中创建的对象,在服务端都是ROLE_Authority。
    如果是Client创建的对象,其Role也是ROLE_Authority,但是这种Actor无法与服务端通信。

服务端SpawnActor创建的Character,若开启了Replication,则其在客户端会生成一个ROLE_SimulatedProxy的Character,此时在服务端用其对应的PlayerController对其Possess,则该客户端Character的Role会变成ROLE_AutonomousProxy

从执行PossessRole被修改为ROLE_AutonomousProxy的堆栈:

UE4Editor-Engine.dll!AActor::SetAutonomousProxy(const bool bInAutonomousProxy, const bool bAllowForcePropertyCompare) Line 3268    C++
UE4Editor-Engine.dll!APawn::PossessedBy(AController * NewController) Line 511    C++
UE4Editor-Engine.dll!ACharacter::PossessedBy(AController * NewController) Line 822    C++
UE4Editor-Engine.dll!APlayerController::OnPossess(APawn * PawnToPossess) Line 770    C++
UE4Editor-Engine.dll!AController::Possess(APawn * InPawn) Line 293    C++

同步相关的常见问题

如何让一个ROLE_SimulatedProxy对象变成ROLE_AutonomousProxy

先补充一个概念:服务端连接(非官方文档,自己通过实践后的理解),比如,AGameModeBase::PostLogin(APlayerController* NewPlayer)中的PlayerController就是拥有服务端连接的对象,在服务端,其可以调用Client function且能保证该Client function在客户端执行;在客户端,其也有权限调用 Server function且能保证该Server function在服务端执行。

两种方式:

  • 方式一:3个步骤,缺一不可:
    • bReplicates设置为true:Actor->SetReplicates(true);

    • 设置自己的owner(SetOwner())为拥有服务端连接的Actor,这个Actor可以是:服务端的PlayerController,或者已经被该PlayerController执行过Possess过的Character,或者执行过SetOwner且Owner为拥有服务端连接的Actor。

        TestActor = GetWorld()->SpawnActor<ATestActor>(ATestActor::StaticClass());
        TestActor->SetReplicates(true);
        TestActor->SetAutonomousProxy(true);
        TestActor->SetOwner(NewPlayer);
      

      两点注意事项:
      1,上述三个步骤中,SetReplicates(true)必须最先执行,否则Actor在客户端的复本仍然是ROLE_SimulatedProxy
      2,SetAutonomousProxy(true);在构造函数中执行无效,必须在Spawn完成之后执行,否则Actor在客户端的复本仍然是ROLE_SimulatedProxy

  • 方式二:使用拥有服务端连接的PlayerControllerPossess一个Character,Possess之前执行Actor->SetReplicates(true);。注意:ROLE_Authority类型的PlayerController才有权限执行Possess。服务端的PlayerController,其Role类型都为ROLE_Authority。
    上文中的Possess堆栈,APawn::PossessedBy(AController* NewController)内部已经帮你执行了Actor->SetAutonomousProxy(true)Actor->SetReplicates(true);

想要了解UE4是如何判断对象是否拥有服务端连接的底层原理,可调试函数AActor::IsNetRelevantFor()

NetMode 和 Role 关系概述
  • 当 NetMode 为NM_Standalone或者NM_DedicatedServer时,所有对象的Role都为ROLE_Authority
  • 当 NetMode 为 NM_Client 时,服务端 Spawn 出来的所有 Character 对象,在每个客户端的Role都为 ROLE_SimulatedProxyPlayerController 比较特殊:当客户端连接服务器成功后,PlayerController 的 Role 自动变为 ROLE_AutonomousProxy ;
Client function 没有在 Client 触发的问题

如果调用 Client function 的对象是在 Server (NM_DedicatedServer) 创建的,默认情况下,该对象上的 Client function 始终会在 Server 执行,且 Client (NM_Client) 不会触发。
如何让在Server上创建的对象的Client function只在Client (NM_Client) 执行?答案:SetOwner(),Owner为拥有服务端连接的对象,或者被有服务端连接的PlayerController执行过Possess的对象(如果对象是Pawn的话)。
如果是非 Server 创建的对象,比如:PlayerController ,那么其内部的 Client function 会在客户端执行。
如何在 Server 上获取这个 Client 的 PlayerController : 重写 AGameMode::InitNewPlayer() 或者 AGameMode::PostLogin() ,PlayerController 会作为参数传递进来,将这个 PlayerController 指针保存下来。

NetMulticast function 没有在 Client 触发的问题

并不是定义了 NetMulticast function ,就一定会在 Client 执行。
比如,在服务端生成了一个 Actor ,且在服务端执行该 Actor 上的 Multicast function ,默认情况下该 Multicast function 只会在 Server 执行。
如果要使该 Actor 的 Multicast function 在客户端也能够执行,分两种情况:

  • 如果是手榴弹这种属于某个角色的Actor:

    • 除了对该 Actor 执行 bReplicates = true; 外,还要执行 bNetUseOwnerRelevancy = true;
    • 该 Actor Spawn 之后,需要执行 Actor->SetOwner(NewOwner);,这个 NewOwner 是一个 Replicated 对象,比如 Character 。NewOwner对象必须拥有服务端连接。对应的Get接口为:GetOwner()
  • 如果是类似场景机关这种对所有玩家共享的Actor,构造函数中执行:

      bReplicates = true;
      bOnlyRelevantToOwner = false;
    
关键函数

1,客户端登陆

void UMyUserWidget::OnBtnLoginClick()
{
    if (APlayerController* PC = GetWorld()->GetFirstPlayerController())
    {
        FString URL = FString::Printf(TEXT("%s:%s?Alias=%s"), *(TxtServerIP->GetText().ToString()), *(TxtServerPort->GetText().ToString()), *(TxtUsername->GetText().ToString()));
        PC->ClientTravel(*URL, TRAVEL_Absolute);
    }
}

拒绝非法登陆请求:

void AFPSGameMode::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
{
    Super::PreLogin(Options, Address, UniqueId, ErrorMessage);

    FString UserName = UGameplayStatics::ParseOption(Options, TEXT("UserName")).TrimStart().TrimEnd();
    if (UserName.Len() == 0 || UserMap.Find(UserName))
    {
        //deny client's login request.
        ErrorMessage = TEXT("User login repeatly!");
    }
}

ErrorMessage设置为非空字符串,就表示拒绝客户端的链接。

2,GameMode::InitNewPlayer(),处理登陆请求时的自定义参数,比如账号名和密码。

FString AReplTestGameMode::InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId, const FString& Options, const FString& Portal)
{
    FString Rs = Super::InitNewPlayer(NewPlayerController, UniqueId, Options, Portal);
    if (AReplTestPlayerController* RTPC = Cast<AReplTestPlayerController>(NewPlayerController))
    {
        FString Alias = UGameplayStatics::ParseOption(Options, TEXT("Alias")).Trim();
        if (Alias.Len() == 0 || AliasMap.Find(Alias))
        {
            return Rs;
        }

        RTPC->SetPlayerAlias(Alias);
        
        PlayerData Data;
        Data.Alias = Alias;
        AliasMap.Add(Alias, Data);
    }

    return Rs;
}

3,GameMode::PostLogin(),登陆完成后的回调函数,创建角色可以放在这个函数中处理。

void AReplTestGameMode::PostLogin(APlayerController* NewPlayer)
{
    Super::PostLogin(NewPlayer);

    if (GetNetMode() == NM_DedicatedServer)
    {
        PlayerCount++;
        if (CharClass)
        {
            if (AReplTestCharacter* Player = GetWorld()->SpawnActor<AReplTestCharacter>(CharClass, SpawnLoc, SpawnRot))
            {
                PlayerList.Add(Player);

                /*if (AMyAIController* AIC = GetWorld()->SpawnActor<AMyAIController>(SpawnLoc, SpawnRot))
                {
                    AIC->Possess(Player);
                }*/

                Player->SpawnDefaultController();

                //设置角色的显示名称
                if (AReplTestPlayerController* RTPC = Cast<AReplTestPlayerController>(NewPlayer))
                {
                    if (PlayerData* Data = AliasMap.Find(RTPC->PlayerAlias()))
                    {
                        Player->SetAlias(*(Data->Alias));
                        Data->RTC = Player;

                        if (AMyAIController* AIC = Cast<AMyAIController>(Player->GetController()))
                        {
                            AIC->SetPlayerAlias(Data->Alias);
                        }
                    }
                }
            }
        }
    }
}

4,Character::BeginPlay(),当创建的Character进入场景时的回调函数,绑定摄像机的逻辑可以放在这个函数中

void AReplTestCharacter::BeginPlay()
{
    Super::BeginPlay();
    if (ROLE_SimulatedProxy == Role && NM_Client == GetNetMode())
    {
        if (APlayerController* PC = GetWorld()->GetFirstPlayerController())
        {
            if (AReplTestPlayerController* RTPC = Cast<AReplTestPlayerController>(PC))
            {
                //因为一个客户端首次加载时会有多个玩家的角色进入场景,这里判断哪个角色才是当前客户端的
                if (Alias_ == RTPC->PlayerAlias())
                {
                    PC->SetViewTarget(this);
                    RTPC->SetSimulatedCharacter(this);
                }
            }
        }
    }
}

5,客户端判断鼠标点击事件,这里加了一个保护,如果鼠标前后两次点击的坐标距离相差小于120,则不向服务端发送位移请求,防止频繁点击时发送消息太频繁。

void AReplTestPlayerController::MoveToMouseCursor()
{
    // Trace to see what is under the mouse cursor
    FHitResult Hit;
    GetHitResultUnderCursor(ECC_Visibility, false, Hit);

    if (Hit.bBlockingHit)
    {
        if (FVector::Dist(LastDestLoc, Hit.ImpactPoint) > 120)
        {
            LastDestLoc = Hit.ImpactPoint;
            SetNewMoveDestination(Hit.ImpactPoint);
        }
    }
}

6,服务端处理Move请求的函数,ServerMoveToDest_Validate判断请求的逻辑是否合法:Pawn是不是当前客户端操控的角色,防止操控其他玩家的角色。

bool AReplTestPlayerController::ServerMoveToDest_Validate(APawn* Pawn, const FVector DestLocation)
{
    //判断请求是否非法,不允许当前客户端操控其他客户端的角色
    bool Rs = false;
    if (AReplTestCharacter* RTC = Cast<AReplTestCharacter>(Pawn))
    {
        Rs = RTC->Alias() == PlayerAlias();
    }
    return Rs;
}

void AReplTestPlayerController::ServerMoveToDest_Implementation(APawn* Pawn, const FVector DestLocation)
{
    if (AMyAIController* AIC = Cast<AMyAIController>(Pawn->GetController()))
    {
        AIC->MoveToLocation(DestLocation);
    }
}

注意事项:

1,Replicated属性只能在服务端修改
Replicated属性只允许服务端修改后通知客户端,而不允许客户端修改Replicated属性后通知到服务端。 参考:Only the server can replicate variables or multicast events to all clients
https://answers.unrealengine.com/questions/459423/change-variable-in-client-want-server-to-see-it-bu.html

2,Server或者Client函数参数只能是指针或者引用,而不能是对象。
比如:假设参数是FString,那么必须是引用:const FString& Str,而不能是FString对象。

3,HUD的构造函数在服务端也会执行,但是DrawHUD()函数不会在服务端执行。
也就是说你要在HUD中判断当前程序是客户端还是服务端,可以不用考虑DrawHUD()函数。

凡是只需要客户端执行的逻辑,比如创建材质、修改颜色、加载贴图等等,一定要将这些逻辑单独封装成函数、且不要和数据更新的逻辑混在一起、且要确保这些函数只在客户端执行。
如何确保某个函数只在客户端执行:最直接最安全的方式是直接判断if(ENetMode::NM_Client == GetNetMode())。优雅一点的方式是通过Client function修饰这些逻辑函数,前提是你能确保这些函数所在的对象一定拥有了服务端连接。

4,当客户端登陆成功后,客户端所有的对象都会被重置,登陆前设置的对数属性值将变为默认值。
比如在启动游戏后登陆之前,你给PlayerController上的string属性设置为"abc",那么登陆成功后,这个属性值就变成了空字符串。

5,GameMode、AIController是为服务端设计的,PlayerController是为客户端设计的。两者虽然在客户端和服务端都可访问,但是前者是在服务端创建的,后者是在客户端创建的,比如PlayerController::GetHitResultAtScreenPosition只在客户端生效,而GameMode::PostLogin()只在服务端生效。

6,服务端的AIController::MoveToLocation()的效果相当于客户端UNavigationSystem::SimpleMoveToLocation()。AIController::MoveToLocation()内部有去调用NavigationSystem相关的接口。

7,客户端的PlayerController可以不用Possess玩家角色,因为客户端相关数据都是以服务端为准,操作角色也是在服务端完成,一般只需要对该角色绑定摄像机即可。Possess的意义之一是为了给 Pawn 赋予访问服务端函数的权限。

8,如果_Validate()函数返回false,则服务器会会认为客户端非法,并主动断开该客户端。断开客户端时服务端会打印:

LogRep: Error: ReceivedRPC: RPC_GetLastFailedReason: XXXX_Validate
LogNet: Error: UActorChannel::ProcessBunch: Replicator.ReceivedBunch failed.  Closing connection. RepObj: ReplTestPlayerController /Game/TopDownCPP/Maps/TopDownExampleMap.TopDownExampleMap:PersistentLevel.ReplTestPlayerController_1, Channel: 2

9,当前客户端只能获取当前控制角色的Controller,无法获取其他客户端的Controller,比如玩家A在玩家B的角色为C1,那么调用C1->GetController()时返回NULL。

10,动画同步
角色X在客户端A播放一个攻击动作,并且角色X在客户端B的视野内,此时需要客户端B也能同步看到角色X的动画,流程如下:
客户端发送请求 -》 服务端执行函数(假设叫ServerPlayAnim()) -》 ServerPlayAnim函数内调用NetMulticast函数 -》 NetMulticast函数内执行播放动画的逻辑。

11,编辑器模式下,即使勾选了 Run Dedicated Server,默认是不会自动连接专用服务器的,如果希望游戏启动时就自动连接专用服务器,需要勾选:Auto Connect to Server (Project Settings -> Level Editor -> Play -> Multiplayer Options),如果你自己实现了登陆逻辑,那么就不要勾选这个。如果是为了方便测试跳过登陆,可以勾选这个选项,并实现对应逻辑。

12,当客户端向服务端传递Rotator时,如果Rotator的值范围为:-180到180,那么传递到服务端时会被自动修改为:0到360。(v4.22)

13,此条内容可以无视,只是个人的备忘录。因为个人还在研究中,未找到正确方案
在服务端创建的Actor,如何让其复制到客户端、或者禁止复制到客户端?
3种情况:

  • 只存在于服务端,不复制到客户端

      Actor->SetReplicates(false);
      Actor->SetAutonomousProxy(false);
      Actor->bNetUseOwnerRelevancy = false;
      Actor->SetOwner(nullptr);
    
  • 存在于服务端,并复制到客户端,但客户端服务端均无权限调用对方的远程函数

      Actor->SetReplicates(true);
      Actor->SetAutonomousProxy(false);
      Actor->bNetUseOwnerRelevancy = false;
      Actor->SetOwner(拥有服务端连接的对象);
    
  • 存在于服务端,并复制到客户端,客户端服务端都拥有权限调用对方的远程函数

      Actor->SetReplicates(true);
      Actor->SetAutonomousProxy(true);
      Actor->bNetUseOwnerRelevancy = true;
      Actor->SetOwner(拥有服务端连接的对象);
    

14,打包版本的相关回调函数触发先后顺序问题

打包版本和PIE模式的Dedicated Server存在很大差异,比如:Actor的BeginPlay函数触发先后顺序,在PIE模式下,服务端生成的Actor,对应的客户端Actor的回调函数执行时机是固定的;而打包版本则不同,服务端生成的Actor,对应的客户端Actor的回调函数执行时机是随机的。所以建议两条:

  1. 完成的功能尽早打包,并以打包版本为准,在PIE模式下测试没有问题,很可能在打包版本下是截然不同的效果。PIE模式下,多关注下控制台的log,看看相关警告,一些隐藏问题可能已经有log提示。
  2. 服务端客户端交互时,两者均不要依赖引擎的回调函数去通知上层逻辑,而是主动抛出通知。比如:客户端中,Actor A 初始化时需要Actor B的数据,那么不要在Actor A的BeginPlay函数去获取Actor B,也不要Actor B的BeginPlay函数中去通知Actor A;而是当Actor A初始化完毕后,主动向中心管理器(比如GameMode或者PlayerController等)发送通知,表明自己已经初始化完毕,当Actor B初始化完毕时亦如此,然后当中心管理器中接收到两者都已初始化完毕的通知后,再主动通知Actor A,告诉他,你现在可以获取Actor B了。

PlayerController为例,假设在服务端的AGameModeBase::PostLogin(APlayerController* NewPlayer)函数创建了一个开启了同步的Actor A,在PIE模式下,客户端的PlayerController::BeginPlay的触发时刻,要先于Actor A的客户端BeginPlay执行,而在打包版本下执行顺序则相反。

15,服务端创建的对象,在客户端的BeginPlay函数触发时,GetOwner()可能为空

不要在客户端的BeginPlay中获取Owner,有时候为空,有时候却不为空:PIE模式下不为空,但是打包版本不确定。

Owner发生变化时,AActor::OnRep_Owner()会被触发。AActor::OnRep_Owner()只在客户端触发。

16,NM_Client模式下,UGameplayStatics::GetGameMode()返回为null,即使参数WorldContext有效;UGameplayStatics::GetGameInstance()表现正常,可以获取到有效的GameInstance对象。

常见问题:

  1. 如果出现以下错误,表示Reliable函数的参数名和引擎生成的代码有同名的情况,把参数名重新改一下即可。

    error : Function parameter: ‘Pawn’ cannot be defined in ‘ServerMoveToDest’ as it is already defined in scope ‘Controller’ (shadowing is not allowed)

  2. 如果服务端SpawnActor时返回NULL,且参数传递都正确,可能是服务端上的对应Actor未清理,比如客户端崩掉了,导致了服务端的Actor未即时清理。

  3. 执行UNavigationSystem::SimpleMoveToLocation(MyCharacter()->GetController(), Location);时位移无效(假设已经生成了NavMesh)。原因是MyCharacter是在客户端生成的,客户端PlayerController传递给NavigationSystem()时执行无效,需要在服务端Spawn这个Character,然后再给其Spawn出一个AIController并Possess。官方模版项目,传递给NavigationSystem的是PlayerController且位移有效,是因为模版项目中设置的DefautlPawnClass,其实是服务端Spawn出来的Character,执行位置也是在服务端。

  4. 在打包版本下,客户端的玩家行走时,摄像机有抖动和卡顿(编辑器模式下正常),原因是没有勾选SpringArmComponent的Lag属性:Enable Camera Lag、Enable Camera Rotation Lag

    Dedicated Servers, Jitter, Matchmaking
    https://forums.unrealengine.com/development-discussion/c-gameplay-programming/96598-dedicated-servers-jitter-matchmaking

  5. 旋转视角时,NM_Standalone 模式下执行 APlayerController::AddYawInput() 或者 APawn::AddControllerYawInput() 没有问题,但是 NM_Client 模式下失效。
    原因:
    可能是Use Controller Rotation Yaw设置成了false。
    解决办法:
    有些情况下,我们不希望使用 Controller Rotation Yaw 作为角色的朝向,那么此时想旋转角色方向,使用如下方式:

     void AMyPlayerController::Turn(float Rate)
     {
         // calculate delta for this frame from the rate information
         if (Rate != 0.f)
         {
             if (AActor* Target = GetViewTarget())
             {
                 Target->AddActorWorldRotation(FRotator(0.f, Rate * InputYawScale, 0.f));
             }
         }
     }
    

    Client连接Dedicated Server的情况下,Client执行APlayerController::AddYawInput()失效,是因为此时Client的PlayerController没有Possess对应的Character。如果在服务端使用PlayerController对Character执行Possess,那么就可以使用APlayerController::AddYawInput()控制摄像机转向,以及使用APawn::AddMovementInput()控制玩家移动。但是要注意,Possess之后,该Character就拥有了访问服务端函数的权限。

  6. 假设角色蓝图中有两个Component:A 和 B,A 设置为 A->SetOnlyOwnerSee(true),且 B 附加在 A 上: B->SetupAttachment(A),那么只能第一个登陆的角色正常,之后登陆的角色会消失不见。
    解决办法:
    这种情况下,A 不要 Attach B,如果要 Attach,B 设置为 SetOnlyOwnerSee(false)(默认为false);或者 A 也隐藏掉。

  7. 如果角色身上 Attach 了一个 BoxComponent 或者 SphereComponent,那么这个Component的 CollisionProfileName 不要设置为 Projectile(First Person Shooter模版项目中的自定义 Collision Channel),(能否设置为其他没试过,最好默认),如果设置成 Projectile,那么角色在移动时会不停抖动(Standalone是否也有这种问题没试过)。

  8. DedicatedServer 模式下,SpawnActor生成出的 Actor,且这个Actor bReplicates 设置为true, 执行ConditionalBeginDestroy()时,一定时间后客户端会崩掉。
    解决办法:
    SpawnActor生成出的 Actor,且改 Actor 同步到远程机器,销毁时,不要用ConditionalBeginDestroy(),而要用Destroy()。Standalone 模式貌似没这种限制。
    崩溃日志:

     Assertion failed: Index >= 0 [File:D:\Build\++UE4+Release-4.16+Compile\Sync\Engine\Source\Runtime\CoreUObject\Public\UObject/UObjectArray.h] [Line: 455] 
    
     UE4Editor_Core!FDebug::AssertFailed() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\core\private\misc\assertionmacros.cpp:349]
     UE4Editor_Engine!UNetDriver::ServerReplicateActors_BuildConsiderList() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\engine\private\networkdriver.cpp:2600]
     UE4Editor_Engine!UNetDriver::ServerReplicateActors() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\engine\private\networkdriver.cpp:3159]
     UE4Editor_Engine!UNetDriver::TickFlush() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\engine\private\networkdriver.cpp:355]
     UE4Editor_Engine!TBaseUObjectMethodDelegateInstance<0,UNetDriver,void __cdecl(float)>::ExecuteIfSafe() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\core\public\delegates\delegateinstancesimpl.h:858]
     UE4Editor_Engine!TBaseMulticastDelegate<void,float>::Broadcast() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\core\public\delegates\delegatesignatureimpl.inl:937]
     UE4Editor_Engine!UWorld::Tick() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\engine\private\leveltick.cpp:1506]
     UE4Editor_UnrealEd!UEditorEngine::Tick() [d:\build\++ue4+release-4.16+compile\sync\engine\source\editor\unrealed\private\editorengine.cpp:1633]
     UE4Editor_UnrealEd!UUnrealEdEngine::Tick() [d:\build\++ue4+release-4.16+compile\sync\engine\source\editor\unrealed\private\unrealedengine.cpp:386]
     UE4Editor!FEngineLoop::Tick() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\launch\private\launchengineloop.cpp:3119]
     UE4Editor!GuardedMain() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\launch\private\launch.cpp:166]
     UE4Editor!GuardedMainWrapper() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\launch\private\windows\launchwindows.cpp:134]
     UE4Editor!WinMain() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\launch\private\windows\launchwindows.cpp:210]
     UE4Editor!__scrt_common_main_seh() [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl:253]
     kernel32
     ntdll
    
  9. USkeletalMeshComponent::SetSkeletalMesh()UStaticMeshComponent::SetStaticMesh()不会Replicate。
    在服务端执行这两个函数并不会将修改同步到客户端,官方推荐方式是:修改Mesh的时候,向客户端发送消息,在客户端修改Mesh。这个bug从4.7版本就出现了,且官方也不准备修复,因为不建议在服务端修改Mesh,毕竟只是显示相关的功能。
    跟显示相关的逻辑尽量全部放在客户端,包括创建SkeletalMeshComponent的逻辑。
    参考:change skeletal mesh, blueprint, multiplayer
    https://answers.unrealengine.com/questions/328659/change-skeletal-mesh-blueprint-multiplayer.html?sort=oldest

  10. Client function 和 Multicast function的可执行时机
    同一帧内:在服务端创建的Actor,SpawnActor之后,接着执行SetOwner,然后调用Client function,客户端函数就可以触发。
    但是如果调用 Multicast function,只会在服务端触发,客户端不会触发。什么时候调用 Multicast function才会在客户端触发?
    答案:该Actor在客户端的BeginPlay()函数触发的时候,也就是要等这个服务端创建的Actor在客户端Replicate完成之后。

  11. 使用ClientTravel连接远程服务器,连接成功后,如果客户端没有被服务器通知切换场景,那么当前客户端的GameInstanceGameMode不会被重置,游戏客户端启动时的对象,比如PlayerController会被重置(触发BeginPlay函数)。 若服务端通知客户端切换场景,除GameInstance之外,其他对象都会被重置,所以如果有些数据需要所有场景都公用的,可以放在GameInstance,否则就必须在各个游戏场景中重新设置这些数据。

  12. 如何让Replicated属性同步到所有客户端?
    默认情况下,UPROPERTY(Replicated)标识的属性只会同步给自己的Owner,可以理解为:只同步给Actor所属的客户端,其他客户端连上来,获取不到这个属性。
    如何让Replicated属性同步给所有客户端?执行:bOnlyRelevantToOwner = false;。默认为true,表示只同步给自己的Owner。

官方文档说在运行时期间修改bOnlyRelevantToOwner对其他客户端无效,需要其他客户端重连才会生效。官方文档不一定准确,待验证。。

bOnlyRelevantToOwner的另一个用途:服务端启动时(比如:GameMode的BeginPlay函数中)创建的对象,即使bReplicates = true;,客户端连上来之后,这些对象不会同步到客户端,如何让客户端登陆成功后自动获取到服务端中已经存在的对象?答案:将这些服务端对象的bOnlyRelevantToOwner设置为false。

bAlwaysRelevantbOnlyRelevantToOwner关系:
bOnlyRelevantToOwner = false;时,Replication的距离裁剪仍然会执行,但bAlwaysRelevant = true;时,则Actor将全场景同步,所以需要严格限制bAlwaysRelevant = true;的Actor数量。

  1. Replicated属性如何选择性的同步给指定客户端?
    使用DOREPLIFETIME_CONDITION修饰同步属性,比如COND_SkipOwner就表示同步给除Owner以外的所有客户端。

    DOREPLIFETIME_CONDITION(AWeaponModule, CurrWeapon, COND_SkipOwner);
    
  2. 如果一个Actor没有SetRootComponent,且没有继承蓝图(蓝图会默认创建一个DefaultComponent作为RootComponent),即使开启同步,也不会同步到客户端。
    如果Actor不需要任何Component,建议在给其创建一个空白Component作为RootComponent。

    AMyActor::AMyActor()
    {
        bReplicates = true;
    
        EmptyRootComp = CreateDefaultSubobject<USceneComponent>(TEXT("EmptyRootComp"));
        if (EmptyRootComp)
        {
            SetRootComponent(EmptyRootComp);
        }
    }
    

    建议为没有Component的纯C++ Actor创建一个RootComponent,否则AActor::IsNetRelevantFor()始终返回false,会跳过Replication的距离裁剪,增加同步开销。

  3. 如果一个Actor开启了同步,但又期望Actor的Component只在DedicatedServer中创建,这种需求无法实现,因为Actor开启同步后,如果只在Server端创建Component,此时Component会被自动置空。

  4. 服务端的SkeletalMesh上Socket坐标和客户端不同步的问题。
    问题现象:
    客户端服务端分别获取的GetSocketTransform()值不一致,有很大的误差。
    可能原因:
    1,服务端的Bone Fresh关闭了。
    解决办法:

    Mesh->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones;
    

    2,动画状态不同步,比如:客户端的当前动画是持手枪,但是服务端的当前动画是持狙击。
    解决办法:
    切换动画时确保客户端服务端同时执行。

  5. 射击游戏中,如果子弹是服务端生成,并同步到客户端,并且子弹是真实弹道(不是射线检测),并且在子弹的Hit事件中Destroy自己,那么当子弹速度过快时,且发射口离障碍物很近时,子弹还没来得及同步到客户端,子弹在服务端就已经消掉掉了,从而导致Hit事件只会在服务端触发,不会在客户端触发。而当发射口离障碍物有一定距离时(距离取决与子弹速度),客户端和服务端的Hit会同时触发。

  6. 销毁Actor时(接口:bool AActor::Destroy(bool bNetForce = false, bool bShouldModifyLevel = true)),如果需要将bNetForce设置为true才能销毁(这种情形用在强制销毁客户端的actor),则说明DS有内存泄漏问题。因为bNetForce只是将客户端的actor销毁,服务端的actor内存还没释放。为了客户端表现,可以提前Destroy(true);销毁客户端actor,但对应的服务端actor一定也要有Destroy();

示例工程下载地址

这个工程涉及内容少一些,适合UE4初学者研读。(里面有些API可能已经过时,以文章内容为准)
http://pan.baidu.com/s/1o7MzmRo

如果对UE4有一定了解,可以研读下面工程,涵盖了UE4 Dedicated Server的关键知识点。(这个工程是我在2017年参加阿姆斯特丹某游戏工作室的笔试后整理的笔记)
https://github.com/dawnarc/ue4_fps_game

参考文章

属性同步:
http://blog.csdn.net/yangxuan0261/article/details/54766955

RPC:
http://blog.csdn.net/yangxuan0261/article/details/54766951

How To Test Dedicated Server Games Via Commandline
https://wiki.unrealengine.com/How_To_Test_Dedicated_Server_Games_Via_Commandline

Networking and Multiplayer
https://docs.unrealengine.com/en-us/Gameplay/Networking/Replication

UE4基础:客户端服务器连接流程
http://wangjie.rocks/2019/03/29/ue4-client-ds-connect/

Unreal Networking Guide Created by Zach Metcalf
http://www.zachmetcalfgames.com/wp-content/uploads/2014/12/zmg_Unreal_Networking_Guide.pdf

深入浅出UE4网络
https://www.cnblogs.com/leonhard-/p/6511821.html

Multiplayer in Unreal Engine: How to Understand Network Replication
https://www.youtube.com/watch?v=JOJP0CvpB8w


天下事,成于精而败于众,立于诚而倾于奢。