【UE-DS】初识DS
初识DS
本篇作为DS入门文档,讲述了DS的构建过程,通过登录、射击游戏的例子对DS开发有一个初步的了解。
Demo链接:https://gitee.com/lixiang2202/uedemo_ds
文档理论性偏弱,操作性较强,建议跟随文档进行操作,收获更多。
Dedicated Server – 专用服务器
来自官方文档的介绍:在虚幻引擎中设置专用服务器 | 虚幻引擎 5.5 文档 | Epic Developer Community

在UE5中,客户端代码和服务器代码是一体的,一般通过 AActor中所维护的 TEnumAsByte<enum ENetRole> Role来区分代码在哪里运行。
1 | |
Actor提供了 AActor::HasAuthority()方法来判断是否在服务器上。
1 | |
启用专用服务器
使用源码引擎
DS需要使用 Development Server编译模式,这个模式只有使用源码编译的UE5所创建的工程才有这个模式。
编译教程可以在网上搜,建议clone时使用ssh协议(git@github.com:EpicGames/UnrealEngine.git),比https协议更加稳定。

如仅需要测试DS,可以不使用源码编译的二进制引擎,可以跳转到文档最后射击小游戏的测试章节。
不过还是建议使用源码引擎体验一次发布流程。
创建项目
使用源码编译的引擎创建项目。本系列Demo采用第三人称模板C++项目(可以勾选StarterContent,后文例子中使用到了这里的特效资源)。
大多数模板中的Pawn和角色默认启用了复制。第三人称模板已拥有会自动复制移动的角色移动组件 。

构建项目
创建完成后,在Source目录下,新建 xxxServer.Target.cs和 xxxClient.Target.cs
复制自动创建的
xxxEditor.Target.cs,然后将脚本内容中的Editor字符串替换为Server或Client。
Target.cs为UBT提供了对应构建目标类型的各种设置,可详见:虚幻引擎构建工具目标参考 | 虚幻引擎 5.5 文档 | Epic Developer Community

创建完成后,在IDE中(文档中使用的是rider)选择构建类型为Development Server,然后进行构建。构建完成后,选择类型为Development Client,进行构建。构建这两个目标耗时较久。

可以看到,在./UEDemo_DS/Binaries/Win64下生成了 UEDemo_DSServer.exe和 UEDemo_DSClient.exe。但此时还无法运行。
设置项目
构建完成后,选择构建模式为Development Editor,编译启动该项目编辑器。
因为项目创建时,选择了第三人称模板,所以在Content中可以看到一些资产。编辑器默认选择的level为 ThirdPersonMap,点击模拟播放后,可以运行第三人称模板的内容。
新建一个空level:TransitionMap,作为切换地图时的过渡level。
在 Project Settings -> Project -> Maps & Modes中,设置 Transition Map和 Server Default Map,如下图所示:
COOK资源
无论是client程序还是server程序,都需要cook资源才能够正确运行。在 Platforms中 BUILD TARGET设置为 UEDemo_DSServer ,然后点击 Cook Content。成功后同理设置 BUILD TARGET为 UEDemo_DSClient。

可以在 UEDemo_DS\Saved\Cooked目录下看到生成了Server和Client的cooked目录,两者的大小是不一样的,Server端cooked要小于Client端cooked。

运行专用服务器
此时可以运行专用服务器了。如果直接双击运行,会在后台运行Server,为了调试方便,建议为UEDemo_DSServer新建快捷方式,在快捷方式的目标栏中,添加-log参数(注意参数前要有空格)

双击快捷方式,可以看到打开一个控制台,这个就是DS服务器了。
打开UE客户端(编辑器模拟启动或者双击打开UEDemo_DSClient.exe),按下键盘’~’键,输入 open 127.0.0.1,登录到DS服务器上。

观察DS控制台,可以看到服务器接受了一个客户端的连接,并且使用的是服务器的地图。(注:可以复制一份ThirdPersonMap做以下修改,并在项目配置中的Server Default Map中修改服务器地图,观察登录DS服务器后客户端加载的地图变为了服务器地图)

