前言

今天完成了教程的 34-37 小节,对应 GitHub 上 2022-2-17 这一天的提交,内容主要为网络设置及武器的简易基类,以及一些 UI。

虽然内容并不多,但是网络部分设计的知识点比较多,这里记录一下。

多人游戏中的关卡切换

无缝 ( seamless ) 与非无缝 ( non-seamless ) 切换

UE4中,Server有2种主要的travel方式:Seamless和non-seamless。Swamless travel是非阻塞的操作,non-seamless是阻塞操作。Travel后,所连接的客户端会一起进入新的地图。当客户端执行non-seamless travel时,client会先和当前server断开,然后重新连接到这个server,此时新的map已经就绪,会再经历一次PreLogin、Login、PostLogin事件。UE4推荐使用seamless travel,更平滑,可以避免重新连接过程中的不稳定因素,而且可以使用一个自定义的TransitionMap来掩盖加载地图的耗时,给玩家一个更好的体验。

有三种情形中必然产生非无缝转移:

  • 初次加载地图时

  • 初次作为客户端连接服务器时

  • 想要终止一个多人模式游戏并启动新游戏时

有三个用来驱动转移的主要函数:UEngine::BrowseUWorld::ServerTravelAPlayerController::ClientTravel

UEngine::Browse

  • 就像是加载新地图时的硬重置。

  • 将始终导致非无缝切换。

  • 将导致服务器在切换到目标地图前与当前客户端断开连接。

  • 客户端将与当前服务器断开连接。

  • 专用服务器无法切换至其他服务器,因此地图必须存储在本地(不能是 URL)。

UWorld::ServerTravel

  • 仅适用于服务器。

  • 会将服务器跳转到新的世界/场景。

  • 所有连接的客户端都会跟随。

  • 这就是多人游戏在地图之间转移时所用的方法,而服务器将负责调用此函数。

  • 服务器将为所有已连接的客户端玩家调用 APlayerController::ClientTravel

APlayerController::ClientTravel

  • 如果从客户端调用,则转移到新的服务器。

  • 如果从服务器调用,则要求特定客户端转移到新地图(但仍然连接到当前服务器)。

启用无缝切换

要启用无缝切换,需要设置一个过渡地图。这需要通过 UGameMapsSettings::TransitionMap 属性进行配置。该属性默认为空,如果游戏保持这一默认状态,ue 就会为过渡地图创建一个空地图。

Transition map 为什么需要,是因为当前必须有一个 world 被加载(拥有map),因此当我们 loading 新地图时不能释放老地图,于是需要一个体积很小的 transition map 作为中转。这样便可以从当前地图转移到过渡地图,再从那里转移到最终的地图。由于过渡地图非常小,因此在 “中转” 当前地图和最终地图时不会造成太大的资源消耗。

设置 AGameModeBase::bUseSeamlessTraveltrue,就可以使用 seamless travel。

SeamlessTravel 流程

通过GetSeamlessTravelActorList保留Actor的大致流程为:

  • 标记保留到Transition Level的actors
  • Travel 到Transition Level
  • 标记保留到Destination Level的actors
  • Travel到final level

SeamlessTravel 过程中保留 actors

SeamlessTravel 到新的 Level 后,原 Level 的大部分对象都会销毁,但 Seamless travel 可以保留当前 level 的一些 actors 到新的level。这比较有用,我们可以把一些actor带到下个地图继续使用,还可以把一些需要传递的信息封装成actor,从而完成信息的跨地图传递,如道具栏物品和玩家等

默认这些actors会被自动保留:

  • GameMode actor(server only) 以及任何通过 AGameModeBase::GetSeamlessTravelActorList 额外添加的任何 actors
  • 任何拥有有效 PlayerState 的 Controllers(server only)
  • 所有 PlayerControllers(server only)
  • 所有 local PlayerControllers(server和client)以及通过 APlayerController::GetSeamlessTravelActorList (在local PlayerControllers上调用)额外添加的任何 actors

由于 seamless travel 的流程为 current map -> transition map -> destination map,因此 “保留” 的含义是可以在 map 之间保留,而至于是保留到 TransitionMap,还是保留到 DestinationMap,不同的 Actor 有些不同。比如 GameMode,默认只会保留到TransitionMap,而 PlayerController 默认会保留到 DestinationMap。至于我们自己通过 GetSeamlessTravelActorList 添加的Actor,则可以自定义要传递到 TransitionMap 还是 DestinationMap 。

ENetRole

AActor 中有个 ENetRole Role 变量用来识别角色的 Actor 的身份。ENetRole 是一个枚举类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** The network role of an actor on a local/remote network context */
UENUM()
enum ENetRole
{
/** No role at all. */
ROLE_None,
/** Locally simulated proxy of this actor. */
ROLE_SimulatedProxy, // 这个actor是其他客户端在本机客户端的一个模拟代理
/** Locally autonomous proxy of this actor. */
ROLE_AutonomousProxy, // 这个actor是本机客户端的自己控制的角色
/** Authoritative control over the actor. */
ROLE_Authority, // 这个actor是服务器上的actor
ROLE_MAX,
};

在项目中,为 OverheadWidget 定义如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void UOverheadWidget::ShowPlayerNetRole(APawn* InPawn)
{
// ENetRole RemoteRole = InPawn->GetRemoteRole();
ENetRole RemoteRole = InPawn->GetLocalRole();

FString Role;
switch (RemoteRole)
{
case ENetRole::ROLE_Authority:
Role = FString("Authority");
break;
case ENetRole::ROLE_AutonomousProxy:
Role = FString("Autonomous Proxy");
break;
case ENetRole::ROLE_SimulatedProxy:
Role = FString("Simulated Proxy");
break;
case ENetRole::ROLE_None:
Role = FString("None");
break;
}

// FString RemoteRoleString = FString::Printf(TEXT("Remote Role: %s"), *Role);
FString RemoteRoleString = FString::Printf(TEXT("Local Role: %s"), *Role);
SetDisplayText(RemoteRoleString);
}

启动三个游戏实例,其中一个作为 Listen Server,其余作为 Client,结果如下:

  • Server 端

可以看出,Server 端的全部角色都是 ROLE_Authority 枚举类型。

  • Client 端

相比较而言,两个客户端则只有自己控制的角色是 ROLE_AutonomousProxy 类型而其余角色均为 ROLE_SimulatedProxy 类型。

看起来十分合理,但是更深层次的内容还是得继续学习。