玩命加载中 . . .

UE4网络编程(UE4++)


许久未见我又开始开工了,这次主要是近期在学习的UE4网络编程,注意此篇文章是翻译自某个PPT,但是是我自己读完后结合自己的理解翻译的,中间可能会有错误(比如map翻译为映射但是实际是地图等类似错误),欢迎大家指正。

Precontext

首先阐述下,UE4的网络编程是典型的(标准的)C/S模型,这就意味着服务器才是权威,并且所有的数据都必须首先从客户端送到数据,接下来服务器会判断那些数据可用并且根据编写的代码对对应数据进行反应。

同时,永远不要相信客户端,因为一旦你相信了他,那么服务器就不会对客户端裁决(反应)前进行检查,这将会导致外挂等欺骗服务器的行为。

Framework & Network

一般来说,在UE4的网络游戏编程中,我们通常分为下面四种framework

  1. 服务器独有:这些对象只会存在于服务器
  2. 服务器和客户端共有:这些对象将会存在与服务器和所有与之连接的客户端
  3. 服务器和某个客户端:这些对象只会存在于服务器和某个客户端(比如LOL里面你玩的英雄,那么一些关于你的英雄的某些asset之类的就是你和服务器独有的)
  4. 客户端独有:这些对象只有在某个客户端中独有

这里所说的某个客户端指的是具有某个Actor所有权的玩家/客户端。而保证“所有权”在后续的RPCs中十分重要,下面有两图用来那些表达那些公共类存在于那个framework中

图0:一些重要的类在网络框架中的分布

image-20200521110906114

图1:对象的类在网络框架中的分布

image-20200521111045360

上面的图1有一个概念需要阐述:公共类,但是这个需要在后面讲到actor的replication才会更清晰(对于actor来说,Tick,Replication和Spawn是三大基本属性,分别对应有了心跳,复制和生生死死)

Game Mode

在网络框架GameMode Class一般来说会分为GameModeBase和GameMode。其中GameModeBase只有很少的细节,因为一些游戏可能并不需要老的GameMode Class中的所有细节。

而对于AGameMode通常用来定义规则,这也包括了使用的类例如Apawn,PlayerController,APlayerState等其他更多。注意这个只能用于服务器,客户端是没有GameMode这个对象的,如果本地客户端想要或得GameMode,只会获得一个nullptr

/* Header file of our GameMode Child Class inside of the Class declaration */
// Maximum Number of Players needed/allowed during this Match
int32 MaxNumPlayers;
// Override Implementation of ReadyToStartMatch
virtual bool ReadyToStartMatch_Implementation() override;

/* CPP file of our GameMode Child Class */
bool ATestGameMode::ReadyToStartMatch_Implementation() {
    Super::ReadyToStartMatch();
    return MaxNumPlayers == NumPlayers;
}
/* Header file of our GameMode Child Class inside of the Class declaration */
// List of PlayerControllers
TArray<class APlayerController*> PlayerControllerList;

// Overriding the PostLogin function
virtual void PostLogin(APlayerController* NewPlayer) override;
/* CPP file of our GameMode Child Class */

void ATestGameMode::PostLogin(APlayerController* NewPlayer) {
    Super::PostLogin(NewPlayer);
    PlayerControllerList.Add(NewPlayer);
}
/* Header file of our GameMode Child Class inside of the Class declaration */
// Maximum Number of Players needed/allowed during this Match
int32 MaxNumPlayers;
// Override BeginPlay, since we need that to recreate the BP version
virtual void BeginPlay()override;
/* CPP file of our GameMode Child Class */
void ATestGameMode::BeginPlay() {
    Super::BeginPlay();
    // 'FCString::Atoi' converts 'FString' to 'int32' and we use the static 'ParseOption' function of the
    // 'UGameplayStatics' Class to get the correct Key from the 'OptionsString'
    MaxNumPlayers = FCString::Atoi( *(UGameplayStatics::ParseOption(OptionsString, “MaxNumPlayers”)) );
}

Game State

GameState类一般来说会AGameStateBase和AGameState。

一般来说GameStateBase会因为 细节会少得多, 因为一些游戏可能并不需要老的GameState Class中的所有细节。

AGameState这个类是在服务器和客户端里信息共享中最重要的类了。GameState一般用来跟踪游戏的当前状态。在对于多人游戏来说,最重要的包括了连接这些游戏的玩家或者其他东西。

这些游戏状态会被复制到所有的连接的客户端中。因此每一个连接的客户端都能够访问他们,因此GameState这类也是在多人游戏中处于中心位置。

例如游戏模式告诉你要鲨掉多少人才能赢的时候,GameState就会持续追踪当前的个人或者团队击杀数。而其中的信息存储方式则完全由你自己个人组织,这或许是一个分数数组,或者是一个用户结构体数组用来追踪你或你团队的信息。

void APlayerState::PostInitializeComponents() {
    []
    UWorld* World = GetWorld();
    // Register this PlayerState with the Game's ReplicationInfo
    if(World->GameState != NULL) {
    World->GameState->AddPlayerState(this);
    }
    []
}

void AGameState::PostInitializeComponents() {
    []
    for(TActorIterator<APlayerState> It(World); It; ++It) {
        AddPlayerState(*It);
    }
}

void AGameState::AddPlayerState(APlayerState* PlayerState) {
    if(!PlayerState->bIsInactive) {
        PlayerArray.AddUnique(PlayerState);
    }
}

比如我们想要传递一个比赛的实时分数,我们可以这么写代码

/* Header file of our GameState Class inside of the Class declaration */
// You need this include to get the Replication working. Good place for it would be your Projects Header!
#include “UnrealNetwork.h”

// Replicated Specifier used to mark this variable to replicate
UPROPERTY(Replicated)
    int32 TeamAScore;
UPROPERTY(Replicated)
    int32 TeamBScore;

// Function to increase the Score of a Team
void AddScore(bool TeamAScored);

/* CPP file of our GameState Class */
// This function is required through the Replicated specifier in the UPROPERTY Macro and is declared by it
void ATestGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const {
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(ATestGameState, TeamAScore);
    DOREPLIFETIME(ATestGameState, TeamBScore);
}

/* CPP file of our GameState Class */
void ATestGameState::AddScore(bool TeamAScored) {
    if(TeamAScored)
        TeamAScore++;
    else
        TeamBScore++;
}

Player State

