本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 —— 《P35 网络角色(Network Role)》 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author) Stephen Ulibarri 发布在 Udemy 上的课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
文章目录
- P35 网络角色(Network Role)
- 35.1 网络角色概念
- 35.2 创建显示网络角色的控件
- 35.3 显示本地网络角色
- 35.4 显示远程网络角色
- 35.5 Summary
P35 网络角色(Network Role)
本节课将讨论虚幻引擎中网络角色(Network role)的概念以及如何在多人游戏中使用它;“网络角色” 包括本地角色(Local role)和远程角色(Remote role),我们将比较两者的不同之处,接着为了更好地掌握(Get a better grasp)网络角色的概念并学以致用,我们将创建一个特殊的部件,它的功能是将玩家控制的游戏人物的当前角色显示在人物头上(Overhead)。
注意:
这里需要避免与虚幻引擎中 “Character
” 的中文翻译 “角色” 混淆,以及与 “Actor Role
” 进行区分。
在Unreal Engine 4(UE4)中,Network Role 和 Actor Role 是两个相关但不同的概念。
区别:
- Network Role 主要用于描述Actor在网络环境中的角色,决定了Actor在客户端和服务器之间的行为和职责。
- Actor Role 主要用于描述Actor在游戏逻辑中的角色,决定了Actor在游戏中的行为和职责。
联系:
- 在大多数情况下,Network Role 和 Actor Role 是一致的。例如,服务器上的Actor通常同时具有 ROLE_Authority 的 Network Role 和 Actor Role。
- 在客户端上,玩家控制的角色通常同时具有 ROLE_AutonomousProxy 的 Network Role 和 Actor Role。
非玩家控制的角色或其他对象通常同时具有 ROLE_SimulatedProxy 的 Network Role 和 Actor Role。
—— CSDN 《【UE 网络】Network Role and Authority、Actors Owner、Actor Role and RemoteRole》
35.1 网络角色概念
-
在多人游戏中,玩家控制的任何给定人物都有多个版本(Mutiple versions)。如果玩家是连接到服务器的客户端,那么他所控制的游戏人物在自己和其他客户端以及服务器都分别存在一个版本;例如,多人游戏中有两个玩家,那么其中一个玩家所控制的游戏人物在自己的机器上有一个版本,在服务器和另一个玩家的机器上也有一个版本(无法被另一个玩家所控制)。由此可知,如果多人游戏中有三个玩家,那么其中一个玩家将会在其他机器上有三个副本,因此如何区分(Distinguish)我们正在处理的角色属于哪个版本就至关重要了。
-
为了解决这个问题(Sort this problem out),虚幻引擎引入了网络角色的概念以及相应的枚举变量(Enum)“
ENetRole
”,它包含几个常用的枚举常量(Enum constant),以供我们识别任何给定的玩家人物的网络角色:- “
ENetRole::ROLE_Authority
”:虚幻引擎使用权威服务器模型(Authoritative server model),“ROLE_Authority
” 会被分配给(Be assigned to)存在于服务器上的任何人物。 - “
ENetRole::ROLE_SimulatedProxy
”:“SimulatedProxy
” 可以翻译成 “模拟代理”,顾名思义,它存在于任何不控制当前玩家人物的其他客户端机器上,即当你在自己的机器上控制人物时,你的机器不是服务器,那么 “`ROLE_SimulatedProxy``” 将会分配给你看到其他人物,它们来自服务器和其他客户端,被其他玩家控制。 - “
ENetRole::ROLE_AutonomousProxy
”:“AutonomousProxy
” 可以翻译成 “自主代理” 存在于可控制当前玩家人物的客户端机器上,即当你在自己的机器上控制人物时,假设你的机器不是服务器,那么“ROLE_AutonomousProxy
” 会被分给你的机器;如果你的机器是服务器。 - “
ENetRole::ROLE_None
”:分配给没有被定义网络角色的人物。
虚幻引擎使用的默认模型是 服务器授权,意味着服务器对游戏状态固定具有权限,而信息固定从服务器复制到客户端。服务器上的Actor应具有授权的本地角色,而其在远程客户端上的对应Actor应具有模拟或自主代理的本地角色。
-
Authority (权威角色 / 权威端)
Authority Actor,又叫做权威端。指的是服务器上的 Actor。服务器是游戏的权威,负责管理和验证所有的游戏状态和操作。- 服务器控制:Authority Actor在服务器上有完全的控制权,所有的游戏逻辑和状态更新都在服务器上进行。
- 状态验证:服务器会验证客户端发送的请求和操作,确保游戏的公平性和一致性。
- 广播更新:服务器会将更新后的状态广播给所有相关的客户端,确保所有客户端的游戏状态保持一致。
-
Simulated Proxy (模拟代理 / 模拟端)
Simulated Proxy Actor通常用于客户端上的非自主 Actor 。这些 Actor 在客户端上进行模拟,但最终的状态由服务器决定。- 客户端模拟:Simulated Proxy Actor 在客户端上进行模拟,以提供即时的反馈和流畅的游戏体验。
- 服务器同步:尽管客户端进行模拟,最终的状态还是由服务器决定并同步到客户端。
- 减少延迟感:通过在客户端进行模拟,可以减少网络延迟带来的影响,使游戏体验更加流畅。
-
Autonomous Proxy (自主代理 / 主动端)
Autonomous Proxy Actor 通常用于客户端拥有的 Actor ,例如玩家控制的角色(Player Character)。这种 Actor 在客户端上有更多的控制权,并且可以自主地进行一些操作。在UE4的网络架构中,主动端(Autonomous Proxy)主要用于玩家角色,以便直接响应玩家输入并进行本地预测。- 客户端控制:Autonomous Proxy Actor 在客户端上有更多的控制权,允许客户端直接对 Actor 进行输入和操作。
- 本地预测:客户端可以进行本地预测,以减少网络延迟带来的影响。例如,玩家移动时,客户端可以立即显示移动效果,而不必等待服务器的确认。
- 同步到服务器:尽管客户端有更多的控制权,但最终的状态还是需要同步到服务器,服务器会进行验证和纠正。
—— CSDN 《【UE 网络】Network Role and Authority、Actors Owner、Actor Role and RemoteRole》
- “
35.2 创建显示网络角色的控件
-
在虚幻引擎内容浏览器 “C++ 类”(C++ Classes)目录下新建一个 “
UserWidget
” C++ 类,命名为 “OverheadWidget
”,路径为“.../Blaster/HUD
”。
-
在 Visual Studio 中打开头文件 “
OverheadWidget.h
”,声明 "UTextBlock
类变量 “DisplayText
” ,然后进行编译。// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" #include "OverheadWidget.generated.h" /** * */ UCLASS() class BLASTER_API UOverheadWidget : public UUserWidget { GENERATED_BODY() /* P35 网络角色(Network Role)*/ public: UPROPERTY((meta = BindWidget)) // 将 C++ 变量 DisplayText 与蓝图部件中的文本块 DisplayText 关联 class UTextBlock* DisplayText; // 创建文本块 C++ 类,我们对这个变量的任何更改都会关联到蓝图部件中的文本块 /* P35 网络角色(Network Role)*/ };
-
在虚幻引擎的内容浏览器 “
/内容/Blueprints
” 目录下新建文件夹 “HUD
”,然后新建一个 “控件蓝图” 类 “WBP_OverheadWidget
”。
-
双击 “
WBP_OverheadWidget
”,进入用户控件设计器窗口。如果在左下 “层级” 面板中有 “画布画板” (Canvas Panel),需要将其删除,接着在 “控制板” 面板中将 “通用” 选项卡下的 “文本”(Text)组件拖拽到设计器中,调整其大小(Resize),重命名为 “DisplayText
”,这里需要和头文件 “OverheadWidget.h
” 中 "UTextBlock
类变量 “DisplayText
” 的变量名保持一致;然后在右侧 “细节” 面板的 “字体”(Font)选项卡下设置 “字体样式”(Typeface)为 “常规”(Regular),设置 “对齐”(Justification)为 “居中对齐”(Align center text)
-
在右上方点击 “图表”(Graph)按钮,进入图表编辑模式,在上方工具栏点击 “类设置”(Class Settings),然后在左下方 “细节”(Details)面板中设置 “类选项” 下的 “父类”(Parent Class)为 “
OverheadWidget
”。
35.3 显示本地网络角色
- 返回 Visual Studio,打开 “
OverheadWidget.h
” 和 “OverheadWidget.cpp
”,覆写原生函数 “OnLevelRemovedFromWorld()
”,当离开当前关卡或进行关卡转移时将调用此函数移除部件 “OverheadWidget
”(注意在 5.1 之后的版本中 “virtual void OnLevelRemoveFromWorld()
” 被去除,取而代之的是 “virtual void NativeDestruct()
”);接着,声明并定义函数 “SetDisplayText()
” 和 “ShowPlayerNetRole()
”,用于获取并展示本机玩家网络角色后设置部件 “OverheadWidget
” 中文本块 “DisplayText
” 显示的文本为玩家的网络角色。/*** OverheadWidget.h ***/ // Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" #include "OverheadWidget.generated.h" /** * */ UCLASS() class BLASTER_API UOverheadWidget : public UUserWidget { GENERATED_BODY() /* P35 网络角色(Network Role)*/ public: UPROPERTY(meta = (BindWidget)) // 将 C++ 变量 DisplayText 与蓝图部件中的文本块 DisplayText 关联 class UTextBlock* DisplayText; // 创建文本块 C++ 类,我们对这个变量的任何更改都会关联到蓝图部件中的文本块 void SetDisplayText(FString TextToDisplay); // 用于设置并显示文本块的文本 UFUNCTION(BlueprintCallable) // 可在蓝图类 BP_Blaster 调用 void ShowPlayerNetRole(APawn* InPawn); // 获取并展示本机玩家网络角色 protected: virtual void OnLevelRemovedFromWorld(ULevel* InLevel, UWorld* InWorld) override; // 覆写原生函数 OnLevelRemovedFromWorld(),当离开当前关卡或进行关卡转移时将调用此函数移除部件 // void OnLevelRemovedFromWorld(): https://docs.unrealengine.com/5.0/en-US/API/Runtime/UMG/Blueprint/UUserWidget/OnLevelRemovedFromWorld/ // 在 5.1 之后的版本中 virtual void OnLevelRemoveFromWorld() 被去除,取而代之的是 virtual void NativeDestruct() // void NativeDestruct(): https://docs.unrealengine.com/5.1/en-US/API/Runtime/UMG/Blueprint/UUserWidget/NativeDestruct/ // virtual void NativeDestruct() override; /* P35 网络角色(Network Role)*/ };
/*** OverheadWidget.cpp ***/ // Fill out your copyright notice in the Description page of Project Settings. /* P35 网络角色(Network Role)*/ #include "OverheadWidget.h" // 原来自动生成的代码是 #include "HUD/OverheadWidget.h",这里需要把 "GameMode/" 去掉,否则找不到文件 "LobbyGameMode.h" #include "Components/TextBlock.h" void UOverheadWidget::SetDisplayText(FString TextToDisplay) // 设置文本块 DisplayText 显示的文本 { if (DisplayText) { DisplayText->SetText(FText::FromString(TextToDisplay)); // 将要展示的文本 TextToDisplay 由虚幻引擎字符流类型 FString 转换为文本类型 FText,并将文本块的文本设置为 TextToDisplay 的内容 } } void UOverheadWidget::ShowPlayerNetRole(APawn* InPawn) // 展示本地玩家网络角色 { ENetRole LocalRole = InPawn->GetLocalRole(); // 获取本地玩家网络角色(本地网络角色会因调用它的机器不同) FString Role; switch (LocalRole) // 根据 LocalRole 的值来给 Role 赋值 { case ENetRole::ROLE_Authority: // 本地玩家是 Authority (权威角色 / 权威端) Role = FString("Authority"); break; // 添加 Break 语句直接退出 switch 分支,后面的 case 语句将不再执行 case ENetRole::ROLE_AutonomousProxy: // 本地玩家是 Autonomous Proxy (权威角色 / 权威端) Role = FString("Autonomous Proxy"); break; case ENetRole::ROLE_SimulatedProxy: // 本地玩家是 Simulated Proxy(模拟代理 / 模拟端) Role = FString("Simulated Proxy"); break; case ENetRole::ROLE_None: // 本地玩家没有分配网络角色 Role = FString("None"); break; default: break; } FString LocalRoleString = FString::Printf(TEXT("Local Role: %s"), *Role); // 打印网络角色以便进行调试 SetDisplayText(LocalRoleString); // 设置文本块 DisplayText 显示的文本 } void UOverheadWidget::OnLevelRemovedFromWorld(ULevel* InLevel, UWorld* InWorld) // 当转移关卡时删除控件 { RemoveFromParent(); // 从父类删除实体,用于从场景中删除实体(Removes this entity from its parent. This is used to remove entities from the scene.) // https://dev.epicgames.com/documentation/zh-cn/uefn/verse-api/unrealenginedotcom/temporary/scenegraph/entity/removefromparent?application_version=1.0 Super::OnLevelRemovedFromWorld(InLevel, InWorld); // 调用父类的 NativeInitializeAnimation() 函数 } /* void UMenu::NativeDestruct() { MenuTearDown(); Super::NativeDestruct(); // 调用父类的 NativeDestruct() 函数 } */ /* P35 网络角色(Network Role)*/
-
打开 “
BlasterCharacter.h
”,声明头部组件 “OverheadWidget
” 为 “ABlasterCharacter
” 类的私有成员变量;然后在 “BlasterCharacter.cpp
” 的构造函数中创建头部组件对象,然后进行编译。/*** BlasterCharacter.h ***/ // Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "GameFramework/Character.h" #include "BlasterCharacter.generated.h" UCLASS() class BLASTER_API ABlasterCharacter : public ACharacter { GENERATED_BODY() public: // Sets default values for this character's properties ABlasterCharacter(); // Called every frame virtual void Tick(float DeltaTime) override; // Called to bind functionality to input virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; protected: // Called when the game starts or when spawned virtual void BeginPlay() override; // 与轴映射相对应的函数 void MoveForward(float Value); // 角色前进或后退 void MoveRight(float Value); // 角色左移或右移 void Turn(float Value); // 角色视角左转或右转 void LookUp(float Value); // 角色俯视或仰视 private: UPROPERTY(VisibleAnywhere, Category = Camera) class USpringArmComponent* CameraBoom; // 添加弹簧臂组件,归类为 “Camera” UPROPERTY(VisibleAnywhere, Category = Camera) class UCameraComponent* FollowCamera; // 添加摄像机组件,归类为 “Camera” /* P35 网络角色(Network Role)*/ // BlueprintReadOnly:表示该变量只能在蓝图中进行读取操作,不能在蓝图中进行写入操作。常用于定义只读变量。 // 我们不能在私有变量中使用关键字 BlueprintReadOnly和 BlueprintReadWrite,除非使用了 meta = (AllowPrivateAccess = "true") 进行指定 // UE4中用于定义蓝图变量的元数据(metadata)的所有关键字及其解释和作用可以参见:https://blog.csdn.net/u013007305/article/details/130450354 UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) class UWidgetComponent* OverheadWidget; // 添加头部组件 /* P35 网络角色(Network Role)*/ public: };
/*** BlasterCharacter.cpp ***/ // Fill out your copyright notice in the Description page of Project Settings. #include "BlasterCharacter.h" #include "GameFramework/SpringArmComponent.h" #include "Camera/CameraComponent.h" #include "GameFramework/CharacterMovementComponent.h" /* P35 网络角色(Network Role)*/ #include "Components/WidgetComponent.h" /* P35 网络角色(Network Role)*/ // Sets default values ABlasterCharacter::ABlasterCharacter() { // Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; // 创建弹簧臂对象 CameraBoom 并设置 CameraBoom 的默认属性 CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom")); // 基于弹簧臂组件类创建对象 CameraBoom->SetupAttachment(GetMesh()); // 设置弹簧臂附加到角色的骨骼网格体组件,如果附加到胶囊体上,角色在做蹲下的动作时,由于胶囊体的大小和路线会发生改变,弹簧臂的高度也会发生改变(弹簧臂将会移动) CameraBoom->TargetArmLength = 600.f; // 设置弹簧臂长度 CameraBoom->bUsePawnControlRotation = true; // 设置弹簧臂跟随角色控制器旋转 // 创建摄像机对象 FollowCamera 并设置 FollowCamera 的默认属性 FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera")); // 基于摄像机组件类创建对象 FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // 将摄像机附加到弹簧臂 CameraBoom 上,并指定插槽名为虚幻引擎摄像机组件成员变量 SocketName FollowCamera->bUsePawnControlRotation = false; // 设置摄像机不跟随角色控制器旋转 // 调整弹簧臂和摄像机的相对位置(也可以在虚幻引擎的蓝图编辑器中进行设置) CameraBoom->SetRelativeLocation(FVector(0, 0, 88)); // 设置弹簧臂和摄像机在蓝图类 “BP_BlasterCharacter” 的相对位置为 (0, 0, 88),以避免它们与地面相撞 bUseControllerRotationYaw = false; // 设置人物不跟随控制器(镜头)转向,也可以在 BP_BlasterCharacter 蓝图编辑器中实现 GetCharacterMovement()->bOrientRotationToMovement = true; // 获取角色移动组件,角色移动时向加速度方向旋转角色,BP_BlasterCharacter 蓝图编辑器中实现 /* P35 网络角色(Network Role)*/ OverheadWidget = CreateDefaultSubobject<UWidgetComponent> (TEXT("OverheadWidget")); // 基于头部组件类创建对象 OverheadWidget->SetupAttachment(RootComponent); // 将头部组件附加到人物根组件 RootComponent 上 /* P35 网络角色(Network Role)*/ } ...
-
在虚幻引擎打开 “
BP_BlasterCharacter
” 蓝图编辑器,在左侧 “组件”(Component)面板中可以看到头部组件 “OverheadWidget
” 已经附加到 “胶囊体组件 (CollisionCylinder)”(Capsule Component(CollisionCylinder)) ,点击它,并在右侧 “细节”(Details)面板 “用户界面”(USER INTERFACE)选项卡下将 “空间”(Space)从 “世界”(World) 设置为 “屏幕”(Screen),将 “控件类”(Widget Class)设置为 “WBP_OverheadWidget”,勾选 “以所需大小绘制”(Draw at Desired Size),这样我们就不必手动设置(Manually set)这个部件的大小。 -
在左侧 “组件”(Component)面板中将 “
OverheadWidget
” 拖拽至 “事件图表”(Event Graph),然后按照下图连接蓝图节点,这段蓝图程序实现了在玩家角色的头顶显示网络角色的功能:节点 “获取用户控件对象”(Get User Widget Object)用于获取当前角色上的 “OverheadWidget
” 组件,返回的是一个 “用户控件” (User Widget)类型的值;节点 “类型转换为WBP_OverheadWidget
”(Cast To WBP_OverheadWidget)将 “用户控件” (User Widget)类型转换为 “WBP_OverheadWidget
” 类型,成功转换后,就可以调用 “WBP_OverheadWidget
” 内部的函数 “Show PlayerNetRole()
”;这个函数的 “目标”(Target) 是OverheadWidget
,输入参数 “In Pawn
” 传入 “Self
”(当前角色 “BlasterCharacter
”)。
-
点击上方 “视口”(Viewport) 选项卡,接着在左侧 “组件”(Component)面板中点击 “
OverheadWidget
”,然后在 “视口” 中将该组件拖拽移动至人物头顶。
-
打开关卡 “
BlasterMap
”,在工具栏点击 “ ⋮ \vdots ⋮”,这是 “修改游戏模式和游戏设置”(Change Play Mode and Play Settings) 的按钮, 修改 “玩家数量”(Number of Players) 为 3,“网络模式”(Net Mode)为 “以监听服务器”(Play as Listen Server),当我们进行测试时,其中一个玩家将作为监听服务器,而其他两个玩家为客户端。
-
点击工具栏的 “播放”(▶)按钮启动运行,可以看到视口面板中的玩家是监听服务器,弹出的两个窗口为客户端。视口面板中显示三个玩家的 本地 网络角色都是 “Authority”,因为在服务器上的人物都具有 本地 “Authority” 角色(权威角色 / 权威端);而能在其中一个客户端被控制的那个人物(下图红圈标注)具有 本地 “Autonomous Proxy” 角色(自主代理 / 主动端),其他不能被控制的人物具有 本地 “Simulated Proxy” 角色(模拟代理 / 模拟端)。但是,我们无法仅从本地角色中分辨出哪个人物是由服务器控制的,下面我们将尝试显示人物的 远程 网络角色。
35.4 显示远程网络角色
- 返回 Visual Studio,打开 “
OverheadWidget.cpp
”,将函数 “ShowPlayerNetRole()
” 中的变量名 “LocalRole
” 改为 “RemoteRole
”。/*** OverheadWidget.cpp ***/ ... void UOverheadWidget::ShowPlayerNetRole(APawn* InPawn) // 展示本地玩家网络角色 { // ENetRole LocalRole = InPawn->GetLocalRole(); // 获取本地玩家网络角色(本地网络角色会因调用它的机器不同) ENetRole RemoteRole = InPawn->GetRemoteRole(); // 获取远程玩家网络角色 FString Role; /* switch (LocalRole) // 根据 LocalRole 的值来给 Role 赋值 { case ENetRole::ROLE_Authority: // 本地玩家是 Authority (权威角色 / 权威端) Role = FString("Authority"); break; // 添加 Break 语句直接退出 switch 分支,后面的 case 语句将不再执行 case ENetRole::ROLE_AutonomousProxy: // 本地玩家是 Autonomous Proxy (权威角色 / 权威端) Role = FString("Autonomous Proxy"); break; case ENetRole::ROLE_SimulatedProxy: // 本地玩家是 Simulated Proxy(模拟代理 / 模拟端) Role = FString("Simulated Proxy"); break; case ENetRole::ROLE_None: // 本地玩家没有分配网络角色 Role = FString("None"); break; default: break; } */ switch (RemoteRole) // 根据 RemoteRole 的值来给 Role 赋值 { case ENetRole::ROLE_Authority: // 远程玩家是 Authority (权威角色 / 权威端) Role = FString("Authority"); break; // 添加 Break 语句直接退出 switch 分支,后面的 case 语句将不再执行 case ENetRole::ROLE_AutonomousProxy: // 远程玩家是 Autonomous Proxy (权威角色 / 权威端) Role = FString("Autonomous Proxy"); break; case ENetRole::ROLE_SimulatedProxy: // 远程玩家是 Simulated Proxy(模拟代理 / 模拟端) Role = FString("Simulated Proxy"); break; case ENetRole::ROLE_None: // 远程玩家没有分配网络角色 Role = FString("None"); break; default: break; } // FString LocalRoleString = FString::Printf(TEXT("Local Role: %s"), *Role); // 打印网络角色以便进行调试 // SetDisplayText(LocalRoleString); // 设置文本块 DisplayText 显示的文本 FString RemoteRoleString = FString::Printf(TEXT("Remote Role: %s"), *Role); // 打印网络角色以便进行调试 SetDisplayText(RemoteRoleString); // 设置文本块 DisplayText 显示的文本 } ...
- 点击工具栏的 “播放”(▶)按钮启动运行,可以看到视口面板中的玩家是监听服务器,弹出的两个窗口为客户端。视口面板中能被控制的那个人物(下图红圈标注)具有 远程 “Autonomous Proxy” 角色(自主代理 / 主动端),其他不能被控制的人物具有 远程 “Simulated Proxy” 角色(模拟代理 / 模拟端);而在客户端中,所有人物无论可不可以被控制,远程 网络角色都是 “Authority”。由此我们就可以根据人物 本地 和 远程 网络角色来判断玩家的机器属于服务器端还是客户端。
在 Actor 的复制过程中,有两个属性扮演了重要角色,分别是 Role 和 RemoteRole。
有了这两个属性,您可以知道:- 谁拥有 actor 的主控权
- actor 是否被复制
- 复制模式
首先一件要确定的事,就是谁拥有特定 actor 的主控权。要确定当前运行的引擎实例是否有主控者,需要查看 Role 属性是否为
ROLE_Authority
。如果是,就表明这个运行中的 虚幻引擎 实例负责掌管此 actor(决定其是否被复制)。
如果 Role 是ROLE_Authority
,RemoteRole 是ROLE_SimulatedProxy
或ROLE_AutonomousProxy
,就说明这个引擎实例负责将此 actor 复制到远程连接。就目前而言,只有服务器能够向已连接的客户端同步 Actor (客户端永远都不能向服务器同步)。始终记住这一点, 只有 服务器 才能看到
Role == ROLE_Authority
和RemoteRole == ROLE_SimulatedProxy
或者ROLE_AutonomousProxy
。
Role/RemoteRole 对调
对于不同的数值观察者,它们的 Role 和 RemoteRole 值可能发生对调。例如,如果您的服务器上有这样的配置:Role == ROLE_Authority
RemoteRole == ROLE_SimulatedProxy
客户端会将其识别为以下形式:
Role == ROLE_SimulatedProxy
RemoteRole == ROLE_Authority
这种情况是正常的,因为服务器要负责掌管 actor 并将其复制到客户端。而客户端只是接收更新,并在更新的间歇模拟 actor。
复制模式
服务器不会在每次更新时复制 actor。这会消耗太多的带宽和 CPU 资源。实际上,服务器会按照AActor::NetUpdateFrequency
属性指定的频度来复制 actor。
因此在 actor 更新的间歇,会有一些时间数据被传递到客户端。这会导致 actor 呈现出断续、不连贯的移动。为了弥补这个缺陷,客户端将在更新的间歇中模拟 actor。
目前共有两种类型的模拟。ROLE_SimulatedProxy
这是标准的模拟途径,通常是根据上次获得的速率对移动进行推算。当服务器为特定的 actor 发送更新时,客户端将向着新的方位调整其位置,然后利用更新的间歇,根据由服务器发送的最近的速率值来继续移动 actor。
使用上次获得的速率值进行模拟,只是普通模拟方式中的一种。您完全可以编写自己的定制代码,在服务器更新的间隔使用其他的一些信息来进行推算。ROLE_AutonomousProxy
这种模拟通常只用于 PlayerController 所拥有的 actor。这说明此 actor 会接收来自真人控制者的输入,所以在我们进行推算时,我们会有更多一些的信息,而且能使用真人输入内容来补足缺失的信息(而不是根据上次获得的速率来进行推算)。
虚幻引擎官方文档 《Actor 的 Role 和 RemoteRole 属性》
35.5 Summary
本节课围绕虚幻引擎的网络角色展开,在多人游戏中,玩家控制的任何给定人物都有多个版本,如果多人游戏中有三个玩家,那么其中一个玩家将会在其他机器上有三个副本,为了解决如何区分我们正在处理的角色属于哪个版本,虚幻引擎引入了网络角色的概念以及相应的枚举变量“ENetRole
”,它包含 “ENetRole::ROLE_Authority
”“ENetRole::ROLE_SimulatedProxy
”、“ENetRole::ROLE_AutonomousProxy
”:以及 “ENetRole::ROLE_None
” 四个常用的枚举常量。为了查看玩家人物在监听服务器和客户端上的本地网络角色和远程网络角色,我们创建 “OverheadWidget
” 控件类,绑定 “UTextBlock
” 文本组件,在玩家人物的蓝图类 “BlasterCharacter
” 中将控件 “OverheadWidget
” 附加至角色头顶,这样就可以动态显示网络角色。最后我们进行了多端测试验证,以监听服务器模式启动多玩家实例,进一步理解本地网络角色和远程网络角色在服务端与客户端的显示逻辑。
在 35.3 显示本地网路角色 和 35.4 显示远程网路角色 中,我们在进行测试时,可以看到两种情况下监听服务器端和客户端出现了 本地网络角色和远程网络角色对调 的现象,对于不同的数值观察者,它们的 “(Local)Role
” 和 “Remote
Role” 值可能发生对调,即如果服务器上有这样的配置 “(Local)Role == ROLE_Authority
” 以及“RemoteRole == ROLE_SimulatedProxy
”,客户端会将其识别为 “(Local)Role == ROLE_SimulatedProxy
” 以及 “RemoteRole == ROLE_Authority
”,这种情况是正常的,因为服务器要负责掌管 Actor 并将其复制到客户端。而客户端只是接收更新,并在更新的间歇模拟 Actor。