第五次引擎编程尝试

ronald1年前职场5720

在前面的章节中,我们已经尝试了制作特效、为游戏添加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)







相关文章

LUA数据结构(二)

LUA数据结构(二)

Lua数据结构thread    Lua中,最主要的线程是协程,它和线程差不多,拥有独立的栈、局部变量和指令指针;和线程区别在于线程可以同时运行多个,而协程同一时刻只能有一个运行    Lua协程接口都放在table coroutine里面,主要包括以下几个:coroutine.create,创建一个协程corouti...

LUA环境搭建

LUA环境搭建

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

几种Lua和C交叉编程的程序写法

Lua程序调用C接口//另一个待Lua调用的C注册函数。 static int sub2(lua_State* L) {     double op1 = luaL_checknumber(L,1);     double op2 ...

协程-有栈协程(coroutine)

协程-有栈协程(coroutine)

概述    后台架构的微服务化,原先的单体应用被按照功能模块切分为若干进程组承担,此种架构演化带来的收益诸如:单进程复杂度降低,代码维护成本降低发布影响范围缩小,发布灵活性提升计算资源更精准的分配... ...    但是这种架构带来的另外的变化就是,原先由单进程承载的事务,可能涉及几个甚至十几个进程;在这种情况下,采...

后台开发人员面试资源汇总(C&C++方向)

后台开发人员面试资源汇总(C&C++方向)

    汇总一下C/C++后台开发方向的学习资料,为避免广告之嫌,只罗列书名不贴链接了1.编程语言类《C++ Primer》《C 专家编程》《深度探索C++对象模型》《Effective C++:改善程序与设计的55个具体做法》《STL源码剖析》2.操作系统类《鸟哥的Linux私房菜 基础学习篇》《深入理解LINUX内核(第3版)》3.软件工程类《大话设计模...

K8S实战技巧(一)

一. 概述        K8S是谷歌开源的一个容器编排管理工具,可以帮助业务实现自动化部署、故障发现、容灾、扩缩容、流量管理等,大大提升业务的服务能力;k8s通过yaml描述文件实现容器的部署,本文介绍一些实际应用过程中可能用到一些k8s的特性及配置方法,适用于对k8s有一定了解的读者。二. 共享进程命名空间&nbs...

发表评论    

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