APlayerState对于辨别特定的玩家来说是最重要的类,这个类能够持有玩家的当前信息。每一个玩家都有他们自己的独有的PlayerState,当然这些PlayerState也能够复制到每一个客户端,且每一个客户端都能够显示这些playerState(就是说你安妮出了5个装备,那么在其他玩家的计分板里面也会在你的信息一栏中出现5个装备)。一个简单访问玩家状态的方法是GameState类里面的玩家数组(类似于你在LOL里面的计分板:里面有每个召唤师的名字,所使用的英雄,当前KDA,击杀小兵,出的装备等)。

/* Header file of our PlayerState Child Class inside of the Class declaration */
// Used to copy properties from the current PlayerState to the passed one
virtual void CopyProperties(class APlayerState* PlayerState);
// Used to override the current PlayerState with the properties of the passed one
virtual void OverrideWith(class APlayerState* PlayerState);

/* CPP file of our PlayerState Child Class */
void ATestPlayerState::CopyProperties(class APlayerState* PlayerState) {
Super::CopyProperties(PlayerState);
    if(PlayerState) {
        ATestPlayerState* TestPlayerState = Cast<ATestPlayerState>(PlayerState);
        if(TestPlayerState)
            TestPlayerState->SomeVariable = SomeVariable;
    }
}

void ATestPlayerState::OverrideWith(class APlayerState* PlayerState) {
    Super::OverrideWith(PlayerState);
    if(PlayerState) {
        ATestPlayerState* TestPlayerState = Cast<ATestPlayerState>(PlayerState);
        if(TestPlayerState)
            SomeVariable = TestPlayerState->SomeVariable
    }
}

Pawn

APawn是一个让玩家实际控制的Actor,在大部分的时候都是一个人类character,但是他可以是任何东西。一个玩家在任意时刻仅可以操作一个pawn,但是他也可以实现多个pawn之间的切换。Pawn大部分时间都是在客户端之间进行复制。

而Pawn的一个子类ACharacter通常被拿来复制,因为他早就加载了一个适用于网络的MovementComponent一个能够处理玩家Character的位置,旋转等复制。

注意,并不是客户端在移动你的角色,所有的移动都是应当在服务器端进行的(数据来源于客户端),处理后再分发到各个客户端上。

virtual void PossessedBy(AController* NewController);
virtual void UnPossessed();

/* Header file of our Pawn Child Class, inside of the Class declaration */
// SkeletalMesh Component, so we have something to hide
class USkeletalMeshComponent* SkeletalMesh;
// Overriding the UnPossessed Event
virtual void UnPossessed() override;

/* Header file of our Pawn Child Class, inside of the Class declaration */
UFUNCTION(NetMulticast, unreliable)
    void Multicast_HideMesh();

/* CPP file of our Pawn Child Class */
void ATestPawn::UnPossessed() {
    Super::UnPossessed();
    Multicast_HideMesh();
}
// You will read later about RPC's and why that '_Implementation' is a thing

void ATestPawn::Multicast_HideMesh_Implementation() {
    SkeletalMesh->SetVisibility(false);
}

/* Header file of our Pawn Child Class, inside of the Class declaration */
// Replicated Health Variable
UPROPERTY(Replicated)
    int32 Health;


// Overriding the Damage Event
virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent,AController* EventInstigator, AActor* DamageCauser) override;

/* CPP file of our Pawn Child Class */
// This function is required through the Replicated specifier in the UPROPERTY Macro and is declared by it
void ATestPawn::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const {
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    // This actually takes care of replicating the Variable
    DOREPLIFETIME(ATestPawn, Health);
}

float ATestPawn::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent,AController* EventInstigator, AActor* DamageCauser) {
    float ActualDamage = Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser);
    Health -= ActualDamage; // Lower the Health of the Player
    if(Health <= 0.0) // And destroy it if the Health is less or equal 0
        Destroy();
    return ActualDamage;
}

Player Controller

APlayerController有可能是我们会接触到的最有趣但是也最复杂的一个类,这个类也是对于大量的客户端stuff来说属于中心地位,因为这是第一个客户端自己的类。

我们可以将PlayerController视作玩家的Input,这也连接了玩家和服务器,即每一个客户有一个其自己的PlayerController。且一个客户端PlayerController只存在于他自己(客户端)服务器,其他的客户端是无法知道你客户端的PlayerController。即

每一个客户端只知道他自己

这就导致了服务器拥有和他连接的所有客户端的PlayerController的引用(Ref)。

而术语Input指的是所有的实际Input(例如键盘某个键的按压松开,鼠标移动,控制轴等)都需要在PlayerController内陈列并处理。

注意:Input总是第一个经过PlayerController,然后再送到服务器,如果PlayerController不使用它,那么这个输入就可能会被其他的类使用,因此在需要的时候,我们可以停止消耗性输入(指的是用一次就丢的那种输入)。

并且我们就需要知道如何才能构建一个正确的PlayerController:通常来说最有名的是

GetPlayerController(0)
    //若括号内不是0而是其他数据,那么就不会返回其他的客户端,这个参数一般来说是使用在本地玩家
    //(分享屏幕之类的)

或者单独的一条语句

UGameplayStatics::GetPlayerController(GetWorld(), 0);

但是他们在服务器和客户端有一些不同,但是不是完全不同

  1. 在Listen-Server调用的时候,将会返回到Listen-Server的PlayerController
  2. 在客户端调用的时候,将会返回到客户端的PlayerController
  3. 在Dedicated Server调用的时候,将会返回到第一个客户端的PlayerController

至于为什么PlayerController对于RFC如此重要,这里只能简短陈述:对于所有的空间只能够存在于客户端和Listen-Server并且仅属于客户端自己,而来自服务器的RPC在服务器并没有实例来运行这个RPC,而且很容易知道,PlayerController是不复制的,这也意味着需要一个方法构建一个“按压键位”到服务器然后改变所对应的变量的一个映射。

而什么不适用RPC在GameState的原因很简单,GameState的所有权在于服务器。

/* Header file of our PlayerController Child Class, inside of the Class declaration */
// Server RPC. You will read more about this in the RPC Chapter
UFUNCTION(Server, unreliable, WithValidation)
void Server_IncreaseVariable();

// Also overriding the BeginPlay function for this example
virtual void BeginPlay() override;

/* Header file of our GameState Child Class, inside of the Class declaration */
// Replicated Integer Variable
UPROPERTY(Replicated)
    int32 OurVariable;
public:
// Function to Increment the Variable
void IncreaseVariable();


/* CPP file of our PlayerController Child Class */
// Otherwise we can't access the GameState functions
#include “TestGameState.h”
// You will read later about RPC's and why that '_Validate' is a thing
bool ATestPlayerController::Server_IncreaseVariable_Validate() {
    return true;
}