此时再次启动一个客户端,登录DS服务器,可以在客户端上看到两个角色。下图分别使用了编辑器模拟和UEDemo_DSClient.exe打开了两个客户端,并通过open命令登录至服务器中。操作一个客户端的角色,在另一个客户端可以看到同步效果。
在
项目设置->引擎->一般设置->帧率,设置平滑帧率,限制最小帧率为30,可以解决失焦的客户端卡顿问题

简单应用
登录服务器
可以使用 APlayerController::ClientTravel接口登录服务器。新建 UUEDemo_DSGameInstance,继承自 UGameInstance,并在项目设置中指定。

新增连接DS服务器的方法,供蓝图使用
1 | |
打开编辑器,新建一个登录界面 UMG_Login,布局如下:

在蓝图中实现点击按钮调用 UUEDemo_DSGameInstance::ConnectDedicatedServer

新建一个Level文件 ClientLevel,设置为客户端默认关卡:

在关卡蓝图中创建界面,并设置鼠标:

重新cook客户端和服务器资源后,启动服务器,客户端,可以看到登录界面,点击登录界面后登录到DS服务器中,客户端此时使用的是服务器的地图。

射击小游戏
使用UE的同步架构,实现一个简单的射击游戏,AB两个角色可以互相发射子弹,被击中后会掉血。这里血量就简单用屏幕日志来输出。
这个小游戏参考了官方文档(虚幻引擎多人游戏编程快速入门指南 | 虚幻引擎 5.5 文档 | Epic Developer Community),借此来简单熟悉UE的网络开发。
血量属性维护
在 AUEDemo_DSCharacter中新增血量属性,并在构造方法中进行初始化。
关于Actor的属性复制可详见:在虚幻引擎中复制Actor属性 本篇文档仅使用部分复制方式
1 | |
重写 GetLifetimeReplicatedProps,注册配置 CurrentHealth的复制方式。
1 | |
声明并实现方法 void OnHealthUpdate(),响应要更新的生命值。修改属性后,在服务器立即调用,在客户端响应 OnRep_CurrentHealth方法,并在其中调用。
1 | |
LOG_XXX方法实际是用宏封装了
GEngine->AddOnScreenDebugMessage()和UE_LOG
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#pragma once
DECLARE_LOG_CATEGORY_EXTERN(LogUEDemoDS, Log, All);
DEFINE_LOG_CATEGORY(LogUEDemoDS);
#define LOG_PRINT(Color, Format, ...) \
{ \
if (GEngine){ GEngine->AddOnScreenDebugMessage(-1, 5.f, Color, FString::Printf(TEXT(Format), ##__VA_ARGS__));} \
UE_LOG(LogUEDemoDS, Error, TEXT(Format), ##__VA_ARGS__); \
}
#define LOG_RED(Format, ...) LOG_PRINT(FColor::Red, Format, ##__VA_ARGS__)
#define LOG_GREEN(Format, ...) LOG_PRINT(FColor::Green, Format, ##__VA_ARGS__)
#define LOG_BLUE(Format, ...) LOG_PRINT(FColor::Blue, Format, ##__VA_ARGS__)
#define LOG_YELLOW(Format, ...) LOG_PRINT(FColor::Yellow, Format, ##__VA_ARGS__)
在 OnRep_CurrentHealth()中调用 OnHealthUpdate()方法
1 | |
实现 SetCurrentHealth和 TakeDamage方法,用于响应伤害。SetCurrentHealth()提供了一个可控的办法从外部修改玩家 CurrentHealth,通过判断 HasAuthority()来限定仅在服务器上执行此函数,同时调用了 OnHealUpdate()以确保客户端和服务器对其都有调用。
而 TakeDamage()方法重写自 APawn,作为外部Actor对其造成伤害后的逻辑触发。
1 | |
测试属性同步
临时在 Jump方法中模拟造成伤害,主动调用 TakeDamage(),对属性同步进行测试。
1 | |
重新编译 Server和 Client,启动2个客户端并登录至服务器,一个客户端角色跳跃,观察另外客户端日志和服务器日志,可以看到属性同步的结果。

如图所示,1号客户端、2号客户端登录在同一个服务器上,1号客户端跳跃:
- 服务器执行方法
SetCurrentHealth响应处理了伤害,并同步给1、2号客户端 - 1号客户端响应
OnRep_CurrentHealth并执行OnHealthUpdate方法。因为是本地客户端IsLocallyControlled,所以输出了蓝色日志和绿色日志 - 2号客户端响应
OnRep_CurrentHealth并执行OnHealthUpdate方法。仅输出了通用的绿色日志
使用复制创建抛射物
接下来,为角色创建一个发射抛射物的功能。
首先新建一个抛射物Actor:
AUEDemo_DSProjectile
在构造方法中设置bReplicates = true,表示这个Actor需要被复制。
在头文件中做如下定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 用于测试碰撞的球体组件。
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
class USphereComponent* SphereComponent;
// 用于提供对象视觉呈现效果的静态网格体。
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
class UStaticMeshComponent* StaticMesh;
// 用于处理投射物移动的移动组件。
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
class UProjectileMovementComponent* ProjectileMovementComponent;
// 在投射物撞击其他对象并爆炸时使用的粒子。
UPROPERTY(EditAnywhere, Category = "Effects")
class UParticleSystem* ExplosionEffect;
//此投射物将造成的伤害类型和伤害。
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Damage")
TSubclassOf<class UDamageType> DamageType;
//此投射物造成的伤害。
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Damage")
float Damage;在构造方法中做如下初始化
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
27
28
29
30
31
32
33
34
35// 定义根节点及碰撞信息
SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
SphereComponent->InitSphereRadius(37.5f);
SphereComponent->SetCollisionProfileName(TEXT("BlockAllDynamic"));
RootComponent = SphereComponent;
if (this->HasAuthority())
{ // 仅在服务器端添加碰撞回调
SphereComponent->OnComponentHit.AddDynamic(this, &AUEDemo_DSProjectile::OnProjectileImpact);
}
// 定义静态网格体
static ConstructorHelpers::FObjectFinder<UStaticMesh> DefaultMesh(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
StaticMesh->SetupAttachment(RootComponent);
if (DefaultMesh.Succeeded())
{
StaticMesh->SetStaticMesh(DefaultMesh.Object);
StaticMesh->SetRelativeLocation(FVector(0.f, 0.f, -37.5f));
StaticMesh->SetRelativeScale3D(FVector(0.75f, 0.75f, 0.75f));
}
// 定义特效
static ConstructorHelpers::FObjectFinder<UParticleSystem> DefaultExplosionEffect(TEXT("/Game/StarterContent/Particles/P_Explosion.P_Explosion"));
if (DefaultExplosionEffect.Succeeded())
{
ExplosionEffect = DefaultExplosionEffect.Object;
}
// 定义移动组件
ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
ProjectileMovementComponent->SetUpdatedComponent(SphereComponent);
ProjectileMovementComponent->InitialSpeed = 1500.f;
ProjectileMovementComponent->MaxSpeed = 1500.f;
ProjectileMovementComponent->bRotationFollowsVelocity = true;
ProjectileMovementComponent->ProjectileGravityScale = 0.f;
// 定义伤害类型和伤害
DamageType = UDamageType::StaticClass();
Damage = 10.f; // 伤害数值伤害反馈
我们希望抛射物在命中时立即销毁并播放一个爆炸特效。重写Destoryed方法,该方法在Actor销毁时调用。1
2
3
4
5
6
7
8virtual void Destroyed() override;
void AUEDemo_DSProjectile::Destroyed()
{
Super::Destroyed();
// 销毁时播放爆炸特效
FVector spawnLocation = GetActorLocation();
UGameplayStatics::SpawnEmitterAtLocation(this, ExplosionEffect, spawnLocation, FRotator::ZeroRotator, true, EPSCPoolMethod::AutoRelease);
}碰撞回调
声明并实现碰撞回调方法,并绑定碰撞回调,注意仅在服务器端绑定。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 在头文件中做如下定义
UFUNCTION(Category=Projectile)
void OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit);
void AUEDemo_DSProjectile::OnProjectileImpact(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit)
{
// 碰撞回调
if (OtherActor)
{
// 应用伤害,最终会调用到TakeDamage函数
UGameplayStatics::ApplyPointDamage(OtherActor, Damage, NormalImpulse, Hit, GetInstigator()->Controller, this, DamageType);
}
// 销毁自身
Destroy();
}
// 在构造方法中为SphereComponent绑定碰撞回调
if (this->HasAuthority())
{ // 仅在服务器端添加碰撞回调
SphereComponent->OnComponentHit.AddDynamic(this, &AUEDemo_DSProjectile::OnProjectileImpact);
}创建抛射物
在角色类AUEDemo_DSCharacter中创建抛射物对象。
每一次设计,都会通过RPC调用到服务器的创建抛射物接口,为防止消息过于频繁,做了每个抛射物生成的延时机制,下面是关键代码:
在头文件中做如下声明:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25UPROPERTY(EditDefaultsOnly, Category="Gameplay|Projectile")
TSubclassOf<class AUEDemo_DSProjectile> ProjectileClass;
/** 射击之间的延迟,单位为秒。用于控制测试发射物的射击速度,还可防止服务器函数的溢出导致将SpawnProjectile直接绑定至输入。*/
UPROPERTY(EditDefaultsOnly, Category="Gameplay")
float FireRate;
/** 若为true,则正在发射投射物。*/
bool bIsFiringWeapon;
/** 用于启动武器射击的函数。*/
UFUNCTION(BlueprintCallable, Category="Gameplay")
void StartFire();
/** 用于结束武器射击的函数。一旦调用这段代码,玩家可再次使用StartFire。*/
UFUNCTION(BlueprintCallable, Category = "Gameplay")
void StopFire();
/** 用于生成投射物的服务器函数。注意该方法声明为服务器RPC,需要实现名为HandleFire_Implementation的方法,客户端调用该方法实际是在服务器端运行。
本篇不对RPC做深入研究,只做了解。*/
UFUNCTION(Server, Reliable)
void HandleFire();
/** 定时器句柄,用于提供生成间隔时间内的射速延迟。*/
FTimerHandle FiringTimer;在cpp中实现:
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
27void AUEDemo_DSCharacter::StartFire()
{ // 按键绑定鼠标左键,这里忽略了绑定过程,可查看demo了解完整过程
if (!bIsFiringWeapon) // 用该标记做防重入检测
{
bIsFiringWeapon = true;
GetWorld()->GetTimerManager().SetTimer(FiringTimer, this, &AUEDemo_DSCharacter::StopFire, FireRate, true);
HandleFire();
}
}
void AUEDemo_DSCharacter::StopFire()
{
bIsFiringWeapon = false;
}
void AUEDemo_DSCharacter::HandleFire_Implementation()
{
// 生成位置、旋转
FVector spawnLocation = GetActorLocation() + ( GetActorRotation() Vector() * 100.0f ) + (GetActorUpVector() * 50.0f);
FRotator spawnRotation = GetActorRotation();
// 生成参数
FActorSpawnParameters spawnParameters;
spawnParameters.Instigator = GetInstigator();
spawnParameters.Owner = this;
// 调用SpawnActor生成投射物
AUEDemo_DSProjectile* spawnedProjectile = GetWorld( ->SpawnActor<AUEDemo_DSProjectile>(spawnLocation, spawnRotation, spawnParameters);
}
测试
编译完成后,启动服务器和两个客户端,在客户端中,按下鼠标左键,可以看到生成一个抛射物并向前运动,直到发生了碰撞,销毁自身并播放了一个爆炸特效。
当碰撞到另外一个角色时,执行了 UGameplayStatics::ApplyPointDamage(),并传递至对方角色的 AUEDemo_DSCharacter::TakeDamage()方法,造成伤害后打印了伤害日志。
更加方便的启动测试方式(注:此种方式无需使用源码引擎,但默认会自动连接DS服务器,所以启动后无法看到登录界面):
在模拟选项中,选择玩家数量为 2,网络模式为 客户端模式。

打开高级设置,或 编辑器偏好设置->多玩家选项中做如下设置:
注意要设置服务器地图命名重载为
ThirdPersonMap,启动参数为-server -log。

设置完毕后,点击模拟播放:

总结
通过本篇文档,可以对DS做一个初步的入门了解(建议参照文档实操一下),因为只是入门文档,所以没有做深入的技术研究。
后续将会以网络开发为主题,对Actor复制和RPC、GamePlay、UE网络模块、DS优化、完整的服务器拓扑结构等进行较为深入的学习。
参考资料
ue4-Network相关-Client和Server的区分_ue4 client server-CSDN博客
