第五次引擎编程尝试

ronald4个月前职场1890

在前面的章节中,我们已经尝试了制作特效、为游戏添加AI并设置AI的UI,到现在这一步我们希望游戏可以多人联网,我们一步步来。

开启多人游戏

image-20220227222126342.png

通过这个我们可以看到角色移动在CS之间同步,但是真正运行的时候发现,距离真正的多人联网游戏还很遥远:


1)射击的时候CS端的弹道不同步


2)NPC的状态CS端不同步


3)游戏状态CS端不同步


... ...


针对上述问题,我们逐步来看:


让发射物在CS之间同步


原先的实现流程如下:

void AFPSTpl2Character::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
        ... ...
        // Bind fire event
        PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AFPSTpl2Character::OnFire);
        ... ...
}

void AFPSTpl2Character::OnFire()
{
     ... ...
     // spawn the projectile at the muzzle
    World->SpawnActor<AFPSTpl2Projectile>(ProjectileClass, SpawnLocation, SpawnRotation, ActorSpawnParams);
    // try and play the sound if specified
    UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation());
    AnimInstance->Montage_Play(FireAnimation, 1.f);
    ... ...
}

在原先的流程中,`OnFire`接口和`IE_Pressed`这个事件绑定,指定事件触发之后本地执行接口`OnFire`,生成`APSTpl2Projectile`对象、播放音效、播放动画,但是在实际多人游戏过程中,子弹的生成应该是S端生成并通知C端,我们修改代码如下:

class AFPSTpl2Character : public ACharacter
{
	... ...
protected:
	/** Fires a projectile. */
	void OnFire();
	UFUNCTION(Server,Reliable,WithValidation)
	void ServerOnFire();
    ... ...
}

AFPSTpl2Projectile::AFPSTpl2Projectile() 
{
	... ...
	SetReplicates(true);
	SetReplicateMovement(true);
    ... ...
}

void AFPSTpl2Character::ServerOnFire_Implementation()
{
	// try and fire a projectile
	if (ProjectileClass != nullptr)
	{
		UWorld* const World = GetWorld();
		if (World != nullptr)
		{
			if (bUsingMotionControllers)
			{
				const FRotator SpawnRotation = VR_MuzzleLocation->GetComponentRotation();
				const FVector SpawnLocation = VR_MuzzleLocation->GetComponentLocation();
				World->SpawnActor<AFPSTpl2Projectile>(ProjectileClass, SpawnLocation, SpawnRotation);
			}
			else
			{
				const FRotator SpawnRotation = GetControlRotation();
				const FVector SpawnLocation = ((FP_MuzzleLocation != nullptr) ? FP_MuzzleLocation->GetComponentLocation() : GetActorLocation()) + SpawnRotation.RotateVector(GunOffset);
				FActorSpawnParameters ActorSpawnParams;
				ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::                                                                         AdjustIfPossibleButDontSpawnIfColliding;
				World->SpawnActor<AFPSTpl2Projectile>(ProjectileClass,SpawnLocation,SpawnRotation,                                                                 ActorSpawnParams);
			}
		}
	}
}

bool AFPSTpl2Character::ServerOnFire_Validate()
{
	return true;
}

在上面的代码主要分为两部分,一个是定义了S端执行的接口,一个是设置字段属性同步:

SeverOnFire

ServerOnFire是服务端执行的接口,这里分为三个部分:UFUNCTION函数标签、ServerOnFire_ImplementationServerOnFire_Validate

UE的RPC机制

所谓RPC是指远程过程调用,即本地调用远端机器执行;从使用上看,调用端只需要关注远端开放的接口,而不需关注底层的网络细节,如:消息的序列化、反序列化、可靠传输等,UE通过UFUNCTION的属性标签提供RPC机制给上层的开发者使用;这里属性标签包括:ServerClientNetMulticast,如:

Server:意味着此接口是S端定义,C端调用