// You will read later about RPC's and why that '_Implementation' is a thing
void ATestPlayerController::Server_IncreaseVariable_Implementation() {
    ATestGameState* GameState = Cast<ATestGameState>(UGameplayStatics::GetGameState(GetWorld()));
    GameState->IncreaseVariable();
}

void ATestPlayerController::BeginPlay() {
    Super::BeginPlay();
    // Make sure only the Client Version of this PlayerController calls the ServerRPC
    if(Role < ROLE_Authority)
        Server_IncreaseVariable();
}

/* CPP file of our GameState Child Class */
// This function is required through the Replicated specifier in the UPROPERTY Macro and is declared by it
void ATestGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const {
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    // This actually takes care of replicating the Variable
    DOREPLIFETIME(ATestGameState, OurVariable);
}

void ATestGameState::IncreaseVariable() {
    OurVariable++;
}

HUD

AHUD是一个只提供给一个用户,并且可以被PlayerController访问的类且AHUD会被自动生成。

在UMG被释放之前,HUD类会在客户端的视口(viewport)绘制出文本,纹理等其他的东西。而在此处我们一般用Widgets(控件)来代替HUD,当然你仍然可以使用HUD来debug或者单独来处理这个类的生成,展示,隐藏和销毁Widget。

由于HUD类无法直接连接到网络,接下来的例子将只会体现在玩家,因此此处略过。

Widgets(UMG)

控件(widgets)一般用在Epic游戏的新UI系统(UMG),它继承于Slate(c++来创建在UE4中使用的UI的模组)。而控件一般来说仅在客户端本地(Listen-Server)。

Widgets不会被复制,并且总是需要一个分离,复制类来表现复制的动作。和上面HUD一样,这里我们依旧略过。

Dedicated vs Listen Server

  1. Dedicated Server

    Dedicated Server是一个不会被用户端请求的专用服务器。它被从游戏客户端分离出来运行并且运行在服务器上使得玩家能够随时加入或者离开。

    Dedicated Server能够在Windows或者Linux下被编译并且能够运行在虚拟服务器上使得来自不同的IP地址段的玩家都能加入进游戏。

    Dedicated Server没有虚拟的部分,英雌他不需要UI,也不需要PlayerController,也没有Character及游戏内其他类似的表达。

  2. Listen Server

    Listen-Server即是服务器也是客户端. 这就意味着总的服务器上总是至少有一个客户端连接。这个总是和服务器连接的客户端就是Listen-Server,并且若Listen-Server断开连接,则总服务器关闭;

    由于他也是一个客户端,所以Listen-Server也需要UI好PlayerController来表达它的客户端成分,通过在Listen-Server使用

        PlayerController(0) 

    这将会返回其特定客户端的PlayerController 。

    由于Listen-Server运行在它自己的客户端上,其IP也是其他的想要连接到这个listen-server的客户端的IP。在经过Dedicated Server比较后,这会经常导致网络用户没有其静态IP。但是使用OnlineSubsystem后可以解释这个问题(至于这个是什么东东后续由阐述)

Replication

复制(Replication)是在服务器传递信息或者数据给服务器的一个行为,这个行为对某些特定实体或者群有一定的限制。对于蓝图来说,其表达复制的方式是通过设定AActor的setting来表达。

拥有复制性质的第一个类是Actor,因此所有能够产生复制性质的类都是Actor的子类(都继承于Actor),虽然他们都不是以相同的方式来实现复制的,也有一些东西不会被复制,比如GameMode不会被复制并且仅存在于Server。

//如何使得一个类复制的设置
ATestCharacter::ATestCharacter() {
    // Network game, so let's setup Replication
    bReplicates = true;
    bReplicateMovement = true;
}

使用UE4++来设置是否可被复制见下面代码

/* Header file inside of the Classes declaration */
// Create replicated health variable
UPROPERTY(Replicated)
    float Health;

/*cpp file*/
void ATestPlayerCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const {
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    // Here we list the variables we want to replicate + a condition if wanted
    DOREPLIFETIME(ATestPlayerCharacter, Health);
}

// Replicates the Variable only to the Owner of this Object/Class
DOREPLIFETIME_CONDITION(ATestPlayerCharacter, Health, COND_OwnerOnly);

下面有一些Condition来表示这些性质是作用在何处的:(翻译就偷懒一下)

Condition Description
COND_InitialOnlyActors This property will only attempt to send on the initial bunch
COND_OwnerOnly This property will only send to the Actor’s owner
COND_SkipOwner This property send to every connection EXCEPT the owner
COND_SimulatedOnly This property will only send to simulated Actors
COND_AutonomousOnly This property will only send to autonomous
COND_SimulatedOrPhysics This property will send to simulated OR bRepPhysics Actors
COND_InitialOrOwner This property will send on the initial packet, or to the Actor’s owner
COND_Custom This property has no particular condition, but wants the ability to toggle on/off via SetCustomIsActiveOverride

注意,所有的复制仅仅存在于从服务器到客户端,而不存在其他的任何渠道。我们也将在后续学习如果从服务器复制一些客户端想要分享给其他的方法。

接下来有一种不同的Replication方法被称为RepNotify。它使用一个函数,当接收到更新的值时,将对所有实例调用该函数。为了达到这个效果,你可以构建出你自己的逻辑(用蓝图)在接收到新的值之后调用这个函数。

/* Header file inside of the Classes declaration */
// Create RepNotify Health variable
UPROPERTY(ReplicatedUsing=OnRep_Health)
float Health;

// Create OnRep Function | UFUNCTION() Macro is important! | Doesn't need to be virtual though
UFUNCTION()
    virtual void OnRep_Health();

/* CPP file of the Class */
void ATestCharacter::OnRep_Health() {
    if(Health < 0.0f)
        PlayDeathAnimation();
}

Remote Procedure Calls(RPC)

其他的复制方式称为“RPC”,“Remote Procedure Call”的简称。它们用于在另一个实例上调用某些内容。这个就和你的电视遥控器远程操纵TV的方法差不多。

UE4使用这个方法让客户端向服务器,服务器向客户端或者服务器向客户端群调用函数。

注意,RPC是没有返回值的,若想要返回特定值,需要使用第二个RPC,但是这个方法只能有一些特定的方法。

  1. Run on Server – Is meant to be executed on the Server Instance of the Actor(在服务器上运行,意味着只能够被服务器实例化的Actor使用)
  2. Run on owning Client – Is meant to be executed on the Owner of this Actor(在个人客户单上运行,意味着只能被自己的Actor使用)
  3. NetMulticast – Is meant to be executed on all Instances of this Actor(在网络多播,意味着任意实例化的Actor均会被调用)

