【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
。
class ADCharacter : ACharacter
{
public:
UPROPERTY( replicated )
float m_health;
}
这样还不够,还需要实现 GetLifetimeReplicatedProps()
函数,并执行想要复制的属性的复制生命周期,就像下面代码所示
void ADCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ADCharacter, m_health);
}
条件属性复制
(待补)
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 权,所以最终会在服务器中执行。
class ADCharacter: public ACharacter
{
public:
void on_attack();
UFUNCTION(reliable, server, WithValidation)
void server_on_attack();
}
// reliable 是为了让 RPC 可靠(本来是不可靠的)
// server 表示 RPC 的类型
// WithValidation 表示需要验证,只有验证为 true 时才进行远程调用,否则断线
void ADCharacter::on_attack()
{
server_on_attack(); // 这会向服务器发起调用请求
}
bool ADCharacter::server_on_attack_Validate()
{
if(...) return false;
return true;
}
void ADCharacter::server_on_attack_Implementation()
{
if (Role == ROLE_Authority) {
...
}
}
// 这里只实现 xxx_Validate() 和 xxx_Implementation() 即可
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
, 并给其贴图作为我们的背景
void ADMainPlayerController::set_camera_pos()
{
PlayerCameraManager->SetActorLocation(FVector(0.f, 0.f, 0.f));
SetViewTarget(PlayerCameraManager);
bShowMouseCursor = true;
}
菜单使用 UMG 制件
在登录界面输入相关的 用户名(用于后面显示)、IP地址、端口号之后,点击登录,会触发事件 ClientTravel()
从而登录到 DS服务器 上
void ADMainPlayerController::on_login_login_bt()
{
if (m_login_ui->m_username_text->GetText().ToString().IsEmpty()) {
UDUtility::debug_out(TEXT("请输入用户名先"));
return;
}
FString url = FString::Printf(TEXT("%s:%s?Alias=%s"),
*(m_login_ui->m_ip_text->GetText().ToString()),
*(m_login_ui->m_port_text->GetText().ToString()),
*(m_login_ui->m_username_text->GetText().ToString()));
UDUtility::log(url);
ClientTravel(*url, TRAVEL_Absolute);
}
我们的这个欢迎场景有独有的 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 里声明了这样一个变量表示当前的在线人数
class ADGameState : public AGameState
{
public:
UPROPERTY(replicated)
int m_player_cnt;
}
void ADGameState::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ADGameState, m_player_cnt);
}
接着我们自然要重写 GameMode 的 PostLogin()
函数 和 Logout()
函数,在玩家登录或退出的时候改变这个变量。
void ADGameMode::PostLogin(APlayerController* NewPlayer)
{
Super::PostLogin(NewPlayer);
if (ADGameState* state = GetGameState<ADGameState>()) {
...
++state->m_player_cnt;
}
}
void ADGameMode::Logout(AController* Exiting)
{
if (ADGameState* state = GetGameState<ADGameState>()) {
--state->m_player_cnt;
}
Super::Logout(Exiting);
}
下面要做的就是把这个变量显示到每个客户端的 ui 上了
我们在 每一个拥有 ROLE_AutonomousProxy
的 PlayerController
里面更新 ui 的显示
if (Role == ROLE_AutonomousProxy && NM_Client == GetNetMode()) {
if (ADGameState* state = Cast<ADGameState>(GetWorld()->GetGameState())) {
m_start_ui->m_hc_text->SetText(FText::FromString(FString::FromInt(state->m_player_cnt)));
}
}
其实也可以在每次玩家登录或登出的时候调用每一个 PlayerController 上的 一个 Client 类型 的RPC 函数,这个 RPC 函数向每一个玩家发送改变 ui 值的信息
同步显示玩家列表
有了上面的经验,我们可以同样地来显示玩家的名称列表,只不过有几点需要注意的地方。
首先,在 UE4 封装的模板类型中,只有 TArray<> 是可以复制的,一开始用 TMap<> 被坑的好惨。
其次,用户名信息可以在 ClientTravel()
时通过 url参数直接传递过去。
void ADMainPlayerController::on_login_login_bt()
{
if (m_login_ui->m_username_text->GetText().ToString().IsEmpty()) {
UDUtility::debug_out(TEXT("请输入用户名先"));
return;
}
FString url = FString::Printf(TEXT("%s:%s?Alias=%s"),
*(m_login_ui->m_ip_text->GetText().ToString()),
*(m_login_ui->m_port_text->GetText().ToString()),
*(m_login_ui->m_username_text->GetText().ToString()));
UDUtility::log(url);
ClientTravel(*url, TRAVEL_Absolute);
}
在 GameMode 中重写 InitNewPlayer()
函数来获得传过来的 url 参数
FString ADGameMode::InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId, const FString& Options, const FString& Portal)
{
FString res = Super::InitNewPlayer(NewPlayerController, UniqueId, Options, Portal);
if (ADPlayerController* pc = Cast<ADPlayerController>(NewPlayerController))
{
FString alias = UGameplayStatics::ParseOption(Options, TEXT("Alias")).TrimStartAndEnd();
if (alias.Len() == 0 || m_players.Find(alias)) { return res; }
pc->m_name = alias; // 获得 controller 的名字
if (ADGameState* state = GetGameState<ADGameState>()) {
state->m_player_names.Add(alias);
//for (auto a : state->m_player_names) { UDUtility::debug_out(a); }
}
FPlayerData tpd; tpd.m_name = alias;
m_players.Add(alias, tpd);
}
return res;
}
更多尝试
Steam联机
Steam, Using the Steam SDK During Development
Online Subsystem Steam(4.17版本)
UE4 提供了连接 Steam 平台的插件,当前的版本甚至都集成了 Steam SDK, 不用自己重新下载编译了。
大体流程如下 - 修改 Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
"OnlineSubsystem",
"OnlineSubsystemUtils"
});
- 修改 Target.cs
bUsesSteam = true;
- 修改
DefaultEngine.ini
[/Script/Engine.GameEngine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="/Script/OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")
[OnlineSubsystem]
DefaultPlatformService=Steam
PollingIntervalInMs=20
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480
GameServerQueryPort=27015
bRelaunchInSteam=false
GameVersion=1.0.0.0
bVACEnabled=1
bAllowP2PPacketRelay=true
P2PConnectionTimeout=90
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="/Script/OnlineSubsystemSteam.SteamNetConnection"
KBE
KBEngine是一款开源的游戏服务端引擎,使用简单的约定协议就能够使客户端与服务端进行交互。
KEB 的 CBE 模块可支持多种游戏引擎的客户端编程,当然也包括UE4。
通过 CBE 的一些 API 可与 UE4 客户端进行通信。
参考资料
Unreal Engine 4 入门学习 - 网络通信应用场景 -- 小姐姐讲的特别通俗易懂
相关文章
- 一步一步来:MQTT服务器搭建、MQTT客户端使用
- 谷歌打开微信定位服务器地址,使用Chrome修改user agent模拟微信内置浏览器
- SVN服务器创建及使用–以文档文件的管理示例
- 使用云服务器与calibre-web构建自己的在线书架(2022年版)
- 监控流媒体服务器的搭建和使用_rtmp推流服务器
- 【云安全最佳实践】使用T-Sec 主机安全普惠版为您的轻量服务器保驾护航!
- 【实用的开源项目】使用服务器部署memos,一款拥有社交功能的、好看的自托管备忘录
- 如何使用Interactsh收集和分析服务器和客户端代码
- springboot整合使用云服务器上的Redis方法
- 器 简单易用:使用Redis服务器管理数据(redis服务)
- 使用MacOS搭建FTP服务器(macosftp)
- 使用vsftpd和MySQL搭建FTP服务器(vsftpdmysql)
- 使用Linux系统搭建FTP服务器的方法(linuxftp软件)
- 使用Linux虚拟机搭建服务器(linux虚拟机做服务器)
- 使用 Docker 快速搭建 Redis 服务器(dockerredis)
- 使用阿里云Linux快速搭建FTP服务器(阿里云linuxftp)
- 器使用MySQL在多台服务器上实现数据共享(mysql多个服务)
- 使用Docker搭建Redis服务器(dockerredis)
- 使用 Docker 企业版搭建自己的私有注册服务器
- 如何使用 bind 设置 DNS 服务器
- 使用Linux搭建VPN服务器:一步步教你如何实现。(linux做vpn服务器)
- 使用Red5搭建Linux流媒体服务器(red5linux)
- lab深入浅出:使用 Linux 搭建 GitLab 服务器(Linux搭建git)
- Linux下使用克隆工具快速搭建服务器(linux克隆工具)
- 搭建Redis服务器,轻松使用MAMP(redismamp)
- 使用YUM包管理你的MySQL服务器(mysql yum包)
- 使用命令行连接Redis服务器(使用命令连接redis)
- 如何有效配置Redis服务器(关于redis的配置)
- 使用Redis集群搭建两台服务器(redis集群两台服务器)