Client:意味着这个接口是C端定义,S端调用

NetMulticast:可以是S端调用,然后S和所有连接上来的C端执行;也可以是C端调用,但仅调用端本地执行


RPC可靠性

默认情况下UE4的RPC是不可靠的,需要保证有序性和送达性是需要reliable关键字指定;指定reliable之后,RPC的调用是保送达,保有序的。


RPC的消息验证

WithValidation是提供了RPC的参数验证,若是设置了,则需要实现FunctionName_WithValidate接口;里面需要对输入的参数进行检查,若是返回false,则断开连接。


_Implementation

ServerOnFire定义的是接口供对端远程调用用,而具体的处理逻辑则需要加上_Implementation后缀


SetReplicates

在前面的逻辑中,我们提供了供C端调用的RPC,通过这种方式可以通知S端创建一个子弹对象,但是这个对象需要将属性同步给所有连接上来的客户端,此时我们就需要调用接口SetReplicates

若是一个AActor调用接口SetReplicates并设置参数为true,则意味着这个Actor会被同步到网络上的客户端,即此Actor若是在服务器被创建,则创建消息也会被同步到客户端;此外若是在服务端状态被修改了,若是此属性是标记需要同步的(即UPROPERTY(Replicated)),则修改也会被同步到客户端上;

可以看到后面设置了SetReplicateMovement,即说明Actor的运动数据是需要同步给客户端的

Property Replication

和属性同步相关的修饰符有以下几个:

NotReplicated:仅适用于结构体的成员变量,用于说明指定字段不进行同步

Replicated:意味着这个属性需要进行网络同步

ReplicatedUsing=FunctionName:指的是,同步此属性之后,若是属性发生了更新,则回调指定的接口FunctionName指定

ReqRetry:仅用于结构体的属性,用于当结构体部分同步成功的时候决定是否进行重传(比如此结构体引用了其他对象,但其他对象还没来得及序列化时);对于简单结构,默认就是重传就可以了,但是对于复杂结构,可能同步数据量大,从节约带宽的角度,这个裁决权交给上层开发者。

每个Actor维护一个含有上述修饰符的属性列表,当属性列表里面的属性发生变化的时候,服务端会将这些属性同步给各个客户端;对于客户端来说,这个属性仅服务端可以修改;客户端修改属性时是不会被同步到服务端或者其他客户端的。

属性同步是可靠的数据传输,并且S端的属性同步是有固定频率的,即若是两次属性同步间隔中,某属性变更了多次,则C端只能获取到最终的属性值,中间的多次变化C端是感受不到的。

可调节的属性同步机制

NetUpdateFrequency:为了能在带宽有限的场景下的游戏体验,UE4提供了NetUpdateFrequency这个变量以便于游戏开发者针对不同对象提供差异化的属性同步服务,这个值越大,则更新频率越高,反之越小。通过为不同Actor设置不同的更新频率,使得游戏内不同敏感度的对象有不同的同步速度,既保证带宽的高效利用,也对CPU的算力进行高效的分配;

MinNetUpdateFrequency:最低的属性更新频率

Update Frequency Decrease Algorithm:属性更新衰减算法,即若一个Actor超过3s属性都没有变化,则会降低属性收集的频率,直到降到最低频率

Update Frequency Increase Algorithm:属性更新强化算法,即在属性值变化较快,在两次更新之间也频繁发生变化,则自动增加属性收集和同步的频率。

条件属性复制

条件属性复制用于对属性复制过程进行更细化的控制;默认情况下,属性复制有一个内置条件,即属性不发生变化就不复制,但为了增加对属性复制的控制,UE4提供了一个宏DOREPLIFETIME_CONDITION,里面的标志位提供了复制属性前一次额外的检查:

  • COND_OwnerOnly:仅发送actor的所有者

  • COND_SkipOwner:除所有者之外的所有连接

  • COND_SimulatedOnly:仅发送给模拟actor

... ...