而要使的RPC完全发挥作用,需要满足下列要求(来自官方文档)

  1. They must be called from Actors. (只能由Actors发出)

  2. The Actor must be replicated. (Actor必须被复制)

  3. If the RPC is being called from Server to be executed on a Client, only the Client who actually owns that Actor will execute the function. (如果RPC能够被服务器调用且会被客户端使用,那么只有对应Actor对应的客户端能够使用这个函数)

  4. If the RPC is being called from Client to be executed on the Server, the Client must own the Actor that the RPC is being called on. (如果一个ROC已经被一个客户端调用且想要让服务器使用,那么客户端必须要拥有这个被RPC调用的Actor)

  5. Multicast RPCs are an exception: (但是多播是一个例外)

    1. If they are called from the Server, the Server will execute them locally, as well as execute them on all currently connected Clients. (如果有一个从服务器来到调用,那么服务器将会本地调用RPC,然后要求所有与之连接的客户端调用同一个RPC)
    2. If they are called from Clients, they will only execute locally, and will not execute on the Server. (如果是从客户端调用的RPC,那么只会本地执行,而不会在服务器执行)
    3. For now, we have a simple throttling mechanism for Multicast events
      1. A Multicast function will not replicate more than twice in a given. (注意这个多播函数不会被复制超过两次)
      2. Actor’s network update period. Long term, we expect to improve on this.(并且Actor的网络会周期性的更新,这是不好的,我们希望提高它的表现)

下面是从服务器发出的RPC的

Actor Ownership Not replicated NetMulticast Server Client
Client-owned Actor Runs on Server Runs on Server and all Clients Runs on Server Runs on Actor’s owning Client
Server-owned Actor Runs on Server Runs on Server and all Clients Runs on Server Runs on Server
Unowned Actor Runs on Server Runs on Server and all Clients Runs on Server Runs on Server

下面是从客户端发出的RPC的

Actor Ownership Not replicated NetMulticast Server Client
Owned by invoking Client Runs on invoking Client Runs on invoking Client Runs on Server Runs on invoking Client
Owned by a different Client Runs on invoking Client Runs on invoking Client Dropped Runs on invoking Client
Server-owned Actor Runs on invoking Client Runs on invoking Client Dropped Runs on invoking Client
Unowned Actor Runs on invoking Client Runs on invoking Client Dropped Runs on invoking Client

接下来是相关代码

#include “UnrealNetwork.h” 
// This is a Server RPC, marked as unreliable and WithValidation (is needed!)
UFUNCTION(Server, unreliable, WithValidation)
void Server_PlaceBomb();

//c++
//The CPP file will implement a different function. This one needs '_Implementation' as a suffix
// This is the actual implementation (Not Server_PlaceBomb). But when calling it, we use "Server_PlaceBomb"
void ATestPlayerCharacter::Server_PlaceBomb_Implementation() {
    // BOOM!
}
//The CPP file also needs a version with '_Validate' as a suffix. Later more about that
bool ATestPlayerCharacter::Server_PlaceBomb_Validate() {
    return true;
}

//The other two types of RPCs are created like this:
//Client RPC, which needs to be marked as 'reliable' or 'unreliable'!
UFUNCTION(Client, unreliable)
    void ClientRPCFunction();
//Multicast RPC, which also needs to be marked as 'reliable' or 'unreliable'!
UFUNCTION(NetMulticast, unreliable)
    void MulticastRPCFunction();
//we can also add the 'reliable' keyword to an RPC to make it reliable.
UFUNCTION(Client, reliable)
    void ReliableClientRPCFunction();

是否有效相关:关于有效性的idea指的是:若一个有效的RPC函数发现了这个参数是”bad“的,那么服务器辨别出来后将会和生成这个RPC对应的服务器或者客户端断开连接:

//Validation is required by now for every ServerRPCFunction. The 'WithValidation' keyword in the UFUNCTION Macro is used for that
UFUNCTION(Server, unreliable, WithValidation)
    void SomeRPCFunction(int32 AddHealth);

//Here is an example of how the '_Validate' Function can be used:
bool ATestPlayerCharacter::SomeRPCFunction_Validate(int32 AddHealth) {
    if(AddHealth > MAX_ADD_HEALTH) {
        return false; // This will disconnect the caller!
    }
    return true; // This will allow the RPC to be called!
}

注意:从客户端到服务器的RPC需要使用’_Validate’函数来启发安全的服务器来RPC函数,并使添加代码的人尽可能容易地根据所有已知的输入约束检查每个参数是否有效

Ownership

这是一个很重要的概念(正如你在前面的表格中看到的一样),首先我们先明确,只有服务器或者客户端才能够“拥有”一个Actor(反例就是PlayerController啦)。

而另外一个例子就是在一个场景中生成一个门,这个门的所有权在于服务器。

如果翻看上面的表格,那么你会发现如果一个客户端想要RPC一个不属于他的Actor的话,服务器会直接将其drop,因此,客户端是无法执行开门这个操作的,因为们的所有权是在服务器的,但是问题来了,如何开门呢?

我们可以使用一个Actor或者Class,使得一个客户端实际拥有这个门,这就是PlayerController开始崭露头角的地方了。因此我们在这里代替输入门的参数和请求一个服务器RPC,我们反而在PlayerController中创建服务器RPC然后使得一些调用门的接口函数(比如Interact())。如果门得到了正确的结构,那么就是调用一个被定义的逻辑,然后截取希望得到的正确的们的状态的复制。

注意:这个接口并不是多玩家游戏独有。

Actors and their Owning Connections

正如我们对提及的每一个类的总览,我们发现PlayerController是第一个拥有“owns”属性的类,这就意味着:

每个连接的客户端都有一个PlayerController,并且被特定的创建。这也可以这么说,一个PlayerController被一个“连接”拥有。因此我们可以确定的是,如果一个Actor能够被某个客户端拥有,那么我们实际上也能够查询到最多的外部所有者,如果这是PlayerController,则拥有PlayerController的连接也拥有这个Actor。

注意Pawn/Character,它们被 PlayerController所拥有,并且此时 PlayerController被一个Pawn所拥有。这意味着,拥有这个PlayerController的连接也拥有Pawn。

这只是当 PlayerController拥有Pawn时的情况;当PlayerController不拥有这个Pawnd时,客户端也不再拥有这个Pawn。

