【ue4】【使用】DS服务器搭建
前言
UE4 专用DS服务器(Standalone Dedicated Server)提供了客户端/服务器模型的网络连接。
这里使用 UE4 自带的 Actor复制(Replicated) 和 RPC 实现一个可以在局域网环境下进行联机的功能。
并同步显示一些信息,如__当前在线的玩家数__,__玩家名称列表__等。
实现流程大纲如下
创建登录界面 -- 可以输入用户名, IP, 端口号等登录信息
创建游戏主场景 -- 也是服务器的默认场景
使用 ClientTravel 建立连接
编译DS服务器,并进行连接测试
同步显示当前在线人数 和 玩家名称列表
先看结果,再谈过程
到这一步会实现一个简单的局域网联机功能,并能显示当前在线用户信息。
下一步添加人物血量和伤害计算之后就可以开打了。
原理篇
Actor -- 基本的复制单位
Replication
Replication 指从__服务器__向__客户端__发送数据的__单向__行为,即数据不会从客户端传向服务器。
如果一个 Actor 被设置为 Replicates,那么将会出现两种情况 - 此 Actor 是在服务端被生成的 -- 它会被所有的客户端生成 - 此 Actor 是在客户端被生成的 -- 它只会在其所在客户端生成
如果需要同步一个 Actor 里的 Actor 变量,则需要将后者本身及指向它的指针变量都设置为 Replication才行。
静态生成的 Actor (编辑器阶段) 和 动态生成的 Actor (游戏运行阶段Spawn) 的同步状态是不同的。
对于在 Editor 中放置的 Actor, - 如果它是 Replicates 的, 那么客户端和服务器就都有一个 Actor,而且是同步的 - 如果它不是 Replicates 的, 那么客户端和服务器也都有一个 Actor, 但是不同步的 - 注意 Replicate Movement 不会自动根据 Replicates 调整
对于在游戏运行时 Spawn 出来的 Actor,且是 Replicates 的 - 如果它是在客户端 Spawn 出来的,那么不用说肯定只有此客户端才有 - 如果它是在服务端 Spawn 出来的, 那么它会在同步到所有的客户端 - 如果 Spawn 它的 Actor 也在客户端存在,如 关卡蓝图 等,那么 - 服务端的会 Spawn 出来并同步到所有的客户端 - 客户端的会 Spawn 出来但只出现在本地 - 这样的结果就是服务端只会看到一个 Actor (即它自己生成的), 而客户端会看到两个 Actor (服务端同步过来的 和 它自己生成的) - 常常使用 Role == ROLE_Authority
或者蓝图节点 Switch has authority
来保证只在服务端生成 Actor
Role
Role 表示当前端对 Actor 的控制权,主要有 Authority
, Simulated_Proxy
和 Autonomouse_Proxy
三种
Authority -- 主控
服务端对所有的 Actor 都是 Authority 的, 包括自己控制的 Actor 和 客户端控制的 Actor。
服务端自己控制的 Actor 在其他客户端上是 Simulated 的。
而服务端上其他玩家的 Actor 也是通过 Pawn 提交到服务端进行计算后再次同步给所有客户端。
Autonomouse_Proxy -- 自治代理
客户端自己控制的 Actor 是 Autonomouose_Proxy, 其它的都是 Simulated_Proxy 的。
Autonomouse 相当于一种更平滑的同步方式,它可以首先获得输入,在收到服务端传来的覆盖信息之前就平滑地模拟移动。
Simulated_Proxy -- 模拟代理
Simulated 只存在于客户端, 它无法受到自己的控制,只接受从服务器同步来的数据。
所以它既可能是服务器控制的,也可能是其他客户端控制的。
与 Autonomouse 不同的是, Simulated 更像是一种 强拉 的同步方式,即以最后的状态去移动物体。
但是我们可以通过自己的插值算法来填补空缺。
判断
在 C++ 里,我们可以直接使用 Role == ROLE_xxx
来进行判断一个 Pawn 的 Role 类型。
在 蓝图里, 通过 Has Authority
来确定是否是服务端,如果不是,则通过判断 pawn 是否是 IsLocallyControlled
来确定是否是 Autonomouse。
Property Replication -- 属性复制
即使一个 Actor 被标记为 Replicates 了, 它的某个属性可能也是不能复制的,这个时候我们需要自己设置其属性为 replicated
。
此时如果此属性在服务端发生了变化,那么它就会被同步到其他客户端上去。
比较常见的就是玩家的 health
。
1 | class ADCharacter : ACharacter |
这样还不够,还需要实现 GetLifetimeReplicatedProps()
函数,并执行想要复制的属性的复制生命周期,就像下面代码所示
1 | void ADCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const |
条件属性复制
(待补)
RPC -- 远程过程调用
RPC是在本地调用但在其他机器上远程执行的函数。
分为 Client
, Server
, NetMulticast
三种。
执行 PRC 的条件
- RPC 必须在 Actor 上调用
- AGameMode APlayerController ACharacter ALevelScriptActor 都可以
- 此 Actor 必须是 Replicates 的。
- RPC 无返回值 -- 所以蓝图里只能标记在
CustomEvent
上。
Client -- 服务器调用,客户端执行
只在拥有这个 Actor 的客户端上执行 -- Autonomous 和 Authority 都可以。
比如说,服务器发起开始游戏的指令,就可以通过 Client 类型的 RPC 通知给客户端。
Server -- 客户端调用,服务器执行
客户端拥有对此 Actor 的 Autonomous, 服务器拥有对此 Actor 的 Authority。
比如说,客户端想执行某个动作(如Attack),就需要通过 Server 类型的 PRC 向服务器发起调用申请,因为服务器对其拥有 Authority 权,所以最终会在服务器中执行。
1 | class ADCharacter: public ACharacter |
1 | void ADCharacter::on_attack() |
NetMulticast -- 多播
如果多播RPC是从服务器调用的,则在服务器上以及所有与其连接的客户端上都会执行。
如果多播RPC是从客户端调用的,则只在本地客户端执行。
Server or Client, that's a question
- GameMode 只存在于 Server 上,作为整场游戏的管理员存在
- 所以 GameMode 里面不能进行属性的同步
- GameState 存在于服务器和客户端上,所以可以通过 GameState 同步一些属性,尤其是全局的属性
- PlayerController
- 存在于服务器上的适用于所有客户端。
- 存在于客户端上的只适用于本地客户端。
- 不适合存储所有客户端都需要的数据
- PlayerState 存在于服务器和客户端上
- 适用于所有的客户端
- 可以用来存储所有客户端都需要的属性,如某个玩家的当前分数
- Pawn 存在于服务器和客户端上
- 玩家死亡之后 Pawn 会被销毁,并在重生时重建,所以有些需要一直保存下去的数据需要存在PlayerState 里
多人游戏中的关卡切换
非无缝切换
是一种阻塞(blocking)操作。
当进行非无缝切换时,客户端会与服务器断开连接,然后重新连接,服务器重新加载地图。
在初次加载地图时、客户端初次连接到服务器时、服务器想终止一场游戏时,都一定会产生非无缝转移。
有三个函数可以实现转移 -- UEngine::Browser
UWorld::ServerTravel
APlayerController::ClientTravel
UEngine::Browser
非无缝切换
服务器切换到目标地图
客户端断开连接
DS服务器无法切换至其他服务器 -- 地图必须是本地地图
UWorld::ServerTravel
仅适用于服务器 -- 看名字也知道
服务器为所有客户端执行 APlayerController::ClientTravel
所有客户端会跟随服务器进入新的地图
APlayerController::ClientTravel
如果从服务器调用,则要求特定的客户端转移到新的地图 -- 但没有断开连接
如果从客户端调用,则转换到新的服务器(新的地图)
无缝切换
是一种非阻塞(non-blocking)操作
需要设置过渡关卡 UGameMapsSettings::TransitionMap
需要将 AGameMode::bUseSeamlessTravel
设置为 true
在加载新关卡前,旧地图需要保留的 Actor 转移到过渡关卡
切换流程
标记出要在过渡关卡中存留的 actor(更多信息请见下面)
转移到过渡关卡
标记出要在最终关卡中存留的 actor(更多信息请见下面)
转移到最终关卡
注
简单起见,我们这里客户端使用 ClientTravel 进行切换关卡并连接到服务器。
因为研究这个东西的周期比较短,上面总结的都是一些比较基础的东西,也会有许多不正确的地方,至于需要更加细节化的地方,会放到以后翅膀硬了再作补充。
但是通过对上面这些知识的利用,我们就可以很轻松地实习一个可以通过DS服务器进行联机并同步一些数据的小demo。
搭建过程
欢迎界面
首先做好欢迎界面和登录界面的UI,这里使用 UMG 制作。
这里的欢迎界面是一个单独的关卡, 我们固定 PlayerController
的 PlayerCameraManager
, 然后使用一个 Plane
, 并给其贴图作为我们的背景
1 | void ADMainPlayerController::set_camera_pos() |
菜单使用 UMG 制件
在登录界面输入相关的 用户名(用于后面显示)、IP地址、端口号之后,点击登录,会触发事件 ClientTravel()
从而登录到 DS服务器 上
1 | void ADMainPlayerController::on_login_login_bt() |
我们的这个欢迎场景有独有的 GameMode 和 PlayerController,在进入主游戏场景之后,会有另外游戏时的 GameMode 和 PlayerController
打包测试
如果不管同步的事情的话,我们的客户端就已经可以很友好地登录到服务器,然后互相看到对方了。
我们可以先打包测试一下,然后再讨论同步的问题。
编译源码
听说只有通过UE4源码才能编译出DS服务器,也不知道是不是真的,还好我一直用的都是编译出来的版本。
编译源码的步骤可移步之前编译的时候留下的摘记 =》ue4有意思之编译源码
构建服务器
在打包之前,我们要设置好 Server 的默认地图,不然玩家 ClientTravel()
之后岂不是要掉侧所里去了。
用 VS 打开工程,将 解决方案配置
改为 Development Server
编译,在 工程目录\Binaries\Win64
下得到 xxxServer.exe
这就是我们构建服务器的最终产物,我们把它拷出来,仔细保管为妙。
构造客户端
在打包之前也要设置好默认地图
同样用 VS 打开工程,将 解决方案配置
分别改为 Development
和 Development Editor
编译一次,保险起见
然后在 Editor 里打包即可
测试
我们将之前妥善保管的 xxxServer.exe
放到 打包目录/WindowsNoEditor/DGame/Binaries/Win64
文件夹下
此文件夹下应该有 xxx.exe
即我们的客户端可执行文件。
然后创建 xxxServer.exe
的快捷方式,并右键属性,在其路径后面加上 -log
以使服务器运行的时候可以显示 log 信息
打开 xxxServer.exe
, 会看到日志信息
注意检查有没有正在监听端口
打开多个 xxx.exe
, 最好在同一局域网下的另外的电脑上也打开几个客户端,以便测试
点击登录即可互相看到对方,这是完成下面的同步之后的结果
同步
同步显示在线人数
在这之前,我决定沉思一下,当前的在线人数这个变量,到底是属于哪个层次的。
首先,肯定不是属于某个玩家的,自然不能放在 PlayerController 或者 Character 里面。
如果每个 PlayerController 都存一个副本, 那每次有玩家登录处理起来也忒复杂,不好不好。
其次,肯定也不能放在 GameMode 里面, 因为 GameMode 只在服务端存在,GameMode 里面的变量没法复制。
那么 GameState 呢,我觉得,没毛病,不妨一试。
于是我在 GameState 里声明了这样一个变量表示当前的在线人数
1 | class ADGameState : public AGameState |
接着我们自然要重写 GameMode 的 PostLogin()
函数 和 Logout()
函数,在玩家登录或退出的时候改变这个变量。
1 | void ADGameMode::PostLogin(APlayerController* NewPlayer) |
1 |
|
下面要做的就是把这个变量显示到每个客户端的 ui 上了
我们在 每一个拥有 ROLE_AutonomousProxy
的 PlayerController
里面更新 ui 的显示
1 | if (Role == ROLE_AutonomousProxy && NM_Client == GetNetMode()) { |
其实也可以在每次玩家登录或登出的时候调用每一个 PlayerController 上的 一个 Client 类型 的RPC 函数,这个 RPC 函数向每一个玩家发送改变 ui 值的信息
同步显示玩家列表
有了上面的经验,我们可以同样地来显示玩家的名称列表,只不过有几点需要注意的地方。
首先,在 UE4 封装的模板类型中,只有 TArray<> 是可以复制的,一开始用 TMap<> 被坑的好惨。
其次,用户名信息可以在 ClientTravel()
时通过 url参数直接传递过去。
1 |
|
在 GameMode 中重写 InitNewPlayer()
函数来获得传过来的 url 参数
1 | FString ADGameMode::InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId, const FString& Options, const FString& Portal) |
更多尝试
Steam联机
Steam, Using the Steam SDK During Development
Online Subsystem Steam(4.17版本)
UE4 提供了连接 Steam 平台的插件,当前的版本甚至都集成了 Steam SDK, 不用自己重新下载编译了。
大体流程如下 - 修改 Build.cs
1 | PublicDependencyModuleNames.AddRange(new string[] { |
Target.cs
1 | bUsesSteam = true; |
- 修改
DefaultEngine.ini
1 | [/Script/Engine.GameEngine] |
KBE
KBEngine是一款开源的游戏服务端引擎,使用简单的约定协议就能够使客户端与服务端进行交互。
KEB 的 CBE 模块可支持多种游戏引擎的客户端编程,当然也包括UE4。
通过 CBE 的一些 API 可与 UE4 客户端进行通信。
参考资料
Unreal Engine 4 入门学习 - 网络通信应用场景 -- 小姐姐讲的特别通俗易懂