Connection Ownership

UE4的网络模块是一个比较复杂的模块,展开讲篇幅过大,这里简单介绍一下:

在UE4中,每个连接上来的客户端和服务器有一个connection描述和管理连接,这个connection会被分配一个player controller,对于player controller来说,他管理有一个pawn,可以理解为玩家控制对象,对于游戏里面的物件,我们可以向上回溯,找到其所属的connection。对于游戏中,角色、角色召唤出来的NPC以及角色身上的物品,他们都拥有同一个connection;但是对于游戏中的AI控制的怪物、场景内不属于任何玩家的物件,他们是没有connection的。

对于connection,服务器需要知道这个connection ownership,即连接的所有权,只有明确了这个所有权,才可以:

1)服务端可以确定在哪个客户端执行指定的RPC,以及客户端调用服务端的RPC最后作用于服务器哪个具体的对象;

2)当Actor属性发生更新的时候,服务端可以决定哪些客户端可以获取这个更新的数据,UE有个bOnlyRelevantOwner标识,当此标识设置为true的时候表示仅actor所属的connection能获取到更新的信息,这个既节约了流量也节约了算力,还防止作弊;

3)条件属性复制的时候,选择将属性同步给指定的owner拥有的connection

参考文献

    RPCs | Unreal Engine Documentation

    《Exploring in UE4》关于网络同步的理解与思考 - 知乎 (zhihu.com)

    Property Specifiers | Unreal Engine Documentation

    深入浅出UE4网络 - Leonhard- - 博客园 (cnblogs.com)







相关文章

K8S入门-概念篇(下)

K8S入门-概念篇(下)

一. volume    volume解决的是pod上不同容器之间共享文件的问题和容器文件持久化的问题;K8S提供了以下几类volume:1. hostPath    hostPath是一种通过pod所在node上的文件系统指定的文件或者目录实现文件共享,如下所示,先在Pod内定义一个volume,类型定义为hostP...

Lua的Upvalue和闭包(二)

Lua的Upvalue和闭包(二)

Lua闭包和Upvalue的实现    前面文章介绍了Lua闭包和upvalue的概念,本文简单过一下Lua对于闭包和upvalue的实现以加深理解。Lua闭包结构    Lua在内存的结构如下所示:#define ClosureHeader \ CommonHeader; lu_by...

Lua和C

Lua和C

一.背景    在实际游戏业务运营过程中,经常会出现一些紧急的配置修改、bug修复等;这种规划外的变更行为希望能做到用户无感知,否则对于游戏体验和产品口碑有很大影响,在此背景下,热更新能力成为成熟上线游戏的标准配置。    一般情况下,后台逻辑热更新能力的技术方案有两种:    ...

关于LUA(上)

    Lua是一种轻量小巧的脚本语言,C语言编写,并提供了易于使用的扩展接口和机制,易于嵌入到应用中,在游戏开发中经常被用来进行外层业务系统的开发。Lua的table    Lua的基本数据类型有八种,分别是:nil、boolean、number、string、userdata、function、thread 和 t...

LUA环境搭建

LUA环境搭建

    本文针对Lua新手,介绍Lua开发环境的搭建。环境搭建目标语法高亮,自动补齐语法错误检查方法跳转lua脚本本地运行断点设置及调试功能编辑器选择    主流代码编辑器有vscode、rider、clion。其中下载链接:    Documentation for Visua...

第一次引擎编程尝试

第一次引擎编程尝试

软硬件要求第一次引擎编程尝试给打出的子弹增加爆炸效果    默认创建的FPS游戏子弹击中物体无特殊效果,仅子弹消失,现在我们希望子弹击中物体之后有爆炸的效果,如下所示:粒子系统    粒子系统是计算机图形引擎中模拟一些特定模糊现象的技术,如火、爆炸、烟雾、水流等效果;在项目工程中,美术在引擎里面制作粒子特效,保存下来就...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。