因此我们需要明确下列事实

  1. RPCs需要辨别按各一个用户需要执行这个Run-On-Client RPC
  2. Actor的重复制和相关性
  3. Actor在被owner涉及时的复制性质

当前已经了解到,当客户机/服务器调用RPC时,它们的反应不同,这取决于它们所属的连接。同时前文也阐述了有关条件复制的情况,其中(C++中)变量仅在一定条件下复制,那么接下来的主题将描述列表的相关部分。

Actor Relevancy and Priority

Relevancy

先引入一个栗子:想象一个具有足够大的关卡或者地图的一个游戏,玩家们能够做一些对于其他玩家不那么重要的事,在这种情况下,我们为什么要关注这些不重要的事情呢?

因此为了提高带宽,UE4的Network Code使我们的服务器能够仅告诉几个与某个行为相关的客户端,并且这些客户端构成了一个相关集。当然,对于这个相集,也有着以下的规则

  1. 如果Actor的状态是 ‘bAlwaysRelevant’(属于Pawn或者PlayerController)或者这个Pawn是某些行为的策划者,例如噪音或者损伤。则他们是相关的( If the Actor is ‘bAlwaysRelevant’, is owned by the Pawn or PlayerController, is the Pawn, or the Pawn is the Instigator of some action like noise or damage, it is relevant );

  2. 如果Actor是’bNetUserOwnerRelevancy’并且具有所有者(就是说这个actor有老大了), 则使用持有者相关性(If the Actor is ‘bNetUserOwnerRelevancy’ and has an Owner, use the Owner’s relevancy );

  3. 如果Actor是’bOnlyRelevantToOwner’并且没有通过第一次检查,则无关 ( If the Actor is ‘bOnlyRelevantToOwner’, and does not pass the first check, it is not relevant);

  4. 如果Actor依附于另一个Actor的Skeleton,则他们的关联性由其基的关联性(If the Actor is attached to the Skeleton of another Actor, then its relevancy is determined by the relevancy of its base );

  5. 如果Actor是隐藏状态 (即’bHidden == true’)并且和根组件没有冲突,则actor与之无关( if the Actor is hidden (‘bHidden == true’) and the root component does not collide then the Actor is not relevant );

    若没有根组件,则 ‘AActor::IsNetRelevantFor()’ 将记录一个警告,并询问是否应将设置为’bAlwaysRelevant = true’ ( if the Actor is hidden (‘bHidden == true’) and the root component does not collide then the Actor is not relevant . If there is no root component, ‘AActor::IsNetRelevantFor()’ will log a warning and ask if the Actor should be set to ‘bAlwaysRelevant = true’ );

  6. 如果将“AGameNetworkManager”设置为基于距离的关联,则Actor根据净剔除距离的远近来决定关联与否(If ‘AGameNetworkManager’ is set to use distance based relevancy, the Actor is relevant if it is closer than the net cull distance);

  7. 注意:由于Pawn和PlayerController重写“AActor::IsNetRelevantFor()”,因此关联条件不同(Pawn and PlayerController override ‘AActor::IsNetRelevantFor()’ and have different conditions for relevancy as a result)。

Prioritization

UE4使用的是平衡加载技术,即优先加载所有的Actors,并且给予每个Actor基于重要性的带宽。每一个Actor有一个浮点型变量称之为“NetPriority”——值越大,则会有更多的带宽将分配给这个。(当值为2时,其分配的带宽会是值为1的Actor的两倍)。

唯一一个能影响他们的优先级的东西就是Ratio(大概意思是各个所占的比率)。因此显而易见的是,你不能够通过增加他们的优先级提高UE4网络的效率 。

而UE4是通过

virtual void AActor::GetNetPriority() override;

来计算他们的当前优先级,为了防止饿死(详见OS),这个函数将会乘当前时间“NetPriority”每当这个Actor被复制。而GetNetPriority 这一函数也考虑了Actor和Viewer的相对位置和距离 。

bOnlyRelevantToOwner = false; 
bAlwaysRelevant = false; 
bReplicateMovement = true; 
bNetLoadOnClient = true; 
bNetUseOwnerRelevancy = false; 
bReplicates = true;

NetUpdateFrequency = 100.0f; 
NetCullDistanceSquared = 225000000.0f; 
NetPriority = 1.0f;

Actor Role and Remote Role

在这里,我们有着关于Actor复制的两个更重要的性质,而这两个性质告诉了我们:

  1. 谁有权(Authority)控制Actor;

    与所有权不同的是,要确定引擎的当前运行实例是否具有权限,需要检查Role的属性是否为Role_Authority。如果是,则引擎的此实例负责此Actor(无论是否复制)。

  2. Actor是否被复制;

    而Role和Remote Role之间是能够依靠谁(指的是服务器和客户端之间)检查相关的值来逆转,举个例子

    //if on the Server you have this configuration:
        if( Role == Role_Authority && RemoteRole = ROLE_SimulatedProxy ) 
    //Then the Client would see it as this:
        if( Role == ROLE_SimulatedProxy && RemoteRole == ROLE_Authority )

    这是有意义的,因为服务器负责Actor并将其复制到客户端。客户端只需要接收更新,并在更新之间模拟Actor即可。

  3. 复制Actor的模式(Mode,个人认为方法也行)

    服务器不会每次更新都更新Actor。这会占用太多的带宽和CPU资源。相反,服务器将以

    AActor::NetUpdateFrequency

    这一属性上指定Actor的复制频率。这意味着,在Actor更新之间,客户端上的一些时间将会过去。这可能会导致Actor在动作上显得零星或不稳定(就是你玩LOL在往前走但是你自己在不断的瞬息)。为了弥补这一点,客户端将在两次更新之间模拟Actor。其具备以下两种模拟

    1. ROLE_SimulatedProxy

      这是标准的模拟路径,通常基于基于最后已知速度的外推运动。当服务器发送特定Actor的更新时,客户端将调整Actor的位置,使其朝向新位置;然后在更新之间,客户端将根据服务器发送的最新速度来继续移动Actor(这只是一个栗子,我们完全可以自定义code来使用其他信息推断)。

    2. ROLE_AutonomousProx

      这通常只用于由玩家控制的Actor身上。这仅仅意味着这个Actor接收到来自PlayerController的输入,所以当我们外推时,我们有更多的信息,并且可以使用实际的PlayerController来填充丢失的信息(而不是基于最后已知的速度外推)。

Traveling in Multiplayer

Non-/Seamless travel

要理解Seamless和Non-seamless的不同实际上十分简单,Seamless指的是一种非阻塞操作,而Nonseamless则是一种阻塞式操作。

客户端的Non-seamless传输即——客户端从服务器断开连接,然后重新连接到同一个服务器,该服务器将随时准备好加载新map。Epic的官方文档建议尽可能频繁地使用Non-seamless travel,因为这将带来更流畅的体验,并且将避免重新连接过程中可能出现的任何问题。有三种方法能够达成这Non-seamless travel:

  1. 第一次加载地图时;
  2. 第一次作为客户端连接到服务器时;
  3. 想要结束多人游戏并开始新游戏时。

Main Traveling Functions

  1. UEngine::Browse(这里的map我个人认为是加载新地图,但是翻译为映射也是有道理的,所以如果有更好的翻译麻烦通知一下翻译的我)
    1. 就像加载map时的硬重置一样;
    2. 始终会导致“Non-seamless”传输;
    3. 将导致服务器在传输到目标map之前会断开当前客户端的连接;
    4. 客户端将断开与当前服务器的连接;
    5. 由于专用服务器无法传输到其他服务器,因此map必须是本地的(即不能是URL)
  2. UWorld::ServerTravel
    1. 仅限服务器;
    2. 当服务器跳转到新的world或者level时;
    3. 所有连接的客户端都将跟随这一转换;
    4. 这是多人游戏从一个地图到另一个地图的方式,服务器负责调用此函数;
    5. 服务器将会为所有连接的客户端调用“APlayerController::ClientTravel”。
  3. APlayerController::ClientTravel
    1. 如果从客户端调用,将转到新服务器;
    2. 如果从服务器调用,将指示特定客户机转到新地图(但保持与当前服务器的连接)。

Enabling Seamless Travel

Seamless传输一般来说都配置有过渡map。这是通过

UGameMapsSettings::TransitionMap

属性配置的。

默认情况下,此属性为空。如果您的游戏将此属性保留为空,则会为其创建一个空地图。过渡map存在的原因是,必须始终有一个已加载的世界(其中包含地图),因此在加载新地图之前,我们无法释放旧地图。由于map可能非常大,因此将新旧map同时存储在内存中十分糟糕,这就是过场图片的来源(比如DNF中你进入一个地下城之前的那个加载图片)。

一旦您想设置了过场图片,您就设置下列

AGameMode::bUseSeamlessTravel = true;

然后从这里开始就可以Seamless传输,当然,这也可以通过游戏模式蓝图和“地图和节点”选项卡中的项目设置来设置。

Persisting Actors / Seamless Travel

当使用Seamless Travel时,可以将Actor从当前Level(持久化)转移到新Level上。这对某些Actor很有用。在默认情况下,以下这些Actor将自动保持不变:

  1. GameMode Actor(仅限服务器):通过

    AGameMode::GetSeamlessTravelActorList'

    进一步添加的任意Actor;

  2. 具有有效PlayerState的所有控制器(仅限服务器);

  3. 所有PlayerController(仅限服务器);

  4. 所有本地PlayerControlller(服务器和客户端):通过

    AplayerControl::GetSeamlessTravelActorList

    进一步添加的任何Actor呼叫本地PlayerControlller

以下是执行Seamless Travel时的一般流程:

  1. 标记将持续到过渡Level(如上所述)的Actor;
  2. 前往过渡Level;
  3. 标记将持续到最后一级Level(见上文)的Actor;
  4. 到达最终level。

Online Subsystem Overview

在线子系统及其接口的存在为在给定环境中跨平台(指的是Steam、Xbox Live、Facebook等)的通用在线功能提供了清晰的逻辑抽象。其中可移植性是主要目标之一。

默认情况下,您将使用“SubsystemNULL”。这允许您主持LAN Sessions(这样您可以通过服务器列表查找会话并将它们加入到您的LAN网络中)或直接通过IP加入。但是它不允许你在互联网上主持这样的 Sessions——因为我们没有向客户端提供服务器/会话列表的主服务器。

子系统,例如Steam,将允许您托管在Internet上可见的服务器/会话。当然也可以创建自己的子系统/主服务器,但这需要在UE4之外编写大量代码。

Online Subsystem Module

Basic Design

这个基本模块——联机子系统负责规范平台如何定义特定模块并在引擎中注册。平台服务的所有访问都将通过此模块进行。加载时,此模块将依次尝试加载 ‘Engine.ini’文件

[OnlineSubsystem]
DefaultPlatformService = <Default Platform Identifier>

如果成功,则当其未指定参数时,此默认联机接口将通过静态访问器可用:

static IOnlineSubsystem* Get(const FName& SubsystemName = NAME_None);

当使用正确的标识符从该函数调用时,将按需加载其他服务。

Use of Delegates

Delegate(委托)与UE3类似,在线子系统在调用具有异步副作用的函数时将大量使用委托。重要的是要按照委托并等待调用正确的委托,然后再调用整个函数链上更远的函数。

若未等待异步任务可能会导致崩溃、意外或者其他未定义(undefined behavior)行为。在连接失败(如电缆拉动或其他断开连接事件)期间,等待代理尤其重要。在理想情况下,任务完成所需的时间可能是瞬间的,但在超时情况下,可能会超过一分钟。

委托接口相当简单,每个委托都清楚地定义在每个接口头的顶部。每个委托都有一个Add、Clear和Trigger函数。(尽管不鼓励手动触发委托)。通常的做法是在调用适当的函数之前Add()委托,然后从委托本身Clear()委托。

以下有一些常用的接口:

  1. Interfaces:并不是所有的平台都会实现所有的接口,游戏代码也应该做相应的规划;
  2. Profile:联机服务配置文件服务的接口定义。配置文件服务定义为与给定用户配置文件及其相关元数据(联机状态、访问权限等)相关的任何内容;
  3. Friends:联机服务的好友服务的接口定义。好友服务是与维护好友和好友列表相关的任何内容;
  4. Sessions:联机服务会话服务的接口定义。会话服务被定义为与管理会话及其状态相关的任何内容;
  5. Shared Cloud:提供用于共享已在云上的文件的接口(请参阅用户云与其他用户);
  6. User Cloud:提供按用户云文件存储的接口;
  7. Leaderboards:提供访问联机排行榜的界面;
  8. Voice:提供在游戏中通过网络进行语音通信的接口;
  9. Achievements:提供读取/写入/解锁成就的界面;
  10. External UI:提供用于访问给定平台外部接口(如果可用)的接口。

Sessions and Matchmaking

匹配是一个将玩家与会话进行配对(不一定是两个,可能多个)的过程。会话基本上是在服务器上运行的具有给定属性集的游戏的一个实例,这些属性集要么是公开( advertise,广而告之)的,以便玩家可以可以找到它并加入;要么是私有的,只有以某种方式被邀请或通知它的玩家才能加入。

想象一下一个在线游戏大厅,上面列出了当前正在玩的所有游戏。列表中的每一个游戏都是一个会话或单独的在线比赛。玩家通过搜索或其他方式与会话进行匹配,然后加入会话进行比赛。

Basic Life-Time of a Session

(偷个懒,主要是翻译为中文后,这些动词的位置歪歪曲的不好看)

  1. Create a new Session with desired Settings;
  2. Wait for Players to request to join the Match;
  3. Register Players who want to join;
  4. Start the Session ;
  5. Play the Match;
  6. End the Session;
  7. Un-register the Players. Either:——
    1. Update the Session if you want to change the Type of Match and go back to waiting for Players to join
    2. Destroy the Session

Session Interface

会话界面(Session Interface),或者说是IOnlineSession,提供了特定于平台的功能,用于设置后台所需的片段,以便执行匹配以及其他允许玩家查找和加入在线游戏的方法。这包括会话管理:通过搜索或其他方式查找会话,以及加入和退出这些会话。会话接口由联机子系统创建和拥有,这意味着它只存在于服务器上。

一般来说同一时间只能存在一个会话接口——引擎当前运行的平台的会话接口。虽然会话接口执行所有会话处理,但游戏通常不会直接与之交互。相反,game Session(AGameSession)会充当会话接口的特定于游戏的包装器,而游戏代码在需要与会话交互时调用它。GameSession由GameMode创建和拥有,并且在运行在线游戏时存在于服务器上。

虽然每个游戏可能有多个游戏会话类型,但一次只能使用一个。对于具有多个GameSession类型的游戏,最常见的情况是当游戏使用专用服务器时添加一种GameSession类型。

Session Setting

SessionSettings是由“FOnlineSessionSettingsclass”定义的一组确定会话特征的属性。

在基本实现中,如下所示:

  1. 允许的玩家数量;
  2. 是公开的还是私有的;
  3. 是否局域网匹配的;
  4. 服务器是专用的还是由玩家托管的;
  5. 是否允许邀请的;
  6. 等等。

使用在线游戏大厅示例,这些游戏中的每一个都是一个会话,并且有自己的会话设置。例如,一些会话可能是玩家对玩家(Player versus Player - PvP),而其他会话是多人合作( Cooperative Multiplayer :Co - Op)。而不同的会话可以播放不同的地图或游玩列表,需要不同数量的玩家等等。

Session Management

Creating Sessions

所有的Session结点都是异步的任务,并且一单对话结束的时候会调用“OnSuccess”或者“OnFailure”,同时我们可以使用最高的exec输出(the top most exec output.)。

关于使用C++建立Session(同时也包括了后面的Destroy,search,join)的方法见下面的网址:https://wiki.unrealengine.com/How_To_Use_Sessions_In_C++#Creating_a_Session(需要某些东西)。

Updating Sessions

一般来说更新是在要更改现有会话的设置时完成的,并使用“IOnlineSession::UpdateSession()”函数执行。

例如,会话当前可能设置为只允许8名玩家,而下一场比赛则需要允许12名玩家。若要更新会话,将调用“UpdateSession()”,并将指定最多12个播放机的新会话设置传递给它。更新会话的请求完成后,将触发“OnUpdateSessionComplete”委托。这提供了执行处理会话设置更改所需的任何配置或初始化的机会。

而更新会话通常在服务器上的匹配项之间执行,但也在客户端上执行,以保持会话信息同步。

Destroying Sessions

当会话结束且不再需要时,将使用“IOnlineSession::DestroySession()”函数销毁会话。

销毁操作完成后,将触发“OnDestroySessionComplete”委托,以执行清理操作。

Searching Sessions

查找会话的最简单方法是搜索与所需设置子集匹配的会话。这可能是因为玩家在用户界面中选择了一个过滤器集合,也可能是在幕后根据玩家的技能和其他因素自动完成的,也可能是两种方法的结合。而搜索会话的最基本形式是经典的服务器-浏览器,它显示所有可用的游戏,并允许玩家根据他们想玩的游戏类型进行过滤。

Joining Sessions

确定要加入的播放机会话后,将通过调用“IOnlineSession::join Session()”启动加入过程,并将玩家的号码、要加入的会话的名称和搜索结果传递给该玩家。加入过程完成时,将触发“OnJoinSessionComplete”委托。

这就是让玩家进入比赛的逻辑。

Cloud-Based Matchmaking

基于云的匹配是指内置的匹配服务,这些服务是可用的,通常是特定于平台的。这类服务的一个例子是通过Microsoft Xbox Live服务提供的TrueSkill系统。

若要在支持它的平台上启动匹配,请调用“IonlineSession::Startmatchmaking()”,并将要匹配的播放机的控制器号、会话名称、创建新会话时要使用的会话设置以及要搜索的设置并传递给该函数。而这个匹配完成时将触发“OnMatchmakingComplete”委托,该委托会传递一个bool变量——指进程是否成功,以及在这种情况下要加入的会话的名称。

可以通过调用“IOnlineSession::CancelMatchmaking()”并将玩家的的控制器号和传递给调用以开始匹配的SessionName来取消进程中的匹配操作。

取消操作完成时将触发“OnCancelMatchmakingCompletedelegate”。

Following and Inviting Friends

在支持好友系统的平台上,玩家可以跟随好友进入会话或邀请好友加入会话。

跟踪一个朋友到一个Session是通过调用’’IOnlineSession::FindFriendSession()“,并将要加入会话的本地玩家号码和已在会话中的朋友的ID,然后这些信息传递给函数,找到会话时将触发“OnFindFriendSessionComplete”委托,该委托包含了可用于加入会话的搜索结果。

玩家还可以用“IOnlineSession::SendSessionInviteToFriend()”或“IOnlineSession::SendSessionInviteToFriend s()”邀请一个或多个好友加入他们的当前会话,并传递要邀请的玩家的本地玩家号、会话名和ID。而当朋友接受邀请时,将触发包含要加入的会话的搜索结果的“OnSessionInviteAccepted”委托。

How to Start a Multiplayer Game

开始多人游戏最简单、最直接的方法是在Play下拉菜单中将玩家数设置为大于1的值(虚幻编辑器“运行”右边的下三角的菜单中选择)。这将自动在服务器和客户端之间创建网络连接。因此,即使您在主菜单级别启动游戏,玩家数设置为大于等于2,游戏也将连接,并且这总是一个网络连接。这不是一个本地的couch-coop多人连接。这需要以不同的方式来处理。

Advanced Settings

高级设置允许您为播放模式指定更多选项。其中一个类别允许您设置多人游戏选项(在同样地方的最下一个选项),相关选项可以查看下表()。

Option Description
Number of Players(游戏人数) This option defines the number of players to spawn in the game when launched. The editor and listen server count as players, a dedicated server will not. Clients make up the remainder of players.(此选项定义启动时要在游戏中生成的玩家数。编辑器和listen server算作玩家,而服务器不会。其余玩家由客户端那边的玩家组成。)
Server Game Options(服务器游戏选项) Here you can specify additional options that will be passed to the server as URL parameters.(在这里,您可以指定将作为URL参数传递给服务器的其他选项。)
Run Dedicated Server(运行专用服务器) If checked, a separate dedicated server will be launched. Otherwise the first player will act as a listen server that all other players can connect to.(如果选中,将启动一个单独的专用服务器。否则,第一个播放器玩家将充当所有其他所有玩家都可以连接到的listen server。)(有点类似于P2P模式)
Auto Connect To Server(自动连接服务) Controls, if the Game should directly connect the Clients to the Server. Means, when you just want to test Gameplay, you can check it and not care about setting up Menu for connecting. Other way round, if you have the Menu, you might want to deactivate the boolean.(控件,游戏应该直接将客户端连接到服务器。即当你只想测试游戏性时,你可以勾选他它而不需要设置连接菜单。另一方面,如果你有菜单,你可能想停用布尔值。)
Route 1st Gamepad to 2nd Client(将第一个Gamepad传送到第二个客户端) When running multiple player windows in a single process, this option determines how the game pad input get routed. If unchecked (default) the 1st game pad is attached to the 1st window, 2nd to the 2nd window, and so on. If it is checked, the 1st game pad goes to the 2nd window. The 1st window can then be controlled by keyboard/mouse, which is convenient if two people are testing on the same computer.(在一个进程中运行多个玩家窗口时,此选项确定如何传输Gamepad输入。默认为第一个Gamepad将附加到第一个窗口,第二个附加到第二个窗口,依此类推。如果选中,第一个Gamepad将转到第二个窗口。第1个窗口可以通过键盘/鼠标控制,如果两个人在同一台计算机上进行测试,这很方便。)
Use Single Process(使用单一进程) This spawns multiple player windows in a single instance of Unreal Engine 4. This will load much faster, but has potential to have more issues. When this is unchecked, additional options become available.(将在单个UE4实例中生多个玩家窗口。这将加载得更快,但有可能有更多的问题。若未选中此项,则提供其他选项。)
Create Audio Device for Every Player(为每个玩家创建音频设备) Enabling this will allow rendering accurate audio from every player’s perspective but will use more CPU.(启用此功能将允许从每个玩家渲染准确的音频,但将使用更多的CPU。)
Play in Editor Description(编辑器中描述) This is a description of what will occur when playing based on the currently applied Multiplayer settings.(这是根据当前应用的多人游戏设置对播放时将发生的情况的描述。)
Use Single Process

当选中Use Single Process时,在UE4的单个实例中生成多个窗口。如果未选中此选项,则将为每个已分配的玩家启动多个UE4实例,并提供其他选项。

Option Description
Editor Multiplayer Mode(编辑器多人模式) This is the NetMode to use for Play In Editor (Play Offline, Play As Listen Server or Play As Client).(这是用于在编辑器中播放的网络模式(离线播放、作为Listen Server播放或作为客户端播放))
Command Line Arguments(命令行参数) Here you can assign additional command line options that will be passed to standalone game instances.(在这里,您可以分配额外的命令行选项,这些选项将传递给独立的游戏实例。)
Multiplayer Window Size (in pixels)(多人窗口像素大小) Define the width/height to use when spawning additional standalone game instances.(定义生成其他独立游戏实例时要使用的宽度/高度。)
Run as Dedicated Server

不启用“运行专用服务器”,第一个客户端将是ListenServer。另一方面,当标记为true时,所有玩家都将是客户。

Start and Connect to a Server - UE4++

Start a Server
UGameplayStatics::OpenLevel(GetWorld(), “LevelName” , true, “listen”);
Connect to a Server
// Assuming you are not already in the PlayerController (if you are, just call ClientTravel directly) 
APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 0); 
PlayerController->ClientTravel(“IPADDRESS”, ETravelType::TRAVEL_Absolute);

Starting via Command Line

这个翻译是不是有点太过于蠢了= =。

Type Command
Listen Server UE4Editor.exe ProjectName MapName?Listen -game
Dedicated Server UE4Editor.exe ProjectName MapName -server -game -log
Client UE4Editor.exe ProjectName ServerIP -game

默认情况下,专用服务器是headless的。如果不使用“-log”,您将看不到任何显示专用服务器的窗口!

Connection Process

当新客户机第一次连接时,会发生一些事情:

  1. 首先,客户端将向服务器发送连接请求;
  2. 服务器将处理此请求,如果服务器不拒绝连接,将向客户端发送一个响应,并提供正确的信息以继续;
  3. 接下来将显示连接过程的主要步骤。注意这是官方文件的直接翻译。
The major steps are

主要步骤是:

  1. 客户端发送连接请求;

  2. 如果服务器接受,它将发送当前map;

  3. 服务器将等待客户端加载此map;

  4. 加载后,服务器将在本地调用“agamomede::PreLogin”——这将给GameMode一个拒绝连接的机会;

  5. 如果接受,服务器将调用“AgameMode::Login”

    此函数的作用是创建一个PlayerController,然后将其复制到新连接的客户端。客户端接收到后,此PlayerController将替换客户端临时的PlayerController,后者在连接过程中用作占位符。

    请注意,此处将调用“APlayerController::BeginPlay”,但是在这个Actor上调用RPC函数还不安全。你应该等到“agamomede::PostLogin”被调用;

  6. 假设一切顺利,“agamomede::PostLogin”被调用:

    此时,服务器可以安全地开始在此PlayerController上调用RPC函数。

以上便是整个UE4网络编程的基本内容了,当然中间涉及到某个需要特殊工具的链接,里面有更丰富的对话链接。但是仅仅只是写这些是不够的,还需要自行写一些类似的游戏才能掌握。希望能够对使用UE4(乃至后续UE5)的在网络游戏上的开发提供一些微小的帮助。


文章作者: AleXandrite
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AleXandrite !
  目录